How to get multiple windows to perform the same menu item action? - swift

I have a Cocoa app which supports multiple windows.
I already know how to get the main window to perform actions for any of the NSMenuItem from NSMenu and including the NSToolbarItem from the NSToolbar.
class MainWindowController: NSWindowController {
// ...
#IBAction func doSomethingIncredible(_ sender: Any?) {
// ...
}
}
That is easy for a single window app but my app supports multiple windows.
But how can I get other windows apart from the main window to access and even validate the NSMenuItem from NSMenu?
An example of this is Safari. It supports multiple windows. You can select Open Location...(⌘L), New Tab (⌘T), Show Sidebar (⇧⌘L) etc. It does the action on the focused window. It is not tied to the main window. It can be done on any windows. How can I do this for my app?
class AnotherWindowController: NSWindowController {
// ...
#IBAction func doSomethingIncredible(_ sender: Any?) {
// ...
}
}
How can the MainWindowController and the AnotherWindowController perform the same action from one of the NSMenuItem from NSMenu when one of them is the key focused window?

Rather than hard-connecting the menu item to a specific controller connect it to the First Responder (the red cube).
The first object in the responder chain which responds to the seclector – usually the frontmost window – catches the action and executes it.

Related

NSEvent.addLocalMonitorForEvents not working in development in app without a Window (building a macOS menu bar app)

I have a simple app that lives in the menu bar that I'm looking to listen to a return key press when the user is in the browser (or anywhere, right now I just want it to fire). Adding a window to applicationDidFinishLaunching and having that window selected registers the keyDown events but in actual use, the app won't have a UI and will just listen from the menu bar
When I remove the window, the keyDown function never fires.
Unsure of what I'm doing incorrectly here. Would love a hand. Thanks!
func applicationDidFinishLaunching(_ aNotification: Notification) {
NSEvent.addLocalMonitorForEvents(matching: .keyDown, handler: keyDown)
}
func keyDown(event: NSEvent) -> NSEvent {
NSLog("key down is \(event.keyCode)");
return event
}
Do I need to add this app to Accessibility settings? If so, I can't find the app to do so after a build and run.

How to override Copy and Paste NSMenuItems for one View Controller Swift macOS

I am writing a macOS application with multiple view controllers, using Storyboards.
In my main View Controller I would like to be able to copy and paste data to the NSPasteboard. The data is related to buttons displayed to the user, and the exact data to be copied varies depending on which button has most recently been pressed/selected.
I would like to be able to override the standard behaviour of the Copy and Paste NSMenuItems when my main View Controller is the front most (key) window, but revert to back to standard behaviour when other windows are in the foreground, as they all contain NSTextFields which can be copied/pasted into.
I have done a lot of googling, and overriding this behaviour is not very well documented. I can achieve it globally by adding an IBAction into the App Delegate, which I could use to call a function in whichever View Controller is key, but this doesn't feel like a very elegant solution.
Currently my IBAction in the App Delegate looks like this:
#IBAction func copy(_ sender: Any) {
if let window = NSApplication.shared.keyWindow {
if let splitView = window.contentViewController as? SplitViewController {
if let controlVC = splitView.controlItem.viewController as? ControlViewController {
controlVC.copyAction(self)
}
}
}
}
Am I missing a neater solution?
Thanks,
Dan

Being notified when the application menu item of an NSApplication's mainMenu is being highlighted

I've got a status bar app. The user can check an option such that the status bar is not used ; in that case, the app will launch 'normally', having a dock icon and a typical main menu.
Thing is, I need to know when the application menu is being highlighted. The application menu is that item in the application's main menu that has the application's name as its title.
I've tried to assign a target and action as well as a delegate, but neither the action or the delegate methods are being called. There's no documentation about this anywhere. Any idea what's happening?
There's no documentation about this anywhere
There is one: NSMenuDelegate
Extend AppDelegate to conform to NSMenuDelegate and implement menuWillOpen, replace <name of application> with the application name
extension AppDelegate : NSMenuDelegate {
func menuWillOpen(_ menu: NSMenu) {
if menu.title == "<name of application>" {
print("application menu will open")
}
}
}
In Interface Builder connect the delegate property of the application menu to AppDelegate

How can I close a Safari App Extension popover programmatically?

