// // MenuButton.swift // MacBackend // // Created by david-swift on 22.10.23. // import AppKit /// A button widget for menus. public struct MenuButton: MenuWidget { /// The button's label. var label: String /// The button's action handler. var handler: () -> Void /// The keyboard shortcut. var shortcut: KeyboardShortcut? /// Whether the button is selected. var selected: Bool? /// Whether the button is enabled. var enabled = true /// The action label. var filteredLabel: String { label.filter { $0.isLetter || $0.isNumber || $0 == "-" || $0 == "." } } /// Initialize a menu button. /// - Parameters: /// - label: The buttons label. /// - handler: The button's action handler. public init(_ label: String, handler: @escaping () -> Void) { self.label = label self.handler = handler } /// The view storage. /// - Parameters: /// - data: The widget data. /// - type: The type of the views. /// - Returns: The view storage. public func container( data: WidgetData, type: Data.Type ) -> ViewStorage where Data: ViewRenderData { let button = NSMenuItem() let storage = ViewStorage(button) update(storage, data: data, updateProperties: true, type: type) return storage } /// Update the stored content. /// - Parameters: /// - storage: The storage to update. /// - data: The widget data. /// - updateProperties: Whether to update the properties. /// - type: The type of the views. public func update( _ storage: ViewStorage, data: WidgetData, updateProperties: Bool, type: Data.Type ) where Data: ViewRenderData { guard let button = storage.pointer as? NSMenuItem else { return } if enabled { button.actionClosure = handler } else { button.action = nil } guard updateProperties else { return } let previousState = storage.previousState as? Self if previousState?.label != label { button.title = label } if let shortcut, previousState?.shortcut != shortcut { button.keyEquivalent = shortcut.character.macOSRepresentation button.keyEquivalentModifierMask = shortcut.modifiers } if let selected, previousState?.selected != selected { button.state = selected ? .on : .off } storage.previousState = self } /// Create a keyboard shortcut for an application from a button. /// /// Note that the keyboard shortcut is available after the view has been visible for the first time. /// - Parameter shortcut: The keyboard shortcut. /// - Returns: The button. public func keyboardShortcut(_ shortcut: KeyboardShortcut) -> Self { modify { $0.shortcut = shortcut } } /// Create a keyboard shortcut for an application from a button. /// /// Note that the keyboard shortcut is available after the view has been visible for the first time. /// - Parameter shortcut: The keyboard shortcut. /// - Returns: The button. public func keyboardShortcut(_ shortcut: Character) -> Self { modify { $0.shortcut = .init(shortcut) } } /// Whether the button is selected. /// - Parameter selected: Whether it is selected. /// - Returns: The button. public func selected(_ selected: Bool = true) -> Self { modify { $0.selected = selected } } /// Whether the button is enabled. /// - Parameter enabled: Whether it is enabled. /// - Returns: The button. public func enabled(_ enabled: Bool = true) -> Self { modify { $0.enabled = enabled } } } extension NSMenuItem { /// The closure key. private static var closureKey: UInt8 = 0 /// The action closure. var actionClosure: (() -> Void)? { get { objc_getAssociatedObject(self, &Self.closureKey) as? () -> Void } set { objc_setAssociatedObject(self, &Self.closureKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) self.target = self self.action = #selector(menuItemAction) } } /// The action closure. @objc private func menuItemAction() { actionClosure?() } }