| | |
| | | import SwiftUI |
| | | import AppKit |
| | | import Carbon |
| | | import ServiceManagement |
| | | |
| | | // MARK: - Application Entry Point |
| | | |
| | |
| | | private var isInEditMode = false // Suppress auto-dismiss during editing |
| | | |
| | | func applicationDidFinishLaunching(_ notification: Notification) { |
| | | // Use .accessory so the app appears in Force Quit (unlike LSUIElement) |
| | | NSApp.setActivationPolicy(.accessory) |
| | | L10n.setup() |
| | | let showDock = UserDefaults.standard.bool(forKey: "showDockIcon") |
| | | NSApp.setActivationPolicy(showDock ? .regular : .accessory) |
| | | setupMenuBar() |
| | | registerHotkey() |
| | | observeOtherWindows() |
| | | observeEditMode() |
| | | observeDockSetting() |
| | | setupLaunchAtLogin() |
| | | } |
| | | |
| | | /// Observe Show in Dock changes so it takes effect immediately. |
| | | private func observeDockSetting() { |
| | | NotificationCenter.default.addObserver( |
| | | forName: UserDefaults.didChangeNotification, |
| | | object: nil, queue: .main |
| | | ) { _ in |
| | | let show = UserDefaults.standard.bool(forKey: "showDockIcon") |
| | | NSApp.setActivationPolicy(show ? .regular : .accessory) |
| | | } |
| | | } |
| | | |
| | | /// Enable launch at login by default; prompt user if disabled. |
| | | private func setupLaunchAtLogin() { |
| | | let key = "launchAtLogin" |
| | | if UserDefaults.standard.object(forKey: key) == nil { |
| | | // First launch: enable by default |
| | | UserDefaults.standard.set(true, forKey: key) |
| | | try? SMAppService.mainApp.register() |
| | | } else if UserDefaults.standard.bool(forKey: key) { |
| | | try? SMAppService.mainApp.register() |
| | | } |
| | | } |
| | | |
| | | // MARK: - Menu Bar |
| | | |
| | | private func setupMenuBar() { |
| | | // Remove old status item if re-creating (language switch, etc.) |
| | | if let existing = statusItem { |
| | | NSStatusBar.system.removeStatusItem(existing) |
| | | } |
| | | statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) |
| | | |
| | | if let button = statusItem.button { |
| | |
| | | let menu = NSMenu() |
| | | menu.addItem( |
| | | NSMenuItem( |
| | | title: "Show Apptag", |
| | | title: tr("menu.show"), |
| | | action: #selector(toggleOverlay), |
| | | keyEquivalent: "" |
| | | ) |
| | |
| | | let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "?" |
| | | let buildNumber = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "?" |
| | | let versionItem = NSMenuItem( |
| | | title: "Version \(appVersion) (\(buildNumber))", |
| | | title: "\(tr("menu.version")) \(appVersion) (\(buildNumber))", |
| | | action: nil, |
| | | keyEquivalent: "" |
| | | ) |
| | |
| | | |
| | | menu.addItem(.separator()) |
| | | let prefsItem = NSMenuItem( |
| | | title: "Preferences...", |
| | | title: tr("menu.preferences"), |
| | | action: #selector(openPreferences), |
| | | keyEquivalent: "," |
| | | ) |
| | | prefsItem.keyEquivalentModifierMask = .command |
| | | menu.addItem(prefsItem) |
| | | menu.addItem(.separator()) |
| | | |
| | | // Language submenu |
| | | let langMenu = NSMenu() |
| | | let currentLang = L10n.currentCode |
| | | for (code, name) in L10n.supported { |
| | | let item = NSMenuItem(title: name, action: #selector(switchLanguage(_:)), keyEquivalent: "") |
| | | item.representedObject = code |
| | | item.state = (code == currentLang) ? .on : .off |
| | | langMenu.addItem(item) |
| | | } |
| | | let langItem = NSMenuItem(title: tr("menu.language"), action: nil, keyEquivalent: "") |
| | | langItem.submenu = langMenu |
| | | menu.addItem(langItem) |
| | | |
| | | menu.addItem(.separator()) |
| | | menu.addItem( |
| | | NSMenuItem( |
| | | title: "Quit", |
| | | title: tr("menu.quit"), |
| | | action: #selector(NSApplication.terminate(_:)), |
| | | keyEquivalent: "q" |
| | | ) |
| | |
| | | hideOverlay() |
| | | NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil) |
| | | } |
| | | |
| | | @objc private func switchLanguage(_ sender: NSMenuItem) { |
| | | guard let code = sender.representedObject as? String else { return } |
| | | L10n.switchTo(code) |
| | | // Rebuild menu to update checkmarks |
| | | setupMenuBar() |
| | | } |
| | | } |
| | | |
| | | // MARK: - NSView-level backdrop dismiss (works even if SwiftUI rendering is slow) |
| | |
| | | @AppStorage("defaultGroupName") private var defaultGroupName = "Other" |
| | | @AppStorage("displayMode") private var displayMode = "flat" |
| | | @AppStorage("hideAppNames") private var hideAppNames = false |
| | | @AppStorage("showDockIcon") private var showDockIcon = false |
| | | @AppStorage("launchAtLogin") private var launchAtLogin = true |
| | | |
| | | @State private var allApps: [AppInfo] = [] |
| | | @State private var tagColors: [String: Int] = [:] |
| | |
| | | // Tab 1: General |
| | | Form { |
| | | Section { |
| | | Toggle("Launch at login", isOn: $launchAtLogin) |
| | | .onChange(of: launchAtLogin) { _, enabled in |
| | | if enabled { |
| | | try? SMAppService.mainApp.register() |
| | | } else { |
| | | try? SMAppService.mainApp.unregister() |
| | | } |
| | | } |
| | | } |
| | | |
| | | Section { |
| | | LabeledContent("App list style:") { |
| | | Picker("", selection: $displayMode) { |
| | | Text("Flat").tag("flat") |
| | |
| | | Text("Adjust the size of group-name labels in the overlay.") |
| | | .font(.caption) |
| | | .foregroundStyle(.secondary) |
| | | } header: { |
| | | Text("Appearance") |
| | | } |
| | | |
| | | Section { |
| | |
| | | Text("Icon display size. Grid columns adjust automatically.") |
| | | .font(.caption) |
| | | .foregroundStyle(.secondary) |
| | | } header: { |
| | | Text("Layout") |
| | | } |
| | | |
| | | Section { |
| | | Toggle("Show in Dock", isOn: $showDockIcon) |
| | | } |
| | | |
| | | Section { |