From 01354e075a7a4a8f652066f5fab8e7b7f4818562 Mon Sep 17 00:00:00 2001 From: Russell Standish Date: Fri, 29 May 2026 11:08:43 +1000 Subject: [PATCH 1/5] feat: Add alignment of a selection of items. For feature #328. --- .../src/app/managers/ContextMenuManager.ts | 24 +++++++++++++ gui-js/libs/shared/src/lib/backend/minsky.ts | 2 ++ model/canvas.h | 4 +++ model/selection.cc | 35 ++++++++++++++++++- model/selection.h | 4 +++ 5 files changed, 68 insertions(+), 1 deletion(-) diff --git a/gui-js/apps/minsky-electron/src/app/managers/ContextMenuManager.ts b/gui-js/apps/minsky-electron/src/app/managers/ContextMenuManager.ts index 1444cc0df..2c54b905e 100644 --- a/gui-js/apps/minsky-electron/src/app/managers/ContextMenuManager.ts +++ b/gui-js/apps/minsky-electron/src/app/managers/ContextMenuManager.ts @@ -528,6 +528,10 @@ export class ContextMenuManager { label: 'Add item to a publication tab', submenu: await ContextMenuManager.pubTabMenu(), }), + new MenuItem({ + label: 'Align', + submenu: Menu.buildFromTemplate(this.alignmentMenu()), + }), new MenuItem({ label: `Delete ${itemInfo.classType}`, click: () => CommandsManager.deleteCurrentItemHavingId(itemInfo.id) @@ -1403,5 +1407,25 @@ export class ContextMenuManager { ]); menu.popup(); } + + static alignmentMenu() { + const labels={ + left: "Left", + centre: "Centre", + right: "Right", + left_right: "Left & Right", + top: "Top", + middle: "Middle", + bottom: "Bottom", + top_bottom: "Top & Bottom", + }; + let menuItems=[]; + for (let i in labels) + menuItems.push(new MenuItem({ + label: labels[i], + click: async ()=>{await minsky.canvas.alignSelection(i);}, + })); + return menuItems; + } } diff --git a/gui-js/libs/shared/src/lib/backend/minsky.ts b/gui-js/libs/shared/src/lib/backend/minsky.ts index 3eb3d7e06..4485f1385 100644 --- a/gui-js/libs/shared/src/lib/backend/minsky.ts +++ b/gui-js/libs/shared/src/lib/backend/minsky.ts @@ -432,6 +432,7 @@ export class Canvas extends RenderNativeWindow { async addSheet(): Promise {return this.$callMethod('addSheet');} async addSwitch(): Promise {return this.$callMethod('addSwitch');} async addVariable(a1: string,a2: string): Promise {return this.$callMethod('addVariable',a1,a2);} + async alignSelection(a1: string): Promise {return this.$callMethod('alignSelection',a1);} async applyDefaultPlotOptions(): Promise {return this.$callMethod('applyDefaultPlotOptions');} async clickType(...args: string[]): Promise {return this.$callMethod('clickType',...args);} async closestInPort(a1: number,a2: number): Promise {return this.$callMethod('closestInPort',a1,a2);} @@ -1941,6 +1942,7 @@ export class Selection extends CppClass { async addWire(...args: any[]): Promise {return this.$callMethod('addWire',...args);} async adjustBookmark(): Promise {return this.$callMethod('adjustBookmark');} async adjustWiresGroup(a1: Wire): Promise {return this.$callMethod('adjustWiresGroup',a1);} + async align(a1: Item,a2: string): Promise {return this.$callMethod('align',a1,a2);} async arguments(): Promise {return this.$callMethod('arguments');} async autoLayout(): Promise {return this.$callMethod('autoLayout');} async bookmark(...args: boolean[]): Promise {return this.$callMethod('bookmark',...args);} diff --git a/model/canvas.h b/model/canvas.h index b7917b907..2a5d6fd78 100644 --- a/model/canvas.h +++ b/model/canvas.h @@ -161,6 +161,10 @@ namespace minsky /// select all items in a given region void select(const LassoBox&); + /// align items in selection + void alignSelection(Selection::Align align) + {if (item) selection.align(*item,align);} + int ravelsSelected() const; ///< number of ravels in selection /// sets itemFocus, and resets mouse offset for placement diff --git a/model/selection.cc b/model/selection.cc index 176efcdfa..726f57dd1 100644 --- a/model/selection.cc +++ b/model/selection.cc @@ -117,7 +117,40 @@ namespace minsky return true; return false; } - + + void Selection::align(const Item& ref, Align align) + { + for (auto& i: items) + switch (align) + { + case left: + i->moveTo(i->x()+ref.left()-i->left(), i->y()); + break; + case centre: + i->moveTo(ref.x(),i->y()); + break; + case right: + i->moveTo(i->x()+ref.right()-i->right(), i->y()); + break; + case left_right: + i->iWidth(ref.iWidth()); + i->moveTo(ref.x(),i->y()); + break; + case top: + i->moveTo(i->x(), i->y()+ref.top()-i->top()); + break; + case middle: + i->moveTo(i->x(), ref.y()); + break; + case bottom: + i->moveTo(i->x(), i->y()+ref.bottom()-i->bottom()); + break; + case top_bottom: + i->iHeight(ref.iHeight()); + i->moveTo(i->x(), ref.y()); + break; + } + } } CLASSDESC_ACCESS_EXPLICIT_INSTANTIATION(minsky::Selection); diff --git a/model/selection.h b/model/selection.h index 9706e8a1d..888eb71a5 100644 --- a/model/selection.h +++ b/model/selection.h @@ -53,6 +53,10 @@ namespace minsky /// return if item is contained in selection bool contains(const ItemPtr& item) const; using Item::contains; + + enum Align {left, centre, right, left_right, top, middle, bottom, top_bottom}; + /// Align items in this selection with the reference item + void align(const Item&, Align); }; } From ecaee310c4c3b51ffe8cf7e9d379f7e4045b1ecb Mon Sep 17 00:00:00 2001 From: Russell Standish Date: Fri, 29 May 2026 11:21:02 +1000 Subject: [PATCH 2/5] Also include groups in alignment Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- model/selection.cc | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/model/selection.cc b/model/selection.cc index 726f57dd1..bfe8294b3 100644 --- a/model/selection.cc +++ b/model/selection.cc @@ -120,21 +120,22 @@ namespace minsky void Selection::align(const Item& ref, Align align) { - for (auto& i: items) + auto apply=[&](const auto& i) + { switch (align) { case left: i->moveTo(i->x()+ref.left()-i->left(), i->y()); break; case centre: - i->moveTo(ref.x(),i->y()); + i->moveTo(ref.x(), i->y()); break; case right: i->moveTo(i->x()+ref.right()-i->right(), i->y()); break; case left_right: i->iWidth(ref.iWidth()); - i->moveTo(ref.x(),i->y()); + i->moveTo(ref.x(), i->y()); break; case top: i->moveTo(i->x(), i->y()+ref.top()-i->top()); @@ -150,6 +151,10 @@ namespace minsky i->moveTo(i->x(), ref.y()); break; } + }; + + for (auto& i: items) apply(i); + for (auto& g: groups) apply(g); } } From b5f52ee7d72e4d4ace18f2a495724c65a4eab3c9 Mon Sep 17 00:00:00 2001 From: Russell Standish Date: Fri, 29 May 2026 11:30:54 +1000 Subject: [PATCH 3/5] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../minsky-electron/src/app/managers/ContextMenuManager.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/gui-js/apps/minsky-electron/src/app/managers/ContextMenuManager.ts b/gui-js/apps/minsky-electron/src/app/managers/ContextMenuManager.ts index 2c54b905e..ae25c2a5f 100644 --- a/gui-js/apps/minsky-electron/src/app/managers/ContextMenuManager.ts +++ b/gui-js/apps/minsky-electron/src/app/managers/ContextMenuManager.ts @@ -1423,7 +1423,10 @@ export class ContextMenuManager { for (let i in labels) menuItems.push(new MenuItem({ label: labels[i], - click: async ()=>{await minsky.canvas.alignSelection(i);}, + click: async ()=>{ + await minsky.canvas.alignSelection(i); + await CommandsManager.requestRedraw(); + }, })); return menuItems; } From 02db4ca684137f74d8064c68ebab204b43a4398c Mon Sep 17 00:00:00 2001 From: Russell Standish Date: Fri, 29 May 2026 11:33:46 +1000 Subject: [PATCH 4/5] Refine commentary Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- model/canvas.h | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/model/canvas.h b/model/canvas.h index 2a5d6fd78..e4555547f 100644 --- a/model/canvas.h +++ b/model/canvas.h @@ -161,9 +161,14 @@ namespace minsky /// select all items in a given region void select(const LassoBox&); - /// align items in selection + /// align items in selection using the current context item as the anchor. + /// No-op when there is no current item (for example, no right-clicked item); + /// callers should only expose/enable this action when an anchor item exists. void alignSelection(Selection::Align align) - {if (item) selection.align(*item,align);} + { + if (item) + selection.align(*item,align); + } int ravelsSelected() const; ///< number of ravels in selection From b8eb2ae43e9582a6898441bb1f5a9ca8eac53921 Mon Sep 17 00:00:00 2001 From: Russell Standish Date: Fri, 29 May 2026 12:07:48 +1000 Subject: [PATCH 5/5] refactor: Refactor alignment menu to use tuple array. --- .../src/app/managers/ContextMenuManager.ts | 36 +++++++++---------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/gui-js/apps/minsky-electron/src/app/managers/ContextMenuManager.ts b/gui-js/apps/minsky-electron/src/app/managers/ContextMenuManager.ts index ae25c2a5f..f1899cae7 100644 --- a/gui-js/apps/minsky-electron/src/app/managers/ContextMenuManager.ts +++ b/gui-js/apps/minsky-electron/src/app/managers/ContextMenuManager.ts @@ -1409,26 +1409,22 @@ export class ContextMenuManager { } static alignmentMenu() { - const labels={ - left: "Left", - centre: "Centre", - right: "Right", - left_right: "Left & Right", - top: "Top", - middle: "Middle", - bottom: "Bottom", - top_bottom: "Top & Bottom", - }; - let menuItems=[]; - for (let i in labels) - menuItems.push(new MenuItem({ - label: labels[i], - click: async ()=>{ - await minsky.canvas.alignSelection(i); - await CommandsManager.requestRedraw(); - }, - })); - return menuItems; + const labels: [string, string][] = [ + ['left', 'Left'], + ['centre', 'Centre'], + ['right', 'Right'], + ['left_right', 'Left & Right'], + ['top', 'Top'], + ['middle', 'Middle'], + ['bottom', 'Bottom'], + ['top_bottom', 'Top & Bottom'], + ]; + return labels.map(([alignment, label]) => + new MenuItem({ + label, + click: async ()=>{await minsky.canvas.alignSelection(alignment);}, + }) + ); } }