// // MIT License // // Copyright (c) 2022 Lukas Romsicki // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // // Source Code: https://github.com/lfroms/fluid-menu-bar-extra // Some parts of the code were altered to fit the needs of this application. // Any modifications that were made are still licensed under the author's // original license (aka MIT license). Contents of this file are excluded // from the coverage of the license being applied to the rest of the code. // import AppKit import os final class CBMenuBarItem: NSObject, NSWindowDelegate { private var statusItem: NSStatusItem var panel: NSPanel private var localEventMonitor: EventMonitor? private var localDragEventMonitor: EventMonitor? private var globalEventMonitor: EventMonitor? override init() { self.statusItem = NSStatusBar.system .statusItem(withLength: NSStatusItem.variableLength) self.statusItem.isVisible = true self.panel = PopoverPanel(viewController: CmdViewController()) self.statusItem.button?.font = NSFont .monospacedSystemFont(ofSize: NSFontDescriptor .preferredFontDescriptor(forTextStyle: .body).pointSize, weight: .regular) super.init() // Events // Shows panel and keeps the button highlighted. localEventMonitor = LocalEventMonitor(mask: [.leftMouseDown]) { [weak self] event in if let button = self?.statusItem.button, event.window == button.window, !event.modifierFlags.contains(.command) { self?.statusButtonPressed(button) return nil } return event } // On click and drag, the button should panel should desappear and // un-highlight. localDragEventMonitor = LocalEventMonitor(mask: [.leftMouseDragged, .keyDown]) { [weak self] event in let modifiers = event.modifierFlags.rawValue if let panel = self?.panel, panel.isKeyWindow { if modsContains(keys: OSCmd, in: modifiers) && event.type == .leftMouseDragged { self?.panel.resignKey() } } return event } // NOTE: The need for this seems a bit questionable. Maybe, this // works properly without global event monitor in MacOS Sequoia? // Resign key whenever clicking outside of panel, thus hiding the // panel and un-highlighting the button. // In order to receive events, the program needs to be codesigned. globalEventMonitor = GlobalEventMonitor(mask: [.leftMouseDown, .rightMouseDown]) { [weak self] event in if let panel = self?.panel, panel.isKeyWindow { panel.resignKey() } } if let statusButton = statusItem.button, let statusButtonWindow = statusButton.window { statusButtonWindow.delegate = self } self.panel.delegate = self localEventMonitor?.start() localDragEventMonitor?.start() } deinit { persistMenuBar(false) localEventMonitor?.stop() localDragEventMonitor?.stop() globalEventMonitor?.stop() panel.close() NSStatusBar.system.removeStatusItem(statusItem) } @objc private func statusButtonPressed(_ sender: NSButton) { if panel.isVisible { panel.resignKey() return } showPanel() } private func showPanel() { setButtonHighlighted(to: true) persistMenuBar(true) setPanelPosition() panel.makeKeyAndOrderFront(nil) } private func dismissPanel() { NSAnimationContext.runAnimationGroup { context in context.duration = 0.3 context.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) panel.animator().alphaValue = 0 } completionHandler: { [weak self] in self?.panel.orderOut(nil) self?.panel.alphaValue = 1 self?.setButtonHighlighted(to: false) persistMenuBar(false) } } private func setPanelPosition() { guard let scrn = NSScreen.main, let iFrame = statusItem.button?.window?.frame else { return } var x = iFrame.origin.x let y = scrn.visibleFrame.height + scrn.visibleFrame.origin.y if (iFrame.origin.x + panel.frame.width) > (scrn.visibleFrame.origin.x + scrn.visibleFrame.width) { x = iFrame.origin.x + iFrame.width - panel.frame.width } if (iFrame.origin.x - panel.frame.width) < (scrn.visibleFrame.origin.x) { x = scrn.visibleFrame.origin.x } panel.setFrameTopLeftPoint(NSPoint(x: x, y: y)) } private func setButtonHighlighted(to highlight: Bool) { self.statusItem.button!.highlight(highlight) } func setImage(title: String?, description: String) { if title != nil { statusItem.button!.image = NSImage(systemSymbolName: title!, accessibilityDescription: description) return } statusItem.button!.image = nil } func setTitle(_ title: String) { statusItem.button!.title = title } func setAutosaveName(_ name: String) { statusItem.autosaveName = name } func setContents(to text: String) { guard let viewContoller = panel.contentViewController as? CmdViewController else { return } viewContoller.setText(text) } func setFile(_ file: CmdFile) { guard let viewContoller = panel.contentViewController as? CmdViewController else { return } viewContoller.setFile(file) } func windowDidResize(_ notification: Notification) { if let cmdPanel = notification.object as? NSPanel, panel == cmdPanel, cmdPanel.isVisible { setPanelPosition() } } func windowDidMove(_ notification: Notification) { if let statusBarButtonWindow = notification.object as? NSWindow, let buttonWindow = statusItem.button?.window, buttonWindow == statusBarButtonWindow, panel.isVisible { setPanelPosition() } } func windowDidBecomeKey(_ notification: Notification) { globalEventMonitor?.start() } func windowDidResignKey(_ notification: Notification) { globalEventMonitor?.stop() dismissPanel() } } // MARK: - Metrics fileprivate enum Metrics { static let windowBorderSize: CGFloat = 2 }