From 92b515cbf8b293b834efc93edeb02f072c158f20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Sat, 10 Oct 2026 02:17:18 -0400 Subject: [PATCH] Print is off where it is not built; no blocked command runs from the palette (D-08) --- crates/ely-platform/src/print.rs | 7 ++- crates/ely-ui/src/commands.rs | 8 +++- crates/ely-ui/src/i18n.rs | 2 + crates/ely-ui/src/shell/root.rs | 76 +++++++++++++++++++++++++++----- tasks/BUGS.md | 2 +- 5 files changed, 81 insertions(+), 14 deletions(-) diff --git a/crates/ely-platform/src/print.rs b/crates/ely-platform/src/print.rs index 91a5eae..c396bfb 100644 --- a/crates/ely-platform/src/print.rs +++ b/crates/ely-platform/src/print.rs @@ -2,6 +2,9 @@ use std::path::Path; +/// This build has a system print panel. +pub const BUILT: bool = cfg!(target_os = "macos"); + /// Shows the print panel for `pdf`; false when cancelled. #[cfg(target_os = "macos")] pub fn print_pdf(pdf: &Path) -> anyhow::Result { @@ -85,8 +88,8 @@ fn paper_for(sizes: &[(f64, f64)]) -> Option<(f64, f64)> { /// Printing needs a system panel this platform build lacks. #[cfg(not(target_os = "macos"))] -pub fn print_pdf(pdf: &Path) -> anyhow::Result { - anyhow::bail!("printing {} is not built on this platform", pdf.display()) +pub fn print_pdf(_: &Path) -> anyhow::Result { + anyhow::bail!("printing is not built on this platform") } #[cfg(test)] diff --git a/crates/ely-ui/src/commands.rs b/crates/ely-ui/src/commands.rs index 9dd5932..0332e67 100644 --- a/crates/ely-ui/src/commands.rs +++ b/crates/ely-ui/src/commands.rs @@ -131,6 +131,9 @@ fn label(command: &Command, cx: &App) -> SharedString { /// Why `id` cannot run; a document lifts the no-document block. pub fn blocked(id: &str, document: bool, cx: &App) -> Option { + if id == PRINT.id && !ely_platform::print::BUILT { + return Some(i18n::text(cx, "command.blocked.unbuilt", &[])); + } let command = RENAMING.iter().find(|command| command.id == id)?; let key = command.blocked.filter(|_| !document)?; Some(i18n::text(cx, key, &[])) @@ -187,6 +190,10 @@ pub fn menus( } pub fn bind_keys(cx: &mut App) { + // Without a print panel, Print stays off the keyboard. + if ely_platform::print::BUILT { + cx.bind_keys([KeyBinding::new(PRINT.keys, Print, Some(CONTEXT))]); + } cx.bind_keys([ KeyBinding::new(NEW.keys, New, Some(CONTEXT)), KeyBinding::new(OPEN.keys, Open, Some(CONTEXT)), @@ -194,7 +201,6 @@ pub fn bind_keys(cx: &mut App) { KeyBinding::new(SAVE_AS.keys, SaveAs, Some(CONTEXT)), KeyBinding::new(RENAME.keys, Rename, Some(CONTEXT)), KeyBinding::new(EXPORT.keys, Export, Some(CONTEXT)), - KeyBinding::new(PRINT.keys, Print, Some(CONTEXT)), KeyBinding::new("secondary-w", CloseTab, Some(CONTEXT)), KeyBinding::new(TOGGLE_PALETTE, ToggleCommandPalette, Some(CONTEXT)), KeyBinding::new("secondary-q", Quit, None), diff --git a/crates/ely-ui/src/i18n.rs b/crates/ely-ui/src/i18n.rs index 301dfac..93f6d40 100644 --- a/crates/ely-ui/src/i18n.rs +++ b/crates/ely-ui/src/i18n.rs @@ -41,6 +41,7 @@ const EN: &[(&str, &str)] = &[ ("command.file.export", "Export…"), ("command.file.print", "Print…"), ("command.blocked.no_document", "No open document"), + ("command.blocked.unbuilt", "Not built for this platform yet"), ("palette.group.file", "File"), ("gallery.ready", "Ready"), ("gallery.nav", "Navigation"), @@ -96,6 +97,7 @@ const ZH_CN: &[(&str, &str)] = &[ ("command.file.export", "导出…"), ("command.file.print", "打印…"), ("command.blocked.no_document", "没有打开的文档"), + ("command.blocked.unbuilt", "此平台尚未构建"), ("palette.group.file", "文件"), ("gallery.ready", "就绪"), ("gallery.nav", "导航"), diff --git a/crates/ely-ui/src/shell/root.rs b/crates/ely-ui/src/shell/root.rs index 0646ab0..c4333a7 100644 --- a/crates/ely-ui/src/shell/root.rs +++ b/crates/ely-ui/src/shell/root.rs @@ -157,16 +157,30 @@ impl Root { } fn attempt(&mut self, id: &'static str, cx: &mut Context) { - if let Some(reason) = commands::blocked(id, self.document, cx) { - log::info!("command: {id} blocked: {reason}"); - self.last = Some(format!("{id} blocked").into()); - self.toaster.update(cx, |toaster, cx| { - toaster.push(Toast::new(reason).severity(Severity::Warning), cx); - }); - cx.notify(); - return; + if !self.refused(id, cx) { + self.ran(id, cx); } - self.ran(id, cx); + } + + /// Runs a palette pick; views act first, so check here. + pub fn run_from_palette(&mut self, id: &str, window: &mut Window, cx: &mut Context) { + if !self.refused(id, cx) { + commands::run(id, window, cx); + } + } + + /// Whether `id` is blocked here; the reason shows. + fn refused(&mut self, id: &str, cx: &mut Context) -> bool { + let Some(reason) = commands::blocked(id, self.document, cx) else { + return false; + }; + log::info!("command: {id} blocked: {reason}"); + self.last = Some(format!("{id} blocked").into()); + self.toaster.update(cx, |toaster, cx| { + toaster.push(Toast::new(reason).severity(Severity::Warning), cx); + }); + cx.notify(); + true } /// Shows `text` in the window's toaster. @@ -305,6 +319,7 @@ impl Render for Root { if !self.palette { return frame.into_any_element(); } + let run = cx.entity(); let palette = CommandPalette::new("command-palette", move |_, cx| { this.update(cx, |this, cx| this.close_palette(cx)); }) @@ -319,7 +334,7 @@ impl Render for Root { }), ) .recent(self.recent.clone()) - .on_run(move |value, window, cx| commands::run(value, window, cx)); + .on_run(move |value, window, cx| run.update(cx, |root, cx| root.run_from_palette(value, window, cx))); frame.child(raise("palette", palette)).into_any_element() } } @@ -332,3 +347,44 @@ fn view(path: &Path) -> std::io::Result<()> { } ely_platform::apps::open_default(path) } + +#[cfg(test)] +mod tests { + use gpui::{FocusHandle, InteractiveElement as _, IntoElement, Render, TestAppContext, div}; + + /// A focused view that counts the saves it gets. + struct Saves(FocusHandle, usize); + + impl Render for Saves { + fn render(&mut self, _: &mut gpui::Window, cx: &mut gpui::Context) -> impl IntoElement { + div().track_focus(&self.0).on_action(cx.listener(|saves, _: &crate::commands::Save, _, _| saves.1 += 1)) + } + } + + #[gpui::test] + fn a_blocked_palette_pick_never_reaches_the_view(cx: &mut TestAppContext) { + cx.update(|cx| { + ely_gpui_component::init_for_tests(cx); + crate::settings::install_defaults(cx); + crate::i18n::install(cx); + }); + let window = cx.add_window(|_, cx| Saves(cx.focus_handle(), 0)); + let (saves, root) = window + .update(cx, |view, window, cx| { + let saves = cx.entity(); + let root = super::tool_root(ely_platform::AppId::Tree, saves.clone().into(), Vec::new(), window, cx); + // The root takes focus as it builds; give it back. + window.focus(&view.0, cx); + (saves, root) + }) + .expect("the window lives"); + cx.run_until_parked(); + // Save without a document is blocked; so is Print unbuilt. + window + .update(cx, |_, window, cx| root.update(cx, |root, cx| root.run_from_palette("file.save", window, cx))) + .expect("the window lives"); + cx.run_until_parked(); + assert_eq!(cx.update(|cx| saves.read(cx).1), 0, "the view never gets it"); + assert_eq!(cx.update(|cx| root.read(cx).toasts(cx).len()), 1, "the reason shows"); + } +} diff --git a/tasks/BUGS.md b/tasks/BUGS.md index 98326d4..24fde95 100644 --- a/tasks/BUGS.md +++ b/tasks/BUGS.md @@ -54,7 +54,7 @@ - [x] D-05 邮件合并某条记录失败时内核文档泄漏 — 失败的记录照样关闭内核文档,再传出首个错误 - [x] D-06 导航栏大纲点击按当前列表取下标,后台重读后可能越界 — 点击按绘制时的列表跳转;补点击路径的回归测试 - [x] D-07 导航栏隐藏时仍随每次光标移动重读大纲与书签 — 隐藏时不跟随标签、不读;再显示时读一次 -- [ ] D-08 Linux、Windows 上“打印…”可用,导出 PDF 后才报“此平台未构建” +- [x] D-08 Linux、Windows 上“打印…”可用,导出 PDF 后才报“此平台未构建” — 未构建平台禁用打印、不绑快捷键、错误不带路径;命令面板先查禁用 - [x] M-01 键入期间外部以相同内容重写文件,丢掉上次停顿之后键入的全部文字 — 先等页面送出未送的键;同文只接过文件与字节、标签为已保存;磁盘回到上次保存的文字时保留编辑并恢复自动保存;R1–R4 字节、自动保存、在途写入、已保存基线;R5 PASS - [ ] M-02 `refresh` 归零版本计数却不换代,在途的一键让标签一直“未落定” - [ ] M-03 干净标签接过磁盘文字后关闭仍问“是否保存”