AvalonEdit: Getting Visual Position for IBackgroundRenderer - c#

In my AvalonEdit-based document editor, I am trying to add marker lines to the text view to indicate folds in the document, in a similar way to how Visual Studio joins the start and end of code block braces with dotted lines.
I have something that produces the correct result if the document is scrolled to the very top, but it doesn't update correctly if the document is scrolled down. Specifically, the lines are drawn as if the text view wasn't scrolled at all, and was still at the top of the document. I suspect the problem has something to do with the TextViewPosition and GetVisualPosition lines, but I don't understand how to correctly get the adjusted visual position with scrolling.
(To be clear, I have checked, and the Draw method is being called at the appropriate times to update the background, it's just that scrolling isn't accounted for when it does)
What I have so far, is the following, on a class which implements IBackgroundRenderer:
public void Draw(TextView textView, DrawingContext drawingContext) {
if (textView == null) { throw new ArgumentNullException("textView"); }
if (drawingContext == null) { throw new ArgumentNullException("drawingContext"); }
if (!textView.VisualLinesValid) { return; }
ReadOnlyCollection<VisualLine> visualLines = textView.VisualLines;
if (visualLines.Count == 0) { return; }
foreach (FoldingSection fold in foldingManager.AllFoldings.Where(f => !f.IsFolded)) {
DocumentLine startLine = textView.Document.GetLineByOffset(fold.StartOffset);
ISegment whitespace = TextUtilities.GetLeadingWhitespace(textView.Document, startLine);
if(whitespace.Length == 0) { continue; }
DocumentLine endLine = textView.Document.GetLineByOffset(fold.EndOffset);
TextLocation foldStart = textView.Document.GetLocation(whitespace.EndOffset);
TextLocation foldEnd = textView.Document.GetLocation(textView.Document.GetOffset(endLine.LineNumber, foldStart.Column));
// I am unsure exactly what TextViewPosition is meant to represent, in contrast to TextLocation
TextViewPosition startViewPos = new TextViewPosition(foldStart);
TextViewPosition endViewPos = new TextViewPosition(foldEnd);
// These lines are definitely not returning what I expect
Point foldStartPos = textView.GetVisualPosition(startViewPos, VisualYPosition.LineBottom);
Point foldEndPos = textView.GetVisualPosition(endViewPos, VisualYPosition.LineBottom);
Brush brush = new SolidColorBrush(LineColor);
brush.Freeze();
Pen dashPen = new Pen(brush, 0.5) { DashStyle = new DashStyle(new double[] { 2, 2 }, 0) };
dashPen.Freeze();
// New point created to avoid issues with nested folds causing slanted lines
drawingContext.DrawLine(dashPen, foldStartPos, new Point(foldStartPos.X, foldEndPos.Y));
}
}
The folding for the document is based on whitespace (very similar to Python-style indentation), hence the use of the leading whitespace for finding the column.
In short, how does one get the properly adjusted visual position from the document line number and column?

GetVisualPosition is documented as:
Returns: The position in WPF device-independent pixels relative to the top left corner of the document.
To use it for painting, you'll want to subtract the scroll position from it:
Point foldStartPos = textView.GetVisualPosition(startViewPos, VisualYPosition.LineBottom);
Point foldEndPos = textView.GetVisualPosition(endViewPos, VisualYPosition.LineBottom);
foldStartPos -= textView.ScrollOffset;
foldEndPos -= textView.ScrollOffset;
As for TextLocation vs. TextViewPosition: there are some cases where there are multiple possible locations that map to the same TextLocation.
There might be custom VisualLineElements with a documentLength of 0.
Or maybe word-wrap is enabled and a line is wrapped at a position where there is no space character: then both the end of the first TextLine and the beginning of the second TextLine refer to the same position in the document.
A TextViewPosition carries some extra information that allows distinguishing these cases. This is mostly important for the caret, so that clicking somewhere places the caret in the clicked position; not another equivalent position.

Related

Find position of left side of object

