refresh NSMenuItem on click/open of NSStatusItem - swift

I have the following extension where I have a NSMenuItem with it's state being either on or off:
extension AppDelegate {
func createStatusBarItem() {
let sBar = NSStatusBar.system
// create status bar item in system status bar
sBarItem = sBar.statusItem(withLength: NSStatusItem.squareLength)
...
let sBarMenu = NSMenu(title: "Options")
// assign menu to status bar item
sBarItem.menu = sBarMenu
let enableDisableMenuItem = NSMenuItem(title: "Enabled", action: #selector(toggleAdvancedMouseHandlingObjc), keyEquivalent: "e")
enableDisableMenuItem.state = sHandler.isAdvancedMouseHandlingEnabled() ? NSControl.StateValue.on : NSControl.StateValue.off
sBarMenu.addItem(enableDisableMenuItem)
...
}
#objc func toggleAdvancedMouseHandlingObjc() {
if sHandler.isAdvancedMouseHandlingEnabled() {
sHandler.disableAdvancedMouseHandling()
} else {
sHandler.enableAdvancedMouseHandling()
}
}
}
As soon as I want to change the state of the sHandler object I would also like to refer this change to the NSMenuItem and enable or disable the check mark depending on the state of NSHandler.
However it looks like the menu is being built only at first launch. How do I re-trigger the menu item in order to show or not show the check mark?

Keep a reference to the created NSMenuItem in your app delegate and update its state (assuming you use the item only in a single menu).
class AppDelegate: NSApplicationDelegate {
var fooMenuItem: NSMenuItem?
}
func createStatusBarItem() {
...
let enableDisableMenuItem = NSMenuItem(title: "Enabled", action: #selector(toggleAdvancedMouseHandlingObjc), keyEquivalent: "e")
self.fooMenuItem = enableDisableMenuItem
...
}
#objc func toggleAdvancedMouseHandlingObjc() {
if sHandler.isAdvancedMouseHandlingEnabled() {
sHandler.disableAdvancedMouseHandling()
} else {
sHandler.enableAdvancedMouseHandling()
}
self.fooMenuItem.state = sHandler.isAdvancedMouseHandlingEnabled() ? NSControl.StateValue.on : NSControl.StateValue.off
}

Related

Why would NSWindowController return nil-value window property?

I'm using modal sheets (slide down from top) to get user input. I currently have 2 that I think are identical except for the UI, each a NIB + NSWindowController-subclass pair. One works as expected, binding input to an array controller and table view. When trying to use the other, the window property of the NSWindowController is nil.
This code works:
#IBAction func addItemButtonClicked(_ button: NSButton) {
let window = document?.windowForSheet
let windowController = NewItemSheetController()
windowController.typeChoices = newItemSheetTypeChoices
windowController.windowTitle = newItemSheetTitle
print(#function, windowController.window) // output below
window?.beginSheet(windowController.window!, completionHandler: { response in
// The sheet has finished. Did user click OK?
if response == NSApplication.ModalResponse.OK {
let structure = (self.newItemSheetController?.structure)!
self.document?.dataSource.structures.append(structure)
}
// All done with window controller.
self.newItemSheetController = nil
})
newItemSheetController = windowController
}
The output of the print statement: "addItemButtonClicked(_:) Optional()"
This code doesn't:
#IBAction func addItemButtonClicked(_ button: NSButton) {
let window = document?.windowForSheet
let windowController = NewRecurrenceItemSheetController()
windowController.windowTitle = newItemSheetTitle
print(#function, windowController.window)
window?.beginSheet(windowController.window!, completionHandler: { response in
// The sheet has finished. Did user click OK?
if response == NSApplication.ModalResponse.OK {
let recurrence = (self.newItemSheetController?.recurrence)!
self.document?.dataSource.recurrences.append(recurrence)
}
// All done with window controller.
self.newItemSheetController = nil
})
newItemSheetController = windowController
}
The output of the print statement: "addItemButtonClicked(_:) nil"
Classes NewItemSheetController and NewRecurrenceItemSheetController are subclasses of NSWindowController and differ only with NSNib.Name and properties related to differing UI. As far as I can see, the XIBs and Buttons are "wired" similarly. The XIBs use corresponding File's Owner. Window objects have default class.
#objcMembers
class NewItemSheetController: NSWindowController {
/// other properties here
dynamic var windowTitle: String = "Add New Item"
override var windowNibName: NSNib.Name? {
return NSNib.Name(stringLiteral: "NewItemSheetController")
}
override func windowDidLoad() {
super.windowDidLoad()
titleLabel.stringValue = windowTitle
}
// MARK: - Outlets
#IBOutlet weak var titleLabel: NSTextField!
#IBOutlet weak var typeChooser: NSPopUpButton!
// MARK: - Actions
#IBAction func okayButtonClicked(_ sender: NSButton) {
window?.endEditing(for: nil)
dismiss(with: NSApplication.ModalResponse.OK)
}
#IBAction func cancelButtonClicked(_ sender: NSButton) {
dismiss(with: NSApplication.ModalResponse.cancel)
}
func dismiss(with response: NSApplication.ModalResponse) {
window?.sheetParent?.endSheet(window!, returnCode: response)
}
}
Why does one return instantiate a windowController object with a nil-valued window property?
In Interface Builder, the XIB Window needed to be attached to File's Owner with a Window outlet and delegate. Thanks #Willeke.

