mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-10-19 01:50:51 -05:00
feat(desktop): summon shortcut toggles -- press while focused hides (#13803)
This commit is contained in:
1 parent
955b039ed0
commit
330246dbf0
3 files changed
+97
-14
No files matched your search
@@ -1,7 +1,7 @@
|
||||
use crate::config::ConfigState;
|
||||
use crate::debug_log::{log_backend_error, MENU_OPEN_DEBUG_LOG_ID, MENU_TOGGLE_DEVTOOLS_ID};
|
||||
use crate::shortcuts::SummonShortcut;
|
||||
use crate::window::{focus_main_window, open_chat_window};
|
||||
use crate::window::{focus_main_window, focused_webview_window, open_chat_window};
|
||||
use tauri::image::Image;
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
use tauri::menu::AboutMetadataBuilder;
|
||||
@@ -131,18 +131,6 @@ fn build_view_menu(app: &AppHandle, menu: &Menu<Wry>) -> tauri::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The window a View-menu action should apply to: whichever one is focused
|
||||
/// (menu accelerators only fire while the app is active, but the app can have
|
||||
/// several windows).
|
||||
fn focused_webview_window(app: &AppHandle) -> Option<tauri::WebviewWindow> {
|
||||
app.webview_windows().into_values().find(|window| {
|
||||
window.is_focused().unwrap_or_else(|e| {
|
||||
log_backend_error(app, &format!("Failed to query window focus: {e}"));
|
||||
false
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn handle_reload(app: &AppHandle) {
|
||||
if let Some(window) = focused_webview_window(app) {
|
||||
crate::commands::reload_page(window);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::config::ConfigState;
|
||||
use crate::debug_log::{log_backend_debug, log_backend_error};
|
||||
use crate::window::{focus_main_window, open_chat_window};
|
||||
use crate::window::{focus_main_window, focused_webview_window, open_chat_window};
|
||||
use std::sync::{Mutex, PoisonError};
|
||||
use tauri::{AppHandle, Manager};
|
||||
use tauri_plugin_global_shortcut::{GlobalShortcutExt, Shortcut, ShortcutState};
|
||||
|
||||
@@ -11,6 +12,11 @@ pub struct SummonShortcut {
|
||||
pub chord: String,
|
||||
}
|
||||
|
||||
/// Labels of the windows the last chord dismissal hid. Auxiliary windows
|
||||
/// have no other path back once hidden (the tray and summon paths only
|
||||
/// manage `main`), so the next summon restores this exact set.
|
||||
struct DismissedWindows(Mutex<Vec<String>>);
|
||||
|
||||
/// Register the configured summon shortcut. This is deliberately the only
|
||||
/// global (system-wide) shortcut the app registers: summoning Onyx is the one
|
||||
/// action that must work while another app is focused. Every other shortcut
|
||||
@@ -30,12 +36,24 @@ pub fn setup_global_shortcuts(app: &AppHandle) {
|
||||
}
|
||||
};
|
||||
|
||||
app.manage(DismissedWindows(Mutex::new(Vec::new())));
|
||||
|
||||
let result = app
|
||||
.global_shortcut()
|
||||
.on_shortcut(shortcut, |app, _shortcut, event| {
|
||||
if event.state() != ShortcutState::Pressed {
|
||||
return;
|
||||
}
|
||||
// Toggle contract: the chord that summons Onyx also dismisses it,
|
||||
// returning focus to whatever app was active before. Dismissal
|
||||
// hides every visible Onyx window -- hiding only the focused one
|
||||
// would just hand focus to the next Onyx window instead of the
|
||||
// previous application.
|
||||
if focused_webview_window(app).is_some() {
|
||||
dismiss_all_windows(app);
|
||||
return;
|
||||
}
|
||||
restore_dismissed_windows(app);
|
||||
let opens_new_chat = app.state::<ConfigState>().config().summon_opens_new_chat;
|
||||
log_backend_debug(
|
||||
app,
|
||||
@@ -65,6 +83,71 @@ pub fn setup_global_shortcuts(app: &AppHandle) {
|
||||
}
|
||||
}
|
||||
|
||||
fn dismiss_all_windows(app: &AppHandle) {
|
||||
let mut hidden = Vec::new();
|
||||
for (label, window) in app.webview_windows() {
|
||||
let visible = window.is_visible().unwrap_or_else(|e| {
|
||||
log_backend_error(app, &format!("Failed to query window visibility: {e}"));
|
||||
false
|
||||
});
|
||||
if !visible {
|
||||
continue;
|
||||
}
|
||||
if let Err(e) = window.hide() {
|
||||
log_backend_error(app, &format!("Failed to hide window \"{label}\": {e}"));
|
||||
} else {
|
||||
hidden.push(label);
|
||||
}
|
||||
}
|
||||
log_backend_debug(
|
||||
app,
|
||||
&format!(
|
||||
"Summon shortcut fired (action=dismiss, windows={})",
|
||||
hidden.len()
|
||||
),
|
||||
);
|
||||
// Merge rather than replace: the queue may still hold labels whose
|
||||
// restore failed earlier, and overwriting would strand those windows.
|
||||
if let Some(state) = app.try_state::<DismissedWindows>() {
|
||||
let mut queued = state.0.lock().unwrap_or_else(PoisonError::into_inner);
|
||||
for label in hidden {
|
||||
if !queued.contains(&label) {
|
||||
queued.push(label);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Re-show the windows the last dismissal hid. `main` is skipped here; the
|
||||
/// summon action that follows shows and focuses it. A window whose `show`
|
||||
/// fails stays queued so a later summon retries it -- dropping the label
|
||||
/// would strand the window hidden with no path back.
|
||||
fn restore_dismissed_windows(app: &AppHandle) {
|
||||
let Some(state) = app.try_state::<DismissedWindows>() else {
|
||||
return;
|
||||
};
|
||||
let labels = std::mem::take(&mut *state.0.lock().unwrap_or_else(PoisonError::into_inner));
|
||||
let mut failed = Vec::new();
|
||||
for label in labels {
|
||||
if label == "main" {
|
||||
continue;
|
||||
}
|
||||
if let Some(window) = app.get_webview_window(&label) {
|
||||
if let Err(e) = window.show() {
|
||||
log_backend_error(app, &format!("Failed to restore window \"{label}\": {e}"));
|
||||
failed.push(label);
|
||||
}
|
||||
}
|
||||
}
|
||||
if !failed.is_empty() {
|
||||
state
|
||||
.0
|
||||
.lock()
|
||||
.unwrap_or_else(PoisonError::into_inner)
|
||||
.extend(failed);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -14,6 +14,18 @@ use window_vibrancy::{apply_vibrancy, NSVisualEffectMaterial};
|
||||
const TITLEBAR_SCRIPT: &str = include_str!("../../src/titlebar.js");
|
||||
const CHAT_LINK_INTERCEPT_SCRIPT: &str = include_str!("scripts/chat_link_intercept.js");
|
||||
|
||||
/// The window a shortcut or menu action should apply to: whichever one is
|
||||
/// focused (the app can have several windows). Focus-query failures are
|
||||
/// logged and treated as unfocused.
|
||||
pub fn focused_webview_window(app: &AppHandle) -> Option<WebviewWindow> {
|
||||
app.webview_windows().into_values().find(|window| {
|
||||
window.is_focused().unwrap_or_else(|e| {
|
||||
log_backend_error(app, &format!("Failed to query window focus: {e}"));
|
||||
false
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn focus_main_window(app: &AppHandle) {
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
if let Err(e) = window.unminimize() {
|
||||
|
||||
Reference in new issue
Block a user