So i want to align 2 objects together on the left side of the object that is the furthest to the left. So i'll sketch out a scenario:
There are 2 images on random positions on the board. You select both images(with a selection tool that has been made) and you than click: "Align objects to the left"
The image that is the furthest to the RIGHT should than snap to the same position of the edge on the left side of the other image. So when clicking the button, my code should calculate both left sides(the edge of the image on the left) of the images position, than check which one if the furthest to the right on the canvas, and move that one to the same X axis as the other image.
This way the end result will be the images will be on the exact same X axis. So if image 1 is on -73 & Image 2 is on -50, image 2 should than also move to -73, regardless of the rotation of either images.
Currently i can only find out how to find the middle position of the image, my code looks like this atm:
using com.company.program.core.pageObjects;
using com.company.program.ui.colorPicker;
using UnityEngine;
namespace com.company.program.core.SelectionManager
{
public static class SelectionAlignment
{
public static void AlignLeft(PageObjectBase pageObject)
{
Debug.Log("Let's check if this is a group first!");
if (pageObject is PageObjectGroup)
Debug.Log("Now we can AlignLeft!");
PageObjectGroup group = (PageObjectGroup)pageObject;
foreach (PageObjectBase objectBase in group.Children)
{
//objectBase.transform.position
Debug.Log("Position is now" + objectBase.transform.position);
Debug.Log("Left Position is" + objectBase.transform.position + -objectBase.transform.right);
}
}
}
}
}
Note: I have no moving function yet as im first trying to figure out what the position of the most left side of the image is. The first Debug.log works and displays the normal position(middle point of image). The second one doesn't work, and displays the same. Both images get instantiated during runtime.
Hopefully this is enough information, i'm a long time lurker but have never posted anything myself, so be gentle on me if i forgot to add information.
From your question left means a smaller X value.
So in general the left edge of an image (assuming PageObjectBase somehow inherits from MonoBehaviour and you are speaking about Image component from Unity UI with a RectTransform)
is the most left of all four corners of the image. You can get all four corners by using GetWorldCorners
private float GetLeftEdge(PageObjectBase obj)
{
RectTransform rectTransformComponent = obj.gameObject.GetComponent<RectTransform>();
if(!rectTransformComponent)
{
Debug.LogError("No Image component found", this);
return 0;
}
Vector3[] v = new Vector3[4];
rectTransformComponent.GetWorldCorners(v);
float mostLeftCorner = float.MaxValue;
foreach(var pos in v)
{
mostLeftCorner = Mathf.Min(mostLeftCorner, pos.x);
}
return mostLeftCorner;
}
This should also work if the images are rotated.
If you are not using RectTransform you have to get the width another way somehow but the rest stays the same.
Than in your loop you first have to get the smallest (most left) edge so I would split it in two loops:
// Start with the max float value so any other value should be smaller
float mostLeftEdge = float.MaxValue;
PageObjectBase referenceToMostLeftObject;
// Get the most left position and object reference
foreach (PageObjectBase objectBase in group.Children)
{
float leftEdge = GetLeftEdge(objectBase);
if(leftEdge < mostLeftEdge)
{
referenceToMostLeftObject = objectBase;
mostLeftEdge = leftEdge;
}
}
// Now you have the most left edge value and the object which is your reference
// Just a little safety skip to not move to strange values
if(referenceToMostLeftObject == null || Mathf.Approximately(mostLeftEdge, float.MaxValue))
{
Debug.LogError("Ups, I think something went wrong getting the mostLeftEdge", this);
return;
}
// Move the other objects to match with that edge
foreach (PageObjectBase objectBase in group.Children)
{
// Skip the reference object
if(objectBase == referenceToMostLeftObject) continue;
// First get the difference
float leftEdge = GetLeftEdge(objectBase);
// Should always be negative
float difference = mostLeftEdge - leftEdge;
// Than move it there
var current = objectBase.transform.position;
// Since difference should be negative
// Adding it to the current position should result in the wanted position
var newPosition = new Vector3(current.x + difference, current.y, current.z);
objectBase.transform.position = newPosition;
}

Combining two existing annotation elements (PDFTron Net SDK)

