R5.2: ElyDoc's right-click menu follows Word by target, items enabled as the kernel's menu allows; two rounds, R2 PASS
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
e3c515b2f4
commit
9d80ba6f39
22 files changed
+764
-146
No files matched your search
@@ -15,6 +15,7 @@ pub(crate) mod input;
|
||||
mod latency;
|
||||
mod layout;
|
||||
mod media;
|
||||
mod menus;
|
||||
mod motion;
|
||||
mod output;
|
||||
mod paint;
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
use super::input::open;
|
||||
use super::layout::png;
|
||||
use super::*;
|
||||
use crate::menu::{self, Target};
|
||||
|
||||
pub(super) const DOC: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
|
||||
<office:document xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0" office:version="1.3" office:mimetype="application/vnd.oasis.opendocument.text">
|
||||
<office:body><office:text><text:p>The cat ran.</text:p>
|
||||
<table:table table:name="T"><table:table-column table:number-columns-repeated="2"/>
|
||||
<table:table-row><table:table-cell><text:p>left</text:p></table:table-cell><table:table-cell><text:p>right</text:p></table:table-cell></table:table-row>
|
||||
<table:table-row><table:table-cell><text:p>down</text:p></table:table-cell><table:table-cell><text:p>more</text:p></table:table-cell></table:table-row>
|
||||
</table:table><text:p>after</text:p></office:text></office:body>
|
||||
</office:document>"#;
|
||||
|
||||
const WAIT: std::time::Duration = std::time::Duration::from_secs(5);
|
||||
|
||||
/// Right-clicks at a point and reads the kernel's menu.
|
||||
fn right(worker: &mut Worker, doc: &Doc, seen: &mut Events, (x, y): (i32, i32)) -> menu::Offered {
|
||||
click(worker, doc, x, y);
|
||||
press(worker, doc, seen, (x, y))
|
||||
}
|
||||
|
||||
/// Right-clicks without moving the selection first.
|
||||
fn press(worker: &mut Worker, doc: &Doc, seen: &mut Events, (x, y): (i32, i32)) -> menu::Offered {
|
||||
for kind in [0, 1] {
|
||||
let event = Mouse { kind, x, y, count: 1, buttons: 4, modifiers: 0 };
|
||||
worker.post_mouse(doc, event).expect("the click posts");
|
||||
}
|
||||
*seen = poll_until(worker, doc, seen, WAIT, |seen| seen.menu.is_some());
|
||||
menu::read(&seen.menu.take().expect("a menu")).expect("the menu parses")
|
||||
}
|
||||
|
||||
/// The middle of the first match of `word`.
|
||||
pub(super) fn find(worker: &mut Worker, doc: &Doc, seen: &mut Events, word: &str) -> (i32, i32) {
|
||||
let before = seen.searched;
|
||||
worker
|
||||
.run(doc, "find.run", &[word, "false", "0", "0", "0", "1"])
|
||||
.expect("the search runs");
|
||||
*seen = poll_until(worker, doc, seen, WAIT, |seen| seen.searched > before);
|
||||
let (x, y, _, h) = seen.found[0];
|
||||
(x + 60, y + h / 2)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_right_click_names_its_target() {
|
||||
if install().is_none() {
|
||||
eprintln!("SKIP: no pinned LOK build");
|
||||
return;
|
||||
}
|
||||
let (dir, mut worker, doc) = open(DOC);
|
||||
click(&mut worker, &doc, 150, 150);
|
||||
let mut seen = Events::default();
|
||||
let at = find(&mut worker, &doc, &mut seen, "cat");
|
||||
let text = right(&mut worker, &doc, &mut seen, at);
|
||||
assert_eq!(text.target, Target::Text);
|
||||
assert!(text.allows(".uno:InsertAnnotation") && text.allows(".uno:Paste"));
|
||||
assert!(!text.allows(".uno:Cut"), "nothing is selected");
|
||||
let at = find(&mut worker, &doc, &mut seen, "left");
|
||||
let table = right(&mut worker, &doc, &mut seen, at);
|
||||
assert_eq!(table.target, Target::Table);
|
||||
for uno in [
|
||||
".uno:InsertRowsBefore",
|
||||
".uno:InsertColumnsAfter",
|
||||
".uno:DeleteTable",
|
||||
".uno:SplitCell",
|
||||
] {
|
||||
assert!(table.allows(uno), "{uno} runs in a cell");
|
||||
}
|
||||
assert!(!table.allows(".uno:MergeCells"), "one cell does not merge");
|
||||
let at = find(&mut worker, &doc, &mut seen, "after");
|
||||
click(&mut worker, &doc, at.0, at.1);
|
||||
let blue = png(dir.path(), "blue.png", [0, 0, 255, 255]);
|
||||
worker.run(&doc, "image.insert", &[&blue]).expect("the image lands");
|
||||
seen = poll_until(&mut worker, &doc, &seen, WAIT, |seen| seen.graphic.is_some());
|
||||
let (x, y, w, h) = seen.graphic.expect("the image is selected");
|
||||
let frame = right(&mut worker, &doc, &mut seen, (x + w / 2, y + h / 2));
|
||||
assert_eq!(frame.target, Target::Frame);
|
||||
for uno in [".uno:Copy", ".uno:WrapOn", ".uno:BringToFront", ".uno:InsertCaptionDialog"] {
|
||||
assert!(frame.allows(uno), "{uno} runs on an image");
|
||||
}
|
||||
assert!(!frame.allows(".uno:InsertAnnotation"), "no comment on an image");
|
||||
}
|
||||
|
||||
const LOCKED: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
|
||||
<office:document xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0" office:version="1.3" office:mimetype="application/vnd.oasis.opendocument.text">
|
||||
<office:body><office:text><text:p>open words</text:p>
|
||||
<text:section text:name="S" text:protected="true"><text:p>sealed words</text:p></text:section>
|
||||
<table:table table:name="T"><table:table-column table:number-columns-repeated="2"/>
|
||||
<table:table-row><table:table-cell table:protected="true"><text:p>fixed</text:p></table:table-cell><table:table-cell><text:p>free</text:p></table:table-cell></table:table-row>
|
||||
</table:table><text:p>after</text:p></office:text></office:body>
|
||||
</office:document>"#;
|
||||
|
||||
#[test]
|
||||
fn protected_text_offers_no_link() {
|
||||
if install().is_none() {
|
||||
eprintln!("SKIP: no pinned LOK build");
|
||||
return;
|
||||
}
|
||||
let (_dir, mut worker, doc) = open(LOCKED);
|
||||
click(&mut worker, &doc, 150, 150);
|
||||
let mut seen = Events::default();
|
||||
for (word, target, link) in [
|
||||
("open", Target::Text, true),
|
||||
("sealed", Target::Text, false),
|
||||
("free", Target::Table, true),
|
||||
("fixed", Target::Table, false),
|
||||
] {
|
||||
let at = find(&mut worker, &doc, &mut seen, word);
|
||||
let offered = press(&mut worker, &doc, &mut seen, at);
|
||||
assert_eq!(offered.target, target, "{word}");
|
||||
assert_eq!(offered.allows(".uno:InsertHyperlink"), link, "{word} selected");
|
||||
}
|
||||
let at = find(&mut worker, &doc, &mut seen, "open");
|
||||
let caret = right(&mut worker, &doc, &mut seen, at);
|
||||
assert!(!caret.allows(".uno:InsertHyperlink"), "the kernel links a selection");
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::input::{open, saved};
|
||||
use super::*;
|
||||
use crate::review;
|
||||
use crate::{menu, review};
|
||||
|
||||
const TEXT: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
|
||||
<office:document xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" office:version="1.3" office:mimetype="application/vnd.oasis.opendocument.text">
|
||||
@@ -125,8 +125,9 @@ fn spelling_suggests_and_ignores() {
|
||||
);
|
||||
let (x, y, _, h) = seen.found[0];
|
||||
let json = menu(&mut worker, &doc, &mut seen, (x + 60, y + h / 2)).expect("a menu");
|
||||
let spelling = review::spelling(&json)
|
||||
let spelling = menu::read(&json)
|
||||
.expect("the menu parses")
|
||||
.spelling
|
||||
.expect("a spelling menu");
|
||||
let (_, rule) = spelling
|
||||
.suggestions
|
||||
@@ -145,13 +146,13 @@ fn spelling_suggests_and_ignores() {
|
||||
);
|
||||
let (x, y, _, h) = seen.found[0];
|
||||
let json = menu(&mut worker, &doc, &mut seen, (x + 60, y + h / 2)).expect("a menu");
|
||||
assert!(review::spelling(&json).expect("parses").is_some());
|
||||
assert!(menu::read(&json).expect("parses").spelling.is_some());
|
||||
worker
|
||||
.run(&doc, "spell.ignore_all", &["Spelling"])
|
||||
.expect("ignore all");
|
||||
let json = menu(&mut worker, &doc, &mut seen, (x + 60, y + h / 2)).expect("a menu");
|
||||
assert_eq!(
|
||||
review::spelling(&json).expect("parses"),
|
||||
menu::read(&json).expect("parses").spelling,
|
||||
None,
|
||||
"an ignored word is no longer wrong"
|
||||
);
|
||||
|
||||
@@ -7,6 +7,7 @@ mod ffi;
|
||||
mod handshake;
|
||||
pub mod history;
|
||||
mod ipc;
|
||||
pub mod menu;
|
||||
pub mod review;
|
||||
pub mod sheet;
|
||||
pub mod show;
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
//! The kernel's context menu: target and flags (DOC-02).
|
||||
|
||||
use std::{collections::HashMap, io};
|
||||
|
||||
use crate::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.
|
||||
Frame,
|
||||
}
|
||||
|
||||
/// A misspelled word's menu: suggestions as the kernel's rules.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Spelling {
|
||||
/// Label and `ApplyRule` value for each suggestion.
|
||||
pub suggestions: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
/// One menu the kernel offered.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Offered {
|
||||
pub target: Target,
|
||||
pub spelling: Option<Spelling>,
|
||||
/// Each command, at any depth, and whether it runs now.
|
||||
enabled: HashMap<String, bool>,
|
||||
}
|
||||
|
||||
impl Offered {
|
||||
/// The kernel offers `uno` and it runs now.
|
||||
pub fn allows(&self, uno: &str) -> bool {
|
||||
self.enabled.get(uno) == Some(&true)
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads a `LOK_CALLBACK_CONTEXT_MENU` payload.
|
||||
pub fn read(json: &str) -> io::Result<Offered> {
|
||||
let list = parse(json)?;
|
||||
let mut enabled = HashMap::new();
|
||||
let mut suggestions = Vec::new();
|
||||
walk(entries(&list, "menu")?, &mut enabled, &mut suggestions)?;
|
||||
let spelling = enabled
|
||||
.keys()
|
||||
.any(|command| command.starts_with(".uno:SpellCheckIgnoreAll"))
|
||||
.then_some(Spelling { suggestions });
|
||||
let target = if enabled.contains_key(".uno:TableInsertMenu") {
|
||||
Target::Table
|
||||
} else if enabled.contains_key(".uno:WrapMenu") {
|
||||
Target::Frame
|
||||
} else {
|
||||
Target::Text
|
||||
};
|
||||
Ok(Offered {
|
||||
target,
|
||||
spelling,
|
||||
enabled,
|
||||
})
|
||||
}
|
||||
|
||||
fn walk(
|
||||
items: &[serde_json::Value],
|
||||
enabled: &mut HashMap<String, bool>,
|
||||
suggestions: &mut Vec<(String, String)>,
|
||||
) -> io::Result<()> {
|
||||
for item in items {
|
||||
if item["type"] == "separator" {
|
||||
continue;
|
||||
}
|
||||
let on = match item["enabled"].as_str() {
|
||||
Some("true") => true,
|
||||
Some("false") => false,
|
||||
_ => return Err(invalid(format!("a menu item has no enabled flag: {item}"))),
|
||||
};
|
||||
// A submenu may have no command.
|
||||
match (item["command"].as_str(), item["type"].as_str()) {
|
||||
(Some(command), _) => {
|
||||
enabled.insert(command.to_string(), on);
|
||||
if let Some(rule) =
|
||||
command.strip_prefix(".uno:SpellCheckApplySuggestion?ApplyRule:string=")
|
||||
{
|
||||
let label = item["text"]
|
||||
.as_str()
|
||||
.ok_or_else(|| invalid(format!("a suggestion has no text: {item}")))?;
|
||||
suggestions.push((label.to_string(), rule.to_string()));
|
||||
}
|
||||
}
|
||||
(None, Some("menu")) => {}
|
||||
(None, _) => return Err(invalid(format!("a menu item has no command: {item}"))),
|
||||
}
|
||||
if let Some(sub) = item.get("menu") {
|
||||
let sub = sub
|
||||
.as_array()
|
||||
.ok_or_else(|| invalid(format!("a submenu is no list: {item}")))?;
|
||||
walk(sub, enabled, suggestions)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -0,0 +1,44 @@
|
||||
use super::*;
|
||||
|
||||
fn item(command: &str, enabled: bool) -> String {
|
||||
format!(r#"{{"text":"x","type":"command","command":"{command}","enabled":"{enabled}"}}"#)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spelling_rows_come_first_class() {
|
||||
let menu = r#"{"menu":[{"text":"The","type":"command","command":".uno:SpellCheckApplySuggestion?ApplyRule:string=Spelling_The","enabled":"true"},{"type":"separator"},{"text":"Ignore All","type":"command","command":".uno:SpellCheckIgnoreAll?Type:string=Spelling","enabled":"true"}]}"#;
|
||||
let offered = read(menu).expect("the menu parses");
|
||||
let spelling = offered.spelling.expect("a spelling menu");
|
||||
assert_eq!(spelling.suggestions, [("The".into(), "Spelling_The".into())]);
|
||||
assert_eq!(offered.target, Target::Text);
|
||||
let other = format!(r#"{{"menu":[{}]}}"#, item(".uno:Cut", true));
|
||||
assert_eq!(read(&other).expect("the menu parses").spelling, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_menu_names_its_target_and_flags() {
|
||||
let table = format!(
|
||||
r#"{{"menu":[{},{{"text":"Insert","type":"menu","command":".uno:TableInsertMenu","menu":[{}],"enabled":"true"}}]}}"#,
|
||||
item(".uno:MergeCells", false),
|
||||
item(".uno:InsertRowsBefore", true),
|
||||
);
|
||||
let table = read(&table).expect("the table menu parses");
|
||||
assert_eq!(table.target, Target::Table);
|
||||
assert!(table.allows(".uno:InsertRowsBefore"), "submenu rows count");
|
||||
assert!(!table.allows(".uno:MergeCells"), "a disabled row");
|
||||
assert!(!table.allows(".uno:DeleteTable"), "an absent row");
|
||||
let frame = format!(
|
||||
r#"{{"menu":[{{"type":"menu","menu":[{}],"enabled":"true"}}]}}"#,
|
||||
item(".uno:WrapMenu", true)
|
||||
);
|
||||
assert_eq!(read(&frame).expect("a commandless submenu").target, Target::Frame);
|
||||
let text = format!(r#"{{"menu":[{}]}}"#, item(".uno:Paste", true));
|
||||
assert_eq!(read(&text).expect("parses").target, Target::Text);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_malformed_menu_fails() {
|
||||
assert!(read(r#"{"menu":[{"type":"command","command":".uno:Cut"}]}"#).is_err());
|
||||
assert!(read(r#"{"menu":[{"type":"command","enabled":"true"}]}"#).is_err());
|
||||
assert!(read(r#"{"items":[]}"#).is_err());
|
||||
}
|
||||
@@ -38,13 +38,6 @@ pub struct Counts {
|
||||
pub selected_characters: u64,
|
||||
}
|
||||
|
||||
/// A misspelled word's menu: suggestions as the kernel's rules.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Spelling {
|
||||
/// Label and `ApplyRule` value for each suggestion.
|
||||
pub suggestions: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
/// The counts from `.uno:WordCount`.
|
||||
pub fn counts(json: &str) -> io::Result<Counts> {
|
||||
let list = parse(json)?;
|
||||
@@ -57,32 +50,6 @@ pub fn counts(json: &str) -> io::Result<Counts> {
|
||||
})
|
||||
}
|
||||
|
||||
/// A context menu's spelling part, if it has one.
|
||||
pub fn spelling(json: &str) -> io::Result<Option<Spelling>> {
|
||||
let list = parse(json)?;
|
||||
let items = entries(&list, "menu")?;
|
||||
let commands: Vec<&str> = items
|
||||
.iter()
|
||||
.filter_map(|item| item["command"].as_str())
|
||||
.collect();
|
||||
if !commands
|
||||
.iter()
|
||||
.any(|command| command.starts_with(".uno:SpellCheckIgnoreAll"))
|
||||
{
|
||||
return Ok(None);
|
||||
}
|
||||
let suggestions = items
|
||||
.iter()
|
||||
.filter_map(|item| {
|
||||
let rule = item["command"]
|
||||
.as_str()?
|
||||
.strip_prefix(".uno:SpellCheckApplySuggestion?ApplyRule:string=")?;
|
||||
Some((item["text"].as_str()?.to_string(), rule.to_string()))
|
||||
})
|
||||
.collect();
|
||||
Ok(Some(Spelling { suggestions }))
|
||||
}
|
||||
|
||||
/// A heading from `.uno:Outline`.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Heading {
|
||||
@@ -190,11 +157,11 @@ pub fn formula(json: &str) -> io::Result<Option<String>> {
|
||||
}
|
||||
}
|
||||
|
||||
fn parse(json: &str) -> io::Result<serde_json::Value> {
|
||||
pub(crate) fn parse(json: &str) -> io::Result<serde_json::Value> {
|
||||
serde_json::from_str(json).map_err(|error| invalid(format!("a bad kernel list: {error}")))
|
||||
}
|
||||
|
||||
fn entries<'a>(list: &'a serde_json::Value, key: &str) -> io::Result<&'a Vec<serde_json::Value>> {
|
||||
pub(crate) fn entries<'a>(list: &'a serde_json::Value, key: &str) -> io::Result<&'a Vec<serde_json::Value>> {
|
||||
list[key]
|
||||
.as_array()
|
||||
.ok_or_else(|| invalid(format!("the list has no {key}")))
|
||||
@@ -257,7 +224,7 @@ fn plain(html: &str) -> String {
|
||||
.replace("&", "&")
|
||||
}
|
||||
|
||||
fn invalid(message: String) -> io::Error {
|
||||
pub(crate) fn invalid(message: String) -> io::Error {
|
||||
io::Error::new(io::ErrorKind::InvalidData, message)
|
||||
}
|
||||
|
||||
@@ -288,12 +255,5 @@ mod tests {
|
||||
let counts = counts(r#"{"document":{"words":11,"characters":41,"charactersNoSpaces":31},"selection":{"words":2,"characters":7,"charactersNoSpaces":6}}"#)
|
||||
.expect("counts parse");
|
||||
assert_eq!((counts.words, counts.selected_characters), (11, 7));
|
||||
let menu = r#"{"menu":[{"text":"The","type":"command","command":".uno:SpellCheckApplySuggestion?ApplyRule:string=Spelling_The"},{"type":"separator"},{"text":"Ignore All","type":"command","command":".uno:SpellCheckIgnoreAll?Type:string=Spelling"}]}"#;
|
||||
let found = spelling(menu)
|
||||
.expect("the menu parses")
|
||||
.expect("a spelling menu");
|
||||
assert_eq!(found.suggestions, [("The".into(), "Spelling_The".into())]);
|
||||
let other = r#"{"menu":[{"text":"Cut","type":"command","command":".uno:Cut"}]}"#;
|
||||
assert_eq!(spelling(other).expect("the menu parses"), None);
|
||||
}
|
||||
}
|
||||
@@ -137,6 +137,7 @@ pub struct LokView {
|
||||
/// The last search outcome: its count and matches.
|
||||
search: (u64, Vec<Rect>),
|
||||
menu: Option<String>,
|
||||
right_at: Option<Point<Pixels>>,
|
||||
fonts: Vec<String>,
|
||||
a11y: crate::A11y,
|
||||
tabs: Vec<i32>,
|
||||
@@ -239,6 +240,7 @@ impl LokView {
|
||||
review: 0,
|
||||
search: (0, Vec::new()),
|
||||
menu: None,
|
||||
right_at: None,
|
||||
fonts: Vec::new(),
|
||||
a11y: crate::A11y::default(),
|
||||
tabs: Vec::new(),
|
||||
|
||||
@@ -313,6 +313,7 @@ impl LokView {
|
||||
return;
|
||||
};
|
||||
window.focus(&self.focus, cx);
|
||||
self.right_at = (event.button == MouseButton::Right).then_some(event.position);
|
||||
if event.button == MouseButton::Left && self.grab(event.position) {
|
||||
cx.notify();
|
||||
return;
|
||||
@@ -391,7 +392,17 @@ impl LokView {
|
||||
}
|
||||
|
||||
pub(super) fn on_copy(&mut self, _: &Copy, _: &mut Window, _: &mut Context<Self>) {
|
||||
self.send(Input::Copy { cut: false });
|
||||
self.copy(false);
|
||||
}
|
||||
|
||||
/// Copies the selection out, or cuts it.
|
||||
pub fn copy(&mut self, cut: bool) {
|
||||
self.send(Input::Copy { cut });
|
||||
}
|
||||
|
||||
/// Pastes the system clipboard.
|
||||
pub fn paste(&mut self) {
|
||||
self.send(Input::Paste);
|
||||
}
|
||||
|
||||
pub(super) fn on_next_field(&mut self, _: &NextField, _: &mut Window, _: &mut Context<Self>) {
|
||||
@@ -410,11 +421,11 @@ impl LokView {
|
||||
}
|
||||
|
||||
pub(super) fn on_cut(&mut self, _: &Cut, _: &mut Window, _: &mut Context<Self>) {
|
||||
self.send(Input::Copy { cut: true });
|
||||
self.copy(true);
|
||||
}
|
||||
|
||||
pub(super) fn on_paste(&mut self, _: &Paste, _: &mut Window, _: &mut Context<Self>) {
|
||||
self.send(Input::Paste);
|
||||
self.paste();
|
||||
}
|
||||
|
||||
/// Pastes values or formulas alone (TABLE-08).
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! What the view knows, for apps to read.
|
||||
|
||||
use gpui::FocusHandle;
|
||||
use gpui::{FocusHandle, Pixels, Point};
|
||||
|
||||
use super::LokView;
|
||||
use crate::Rect;
|
||||
@@ -91,9 +91,10 @@ impl LokView {
|
||||
&self.fonts
|
||||
}
|
||||
|
||||
/// Takes the context menu the kernel last offered.
|
||||
pub fn take_menu(&mut self) -> Option<String> {
|
||||
self.menu.take()
|
||||
/// Takes the kernel's last menu and the click that asked.
|
||||
pub fn take_menu(&mut self) -> Option<(String, Option<Point<Pixels>>)> {
|
||||
let menu = self.menu.take()?;
|
||||
Some((menu, self.right_at.take()))
|
||||
}
|
||||
|
||||
/// The paragraph's own tab stops, in 1/100 mm.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! ElyDoc's find, spelling, compare and merge messages.
|
||||
//! ElyDoc's find, spelling, menu, compare and merge messages.
|
||||
|
||||
pub(super) const EN: &[(&str, &str)] = &[
|
||||
("menu.tools", "Tools"),
|
||||
@@ -13,6 +13,36 @@ 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"),
|
||||
("doc.menu.link", "Link…"),
|
||||
("doc.menu.comment", "New Comment"),
|
||||
("doc.menu.insert", "Insert"),
|
||||
("doc.menu.column_left", "Insert Columns to the Left"),
|
||||
("doc.menu.column_right", "Insert Columns to the Right"),
|
||||
("doc.menu.row_above", "Insert Rows Above"),
|
||||
("doc.menu.row_below", "Insert Rows Below"),
|
||||
("doc.menu.delete", "Delete"),
|
||||
("doc.menu.delete_rows", "Delete Rows"),
|
||||
("doc.menu.delete_columns", "Delete Columns"),
|
||||
("doc.menu.delete_table", "Delete Table"),
|
||||
("doc.menu.merge", "Merge Cells"),
|
||||
("doc.menu.split", "Split Cells"),
|
||||
("doc.menu.split_columns", "Two Columns"),
|
||||
("doc.menu.split_rows", "Two Rows"),
|
||||
("doc.menu.even_rows", "Distribute Rows Evenly"),
|
||||
("doc.menu.even_columns", "Distribute Columns Evenly"),
|
||||
("doc.menu.wrap", "Wrap Text"),
|
||||
("doc.menu.inline", "In Line with Text"),
|
||||
("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"),
|
||||
("doc.menu.caption", "Insert Caption…"),
|
||||
("doc.compare.open", "Compare With…"),
|
||||
("doc.compare.name", "{name} compared"),
|
||||
("doc.output.export_title", "Export as PDF"),
|
||||
@@ -78,6 +108,36 @@ 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", "粘贴"),
|
||||
("doc.menu.link", "链接…"),
|
||||
("doc.menu.comment", "新建批注"),
|
||||
("doc.menu.insert", "插入"),
|
||||
("doc.menu.column_left", "在左侧插入列"),
|
||||
("doc.menu.column_right", "在右侧插入列"),
|
||||
("doc.menu.row_above", "在上方插入行"),
|
||||
("doc.menu.row_below", "在下方插入行"),
|
||||
("doc.menu.delete", "删除"),
|
||||
("doc.menu.delete_rows", "删除行"),
|
||||
("doc.menu.delete_columns", "删除列"),
|
||||
("doc.menu.delete_table", "删除表格"),
|
||||
("doc.menu.merge", "合并单元格"),
|
||||
("doc.menu.split", "拆分单元格"),
|
||||
("doc.menu.split_columns", "拆分为两列"),
|
||||
("doc.menu.split_rows", "拆分为两行"),
|
||||
("doc.menu.even_rows", "平均分布各行"),
|
||||
("doc.menu.even_columns", "平均分布各列"),
|
||||
("doc.menu.wrap", "环绕文字"),
|
||||
("doc.menu.inline", "嵌入型"),
|
||||
("doc.menu.square", "四周型"),
|
||||
("doc.menu.top_bottom", "上下型"),
|
||||
("doc.menu.in_front", "浮于文字上方"),
|
||||
("doc.menu.front", "置于顶层"),
|
||||
("doc.menu.forward", "上移一层"),
|
||||
("doc.menu.back", "置于底层"),
|
||||
("doc.menu.backward", "下移一层"),
|
||||
("doc.menu.caption", "插入题注…"),
|
||||
("doc.compare.open", "与其他版本比较…"),
|
||||
("doc.compare.name", "{name} 比较结果"),
|
||||
("doc.output.export_title", "导出为 PDF"),
|
||||
|
||||
Reference in new issue
Block a user