I have wrapping NSTextView
instances stacked vertically, for example:
The quick brown fox
jumps over the lazy dog
Jackdaws love my big
sphinx of quartz
I need to move between them with up/down arrows. For example, when the cursor is positioned after the l in "lazy" and the user presses the down arrow, the cursor should jump right after the y in "my" – like it would do if these sentences were in the same text view.
By default, when the down arrow is pressed while the cursor is at the last wrapped line, a text view moves it to the end of that line. While I can use textView(_:doCommandBy:)
in NSTextViewDelegate
to detect the "arrow down" selector and override the default behavior, there are two problems:
I can determine if the cursor is at the last line by getting its position via the selectedRanges
property and then checking for the newline character after this position, but it is not possible to know if it is at the last wrapped line, i.e. near the border of the current text view.
I need to know the X coordinate of the cursor to place it at approximately the same X coordinate in another text view (the fonts won't necessarily be fixed-width, so I can't rely on the character count).
I suppose both of them can be resolved via NSLayoutManager
, but I can't wrap my head around all of its available methods.
It turned out to be relatively easy, here's what I've done (the examples are in C#). First, boundingRect(forGlyphRange:in:)
gets the cursor's location in the current view:
var cursorLocation = new NSRange(CurrentTextView.SelectedRange.Location, 0);
var cursorCoordinates = CurrentTextView.LayoutManager.BoundingRectForGlyphRange(cursorLocation, CurrentTextView.TextContainer).Location;
Then if the second text view is below, the insertion point will be at 0 on the Y axis:
var insertionPoint = new CGPoint(cursorCoordinates.X, 0);
And if it is above, then another view's height should be used (reduced by 1, otherwise the resulting character index will be incorrect and the cursor will be placed at the end of the line):
var insertionPoint = new CGPoint(cursorCoordinates.X, AnotherTextView.Bounds.Size.Height - 1);
After getting the insertion point, another view needs to become the first responder and then characterIndexForInsertion(at:)
does the job:
Window.MakeFirstResponder(AnotherTextView);
var index = AnotherTextView.CharacterIndex(insertionPoint);
AnotherTextView.SelectedRange = new NSRange(index, 0);