Cocoa Inspector - How to Reset Window Title After Closing Last Document - cocoa

My document-based cocoa app has a shared inspector window whose contents change depending on which document is active.
The inspector window controller is a shared singleton, instantiated form its storyboard on demand.
The document class simply creates its main window from a storyboard, and becomes the window's delegate:
class Document: NSDocument {
override func makeWindowControllers() {
let storyboard = NSStoryboard(name: "Main", bundle: nil)
guard let windowController = storyboard.instantiateController(withIdentifier: "Document Window Controller") as? NSWindowController else {
fatalError("Storyboard Inconsistency!")
}
windowController.window?.delegate = self
self.addWindowController(windowController)
}
Whenever a document's main window becomes active, it adds the inspector's window controller to its own:
extension Document: NSWindowDelegate {
func windowDidBecomeMain(_ notification: Notification) {
self.addWindowController(InspectorWindowController.shared)
}
}
(this also updates the window controller's document property)
In anticipation to the case where the last document is closed, I also added:
func windowWillClose(_ notification: Notification) {
self.removeWindowController(InspectorWindowController.shared)
}
(This is only needed for the last document, since otherwise the new active document takes over and the window controller is automatically removed from the closing document once it is added to the newly activated document)
The Inspector itself overrides the property document and the method windowTitle(forDocumentDisplayName:), in order to keep up with the active document:
class InspectorWindowController
override var document: AnyObject? {
didSet {
// (Update view controller's contents)
}
}
override func windowTitle(forDocumentDisplayName displayName: String) -> String {
if document == nil {
return "Inspector - No Active Document"
} else {
return "Inspector - \(displayName)"
}
}
The problem is, when I close the last open document window, the inspector's window title stays at the (custom) title set for the last document. That is, when the inspector window controller's document property is set to nil, windowTitle(forDocumentDisplayName:) is not called.
Even calling synchronizeWindowTitleWithDocumentName() does not help, since the docs clearly mention that:
Does nothing if the window controller has no associated document or
loaded window. This method queries the window controller’s document to
get the document’s display name and full filename path, then calls
windowTitle(forDocumentDisplayName:) to get the display name to show
in the window title.
(emphasis mine)
I can reset the Inspector's content to the "No document" state; How can I do the same for the window title?

OK, I found the (silly and obvious) answer:
override var document: AnyObject? {
didSet {
// (Update view controller's contents, etc...)
if document == nil {
self.window?.title = "Inspector - No Active Document"
}
}
}
I'm not sure if this is the correct way of dealing with it, but it does get the job done.
I will accept any better answer though.

Related

How to get state restoration to restore all windows/tabs on macOS?

I have a non-document based macOS AppKit app. It has one window instantiated automatically by the storyboard. I have sub-classed NSWindowController and added a override func newWindowForTab(_ sender: Any?) to enable the + button on the tab-bar. My main view controller lets the user rename the tab title and the window title is set to the same. This is kind of like how Xcode tab renaming works.
Additionally I have sub-classed NSWindow and added a restorableStateKeyPaths to ensure tab and window titles are automatically restored on app restart.
This all works great.
But only for the first tab. The main window is loaded and it has the tab and window titles set automatically.
The other tabs (windows) are not restored.
Any hints on what I miss to make all tabs restored?
My NSWindowController:
class MyWindowController: NSWindowController {
var subview: MyWindowController?
#IBAction override func newWindowForTab(_ sender: Any?) {
let story = self.storyboard
let windowVC = story?.instantiateInitialController() as! Self
window?.addTabbedWindow(windowVC.window!, ordered: .above)
subview = windowVC
windowVC.window?.orderFront(self.window)
windowVC.window?.makeKey()
}
}
My NSWindow:
class MyWindow: NSWindow {
override class var restorableStateKeyPaths: [String] {
return [ "self.tab.title", "self.title" ]
}
}
First you need to make sure that state restoration is enabled for your user, you can do this by going to Preferences->General and unchecking "Close windows when quitting an app".
Then you should use a restoration class in order to restore all open windows.
Basically if an NSWindow doesn't have a restoration class it won't be preserved across launches, that includes your storyboard loaded window. In this case what is happening is Cocoa is ignoring all window preservation because you haven't defined a restoration class for any of your windows so it resorts to its default behavior which is loading the initial storyboard controller.
Implementing restoration class is easy, just create a restoration class that inherits from NSObject and conforms to NSWindowRestoration, then implement its only required type method restoreWindow(identifier:state:completionHandler) like so:
class MyAppWindowRestoration: NSObject, NSWindowRestoration {
static func restoreWindow(withIdentifier identifier: NSUserInterfaceItemIdentifier,
state: NSCoder,
completionHandler: #escaping (NSWindow?, Error?) -> Void) {
// 1.- Retrieve and show the window
// Retrieve a new instance of the only window
let window = (NSStoryboard.main?.instantiateInitialController() as? NSWindowController)?.window
// Call the completion handler with the window and no errors
completionHandler(window, nil)
}
}
Then just assign this class as the window restoration class on every window you want restored, you can do this everywhere after the window has loaded:
window.restorationClass = MyAppWindowRestoration.self
Unfortunately Apple's documentation on state restoration completely sucks so if you have any more questions let me know ;)

Hide NSWindow New tab button

In macOS 10.12 there is a new tab bar that is added to NSWindows for NSDocument apps. You can prevent the toolbar from appearing (see How do I disable the Show Tab Bar menu option in Sierra apps?). But how to remove the "+" button for adding new Windows?
According to the AppKit release notes, returning false for responding newWindowForTab(_:) action message in a NSDocumentController subclass disables "+" button in the tab bar.
override func responds(to aSelector: Selector!) -> Bool {
if #available(OSX 10.12, *) {
if aSelector == #selector(NSResponder.newWindowForTab(_:)) {
return false
}
}
return super.responds(to: aSelector)
}
See "New Button" section in the AppKit Release Notes for macOS 10.12.
Depends of your Application functionality you may subclass NSDocumentController and return empty array for documentClassNames property.
class MyDocumentController: NSDocumentController {
override var documentClassNames: [String] {
return [] // This will disable "+" plus button in NSWindow tab bar.
}
}
Here is a documentation of the documentClassNames property:
documentClassNames
An array of strings representing the custom document classes supported by this app.
The items in the array are NSString objects, each of which represents the name of a document subclasses supported by the app. The document class names are derived from the app’s Info.plist. You can override this property and use it to return the names of document classes that are dynamically loaded from plugins.
Source
And here is explanation how documentClassNames property affects NSWindow tab bar plus button appearance:
New Button
The plus button will be shown if newWindowForTab: is implemented in the responder chain. NSDocumentController informally implements newWindowForTab:, but only returns YES from respondsToSelector: for this selector if the self.documentClassNames.count > 0 and if the app has a default new document type. In other words, it only responds to it if NSDocument has at least one registered document class name which can be edited.
Source
Just set ‘Tabbing Mode’ to Disallowed in Interface Builder for your NSWindow.
Change this
#IBAction override func newWindowForTab(_ sender: Any?) {}
into this
#IBAction func myButton(_ sender: Any?) {}
This will hide the plus button. The tabbing still works

