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");
}
+1
View File
@@ -9,6 +9,7 @@ pub mod engine;
pub mod files;
mod find;
pub mod lok;
pub mod menu;
mod open;
mod recover;
pub mod panel;
+86
View File
@@ -0,0 +1,86 @@
//! Ely's right-click menus, gated by the kernel's (DOC-02).
use std::io;
use ely_gpui_component::{
menus::{ContextMenu, Menu, MenuItem},
primitives::IconName,
};
use ely_lok::{LokView, menu::Offered};
use gpui::{App, Entity, Pixels, Point};
/// One menu, asked for at a point in one tab.
pub struct Offer<W> {
pub tab: u64,
pub what: W,
/// Bumps for each menu, so each opens once.
request: (u64, Point<Pixels>),
}
impl<W> Offer<W> {
/// The next menu after `last`.
pub fn new(last: Option<&Offer<W>>, tab: u64, what: W, at: Point<Pixels>) -> Self {
let number = last.map_or(1, |last| last.request.0 + 1);
Offer { tab, what, request: (number, at) }
}
/// Its host, opened at the point.
pub fn host(&self, id: &'static str, menu: Menu) -> ContextMenu {
ContextMenu::new(id, menu).manual(Some(self.request))
}
}
/// The view's last kernel menu and where it opens.
pub fn take(view: &Entity<LokView>, cx: &mut App) -> Option<io::Result<(Offered, Point<Pixels>)>> {
let (json, at) = view.update(cx, |view, _| view.take_menu())?;
let offered = match ely_lok::menu::read(&json) {
Ok(offered) => offered,
Err(error) => return Some(Err(error)),
};
// Without a click it opens at the caret.
let caret = || view.read(cx).cursor_bounds().map(|caret| caret.bottom_left());
let Some(at) = at.or_else(caret) else {
log::warn!("office: a menu with no click and no caret");
return None;
};
log::info!("office: a {:?} menu", offered.target);
Some(Ok((offered, at)))
}
/// A menu row: its label key and its gating command.
pub trait Pick: Copy + 'static {
fn row(self) -> (&'static str, &'static str);
}
/// A row, a submenu or a line.
pub enum Entry<P: 'static> {
Row(P),
Sub(&'static str, &'static [P]),
Line,
}
/// Cut, copy and paste as one row of icons.
pub fn clipboard<P: Pick>(menu: Menu, picks: [P; 3], item: impl Fn(P) -> MenuItem) -> Menu {
let icons = [IconName::Scissors, IconName::Copy, IconName::Clipboard];
let row = picks.into_iter().zip(icons).map(|(pick, icon)| item(pick).icon(icon));
menu.buttons(row)
}
/// Adds entries; a submenu is on when any row is.
pub fn entries<P: Pick>(
menu: Menu,
entries: &[Entry<P>],
cx: &App,
on: impl Fn(P) -> bool,
item: impl Fn(P) -> MenuItem,
) -> Menu {
entries.iter().fold(menu, |menu, entry| match entry {
Entry::Row(pick) => menu.item(item(*pick)),
Entry::Sub(key, picks) => {
let sub = picks.iter().fold(Menu::new(), |sub, pick| sub.item(item(*pick)));
let any = picks.iter().any(|pick| on(*pick));
menu.item(MenuItem::submenu(crate::text(cx, key, &[]), sub).disabled(!any))
}
Entry::Line => menu.separator(),
})
}
+8
View File
@@ -66,6 +66,10 @@ pub(super) const EN: &[(&str, &str)] = &[
("table.cells.hide_columns", "Hide Columns"),
("table.cells.show_columns", "Show Columns"),
("table.cells.freeze", "Freeze or Unfreeze Panes"),
("table.menu.insert", "Insert"),
("table.menu.delete", "Delete"),
("table.menu.clear", "Clear Contents"),
("table.menu.format", "Format Cells"),
("table.size.row", "Row Height"),
("table.size.column", "Column Width"),
("table.size.label", "Centimeters"),
@@ -157,6 +161,10 @@ pub(super) const ZH_CN: &[(&str, &str)] = &[
("table.cells.hide_columns", "隐藏列"),
("table.cells.show_columns", "显示列"),
("table.cells.freeze", "冻结或取消冻结窗格"),
("table.menu.insert", "插入"),
("table.menu.delete", "删除"),
("table.menu.clear", "清除内容"),
("table.menu.format", "设置单元格格式"),
("table.size.row", "行高"),
("table.size.column", "列宽"),
("table.size.label", "厘米"),
+14 -14
View File
@@ -13,9 +13,9 @@ pub(super) const EN: &[(&str, &str)] = &[
("office.find.stale", "Edited. Enter counts again."),
("doc.spell.ignore_all", "Ignore All"),
("doc.spell.none", "No suggestions"),
("doc.menu.cut", "Cut"),
("doc.menu.copy", "Copy"),
("doc.menu.paste", "Paste"),
("office.menu.cut", "Cut"),
("office.menu.copy", "Copy"),
("office.menu.paste", "Paste"),
("doc.menu.link", "Link…"),
("doc.menu.comment", "New Comment"),
("doc.menu.insert", "Insert"),
@@ -38,10 +38,10 @@ pub(super) const EN: &[(&str, &str)] = &[
("doc.menu.square", "Square"),
("doc.menu.top_bottom", "Top and Bottom"),
("doc.menu.in_front", "In Front of Text"),
("doc.menu.front", "Bring to Front"),
("doc.menu.forward", "Bring Forward"),
("doc.menu.back", "Send to Back"),
("doc.menu.backward", "Send Backward"),
("office.menu.front", "Bring to Front"),
("office.menu.forward", "Bring Forward"),
("office.menu.back", "Send to Back"),
("office.menu.backward", "Send Backward"),
("doc.menu.caption", "Insert Caption…"),
("doc.compare.open", "Compare With…"),
("doc.compare.name", "{name} compared"),
@@ -108,9 +108,9 @@ pub(super) const ZH_CN: &[(&str, &str)] = &[
("office.find.stale", "已改动,回车重新计数"),
("doc.spell.ignore_all", "全部忽略"),
("doc.spell.none", "无建议"),
("doc.menu.cut", "剪切"),
("doc.menu.copy", "复制"),
("doc.menu.paste", "粘贴"),
("office.menu.cut", "剪切"),
("office.menu.copy", "复制"),
("office.menu.paste", "粘贴"),
("doc.menu.link", "链接…"),
("doc.menu.comment", "新建批注"),
("doc.menu.insert", "插入"),
@@ -133,10 +133,10 @@ pub(super) const ZH_CN: &[(&str, &str)] = &[
("doc.menu.square", "四周型"),
("doc.menu.top_bottom", "上下型"),
("doc.menu.in_front", "浮于文字上方"),
("doc.menu.front", "置于顶层"),
("doc.menu.forward", "上移一层"),
("doc.menu.back", "置于底层"),
("doc.menu.backward", "下移一层"),
("office.menu.front", "置于顶层"),
("office.menu.forward", "上移一层"),
("office.menu.back", "置于底层"),
("office.menu.backward", "下移一层"),
("doc.menu.caption", "插入题注…"),
("doc.compare.open", "与其他版本比较…"),
("doc.compare.name", "{name} 比较结果"),