239 lines
7.9 KiB
Swift
239 lines
7.9 KiB
Swift
//
|
|
// 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
|
|
}
|