Keytips: Alt, F opens the File view, and its F letters still run (part of #893)

This commit is contained in:
Marco Arnulfo
2026-10-13 07:31:28 +02:00
parent 6405b00cf7
commit 06c8ff640d
7 changed files with 55 additions and 3 deletions
@@ -6,6 +6,7 @@ use super::{Action, Entry};
/// whenever a pressed letter starts a deeper sequence, so only terminal entries are
/// listed here. See keytip_table.rs for the helpers that read this table.
pub const SEQUENCES: &[(&str, Entry)] = &[
("F", Entry::Tab("File")), // Excel: File tab (Backstage view); Opens the File view on Home; its letters below still run their commands.
("H", Entry::Tab("Home")),
("N", Entry::Tab("Insert")),
("JI", Entry::Tab("Draw")), // Two-letter unit. Deeper Draw letters differ between Excel and GridCraft; left blank for future contributors.
+7
View File
@@ -11,6 +11,13 @@
" - Keep entries grouped by tab and sorted by sequence within a group; the FIRST entry for a tab is its canonical badge (legacy aliases come after it)."
],
"sequences": [
{
"seq": "F",
"kind": "tab",
"target": "File",
"excel": "File tab (Backstage view)",
"note": "Opens the File view on Home; its letters below still run their commands."
},
{
"seq": "H",
"kind": "tab",
+8 -1
View File
@@ -1095,7 +1095,8 @@ impl SheetApp {
// The language the widgets translate with this frame (see `i18n::current`).
i18n::set_current(&ctx, self.ui.language);
let t = theme::Tokens::get(&ctx);
if self.backstage.is_none() {
// Keytips opened the File view (Alt, F): its letters keep working there.
if self.backstage.is_none() || self.keytips.prefix().is_some() {
self.ribbon_keys(&ctx);
}
ribbon::title_bar(self, ui);
@@ -1172,12 +1173,18 @@ impl SheetApp {
&& self.grid.renaming_tab.is_none()
&& (before.is_some() || !egui::Popup::is_any_open(ctx));
let actions = self.keytips.process(ctx, enabled);
// Backing out of F to the tab letters, or finishing an F sequence, closes the File view
// that Alt, F opened: the command runs on the sheet, as it did without the view.
if self.backstage.is_some() && before.is_some() && (self.keytips.prefix() == Some("") || !actions.is_empty()) {
self.backstage = None;
}
if before.is_some() && before.as_deref() != self.keytips.prefix() && !ctx.input(|i| i.pointer.any_pressed()) {
// Leave pointer cancellation to the popup so clicking a menu item still works.
egui::Popup::close_all(ctx);
}
for action in actions {
match action {
keytips::Action::Tab("File") => self.open_backstage(backstage::Page::Home),
keytips::Action::Tab(tab) => {
self.run_or_alert("ui.ribbonTab", json!({"tab": tab}));
self.ui.ribbon_collapsed = false;
+5
View File
@@ -263,6 +263,11 @@ fn file_menu(app: &mut SheetApp, ui: &mut Ui) {
if file.clicked() {
app.open_backstage(crate::backstage::Page::Home);
}
if app.keytips.prefix() == Some("")
&& let Some(sequence) = keytip_table::tab_sequence("File")
{
keytip(app, ui, sequence, pos2(file.rect.center().x, file.rect.bottom() - 1.0));
}
}
/// System / Light / Dark, as in the View tab's Display theme box (`view.theme`).
+19
View File
@@ -316,6 +316,25 @@ fn tab_letters_switch_to_their_ribbon_tabs() {
assert_eq!(h.state().keytips.prefix(), None, "terminal tab units finish cleanly");
}
#[test]
fn f_opens_the_file_view_and_its_letters_still_run() {
use gridcraft_ui_egui::backstage::Page;
let mut h = harness(blank());
sequence(&mut h, &[Key::F], false);
assert_eq!(h.state().backstage.as_ref().map(|b| b.page), Some(Page::Home), "F opens the File view on Home");
assert_eq!(h.state().keytips.prefix(), Some("F"));
// Esc backs out one level: the File view closes and the tab letters show again.
key(&mut h, Key::Escape);
assert!(h.state().backstage.is_none());
assert_eq!(h.state().keytips.prefix(), Some(""));
letter(&mut h, Key::F, false);
assert!(h.state().backstage.is_some());
letter(&mut h, Key::N, false);
assert!(h.state().session.journal.iter().any(|(id, _)| id == "file.new"), "F, N still runs New");
assert!(h.state().backstage.is_none(), "the command runs on the sheet, as before the view opened");
assert_eq!(h.state().keytips.prefix(), None);
}
#[test]
fn view_gridlines_sequence_runs_end_to_end() {
let mut h = harness(blank());
+9 -1
View File
@@ -59,6 +59,14 @@ fn sequence(h: &mut Harness, seq: &str, text_first: bool) {
}
}
#[test]
fn keytip_f_() {
let mut h = harness(blank());
sequence(&mut h, "F", false);
assert!(h.state().backstage.is_some(), "F must open the File view");
assert_eq!(h.state().keytips.prefix(), Some("F"));
}
#[test]
fn keytip_h_() {
let mut h = harness(blank());
@@ -2741,7 +2749,7 @@ fn keytip_q_() {
#[test]
fn generated_test_count_matches_the_table() {
const EXPECTED: usize = 364;
const EXPECTED: usize = 365;
assert_eq!(gridcraft_ui_egui::keytips::keytip_table::SEQUENCES.len(), EXPECTED);
}
+6 -1
View File
@@ -10,7 +10,8 @@
use std::fs;
use std::path::Path;
const TABS: &[&str] = &["Home", "Insert", "Draw", "Page Layout", "Formulas", "Data", "Review", "View", "Automate"];
/// "File" is not a ribbon tab: its sequence opens the Backstage view.
const TABS: &[&str] = &["File", "Home", "Insert", "Draw", "Page Layout", "Formulas", "Data", "Review", "View", "Automate"];
/// Commands whose empty-params behaviour depends on the host machine (e.g. the
/// system dictionary). These are never probed; their tests assert "invoked
@@ -260,6 +261,10 @@ fn render_tests(entries: &[Entry], probe: &Probe) -> String {
let mut out = String::from(HEADER);
for e in entries {
let body = match e.kind.as_str() {
"tab" if e.target == "File" => format!(
"sequence(&mut h, {seq:?}, false);\n assert!(h.state().backstage.is_some(), \"{seq} must open the File view\");\n assert_eq!(h.state().keytips.prefix(), Some({seq:?}));",
seq = e.seq,
),
"tab" => format!(
"sequence(&mut h, {seq:?}, false);\n assert_eq!(h.state().ui.ribbon_tab, {tab:?}, \"{seq} must switch to the {tab} tab\");",
seq = e.seq,