diff --git a/openless-all/app/src-tauri/src/commands.rs b/openless-all/app/src-tauri/src/commands.rs index d7bf12d8..6d1e1425 100644 --- a/openless-all/app/src-tauri/src/commands.rs +++ b/openless-all/app/src-tauri/src/commands.rs @@ -101,10 +101,7 @@ fn persist_settings( mut prefs: UserPreferences, ) -> Result<(), String> { sync_dictation_hotkey_legacy_fields(&mut prefs); - reject_dictation_translation_hotkey_overlap( - &prefs.dictation_hotkey, - &prefs.translation_hotkey, - )?; + reject_hotkey_collisions(&prefs)?; coord.write_settings(prefs)?; coord.refresh_dictation_hotkey(); coord.refresh_qa_hotkey(); @@ -750,6 +747,9 @@ pub fn set_qa_hotkey( let mut prefs = coord.prefs().get(); if let Some(binding) = binding.as_ref() { reject_dictation_qa_hotkey_overlap(&prefs.dictation_hotkey, binding)?; + reject_qa_translation_hotkey_overlap(binding, &prefs.translation_hotkey)?; + reject_qa_switch_style_hotkey_overlap(binding, &prefs.switch_style_hotkey)?; + reject_qa_open_app_hotkey_overlap(binding, &prefs.open_app_hotkey)?; } prefs.qa_hotkey = binding; coord.prefs().set(prefs).map_err(|e| e.to_string())?; @@ -789,6 +789,8 @@ pub fn set_dictation_hotkey( reject_dictation_qa_hotkey_overlap(&binding, qa_hotkey)?; } reject_dictation_translation_hotkey_overlap(&binding, &prefs.translation_hotkey)?; + reject_dictation_switch_style_hotkey_overlap(&binding, &prefs.switch_style_hotkey)?; + reject_dictation_open_app_hotkey_overlap(&binding, &prefs.open_app_hotkey)?; prefs.dictation_hotkey = binding; sync_dictation_hotkey_legacy_fields(&mut prefs); coord.prefs().set(prefs).map_err(|e| e.to_string())?; @@ -805,6 +807,11 @@ pub fn set_translation_hotkey( crate::shortcut_binding::validate_binding(&binding).map_err(|e| e.to_string())?; let previous = coord.prefs().get(); reject_dictation_translation_hotkey_overlap(&previous.dictation_hotkey, &binding)?; + if let Some(qa_hotkey) = previous.qa_hotkey.as_ref() { + reject_qa_translation_hotkey_overlap(qa_hotkey, &binding)?; + } + reject_translation_switch_style_hotkey_overlap(&binding, &previous.switch_style_hotkey)?; + reject_translation_open_app_hotkey_overlap(&binding, &previous.open_app_hotkey)?; let mut prefs = previous.clone(); prefs.translation_hotkey = binding; coord.prefs().set(prefs).map_err(|e| e.to_string())?; @@ -826,6 +833,12 @@ pub fn set_switch_style_hotkey( crate::shortcut_binding::validate_binding(&binding).map_err(|e| e.to_string())?; reject_modifier_only_action_shortcut(&binding)?; let mut prefs = coord.prefs().get(); + reject_dictation_switch_style_hotkey_overlap(&prefs.dictation_hotkey, &binding)?; + reject_translation_switch_style_hotkey_overlap(&prefs.translation_hotkey, &binding)?; + if let Some(qa_hotkey) = prefs.qa_hotkey.as_ref() { + reject_qa_switch_style_hotkey_overlap(qa_hotkey, &binding)?; + } + reject_switch_style_open_app_hotkey_overlap(&binding, &prefs.open_app_hotkey)?; prefs.switch_style_hotkey = binding; coord.prefs().set(prefs).map_err(|e| e.to_string())?; coord.update_switch_style_hotkey_binding(); @@ -840,6 +853,12 @@ pub fn set_open_app_hotkey( crate::shortcut_binding::validate_binding(&binding).map_err(|e| e.to_string())?; reject_modifier_only_action_shortcut(&binding)?; let mut prefs = coord.prefs().get(); + reject_dictation_open_app_hotkey_overlap(&prefs.dictation_hotkey, &binding)?; + reject_translation_open_app_hotkey_overlap(&prefs.translation_hotkey, &binding)?; + if let Some(qa_hotkey) = prefs.qa_hotkey.as_ref() { + reject_qa_open_app_hotkey_overlap(qa_hotkey, &binding)?; + } + reject_switch_style_open_app_hotkey_overlap(&prefs.switch_style_hotkey, &binding)?; prefs.open_app_hotkey = binding; coord.prefs().set(prefs).map_err(|e| e.to_string())?; coord.update_open_app_hotkey_binding(); @@ -880,6 +899,8 @@ pub fn set_combo_hotkey(coord: CoordinatorState<'_>, binding: ComboBinding) -> R reject_dictation_qa_hotkey_overlap(&shortcut, qa_hotkey)?; } reject_dictation_translation_hotkey_overlap(&shortcut, &prefs.translation_hotkey)?; + reject_dictation_switch_style_hotkey_overlap(&shortcut, &prefs.switch_style_hotkey)?; + reject_dictation_open_app_hotkey_overlap(&shortcut, &prefs.open_app_hotkey)?; prefs.custom_combo_hotkey = Some(binding); prefs.dictation_hotkey = shortcut; sync_dictation_hotkey_legacy_fields(&mut prefs); @@ -924,14 +945,97 @@ fn reject_dictation_qa_hotkey_overlap( Ok(()) } +fn reject_hotkey_overlap( + left: &ShortcutBinding, + right: &ShortcutBinding, + message: &'static str, +) -> Result<(), String> { + if shortcut_bindings_overlap(left, right) { + return Err(message.into()); + } + Ok(()) +} + +fn reject_hotkey_collisions(prefs: &UserPreferences) -> Result<(), String> { + if let Some(qa_hotkey) = prefs.qa_hotkey.as_ref() { + reject_dictation_qa_hotkey_overlap(&prefs.dictation_hotkey, qa_hotkey)?; + reject_qa_translation_hotkey_overlap(qa_hotkey, &prefs.translation_hotkey)?; + reject_qa_switch_style_hotkey_overlap(qa_hotkey, &prefs.switch_style_hotkey)?; + reject_qa_open_app_hotkey_overlap(qa_hotkey, &prefs.open_app_hotkey)?; + } + reject_dictation_translation_hotkey_overlap(&prefs.dictation_hotkey, &prefs.translation_hotkey)?; + reject_dictation_switch_style_hotkey_overlap(&prefs.dictation_hotkey, &prefs.switch_style_hotkey)?; + reject_dictation_open_app_hotkey_overlap(&prefs.dictation_hotkey, &prefs.open_app_hotkey)?; + reject_translation_switch_style_hotkey_overlap( + &prefs.translation_hotkey, + &prefs.switch_style_hotkey, + )?; + reject_translation_open_app_hotkey_overlap(&prefs.translation_hotkey, &prefs.open_app_hotkey)?; + reject_switch_style_open_app_hotkey_overlap(&prefs.switch_style_hotkey, &prefs.open_app_hotkey)?; + Ok(()) +} + fn reject_dictation_translation_hotkey_overlap( dictation: &ShortcutBinding, translation: &ShortcutBinding, ) -> Result<(), String> { - if shortcut_bindings_overlap(dictation, translation) { - return Err("翻译快捷键不能和听写快捷键相同".into()); - } - Ok(()) + reject_hotkey_overlap(dictation, translation, "翻译快捷键不能和听写快捷键相同") +} + +fn reject_dictation_switch_style_hotkey_overlap( + dictation: &ShortcutBinding, + switch_style: &ShortcutBinding, +) -> Result<(), String> { + reject_hotkey_overlap(dictation, switch_style, "切换风格快捷键不能和听写快捷键相同") +} + +fn reject_dictation_open_app_hotkey_overlap( + dictation: &ShortcutBinding, + open_app: &ShortcutBinding, +) -> Result<(), String> { + reject_hotkey_overlap(dictation, open_app, "打开应用快捷键不能和听写快捷键相同") +} + +fn reject_qa_translation_hotkey_overlap( + qa: &ShortcutBinding, + translation: &ShortcutBinding, +) -> Result<(), String> { + reject_hotkey_overlap(qa, translation, "翻译快捷键不能和 QA 快捷键相同") +} + +fn reject_qa_switch_style_hotkey_overlap( + qa: &ShortcutBinding, + switch_style: &ShortcutBinding, +) -> Result<(), String> { + reject_hotkey_overlap(qa, switch_style, "切换风格快捷键不能和 QA 快捷键相同") +} + +fn reject_qa_open_app_hotkey_overlap( + qa: &ShortcutBinding, + open_app: &ShortcutBinding, +) -> Result<(), String> { + reject_hotkey_overlap(qa, open_app, "打开应用快捷键不能和 QA 快捷键相同") +} + +fn reject_translation_switch_style_hotkey_overlap( + translation: &ShortcutBinding, + switch_style: &ShortcutBinding, +) -> Result<(), String> { + reject_hotkey_overlap(translation, switch_style, "切换风格快捷键不能和翻译快捷键相同") +} + +fn reject_translation_open_app_hotkey_overlap( + translation: &ShortcutBinding, + open_app: &ShortcutBinding, +) -> Result<(), String> { + reject_hotkey_overlap(translation, open_app, "打开应用快捷键不能和翻译快捷键相同") +} + +fn reject_switch_style_open_app_hotkey_overlap( + switch_style: &ShortcutBinding, + open_app: &ShortcutBinding, +) -> Result<(), String> { + reject_hotkey_overlap(switch_style, open_app, "打开应用快捷键不能和切换风格快捷键相同") } fn shortcut_bindings_overlap(left: &ShortcutBinding, right: &ShortcutBinding) -> bool { @@ -1429,6 +1533,46 @@ mod tests { assert!(writer.saved.lock().unwrap().is_none()); } + #[test] + fn persist_settings_rejects_translation_switch_style_overlap() { + let writer = FakeSettingsWriter::default(); + let binding = ShortcutBinding { + primary: "T".into(), + modifiers: vec!["cmd".into(), "shift".into()], + }; + let prefs = UserPreferences { + translation_hotkey: binding.clone(), + switch_style_hotkey: binding, + ..Default::default() + }; + + assert_eq!( + persist_settings(&writer, prefs), + Err("切换风格快捷键不能和翻译快捷键相同".into()) + ); + assert!(writer.saved.lock().unwrap().is_none()); + } + + #[test] + fn persist_settings_rejects_switch_style_open_app_overlap() { + let writer = FakeSettingsWriter::default(); + let binding = ShortcutBinding { + primary: "K".into(), + modifiers: vec!["cmd".into(), "shift".into()], + }; + let prefs = UserPreferences { + switch_style_hotkey: binding.clone(), + open_app_hotkey: binding, + ..Default::default() + }; + + assert_eq!( + persist_settings(&writer, prefs), + Err("打开应用快捷键不能和切换风格快捷键相同".into()) + ); + assert!(writer.saved.lock().unwrap().is_none()); + } + #[tokio::test] async fn fetch_provider_models_omits_authorization_when_api_key_is_empty() { let listener = TcpListener::bind("127.0.0.1:0").unwrap(); diff --git a/openless-all/app/src-tauri/src/coordinator.rs b/openless-all/app/src-tauri/src/coordinator.rs index 06106509..13fa2cd7 100644 --- a/openless-all/app/src-tauri/src/coordinator.rs +++ b/openless-all/app/src-tauri/src/coordinator.rs @@ -1505,6 +1505,39 @@ fn reset_shortcut_held_state(inner: &Arc) { if let Some(monitor) = inner.hotkey.lock().as_ref() { monitor.reset_held_state(); } + let prefs = inner.prefs.get(); + if let Some(binding) = prefs.qa_hotkey.as_ref() { + if crate::shortcut_binding::legacy_modifier_trigger(binding).is_none() { + if let Some(monitor) = inner.qa_hotkey.lock().as_ref() { + if let Err(e) = monitor.update_binding(binding.clone()) { + log::warn!("[coord] reset QA hotkey latch failed: {e}"); + } + } + } + } + if !is_builtin_translation_shift(&prefs.translation_hotkey) + && crate::shortcut_binding::legacy_modifier_trigger(&prefs.translation_hotkey).is_none() + { + if let Some(monitor) = inner.translation_hotkey.lock().as_ref() { + if let Err(e) = monitor.update_binding(prefs.translation_hotkey.clone()) { + log::warn!("[coord] reset translation hotkey latch failed: {e}"); + } + } + } + if !is_modifier_only_shortcut(&prefs.switch_style_hotkey) { + if let Some(monitor) = inner.switch_style_hotkey.lock().as_ref() { + if let Err(e) = monitor.update_binding(prefs.switch_style_hotkey.clone()) { + log::warn!("[coord] reset switch-style hotkey latch failed: {e}"); + } + } + } + if !is_modifier_only_shortcut(&prefs.open_app_hotkey) { + if let Some(monitor) = inner.open_app_hotkey.lock().as_ref() { + if let Err(e) = monitor.update_binding(prefs.open_app_hotkey.clone()) { + log::warn!("[coord] reset open-app hotkey latch failed: {e}"); + } + } + } } async fn handle_pressed_edge(inner: &Arc) {