diff --git a/crates/ui-egui/src/backstage.rs b/crates/ui-egui/src/backstage.rs index 922baabc5..6472ec310 100644 --- a/crates/ui-egui/src/backstage.rs +++ b/crates/ui-egui/src/backstage.rs @@ -2,7 +2,7 @@ //! and sheet tabs until Esc or the back arrow returns to the sheet. //! //! A navigation column on the left picks a page (Home, New, Open, Info, Save As, Print, Export, -//! Options); Save and Close act at once. Every action runs an existing command through +//! Options, Account); Save and Close act at once. Every action runs an existing command through //! [`SheetApp::run`] or an existing dialog through [`SheetApp::open_dialog`]; the state here is //! UI-only and never saved. Recent workbooks are listed by path, with when each was last changed: //! that is asked of the file system on a thread of its own, never while drawing, and only the @@ -36,10 +36,11 @@ pub enum Page { Print, Export, Options, + Account, } impl Page { - pub const ALL: [Page; 8] = [Page::Home, Page::New, Page::Open, Page::Info, Page::SaveAs, Page::Print, Page::Export, Page::Options]; + pub const ALL: [Page; 9] = [Page::Home, Page::New, Page::Open, Page::Info, Page::SaveAs, Page::Print, Page::Export, Page::Options, Page::Account]; /// The id `ui.backstage` and `ui.dialog {"name": "backstage", "page": …}` take. pub fn id(self) -> &'static str { @@ -52,6 +53,7 @@ impl Page { Page::Print => "print", Page::Export => "export", Page::Options => "options", + Page::Account => "account", } } @@ -71,6 +73,7 @@ impl Page { Page::Print => "Print", Page::Export => "Export", Page::Options => "Options", + Page::Account => "Account", } } @@ -84,6 +87,7 @@ impl Page { Page::Print => Icon::Print, Page::Export => Icon::Share, Page::Options => Icon::Settings, + Page::Account => Icon::Info, } } } @@ -190,9 +194,12 @@ pub fn show(app: &mut SheetApp, ui: &mut Ui) { out.close = true; } } + // Bottom up: Account sits just above Options, as in Excel. ui.with_layout(egui::Layout::bottom_up(egui::Align::Min), |ui| { - if nav_item(ui, Page::Options.icon(), lang.tr("Options"), bs.page == Page::Options, nav_text, nav_sel).clicked() { - out.page = Some(Page::Options); + for page in [Page::Options, Page::Account] { + if nav_item(ui, page.icon(), lang.tr(page.label()), bs.page == page, nav_text, nav_sel).clicked() { + out.page = Some(page); + } } }); }); @@ -211,6 +218,7 @@ pub fn show(app: &mut SheetApp, ui: &mut Ui) { Page::Print => print(app, ui, &mut bs, &mut out), Page::Export => export(app, ui, &mut out), Page::Options => options(app, ui), + Page::Account => account(app, ui), } }); }); @@ -996,6 +1004,51 @@ fn options(app: &mut SheetApp, ui: &mut Ui) { dim(ui, lang.tr("Calculation settings are saved with the workbook.")); } +/// The platform the app was built for, e.g. "macOS (aarch64)"; the web build reports "Web". +fn platform() -> String { + let os = match std::env::consts::OS { + "macos" => "macOS", + "windows" => "Windows", + "linux" => "Linux", + "freebsd" => "FreeBSD", + "openbsd" => "OpenBSD", + "netbsd" => "NetBSD", + _ if cfg!(target_arch = "wasm32") => return "Web".into(), + other => other, + }; + format!("{os} ({})", std::env::consts::ARCH) +} + +/// Excel's File › Account: product information and the About window. +fn account(app: &mut SheetApp, ui: &mut Ui) { + let t = Tokens::get(ui.ctx()); + let lang = app.ui.language; + section(ui, lang, "Product Information"); + ui.label(RichText::new("GridCraft").font(theme::ui_bold(16.0)).color(t.text)); + ui.add_space(8.0); + egui::Grid::new("backstage_account").num_columns(2).spacing(vec2(18.0, 8.0)).show(ui, |ui| { + for (label, value) in + [("Version", env!("CARGO_PKG_VERSION").to_string()), ("Platform", platform()), ("Licence", env!("CARGO_PKG_LICENSE").to_string())] + { + ui.label(RichText::new(lang.tr(label)).color(t.text_dim)); + ui.label(value); + ui.end_row(); + } + }); + ui.add_space(18.0); + ui.horizontal(|ui| { + if square_button(ui, Icon::Info, lang.tr("About GridCraft")).clicked() { + app.open_dialog("about", json!({})); + } + ui.add_space(12.0); + ui.vertical(|ui| { + ui.add_space(14.0); + ui.label(RichText::new(lang.tr("About GridCraft")).font(theme::ui_bold(13.5)).color(t.text)); + dim(ui, lang.tr("Version, contributors and the AI models that helped")); + }); + }); +} + #[cfg(test)] mod tests { use super::*; @@ -1076,7 +1129,7 @@ mod tests { assert_eq!(Page::parse(page.id()), Some(page)); } assert_eq!(Page::parse(" SAVEAS "), Some(Page::SaveAs)); - assert_eq!(Page::parse("account"), None); + assert_eq!(Page::parse("feedback"), None); assert_eq!(Page::parse(""), None); } diff --git a/crates/ui-egui/src/control.rs b/crates/ui-egui/src/control.rs index 2ea5cfae0..09f6fce5e 100644 --- a/crates/ui-egui/src/control.rs +++ b/crates/ui-egui/src/control.rs @@ -184,7 +184,7 @@ pub fn handle(app: &mut SheetApp, ctx: &egui::Context, method: &str, p: &Json) - ("options.autoComplete", "Enable AutoComplete for cell values", "{on?: bool} (toggles without it) → {on}"), ("view.interfaceScale", "Interface Size", "{percent: 75–200 | \"auto\"} (100 = auto; clamped)"), ("ui.dialog", "Open Dialog", ""), - ("ui.backstage", "File (Backstage view)", "{page?: home|new|open|info|saveAs|print|export|options|close}"), + ("ui.backstage", "File (Backstage view)", "{page?: home|new|open|info|saveAs|print|export|options|account|close}"), ( "ui.printSettings", "Print Settings", diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index 98919f8a4..9c1e4714d 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -519,6 +519,13 @@ const JAPANESE: &[(&str, &str)] = &[ ("Export", "エクスポート"), ("Close", "閉じる"), ("Options", "オプション"), + ("Account", "アカウント"), + ("Product Information", "製品情報"), + ("About GridCraft", "GridCraft について"), + ("Version", "バージョン"), + ("Platform", "プラットフォーム"), + ("Licence", "ライセンス"), + ("Version, contributors and the AI models that helped", "バージョン、貢献者、支援した AI モデル"), ("Blank workbook", "空白のブック"), ("Recent", "最近使ったアイテム"), ("No recent workbooks yet.", "最近使ったブックはまだありません。"), diff --git a/crates/ui-egui/src/i18n/cs.rs b/crates/ui-egui/src/i18n/cs.rs index 7ed8372c7..725cb176c 100644 --- a/crates/ui-egui/src/i18n/cs.rs +++ b/crates/ui-egui/src/i18n/cs.rs @@ -258,6 +258,12 @@ pub(super) fn translate(text: &str) -> Option<&'static str> { "Export" => "Exportovat", "Close" => "Zavřít", "Options" => "Možnosti", + "Account" => "Účet", + "Product Information" => "Informace o produktu", + "About GridCraft" => "O aplikaci GridCraft", + "Version" => "Verze", + "Platform" => "Platforma", + "Licence" => "Licence", "Blank workbook" => "Prázdný sešit", "Recent" => "Naposledy použité", "No recent workbooks yet." => "Zatím žádné naposledy použité sešity.", diff --git a/crates/ui-egui/src/i18n/de.rs b/crates/ui-egui/src/i18n/de.rs index 371ea7981..cee95f0f6 100644 --- a/crates/ui-egui/src/i18n/de.rs +++ b/crates/ui-egui/src/i18n/de.rs @@ -256,6 +256,13 @@ pub(super) fn translate(text: &str) -> Option<&'static str> { "Export" => "Exportieren", "Close" => "Schließen", "Options" => "Optionen", + "Account" => "Konto", + "Product Information" => "Produktinformationen", + "About GridCraft" => "Info zu GridCraft", + "Version" => "Version", + "Platform" => "Plattform", + "Licence" => "Lizenz", + "Version, contributors and the AI models that helped" => "Version, Mitwirkende und die KI-Modelle, die geholfen haben", "Blank workbook" => "Leere Arbeitsmappe", "Recent" => "Zuletzt verwendet", "No recent workbooks yet." => "Noch keine zuletzt verwendeten Arbeitsmappen.", diff --git a/crates/ui-egui/src/i18n/es.rs b/crates/ui-egui/src/i18n/es.rs index 4ce231bca..e0b1ebf10 100644 --- a/crates/ui-egui/src/i18n/es.rs +++ b/crates/ui-egui/src/i18n/es.rs @@ -257,6 +257,13 @@ pub(super) fn translate(text: &str) -> Option<&'static str> { "Export" => "Exportar", "Close" => "Cerrar", "Options" => "Opciones", + "Account" => "Cuenta", + "Product Information" => "Información del producto", + "About GridCraft" => "Acerca de GridCraft", + "Version" => "Versión", + "Platform" => "Plataforma", + "Licence" => "Licencia", + "Version, contributors and the AI models that helped" => "Versión, colaboradores y los modelos de IA que ayudaron", "Blank workbook" => "Libro en blanco", "Recent" => "Recientes", "No recent workbooks yet." => "Todavía no hay libros recientes.", diff --git a/crates/ui-egui/src/i18n/fr.rs b/crates/ui-egui/src/i18n/fr.rs index 82027e847..38884afda 100644 --- a/crates/ui-egui/src/i18n/fr.rs +++ b/crates/ui-egui/src/i18n/fr.rs @@ -342,6 +342,13 @@ pub(super) fn translate(text: &str) -> Option<&'static str> { "Export" => "Exporter", "Close" => "Fermer", "Options" => "Options", + "Account" => "Compte", + "Product Information" => "Informations sur le produit", + "About GridCraft" => "À propos de GridCraft", + "Version" => "Version", + "Platform" => "Plateforme", + "Licence" => "Licence", + "Version, contributors and the AI models that helped" => "Version, contributeurs et les modèles d'IA qui ont aidé", "Blank workbook" => "Classeur vide", "Recent" => "Récent", "No recent workbooks yet." => "Aucun classeur récent pour le moment.", diff --git a/crates/ui-egui/src/i18n/ko.rs b/crates/ui-egui/src/i18n/ko.rs index 1902ad301..3fe8923e8 100644 --- a/crates/ui-egui/src/i18n/ko.rs +++ b/crates/ui-egui/src/i18n/ko.rs @@ -344,6 +344,13 @@ pub(super) fn translate(text: &str) -> Option<&'static str> { "Export" => "내보내기", "Close" => "닫기", "Options" => "옵션", + "Account" => "계정", + "Product Information" => "제품 정보", + "About GridCraft" => "GridCraft 정보", + "Version" => "버전", + "Platform" => "플랫폼", + "Licence" => "라이선스", + "Version, contributors and the AI models that helped" => "버전, 기여자 및 도움을 준 AI 모델", "Blank workbook" => "새 통합 문서", "Recent" => "최근 항목", "No recent workbooks yet." => "최근 통합 문서가 아직 없습니다.", diff --git a/crates/ui-egui/src/i18n/pt.rs b/crates/ui-egui/src/i18n/pt.rs index fb9236cdb..17bf1dac7 100644 --- a/crates/ui-egui/src/i18n/pt.rs +++ b/crates/ui-egui/src/i18n/pt.rs @@ -345,6 +345,11 @@ pub(super) fn translate(text: &str) -> Option<&'static str> { "Export" => "Exportar", "Close" => "Fechar", "Options" => "Opções", + "Account" => "Conta", + "Product Information" => "Informações do produto", + "Version" => "Versão", + "Platform" => "Plataforma", + "Licence" => "Licença", "Blank workbook" => "Pasta de trabalho em branco", "Recent" => "Recentes", "No recent workbooks yet." => "Ainda não há pastas de trabalho recentes.", diff --git a/crates/ui-egui/src/i18n/ru.rs b/crates/ui-egui/src/i18n/ru.rs index 765f6bb32..feaf8fdca 100644 --- a/crates/ui-egui/src/i18n/ru.rs +++ b/crates/ui-egui/src/i18n/ru.rs @@ -344,6 +344,13 @@ pub(super) fn translate(text: &str) -> Option<&'static str> { "Export" => "Экспорт", "Close" => "Закрыть", "Options" => "Параметры", + "Account" => "Учетная запись", + "Product Information" => "Сведения о продукте", + "About GridCraft" => "О программе GridCraft", + "Version" => "Версия", + "Platform" => "Платформа", + "Licence" => "Лицензия", + "Version, contributors and the AI models that helped" => "Версия, участники и модели ИИ, которые помогали", "Blank workbook" => "Пустая книга", "Recent" => "Последние", "No recent workbooks yet." => "Недавних книг пока нет.", diff --git a/crates/ui-egui/src/i18n/sr.rs b/crates/ui-egui/src/i18n/sr.rs index 85bd2da72..747fc82c6 100644 --- a/crates/ui-egui/src/i18n/sr.rs +++ b/crates/ui-egui/src/i18n/sr.rs @@ -336,6 +336,13 @@ pub(super) fn translate(text: &str) -> Option<&'static str> { "Export" => "Izvezi", "Close" => "Zatvori", "Options" => "Opcije", + "Account" => "Nalog", + "Product Information" => "Informacije o proizvodu", + "About GridCraft" => "O programu GridCraft", + "Version" => "Verzija", + "Platform" => "Platforma", + "Licence" => "Licenca", + "Version, contributors and the AI models that helped" => "Verzija, saradnici i AI modeli koji su pomogli", "Blank workbook" => "Prazna radna sveska", "Recent" => "Nedavno", "No recent workbooks yet." => "Još nema nedavnih radnih svezaka.", diff --git a/crates/ui-egui/src/i18n/zh.rs b/crates/ui-egui/src/i18n/zh.rs index 00175925e..0365002c6 100644 --- a/crates/ui-egui/src/i18n/zh.rs +++ b/crates/ui-egui/src/i18n/zh.rs @@ -342,6 +342,13 @@ pub(super) fn translate(text: &str) -> Option<&'static str> { "Export" => "导出", "Close" => "关闭", "Options" => "选项", + "Account" => "账户", + "Product Information" => "产品信息", + "About GridCraft" => "关于 GridCraft", + "Version" => "版本", + "Platform" => "平台", + "Licence" => "许可证", + "Version, contributors and the AI models that helped" => "版本、贡献者以及参与协助的 AI 模型", "Blank workbook" => "空白工作簿", "Recent" => "最近", "No recent workbooks yet." => "还没有最近使用的工作簿。", diff --git a/crates/ui-egui/tests/file_menu.rs b/crates/ui-egui/tests/file_menu.rs index 6cce9cdba..e8bf4761a 100644 --- a/crates/ui-egui/tests/file_menu.rs +++ b/crates/ui-egui/tests/file_menu.rs @@ -122,7 +122,7 @@ fn file_options_opens_the_options_page_and_pages_are_reachable_by_command() { h.run_steps(2); assert_eq!(page(&h), Some(id)); } - assert!(h.state_mut().run("ui.backstage", serde_json::json!({"page": "account"})).is_err()); + assert!(h.state_mut().run("ui.backstage", serde_json::json!({"page": "feedback"})).is_err()); h.state_mut().run("ui.backstage", serde_json::json!({"page": "close"})).unwrap(); assert!(h.state().backstage.is_none()); // The back arrow returns to the sheet too. @@ -159,6 +159,20 @@ fn translated_options_headings_show_no_context_prefix() { } } +#[test] +fn file_account_shows_the_version_and_opens_about() { + let mut h = harness(Services::default()); + choose(&mut h, "Account"); + assert_eq!(page(&h), Some("account")); + assert!(h.state().dialog.is_none()); + // The About dialog also shows the version; it must not be open yet. + h.get_by_label(env!("CARGO_PKG_VERSION")); + h.get_by_label(env!("CARGO_PKG_LICENSE")); + click(&mut h, "About GridCraft"); + assert_eq!(h.state().dialog.as_ref().map(|d| d.name()), Some("about")); + assert_eq!(page(&h), Some("account"), "About opens over the File view, like Protect Workbook on Info"); +} + #[test] fn export_as_pdf_is_delivered_as_a_download() { let files = Arc::new(Mutex::new(Vec::<(String, Vec)>::new())); diff --git a/docs/control-protocol.md b/docs/control-protocol.md index e82ae7299..57ff495e9 100644 --- a/docs/control-protocol.md +++ b/docs/control-protocol.md @@ -36,7 +36,7 @@ connection; nothing sent after it runs. Blank lines are skipped. | `ui.edit.begin` / `ui.edit.text` / `ui.edit.commit` / `ui.edit.cancel` | `{text?}` / `{move?: down\|right\|up\|left\|none}` | Drive the cell editor | | `ui.ribbon` | `{tab}` | Switch ribbon tab | | `ui.dialog` / `ui.dialog.set` / `ui.dialog.confirm` / `ui.dialog.cancel` | `{name}` / `{field, value}` | Open, fill in and confirm dialogs | -| `ui.backstage` (via `engine.execute`) | `{page?: home\|new\|open\|info\|saveAs\|print\|export\|options\|close}` | Open the File tab's Backstage view on a page, or close it (`ui.inspect` reports the open page as `backstage`) | +| `ui.backstage` (via `engine.execute`) | `{page?: home\|new\|open\|info\|saveAs\|print\|export\|options\|account\|close}` | Open the File tab's Backstage view on a page, or close it (`ui.inspect` reports the open page as `backstage`) | | `ui.screenshot` | `{path?}` | PNG of the window (base64 when no path). Animations are settled first: the picture shows the final state | | `ui.set` | `{dark?, formulaBar?, ribbonCollapsed?, animations?}` | UI preferences (`formulaBar` runs `view.formulaBar`) | | `ui.resize` / `ui.focus` | `{width, height}` | Window control |