diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 0286be25..1b6da092 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -601,6 +601,10 @@ pub fn run() { .run(|app, event| { // 点 Dock 图标 / 从 ⌘Tab 回来时把窗口叫回来。没有这条,一个 // 已经隐藏窗口的菜单栏应用在 Dock 上点了没反应。 + // + // **窗口关着(不在 Dock 里)时从访达、聚焦搜索、启动台再打开一次,系统发来的 + // 也是这一个**(实测:Accessory 的应用照样收到)。菜单栏设成不显示时,这就是 + // 回到窗口的路 —— 所以关窗之后照旧退回菜单栏应用,不为它留着 Dock 图标 #[cfg(target_os = "macos")] if let tauri::RunEvent::Reopen { .. } = event { let _ = show_main_window(app); diff --git a/src-tauri/src/menubar/macos.rs b/src-tauri/src/menubar/macos.rs index 8e847c3c..410f9974 100644 --- a/src-tauri/src/menubar/macos.rs +++ b/src-tauri/src/menubar/macos.rs @@ -63,7 +63,10 @@ type OnAction = Rc; type OnOpen = Rc; struct Ui { - item: Retained, + /// 菜单栏上的那一项。**设成不显示时整个拿掉**(`removeStatusItem`),换回来时再建一个、 + /// 挂上同一份菜单。不用 `setVisible(false)`:它按 autosaveName 记进系统的偏好里,下次 + /// 启动这一项先是藏着的 —— 换回不认识「不显示」的旧版本,它就再也出不来了 + item: Option>, menu: Retained, target: Retained, /// 现在菜单里每一行的样子。样子没变就就地改,变了才重建 @@ -86,7 +89,8 @@ thread_local! { static OPEN: Cell = const { Cell::new(false) }; } -/// 在菜单栏上挂出来。只调一次 +/// 备好菜单和回调。只调一次。**菜单栏上的那一项在第一次 [`apply`] 时才挂出来**: +/// 设成不显示的,启动时一帧都不出现 pub fn install( mtm: MainThreadMarker, on_action: impl Fn(Action) + 'static, @@ -95,18 +99,14 @@ pub fn install( ON_ACTION.with(|a| *a.borrow_mut() = Some(Rc::new(on_action))); ON_OPEN.with(|o| *o.borrow_mut() = Some(Rc::new(on_open))); - let item = NSStatusBar::systemStatusBar().statusItemWithLength(NSVariableStatusItemLength); - // 用户按住 ⌘ 拖过的位置,下次启动还在 - item.setAutosaveName(Some(&NSString::from_str("ThinkWatchLite"))); let target = Target::new(mtm); let menu = NSMenu::new(mtm); menu.setMinimumWidth(MENU_WIDTH); menu.setAutoenablesItems(false); menu.setDelegate(Some(ProtocolObject::from_ref(&*target))); - item.setMenu(Some(&menu)); UI.with(|ui| { *ui.borrow_mut() = Some(Ui { - item, + item: None, menu, target, shapes: Vec::new(), @@ -119,13 +119,18 @@ pub fn install( } /// 照着模型画一遍。**没变的不动**:菜单栏那一块文字和颜色都没变就不重画,菜单行的 -/// 样子没变就只改文字 +/// 样子没变就只改文字。设成不显示的,拿掉菜单栏上的那一项([`hide`]) pub fn apply(mtm: MainThreadMarker, bar: &Bar, rows: &[Row]) { + if bar.style == Style::Hidden { + hide(mtm); + return; + } UI.with(|cell| { let mut guard = cell.borrow_mut(); let Some(ui) = guard.as_mut() else { return }; + let item = ui.item.get_or_insert_with(|| status_item(&ui.menu)); if ui.bar.as_ref() != Some(bar) { - draw_bar(mtm, &ui.item, bar); + draw_bar(mtm, item, bar); ui.bar = Some(bar.clone()); } let shapes: Vec = rows.iter().map(Row::shape).collect(); @@ -139,6 +144,32 @@ pub fn apply(mtm: MainThreadMarker, bar: &Bar, rows: &[Row]) { }); } +/// 拿掉菜单栏上的那一项(设置里选了不显示)。**菜单留着**:换回来时挂到新建的那一项上, +/// 菜单行照旧就地改。已经拿掉了就什么都不做 +pub fn hide(_mtm: MainThreadMarker) { + UI.with(|cell| { + let mut guard = cell.borrow_mut(); + let Some(ui) = guard.as_mut() else { return }; + let Some(item) = ui.item.take() else { return }; + // 菜单开着的话先收起来,不留一份挂在空处的菜单 + if OPEN.with(Cell::get) { + ui.menu.cancelTrackingWithoutAnimation(); + } + NSStatusBar::systemStatusBar().removeStatusItem(&item); + // 再挂出来的是新的一项,那一块要整个重画 + ui.bar = None; + }); +} + +/// 挂一项到菜单栏上,点开是 `menu` +fn status_item(menu: &NSMenu) -> Retained { + let item = NSStatusBar::systemStatusBar().statusItemWithLength(NSVariableStatusItemLength); + // 用户按住 ⌘ 拖过的位置,下次启动、下次挂出来都还在 + item.setAutosaveName(Some(&NSString::from_str("ThinkWatchLite"))); + item.setMenu(Some(menu)); + item +} + // ------------------------------------------------------------------ 菜单栏那一块 /// 菜单栏那一块的高度。**按系统给的厚度画**,不写死 diff --git a/src-tauri/src/menubar/macos/preview.rs b/src-tauri/src/menubar/macos/preview.rs index dd6b286b..e2ce5053 100644 --- a/src-tauri/src/menubar/macos/preview.rs +++ b/src-tauri/src/menubar/macos/preview.rs @@ -29,7 +29,12 @@ pub fn is_open() -> bool { /// CFRunLoop 为了不让主队列重入,那期间不再处理主队列 —— 投递过来的更新全都要等 /// 菜单关上。用户真点菜单栏时跟踪循环是从事件分发开始的,没有这个问题 pub fn open_menu_later(mtm: MainThreadMarker, delay: f64) { - let button = UI.with(|ui| ui.borrow().as_ref().and_then(|ui| ui.item.button(mtm))); + let button = UI.with(|ui| { + ui.borrow() + .as_ref() + .and_then(|ui| ui.item.as_ref()) + .and_then(|item| item.button(mtm)) + }); if let Some(button) = button { let none: Option<&AnyObject> = None; let _: () = unsafe { diff --git a/src-tauri/src/menubar/mod.rs b/src-tauri/src/menubar/mod.rs index 9eaf51e2..faed352d 100644 --- a/src-tauri/src/menubar/mod.rs +++ b/src-tauri/src/menubar/mod.rs @@ -52,15 +52,18 @@ pub fn style() -> Style { match STYLE.load(Ordering::Relaxed) { 1 => Style::Icon, 2 => Style::Numbers, + 3 => Style::Hidden, _ => Style::Full, } } /// 实际画出来的那一档。 /// -/// **macOS 才有三档可选。**别处的通知区只认一张正方形图标(100% DPI 下 +/// **macOS 才有这几档可选。**别处的通知区只认一张正方形图标(100% DPI 下 /// 16×16),两行数字塞不进去 —— 那几行在右键菜单里给,所以无论存的是什么, -/// 画出来的都是图标。 +/// 画出来的都是图标。「不显示」也只有 macOS 有:那边藏掉托盘图标,窗口关着时 +/// 就只剩再启动一次这一条路,而这条路在各个桌面上走不走得通不一样(见 +/// `window::relaunch_token`)。 /// /// 和 [`style`] 分开,因为它们回答的是两个问题:设置页问「存了什么」, /// 渲染问「画得出什么」。把它们合成一个,那条「存进去什么就读回什么」的 @@ -82,6 +85,7 @@ pub fn set_style(style: Style) { Style::Full => 0, Style::Icon => 1, Style::Numbers => 2, + Style::Hidden => 3, }, Ordering::Relaxed, ); @@ -144,6 +148,14 @@ async fn run(app: tauri::AppHandle) { let Some(state) = app.try_state::() else { return; }; + // 设成不显示:菜单栏上没有这一项,也就不问 core 要数。换成别的样式时 + // `menubar_now` 叫醒这里,收一次、画出来 + if drawn_style() == Style::Hidden { + #[cfg(target_os = "macos")] + macos::on_main(macos::hide); + now.notified().await; + continue; + } let mut snap = collect(&app, &state, &mut credits).await; present(&snap); let day_end = snap @@ -942,12 +954,21 @@ mod tests { assert_eq!(why(CmdError::plain("剪贴板不可用")), "剪贴板不可用"); } + /// 存进去什么就读回什么;**画出来的「不显示」只有 macOS 有**,别处照样是图标 —— + /// 那边藏掉托盘图标,窗口关着时就够不着这个应用了。(一条测试:样式是全局的, + /// 拆成两条会在并行跑时互相改掉) #[test] fn the_style_survives_a_round_trip() { - for s in [Style::Full, Style::Icon, Style::Numbers] { + for s in [Style::Full, Style::Icon, Style::Numbers, Style::Hidden] { set_style(s); assert_eq!(style(), s); } + set_style(Style::Hidden); + assert_eq!( + drawn_style() == Style::Hidden, + cfg!(target_os = "macos"), + "只有 macOS 能不显示" + ); set_style(Style::Full); } diff --git a/src-tauri/src/menubar/model.rs b/src-tauri/src/menubar/model.rs index a6e3004c..931a8dc3 100644 --- a/src-tauri/src/menubar/model.rs +++ b/src-tauri/src/menubar/model.rs @@ -4,7 +4,7 @@ //! 菜单;什么时候出现哪一节、数字怎么写、什么时候变色,都在这里定、在这里测。 //! 画的那一层只管照着画。 -/// 菜单栏上显示什么。设置里的三档,出厂是标识和数值 +/// 菜单栏上显示什么。设置里的四档,出厂是标识和数值 #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] #[serde(rename_all = "lowercase")] pub enum Style { @@ -12,6 +12,9 @@ pub enum Style { Full, Icon, Numbers, + /// 不显示:菜单栏上没有这一项。窗口关着时再打开一次应用,主窗口就回来了 + /// (`RunEvent::Reopen`,见 `lib.rs`) + Hidden, } /// 网关此刻的状态。守护进程的几种状态按「用户该知道什么」归成这几类 diff --git a/src-tauri/src/prefs.rs b/src-tauri/src/prefs.rs index ec5b72f4..71e4077d 100644 --- a/src-tauri/src/prefs.rs +++ b/src-tauri/src/prefs.rs @@ -31,7 +31,7 @@ pub struct Prefs { pub theme: Option, /// 提醒:系统通知 / 仅在应用内 / 关闭。**只有这一个,不分类。** pub notices: Mode, - /// 菜单栏上显示什么:标识和数值(出厂)/ 仅标识 / 仅数值。 + /// 菜单栏上显示什么:标识和数值(出厂)/ 仅标识 / 仅数值 / 不显示。 /// /// **没写就是出厂那一档**,而不是整个设置文件读不出来、连语言和外观一起被冲回 /// 出厂值 @@ -250,6 +250,17 @@ mod tests { assert_eq!(load(&dir), want); } + /// 菜单栏不显示,存成 `"hidden"`(界面那边的 `MenubarStyle` 是同一个写法),读回来还是它 + #[test] + fn a_hidden_menubar_is_written_as_hidden_and_read_back() { + let (_tmp, dir) = tmp(); + update(&dir, |p| p.menubar = crate::menubar::Style::Hidden).unwrap(); + let raw: serde_json::Value = + serde_json::from_slice(&std::fs::read(prefs_path(&dir)).unwrap()).unwrap(); + assert_eq!(raw["menubar"], "hidden"); + assert_eq!(load(&dir).menubar, crate::menubar::Style::Hidden); + } + /// 旧的设置文件里没有菜单栏这一项:**别的设置照旧**,菜单栏按出厂那一档 #[test] fn a_file_without_the_menubar_setting_keeps_everything_else() { diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs index 8ff72941..4813aaf7 100644 --- a/src-tauri/src/settings.rs +++ b/src-tauri/src/settings.rs @@ -265,17 +265,28 @@ pub(crate) fn maybe_notify_first_autostart(app: &tauri::AppHandle) { "It started at login. When the window is closed, the app keeps running in the system tray." ), ); + // 菜单栏设成不显示时,那里没有它:说怎么回到窗口 #[cfg(target_os = "macos")] - let (title, body) = ( - tr!( - "ThinkWatch 已在菜单栏运行", - "ThinkWatch Is Running in the Menu Bar" - ), - tr!( - "开机时已自动启动。窗口关闭后,应用仍在菜单栏中运行。", - "It started at login. When the window is closed, the app keeps running in the menu bar." - ), - ); + let (title, body) = if menubar::style() == menubar::Style::Hidden { + ( + tr!("ThinkWatch 已在后台运行", "ThinkWatch Is Running"), + tr!( + "开机时已自动启动。再次打开 ThinkWatch Lite 即可显示主窗口。", + "It started at login. Open ThinkWatch Lite again to show the main window." + ), + ) + } else { + ( + tr!( + "ThinkWatch 已在菜单栏运行", + "ThinkWatch Is Running in the Menu Bar" + ), + tr!( + "开机时已自动启动。窗口关闭后,应用仍在菜单栏中运行。", + "It started at login. When the window is closed, the app keeps running in the menu bar." + ), + ) + }; n.announce("autostart", title, body); } tracing::info!("首次开机自启,已提示一次"); diff --git a/src/settings/GeneralSection.i18n.ts b/src/settings/GeneralSection.i18n.ts index e4ec8ac2..e3a76edb 100644 --- a/src/settings/GeneralSection.i18n.ts +++ b/src/settings/GeneralSection.i18n.ts @@ -22,6 +22,9 @@ export const generalText = messages( menubarFull: "标识和数值", menubarIcon: "仅标识", menubarNumbers: "仅数值", + menubarHidden: "不显示", + /** 选了不显示时替换上面那句:窗口关着时怎么回来 */ + menubarHiddenWhat: "从访达或聚焦搜索再次打开 ThinkWatch Lite,即可显示主窗口。", autostart: "开机启动", autostartWhat: isWindows @@ -29,6 +32,8 @@ export const generalText = messages( : isLinux ? "登录后仅在系统托盘显示图标,不打开窗口。" : "登录后仅在菜单栏显示图标,不打开窗口。", + /** 菜单栏设成不显示时(只有 macOS):登录后什么都不出现 */ + autostartHiddenWhat: "登录后在后台运行,不打开窗口。", notices: "提醒", noticeSystem: "系统通知", @@ -52,6 +57,8 @@ export const generalText = messages( menubarFull: "Icon and numbers", menubarIcon: "Icon only", menubarNumbers: "Numbers only", + menubarHidden: "Hidden", + menubarHiddenWhat: "To show the main window, open ThinkWatch Lite again from Finder or Spotlight.", autostart: "Launch at login", autostartWhat: isWindows @@ -59,6 +66,7 @@ export const generalText = messages( : isLinux ? "At login, only the icon appears in the system tray; no window opens." : "At login, only the icon appears in the menu bar; no window opens.", + autostartHiddenWhat: "At login, it runs in the background; no window opens.", notices: "Notices", noticeSystem: "System notification", diff --git a/src/settings/GeneralSection.tsx b/src/settings/GeneralSection.tsx index 3b272118..e339be9c 100644 --- a/src/settings/GeneralSection.tsx +++ b/src/settings/GeneralSection.tsx @@ -37,8 +37,9 @@ export function GeneralSection() { {/* - **只有 macOS 有这三档。**别处的通知区只认一张正方形图标(100% DPI 下 - 16×16),塞不下两行数字 —— 那几行在右键菜单里给 + **只有 macOS 有这几档。**别处的通知区只认一张正方形图标(100% DPI 下 + 16×16),塞不下两行数字 —— 那几行在右键菜单里给;「不显示」也不给,见 + `src-tauri/src/menubar/mod.rs` 的 `drawn_style` */} {isMac && } @@ -149,7 +150,13 @@ function AppearanceRow() { ); } -/** 菜单栏:标识和数值 / 仅标识 / 仅数值。数值是今日的 token 和费用 */ +/** + * 菜单栏:标识和数值 / 仅标识 / 仅数值 / 不显示。数值是今日的 token 和费用。 + * + * **选了不显示,说明那一句换成怎么回到窗口**:窗口关着时应用不在 Dock 里,菜单栏上也 + * 没有它,再打开一次应用是唯一的入口(Rust 那边接的是 `RunEvent::Reopen`)。一直写在 + * 这一行上,而不是弹一次就没了 —— 要找的时候还在这里 + */ function MenubarRow() { const t = useText(generalText); const r = useResource(APP_KEYS.menubar, settingsApi.menubar); @@ -160,9 +167,9 @@ function MenubarRow() { + {(style) => ( label={t.menubar} @@ -171,6 +178,7 @@ function MenubarRow() { { id: "full", label: t.menubarFull }, { id: "icon", label: t.menubarIcon }, { id: "numbers", label: t.menubarNumbers }, + { id: "hidden", label: t.menubarHidden }, ]} onChange={(s) => void set(s)} /> @@ -190,12 +198,14 @@ function AutostartRow() { const t = useText(generalText); const r = useResource(APP_KEYS.autostart, settingsApi.autostart); const [pending, set] = useWrite(r, settingsApi.setAutostart); + // 菜单栏设成不显示时,登录后菜单栏上也没有它(和上面那一行同一份数据,不多问一次) + const menubar = useResource(isMac ? APP_KEYS.menubar : null, settingsApi.menubar); return ( {(on) => ( diff --git a/src/settings/api.ts b/src/settings/api.ts index a88e2cdf..395e8d42 100644 --- a/src/settings/api.ts +++ b/src/settings/api.ts @@ -28,8 +28,8 @@ export interface LanguageView { system: Lang; } -/** 菜单栏:标识和数值 / 仅标识 / 仅数值(只有 macOS 有) */ -export type MenubarStyle = "full" | "icon" | "numbers"; +/** 菜单栏:标识和数值 / 仅标识 / 仅数值 / 不显示(只有 macOS 有) */ +export type MenubarStyle = "full" | "icon" | "numbers" | "hidden"; /** 提醒:系统通知 / 仅在应用内 / 关闭 */ export type NoticeMode = "system" | "app" | "off";