How to add `toggleSidebar` NSToolbarItem in Catalyst?

In my app, I added a toggleSidebar item to the NSToolbar.
#if targetEnvironment(macCatalyst)
extension SceneDelegate: NSToolbarDelegate {
func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
return [NSToolbarItem.Identifier.toggleSidebar, NSToolbarItem.Identifier.flexibleSpace, AddRestaurantButtonToolbarIdentifier]
}
}
#endif
However, when I compile my app to Catalyst, the button is disabled. Does anybody know what else I need to do to hook it up?
If you look at the documentation for .toggleSidebar/NSToolbarToggleSidebarItemIdentifier you will see:
The standard toolbar item identifier for a sidebar. It sends toggleSidebar: to firstResponder.
Adding that method to your view controller will enable the button in the toolbar:
Swift:
#objc func toggleSidebar(_ sender: Any) {
}
Objective-C:
- (void)toggleSidebar:(id)sender {
}
Your implementation will need to do whatever you want to do when the user taps the button in the toolbar.
Normally, under a real macOS app using an NSSplitViewController, this method is handled automatically by the split view controller and you don't need to add your own implementation of toggleSidebar:.
The target needs changed to self, this is shown in this Apple sample where it is done for the print item but can easily be changed to the toggle split item as I did after the comment.
/** This is an optional delegate function, called when a new item is about to be added to the toolbar.
This is a good spot to set up initial state information for toolbar items, particularly items
that you don't directly control yourself (like with NSToolbarPrintItemIdentifier).
The notification's object is the toolbar, and the "item" key in the userInfo is the toolbar item
being added.
*/
func toolbarWillAddItem(_ notification: Notification) {
let userInfo = notification.userInfo!
if let addedItem = userInfo["item"] as? NSToolbarItem {
let itemIdentifier = addedItem.itemIdentifier
if itemIdentifier == .print {
addedItem.toolTip = NSLocalizedString("print string", comment: "")
addedItem.target = self
}
// added code
else if itemIdentifier == .toggleSidebar {
addedItem.target = self
}
}
}
And then add the action to the scene delegate by adding the Swift equivalent of this:
- (IBAction)toggleSidebar:(id)sender{
UISplitViewController *splitViewController = (UISplitViewController *)self.window.rootViewController;
[UIView animateWithDuration:0.2 animations:^{
splitViewController.preferredDisplayMode = (splitViewController.preferredDisplayMode != UISplitViewControllerDisplayModePrimaryHidden ? UISplitViewControllerDisplayModePrimaryHidden : UISplitViewControllerDisplayModeAllVisible);
}];
}
When configuring your UISplitViewController, set the primaryBackgroundStyle to .sidebar
let splitVC: UISplitViewController = //your application's split view controller
splitVC.primaryBackgroundStyle = .sidebar
This will enable your NSToolbarItem with the system identifier .toggleSidebar and it will work automatically with the UISplitViewController in Mac Catalyst without setting any target / action code.
This answer is mainly converting #malhal's answer to the latest Swift version
You will need to return [.toggleSidebar] in toolbarDefaultItemIdentifiers.
In toolbarWillAddItem you will write the following (just like the previous answer suggested):
func toolbarWillAddItem(_ notification: Notification) {
let userInfo = notification.userInfo!
if let addedItem = userInfo["item"] as? NSToolbarItem {
let itemIdentifier = addedItem.itemIdentifier
if itemIdentifier == .toggleSidebar {
addedItem.target = self
addedItem.action = #selector(toggleSidebar)
}
}
}
Finally, you will add your toggleSidebar method.
#objc func toggleSidebar() {
let splitController = self.window?.rootViewController as? MainSplitController
UIView.animate(withDuration: 0.2) {
splitController?.preferredDisplayMode = (splitController?.preferredDisplayMode != .primaryHidden ? .primaryHidden : .allVisible)
}
}
A few resources that might help:
Integrating a Toolbar and Touch Bar into Your App
Mac Catalyst: Adding a Toolbar
The easiest way to use the toggleSidebar toolbar item is to set primaryBackgroundStyle to .sidebar, as answered by #Craig Scrogie.
That has the side effect of enabling the toolbar item and hiding/showing the sidebar.
If you don't want to use the .sidebar background style, you have to implement toggling the sidebar and validating the toolbar item in methods on a class in your responder chain. I put these in a subclass of UISplitViewController.
#objc func toggleSidebar(_ sender: Any?) {
UIView.animate(withDuration: 0.2, animations: {
self.preferredDisplayMode =
(self.displayMode == .secondaryOnly) ?
.oneBesideSecondary : .secondaryOnly
})
}
#objc func validateToolbarItem(_ item: NSToolbarItem)
-> Bool {
if item.action == #selector(toggleSidebar) {
return true
}
return false
}

