collective

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

diff --git a/crates/ui-egui/src/app/commands.rs b/crates/ui-egui/src/app/commands.rs--- a/crates/ui-egui/src/app/commands.rs+++ b/crates/ui-egui/src/app/commands.rs@@ -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"),diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs--- a/crates/ui-egui/src/dialogs.rs+++ b/crates/ui-egui/src/dialogs.rs@@ -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)?);diff --git a/crates/ui-egui/src/dialogs/bookmarks.rs b/crates/ui-egui/src/dialogs/bookmarks.rs--- a/crates/ui-egui/src/dialogs/bookmarks.rs+++ b/crates/ui-egui/src/dialogs/bookmarks.rs@@ -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+}diff --git a/crates/ui-egui/src/dialogs/chrome.rs b/crates/ui-egui/src/dialogs/chrome.rs--- a/crates/ui-egui/src/dialogs/chrome.rs+++ b/crates/ui-egui/src/dialogs/chrome.rs@@ -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",diff --git a/crates/ui-egui/src/dialogs/dispatch.rs b/crates/ui-egui/src/dialogs/dispatch.rs--- a/crates/ui-egui/src/dialogs/dispatch.rs+++ b/crates/ui-egui/src/dialogs/dispatch.rs@@ -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)));diff --git a/crates/ui-egui/src/dialogs/tests.rs b/crates/ui-egui/src/dialogs/tests.rs--- a/crates/ui-egui/src/dialogs/tests.rs+++ b/crates/ui-egui/src/dialogs/tests.rs@@ -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}));diff --git a/docs/ui-parity.md b/docs/ui-parity.md--- a/docs/ui-parity.md+++ b/docs/ui-parity.md@@ -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 |