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");
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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(),
|
||||
})
|
||||
}
|
||||
@@ -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", "厘米"),
|
||||
|
||||
@@ -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} 比较结果"),
|
||||
|
||||
Reference in new issue
Block a user