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:
1 parent
07c0b43542
commit
87886c8a8d
30 files changed
+1010
-106
No files matched your search
@@ -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"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
Reference in new issue
Block a user