Browse / github.com/storytold/wordcraft / patch
View › Macros opens a dialog to run, delete and record macros (#4327)
View › Macros button does nothing (Record Macro / Alt+F8 untested) ## What happens Clicking **View › Macros** in the ribbon opens nothing: no dialog, no menu, no error. ## What I expected A macro list or dialog, as in Word. The command search lists both "Record Macro" and "Macros" under View › Macros (Alt+F8), so the commands seem to exist. ## Steps to reproduce 1. Open WordCraft and create a blank document. 2. Go to the **View** tab. 3. Click **Macros** (the icon in the Macros group). 4. Nothing appears. ## Other things I tried - Command search (typing "mac") shows "Record Macro" and "Macros — Alt+F8". - Running those from command search: [FILL IN: what h
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- x-labb (imported from pull request #4426, open)
- Base
2cf1d6c7ae94723a45bd6e7e8c884fe375148750- Size
- +133 −1 in 7 files
- Review
- pending
- Shared
- 2026-10-28
- Id
ad9c7d4d986a7d98f44660fa
How it went
No other operator has reported using it yet.
Diff
raw · crates/ui-egui/src/app/commands.rs crates/ui-egui/src/dialogs.rs crates/ui-egui/src/dialogs/bookmarks.rs crates/ui-egui/src/dialogs/chrome.rs crates/ui-egui/src/dialogs/dispatch.rs crates/ui-egui/src/dialogs/tests.rs docs/ui-parity.md
@@ -942,6 +942,8 @@ fn input_dialog(id: &str, params: &Value) -> Option<&'static str> { "references.citation" if !has("tag") && !has("source") => Some("createSource"), // Templates and Add-ins, and the Organizer without a file to list (#377). "tools.templates" => Some("templates"),+ // Macros without a name to run, define or delete shows its dialog (#4327); scripts get the list.+ "tools.macros" if !["run", "define", "delete"].into_iter().any(has) => Some("macros"), "styles.organizer" if !has("path") => Some("organizer"), // Language, Envelopes and Labels without settings show their dialogs (#407). "review.language" if params.as_object().is_none_or(|m| m.is_empty()) => Some("language"),@@ -278,6 +278,10 @@ pub enum Dialog { /// The line the border choices draw. pen: crate::dialogs_table_style::BorderPen, },+ /// View › Macros: the recorded macros to run or delete, and Record (#4327).+ Macros {+ name: String,+ }, Commands { query: String, /// Opened by a shortcut that may also send its key's text (Alt+Q's "q", in the same frame@@ -502,6 +506,7 @@ impl Dialog { Dialog::TableStyle { id: None, .. } => "newTableStyle", Dialog::TableStyle { .. } => "modifyTableStyle", Dialog::Commands { .. } => "commands",+ Dialog::Macros { .. } => "macros", Dialog::Insert(d) => d.name(), Dialog::Refs(d) => d.name(), Dialog::Icons { .. } => "icons",@@ -732,6 +737,7 @@ impl Dialog { "manageStyles" => Dialog::ManageStyles { alphabetical: false, selected: s("style") }, "newTableStyle" | "modifyTableStyle" => crate::dialogs_table_style::open(app, name)?, "commands" => Dialog::Commands { query: String::new(), swallow: true },+ "macros" => Dialog::Macros { name: String::new() }, "icons" => Dialog::Icons { query: String::new(), category: String::new(), selected: String::new() }, "tableProperties" => { let form = Box::new(TableForm::read(app)?);@@ -198,3 +198,63 @@ pub(super) fn enclose_preview(ui: &mut Ui, style: &str, text: &str, shape: &str) let dy = if shape == "triangle" && box_h > 0.0 { h * 0.3 } else { 0.0 }; p.text(c + vec2(0.0, dy), egui::Align2::CENTER_CENTER, text, egui::FontId::proportional(px), ink); }++/// View › Macros (#4327): the macros recorded or defined this session, with Run and Delete beside+/// the list and Record below it. While a recording is running the box offers Stop Recording instead.+/// The list is read from the session, not from `tools.macros`, which would ask to open this+/// dialog again. Run and Record close the dialog; Delete leaves it open.+pub(super) fn macros(app: &mut WordApp, ui: &mut Ui, name: &mut String) -> bool {+ let names: Vec<String> = app.session.macros.keys().cloned().collect();+ let recording = app.session.recording.as_ref().map(|(n, _)| n.clone());+ let t = Tokens::get(ui.ctx());+ let (mut run, mut delete, mut record) = (false, false, false);+ ui.horizontal_top(|ui| {+ ui.vertical(|ui| {+ let name_label = ui.label(tl!("Macro name:"));+ ui.add(egui::TextEdit::singleline(name).desired_width(240.0)).labelled_by(name_label.id);+ egui::Frame::NONE.stroke(egui::Stroke::new(1.0, t.border)).inner_margin(4).show(ui, |ui| {+ ui.set_width(232.0);+ ui.set_height(150.0);+ egui::ScrollArea::vertical().id_salt("macro_list").max_height(150.0).auto_shrink([false, false]).show(ui, |ui| {+ if names.is_empty() {+ ui.label(egui::RichText::new(tl!("There are no macros yet. Record one to add it here.")).small().color(t.text_dim));+ }+ for n in &names {+ if ui.selectable_label(*n == *name, n.as_str()).clicked() {+ *name = n.clone();+ }+ }+ });+ });+ });+ ui.vertical(|ui| {+ ui.add_space(18.0);+ let listed = names.contains(name);+ let w = vec2(110.0, 0.0);+ run = ui.add_enabled(listed && recording.is_none(), egui::Button::new(tl!("Run")).min_size(w)).clicked();+ delete = ui.add_enabled(listed, egui::Button::new(tl!("Delete")).min_size(w)).clicked();+ let record_label = if recording.is_some() { tl!("Stop Recording") } else { tl!("Record…") };+ record = ui.add_enabled(recording.is_some() || !name.trim().is_empty(), egui::Button::new(record_label).min_size(w)).clicked();+ });+ });+ if run {+ let _ = app.run("tools.macros", json!({"run": name.as_str()}));+ return true;+ }+ if delete && app.run("tools.macros", json!({"delete": name.as_str()})).is_ok() {+ name.clear();+ }+ if record {+ // Stopping takes no name; starting records under the one typed.+ let _ = app.run("tools.recordMacro", if recording.is_some() { json!({}) } else { json!({"name": name.trim()}) });+ return true;+ }+ ui.add_space(8.0);+ let mut close = ui.input(|i| i.key_pressed(egui::Key::Escape));+ ui.horizontal(|ui| {+ ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {+ close |= ui.button(tl!("Close")).clicked();+ });+ });+ close+}@@ -97,6 +97,7 @@ pub fn show(app: &mut WordApp, ctx: &egui::Context) { Dialog::TableStyle { id: None, .. } => "New Table Style", Dialog::TableStyle { .. } => "Modify Table Style", Dialog::Commands { .. } => "Search Commands",+ Dialog::Macros { .. } => "Macros", Dialog::Insert(d) => d.title(), Dialog::Refs(d) => d.title(), Dialog::Icons { .. } => "Icons",@@ -1,4 +1,4 @@-use super::bookmarks::{bookmark, enclose_preview, icons_body};+use super::bookmarks::{bookmark, enclose_preview, icons_body, macros}; use super::chrome::{buttons, color_field}; use super::find::{FindTab, SEARCH_SUGGESTIONS, command_matches, find_in_navigation_pane, goto_body, special_menu, swallow_shortcut_text}; use super::font::{apply_font, preview_size};@@ -874,6 +874,7 @@ pub(super) fn body(app: &mut WordApp, ui: &mut Ui, d: &mut Dialog) -> bool { } Dialog::Insert(f) => crate::dialogs_insert::body(app, ui, f), Dialog::Refs(f) => crate::dialogs_refs::body(app, ui, f),+ Dialog::Macros { name } => macros(app, ui, name), Dialog::Commands { query, swallow } => { if *swallow { ui.ctx().input_mut(|i| i.events.retain(|e| !swallow_shortcut_text(swallow, e)));@@ -144,6 +144,67 @@ fn find_replace_and_go_to_are_one_dialog() { } } +/// #4327: View › Macros opens a dialog that runs, deletes and records macros; the command with+/// a name to run or delete (what scripts send) still acts without one.+#[test]+fn the_macros_button_opens_a_dialog_that_runs_deletes_and_records() {+ fn press(h: &mut egui_kittest::Harness<'_, WordApp>, label: &str) {+ use egui_kittest::kittest::Queryable;+ let dialog = h.ctx.memory(|m| m.area_rect(egui::Id::unique(("dialog", "Macros")))).expect("the dialog is open");+ h.query_all_by_label(label).find(|n| dialog.contains(n.rect().center())).expect(label).click();+ for _ in 0..3 {+ h.step();+ }+ }+ let mut app = app_with("Start");+ for name in ["Greet", "Spare"] {+ app.run("tools.macros", json!({"define": {"name": name, "steps": [{"command": "text.insert", "params": {"text": name}}]}})).unwrap();+ }+ assert!(app.dialog.is_none(), "defining a macro opens nothing");+ assert_eq!(app.run("tools.macros", json!({})).unwrap(), json!({"pending": "macros"}));+ let mut h = app_harness(app);+ h.run_steps(4);+ assert!(matches!(&h.state().dialog, Some(Dialog::Macros { .. })));++ press(&mut h, "Spare");+ press(&mut h, "Delete");+ let names: Vec<&String> = h.state().session.macros.keys().collect();+ assert_eq!(names, ["Greet"]);+ assert!(h.state().dialog.is_some(), "Delete leaves the dialog open");++ press(&mut h, "Greet");+ press(&mut h, "Run");+ assert!(h.state().dialog.is_none());+ assert!(body_of(h.state()).contains("Greet"), "the macro ran");++ // A macro whose name has spaces around it (only a script can make one) runs by its exact name.+ h.state_mut()+ .run("tools.macros", json!({"define": {"name": " Pad ", "steps": [{"command": "text.insert", "params": {"text": "Padded"}}]}}))+ .unwrap();+ h.state_mut().dialog = Dialog::open("macros", h.state_mut());+ if let Some(Dialog::Macros { name }) = &mut h.state_mut().dialog {+ *name = " Pad ".into();+ }+ h.run_steps(3);+ press(&mut h, "Run");+ assert!(h.state().dialog.is_none());+ assert!(body_of(h.state()).contains("Padded"), "the macro with spaces in its name ran");++ // Record starts a recording under the typed name; the dialog then offers to stop it.+ h.state_mut().dialog = Dialog::open("macros", h.state_mut());+ if let Some(Dialog::Macros { name }) = &mut h.state_mut().dialog {+ *name = "Fresh".into();+ }+ h.run_steps(3);+ press(&mut h, "Record…");+ assert_eq!(h.state().session.recording.as_ref().map(|(n, _)| n.as_str()), Some("Fresh"));+ assert!(h.state().dialog.is_none());+}++fn body_of(app: &WordApp) -> String {+ app.session.doc.plain_text(wordcraft_doc::StoryRef::Body)+}+ fn app_with(text: &str) -> WordApp { let mut app = WordApp::new(Session::new(wordcraft_doc::Document::new()), crate::Services::default()); let _ = app.run("document.setText", json!({"text": text}));@@ -82,6 +82,7 @@ Footnote and endnote editing keeps body and note text at normal opacity, on ligh | Date | Change | Summary | |---|---|---|+| 2026-10-26 | trivial | View › Macros opens a Macros dialog (also Alt+F8 and command search): the macros of this session listed with Run, Delete and Record, which reads Stop Recording while a recording runs; the button did nothing before, as `tools.macros` without a macro to run, define or delete only returned the list (#4327) | | 2026-10-22 | trivial | View › Window › New Window opens another window on the document, focused and cascaded; two windows on one document are titled `Name:1` and `Name:2` (one window: the plain name). Each window keeps its own selection, scroll position, zoom, view mode, ruler and panes; the text, undo history, unsaved state and file are the document's, so an edit or Undo in either window shows in both, and a caret in text the other window deleted moves to the nearest place left. Closing one of a document's windows asks nothing; closing its last asks about unsaved changes, and quitting asks once per document. Switch Windows lists every window. The web app keeps one window | | 2026-10-22 | trivial | One native window per open document on the desktop (the web app keeps one window): New and Open open the document in a window of its own, focused and cascaded from the last; Open replaces an untouched blank document in its window and brings an already-open file's window forward; each window keeps its own ribbon tab, File page, dialogs, panes and composition; View › Window › Switch Windows lists the windows and brings the chosen one forward; File › Close and the window's close button close the window (asking about unsaved changes) and the window focused before comes forward, while the last window stays open with no document; quitting asks Save / Don't Save / Cancel about each document in its own window | | 2026-10-22 | trivial | Several open documents in one window (one window per document comes next): View › Window › Switch Windows lists the open documents, the shown one marked and unsaved ones dotted, and shows the one chosen, with its own scroll position, zoom, comment drafts and AutoSave file; File › New, Open, Envelopes, Labels, Finish & Merge and personal templates open beside the open documents without asking about unsaved changes; File › Close shows the next open document; the window's close button asks Save / Don't Save / Cancel about each document with unsaved changes, showing it, and Cancel keeps the app open. Untitled documents are numbered (Document2) in the interface language |