Can I end editing for the field editor's control without disrupting focus? - cocoa

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.

Related

wxWidgets pprogrammaticaly move to next input control

I originally had code that set the focus to the first widget in a dialog, in the onInit method. But there were problems with it: if I pressed TAB, indeed focus moved to next control (wxTextCtrl), which got the blue 'focus' color, but the 'focus' color/highlight was not removed from previous focus widget. So now it looked like both first and second control had focus at the same time...
When cycling manually (by pressing TAB) full circle (till last control and then wrap around to the first), suddenly all worked well. That is, when moving focus from first control to next one, the first visually lost focus (blue color was removed) as it should. From now on, only one item had the focus color/highlight.
So instead of setting focus on the first control, I tried a different approach: I set the focus to the last control in the dialog, which is always the OK button. Next, I want to emulate programmatically that a TAB is pressed and received by the dialog. So I wrote this (inside Dialog::onInit):
m_buttonOK->SetFocus();
wxKeyEvent key;
key.SetEventObject(this);
key.SetEventType(wxEVT_CHAR);
key.m_keyCode=WXK_TAB;
ProcessWindowEvent(key);
Now the focus indeed moves away from the OK button, but it does not wrap around to the first control.
Only when I manually press TAB after the dialog opened, the first item gets focus.
Question: why does this wrapping around to set focus on first widget not work with the code shown above?
First of all, your initial problem is almost certainly related to not calling event.Skip() in one of your event handlers, see the note in wxFocusEvent documentation.
Second, you can't send wx events to the native windows, they don't know anything about it. In this particular case you can use wxWindow::Navigate() to do what you want, but generally speaking what you're doing simple can't, and won't, work reliably.

NSTextStorageDelegate's textStorage(_,willProcessEditing:,range:,changeInLength:) moves selection

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()
}

How to tell NSTextView which side to extend selection on?

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.

Is NSTextView's insertText: *really* not suitable for programmatic modification of text?

I've written an NSTextView subclass that does frequent programmatic modification of the text within itself (kinda like an IDE's code formatting - auto-insertion of close braces, for example).
My initial implementation of this used NSTextView's insertText:. This actually appeared to work completely fine. But then while reading the NSTextView documentation (which I do for fun sometimes), I noticed in the Discussion section for insertText:
This method is the entry point for inserting text typed by the user and is generally not suitable for other purposes. Programmatic modification of the text is best done by operating on the text storage directly.
Oh, my bad, I thought. So I dutifully went around changing all my insertText calls to calls to the underlying NSTextStorage (replaceCharactersInRange:withString:, mostly). That appeared to work OK, until I noticed that it completely screws up Undo (of course, because Undo is handled by NSTextView, not NSTextStorage).
So before I haul off and put a buncha undo code in my text storage, I wonder if maybe I've been Punk'd, and really insertText: isn't so bad?
Right, so my question is this: is NSTextView's insertText: call really "not suitable" for programmatic modification of the text of an NSTextView, and if so, why?
insertText: is a method of NSResponder -- in general these would be thought of as methods that respond to user events. To a certain degree they imply a "user action." When the docs tell you to edit the NSTextStorage directly if you want to change things programmatically, the word "programmatically" is being used to distinguish user intent from application operation. If you want your changes to be undoable as if they were user actions, then insertText: seems like it would be OK to use. That said, most of the time, if the modification was not initiated by a user action, then the user won't consider it to be an undoable action, and making it a unit of undoable action would lead to confusion.
For example, say I pasted a word, "foo", into your text view, and your application then colored that word red (for whatever reason). If I then select undo, I expect my action to be the thing that's undone, not the coloring. The coloring isn't part of my user intent. If I then have to hit Cmd-Z again to actually undo my action, I'm left thinking, "WTF?"
NSUndoManager has support for grouping events via beginUndoGrouping and endUndoGrouping. This can allow the unit of user intent (the paste operation) to be grouped with the application coloring into a single "unit" of undo. In the simplest case, what you might want to try here is to set groupsByEvent on the NSUndoManager to YES and then find a way to trigger your application's action to occur in the same pass of the runLoop as the user action. This will cause NSUndoManager to group them automatically.
Beyond that, say if your programmatic modifications need to come in asynchronously or something, you'll need to manage these groupings somehow yourself, but that's likely going to be non-trivial to implement.
I don't know if you saw my similar question from a couple years ago, but I can tell you that #ipmcc is correct and that trying to manually manage the undo stack while making programmatic changes to the NSTextStorage is extremely non-trivial. I spent weeks on it and failed.
But your question and #ipmcc's answer make me think that what I was trying to do (and it sounds like pretty much the exact same thing that you are trying to do) may actually be more in the realm of responding to user intent than what the docs mean by programmatic change. So maybe your original solution of using insertText: is the right way to do it. It's been so long since I abandoned my project that I can't remember for sure if I ever tried that or not, but I don't think I did because I was trying to build my editor using just delegate methods, without subclassing NSTextView.
In my case, as an example, if the user selects some text and hits either the open or closed bracket key, instead of the default behavior of replacing the selected text with the bracket, what I want to do is wrap the selected text in brackets. And if the user then hits cmd-Z, I want the brackets to disappear.
Since I posted this question, I've moved forward with my original implementation, and it appears to be working great. So I think the answer to this question is:
insertText: is completely suitable for the programmatic modification
of text, for this particular use case.
I believe that the note is referring to pure programmatic modification of text, for example, setting all the text of a textview. I could definitely see that insertText: would not be appropriate for that. For my intended purpose, however - adding to or editing characters in direct response to user actions - insertText: is entirely appropriate.
In order to make my text modifications atomic with the user interactions that triggered them (as #ipmcc mentions in his answer), I do my own undo handling in an insertText: override. I wrote about this in #pjv's similar question. There's also a sample project on github, and I'll probably write it up on my blog at some point.

Filtering text in NSTableView

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.

Resources