I am trying to create a custom annotation element combining an arrow and a freetext box element. The Arrow element i would like it to have its default behavior as originally assigned in the SDK, the freetext box though I would like to have a default size, predefined text in it and follow the arrow tail as soon as the arrow is moved or resized (selection, scaling transformation of the textbox I would like to disable it). Currently I have managed to draw an arrow with a free text element appearing on it's tail, but I am not aware of how I could make that free text element follow the arrow tail when its position changes and disable all its functionality (selection, scaling transformation, text input, etc...). Is there a way to group two existing annotation elements into one, or is there another easier approach of creating an arrow with textbox in its' tail including predefined text? Thank you in advance.
The code below shows how to add text to the default appearance that PDFNet will generate. Essentially you are decorating the default appearance.
The best thing to do is use our default appearance, and then overlay with your own content.
After calling Annot.RefreshAppearance, you would call something like the following.
static public void AddDecorations(Annots.Line line, PDFDoc doc)
{
ElementReader reader = new ElementReader();
ElementWriter writer = new ElementWriter();
ElementBuilder builder = new ElementBuilder();
writer.Begin(doc); // start new content stream
SDF.Obj old_app_stm = line.GetAppearance();
reader.Begin(old_app_stm);
Element element;
// isolate PDFNet default appearance in group
writer.WriteElement(builder.CreateGroupBegin());
while ((element = reader.Next()) != null)
{
writer.WriteElement(element);
}
element = builder.CreateGroupEnd();
writer.WriteElement(element);
/////////////////////////////////////////////////////
// Create matrix to position and rotate new text
Point start_pt = line.GetStartPoint();
Point end_pt = line.GetEndPoint();
double xDiff = end_pt.x - start_pt.x;
double yDiff = end_pt.y - start_pt.y;
double angle = Math.Atan2(yDiff, xDiff);
Matrix2D mtx = Matrix2D.RotationMatrix(-angle);
mtx.m_h = start_pt.x;
mtx.m_v = start_pt.y;
/////////////////////////////////////////////////////
element = builder.CreateTextBegin(Font.Create(doc, Font.StandardType1Font.e_helvetica_bold), 8);
writer.WriteElement(element);
element = builder.CreateTextRun(String.Format("{0}", line.GetSDFObj().GetObjNum()));
element.SetTextMatrix(mtx);
writer.WriteElement(element);
Rect new_bbox = new Rect();
element.GetBBox(new_bbox);
element = builder.CreateTextEnd();
writer.WriteElement(element);
// update bounding boxes
Rect old_bbox = new Rect(old_app_stm.FindObj("BBox"));
old_bbox.Normalize(); // make sure x1,y1 is bottom left
new_bbox.Normalize();
new_bbox = new Rect(Math.Min(new_bbox.x1, old_bbox.x1), Math.Min(new_bbox.y1, old_bbox.y1), Math.Max(new_bbox.x2, old_bbox.x2), Math.Max(new_bbox.y2, old_bbox.y2));
SDF.Obj new_app_stm = writer.End();
new_app_stm.PutRect("BBox", new_bbox.x1, new_bbox.y1, new_bbox.x2, new_bbox.y2);
line.SetRect(new_bbox);
line.SetAppearance(new_app_stm);
}
In summary, you wrap the pre-existing drawing elements in a group, and then write your own new content. Then update the bounding box rectangle, and update the appearance stream and annotation bounding boxes.

WPF Path's Geometry Transform performance gets worse with LineGeometries' length increase

