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()
}
Related
I know that styles have an attribute to allow/avoid changes in the text in which they are applied:
SCI_STYLESETCHANGEABLE(int style, bool changeable)
However, the documentation says "This is an experimental and incompletely implemented style attribute.", which seems it is not "production-ready"
I think there is another alternative, using "Indicators":
To protect a range of text, we could apply an specific indicator to it and in keydown event detect "del" & "backspace". If the character that follows current position (caret position) in "del" case or that precedes current position in "backspace" case has this indicator, then we must cancel the event and do not erase the text.
Which of this 2 alternatives is better? There is another better way?
Note: ScintillaNet is based on Scintilla and I assume that the extensive documentation of the last one is valid for both.
I share with you one of the responses in "scintilla-interest" google-groups that published Neil Hodgson, the creator of Scintilla.
[Ian G.]> SCI_STYLESETCHANGEABLE is what I tried initially before I resorted to writing this patch. The only real disadvantage I can see is that in cases like my own it doubles the number of styles needed because every visual style that can occur in a writeable region can also occur in a protected region, but I could live with that.
[Neil Hodgson] It would be better to use an indicator for protected areas since they are relatively space efficient and avoid doubling the number of styles.
complete post in scintilla-interest
The database application I am working on can have a window with multiple NSTextView elements for displaying and editing data. When the current spot in the database is repositioned, all of the NSTextView objects in the window need to be updated with new contents. This is done with a loop that scans each object and checks to see if it needs to be updated. If it does, the new value is calculated, then updated by using the [NSTextView setString:] method. Here is a simplified version of the code involved.
for formObject in formObjectsInWindow {
NSTextView * objectTextView = [formObject textView];
NSString * updatedValue = [formObject calculateValue];
[objectTextView setString: updatedValue];
}
This works, but if there are a lot of objects, it is somewhat slow. Probably related, the display does not update all at once, you can actually see a "ripple" as the objects are updated, as illustrated in this movie (this movie has been slowed down to 1/4 speed to make the ripple effect more pronounced, but it is definitely visible at full speed).
If you've gotten this far, you might suspect that the calculateValue method is slow, but that isn't the problem. In other places the same code is used and runs at tens of thousands of operations per second. Also, this delay only occurs during update operations, it doesn't occur when the window is first opened, even though the same calculations are required at that time. Here is an example. Notice that when I switch back to the detail view all the NSTextView objects update instantaneously, even though the record changed and all of the values are different.
My suspicion is that the [NSTextView setString:] method is updating the off-screen buffer, then immediately copying that to the on-screen buffer, so that this double buffering is happening over and over again for each item, causing the delay and ripple. If so, I'm sure there must be some way to prevent this so that the screen is only updated at the end after all of the values have been updated. It's probably something simple that I am missing, but I'm afraid I am stumped as to how this is supposed to be done.
By the way, this application does not use layer-backed views, and is not linked against the QuartzCore framework.
I brought up this question with Apple engineers at the WWDC 2018 labs. It turns out the problem is that the setString: method does not mark the NSTextView object as needing display. The system does eventually notice that the text has changed and updates the display, but this happens in an asynchronous process, hence the "ripple" effect. So the workaround is simply to add a call to setNeedsDisplay after calling setString.
[objectTextView setString: updatedValue]
[objectTextView setNeedsDisplay:YES];
Adding this one line of code fixed the problem, no more ripple effect.
I'm told that this is actually a bug, so hopefully this extra line won't be needed in future versions of macOS. As requested, a radar has been filed (41273611 if any Apple engineers are reading this).
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.
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.
I'm new to developing on the Mac and am looking to implement an interface similar to Spotlight's - the main part which seems to be an expanding table/grid view.
I was wondering if there is a component Apple provides for creating something like this or is available open source else where.
Of course if not I'll just try and work something out myself but it's always worth checking!
Thanks for your help in advance.
New Answer (December, 2015)
These days I'd go with a vertical stack view ( NSStackView ).
You can use its hiding priorities to guarantee the number of results you show will fit (it'll hide those it can't). Note, it doesn't reuse views like a table view reuses cell views, so it's only appropriate for a limited number of "results" in your case, especially since it doesn't make sense to add a bunch of subviews that'll never appear. I'd go so far as to say outright you shouldn't use it for lists of things you intend to scroll (in this case, go with a table view).
The priority setting can be used to make sure your assumption of what should be "enough" results doesn't cause ugly layout issues by letting the stack view "sacrifice" the last few.
You can even emulate Spotlight's "Spotlight Preferences" entry (or a "show all" option) by adding it last and setting its priority to required (1000) so it always stays put even if result entries above it are hidden due to lack of space.
Lately all my UI designs for 10.11 (and beyond) have been making heavy use of them. I keep finding new ways to simplify my layouts with them. Given how lightweight they are, they should be your go-to solution first unless you need something more complex (Apple engineers stated in WWDC videos they're intended to be used in this way).
Old 2011 Answer
This is private Apple API. I don't know of any open-source initiatives that mimic it off-hand.
Were I trying to do it, I might use an NSTableView with no enclosing scroll view, no headers, two columns, right-justified lighter-colored text in the left column, the easily-googled image/text cell in the right column, with vertical grid lines turned on. The container view would observe the table view for frame changes and resize/reposition accordingly.
Adding: It might be a good idea also to see if the right/left justified text (or even the position of the columns) is different in languages with different sweep paths. Example: Arabic and Hebrew are read right-to-left. Better to adapt than to say "who cares" (he says flippantly while knowing full well his own apps have problems with this sort of thing :-)). You can test this by making sure such languages are installed on your computer, then switching between them and testing out Spotlight. Changing languages shouldn't pose an issue since the language switching UI doesn't rely on reading a foreign language. :-)