I'm building a Safari App Extension using XCode 8.3 and Swift 3, following the Safari App Extension Programming Guide. The extension includes a popover that appears when the extension's toolbar item is clicked. The popover view contains a few buttons linked to actions the user can perform.
I want clicking one of these buttons to close the popover after its action has been performed. By default, clicking anywhere outside of a popover closes it, but I haven't been able to find any other way to close the popover, either in the guide or in the docs.
I know that NSPopover has a performClose method, but there doesn't appear to be a way to access the popover itself from within the extension: the app extension only lets you provide a SFSafariExtensionViewController, whose contents magically appear within the popover.
I've also tried using dismissViewController as described in this StackOverflow answer, but in my view controller self.presenting is always nil, and self.dismissViewController(self) just crashes the extension with the message:
dismissViewController:: Error: maybe this view controller was not presented?.
Lastly, I noticed a related question about programmatically opening the toolbar item popover has gone unanswered the past 6 months. This leads me to suspect Apple may simply have strict limits on how the popover can be opened and closed. Even if this is the case, it would be nice to know for sure what the limitations are.
I'll add an answer in case anyone stumbles upon this question.
A dissmissPopover() instance method has been added to the SFSafariExtensionViewController class. This can be used to programatically close the popover.
The default template given when creating a Safari App Extension in XCode gives you a SafariExtensionViewController class that extends SFSafariExtensionViewController and holds a shared instance as a static field called 'shared', so you can call the dismissPopover() method from that instance.
For example:
class SafariExtensionHandler: SFSafariExtensionHandler {
func myFunc() {
// do stuff;
SafariExtensionViewController.shared.dismissPopover()
// do other stuff;
}
}
I did it by calling dismiss method like below
#IBAction func onLoginBtnClicked (_ sender: Any) {
NSLog("Button clicked")
self.dismiss(self)
}

Use NSPanel for user input. Not opening up again

I want to display a 'NSPanel' for the user to input a name for a new folder. Why a NSPanel? Because it looks awesome! It hosts one TextField and one PushButton to confirm the name. It shall also close the window when clicked.
It displays when the "add" button gets clicked in my menu. It also closes when the "done" button gets clicked in the NSPanel. But when I click "add" again it doesn't show up anymore. That also occurs when I close it via the normal "close button" in the title bar. So it is not explicitly related to the "done"-PushButton. I also tested implementing func windowWillClose(notification: NSNotification) which also doesn't get triggered in either cases. What could be the problem? Also, does it somehow need to be a "new" window every time? Or am I using this correctly for user input? I mean it just gets instantiated once and then "shown" and "unshown" or am I wrong?
So I did a new Cocoa-Class - Subclass of NSWindowController - and let xCode create a .xib for that also. In that .xib I "designed" the NSPanel. I ticked visible at launch without that the window wouldn't appear when the menu button gets clicked. I also hooked up an IBOutlet for the NSPanelin my Cocoa Class. My Class at the moment looks like this:
import Cocoa
class NamingHUD: NSWindowController, NSWindowDelegate {
#IBOutlet var insertNameWindow: NSPanel!
#IBOutlet weak var nameTextField: NSTextField!
override var windowNibName : String! {
return "NamingHUD"
}
override func windowDidLoad() {
super.windowDidLoad()
insertNameWindow.center()
insertNameWindow.makeKeyAndOrderFront(nil)
NSApp.activateIgnoringOtherApps(true)
}
#IBAction func userSetName(sender: NSButton) {
print("Close button clicked")
insertNameWindow.close()
}
}
In my Main Class I declared it as a variable like this:
var namingHUD:NamingHUD!
and then in override func awakeFromNib() as:
namingHUD = NamingHUD()
as well as in a click handler like:
#IBAction func addClicked(sender: NSMenuItem) {
namingHUD.showWindow(nil)
}
Now. When I click and addClicked() gets called the window shows up as expected. Fine! I enter a name and hit the "done" button and it closes the window properly. Also Fine! But when I click again, say to add another folder, the window doesn't show up anymore. I also created a Preferences Window the exact same way. But with a Window instead of a NSPanel inside. That totally works as it should.
So I clearly confused something or forget something. What could it be? I openly admit that it is the first time I am working with any kind of window outside of following a tutorial. So I clearly didn't grasp the whole concept of it. I read up about windows in Apples Developer Guide and it kinda makes sense. But... well, doesn't work at the moment. Am I "misusing" the NSPanel? Shouldn't be the case as it inherits from NSWindow or?
Did you connect the window outlet of NamingHUD to your awesome panel? Nibs are loaded lazily:
namingHUD = NamingHUD() // init the controller but doesn't load the nib
...
namingHUD.showWindow(nil) // now you are loading it for the first time
It works the first time because showWindow() loads the nib and show the window referenced by the window outlet. Your panel shows up because it's set to "Visible at launch". Your of course had no window to show.
Subsequent clicks don't load the nib file again, only order the window outlet to show up. That's why your panel did not show again. FYI: an NSPanel is a subclass of NSWindow so it has everything that NSWindow has, and then some more.