R5.5: ElyTable right-click menus, Excel-style; three rounds used, last fixes tested

Grid and header menus share ely_office::menu with ElyDoc. Each item's
availability is the kernel's state for the exact command it runs
(patch 0045). Patch 0046 lets a header select its row or column while
a shape is selected or its text is edited: the view shell ends the
drawing edit, keeps the typed text, leaves drawing mode, then marks.
Ely no longer sends Esc first.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-14 13:02:10 -04:00
1 parent 07c0b43542
commit 87886c8a8d
30 files changed
+1010 -106

No files matched your search

+1 -1
View File
@@ -143,7 +143,7 @@ fn every_mapped_command_reaches_the_kernel_and_reports_state() {
assert!(text.contains("fo:font-weight=\"bold\""), "bold landed");
assert_eq!(
commands::all().count(),
247,
248,
"the rows of text, layout, refs, tools, sheet, calc, data, analysis, page, show and draw"
);
}
+1
View File
@@ -153,6 +153,7 @@ pub const QUERIES: &[&str] = &[
crate::sheet::NUMBER,
crate::sheet::FILTER,
crate::sheet::LIST,
crate::menu::STATES,
];
/// The kernel's font families, from `.uno:CharFontName`.
+1
View File
@@ -100,6 +100,7 @@ pub(super) const ROWS: &[Command] = &[
Check::Undoable,
),
command("cells.split", ".uno:SplitCell", &[], Check::Undoable),
command("cells.clear", ".uno:ClearContents", &[], Check::Undoable),
command("view.freeze", ".uno:FreezePanes", &[], Check::Finished),
command("rows.hide", ".uno:HideRow", &[], Check::Undoable),
command("rows.show", ".uno:ShowRow", &[], Check::Undoable),
+46 -2
View File
@@ -2,15 +2,22 @@
use std::{collections::HashMap, io};
use crate::review::{entries, invalid, parse};
use crate::{
Doc, Worker,
review::{entries, invalid, parse},
};
/// Where a right click landed, per the kernel's menu.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Target {
Text,
Table,
/// A graphic, an object or a frame.
/// A Writer graphic, object or frame.
Frame,
/// A spreadsheet cell.
Cell,
/// A drawing object outside Writer.
Object,
}
/// A misspelled word's menu: suggestions as the kernel's rules.
@@ -36,6 +43,39 @@ impl Offered {
}
}
/// Which asked commands run now, per the kernel's dispatcher.
#[derive(Clone, Debug, PartialEq)]
pub struct Runs(HashMap<String, bool>);
impl Runs {
/// The kernel lets `uno` run now.
pub fn allows(&self, uno: &str) -> bool {
self.0.get(uno) == Some(&true)
}
}
/// The read of which commands run now (patch 0045).
pub const STATES: &str = ".uno:CommandStates";
/// Asks the kernel which of `commands` run now.
pub fn runs(worker: &mut Worker, doc: &Doc, commands: &[&str]) -> io::Result<Runs> {
let json = worker.values(doc, &format!("{STATES}?commands={}", commands.join(",")))?;
read_runs(&json, commands)
}
/// Reads the answer; every asked command must be in it.
fn read_runs(json: &str, commands: &[&str]) -> io::Result<Runs> {
let value = parse(json)?;
let states = value["commandStates"]
.as_object()
.ok_or_else(|| invalid(format!("no command states: {json}")))?;
let read = |command: &&str| match states.get(*command).and_then(serde_json::Value::as_bool) {
Some(on) => Ok((command.to_string(), on)),
None => Err(invalid(format!("no state for {command}: {json}"))),
};
Ok(Runs(commands.iter().map(read).collect::<io::Result<_>>()?))
}
/// Reads a `LOK_CALLBACK_CONTEXT_MENU` payload.
pub fn read(json: &str) -> io::Result<Offered> {
let list = parse(json)?;
@@ -50,6 +90,10 @@ pub fn read(json: &str) -> io::Result<Offered> {
Target::Table
} else if enabled.contains_key(".uno:WrapMenu") {
Target::Frame
} else if enabled.contains_key(".uno:InsertCell") {
Target::Cell
} else if enabled.contains_key(".uno:ArrangeMenu") {
Target::Object
} else {
Target::Text
};
+15
View File
@@ -32,6 +32,10 @@ fn the_menu_names_its_target_and_flags() {
item(".uno:WrapMenu", true)
);
assert_eq!(read(&frame).expect("a commandless submenu").target, Target::Frame);
let cell = format!(r#"{{"menu":[{}]}}"#, item(".uno:InsertCell", false));
assert_eq!(read(&cell).expect("parses").target, Target::Cell);
let object = format!(r#"{{"menu":[{}]}}"#, item(".uno:ArrangeMenu", true));
assert_eq!(read(&object).expect("parses").target, Target::Object);
let text = format!(r#"{{"menu":[{}]}}"#, item(".uno:Paste", true));
assert_eq!(read(&text).expect("parses").target, Target::Text);
}
@@ -42,3 +46,14 @@ fn a_malformed_menu_fails() {
assert!(read(r#"{"menu":[{"type":"command","enabled":"true"}]}"#).is_err());
assert!(read(r#"{"items":[]}"#).is_err());
}
#[test]
fn command_states_answer_each_asked_command() {
let json = r#"{"commandStates":{".uno:DeleteRows":false,".uno:InsertRowsBefore":true}}"#;
let runs = read_runs(json, &[".uno:InsertRowsBefore", ".uno:DeleteRows"]).expect("parses");
assert!(runs.allows(".uno:InsertRowsBefore"));
assert!(!runs.allows(".uno:DeleteRows"));
assert!(read_runs(json, &[".uno:HideRow"]).is_err(), "an unanswered command");
let text = r#"{"commandStates":{".uno:Cut":"true"}}"#;
assert!(read_runs(text, &[".uno:Cut"]).is_err(), "a state is a boolean");
}