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..f1899cae7 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,24 @@ export class ContextMenuManager { ]); menu.popup(); } + + static alignmentMenu() { + 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);}, + }) + ); + } } 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..e4555547f 100644 --- a/model/canvas.h +++ b/model/canvas.h @@ -161,6 +161,15 @@ namespace minsky /// select all items in a given region void select(const LassoBox&); + /// 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); + } + 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..bfe8294b3 100644 --- a/model/selection.cc +++ b/model/selection.cc @@ -117,7 +117,45 @@ namespace minsky return true; return false; } - + + void Selection::align(const Item& ref, Align align) + { + 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()); + 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; + } + }; + + for (auto& i: items) apply(i); + for (auto& g: groups) apply(g); + } } 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); }; }