From 70a1cc2e63d2c49a135bd89cec3ffb1d427d90ee Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Tue, 30 Jun 2026 16:21:47 +0800
Subject: [PATCH] Freeze prep TagLauncher 8.2.9 Pro status UI
---
src/Apptag/ApptagApp.swift | 524 +++++++++++++++++++++++++++++++++++++++++++++++++++++----
1 files changed, 480 insertions(+), 44 deletions(-)
diff --git a/src/Apptag/ApptagApp.swift b/src/Apptag/ApptagApp.swift
index e155bc6..0166772 100644
--- a/src/Apptag/ApptagApp.swift
+++ b/src/Apptag/ApptagApp.swift
@@ -17,7 +17,7 @@
Settings {
PreferencesView()
}
- .defaultSize(width: 1000, height: 480)
+ .defaultSize(width: 1000, height: 640)
.commands {
CommandGroup(replacing: .systemServices) { }
CommandGroup(replacing: .appVisibility) { }
@@ -28,29 +28,44 @@
// MARK: - App Delegate (menubar + overlay window + hotkey)
+private enum HotkeyRegistrationAttempt {
+ case success(EventHotKeyRef)
+ case failure(OSStatus)
+}
+
final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
+ static private(set) weak var shared: AppDelegate?
+
private static let showDockIconKey = "showDockIcon"
private static let statusItemAutosaveName = AppIdentity.statusItemAutosaveName
private static let statusItemButtonIdentifier = NSUserInterfaceItemIdentifier("TagLauncherStatusItemButton")
private static let statusItemAccessibilityLabel = AppIdentity.displayName
private static let showAppListMenuItemIdentifier = NSUserInterfaceItemIdentifier("TagLauncherShowAppListMenuItem")
+ private static let proStatusMenuItemIdentifier = NSUserInterfaceItemIdentifier("TagLauncherProStatusMenuItem")
private static let helpMenuItemIdentifier = NSUserInterfaceItemIdentifier("TagLauncherHelpMenu")
private static let downloadHelpMenuItemIdentifier = NSUserInterfaceItemIdentifier("TagLauncherDownloadHelpMenuItem")
private static let externalActivationNotification = Notification.Name("TagLauncherExternalActivationRequested")
private static let externalActivationObject = AppIdentity.bundleIdentifier
+ private static let externalInvocationScheme = "taglauncher"
+ private static let externalInvocationShowHost = "show"
private static let duplicateLaunchSuppressReopenKey = "duplicateLaunchSuppressReopenAt"
+ private static let overlaySpaceHotkeyID: UInt32 = 3
private static let launcherOverlayLevel = NSWindow.Level(rawValue: NSWindow.Level.mainMenu.rawValue - 1)
private static let overlayDefaultLevel = launcherOverlayLevel
private static let overlayTextInputLevel = launcherOverlayLevel
+ private static let settingsContentSize = NSSize(width: 1000, height: 640)
private var statusItem: NSStatusItem?
private var overlayKeyMonitor: Any?
private var quickSearchLocalMouseMonitor: Any?
private var quickSearchExternalMouseMonitor: Any?
private var settingsWindow: NSWindow? // Track Settings window to keep it above overlay
+ private var explicitPreferencesOpenRequestedAt = Date.distantPast
private var mainHotkeyRef: EventHotKeyRef?
private var quickSearchHotkeyRef: EventHotKeyRef?
+ private var overlaySpaceHotkeyRef: EventHotKeyRef?
private var hotkeyEventHandlerInstalled = false
+ private var configuredHotkeysSuspendedForRecording = false
private var isQuickSearchOpen = false
private var quickSearchShouldHideOverlayOnClose = false
private var quickSearchOnlyOverlaySession = false
@@ -63,6 +78,13 @@
private var statusMenuScreenForNextOverlay: NSScreen?
private var suppressReopenUntil = Date.distantPast
private var overlayOpenedByQuickSearchOnly = false
+ private var didFinishLaunching = false
+ private var pendingShowOverlayInvocation = false
+
+ override init() {
+ super.init()
+ Self.shared = self
+ }
private lazy var overlayController = OverlayWindowController(
dependencies: OverlayWindowController.Dependencies(
@@ -125,14 +147,16 @@
refreshChromeState: { [weak self] activate, avoidSpaceSwitch in
self?.refreshLauncherChromeState(activate: activate, avoidSpaceSwitch: avoidSpaceSwitch)
},
- onWillHide: {
+ onWillHide: { [weak self] in
+ self?.unregisterOverlaySpaceHotkey()
TagDatabase.flushPendingCategorySchemeBackupBatch()
},
onDidHide: { [weak self] in
self?.overlayOpenedByQuickSearchOnly = false
NotificationCenter.default.post(name: .tagLauncherOverlayDidHide, object: nil)
},
- onDidShow: {
+ onDidShow: { [weak self] in
+ self?.refreshOverlaySpaceHotkeyRegistration()
NotificationCenter.default.post(name: .tagLauncherOverlayDidShow, object: nil)
}
)
@@ -192,8 +216,10 @@
func applicationDidFinishLaunching(_ notification: Notification) {
AppDefaults.register()
L10n.setup()
+ ProEntitlementCenter.shared.start()
migrateDefaultGroupName()
TagDatabase.seedDefaultTags()
+ _ = TagDatabase.relocalizeSystemTagsForCurrentLanguage()
syncChromeSettings(force: true)
observeHotkeyStatusChanges()
registerConfiguredHotkeys()
@@ -207,8 +233,12 @@
observeApplicationMenuChanges()
observeChromeSettings()
observeLanguageChanges()
+ observeProEntitlementChanges()
setupLaunchAtLogin()
suppressReopenUntil = Date().addingTimeInterval(1.0)
+ didFinishLaunching = true
+ closeRestoredPreferencesWindowsDuringLaunch()
+ consumePendingShowOverlayInvocationIfNeeded()
configureApplicationMenuWhenAvailable(retries: 200)
warmAppIndexInBackground()
relocalizeDefaultAppNotesForCurrentLanguageAsync()
@@ -241,6 +271,54 @@
return false // Suppress default "unhide all windows" behavior
}
+ func application(_ application: NSApplication, open urls: [URL]) {
+ for url in urls {
+ handleExternalInvocationURL(url)
+ }
+ }
+
+ private enum ExternalInvocationRoute {
+ case showOverlay
+ }
+
+ private func handleExternalInvocationURL(_ url: URL) {
+ guard externalInvocationRoute(for: url) == .showOverlay else {
+ Diagnostics.log("app.externalInvocation.ignored", [
+ "url": url.absoluteString
+ ])
+ return
+ }
+ requestShowOverlayFromExternalInvocation()
+ }
+
+ private func externalInvocationRoute(for url: URL) -> ExternalInvocationRoute? {
+ guard url.scheme?.lowercased() == Self.externalInvocationScheme else {
+ return nil
+ }
+ let host = url.host?.lowercased()
+ let path = url.path
+ if host == Self.externalInvocationShowHost, path.isEmpty || path == "/" {
+ return .showOverlay
+ }
+ return nil
+ }
+
+ private func requestShowOverlayFromExternalInvocation() {
+ guard didFinishLaunching else {
+ pendingShowOverlayInvocation = true
+ return
+ }
+ pendingShowOverlayInvocation = false
+ suppressReopenUntil = Date().addingTimeInterval(1.0)
+ dismissQuickSearchIfNeeded()
+ showOrFocusOverlay()
+ }
+
+ private func consumePendingShowOverlayInvocationIfNeeded() {
+ guard pendingShowOverlayInvocation else { return }
+ requestShowOverlayFromExternalInvocation()
+ }
+
private func isRecentUserClickNearDockArea() -> Bool {
let lastMouseDown = CGEventSource.secondsSinceLastEventType(
.combinedSessionState,
@@ -250,12 +328,15 @@
.combinedSessionState,
eventType: .leftMouseUp
)
- let recentMouseClick = min(lastMouseDown, lastMouseUp) < 1.2
+ let recentMouseClick = min(lastMouseDown, lastMouseUp) < 0.9
return recentMouseClick && isPointerNearDockArea()
}
private func isPointerNearDockArea() -> Bool {
let mouse = NSEvent.mouseLocation
+ let dockOrientation = UserDefaults(suiteName: "com.apple.dock")?
+ .string(forKey: "orientation") ?? "bottom"
+ let hiddenDockEdgeTolerance: CGFloat = 96
return NSScreen.screens.contains { screen in
let frame = screen.frame
let visible = screen.visibleFrame
@@ -269,7 +350,16 @@
let rightDock = visible.maxX < frame.maxX
&& mouse.x <= frame.maxX
&& mouse.x >= visible.maxX - 24
- return bottomDock || leftDock || rightDock
+ let hiddenDockFallback: Bool
+ switch dockOrientation {
+ case "left":
+ hiddenDockFallback = mouse.x <= frame.minX + hiddenDockEdgeTolerance
+ case "right":
+ hiddenDockFallback = mouse.x >= frame.maxX - hiddenDockEdgeTolerance
+ default:
+ hiddenDockFallback = mouse.y <= frame.minY + hiddenDockEdgeTolerance
+ }
+ return bottomDock || leftDock || rightDock || hiddenDockFallback
}
}
@@ -277,6 +367,7 @@
hideOverlay(force: true, discardWindow: true)
unregisterHotkey(for: .main)
unregisterHotkey(for: .quickSearch)
+ unregisterOverlaySpaceHotkey()
TagDatabase.flushPendingCategorySchemeBackupBatch()
removeOverlayKeyMonitor()
removeQuickSearchExternalMouseMonitor()
@@ -329,11 +420,53 @@
}
}
+ private func foregroundWindowForActivationPolicyRestore() -> NSWindow? {
+ if let settingsWindow, settingsWindow.isVisible {
+ return settingsWindow
+ }
+ if let overlayWindow, overlayWindow.isVisible {
+ return overlayWindow
+ }
+ return NSApp.keyWindow
+ }
+
+ private func setLauncherActivationPolicy(_ desiredPolicy: NSApplication.ActivationPolicy) {
+ guard NSApp.activationPolicy() != desiredPolicy else { return }
+
+ let restoreWindow = desiredPolicy == .accessory && NSApp.isActive
+ ? foregroundWindowForActivationPolicyRestore()
+ : nil
+
+ // Deactivate before switching regular -> accessory; otherwise Dock can
+ // keep a stale running tile until Dock itself restarts.
+ if desiredPolicy == .accessory && NSApp.isActive {
+ NSApp.deactivate()
+ }
+
+ NSApp.setActivationPolicy(desiredPolicy)
+
+ guard desiredPolicy == .accessory,
+ let restoreWindow,
+ restoreWindow.isVisible
+ else { return }
+
+ DispatchQueue.main.async { [weak self, weak restoreWindow] in
+ guard let self,
+ let restoreWindow,
+ restoreWindow.isVisible,
+ self.requiresForegroundOwnership
+ else { return }
+ NSApp.activate(ignoringOtherApps: true)
+ restoreWindow.makeKeyAndOrderFront(nil)
+ restoreWindow.orderFrontRegardless()
+ }
+ }
+
private func beginLauncherForegroundOwnership(activate: Bool = true, keyWindow: NSWindow? = nil) {
let showDock = UserDefaults.standard.bool(forKey: Self.showDockIconKey)
let desiredPolicy: NSApplication.ActivationPolicy = showDock ? .regular : .accessory
if NSApp.activationPolicy() != desiredPolicy {
- NSApp.setActivationPolicy(desiredPolicy)
+ setLauncherActivationPolicy(desiredPolicy)
}
if activate, showDock {
claimLauncherForeground(keyWindow: keyWindow)
@@ -350,7 +483,7 @@
overlayGeneration expectedOverlayGeneration: Int? = nil
) {
if NSApp.activationPolicy() != .regular {
- NSApp.setActivationPolicy(.regular)
+ setLauncherActivationPolicy(.regular)
}
if isOverlayVisible && !NSApp.presentationOptions.contains(.hideDock) {
@@ -412,7 +545,7 @@
? .regular
: (showDock ? .regular : .accessory))))
if NSApp.activationPolicy() != desiredPolicy {
- NSApp.setActivationPolicy(desiredPolicy)
+ setLauncherActivationPolicy(desiredPolicy)
}
let desiredPresentation: NSApplication.PresentationOptions = isOverlayVisible ? [.hideDock] : []
@@ -422,14 +555,17 @@
if activate && requiresForegroundOwnership
&& !shouldStayAccessoryForCurrentFullscreenSpace
- && !shouldStayAccessoryForHiddenDockChrome
&& !shouldStayAccessoryForQuickOnlySearch {
let keyWindow = isSettingsVisible ? settingsWindow : (isOverlayVisible ? overlayWindow : nil)
- claimLauncherForeground(
- keyWindow: keyWindow,
- retries: isOverlayVisible && !isSettingsVisible ? 5 : 0,
- overlayGeneration: isOverlayVisible ? overlayGeneration : nil
- )
+ if shouldStayAccessoryForHiddenDockChrome {
+ beginLauncherForegroundOwnership(activate: true, keyWindow: keyWindow)
+ } else {
+ claimLauncherForeground(
+ keyWindow: keyWindow,
+ retries: isOverlayVisible && !isSettingsVisible ? 5 : 0,
+ overlayGeneration: isOverlayVisible ? overlayGeneration : nil
+ )
+ }
}
}
@@ -604,6 +740,9 @@
versionItem.isEnabled = false
menu.addItem(versionItem)
+ let proStatusItem = makeProStatusMenuItem()
+ menu.addItem(proStatusItem)
+
menu.addItem(.separator())
let prefsItem = NSMenuItem(
title: tr("menu.preferences"),
@@ -640,6 +779,7 @@
}
func menuWillOpen(_ menu: NSMenu) {
+ configureProStatusMenuItem(in: menu)
statusMenuScreenForNextOverlay = overlayController.screenContainingCurrentPointer()
?? statusItem?.button?.window?.screen
?? NSScreen.main
@@ -712,11 +852,58 @@
return image
}
+ private func makeProStatusMenuItem() -> NSMenuItem {
+ let item = NSMenuItem(
+ title: "",
+ action: #selector(openProStatusFromStatusMenu(_:)),
+ keyEquivalent: ""
+ )
+ item.identifier = Self.proStatusMenuItemIdentifier
+ item.target = self
+ configureProStatusMenuItem(item)
+ return item
+ }
+
+ private func configureProStatusMenuItem(in menu: NSMenu) {
+ guard let item = menu.items.first(where: { $0.identifier == Self.proStatusMenuItemIdentifier }) else {
+ return
+ }
+ configureProStatusMenuItem(item)
+ }
+
+ private func configureProStatusMenuItem(_ item: NSMenuItem) {
+ let isUnlocked = ProEntitlementPolicy.accessState().isUnlocked
+ item.title = tr(isUnlocked ? "menu.proStatus.unlocked" : "menu.proStatus.free")
+ item.action = #selector(openProStatusFromStatusMenu(_:))
+ item.target = self
+ item.isEnabled = true
+ item.image = isUnlocked ? makeProStatusMenuIcon() : nil
+ }
+
+ private func makeProStatusMenuIcon() -> NSImage? {
+ guard let symbol = NSImage(systemSymbolName: "crown.fill", accessibilityDescription: "Pro") else {
+ return nil
+ }
+ let configuration = NSImage.SymbolConfiguration(pointSize: 13, weight: .semibold)
+ let configuredSymbol = symbol.withSymbolConfiguration(configuration) ?? symbol
+ let image = NSImage(size: NSSize(width: 16, height: 16))
+ image.lockFocus()
+ NSColor(red: 0.96, green: 0.63, blue: 0.10, alpha: 1.0).set()
+ let iconRect = NSRect(x: 1.5, y: 1.5, width: 13, height: 13)
+ configuredSymbol.draw(in: iconRect, from: .zero, operation: .sourceOver, fraction: 1.0)
+ NSColor(red: 0.96, green: 0.63, blue: 0.10, alpha: 1.0).set()
+ iconRect.fill(using: .sourceAtop)
+ image.unlockFocus()
+ image.isTemplate = false
+ image.accessibilityDescription = "Pro"
+ return image
+ }
+
private var showAppListMenuTitle: String {
if LauncherHotkeyRegistrationStore.state(for: .main) == .failed {
return tr("menu.showShortcutUnavailable")
}
- return "\(tr("menu.showAppList")) \(LauncherHotkey.main.displayString)"
+ return "\(tr("menu.showAppList")) \(LauncherHotkeySettings.effectiveHotkey(for: .main).displayString)"
}
private func observeApplicationMenuChanges() {
@@ -1094,14 +1281,106 @@
// MARK: - Global Hotkeys
private func registerConfiguredHotkeys() {
+ guard !configuredHotkeysSuspendedForRecording else { return }
installHotkeyEventHandlerIfNeeded()
- registerFixedHotkey(.main)
- registerFixedHotkey(.quickSearch)
+ registerEffectiveHotkey(.main)
+ registerEffectiveHotkey(.quickSearch)
}
- private func registerFixedHotkey(_ kind: LauncherHotkeyKind) {
+ func suspendConfiguredHotkeysForRecording() {
+ guard !configuredHotkeysSuspendedForRecording else { return }
+ configuredHotkeysSuspendedForRecording = true
+ unregisterHotkey(for: .main)
+ unregisterHotkey(for: .quickSearch)
+ }
+
+ func resumeConfiguredHotkeysAfterRecording() {
+ guard configuredHotkeysSuspendedForRecording else { return }
+ configuredHotkeysSuspendedForRecording = false
+ registerConfiguredHotkeys()
+ }
+
+ private func registerEffectiveHotkey(_ kind: LauncherHotkeyKind) {
unregisterHotkey(for: kind)
- let hotkey = kind.hotkey
+ let hotkey = LauncherHotkeySettings.effectiveHotkey(for: kind)
+ registerAndStoreHotkey(hotkey, for: kind, markFailure: true)
+ }
+
+ @discardableResult
+ func applyCustomHotkey(_ hotkey: LauncherHotkey, for kind: LauncherHotkeyKind) -> LauncherHotkeyCustomizationResult {
+ guard ProEntitlementPolicy.isUnlocked(.customHotkeys) else {
+ return .locked
+ }
+ if let error = LauncherHotkeySettings.validationError(for: hotkey, kind: kind) {
+ return .invalid(error)
+ }
+ return activateHotkey(hotkey, for: kind, persistCustom: true)
+ }
+
+ @discardableResult
+ func restoreDefaultHotkey(for kind: LauncherHotkeyKind) -> LauncherHotkeyCustomizationResult {
+ guard ProEntitlementPolicy.isUnlocked(.customHotkeys) else {
+ return .locked
+ }
+ return activateHotkey(LauncherHotkeySettings.defaultHotkey(for: kind), for: kind, persistCustom: false)
+ }
+
+ private func activateHotkey(
+ _ hotkey: LauncherHotkey,
+ for kind: LauncherHotkeyKind,
+ persistCustom: Bool
+ ) -> LauncherHotkeyCustomizationResult {
+ installHotkeyEventHandlerIfNeeded()
+
+ if LauncherHotkeySettings.effectiveHotkey(for: kind) == hotkey,
+ hotkeyRef(for: kind) != nil {
+ if persistCustom {
+ LauncherHotkeySettings.save(hotkey, for: kind)
+ LauncherHotkeyRegistrationStore.setActive(for: kind)
+ return .saved
+ }
+ LauncherHotkeySettings.clearCustomHotkey(for: kind)
+ LauncherHotkeyRegistrationStore.setActive(for: kind)
+ return .restored
+ }
+
+ switch tryRegisterHotkey(hotkey, for: kind, markFailure: false) {
+ case .success(let newRef):
+ if let oldRef = hotkeyRef(for: kind) {
+ UnregisterEventHotKey(oldRef)
+ }
+ setHotkeyRef(newRef, for: kind)
+ if persistCustom {
+ LauncherHotkeySettings.save(hotkey, for: kind)
+ LauncherHotkeyRegistrationStore.setActive(for: kind)
+ return .saved
+ }
+ LauncherHotkeySettings.clearCustomHotkey(for: kind)
+ LauncherHotkeyRegistrationStore.setActive(for: kind)
+ return .restored
+ case .failure(let status):
+ return .registrationFailed(status)
+ }
+ }
+
+ private func registerAndStoreHotkey(
+ _ hotkey: LauncherHotkey,
+ for kind: LauncherHotkeyKind,
+ markFailure: Bool
+ ) {
+ switch tryRegisterHotkey(hotkey, for: kind, markFailure: markFailure) {
+ case .success(let newRef):
+ setHotkeyRef(newRef, for: kind)
+ case .failure:
+ setHotkeyRef(nil, for: kind)
+ }
+ }
+
+ private func tryRegisterHotkey(
+ _ hotkey: LauncherHotkey,
+ for kind: LauncherHotkeyKind,
+ markFailure: Bool
+ ) -> HotkeyRegistrationAttempt {
var hotkeyID = EventHotKeyID()
hotkeyID.signature = OSType(0x41505447) // 'APTG'
@@ -1118,12 +1397,16 @@
)
if status == noErr, let newRef {
- setHotkeyRef(newRef, for: kind)
- LauncherHotkeyRegistrationStore.setActive(for: kind)
+ if markFailure {
+ LauncherHotkeyRegistrationStore.setActive(for: kind)
+ }
+ return .success(newRef)
} else {
- setHotkeyRef(nil, for: kind)
- LauncherHotkeyRegistrationStore.setFailed(status, for: kind)
+ if markFailure {
+ LauncherHotkeyRegistrationStore.setFailed(status, for: kind)
+ }
print("[TagLauncher] Fixed hotkey registration failed for \(kind.rawValue): \(status)")
+ return .failure(status)
}
}
@@ -1133,6 +1416,18 @@
object: nil,
queue: .main
) { [weak self] _ in
+ self?.setupMenuBar()
+ self?.configureApplicationMenuWhenAvailable(retries: 2)
+ }
+ }
+
+ private func observeProEntitlementChanges() {
+ NotificationCenter.default.addObserver(
+ forName: .tagLauncherProEntitlementChanged,
+ object: nil,
+ queue: .main
+ ) { [weak self] _ in
+ self?.registerConfiguredHotkeys()
self?.setupMenuBar()
self?.configureApplicationMenuWhenAvailable(retries: 2)
}
@@ -1189,9 +1484,27 @@
])
if id == LauncherHotkeyKind.quickSearch.eventID {
showQuickSearchFromGlobalHotkey()
+ } else if id == Self.overlaySpaceHotkeyID {
+ handleOverlaySpaceHotkey()
} else {
performToggleOverlay(preferredScreen: nil)
}
+ }
+
+ private func handleOverlaySpaceHotkey() {
+ guard canUseOverlaySpaceHotkey else { return }
+ unregisterOverlaySpaceHotkey()
+ requestQuickSearch(source: QuickSearchOpenSource.mainOverlay)
+ }
+
+ private var canUseOverlaySpaceHotkey: Bool {
+ overlayWindow?.isVisible == true
+ && !isSettingsVisible
+ && !isQuickSearchOpen
+ && !quickSearchOnlyOverlaySession
+ && !isInEditMode
+ && !isEditingAppNote
+ && !isModalInteractionActive
}
private func showQuickSearchFromGlobalHotkey() {
@@ -1249,6 +1562,46 @@
}
}
+ private func refreshOverlaySpaceHotkeyRegistration() {
+ if canUseOverlaySpaceHotkey {
+ registerOverlaySpaceHotkeyIfNeeded()
+ } else {
+ unregisterOverlaySpaceHotkey()
+ }
+ }
+
+ private func registerOverlaySpaceHotkeyIfNeeded() {
+ guard overlaySpaceHotkeyRef == nil else { return }
+
+ var hotkeyID = EventHotKeyID()
+ hotkeyID.signature = OSType(0x41505447) // 'APTG'
+ hotkeyID.id = Self.overlaySpaceHotkeyID
+
+ var newRef: EventHotKeyRef?
+ let status = RegisterEventHotKey(
+ UInt32(kVK_Space),
+ 0,
+ hotkeyID,
+ GetApplicationEventTarget(),
+ 0,
+ &newRef
+ )
+
+ if status == noErr, let newRef {
+ overlaySpaceHotkeyRef = newRef
+ } else {
+ overlaySpaceHotkeyRef = nil
+ Diagnostics.log("app.overlay.spaceHotkey.failed", ["status": status])
+ }
+ }
+
+ private func unregisterOverlaySpaceHotkey() {
+ if let overlaySpaceHotkeyRef {
+ UnregisterEventHotKey(overlaySpaceHotkeyRef)
+ self.overlaySpaceHotkeyRef = nil
+ }
+ }
+
// MARK: - Preferences
/// Hide overlay when any other window becomes key (catches Cmd+, via SwiftUI Settings).
@@ -1295,15 +1648,22 @@
/// Settings must always appear centered over the current overlay view and float above it.
private func prepareSettingsWindow(_ window: NSWindow) {
dismissQuickSearchIfNeeded()
- let settingsSize = NSSize(width: 1000, height: 480)
+ let settingsSize = Self.settingsContentSize
+ let settingsFrameSize = window.frameRect(
+ forContentRect: NSRect(origin: .zero, size: settingsSize)
+ ).size
window.identifier = NSUserInterfaceItemIdentifier("TagLauncherPreferencesWindow")
- window.minSize = settingsSize
- window.maxSize = settingsSize
- if abs(window.frame.width - settingsSize.width) > 0.5 || abs(window.frame.height - settingsSize.height) > 0.5 {
- window.setFrame(
- NSRect(origin: window.frame.origin, size: settingsSize),
- display: false
- )
+ window.isRestorable = false
+ window.restorationClass = nil
+ _ = window.setFrameAutosaveName("")
+ window.contentMinSize = settingsSize
+ window.contentMaxSize = settingsSize
+ window.minSize = settingsFrameSize
+ window.maxSize = settingsFrameSize
+ let currentContentSize = window.contentView?.bounds.size ?? window.contentLayoutRect.size
+ if abs(currentContentSize.width - settingsSize.width) > 0.5
+ || abs(currentContentSize.height - settingsSize.height) > 0.5 {
+ window.setContentSize(settingsSize)
}
if let overlayWindow, overlayWindow.isVisible {
@@ -1328,6 +1688,7 @@
window.makeKeyAndOrderFront(nil)
window.orderFrontRegardless()
settingsWindow = window
+ refreshOverlaySpaceHotkeyRegistration()
refreshLauncherChromeState(
activate: !overlayAvoidsSpaceSwitch,
avoidSpaceSwitch: overlayAvoidsSpaceSwitch
@@ -1432,6 +1793,7 @@
activate: shouldRefocusOverlay && !self.overlayAvoidsSpaceSwitch,
avoidSpaceSwitch: self.overlayAvoidsSpaceSwitch
)
+ self.refreshOverlaySpaceHotkeyRegistration()
guard shouldRefocusOverlay else { return }
DispatchQueue.main.asyncAfter(deadline: .now() + 0.12) { [weak self] in
self?.showOrFocusOverlay(preferredScreen: preferredScreen)
@@ -1446,7 +1808,9 @@
object: nil,
queue: .main
) { [weak self] notification in
- self?.isInEditMode = (notification.userInfo?["active"] as? Bool) ?? false
+ guard let self else { return }
+ self.isInEditMode = (notification.userInfo?["active"] as? Bool) ?? false
+ self.refreshOverlaySpaceHotkeyRegistration()
}
}
@@ -1463,6 +1827,7 @@
self.promoteOverlayToForegroundInput()
}
self.updateOverlayLevelForTextInput()
+ self.refreshOverlaySpaceHotkeyRegistration()
}
}
@@ -1529,6 +1894,7 @@
self.hideOverlay(force: true, discardWindow: wasQuickSearchOnlyOverlay)
}
}
+ self.refreshOverlaySpaceHotkeyRegistration()
}
NotificationCenter.default.addObserver(
@@ -1536,7 +1902,9 @@
object: nil,
queue: .main
) { [weak self] notification in
- self?.isModalInteractionActive = (notification.userInfo?["active"] as? Bool) ?? false
+ guard let self else { return }
+ self.isModalInteractionActive = (notification.userInfo?["active"] as? Bool) ?? false
+ self.refreshOverlaySpaceHotkeyRegistration()
}
NotificationCenter.default.addObserver(
@@ -1572,12 +1940,7 @@
) { [weak self] event in
guard let self, self.isQuickSearchOpen else { return event }
if event.window === self.overlayWindow {
- NotificationCenter.default.post(
- name: .tagLauncherQuickSearchDismissRequested,
- object: nil,
- userInfo: ["source": QuickSearchDismissSource.mouse]
- )
- return nil
+ return event
}
return event
}
@@ -1608,9 +1971,13 @@
forName: .tagLauncherOpenPreferencesRequested,
object: nil,
queue: .main
- ) { [weak self] _ in
- self?.openPreferences()
+ ) { [weak self] notification in
+ self?.openPreferences(targetTab: Self.preferencesTabTarget(from: notification))
}
+ }
+
+ private static func preferencesTabTarget(from notification: Notification) -> String? {
+ notification.userInfo?[SettingsTabTarget.userInfoKey] as? String
}
private func observeExternalActivationRequests() {
@@ -1632,6 +1999,15 @@
}
@objc private func openPreferences(_ sender: Any? = nil) {
+ openPreferences(targetTab: nil)
+ }
+
+ @objc private func openProStatusFromStatusMenu(_ sender: NSMenuItem) {
+ openPreferences(targetTab: SettingsTabTarget.pro)
+ }
+
+ private func openPreferences(targetTab: String?) {
+ explicitPreferencesOpenRequestedAt = Date()
dismissQuickSearchIfNeeded()
TagDatabase.flushPendingCategorySchemeBackupBatch()
if overlayAvoidsSpaceSwitch {
@@ -1647,21 +2023,49 @@
if let settingsWindow {
prepareSettingsWindow(settingsWindow)
+ requestPreferencesTabSelection(targetTab)
return
}
- let settingsSize = NSSize(width: 1000, height: 480)
let window = NSWindow(
- contentRect: NSRect(origin: .zero, size: settingsSize),
+ contentRect: NSRect(origin: .zero, size: Self.settingsContentSize),
styleMask: [.titled, .closable],
backing: .buffered,
defer: false
)
window.title = tr("menu.preferences").replacingOccurrences(of: "…", with: "")
- window.contentView = NSHostingView(rootView: PreferencesView())
+ window.contentView = NSHostingView(rootView: PreferencesView(initialTabRawValue: targetTab))
window.isReleasedWhenClosed = false
settingsWindow = window
prepareSettingsWindow(window)
+ }
+
+ private func closeRestoredPreferencesWindowsDuringLaunch() {
+ for delay in [0.25, 0.8, 1.6, 2.6] {
+ DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
+ self?.closeRestoredPreferencesWindowIfNeeded()
+ }
+ }
+ }
+
+ private func closeRestoredPreferencesWindowIfNeeded() {
+ guard Date().timeIntervalSince(explicitPreferencesOpenRequestedAt) > 3.0 else { return }
+ for window in NSApp.windows where isSettingsWindowCandidate(window) {
+ detachSettingsWindow(window)
+ window.close()
+ if settingsWindow == window {
+ settingsWindow = nil
+ }
+ }
+ }
+
+ private func requestPreferencesTabSelection(_ targetTab: String?) {
+ guard let targetTab else { return }
+ NotificationCenter.default.post(
+ name: .tagLauncherPreferencesTabRequested,
+ object: nil,
+ userInfo: [SettingsTabTarget.userInfoKey: targetTab]
+ )
}
private func dismissQuickSearchIfNeeded() {
@@ -1735,7 +2139,13 @@
super.mouseDown(with: event)
return
}
+ if routeUsageTipsMouseDownIfNeeded(event) {
+ return
+ }
if hit == self {
+ if shouldSwallowUsageTipsBackdropClick(at: location) {
+ return
+ }
if quickSearchSuppressesBackdropDismiss {
NotificationCenter.default.post(
name: .tagLauncherQuickSearchDismissRequested,
@@ -1770,6 +2180,32 @@
super.mouseDown(with: event)
}
+ private func routeUsageTipsMouseDownIfNeeded(_ event: NSEvent) -> Bool {
+ guard let appGridHost = findAppGridCollectionHost(in: self) else { return false }
+ return appGridHost.handleUsageTipsMouseDown(event)
+ }
+
+ private func findAppGridCollectionHost(in view: NSView) -> AppGridCollectionHostView? {
+ if let host = view as? AppGridCollectionHostView {
+ return host
+ }
+ for subview in view.subviews {
+ if let host = findAppGridCollectionHost(in: subview) {
+ return host
+ }
+ }
+ return nil
+ }
+
+ private func shouldSwallowUsageTipsBackdropClick(at location: NSPoint) -> Bool {
+ guard !UserDefaults.standard.bool(forKey: "hideUsageTips") else { return false }
+ let height = min(bounds.height, AppGridUsageTipsMetrics.reservedHeight)
+ guard height > 0 else { return false }
+ let y = isFlipped ? max(0, bounds.height - height) : 0
+ let region = NSRect(x: 0, y: y, width: bounds.width, height: height)
+ return region.contains(location)
+ }
+
private func observeBackdropDismissSuppressionChanges() {
modalInteractionObserver = NotificationCenter.default.addObserver(
forName: .tagLauncherModalInteractionChanged,
--
Gitblit v1.9.3