I'm currently trying to create a little plot interactive editor, using WPF.
On maximized window the plot dragging with mouse is not responsive enough because of the plot grid.
I got a path for my plot grid lying inside a Canvas control (render transform just shifts it to the bottom of the canvas)
<Path Name="VisualGrid" RenderTransform="{StaticResource PlotTechnicalAdjust}" Style="{DynamicResource ResourceKey=GridStyle}" Panel.ZIndex="1"/>
Here is how grid is created; _curState has actual camera "viewport" metadata
if (_curState.Changes.ScaleStepXChanged)
{
foreach (TextBlock item in _xLabels)
{
DeleteLabel(item);
}
_xLabels.Clear();
double i = _curState.LeftEdgeLine;
_gridGeom.Children[(int)GridGeomIndexes.VerticalLines] = new GeometryGroup { Transform = _verticalLinesShift};
var verticalLines =(GeometryGroup)_gridGeom.Children[(int)GridGeomIndexes.VerticalLines];
while (i <= _curState.RightEdgeLine * (1.001))
{
verticalLines.Children.Add(new LineGeometry(new Point(i * _plotParameters.PixelsPerOneX, 0),
new Point(i * _plotParameters.PixelsPerOneX,
-_wnd.ContainerGeneral.Height)));
_xLabels.Add(CreateLabel(i, Axis.X));
i += _curState.CurrentScaleStepX;
}
_curState.Changes.ScaleStepXChanged = false;
}
if (_curState.Changes.ScaleStepYChanged)
{
foreach (TextBlock item in _yLabels)
{
DeleteLabel(item);
}
_yLabels.Clear();
double i = _curState.BottomEdgeLine;
_gridGeom.Children[(int)GridGeomIndexes.HorizontalLines] = new GeometryGroup { Transform = _horizontalLinesShift};
var horizontalLines = (GeometryGroup)_gridGeom.Children[(int)GridGeomIndexes.HorizontalLines];
while (i <= _curState.TopEdgeLine * (1.001))
{
horizontalLines.Children.Add(new LineGeometry(new Point(0, -i * _plotParameters.PixelsPerOneY),
new Point(_wnd.ContainerGeneral.Width,
-i * _plotParameters.PixelsPerOneY)));
_yLabels.Add(CreateLabel(i, Axis.Y));
i += _curState.CurrentScaleStepY;
}
_curState.Changes.ScaleStepYChanged = false;
}
Where Transforms are composition of TranslateTransform and ScaleTransform (for vertical lines I only use X components and only Y for horizontal lines).
After beeing created those GeometryGroups are only edited if a new line apears into camera or an existing line exits viewable space. Grid is only recreated when axis graduations have to be changed after zooming.
I have a dragging option implemented like this:
private Point _cursorOldPos = new Point();
private void OnDragPlotMouseMove(object sender, MouseEventArgs e)
{
if (e.Handled)
return;
Point cursorNewPos = e.GetPosition(ContainerGeneral);
_plotView.TranslateShiftX.X += cursorNewPos.X - _cursorOldPos.X;
_plotView.TranslateShiftY.Y += cursorNewPos.Y - _cursorOldPos.Y;
_cursorOldPos = cursorNewPos;
e.Handled = true;
}
This works perfectly smooth with a small window (1200x400 units) for a large amount of points (like 100+).
But for a large window (fullscreen 1920x1080) it happens pretty jittery even without any data-point controls on canvas.
The strange moment is that lags don't appear when I order my GridGenerator to keep around 100+ lines for small window and drag performance suffers when I got less than 50 lines on maximezed. It makes me think that it might somehow depend not on a number of elements inside a geometry, but on their linear size.
I suppose I should mention that OnSizeChanged I adjust the ContainerGeneral canvas' height and width and simply re-create the grid.
Checked the number of lines stored in runtime to make sure I don't have any extras. Tried using Image with DrawingVisual instead of Path. Nothing helped.
Appearances for clearer understanding
It was all about stroke dashes and WPF's unhealthy desire to count them all while getting hit test bounds for DrawingContext.
The related topic is Why does use of pens with dash patterns cause huge (!) performance degredation in WPF custom 2D drawing?

ILNumerics: Get mouse coordinates with ILLinePlot

