diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index e3e8762acb..562936d46c 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -177,6 +177,15 @@ pub async fn main() { app_handle.tray().create_app_menu().unwrap(); } + { + use tauri_plugin_tray::HyprMenuItem; + app_handle.on_menu_event(|app, event| { + if let Ok(item) = HyprMenuItem::try_from(event.id().clone()) { + item.handle(app); + } + }); + } + { use tauri_plugin_settings::SettingsPluginExt; if let Ok(base) = app_handle.settings().global_base() @@ -272,6 +281,11 @@ pub async fn main() { } api.prevent_exit(); + + for (_, window) in app.webview_windows() { + let _ = window.close(); + } + let _ = app.set_activation_policy(tauri::ActivationPolicy::Accessory); } tauri::RunEvent::Exit => { diff --git a/crates/intercept/swift-lib/src/lib.swift b/crates/intercept/swift-lib/src/lib.swift index c3611c2250..c59f1d486c 100644 --- a/crates/intercept/swift-lib/src/lib.swift +++ b/crates/intercept/swift-lib/src/lib.swift @@ -38,13 +38,6 @@ private final class QuitInterceptor { case holding } - private enum Event { - case cmdQPressed - case keyReleased - case dismissTimerFired - case quitTimerFired - } - private var keyMonitor: Any? private var panel: NSPanel? private var state: State = .idle @@ -63,12 +56,13 @@ private final class QuitInterceptor { return self.handleKeyDown(event) case .keyUp: self.handleKeyUp(event) + return event case .flagsChanged: self.handleFlagsChanged(event) + return event default: - break + return event } - return event } } @@ -131,27 +125,16 @@ private final class QuitInterceptor { container.layer?.cornerRadius = QuitOverlay.cornerRadius container.layer?.masksToBounds = true - let font = QuitOverlay.font - - let pressLabel = NSTextField(labelWithString: QuitOverlay.pressText) - pressLabel.font = font - pressLabel.textColor = QuitOverlay.primaryTextColor - pressLabel.alignment = .left - pressLabel.sizeToFit() - - let holdLabel = NSTextField(labelWithString: QuitOverlay.holdText) - holdLabel.font = font - holdLabel.textColor = QuitOverlay.secondaryTextColor - holdLabel.alignment = .left - holdLabel.sizeToFit() + let pressLabel = makeLabel(QuitOverlay.pressText, color: QuitOverlay.primaryTextColor) + let holdLabel = makeLabel(QuitOverlay.holdText, color: QuitOverlay.secondaryTextColor) - let pressPrefixWidth = NSAttributedString( - string: "Press ", attributes: [.font: font] - ).size().width - let holdPrefixWidth = NSAttributedString( - string: "Hold ", attributes: [.font: font] - ).size().width - let prefixDelta = pressPrefixWidth - holdPrefixWidth + let prefixDelta = + NSAttributedString( + string: "Press ", attributes: [.font: QuitOverlay.font] + ).size().width + - NSAttributedString( + string: "Hold ", attributes: [.font: QuitOverlay.font] + ).size().width let spacing: CGFloat = 10 let totalHeight = pressLabel.frame.height + spacing + holdLabel.frame.height @@ -176,13 +159,18 @@ private final class QuitInterceptor { return container } - // MARK: - Show / Hide - - func showOverlay() { - showPanel() + private func makeLabel(_ text: String, color: NSColor) -> NSTextField { + let label = NSTextField(labelWithString: text) + label.font = QuitOverlay.font + label.textColor = color + label.alignment = .left + label.sizeToFit() + return label } - private func showPanel() { + // MARK: - Panel Visibility + + func showOverlay() { if panel == nil { panel = makePanel() } @@ -212,66 +200,55 @@ private final class QuitInterceptor { // MARK: - State Machine - private func transition(_ event: Event) { - switch (state, event) { - case (.idle, .cmdQPressed): + private func onCmdQPressed() { + switch state { + case .idle: state = .awaiting - showPanel() - startDismissTimer() + showOverlay() + scheduleTimer(&dismissTimer, delay: QuitOverlay.overlayDuration) { [weak self] in + guard let self, self.state == .awaiting else { return } + self.state = .idle + self.hidePanel() + } - case (.awaiting, .cmdQPressed): + case .awaiting: state = .holding - cancelDismissTimer() - startQuitTimer() - - case (.awaiting, .keyReleased): - break - - case (.awaiting, .dismissTimerFired): - state = .idle - hidePanel() - - case (.holding, .keyReleased): - state = .idle - cancelQuitTimer() - performClose() - - case (.holding, .quitTimerFired): - performQuit() + cancelTimer(&dismissTimer) + scheduleTimer(&quitTimer, delay: QuitOverlay.holdDuration) { [weak self] in + self?.performQuit() + } - default: + case .holding: break } } - // MARK: - Timers + private func onKeyReleased() { + switch state { + case .idle, .awaiting: + break - private func startDismissTimer() { - dismissTimer?.cancel() - let timer = DispatchWorkItem { [weak self] in - self?.transition(.dismissTimerFired) + case .holding: + state = .idle + cancelTimer(&quitTimer) + performClose() } - dismissTimer = timer - DispatchQueue.main.asyncAfter(deadline: .now() + QuitOverlay.overlayDuration, execute: timer) } - private func cancelDismissTimer() { - dismissTimer?.cancel() - dismissTimer = nil - } + // MARK: - Timer Helpers - private func startQuitTimer() { - quitTimer?.cancel() - let timer = DispatchWorkItem { [weak self] in - self?.transition(.quitTimerFired) - } - quitTimer = timer - DispatchQueue.main.asyncAfter(deadline: .now() + QuitOverlay.holdDuration, execute: timer) + private func scheduleTimer( + _ timer: inout DispatchWorkItem?, delay: TimeInterval, action: @escaping () -> Void + ) { + timer?.cancel() + let workItem = DispatchWorkItem(block: action) + timer = workItem + DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: workItem) } - private func cancelQuitTimer() { - quitTimer?.cancel() - quitTimer = nil + private func cancelTimer(_ timer: inout DispatchWorkItem?) { + timer?.cancel() + timer = nil } // MARK: - Event Handlers @@ -280,19 +257,28 @@ private final class QuitInterceptor { let flags = event.modifierFlags.intersection(.deviceIndependentFlagsMask) let isQ = event.charactersIgnoringModifiers?.lowercased() == "q" guard flags.contains(.command), isQ else { return event } + + if flags.contains(.shift) { + performQuit() + return nil + } + if event.isARepeat { return nil } - transition(.cmdQPressed) + onCmdQPressed() return nil } private func handleKeyUp(_ event: NSEvent) { - let isQ = event.charactersIgnoringModifiers?.lowercased() == "q" - if isQ { transition(.keyReleased) } + if event.charactersIgnoringModifiers?.lowercased() == "q" { + onKeyReleased() + } } private func handleFlagsChanged(_ event: NSEvent) { let flags = event.modifierFlags.intersection(.deviceIndependentFlagsMask) - if !flags.contains(.command) { transition(.keyReleased) } + if !flags.contains(.command) { + onKeyReleased() + } } } diff --git a/plugins/tray/src/ext.rs b/plugins/tray/src/ext.rs index 8df101d675..4d0adf7082 100644 --- a/plugins/tray/src/ext.rs +++ b/plugins/tray/src/ext.rs @@ -110,13 +110,6 @@ impl<'a, M: tauri::Manager> Tray<'a, tauri::Wry, M> { .icon_as_template(true) .menu(&menu) .show_menu_on_left_click(true) - .on_menu_event(move |app: &AppHandle, event| { - // Tauri dispatches menu events globally, so we receive events from context menus - // created elsewhere. TryFrom gracefully ignores unknown menu IDs that don't belong to the tray menu. - if let Ok(item) = HyprMenuItem::try_from(event.id.clone()) { - item.handle(app); - } - }) .build(app)?; Ok(()) diff --git a/plugins/tray/src/menu_items/tray_quit.rs b/plugins/tray/src/menu_items/tray_quit.rs index b881757219..ce69f383d4 100644 --- a/plugins/tray/src/menu_items/tray_quit.rs +++ b/plugins/tray/src/menu_items/tray_quit.rs @@ -11,7 +11,7 @@ impl MenuItemHandler for TrayQuit { const ID: &'static str = "hypr_tray_quit"; fn build(app: &AppHandle) -> Result> { - let item = MenuItem::with_id(app, Self::ID, "Quit Completely", true, Some("cmd+q"))?; + let item = MenuItem::with_id(app, Self::ID, "Quit Completely", true, Some("cmd+shift+q"))?; Ok(MenuItemKind::MenuItem(item)) }