NSProgressIndicator on Modal Sheet Doesn't Animate

My main window controller has a toolbar item that triggers the presentation of a modal sheet. The sheet is supposed to display the progress of a lengthy, asynchronous process (e.g., sync local data with a server).
However, I can not get the (indeterminate) progress indicator to animate.
This is the action that triggers the modal sheet:
var syncProgressWindowController: SyncProgressWindowController!
// ...
#IBAction func syncWithServer(_ sender: AnyObject) {
// (Actual HTTP code not implemented)
syncProgressWindowController = SyncProgressWindowController()
syncProgressWindowController.loadWindow()
guard let modalWindow = syncProgressWindowController.window else {
return
}
self.window?.beginSheet(modalWindow, completionHandler: { (response) in
// THIS GETS EXECUTED.
// However, the code below has no effect:
self.syncProgressWindowController.progressIndicator.startAnimation(self)
// self.syncProgressWindowController.progressIndicator is
// NOT nil, despite windowDidLoad() not being called
// (see below)
})
}
The modal sheet window controller (class SyncProgressWindowController above) is defined like this:
#IBOutlet weak var progressIndicator: NSProgressIndicator!
convenience init() {
self.init(windowNibName: "SyncProgressWindow")
}
override func windowDidLoad() {
super.windowDidLoad()
// Breakpoints here don't work, logs don't print to the console.
// Not called? But outlet _is_ set (see above).
}
The xib file (SyncProgressWindow.xib) has:
File's Owner Identity/Class set to "SyncProgressWindowController"
Window has New Referencing Outlet to File's Owner's window
Window has delegate outlet wired to "File's Owner" (just in case - but delegate methods don't seem to get called either).
Window has "Visible at Launch" unchecked (and is therefore displayed modally with no problems).
Progress has New Referencing Outlet wired to File's Owner's progressIndicator.
However:
SyncProgressWindowController's windowDidLoad() does not get called (Execution does not stop at breakpoints there and logs aren't printed).
Despite that, the property/outlet progressIndicator is set somehow, because the app does not crash when I attempt to animate it, with code like this:
self.syncProgressWindowController.progressIndicator.startAnimation(self)
What am I missing?
completionHandler will be fired when you close sheet by endSheet(_:returnCode:) So you start indicator before sheet will be closed.
I'm not good in xib files, but when i disabled row with loadWindow, windowDidLoad was called. I'm not sure it's right way.

