If I have an NSTextView which is in this state:
How can I tell the textview that if a user presses shift+right, that rather than extending right towards the 'o', it instead de-selects the 'e'? I though this had to do with the affinity, but I have tried setting it to both NSSelectionAffinityUpstream and NSSelectionAffinityDownstream via the following code:
[self setSelectionRange: NSMakeRange(9,6)
affinity: x
stillSelecting: NO];
But that made no different. Hitting shift+right still selected the 'o'.
NSTextView knows how to do this SOMEHOW, because if you cursor position between 'w' and 'o', then hit shift+left until it matches the screenshot, then hit shift+right, it matches the behaviour I mentioned.
I'm ok to override the shift+arrow code and roll my own, but I would rather allow NSTextView to do its own thing. Anyone know if I am missing anything?
I'm not sure what you're trying to do, since this is the default text movement/selection modification behavior. Are you trying to override this to always do this one thing or are you trying to add another keyboard shortcut that overrides this behavior? In either case, some background:
Selection affinity doesn't quite work the way it sounds (this is a surprise to me after researching it just now). In fact there seems to be a disconnect between the affinity and the inherited NSResponder actions corresponding to movement and selection modification. I'll get to that in a moment.
What you want to look at are the responder actions like -moveBackwardAndModifySelection:, -moveWordBackwardAndModifySelection:, and so on. Per the documentation, the first call to such selection-modifying movement actions determines the "end" of the selection that will be modified with subsequent calls to modify selection in either direction. This means that if your first action was to select forward (-moveForwardAndModifySelection:), the "forward end" (right end in left-to-right languages; left end in right-to-left, automagically) is what will be modified from that point forward, whether you call -moveForward... or -moveBackward....
I discovered the disconnect between affinity and -move...AndModifySelection: by looking at the open source Cocoatron version of NSTextView. It appears the internal _affinity property isn't consulted in any of the move-and-select methods. It determines whether the selection range change should be "upstream" or "downstream" based on _selectionOrigin, a private property that is set when the selection ranges are modified (via the -setSelectedRange(s)... method or a mouse down / drag). I think the affinity property is just there for you to consult. Overriding it to always return one value or another doesn't change any behavior, it just misreports to outsiders. The _selectionOrigin seems only to be modified via -setSelectedRanges... method if the selection is zero-length (ie, just cursor placement).
So, with all this in mind, you might have to add one step to your manual selection modification: First set an empty selection with a location where you'd like the selection origin to be (forward end if you want backward affinity; backward end if you want forward affinity), then set a non-zero-length selection with the desired affinity.
Roundabout and ridiculous, I know, but I think that's how it has to be, given the CocoaTron source code.
Related
I'm trying to implement a syntax-coloring text editor that also does things like insert whitespace at the start of a new line for you, or replace text with text attachments.
After perusing the docs again after a previous implementation had issues with undoing, it seems like the recommended bottleneck for this is NSTextStorageDelegate's textStorage(_,willProcessEditing:,range:,changeInLength:) method (which states that Delegates can change the characters or attributes., whereas didProcessEditing says I can only change attributes). This works fine, except that whenever I actually change attributes or text, the text insertion mark moves to the end of whatever range of text I modify (so if I change the style of the entire line, the cursor goes at the end of the line).
Does anybody know what additional call(s) I am missing that tell NSTextStorage/NSTextView not to screw up the insertion mark? Also, once I insert text, I might have to tell it to move the insertion mark to account for text I've inserted.
Note: I've seen Modifying NSTextStorage causes insertion point to move to the end of the line, but that assumes I'm subclassing NSTextStorage, so I can't use the solution there (and would rather not subclass NSTextStorage, as it's a semi-abstract subclass and I'd lose certain behaviours of Apple's class if I subclassed it).
I found out the source of the problem.
And the only solution that will work robustly based on reasons inherent to the Cocoa framework instead of mere work-arounds. (Note there's probably at least one other, metastable approach based on a ton of quick-fixes that produces a similar result, but as metastable alternatives go, that'll be very fragile and require a ton of effort to maintain.)
TL;DR Problem: NSTextStorage collects edited calls and combines the ranges, starting with the user-edited change (e.g. the insertion), then adding all ranges from addAttributes(_:range:) calls during highlighting.
TL;DR Solution: Perform highlighting from textDidChange(_:) exclusively.
Details
This only applies to a single processEditing() run, both in NSTextStorage subclasses and in NSTextStorageDelegate callbacks.
The only safe way to perform highlighting I found is to hook into NSText.didChangeNotification or implement NSTextDelegate.textDidChange(_:).
As per #Willeke's comments to the OP's question, this is the best place to perform changes after the layout pass. But as opposed to the comment thread, setting back NSText.selectedRange does not suffice. You won't notice the problem of post-fixing the selection after the caret has moved away until
you highlight whole blocks of text,
spanning multiple lines, and
exceeding the visible (NSClipView) boundaries of the scroll view.
In this rare case, most keystrokes will make the scroll view jiggle or bounce around. But there's no additional quick-fix against this. I tried. Neither preventing sending the scroll commands from private API in NSLayoutManager nor avoiding scrolling by overriding all methods with "scroll" in them from a NSTextView subclass works well. You can stop scrolling to the insertion point altogether, sure, but no such luck getting a solid algorithm out that does not scroll only when you perform highlighting.
The didChangeNotification approach does work reliably in all situations I and my app's testers were able to come up with (including a crash situation as weird as scrolling the text and then, during the animation, replacing the string with something shorter -- yeah, try to figure that kind of stuff out from crash logs that report invalid glyph generation ...).
This approach works because it does 2 glyph generation passes:
One pass for the edited range, in the case of typing for every key stroke with a NSRange of length 1, sending the edited notification with both [.editedCharacters, .editedAttributes], the former being responsible for moving the caret;
another pass for whatever range is affected by syntax highlighting, sending the edited notification with [.editedAttributes] only, thus not affecting the caret's position at all.
Even more details
In case you want to know more about the source of the problem, I put more my research, different approaches, and details of the solution in a much longer blog post for reference. This here, though, is the solution itself. http://christiantietze.de/posts/2017/11/syntax-highlight-nstextstorage-insertion-point-change/
The above accepted answer with the notification center worked for me, but I had to include one more thing when editing text. (Which may be different from selection).
The editedRange of the NSTextStorage was whack after the notification center callback. So I keep track of the last known value myself by overriding the processEditing function and using that value later when I get the callback.
override func processEditing() {
// Hack.. the editedRange property when reading from the notification center callback is weird
lastEditedRange = editedRange
super.processEditing()
}
For mouse I'm using:
ourEvent = CGEventCreate(None);
currentpos = CGEventGetLocation(ourEvent);
What can I use for the caret?
First the bad news.
Not every app is Cocoa-based, and those that are neither Cocoa nor Carbon nor a straight mix of the two—i.e., those based on wxWidgets, Qt, or some other cross-platform framework—typically reimplement the entire GUI stack on top of raw event and drawing primitives.
That means that there is typically no way to get this information from those applications (unless they're scriptable and expose it that way).
The good news is, Cocoa apps and some Carbon apps may expose this via Accessibility.
The user will need to have assistive devices turned on in System Preferences. Once that condition is met, you can use the Accessibility framework to get the frontmost application, get its focused window, get its focused view, and get its selection ranges.
A text view with an insertion point has exactly one selection range, and that range is empty (length=0). The location is where the insertion point is.
Of course, those are character indexes, not on-screen bounds.
That's where parameterized attributes come in. There's one for converting ranges to bounds. That's the one you want.
Theoretically (I haven't tried this), you should be able to convert the empty range of the insertion point to an empty or nearly-empty rectangle whose location is somewhere within the vertical line of the insertion point.
Make sure you test this with text views that are in scroll views, particularly when the insertion point is scrolled partially or completely out of view.
You'll want to use the Accessibility Inspector to see for yourself where your application will need to look, and to test individual applications and investigate reported failures.
You can get it from the Developer Downloads page, in the “Accessibility Tools” disk image.
If you want to focus a window, forging a mouse event to click on it is a bad idea—anything can happen if you click on the wrong thing. Send the window an kAXRaiseAction action instead.
If you want to set a text view's insertion point (and are looking to find where you need to forge a mouse event to click to set it in the desired position), again, that's a bad way to do it. Set the view's kAXSelectedTextRangesAttribute attribute instead. Again, an insertion point is a single empty range.
Did you try like this below?
NSPoint p=[[NSApp currentEvent]locationInWindow];
CGFloat X=p.x;
CGFloat Y=p.y;
NSLog(#"%f %f",X,Y);
I have an NSTableView in which I need to be able to intercept keyboard events within an editable cell and replace the cell with strings. For example, press "a" and have the "a" intercepted and the value "Alpha" assigned when the table reloads. The actual situation is a bit more complex in that I'm also handling HID devices, but that's a whole other story. Suffice it to say, I just want to be able to go into edit mode and stop the keyboard-generated values from being displayed.
The latter part of this (displaying "Alpha") is easy, but I can't figure out the first part of the problem. If I subclass the table and make it first responder, I can receive the keyDown: event, but once the user double-clicks on a cell and starts typing, this goes silent.
Since none of the other NSTableView components (NSCell, NSTextFieldCell, etc) derive from NSResponder, I'm assuming there is an NSTextField buried in there somewhere.
So, what's the best way to filter text once the user goes into cell edit mode?
As always happens: after working on this for eight hours, reading all the docs five times, and then resorting to the net, I find the answer five minutes later:
- (BOOL)textShouldBeginEditing:(NSText *)textObject.
Sorry to consume bandwidth.
There are times when it makes sense to force an end of any edits that are currently being made. For instance, if the user is typing a value in a field on my document, and then clicks to the preview window, or chooses "Print" or "Save" from the menu.
The problem is, by forcing an end to editing (either by asking the NSWindow to make itself first responder, or by calling endEditingFor: on the window), the first responder focus is no longer on the field in question. This is disruptive to the user in some situations, where the action being taken is iterative and does not signify an end to their work on the document.
My first attempt at solving this is to pay attention whatever the current firstResponder is, and then to restore it after ending editing, by using NSWindow's "makeFirstResponder:". This works OK, but it has the undesired effect e.g. on NSTextFields of resetting the selection in the field editor to the entire length of the string contents.
Is there some trick I can use to force the entire system of "endEditing" methods to be called without disrupting the current field editor at all?
Thanks,
Daniel
Why not use your original method but also record where/what the selection range looks like and restore it after restoring the first responder?
I agree that this is the way to do it. On iPhone it is particularly important not to have the keyboard jumping up and down at inopportune moments. I have used:
[textView endEditing:YES];
[textView becomeFirstResponder];
successfully to complete pending spelling correction (as though a space were hit) before taking action on the content of a UITextView, but without any side-effects.
I'm working on a Win32 control. There could be hundreds of "items" on this control. Those are not windows, but internal objects (eg: rectangles). Depending on the mouse position I want to change the mouse cursor. That is fine, I can use WM_SETCURSOR.
At the same time based on mouse move I want to display a status bar which shows details about the object currently under the mouse. For that I can use WM_MOUSEMOVE.
Because there could be hundreds of items, traveling all of them to find one under the mouse, well it's not efficient, especially two times (one for set cursor, one for mouse move).
To make it short, do you know if WM_SETCURSOR and WM_MOUSEMOVE are ALWAYS in pair? In that case I can calculate what I want during WM_SETCURSOR. The other option would be to set the mouse cursor during WM_MOUSEMOVE, but as far as I know that it's not a good solution (will flicker).
Thanks
While they might currently always come as a matched pair, you probably can't rely on this behaviour.
You can set the cursor during WM_MOUSEMOVE (using SetCursor), and it won't flicker, as long as (IIRC), you return TRUE from WM_SETCURSOR without doing anything (i.e. you eat the message), and your window doesn't have a class cursor assigned to it.
You might also try GetMessagePos() (gives cursor screen coordinates), then MapWindowPoints() and see if it's in hot rectangle, or something similar.
Most important of all is that your window message handlers shouldn't worry about holding or calculating anything. You should simply signal your application's logic that the mouse is potentially over new area and make it find the object(s). Once you find the hot area (or more than one), cache its (their) boundaries and check the following mouse moves against those. Once the mouse moves out from one of them, you can rebuild your hot-object-list.
You don't have to be hunting for the hot area all over the control on every mouse move.
In case when there can be many objects sharing the same area, there's the question of z-order. Think about it when you're creating those objects and handle their movement.
Also you should think about an efficient data structure holding the object coordinates so you don't have to check every single object every time you're looking for the hot one.
Just my two euros. ;)
Is there any way to cache the last item that was found, and shortcut the lookup if the cursor is in the same place? That would be the most robust solution.