NSTouchBar integration not calling

I am integrating TouchBar support to my App. I used the how to from Rey Wenderlich and implemented everything as follows:
If self.touchBarArraygot filled the makeTouchBar() Method returns the NSTouchBar object. If I print out some tests the identifiers object is filled and works.
What not work is that the makeItemForIdentifier method not get triggered. So the items do not get created and the TouchBar is still empty.
Strange behavior: If I add print(touchBar) and a Breakpoint before returning the NSTouchBar object it works and the TouchBar get presented as it should (also the makeItemForIdentifier function gets triggered). Even if it disappears after some seconds... also strange.
#available(OSX 10.12.2, *)
extension ViewController: NSTouchBarDelegate {
override func makeTouchBar() -> NSTouchBar? {
if(self.touchBarArray.count != 0) {
let touchBar = NSTouchBar()
touchBar.delegate = self
touchBar.customizationIdentifier = NSTouchBarCustomizationIdentifier("com.TaskControl.ViewController.WorkspaceBar")
var identifiers: [NSTouchBarItemIdentifier] = []
for (workspaceId, _) in self.touchBarArray {
identifiers.append(NSTouchBarItemIdentifier("com.TaskControl.ViewController.WorkspaceBar.\(workspaceId)"))
}
touchBar.defaultItemIdentifiers = identifiers
touchBar.customizationAllowedItemIdentifiers = identifiers
return touchBar
}
return nil
}
func touchBar(_ touchBar: NSTouchBar, makeItemForIdentifier identifier: NSTouchBarItemIdentifier) -> NSTouchBarItem? {
if(self.touchBarArray.count != 0) {
for (workspaceId, data) in self.touchBarArray {
if(identifier == NSTouchBarItemIdentifier("com.TaskControl.ViewController.WorkspaceBar.\(workspaceId)")) {
let saveItem = NSCustomTouchBarItem(identifier: identifier)
let button = NSButton(title: data["name"] as! String, target: self, action: #selector(self.touchBarPressed))
button.bezelColor = NSColor(red:0.35, green:0.61, blue:0.35, alpha:1.00)
saveItem.view = button
return saveItem
}
}
}
return nil
}
}
self.view.window?.makeFirstResponder(self) in viewDidLoad() did solve the problem.

How do I change/modify the displayed title of an NSPopUpButton

I would like an NSPopUpButton to display a different title than the title of the menu item that is selected.
Suppose I have an NSPopUpButton that lets the user pick a list of currencies, how can I have the collapsed/closed button show only the currencies abbreviation instead of the menu title of the selected currency (which is the full name of the currency)?
I imagine I can override draw in a subclass (of NSPopUpButtonCell) and draw the entire button myself, but I would prefer a more lightweight approach for now that reuses the system's appearance.
The menu items have the necessary information about the abbreviations, so that's not part of the question.
Subclass NSPopUpButtonCell, override drawTitle(_:withFrame:in:) and call super with the title you want.
override func drawTitle(_ title: NSAttributedString, withFrame frame: NSRect, in controlView: NSView) -> NSRect {
var attributedTitle = title
if let popUpButton = self.controlView as? NSPopUpButton {
if let object = popUpButton.selectedItem?.representedObject as? Dictionary<String, String> {
if let shortTitle = object["shortTitle"] {
attributedTitle = NSAttributedString(string:shortTitle, attributes:title.attributes(at:0, effectiveRange:nil))
}
}
}
return super.drawTitle(attributedTitle, withFrame:frame, in:controlView)
}
In the same way you can override intrinsicContentSize in a subclass of NSPopUpButton. Replace the menu, call super and put the original menu back.
override var intrinsicContentSize: NSSize {
if let popUpButtonCell = self.cell {
if let orgMenu = popUpButtonCell.menu {
let menu = NSMenu(title: "")
for item in orgMenu.items {
if let object = item.representedObject as? Dictionary<String, String> {
if let shortTitle = object["shortTitle"] {
menu.addItem(withTitle: shortTitle, action: nil, keyEquivalent: "")
}
}
}
popUpButtonCell.menu = menu
let size = super.intrinsicContentSize
popUpButtonCell.menu = orgMenu
return size
}
}
return super.intrinsicContentSize
}
Ok, so I found out how I can modify the title. I create a cell subclass where I override setting the title based on the selected item. In my case I check the represented object as discussed in the question.
final class MyPopUpButtonCell: NSPopUpButtonCell {
override var title: String! {
get {
guard let selectedCurrency = selectedItem?.representedObject as? ISO4217.Currency else {
return selectedItem?.title ?? ""
}
return selectedCurrency.rawValue
}
set {}
}
}
Then in my button subclass I set the cell (I use xibs)
override func awakeFromNib() {
guard let oldCell = cell as? NSPopUpButtonCell else { return }
let newCell = MyPopUpButtonCell()
newCell.menu = oldCell.menu
newCell.bezelStyle = oldCell.bezelStyle
newCell.controlSize = oldCell.controlSize
newCell.autoenablesItems = oldCell.autoenablesItems
newCell.font = oldCell.font
cell = newCell
}
A downside though is that I have to copy over all attributes of the cell I configured in Interface Builder. I can of course just set the cell class in Interface Builder, which makes this superfluous.
One thing I haven't figured out yet is how I can have the button have the correct intrinsic content size now. It still tries to be as wide as the longest regular title.
And the second thing I haven't figured out is how to make this work with bindings. If the buttons content is provided via Cocoa Bindings then I can only bind the contentValues, and the cell's title property is never called.

Selector() with parameter in Swift + NSMenuItem

I'm currently trying to list all keys in a Keychain as NSMenuItems and when I click one, I want it to call a function with a String parameter BUT
with my current code every key gets removed when I run my app not only the key I click on.
This is my current code:
NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
let menu = NSMenu()
let internetKeychain = Keychain(server: "example.com", protocolType: .https, authenticationType: .htmlForm)
func applicationDidFinishLaunching(_ aNotification: Notification) {
for key in internetKeychain.allKeys() {
menu.addItem(NSMenuItem(title: "🚮 \(key)", action: Selector(deleteKey(key: "\(key)")), keyEquivalent: ""));
}
if let button = statusItem.button {
button.title = "🔑"
button.target = self }
statusItem.menu = menu
NSApp.activate(ignoringOtherApps: true)
}
func deleteKey(key: String) -> String {
do {
try addInternetPasswordVC().internetKeychain.remove("\(key)")
print("key: \(key) has been removed")
} catch let error {
print("error: \(error)") }
refreshMenu()
return key
}
...
}
I suspect
Option 1: Selectors accept functions with parameters (or just in some extent)
Option 2: I made a little mistake in the function in the first or last line.
The signature of a target / action method takes either no parameter or passes the affected item (in this case the NSMenuItem instance) and I doubt that it can return anything.
menu.addItem(NSMenuItem(title: "🚮 \(key)", action: #selector(deleteKey(_:)), keyEquivalent: ""));
...
func deleteKey(_ sender: NSMenuItem) {
do {
let key = sender.title.substring(from: sender.title.range(of: " ")!.upperBound)
try addInternetPasswordVC().internetKeychain.remove("\(key)")
print("key: \(key) has been removed")
refreshMenu()
} catch let error {
print("error: \(error)")
}
}
PS: To call refreshMenu() is only useful when the key is removed I guess.