prole/prole-app/Sources/StatusItemController.swift
chrisfu 197c72dd7a Refactor prole-app and establish temporary release process
- Moved prole-tools-app to prole-app at the project root to make it self-contained for transition to its own repository.
- Created prole-tools-app/dist/ directory to host build artifacts.
- Generated distribution artifacts (Prole Tools.app and Prole Tools.zip) using prole-app/build.sh package.
- Checked in the generated artifacts to prole-tools-app/dist/ (bypassing .gitignore for temporary release process).
Changes Summary
•
Renamed directory prole-tools-app/ to prole-app/.
•
Populated prole-tools-app/dist/ with the latest build output from prole-app/build.sh.
•
Staged all changes, including the forced addition of ignored artifacts in prole-tools-app/dist/.
2026-01-18 15:04:23 -08:00

134 lines
4.7 KiB
Swift

import AppKit
final class StatusItemController {
struct Actions {
let onLeftClick: () -> Void
let onToggleStatusBar: () -> Void
let onToggleAppWindow: () -> Void
let onRefresh: () -> Void
let onQuit: () -> Void
}
private var statusItem: NSStatusItem?
private let actions: Actions
private var stateStatusBarVisible: Bool = true
private var stateAppWindowVisible: Bool = false
init(actions: Actions) {
self.actions = actions
createStatusItemIfNeeded()
}
func setState(statusBarVisible: Bool, appWindowVisible: Bool) {
self.stateStatusBarVisible = statusBarVisible
self.stateAppWindowVisible = appWindowVisible
}
func hideStatusItem() {
if let item = statusItem {
NSStatusBar.system.removeStatusItem(item)
statusItem = nil
}
}
func showStatusItem() {
createStatusItemIfNeeded()
}
var isVisible: Bool { statusItem != nil }
private func createStatusItemIfNeeded() {
guard statusItem == nil else { return }
let item = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
statusItem = item
if let button = item.button {
// Use a proper template image so it always tints/appears in light/dark menu bar
button.image = StatusItemController.makeTemplateGlyphImage("P")
button.imagePosition = .imageOnly
button.target = self
button.action = #selector(handleClick)
// Only respond to left click; right click intentionally does nothing
button.sendAction(on: [.leftMouseUp])
button.toolTip = "Prole Tools — Click for menu"
button.appearsDisabled = false
}
}
@objc private func handleClick(_ sender: Any?) {
// Always open the context menu on left click
showMenu()
}
private func showMenu() {
guard let button = statusItem?.button else { return }
let menu = NSMenu(title: "ProleStatus")
let statusBarTitle = stateStatusBarVisible ? "Hide Status Bar Icon" : "Show Status Bar Icon"
let appWindowTitle = stateAppWindowVisible ? "Hide Main Window" : "Show Main Window"
let sbItem = NSMenuItem(title: statusBarTitle, action: #selector(toggleStatusBar), keyEquivalent: "")
sbItem.target = self
menu.addItem(sbItem)
let winItem = NSMenuItem(title: appWindowTitle, action: #selector(toggleAppWindow), keyEquivalent: "")
winItem.target = self
menu.addItem(winItem)
menu.addItem(.separator())
let refreshItem = NSMenuItem(title: "Refresh Now", action: #selector(refreshNow), keyEquivalent: "r")
refreshItem.target = self
menu.addItem(refreshItem)
menu.addItem(.separator())
let quitItem = NSMenuItem(title: "Quit Prole Tools", action: #selector(quitApp), keyEquivalent: "q")
quitItem.target = self
menu.addItem(quitItem)
let point = NSPoint(x: 0, y: button.bounds.minY - 3)
menu.popUp(positioning: nil, at: point, in: button)
}
@objc private func toggleStatusBar() { actions.onToggleStatusBar() }
@objc private func toggleAppWindow() { actions.onToggleAppWindow() }
@objc private func refreshNow() { actions.onRefresh() }
@objc private func quitApp() { actions.onQuit() }
}
// MARK: - Icon drawing
extension StatusItemController {
/// Create a monochrome template image from a single-character glyph to use as a status bar icon.
/// The image is marked as template so macOS tints it appropriately.
fileprivate static func makeTemplateGlyphImage(_ char: Character) -> NSImage? {
let size = NSSize(width: 18, height: 18)
let image = NSImage(size: size)
image.lockFocus()
defer { image.unlockFocus() }
// Clear background (transparent)
NSColor.clear.set()
NSBezierPath(rect: NSRect(origin: .zero, size: size)).fill()
// Draw the glyph centered
let string = String(char)
let font = NSFont.monospacedSystemFont(ofSize: 14, weight: .bold)
let attrs: [NSAttributedString.Key: Any] = [
.font: font,
.foregroundColor: NSColor.black // template ignores color; use black base
]
let attributed = NSAttributedString(string: string, attributes: attrs)
let textSize = attributed.size()
let rect = NSRect(
x: (size.width - textSize.width) / 2.0,
y: (size.height - textSize.height) / 2.0,
width: textSize.width,
height: textSize.height
)
attributed.draw(in: rect)
image.isTemplate = true
return image
}
}