Cocoa: How to set window title from within view controller in Swift?

I've tried to build on a Cocoa app which uses storyboard and Swift in Xcode 6. However, when I tried to alter the title of window from within NSViewController, the following code doesn't work.
self.title = "changed label"
When I wrote the above code in viewDidLoad() function, the resultant app's title still remains window.
Also, the following code causes an error, since View Controller doesn't have such property as window.
self.window.title = "changed label"
So how can I change the title of window programmatically in Cocoa app which is built on storyboard?
There are 2 problems with your code:
viewDidLoad is called before the view is added to the window
NSViewController does not have a window property
To fix the first one, you could override viewDidAppear(). This method is called after the view has fully transitioned onto the screen. At that point it is already added to a window.
To get a reference to the window title, you can access a view controller's window via its view: self.view.window.title
Just add the following to your view controller subclass, and the window title should change:
override func viewDidAppear() {
super.viewDidAppear()
self.view.window?.title = "changed label"
}
This worked for me, currentDict is NSDictionary passed from previous viewController
var currentDict:NSDictionary?
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
if let myString:String = currentDict?["title"] as? String {
self.title = myString
}
}

Is there a way to assign multiple key equivalents to a Menu Item in Cocoa (via IB or programmatically)?

Specifically, I want my "New" menu item to respond to both Cmd+N and Cmd+T since it will open a new document in a tab.* How can I do this either in Interface Builder or programmatically?
* I can explain the reasoning further if needed, but I'm hoping to avoid a discussion of the merits and rather focus on how to do it, not why to do it.
Make a second one (easiest way being to duplicate it) and set it as hidden. It won't show up when the user pulls open the menu, but as long as it's enabled, its key equivalents should still be in effect.
A simple way to have two or more Key Equivalents for an action is to duplicate the NSMenuItem and add a special Tag for these "alternatives" menu items.
Then set the AppDelegate the delegate (NSMenuDelegate) of the corresponding enclosing NSMenu (where the inner items need the visibility to be updated).
Hidden menu items (or items with a hidden superitem) do not appear in
a menu and do not participate in command key matching.
When the NSMenu open, hides this alternates NSMenuItem, when it close, display them.
Example in Swift 3:
class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
func applicationDidFinishLaunching(_ aNotification: Notification) {
NSApp.mainMenu?.item(withTitle: "View")?.submenu?.item(withTitle: "Zoom")?.submenu?.delegate = self
}
func toggleVisibility(_ visible: Bool, ofAlternatesKeyEquivalentsItems items: [NSMenuItem]) {
for item in items.filter({ $0.tag == 2 }) {
item.isHidden = !visible
}
}
func menuWillOpen(_ menu: NSMenu) {
if menu.title == "Zoom" {
toggleVisibility(false, ofAlternatesKeyEquivalentsItems: menu.items)
}
}
func menuDidClose(_ menu: NSMenu) {
if menu.title == "Zoom" {
toggleVisibility(true, ofAlternatesKeyEquivalentsItems: menu.items)
}
}
}

Resources