From daadbe15d47f5e22d5fae3e7f9cb17303bc7dbf9 Mon Sep 17 00:00:00 2001 From: StudentWeis Date: Mon, 11 May 2026 22:31:16 +0800 Subject: [PATCH 1/2] feat(gui): replace tray Show button with Settings and About entries Remove the "Show" menu item from the system tray and add "Settings" and "About" entries that open the window directly to the corresponding panel. Left-clicking the tray icon now opens Settings. Add an `activated` flag so the hidden window renders only background until first activation, preventing a flash of the clipboard list. Refs #126 --- assets/locales/en.toml | 3 +- assets/locales/ja.toml | 3 +- assets/locales/zh-CN.toml | 3 +- src/gui/board.rs | 2 + src/gui/board/actions.rs | 1 + src/gui/board/render.rs | 8 +++ src/gui/tray.rs | 114 ++++++++++++++++++++++++++------------ src/i18n.rs | 6 +- 8 files changed, 100 insertions(+), 40 deletions(-) diff --git a/assets/locales/en.toml b/assets/locales/en.toml index 03ec336..69007fb 100644 --- a/assets/locales/en.toml +++ b/assets/locales/en.toml @@ -4,7 +4,8 @@ language_name = "English" # Application # Tray menu -tray_show = "Show" +tray_settings = "Settings" +tray_about = "About" tray_quit = "Quit" # Main window diff --git a/assets/locales/ja.toml b/assets/locales/ja.toml index ce23e44..3b343f6 100644 --- a/assets/locales/ja.toml +++ b/assets/locales/ja.toml @@ -5,7 +5,8 @@ language_name = "日本語" # Application # Tray menu -tray_show = "表示" +tray_settings = "設定" +tray_about = "について" tray_quit = "終了" # Main window diff --git a/assets/locales/zh-CN.toml b/assets/locales/zh-CN.toml index d7ae4e7..88e2b35 100644 --- a/assets/locales/zh-CN.toml +++ b/assets/locales/zh-CN.toml @@ -4,7 +4,8 @@ language_name = "简体中文" # 应用程序 # 托盘菜单 -tray_show = "显示" +tray_settings = "设置" +tray_about = "关于" tray_quit = "退出" # 主窗口 diff --git a/src/gui/board.rs b/src/gui/board.rs index 17e5cf7..4c515d5 100644 --- a/src/gui/board.rs +++ b/src/gui/board.rs @@ -173,6 +173,7 @@ pub(crate) struct RopyBoard { pub(crate) confirm_mode: ConfirmMode, pub(crate) layout_mode: LayoutMode, pub(crate) pinned: bool, + pub(crate) activated: bool, pub(crate) hotkey_tx: Option>, pub(crate) update_manager: UpdateManager, /// Which clear action is currently awaiting confirmation @@ -479,6 +480,7 @@ impl RopyBoard { confirm_mode, layout_mode, pinned: false, + activated: false, hotkey_tx: None, update_manager: UpdateManager::new(), clear_confirm_action: ClearConfirmAction::AllHistory, diff --git a/src/gui/board/actions.rs b/src/gui/board/actions.rs index 3df4bf4..ec7b985 100644 --- a/src/gui/board/actions.rs +++ b/src/gui/board/actions.rs @@ -257,6 +257,7 @@ impl RopyBoard { self.reveal_selected_record(); self.active_panel = ActivePanel::ClipboardList; self.ui_state.clear_confirm = crate::gui::board::ClearConfirmState::Hidden; + self.activated = true; reset_window_geometry_for_activation(window, default_window_size()); active_window(window, cx); } diff --git a/src/gui/board/render.rs b/src/gui/board/render.rs index edb154a..69bff45 100644 --- a/src/gui/board/render.rs +++ b/src/gui/board/render.rs @@ -23,6 +23,13 @@ impl Render for RopyBoard { .px_4() .pb_4(); + if !self.activated { + return gpui::div() + .size_full() + .child(base.bg(self.main_panel_surface(cx.theme().background))) + .into_any_element(); + } + let body: AnyElement = match self.active_panel { ActivePanel::Settings => base .bg(self.main_panel_surface(cx.theme().background)) @@ -80,5 +87,6 @@ impl Render for RopyBoard { .children(notifs), ) }) + .into_any_element() } } diff --git a/src/gui/tray.rs b/src/gui/tray.rs index 1ada9e6..c7e36dc 100644 --- a/src/gui/tray.rs +++ b/src/gui/tray.rs @@ -12,7 +12,8 @@ use tray_icon::{ use crate::{constants::APP_NAME, i18n::I18n}; /// Fixed menu IDs so event handlers remain valid after menu rebuilds. -const TRAY_SHOW_ID: &str = "tray_show"; +const TRAY_SETTINGS_ID: &str = "tray_settings"; +const TRAY_ABOUT_ID: &str = "tray_about"; const TRAY_QUIT_ID: &str = "tray_quit"; const TRAY_ICON_SIZE: u32 = 32; @@ -62,20 +63,23 @@ impl TrayState { } pub(crate) fn build_tray_menu(i18n: &I18n) -> Result> { - let show_item = MenuItem::with_id(TRAY_SHOW_ID, i18n.t("tray_show"), true, None); + let settings_item = MenuItem::with_id(TRAY_SETTINGS_ID, i18n.t("tray_settings"), true, None); + let about_item = MenuItem::with_id(TRAY_ABOUT_ID, i18n.t("tray_about"), true, None); let quit_item = MenuItem::with_id(TRAY_QUIT_ID, i18n.t("tray_quit"), true, None); let tray_menu = Menu::new(); - tray_menu.append(&show_item)?; + tray_menu.append(&settings_item)?; + tray_menu.append(&about_item)?; tray_menu.append(&quit_item)?; Ok(tray_menu) } pub(crate) fn init_tray( i18n: &I18n, -) -> Result<(TrayIcon, MenuId, MenuId), Box> { +) -> Result<(TrayIcon, MenuId, MenuId, MenuId), Box> { let tray_menu = build_tray_menu(i18n)?; - let show_id = MenuId::new(TRAY_SHOW_ID); + let settings_id = MenuId::new(TRAY_SETTINGS_ID); + let about_id = MenuId::new(TRAY_ABOUT_ID); let quit_id = MenuId::new(TRAY_QUIT_ID); let icon = create_icon()?; @@ -87,7 +91,7 @@ pub(crate) fn init_tray( .with_menu_on_left_click(false) .build()?; - Ok((tray, show_id, quit_id)) + Ok((tray, settings_id, about_id, quit_id)) } fn create_icon() -> Result> { @@ -122,7 +126,8 @@ fn load_tray_icon_rgba(bytes: &[u8]) -> Result<(Vec, u32, u32), image::Image #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum TrayEvent { - Show, + Settings, + About, Quit, } @@ -178,8 +183,11 @@ fn spawn_tray_event_loop( cx.spawn(async move |async_app| { while let Ok(event) = rx.recv().await { match event { - TrayEvent::Show => { - let _ = async_app.update(|cx| send_active_action(window_handle, cx)); + TrayEvent::Settings => { + let _ = async_app.update(|cx| send_open_settings(window_handle, cx)); + } + TrayEvent::About => { + let _ = async_app.update(|cx| send_open_about(window_handle, cx)); } TrayEvent::Quit => { let _ = async_app.update(|cx| cx.quit()); @@ -195,12 +203,12 @@ pub(crate) fn start_tray_handler_inner( tx: async_channel::Sender, ) -> Option { match init_tray(i18n) { - Ok((tray, show_id, quit_id)) => { + Ok((tray, settings_id, about_id, quit_id)) => { tracing::info!("tray icon initialized successfully"); #[cfg(not(target_os = "linux"))] spawn_tray_icon_event_forwarder(tx.clone()); - spawn_tray_menu_event_forwarder(tx, show_id, quit_id); + spawn_tray_menu_event_forwarder(tx, settings_id, about_id, quit_id); Some(tray) } @@ -211,23 +219,64 @@ pub(crate) fn start_tray_handler_inner( } } -pub(crate) fn send_active_action(window_handle: WindowHandle, cx: &mut App) { +pub(crate) fn send_open_settings(window_handle: WindowHandle, cx: &mut App) { + window_handle + .update(cx, |root, window: &mut gpui::Window, cx| { + if let Ok(board) = root + .view() + .clone() + .downcast::() + { + board.update(cx, |board, cx| { + board.active_panel = crate::gui::board::ActivePanel::Settings; + board + .settings_editor + .panel_state + .window_opacity_slider_visible = true; + board.activated = true; + window.focus(&board.focus_handle); + cx.notify(); + }); + } + super::active_window(window, cx); + }) + .ok(); +} + +pub(crate) fn send_open_about(window_handle: WindowHandle, cx: &mut App) { window_handle - .update(cx, |_, window: &mut gpui::Window, cx| { - window.dispatch_action(Box::new(crate::gui::board::Active), cx); + .update(cx, |root, window: &mut gpui::Window, cx| { + if let Ok(board) = root + .view() + .clone() + .downcast::() + { + board.update(cx, |board, cx| { + board.active_panel = crate::gui::board::ActivePanel::About; + board.activated = true; + cx.notify(); + }); + } + super::active_window(window, cx); }) .ok(); } fn spawn_tray_menu_event_forwarder( tx: async_channel::Sender, - show_id: MenuId, + settings_id: MenuId, + about_id: MenuId, quit_id: MenuId, ) { let receiver = tray_icon::menu::MenuEvent::receiver().clone(); super::utils::spawn_event_forwarder("tray-menu-event-forwarder", tx, move |forward| { while let Ok(event) = receiver.recv() { - if !forward(tray_event_from_menu_event(&event, &show_id, &quit_id)) { + if !forward(tray_event_from_menu_event( + &event, + &settings_id, + &about_id, + &quit_id, + )) { break; } } @@ -248,11 +297,14 @@ fn spawn_tray_icon_event_forwarder(tx: async_channel::Sender) { fn tray_event_from_menu_event( event: &tray_icon::menu::MenuEvent, - show_id: &MenuId, + settings_id: &MenuId, + about_id: &MenuId, quit_id: &MenuId, ) -> Option { - if event.id == *show_id { - Some(TrayEvent::Show) + if event.id == *settings_id { + Some(TrayEvent::Settings) + } else if event.id == *about_id { + Some(TrayEvent::About) } else if event.id == *quit_id { Some(TrayEvent::Quit) } else { @@ -265,7 +317,7 @@ fn tray_event_from_icon_event(event: &TrayIconEvent) -> Option { if let TrayIconEvent::Click { button, .. } = event && *button == tray_icon::MouseButton::Left { - Some(TrayEvent::Show) + Some(TrayEvent::Settings) } else { None } @@ -321,27 +373,24 @@ mod tests { } #[rstest] - #[case(TRAY_SHOW_ID, Some(TrayEvent::Show))] + #[case(TRAY_SETTINGS_ID, Some(TrayEvent::Settings))] + #[case(TRAY_ABOUT_ID, Some(TrayEvent::About))] #[case(TRAY_QUIT_ID, Some(TrayEvent::Quit))] #[case("other", None)] fn test_tray_event_from_menu_event_matches_menu_id( #[case] event_id: &str, #[case] expected: Option, ) { - let show_id = MenuId::new(TRAY_SHOW_ID); + let settings_id = MenuId::new(TRAY_SETTINGS_ID); + let about_id = MenuId::new(TRAY_ABOUT_ID); let quit_id = MenuId::new(TRAY_QUIT_ID); let event = tray_icon::menu::MenuEvent { id: MenuId::new(event_id), }; - let result = tray_event_from_menu_event(&event, &show_id, &quit_id); + let result = tray_event_from_menu_event(&event, &settings_id, &about_id, &quit_id); - assert!(matches!( - (result, expected), - (Some(TrayEvent::Show), Some(TrayEvent::Show)) - | (Some(TrayEvent::Quit), Some(TrayEvent::Quit)) - | (None, None) - )); + assert_eq!(result, expected); } #[cfg(not(target_os = "linux"))] @@ -354,7 +403,7 @@ mod tests { button: tray_icon::MouseButton::Left, button_state: tray_icon::MouseButtonState::Up, }, - Some(TrayEvent::Show) + Some(TrayEvent::Settings) )] #[case( TrayIconEvent::Click { @@ -380,9 +429,6 @@ mod tests { ) { let result = tray_event_from_icon_event(&event); - assert!(matches!( - (result, expected), - (Some(TrayEvent::Show), Some(TrayEvent::Show)) | (None, None) - )); + assert_eq!(result, expected); } } diff --git a/src/i18n.rs b/src/i18n.rs index 8fc69e0..d877ddb 100644 --- a/src/i18n.rs +++ b/src/i18n.rs @@ -149,19 +149,19 @@ mod tests { let i18n = I18n::new(Language::new("en")); assert!(i18n.is_ok()); let i18n = i18n.unwrap(); - assert_eq!(i18n.t("tray_show"), "Show"); + assert_eq!(i18n.t("tray_settings"), "Settings"); } #[test] #[expect(clippy::unwrap_used)] fn test_i18n_language_switch() { let mut i18n = I18n::new(Language::new("en")).unwrap(); - assert_eq!(i18n.t("tray_show"), "Show"); + assert_eq!(i18n.t("tray_settings"), "Settings"); // Switch to Chinese let result = i18n.set_language(Language::new("zh-CN")); assert!(result.is_ok()); - assert_eq!(i18n.t("tray_show"), "显示"); + assert_eq!(i18n.t("tray_settings"), "设置"); } #[test] From ee1f30fea0e5c173676a5561a9a4d1b33d1a13a9 Mon Sep 17 00:00:00 2001 From: StudentWeis Date: Mon, 11 May 2026 22:32:48 +0800 Subject: [PATCH 2/2] docs: update contribution guidelines for mandatory issue templates and remove outdated checklist item --- .agents/skills/contribution-flow/SKILL.md | 4 ++-- .github/PULL_REQUEST_TEMPLATE.md | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.agents/skills/contribution-flow/SKILL.md b/.agents/skills/contribution-flow/SKILL.md index e5d90af..6833f61 100644 --- a/.agents/skills/contribution-flow/SKILL.md +++ b/.agents/skills/contribution-flow/SKILL.md @@ -20,7 +20,7 @@ Shared vocabulary used throughout the flow below: `` and `` placeho ### 1. Open the Issue -If the project has issue templates (`.github/ISSUE_TEMPLATE/`), pick the appropriate one. Otherwise use a concise description with acceptance criteria. Fill it into `/tmp/issue-body.md`, then: +Pick the appropriate issue template from `.github/ISSUE_TEMPLATE/` — using a template is **mandatory**. Fill it into `/tmp/issue-body.md`, then: ```bash gh issue create --title ": ..." --label "" --body-file /tmp/issue-body.md @@ -61,7 +61,7 @@ Multiple commits are fine — they are squashed on merge. ### 6. Push & Open the PR -If the project has a PR template (`.github/PULL_REQUEST_TEMPLATE.md`), fill it into `/tmp/pr-body.md`. Otherwise write a concise summary with a test plan. Then: +Fill in the PR template from `.github/PULL_REQUEST_TEMPLATE.md` into `/tmp/pr-body.md` — using the template is **mandatory**. Then: ```bash git push -u origin HEAD diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 428b788..3b680f0 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -31,6 +31,5 @@ Closes # - [ ] PR title follows Conventional Commits - [ ] No hardcoded user-facing strings — i18n keys added to all locale files -- [ ] New UI components use `gpui-component` - [ ] Errors defined with `thiserror` (no manual `impl Display/Error`) - [ ] No unrelated changes mixed in