// // 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 Cocoa 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()) 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 if let panel = self?.panel, panel.isKeyWindow { if event.modifierFlags.contains(.command) && 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 statusItemView = statusItem.button?.window else { panel.center() return } var targetRect = statusItemView.frame if let screen = statusItemView.screen { let panelWidth = panel.frame.width if statusItemView.frame.origin.x + panelWidth > screen.visibleFrame.width { targetRect.origin.x += statusItemView.frame.width targetRect.origin.x -= panelWidth targetRect.origin.x += Metrics.windowBorderSize } else { targetRect.origin.x -= Metrics.windowBorderSize } } else { targetRect.origin.x -= Metrics.windowBorderSize } if targetRect.origin.x < 0 { targetRect.origin.x = 0 } panel.setFrameTopLeftPoint(targetRect.origin) } 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 { setPanelPosition() } } func windowDidMove(_ notification: Notification) { if let statusBarButtonWindow = notification.object as? NSWindow, let buttonWindow = statusItem.button?.window, buttonWindow == statusBarButtonWindow { setPanelPosition() } } func windowDidBecomeKey(_ notification: Notification) { globalEventMonitor?.start() } func windowDidResignKey(_ notification: Notification) { globalEventMonitor?.stop() dismissPanel() } } // MARK: - Metrics fileprivate enum Metrics { static let windowBorderSize: CGFloat = 2 }