I'm using ilnumerics with C# on a simple WindowsFormsApplication and created some LinePlots in a PlotCube on an ILPanel. Like in:
How to find the 3D coordinates of a surface from the click location of the mouse on the ILNumerics surface plots?
i tried to get the translated coordinates while clicking into the PlotCube to create something similar to the data tips at MATLAB.
Instead of a ILSurface I'm using ILLinePlot and my x-Axis is of logarithmic scale.
So I adapted the method mentioned at the above link to my setting.
My problem is that I only get correct coordinates when i am clicking exactly at the LinePlot!
When clicking right next to the line or anywhere else at the PlotCube the above MouseClick method runs into a NullReferenceException.
My idea for that problem was not to use the MouseClick target to get the transformations if i didn't hit the LinePlot but to set the group to the LinePlot like as if I had clicked on it (see at my example).
But although I now get the same groups in the while-loop and no Exception debugging shows that the transformation matrices of the PlotsData group and the PlotCubeScale group differ in both cases.
If I don't hit the LinePlot all transformation matrices are just the identities.
Here comes my short example:
private void ilPanel1_Load(object sender, EventArgs e)
{
ILArray<double> A = new Double[,] {{1,4,0},{10,12,0},{100,10,0},{1000,18,0},{10000,15,0}};
var scene = new ILScene() {
new ILPlotCube(twoDMode: true){
new ILLinePlot(ILMath.tosingle(A))
}
};
scene.First<ILPlotCube>().ScaleModes.XAxisScale = AxisScale.Logarithmic;
scene.First<ILPlotCube>().MouseEnter += (_s, _a) =>
{
if (!_a.DirectionUp)
{
//onplot is a global flag which is true if the mouse is over the LinePlot
Text = "On Plot - Target: " + _a.Target.ToString();
onplot = true;
}
};
scene.First<ILPlotCube>().MouseLeave += (_s, _a) =>
{
if (!_a.DirectionUp)
{
Text = "Off Plot - Target: " + _a.Target.ToString();
onplot = false;
}
};
scene.First<ILPlotCube>().MouseClick += (s, arg) =>
{
if (!arg.DirectionUp)
return;
var group = arg.Target.Parent;
// *new part: group holds my LinePlot if I clicked on it
// else it holds the PlotCube
if (!onplot)
{
// so I search the LinePlot at set group to it
foreach (ILLineStrip strip in scene.Find<ILLineStrip>())
{
if (strip.Tag == "LinePlotLine")
{
group = strip.Parent;
}
}
}
if (group != null)
{
// walk up to the next camera node
Matrix4 trans = group.Transform;
while (!(group is ILCamera) && group != null)
{
group = group.Parent;
// collect all nodes on the path up
trans = group.Transform * trans;
}
if (group != null && (group is ILCamera))
{
// convert args.LocationF to world coords
// The Z coord is not provided by the mouse! -> choose arbitrary value
var pos = new Vector3(arg.LocationF.X * 2 - 1, arg.LocationF.Y * -2 + 1, 0);
// invert the matrix.
trans = Matrix4.Invert(trans);
// trans now converts from the world coord system (at the camera) to
// the local coord system in the 'target' group node (surface).
// In order to transform the mouse (viewport) position, we
// left multiply the transformation matrix.
pos = trans * pos;
// here I take care of the logarithmic scale of the xAxis
pos.X = (float)ILMath.pow(10.0, pos.X);
// view result in the window title
Text = "Model Position: " + pos.ToString();
}
}
};
ilPanel1.Scene = scene;
}
Why do the group.Transform-matrices differ depending on my clicking position? The while-loop is exactly the same in both cases.
I hope that anyone can understand my problem. Although searching for weeks I didn't find any answer or different ways to reach my goal which is just to show the user the Coordinates of the Points on the line where he is clicking.
Thank you very much in advance for any help.
One work around is to get the lineplot node from the mouse handler's target rather than from the scene directly. That way we always get the right transforms. Replace your logic of identifying the lineplot with the following
ILGroup plotcubeGroup = e.Target as ILGroup;
ILGroup group = plotcubeGroup.Children.Where(item => item.Tag == "LinePlot").First() as ILGroup;
if(group != null)
{
// continue with same logic as above
}
Hope it helps.

Draw a static line between characters in a TextBox

I have a TextBox. I want the user to be able to click inside it; when he does, a red marker line should appear between the two characters closest to the click position, and remain here. The user could do that multiple times.
I'm new to WPF, but I guess, as in Winforms, I will have to hack a messy OnRender method. So far, it's okay.
What I'd really like to know is: how to get the two closest characters to the click position?
I was about to do a pixel check but it seems pretty heavy.
You could try:
textBox.GetCharacterIndexFromPoint(point, true);
Well I found what I wanted to do, and it's way simpler than I thought (even though FINDING the actual way was a pain).
Just add this handler to your textbox's SelectionChanged event:
private void placeMarker(object sender, RoutedEventArgs e)
{
// txt_mark is your TextBox
int index = txt_mark.CaretIndex;
// I don't want the user to be able to place a marker at index = 0 or after the last character
if (txt_mark.Text.Length > first && first > 0)
{
Rect rect = txt_mark.GetRectFromCharacterIndex(first);
Line line = new Line();
// If by any chance, your textbox is in a scroll view,
// use the scroll view's margin instead
line.X1 = line.X2 = rect.Location.X + txt_mark.Margin.Left;
line.Y1 = rect.Location.Y + txt_mark.Margin.Top;
line.Y2 = rect.Bottom + txt_mark.Margin.Top;
line.HorizontalAlignment = HorizontalAlignment.Left;
line.VerticalAlignment = VerticalAlignment.Top;
line.StrokeThickness = 1;
line.Stroke = Brushes.Red;
// grid1 or any panel you have
grid1.Children.Add(line);
// set the grid position of the line to txt_mark's (or the scrollview's if there is one)
Grid.SetRow(line, Grid.GetRow(txt_mark));
Grid.SetColumn(line, Grid.GetColumn(txt_mark));
}
}
You might want to add some kerning or simply increase the font size for the markers not to ruin readability.

Categories

Resources