When an NSWindowController has an NSDocument assigned, its edited status is automatically set based on the current location in the undo/redo stack relative to when the document was last saved. It works great so that the "Edited" title of the window is automatically set when someone performs an undoable action since the last save.
In my case, I have multiple window controllers for the document, because I have multiple tabs that each control a segment of the data. So I want a separate undoManager for each window/tab so that the changes in one tab are not undone when the user hits undo in a separate tab. I.e., I need to have isolated undo managers.
This works fine, but since I'm not ever talking to the NSDocument's undo manager, the edited status of the window is not being updated when I make undoable changes in any of the tabs. How can I signal to the document that I want its edited status to be contingent on the undo stack location for a number of other undo managers?
Related
I have a Cocoa app that presents a folder structure to the user and allows basic file system operations such as move/copy/rename.
I perform all file system access on a background queue and use file coordination via NSFileCoordinator.
Let's imagine the user drags the file "Notes.txt" into the folder "Project B". Here is what I do:
I schedule the file operation of moving the file on the background queue.
Once that's done, it calls a block on the main queue where I update the outline view
Let's assume the background operation moving the file takes some time. I need to prevent that the user continues to move files around or performs other actions until the first operation completes.
Hence I would like to:
Disable all user interaction with the window
Disable most menu items and keyboard shortcuts
Keep certain menus such as [Quit Cmd-Q] working
Don't block the main thread
One solution that comes to mind: use a modal sheet?.
I don't want to do this, because most of the time, the operation will finish quickly and the sheet would only be shown for a fraction of a second, which is distracting.
I keep track of how long the operation takes would switch from "interaction blocked mode" to "modal sheet" if it takes more than 500 ms.
The question is: how can I implement such a "user interaction blocked mode" without actually presenting a modal sheet?
Alternative idea: disabling all controls
I considered setting isEnabled = false for all controls, but that's not an option, because the UI can be arbitrarily complex and it won't prevent actions via the menu or keyboard shortcuts.
You may find helpful The Responder Chain article in Apple's Documentation. Proper solution depends on what you exactly need. Overriding NSMenu's - (BOOL)validateMenuItem:(NSMenuItem *)menuItem; let you control over enable menu items and its shortcuts. You may also put transparent NSView over the area you want to prevent from user's interactions.
It is rarely used, but Apple's NSDocument documentation describes how to set up an NSDocument with multiple windows for a single document. I'm working on a database application that does this. Here is an example of a checkbook database document with two windows open. Each window shows a different view of the document, in this case a spreadsheet like view in the back, and a chart summarizing this data set in the front window. This example shows two windows for one document, but the user can create as many windows per document as they want, each displaying the same underlying document in a different way.
Everything works fine, except that if a system dialog sheet is opened (Save As, Print, Page Setup), most of the time (but not every time) the dialog sheet jumps to another window and attaches to that window instead of the current window, as shown in this movie.
Notice that although the dialog sheet attaches to the window containing the chart, it is correctly printing the content in the spreadsheet window. If I press Print, the correct content will be printed.
For printing, all our code does is call the NSDocument printDocument: method.
[NSApplication sendActionToFirstResponder:#selector(printDocument:)];
Page Setup code is also just calling NSDocument.
[NSApplication sendActionToFirstResponder:#selector(runPageLayout:)];
Our code is not customizing any of these dialog sheets, they are completely stock.
For the Save As command there is no code in our application at all, this appears automatically in the menu when the option key is pressed.
This problem appears in all versions of macOS supported by our application, from 10.9 thru 10.13. Perhaps this is an AppKit bug that is rarely seen because multiple windows with a single document is so rarely used?
This problem doesn't cause a crash, or prevent a user from doing what they want, but it is very visibly incorrect and reduces user confidence in the quality of the program.
For my reference this is #221 in the Panorama X issue tracker.
Implement/override NSDocument property windowForSheet.
The value of this property may be nil, in which case the sender should present an app-modal panel. The NSDocument implementation of this property sets the value to the window of the first window controller, or [NSApp mainWindow] if there are no window controllers or if the first window controller has no window.
So I have looked through existing answers tagged with dockpanel-suite and have not found what I am looking for (as I type this, it is also not appearing in the Similar Questions area). For starters, note that I am NOT asking about saving and restoring the entire Workspace.
So here is the scenario. I have a graphical window (we will call it "Timeline") that is added upon user request. When it is added, it is automatically docked to the bottom-most area of the main form. The user then takes the Timeline window and redocks it somewhere else (could be docked to an edge, or within another docking pane) and changes its docking behavior (floating, auto-hide, tab, etc.).
A demonstrable example is in Visual Studio. If you have the Solution Explorer on a tab within an docked pane on the right and close Solution Explorer, you can go to View -> Solution Explorer and bring it back up again, and it restores to the correct location.
Now the user closes the Timeline window entirely by hitting the [x] on its pane, and in the future, they request to add it to the application again. I want to bring it back in the last dock state and position it was in when it was last closed.
Now, I appear to be able to catch the closing of the pane with the ContentRemoved event, but in there e.Content.DockHandler.Pane, e.Content.DockHandler.PanelPane and e.Content.DockHandler.FloatPane are all null so I have no obvious way to get the previous dock geometry. e.Content.DockHandler.DockPanel is valid, but it is the parent/root docking panel, and calling SaveAsXml(...) does nothing for me because it would get the entire workspace.
Even if I were able to capture it here, information I need. However, I do not appear to be able to simply call LoadFromXml(...) on anything either.
If I try to do it before the DockContent object is added to the DockPanel, DockHandler.DockPanel member is null, so I appear to have no place to restore the XML into, even if I was able to get it.
There are no other events hanging off of DockPanel that seem to be able to help me here.
So - is there a way to do this, and what is the correct way to do it? I want to make sure I am not barking up the wrong tree with trying to capture the dock information as XML when closed and restore it later.
I have toyed with the idea of not actually closing the window but just undocking and hiding it, but have not explored that very far yet. Same with hooking the DockChanged event, but it does not seem to fire on the DockContent objects being docked/floated/etc. and I am not sure why.
Also the solution needs to be robust enough so that I can correctly handle scenarios such as if the last docking parent no longer exists. For example, if it was docked as a tab somewhere, but now that parent window (containing the tabs) has also been closed. I do not know if LoadFromXml, presuming it is the right way to do, is robust enough to handle this scenario, as I have not been able to test it yet.
If I understand the question, what I do is to trap the Closing or FormClosing event, .Hide() the form and set e.Cancel = true. If you are using DockContent, then there is a HideOnClose() that does the work for you. Then when you want to "re-open" the window, you simply use an empty .Show(), and it will Show right where it was when you "closed" it.
As far as saving if the last docking parent no longer exists, I agree with Lex Li, that will take a hack.
Why is [self undoManager] zero in a child window, in a doc-based app?
Should it not refer to the undo manager of its parent window? In the parent window, I get an actual address for the undo manager!
The undoManageris not a member of NSWindowController.
This is just a NSDocument "feature".
An excerpt from the NSDocument docs :
... A document manages its window’s edited status and is set up to perform undo and redo operations. ....
Section "Subclassing NSDocument":
.... Subclasses are also responsible for the creation of the window controllers that manage document windows and for the implementation of undo and redo. ....
The code you've written won't work on other strongly typed languages because you would send a message to an object that doesn't exist. I'm pretty sure you should have a compiler warning here.
Hope this helps,
best,
Flo
Flo's answer was a good starting point. Some time later, it turns out that the responder chain is somehow acting up (or, it may be me :-) ).
The child window, controlled by NSWindowController, should automatically (??) have a document property so that [self document] returns the document associated with this window. It's easy to pull the Undo manager from that.
However, in my application (and in a small testing app, too) this document is not set. When I set it manually from within the document ([newWindow setDocument:self]), everything works: registering the undo/redo actions, the menu bar, etc.
In the following blog post the author describes the need to store page state, e.g. the text within a TextBox control, in the Page State dictionary so that it is restored when navigating between pages:
http://www.wintellect.com/CS/blogs/jgarland/archive/2011/01/26/a-matter-of-state-part-1.aspx
However, I have created a very simple application that has one page with a Button and a TextBlock and a ListBox of items. The button navigates to a dummy page, via NavigationService.Navigate. Now, if I scroll the list and input some text into my TextBox, navigate to the dummy page, then hit the back-button, I can see that my text is still present in the TextBox and the scroll position was preserved.
My question is, (tombstoning aside) do I ever need to persist the state of UI controls when simply navigating between them? It would appear that the frameowkr does this for me (despite the blog post above!).
You should persist state if it makes sense in the context of your application and will be helpful to the user.
This almost certainly means when tombstoning but probably not when the application is closed via the back button and then restarted.
In your scenario, the scroll position and text will be lost on tombstoning so you probably want to save these details.
Saving state is only relevant in the context of tombstoning and launching new instances of an application so (a tiny number of exceptional cases aside—and it doesn't sound like you are one of them) it doesn't make sense to talk about saving state outside of this.