mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-27 12:16:54 -06:00
282 lines
12 KiB
Rust
282 lines
12 KiB
Rust
//! macOS: the native menu bar (File, Edit, View, Insert, Format, Tools, Data, Window, Help), as
|
|
//! a desktop spreadsheet has on the Mac. Items run the same commands as the ribbon.
|
|
|
|
use std::str::FromStr;
|
|
use std::sync::mpsc::{Receiver, channel};
|
|
|
|
use gridcraft_ui_egui::SheetApp;
|
|
use muda::accelerator::Accelerator;
|
|
use muda::{Menu, MenuEvent, MenuItem, PredefinedMenuItem, Submenu};
|
|
use serde_json::{Value, json};
|
|
|
|
type Entry = (&'static str, &'static str, Option<&'static str>);
|
|
|
|
/// (label, command or `dialog:name`, accelerator). `-` is a separator.
|
|
fn tree() -> Vec<(&'static str, Vec<Entry>)> {
|
|
vec![
|
|
(
|
|
"File",
|
|
vec![
|
|
("New Workbook", "file.new", Some("CMD+N")),
|
|
("New from Sample…", "dialog:start", None),
|
|
("Open…", "file.open", Some("CMD+O")),
|
|
("-", "", None),
|
|
("Close", "file.close", Some("CMD+W")),
|
|
("Save", "file.save", Some("CMD+S")),
|
|
("Save As…", "file.saveAs", Some("CMD+SHIFT+S")),
|
|
("Export as CSV…", "dialog:saveCopy:csv", None),
|
|
("Save as Web Page…", "dialog:saveCopy:html", None),
|
|
("-", "", None),
|
|
("Page Setup…", "dialog:pageSetup", None),
|
|
("Print…", "file.print", Some("CMD+P")),
|
|
("-", "", None),
|
|
("Properties…", "file.properties", None),
|
|
],
|
|
),
|
|
(
|
|
"Edit",
|
|
vec![
|
|
("Undo", "edit.undo", None),
|
|
("Redo", "edit.redo", None),
|
|
("-", "", None),
|
|
("Cut", "edit.cut", None),
|
|
("Copy", "edit.copy", None),
|
|
("Paste", "edit.paste", None),
|
|
("Paste Special…", "dialog:pasteSpecial", Some("CTRL+CMD+V")),
|
|
("-", "", None),
|
|
("Fill Down", "edit.fillDown", None),
|
|
("Fill Right", "edit.fillRight", None),
|
|
("Series…", "dialog:series", None),
|
|
("Flash Fill", "edit.flashFill", None),
|
|
("-", "", None),
|
|
("Clear All", "edit.clearAll", None),
|
|
("Clear Formats", "edit.clearFormats", None),
|
|
("Clear Contents", "edit.clearContents", None),
|
|
("-", "", None),
|
|
("Delete…", "dialog:deleteCells", None),
|
|
("Delete Sheet", "home.deleteSheet", None),
|
|
("-", "", None),
|
|
("Find…", "dialog:find", None),
|
|
("Replace…", "dialog:find:replace", Some("CTRL+H")),
|
|
("Go To…", "dialog:goTo", Some("CTRL+G")),
|
|
],
|
|
),
|
|
(
|
|
"View",
|
|
vec![
|
|
("Normal", "view.normal", None),
|
|
("Page Layout", "view.pageLayout", None),
|
|
("Page Break Preview", "view.pageBreakPreview", None),
|
|
("-", "", None),
|
|
("Formula Bar", "view.formulaBar", None),
|
|
("Gridlines", "view.gridlines", None),
|
|
("Headings", "view.headings", None),
|
|
("-", "", None),
|
|
("Zoom…", "dialog:zoom", None),
|
|
("Zoom to Selection", "view.zoomToSelection", None),
|
|
("Freeze Panes", "view.freezePanes", None),
|
|
("Freeze Top Row", "view.freezeTopRow", None),
|
|
("Unfreeze Panes", "view.unfreezePanes", None),
|
|
("-", "", None),
|
|
("Dark Mode", "view.darkMode", None),
|
|
("Collapse Ribbon", "view.collapseRibbon", Some("CMD+ALT+R")),
|
|
],
|
|
),
|
|
(
|
|
"Insert",
|
|
vec![
|
|
("Cells…", "dialog:insertCells", None),
|
|
("Rows", "home.insertRows", None),
|
|
("Columns", "home.insertColumns", None),
|
|
("Sheet", "home.insertSheet", Some("SHIFT+F11")),
|
|
("-", "", None),
|
|
("Chart", "insert.recommendedCharts", None),
|
|
("Sparklines…", "dialog:sparkline", None),
|
|
("PivotTable", "insert.pivotTable", None),
|
|
("Table", "insert.table", None),
|
|
("-", "", None),
|
|
("Function…", "dialog:insertFunction", Some("SHIFT+F3")),
|
|
("Name…", "dialog:defineName", None),
|
|
("New Comment", "dialog:comment", None),
|
|
("New Note", "dialog:note", None),
|
|
("Picture…", "dialog:insertPicture", None),
|
|
("Text Box", "insert.textBox", None),
|
|
("Link…", "dialog:insertLink", None),
|
|
("Checkbox", "insert.checkbox", None),
|
|
],
|
|
),
|
|
(
|
|
"Format",
|
|
vec![
|
|
("Cells…", "dialog:formatCells", None),
|
|
("Row Height…", "dialog:rowHeight", None),
|
|
("AutoFit Row Height", "home.autofitRowHeight", None),
|
|
("Column Width…", "dialog:columnWidth", None),
|
|
("AutoFit Column Width", "home.autofitColumnWidth", None),
|
|
("-", "", None),
|
|
("Hide Rows", "home.hideRows", None),
|
|
("Unhide Rows", "home.unhideRows", None),
|
|
("Hide Columns", "home.hideColumns", None),
|
|
("Unhide Columns", "home.unhideColumns", None),
|
|
("-", "", None),
|
|
("Rename Sheet…", "dialog:renameSheet", None),
|
|
("Hide Sheet", "sheet.hide", None),
|
|
("Unhide Sheet…", "sheet.unhide", None),
|
|
("-", "", None),
|
|
("Conditional Formatting…", "dialog:manageRules", None),
|
|
("Format as Table", "insert.table", None),
|
|
],
|
|
),
|
|
(
|
|
"Tools",
|
|
vec![
|
|
("Spelling…", "dialog:spelling", None),
|
|
("Workbook Statistics", "dialog:statistics", None),
|
|
("Check Accessibility", "dialog:accessibility", None),
|
|
("-", "", None),
|
|
("Protect Sheet…", "dialog:protectSheet", None),
|
|
("Protect Workbook", "review.protectWorkbook", None),
|
|
("-", "", None),
|
|
("Goal Seek…", "dialog:goalSeek", None),
|
|
("Error Checking…", "dialog:errorChecking", None),
|
|
("Evaluate Formula…", "dialog:evaluateFormula", None),
|
|
("-", "", None),
|
|
("Record Actions", "automate.recordActions", None),
|
|
("Command Palette…", "dialog:commandSearch", Some("CMD+SHIFT+P")),
|
|
],
|
|
),
|
|
(
|
|
"Data",
|
|
vec![
|
|
("Sort…", "dialog:sort", None),
|
|
("Sort A to Z", "data.sortAscending", None),
|
|
("Sort Z to A", "data.sortDescending", None),
|
|
("Filter", "data.filter", None),
|
|
("Clear Filter", "data.clearFilter", None),
|
|
("-", "", None),
|
|
("Text to Columns…", "dialog:textToColumns", None),
|
|
("Remove Duplicates…", "dialog:removeDuplicates", None),
|
|
("Data Validation…", "dialog:dataValidation", None),
|
|
("-", "", None),
|
|
("Group", "data.group", None),
|
|
("Ungroup", "data.ungroup", None),
|
|
("Subtotal…", "dialog:subtotal", None),
|
|
("-", "", None),
|
|
("Calculate Now", "formulas.calculateNow", None),
|
|
],
|
|
),
|
|
("Window", vec![("New Window", "view.newWindow", None), ("Next Sheet", "sheet.next", None), ("Previous Sheet", "sheet.previous", None)]),
|
|
(
|
|
"Help",
|
|
vec![
|
|
("Agent Control (MCP)…", "dialog:agents", None),
|
|
("Contributors…", "dialog:about:Contributors", None),
|
|
("GridCraft on getartcraft.com", "url:https://getartcraft.com/apps/gridcraft", None),
|
|
("Join the ArtCraft Discord…", "url:https://discord.gg/artcraft", None),
|
|
],
|
|
),
|
|
]
|
|
}
|
|
|
|
pub struct NativeMenu {
|
|
_menu: Menu,
|
|
events: Receiver<MenuEvent>,
|
|
}
|
|
|
|
impl NativeMenu {
|
|
pub fn install(ctx: &egui::Context) -> NativeMenu {
|
|
let menu = Menu::new();
|
|
let app_menu = Submenu::new("GridCraft", true);
|
|
let about = MenuItem::with_id("dialog:about", "About GridCraft", true, None);
|
|
let _ = app_menu.append_items(&[
|
|
&about,
|
|
&PredefinedMenuItem::separator(),
|
|
&PredefinedMenuItem::services(None),
|
|
&PredefinedMenuItem::separator(),
|
|
&PredefinedMenuItem::hide(None),
|
|
&PredefinedMenuItem::hide_others(None),
|
|
&PredefinedMenuItem::show_all(None),
|
|
&PredefinedMenuItem::separator(),
|
|
&PredefinedMenuItem::quit(None),
|
|
]);
|
|
let _ = menu.append(&app_menu);
|
|
for (title, entries) in tree() {
|
|
let sub = Submenu::new(title, true);
|
|
for (label, id, acc) in entries {
|
|
if label == "-" {
|
|
let _ = sub.append(&PredefinedMenuItem::separator());
|
|
continue;
|
|
}
|
|
let accel = acc.and_then(|a| Accelerator::from_str(a).ok());
|
|
let item = MenuItem::with_id(id, label, true, accel);
|
|
let _ = sub.append(&item);
|
|
}
|
|
let _ = menu.append(&sub);
|
|
}
|
|
menu.init_for_nsapp();
|
|
let (tx, rx) = channel();
|
|
let ctx = ctx.clone();
|
|
MenuEvent::set_event_handler(Some(move |e: MenuEvent| {
|
|
let _ = tx.send(e);
|
|
ctx.request_repaint();
|
|
}));
|
|
NativeMenu { _menu: menu, events: rx }
|
|
}
|
|
|
|
pub fn poll(&self, app: &mut SheetApp, ctx: &egui::Context) {
|
|
while let Ok(ev) = self.events.try_recv() {
|
|
let id = ev.id.as_ref().to_string();
|
|
// Text selection belongs to the inline editor, not the worksheet clipboard/undo.
|
|
if app.text_box_editor.is_some() {
|
|
let clipboard = match id.as_str() {
|
|
"edit.copy" => Some(egui::ViewportCommand::RequestCopy),
|
|
"edit.cut" => Some(egui::ViewportCommand::RequestCut),
|
|
"edit.paste" => Some(egui::ViewportCommand::RequestPaste),
|
|
_ => None,
|
|
};
|
|
if let Some(command) = clipboard {
|
|
ctx.send_viewport_cmd(command);
|
|
continue;
|
|
}
|
|
if id == "edit.undo" || id == "edit.redo" {
|
|
ctx.input_mut(|i| {
|
|
i.events.push(egui::Event::Key {
|
|
key: egui::Key::Z,
|
|
physical_key: None,
|
|
pressed: true,
|
|
repeat: false,
|
|
modifiers: egui::Modifiers { command: true, mac_cmd: true, shift: id == "edit.redo", ..Default::default() },
|
|
});
|
|
});
|
|
continue;
|
|
}
|
|
}
|
|
if let Some(url) = id.strip_prefix("url:") {
|
|
if let Some(open) = &app.services.open_url {
|
|
open(url);
|
|
}
|
|
} else if let Some(rest) = id.strip_prefix("dialog:") {
|
|
let (name, arg) = rest.split_once(':').unwrap_or((rest, ""));
|
|
let params: Value = match (name, arg) {
|
|
("saveCopy", fmt) => json!({"format": fmt}),
|
|
("find", "replace") => json!({"replace": true}),
|
|
("note", _) => json!({"threaded": false}),
|
|
("comment", _) => json!({"threaded": true}),
|
|
("about", tab) => json!({"tab": tab}),
|
|
_ => json!({}),
|
|
};
|
|
let name = if name == "note" { "comment" } else { name };
|
|
app.open_dialog(name, params);
|
|
} else if id == "edit.copy" || id == "edit.cut" {
|
|
if let Ok(r) = app.run(&id, json!({}))
|
|
&& let Some(t) = r.get("text").and_then(Value::as_str)
|
|
{
|
|
ctx.copy_text(t.to_string());
|
|
}
|
|
} else {
|
|
app.run_or_alert(&id, json!({}));
|
|
}
|
|
}
|
|
}
|
|
}
|