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
@@ -37,7 +37,7 @@ pub struct Docs {
|
||||
pub review: Entity<Review>,
|
||||
/// What the right column shows.
|
||||
pub side: Side,
|
||||
pub offer: Option<crate::menu::Offer>,
|
||||
pub offer: Option<ely_office::menu::Offer<ely_lok::menu::Offered>>,
|
||||
pub merge: Entity<crate::merge::Merge>,
|
||||
pub navigator: Entity<crate::navigate::Navigator>,
|
||||
/// The left column shows the navigator.
|
||||
|
||||
+38
-75
@@ -2,26 +2,19 @@
|
||||
|
||||
use ely_gpui_component::{
|
||||
menus::{ContextMenu, Menu, MenuItem},
|
||||
primitives::{IconName, Severity},
|
||||
primitives::Severity,
|
||||
};
|
||||
use ely_lok::{
|
||||
LokView,
|
||||
menu::{Offered, Target},
|
||||
};
|
||||
use gpui::{App, Context, Entity, Pixels, Point, Window};
|
||||
use ely_office::menu::{self, Entry, Offer};
|
||||
use gpui::{App, Context, Entity, Window};
|
||||
|
||||
use crate::docs::Docs;
|
||||
use crate::refs::{InsertCaption, InsertComment, InsertLink};
|
||||
use ely_office::prelude::*;
|
||||
|
||||
/// A menu the kernel offered, asked for at a point.
|
||||
pub struct Offer {
|
||||
tab: u64,
|
||||
pub(crate) offered: Offered,
|
||||
/// Bumps for each menu, so each opens once.
|
||||
request: (u64, Point<Pixels>),
|
||||
}
|
||||
|
||||
/// One thing the menu does.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Pick {
|
||||
@@ -53,14 +46,8 @@ pub enum Pick {
|
||||
Caption,
|
||||
}
|
||||
|
||||
/// A row, a submenu or a line.
|
||||
enum Entry {
|
||||
Row(Pick),
|
||||
Sub(&'static str, &'static [Pick]),
|
||||
Line,
|
||||
}
|
||||
|
||||
const TABLE: &[Entry] = &[
|
||||
const TABLE: &[Entry<Pick>] = &[
|
||||
Entry::Line,
|
||||
Entry::Sub(
|
||||
"doc.menu.insert",
|
||||
&[Pick::ColumnLeft, Pick::ColumnRight, Pick::RowAbove, Pick::RowBelow],
|
||||
@@ -78,27 +65,27 @@ const TABLE: &[Entry] = &[
|
||||
Entry::Row(Pick::Comment),
|
||||
];
|
||||
|
||||
const FRAME: &[Entry] = &[
|
||||
const FRAME: &[Entry<Pick>] = &[
|
||||
Entry::Line,
|
||||
Entry::Sub(
|
||||
"doc.menu.wrap",
|
||||
&[Pick::Inline, Pick::Square, Pick::TopBottom, Pick::InFront],
|
||||
),
|
||||
Entry::Sub("doc.menu.front", &[Pick::Front, Pick::Forward]),
|
||||
Entry::Sub("doc.menu.back", &[Pick::Back, Pick::Backward]),
|
||||
Entry::Sub("office.menu.front", &[Pick::Front, Pick::Forward]),
|
||||
Entry::Sub("office.menu.back", &[Pick::Back, Pick::Backward]),
|
||||
Entry::Line,
|
||||
Entry::Row(Pick::Caption),
|
||||
Entry::Row(Pick::Comment),
|
||||
];
|
||||
|
||||
const TEXT: &[Entry] = &[Entry::Row(Pick::Link), Entry::Row(Pick::Comment)];
|
||||
const TEXT: &[Entry<Pick>] = &[Entry::Line, Entry::Row(Pick::Link), Entry::Row(Pick::Comment)];
|
||||
|
||||
impl Pick {
|
||||
/// Its label key and the kernel command that gates it.
|
||||
impl menu::Pick for Pick {
|
||||
fn row(self) -> (&'static str, &'static str) {
|
||||
match self {
|
||||
Pick::Cut => ("doc.menu.cut", ".uno:Cut"),
|
||||
Pick::Copy => ("doc.menu.copy", ".uno:Copy"),
|
||||
Pick::Paste => ("doc.menu.paste", ".uno:Paste"),
|
||||
Pick::Cut => ("office.menu.cut", ".uno:Cut"),
|
||||
Pick::Copy => ("office.menu.copy", ".uno:Copy"),
|
||||
Pick::Paste => ("office.menu.paste", ".uno:Paste"),
|
||||
Pick::Link => ("doc.menu.link", ".uno:InsertHyperlink"),
|
||||
Pick::Comment => ("doc.menu.comment", ".uno:InsertAnnotation"),
|
||||
Pick::ColumnLeft => ("doc.menu.column_left", ".uno:InsertColumnsBefore"),
|
||||
@@ -117,47 +104,41 @@ impl Pick {
|
||||
Pick::Square => ("doc.menu.square", ".uno:WrapOn"),
|
||||
Pick::TopBottom => ("doc.menu.top_bottom", ".uno:WrapOff"),
|
||||
Pick::InFront => ("doc.menu.in_front", ".uno:WrapThrough"),
|
||||
Pick::Front => ("doc.menu.front", ".uno:BringToFront"),
|
||||
Pick::Forward => ("doc.menu.forward", ".uno:ObjectForwardOne"),
|
||||
Pick::Back => ("doc.menu.back", ".uno:SendToBack"),
|
||||
Pick::Backward => ("doc.menu.backward", ".uno:ObjectBackOne"),
|
||||
Pick::Front => ("office.menu.front", ".uno:BringToFront"),
|
||||
Pick::Forward => ("office.menu.forward", ".uno:ObjectForwardOne"),
|
||||
Pick::Back => ("office.menu.back", ".uno:SendToBack"),
|
||||
Pick::Backward => ("office.menu.backward", ".uno:ObjectBackOne"),
|
||||
Pick::Caption => ("doc.menu.caption", ".uno:InsertCaptionDialog"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Pick {
|
||||
/// The kernel offers it and it runs now.
|
||||
pub(crate) fn on(self, offered: &Offered) -> bool {
|
||||
offered.allows(self.row().1)
|
||||
offered.allows(menu::Pick::row(self).1)
|
||||
}
|
||||
}
|
||||
|
||||
impl Docs {
|
||||
/// Turns the kernel's context menu into Ely's.
|
||||
pub(crate) fn offer_menu(&mut self, id: u64, view: &Entity<LokView>, cx: &mut Context<Self>) {
|
||||
let Some((json, at)) = view.update(cx, |view, _| view.take_menu()) else {
|
||||
return;
|
||||
};
|
||||
let offered = match ely_lok::menu::read(&json) {
|
||||
Ok(offered) => offered,
|
||||
Err(error) => {
|
||||
let (offered, at) = match menu::take(view, cx) {
|
||||
None => return,
|
||||
Some(Ok(taken)) => taken,
|
||||
Some(Err(error)) => {
|
||||
let text = Docs::text(cx, "office.error.engine", &[("error", &error.to_string())]);
|
||||
return self.toast(text, Severity::Danger, cx);
|
||||
}
|
||||
};
|
||||
// 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!("doc: a menu with no click and no caret");
|
||||
return;
|
||||
};
|
||||
let number = self.offer.as_ref().map_or(1, |offer| offer.request.0 + 1);
|
||||
if matches!(offered.target, Target::Cell | Target::Object) {
|
||||
let error = format!("a {:?} menu in a text document", offered.target);
|
||||
let text = Docs::text(cx, "office.error.engine", &[("error", &error)]);
|
||||
return self.toast(text, Severity::Danger, cx);
|
||||
}
|
||||
let suggestions = offered.spelling.as_ref().map_or(0, |spelling| spelling.suggestions.len());
|
||||
log::info!("doc: a {:?} menu, {suggestions} suggestions", offered.target);
|
||||
self.offer = Some(Offer {
|
||||
tab: id,
|
||||
offered,
|
||||
request: (number, at),
|
||||
});
|
||||
log::info!("doc: {suggestions} suggestions");
|
||||
self.offer = Some(Offer::new(self.offer.as_ref(), id, offered, at));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -167,8 +148,7 @@ impl Docs {
|
||||
.offer
|
||||
.as_ref()
|
||||
.filter(|offer| Some(offer.tab) == self.core.active)?;
|
||||
let menu = menu_of(&cx.entity(), &offer.offered, cx);
|
||||
Some(ContextMenu::new("doc-menu", menu).manual(Some(offer.request)))
|
||||
Some(offer.host("doc-menu", menu_of(&cx.entity(), &offer.what, cx)))
|
||||
}
|
||||
|
||||
/// Runs one menu row.
|
||||
@@ -219,7 +199,7 @@ impl Docs {
|
||||
pub(crate) fn menu_of(this: &Entity<Docs>, offered: &Offered, cx: &App) -> Menu {
|
||||
let item = |pick: Pick| {
|
||||
let this = this.clone();
|
||||
MenuItem::new(Docs::text(cx, pick.row().0, &[]))
|
||||
MenuItem::new(Docs::text(cx, menu::Pick::row(pick).0, &[]))
|
||||
.disabled(!pick.on(offered))
|
||||
.on_click(move |window, cx| this.update(cx, |docs, cx| docs.choose(pick, window, cx)))
|
||||
};
|
||||
@@ -244,31 +224,14 @@ pub(crate) fn menu_of(this: &Entity<Docs>, offered: &Offered, cx: &App) -> Menu
|
||||
);
|
||||
menu = menu.item(ignore).separator();
|
||||
}
|
||||
let clipboard = [
|
||||
(Pick::Cut, IconName::Scissors),
|
||||
(Pick::Copy, IconName::Copy),
|
||||
(Pick::Paste, IconName::Clipboard),
|
||||
];
|
||||
menu = menu
|
||||
.buttons(clipboard.map(|(pick, icon)| item(pick).icon(icon)))
|
||||
.separator();
|
||||
let entries = match offered.target {
|
||||
let menu = menu::clipboard(menu, [Pick::Cut, Pick::Copy, Pick::Paste], item);
|
||||
let rows = match offered.target {
|
||||
Target::Table => TABLE,
|
||||
Target::Frame => FRAME,
|
||||
Target::Text => TEXT,
|
||||
Target::Cell | Target::Object => unreachable!("offer_menu refuses them"),
|
||||
};
|
||||
for entry in entries {
|
||||
menu = 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 on = picks.iter().any(|pick| pick.on(offered));
|
||||
menu.item(MenuItem::submenu(Docs::text(cx, key, &[]), sub).disabled(!on))
|
||||
}
|
||||
Entry::Line => menu.separator(),
|
||||
};
|
||||
}
|
||||
menu
|
||||
menu::entries(menu, rows, cx, |pick| pick.on(offered), item)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -41,7 +41,7 @@ fn right_click(cx: &mut VisualTestContext, docs: &Entity<Docs>, at: Point<Pixels
|
||||
docs.read(app)
|
||||
.offer
|
||||
.as_ref()
|
||||
.is_some_and(|offer| offer.offered.target == target)
|
||||
.is_some_and(|offer| offer.what.target == target)
|
||||
};
|
||||
for _ in 0..500 {
|
||||
cx.run_until_parked();
|
||||
@@ -79,7 +79,7 @@ fn a_right_click_opens_word_rows_where_it_lands() {
|
||||
"the menu opens at the click, not the caret: {off:?}"
|
||||
);
|
||||
let on = |shown: &mut VisualTestContext, pick: Pick| {
|
||||
shown.update(|_, app| pick.on(&docs.read(app).offer.as_ref().expect("an offer").offered))
|
||||
shown.update(|_, app| pick.on(&docs.read(app).offer.as_ref().expect("an offer").what))
|
||||
};
|
||||
assert!(on(&mut shown, Pick::Comment));
|
||||
assert!(!on(&mut shown, Pick::Link), "the kernel links a selection");
|
||||
|
||||
@@ -126,7 +126,7 @@ fn a_misspelled_word_offers_suggestions() {
|
||||
settle(&cx, |app| docs.read(app).offer.is_some());
|
||||
let rule = cx.update(|app| {
|
||||
let offer = docs.read(app).offer.as_ref().expect("an offer");
|
||||
let spelling = offer.offered.spelling.as_ref().expect("a spelling menu");
|
||||
let spelling = offer.what.spelling.as_ref().expect("a spelling menu");
|
||||
let (_, rule) = spelling
|
||||
.suggestions
|
||||
.iter()
|
||||
|
||||
Reference in new issue
Block a user