NSTextView in NSPersistentDocument doesn't update dirty flag until loses first responder - cocoa

I have an NSTextView in an NSPersistentDocument window. I bind the text field's contents to a "binary data" Core Data field, but when I type text into the text view, the document's title bar doesn't say "Edited" until the text view loses focus. Thus, if I quit after making an edit, the new data isn't saved.
If I pass the NSContinuouslyUpdatesValueBindingOption flag to the text view binding, "Edited" appears immediately, but performance really suffers in long documents.
How do I let Core Data know that there are unsaved changes without actually assigning all the text data on every change?
(This question is like "Binded NSTextField doesn't update the entity until it lose the focus" except I can't use NSContinuouslyUpdatesValueBindingOption because it makes editing operations very slow.)

I think this is not possible as far as i understood.
When you assign the changes to a property of a NSManagnedObject, CoreData manage to diry state (and the undo stuff) for you. If you just try to change the diry without the data a potential save operation will not work.
Take a look into "The Document Architecture Provides Undo Support for Free" how the dirty state and the undo support are implemented.
If you have really large text documents, i suggest you should not store them in a CoreData property. As you can read in "Incremental Data Reading and Writing" i suggest to store the text in a separate file and use a NSFileWrapper. At least I use this solution for my application.
This is btw. what CoreData itself suggest here "...It is better, however, if you are able to store BLOBs as resources on the filesystem, and to maintain links (such as URLs or paths) to those resources. You can then load a BLOB as and when necessary"
I don't know what kind of text you have in your NSTextView but you was taking about "long documents".

If you subclass your "NSTextView", you can catch the "insertText:" method and then as soon as a character is typed, set the "document Edited" flag of the document without the heavy duty (and CPU intensive) binding you're doing.

Related

Removing redo actions in NSUndoManager

I am facing an architecture question and I am wondering if anyone know if my idea is feasible or has a better architecture idea.
My situation is that I have CoreData data model for tracking some financial data, imagine it is bank accounts (it's not so don't worry about the security thing). Data for the core data objects is mainly user input.
I know well how to make user input screens (implemented as sheets on top of the main window) and how to implement undo for these input sheets. For simple sheets like edit account i simply copy data from the relevant core data object to the sheet's controller and write back on OK-Close. The write back to the CoreData object is wrapped in an undo grouping which gives me a single undoable action to restore the state to before the edit. This works fine.
Now I am contemplating a more complex edit control where the transactions on the account would be shown in an editable grid (like excel) and it would be possible to edit, add or remove actions. For this control it seem difficult to copy all data to a new data structure. Instead I was thinking of using the core data store as the data source directly, meaning core data objects would change, be created and deleted as the user edit rows in the control. Within this control i then know how to get step by step undo.
The issue comes upon end of the edit session if the user then clicks cancel. I figure to handle this instance I could wrap all actions in the control into one undo group and undo that.
The issue is that after this the canceled actions are then available as a redo on the redo stack. This is cleared as soon as the user does another undoable action but I would like to ensure that this redo action is never recorded at all.
A work around I have thought about is to do some (no-op) action that is undoable after the cancel to clear the redo stack, but then this no-op shows up in the undo stack instead which is also undesirable.
Basically what I'd like to do is to do some form of core data rollback to the state before the complex edit sheet is activated without this rollback being re-doable. The edit sheet is modal and it can be guaranteed that no other changes will happen in the core data model while the edit sheet is active.
Also worth knowing may be that the core data context is not guaranteed to be saved before the edit sheet is activated so simply throwing away the context and reloading from persistent store is not an option. (And would also lose previous undo history as well which I do not want)
The simple question is then, how do I clear the redo stack of the core data NSUndoManager? More generally does anyone know if this is the right solution to my problem or where should I look for a better design solution?

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.

NSTextView / NSScrollView - A few questions to help me understand it's proper usage

I have created a "notes" field designed to hold multiple paragraphs of text which I would like to store in a custom object. Originally, I just used an NSTextField as a temporary solution, but this does not allow me to scroll or have multiple paragraphs of text...
In IB I have placed a NSTextView (which seems to be wrapped inside an NSScrollView.) Upon execution of my program, seems to allow me to enter text in multiple paragraphs, scroll, etc. In short it LOOKS to be exactly what I want would like it to be. So far so good.
Now, I need to retrieve the data from this field and store it in my custom object. This is where I'm getting a bit lost within the developer documentation...
My goals are fairly straight forward:
Allow users to type away in the box.
Store the contents of the box into a variable (array, etc.) in my custom object when the user moves to another field, leaving the notes field.
Display the users stored text in the text box next time the record is viewed.
Second, is there a simple way to retrieve and store the data into a "notes" variable in my custom object (such as an NSString object? I would think having multiple would exclude an NSString object as an option here, but maybe I'm wrong) or am I getting into a more complex area here (such as having to store it in an array of NSString objects, etc.)?
Any help would be appreciated!
You can get the data using -string, defined by NSText (e.g. NSString *savedString = [aTextView string])
Your save code can be put in your NSTextDelegate (read, delegate of the NSTextView, because it's the immediate superclass), in – textDidEndEditing: which will be called, well, when editing is finished (e.g. when the user clicks outside the view) or one of the other methods.
Then to reload the saved string if you emptied the text view or something, use [textView setString:savedString] before editing begins.
NSTextDelegate documentation: here.
I'm not sure what you mena when you say "store the contents of the box into a variable (array, etc.) Are you hoping for an array of custom notes? Text views store a string of data, so the easiest way of storing its value is using one string; if you need an array of notes you'd have to split the string value into different paragraphs, which shouldn't be too hard.

Designing custom NSTextView

I need to design a custom text view that displays logs that my application produces. Logs have a specific packed binary format, each entry includes a number of fields besides an actual string (log level, date, source). Now these logs can be huge, hundreds of megabytes of data. I need to implement features such all quick filtering based on message type/source, searching, control over memory layout, etc. Since NSTextView supports most of these features i decided to start from there.
I obviously need my custom text storage to provide access to my packed log format, to load new strings on demand when user scrolls the log view window. I also need to selectively display lines of logs based on current active filters (display only warnings for example) without reloading the whole text into the view again, just filtering out the lines as they are displayed.
I have looked at NSTextStorage and it advises to overload -string, which does not exactly fits the purpose. Could anyone please give a couple of pointers to guide my further research? I am relatively new to cocoa's text handling.
Not a direct answer to your question, but a possible alternative good enough for Apple:
Why not do as Console.app does with proper system logs? Each log entry (though it might be multiple lines) starts with a very specific format. Console.app uses an outline view (an entry has a child row if the line is too long for the table row to keep all entries the same size for easy perusal). Check it out in /Applications/Utilities/Console.app under a standard log.
The benefits: simple selection of entire entry, very easy search filtering, alternating row colors make individual entries easier to see, you could use variable row height to show the whole message if you didn't like the truncated / disclosure approach.

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

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.

Resources