From 01cc2b9f4c54dbafdcbd00b189aaf707d65b4de3 Mon Sep 17 00:00:00 2001 From: Niels Rademaker Date: Fri, 26 Dec 2025 18:29:43 +0100 Subject: [PATCH 1/3] context menu interactivity in edit mode + after wedge clicking --- .../src/app/events/electron.events.ts | 4 +- .../src/app/managers/CommandsManager.ts | 4 +- .../src/app/managers/ContextMenuManager.ts | 578 ++++++++---------- .../communication/communication.service.ts | 11 +- gui-js/libs/shared/src/lib/backend/minsky.ts | 6 + .../input-modal/input-modal.component.scss | 5 +- model/godleyIcon.h | 8 +- model/godleyTableWindow.cc | 9 + model/godleyTableWindow.h | 11 +- 9 files changed, 295 insertions(+), 341 deletions(-) diff --git a/gui-js/apps/minsky-electron/src/app/events/electron.events.ts b/gui-js/apps/minsky-electron/src/app/events/electron.events.ts index e5f398992..75253c21a 100644 --- a/gui-js/apps/minsky-electron/src/app/events/electron.events.ts +++ b/gui-js/apps/minsky-electron/src/app/events/electron.events.ts @@ -230,8 +230,8 @@ ipcMain.handle( } ); -ipcMain.on(events.CONTEXT_MENU, async (event, { x, y, type, command}) => { - await ContextMenuManager.initContextMenu(event, x, y, type, command); +ipcMain.on(events.CONTEXT_MENU, async (event, { x, y, type, command, leftClick}) => { + await ContextMenuManager.initContextMenu(event, x, y, type, command, leftClick); }); ipcMain.on( diff --git a/gui-js/apps/minsky-electron/src/app/managers/CommandsManager.ts b/gui-js/apps/minsky-electron/src/app/managers/CommandsManager.ts index e3fc658e9..7ff12e7e9 100644 --- a/gui-js/apps/minsky-electron/src/app/managers/CommandsManager.ts +++ b/gui-js/apps/minsky-electron/src/app/managers/CommandsManager.ts @@ -229,7 +229,7 @@ export class CommandsManager { title: `Edit godley title`, url: `#/headless/edit-godley-title?title=${encodeURIComponent(title) || ''}&itemId=${godleyId}`, useContentSize: true, - height: 100, + height: 80, width: 400, }); } @@ -239,7 +239,7 @@ export class CommandsManager { title: `Edit godley currency`, url: `#/headless/edit-godley-currency`, useContentSize: true, - height: 100, + height: 80, width: 400, }); } 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 38d30a1cb..2937ac59c 100644 --- a/gui-js/apps/minsky-electron/src/app/managers/ContextMenuManager.ts +++ b/gui-js/apps/minsky-electron/src/app/managers/ContextMenuManager.ts @@ -16,42 +16,48 @@ export class ContextMenuManager { private static showAllPlotsOnTabChecked = false; - public static async initContextMenu(event: IpcMainEvent, x: number, y: number, type: string, command: string) { - switch (type) - { - case "canvas": + public static async initContextMenu(event: IpcMainEvent, x: number, y: number, type: string, command: string, leftClick: boolean) { + const mainWindow = WindowManager.getMainWindow(); + this.x = x; + this.y = y; + + if(leftClick) { + if(type === 'canvas' && WindowManager.currentTab.$equal(minsky.canvas)) { + this.leftMouseGodley(mainWindow); + } + } else { + switch (type) { - const mainWindow = WindowManager.getMainWindow(); - this.x = x; - this.y = y; - - const currentTab = WindowManager.currentTab; - if (currentTab.$equal(minsky.canvas)) - this.initContextMenuForWiring(mainWindow); - else if (currentTab.$equal(minsky.phillipsDiagram)) - this.initContextMenuForPhillipsDiagram(mainWindow); - else - this.initContextMenuForPublication(event, mainWindow,currentTab); + case "canvas": + { + const currentTab = WindowManager.currentTab; + if (currentTab.$equal(minsky.canvas)) + this.initContextMenuForWiring(mainWindow); + else if (currentTab.$equal(minsky.phillipsDiagram)) + this.initContextMenuForPhillipsDiagram(mainWindow); + else + this.initContextMenuForPublication(event, mainWindow,currentTab); + } + break; + case 'publication-button': + this.initContextMenuForPublicationTabButton(event,command); + break; + case "godley": + this.initContextMenuForGodleyPopup(command,x,y); + break; + case "html-godley": + this.initContextMenuForGodleyHTMLPopup(event, command,x,y); + break; + case "ravel": + this.initContextMenuForRavelPopup(command,x,y); + break; + case "csv-import": + this.initContextMenuForCSVImport(event, command,x,y); + break; + default: + log.warn("Unknown context menu for ",type); + break; } - break; - case 'publication-button': - this.initContextMenuForPublicationTabButton(event,command); - break; - case "godley": - this.initContextMenuForGodleyPopup(command,x,y); - break; - case "html-godley": - this.initContextMenuForGodleyHTMLPopup(event, command,x,y); - break; - case "ravel": - this.initContextMenuForRavelPopup(command,x,y); - break; - case "csv-import": - this.initContextMenuForCSVImport(event, command,x,y); - break; - default: - log.warn("Unknown context menu for ",type); - break; } } @@ -66,23 +72,17 @@ export class ContextMenuManager { }), new MenuItem({ label: 'Rotate item', - click: () => { - pubTab.rotateItemAt(this.x,this.y); - }, + click: () => pubTab.rotateItemAt(this.x,this.y), enabled: await pubTab.getItemAt(this.x,this.y), }), new MenuItem({ label: 'Toggle editor mode', - click: () => { - pubTab.toggleEditorModeAt(this.x,this.y); - }, + click: () => pubTab.toggleEditorModeAt(this.x,this.y), enabled: await pubTab.getItemAt(this.x,this.y), }), new MenuItem({ label: 'Remove item', - click: () => { - pubTab.removeItemAt(this.x,this.y); - }, + click: () => pubTab.removeItemAt(this.x,this.y), enabled: await pubTab.getItemAt(this.x,this.y), }), ]; @@ -123,9 +123,7 @@ export class ContextMenuManager { const menuItems = [ new MenuItem({ label: 'Rotate', - click: async () => { - minsky.phillipsDiagram.startRotatingItem(this.x,this.y); - }, + click: () => minsky.phillipsDiagram.startRotatingItem(this.x,this.y), }), ]; @@ -189,6 +187,36 @@ export class ContextMenuManager { ); } } + + private static async leftMouseGodley(mainWindow: BrowserWindow) { + const itemInfo = await CommandsManager.getItemInfo(this.x, this.y); + if(itemInfo?.classType === ClassType.GodleyIcon) { + let godley=new GodleyIcon(minsky.canvas.item); + minsky.nameCurrentItem(await godley.id()); + const editorModeChecked = await godley.editorMode(); + + const editorX = await godley.toEditorX(this.x) / await godley.editor.zoomFactor(); + const editorY = await godley.toEditorY(this.y) / await godley.editor.zoomFactor(); + const col=await godley.editor.colX(editorX); + const clickType = await godley.editor.clickType(editorX, editorY); + + if(editorModeChecked && clickType === 'importStock') { + const stockImportMenuItems=[]; + let importOptions=await godley.editor.matchingTableColumnsByCol(col); + for (let v of importOptions) { + stockImportMenuItems.push({ + label: v, + click: (item) => godley.editor.importStockVarByCol(item.label, col) + }); + } + ContextMenuManager.buildAndDisplayContextMenu( + stockImportMenuItems, + mainWindow + ); + } + } + } + private static async rightMouseGodley( itemInfo: CanvasItem ): Promise { @@ -196,13 +224,11 @@ export class ContextMenuManager { const menuItems: MenuItem[] = [ new MenuItem({ label: 'Copy', - click: () => {minsky.canvas.copyItem();} + click: () => minsky.canvas.copyItem() }), new MenuItem({ label: 'Rename all instances', - click: async () => { - await CommandsManager.renameAllInstances(itemInfo); - }, + click: () => CommandsManager.renameAllInstances(itemInfo) }), ]; @@ -219,17 +245,15 @@ export class ContextMenuManager { const menuItems = [ new MenuItem({ label: 'Edit', - click: async () => { - await CommandsManager.editVar(); - }, + click: () => CommandsManager.editVar(), }), new MenuItem({ label: 'Copy', - click: () => {minsky.canvas.copyItem();} + click: () => minsky.canvas.copyItem() }), new MenuItem({ label: 'Remove', - click: () => {minsky.canvas.removeItemFromItsGroup();} + click: () => minsky.canvas.removeItemFromItsGroup() }), ]; @@ -243,15 +267,15 @@ export class ContextMenuManager { const menuItems = [ new MenuItem({ label: 'Description', - click: () => {CommandsManager.postNote('wire');} + click: () => CommandsManager.postNote('wire') }), new MenuItem({ label: 'Straighten', - click: () => {minsky.canvas.wire.straighten();} + click: () => minsky.canvas.wire.straighten() }), new MenuItem({ label: 'Delete wire', - click: () => {minsky.canvas.deleteWire();} + click: () => minsky.canvas.deleteWire() }), ]; @@ -266,54 +290,48 @@ export class ContextMenuManager { const menuItems = [ new MenuItem({ label: 'Cut', - click: () => {minsky.cut();}, + click: () => minsky.cut(), enabled: selectionSize > 0 }), new MenuItem({ label: 'Copy selection', - click: () => {minsky.copy();}, + click: () => minsky.copy(), enabled: selectionSize > 0 }), new MenuItem({ label: 'Save selection as', - click: async () => { - await CommandsManager.saveSelectionAsFile(); - }, + click: () => CommandsManager.saveSelectionAsFile(), enabled: selectionSize > 0 }), new MenuItem({ label: 'Paste selection', - click: () => { - CommandsManager.pasteAt(this.x, this.y); - }, + click: () => CommandsManager.pasteAt(this.x, this.y) }), new MenuItem({ label: 'Bookmark here', - click: async () => { - await CommandsManager.bookmarkAt(this.x, this.y); - }, + click: () => CommandsManager.bookmarkAt(this.x, this.y) }), new MenuItem({ label: 'Group', - click: () => {minsky.canvas.groupSelection();} + click: () => minsky.canvas.groupSelection() }), new MenuItem({ label: 'Link selected Ravels', - click: async () => {minsky.canvas.lockRavelsInSelection();}, + click: () => minsky.canvas.lockRavelsInSelection(), enabled: ravelsSelected > 1 }), new MenuItem({ label: 'Unlink selected Ravels', - click: async () => {minsky.canvas.unlockRavelsInSelection();}, + click: () => minsky.canvas.unlockRavelsInSelection(), enabled: ravelsSelected > 0 }), new MenuItem({ label: 'Open master group', - click: () => { - minsky.openModelInCanvas(); - BookmarkManager.updateBookmarkList(); + click: async () => { + await minsky.openModelInCanvas(); + await BookmarkManager.updateBookmarkList(); } }), ]; @@ -331,9 +349,7 @@ export class ContextMenuManager { label: 'Help', visible: WindowManager.currentTab.$equal(minsky.canvas), - click: async () => { - await CommandsManager.help(this.x, this.y); - }, + click: () => CommandsManager.help(this.x, this.y), }), ...menuItems, ]); @@ -350,15 +366,11 @@ export class ContextMenuManager { return [ { label: 'Save by datapoint per row', - click: async () => { - await CommandsManager.exportItemAsCSV(item); - }, + click: () => CommandsManager.exportItemAsCSV(item) }, { label: 'Save by series per row', - click: async () => { - await CommandsManager.exportItemAsCSV(item,true); - }, + click: () => CommandsManager.exportItemAsCSV(item,true) }, ]; } @@ -367,9 +379,7 @@ export class ContextMenuManager { let menuItems: MenuItem[] = [ new MenuItem({ label: 'Description', - click: () => { - CommandsManager.postNote('item'); - }, + click: () => CommandsManager.postNote('item') }), ]; @@ -381,9 +391,7 @@ export class ContextMenuManager { ...(await ContextMenuManager.buildContextMenuForVariables(itemInfo)), new MenuItem({ label: `Rotate ${itemInfo.classType}`, - click: () => { - minsky.canvas.rotateItem(this.x, this.y); - } + click: () => minsky.canvas.rotateItem(this.x, this.y) }), ]; break; @@ -397,9 +405,7 @@ export class ContextMenuManager { ...(await ContextMenuManager.buildContextMenuForOperations(itemInfo)), new MenuItem({ label: `Rotate ${itemInfo.classType}`, - click: () => { - minsky.canvas.rotateItem(this.x, this.y); - } + click: () => minsky.canvas.rotateItem(this.x, this.y) }), ]; break; @@ -425,9 +431,7 @@ export class ContextMenuManager { ...(await ContextMenuManager.buildContextMenuForGroup()), new MenuItem({ label: `Rotate ${itemInfo.classType}`, - click: () => { - minsky.canvas.rotateItem(this.x, this.y); - } + click: () => minsky.canvas.rotateItem(this.x, this.y) }), ]; break; @@ -437,13 +441,11 @@ export class ContextMenuManager { ...menuItems, new MenuItem({ label: 'Copy item', - click: async () => {minsky.canvas.copyItem();} + click: () => minsky.canvas.copyItem() }), new MenuItem({ label: `Rotate ${itemInfo.classType}`, - click: () => { - minsky.canvas.rotateItem(this.x, this.y); - } + click: () => minsky.canvas.rotateItem(this.x, this.y) }), ]; @@ -482,15 +484,15 @@ export class ContextMenuManager { submenu: [ { label: 'Head', - click: () => {sheet.showRowSlice("head");} + click: () => sheet.showRowSlice("head") }, { label: 'Tail', - click: () => {sheet.showRowSlice("tail");} + click: () => sheet.showRowSlice("tail") }, { label: 'Head & Tail', - click: () => {sheet.showRowSlice("headAndTail");} + click: () => sheet.showRowSlice("headAndTail") }, ] }), @@ -499,15 +501,15 @@ export class ContextMenuManager { submenu: [ { label: 'Head', - click: () => {sheet.showColSlice("head");} + click: () => sheet.showColSlice("head") }, { label: 'Tail', - click: () => {sheet.showColSlice("tail");} + click: () => sheet.showColSlice("tail") }, { label: 'Head & Tail', - click: () => {sheet.showColSlice("headAndTail");} + click: () => sheet.showColSlice("headAndTail") }, ] }), @@ -529,9 +531,7 @@ export class ContextMenuManager { }), new MenuItem({ label: `Delete ${itemInfo.classType}`, - click: () => { - CommandsManager.deleteCurrentItemHavingId(itemInfo.id); - }, + click: () => CommandsManager.deleteCurrentItemHavingId(itemInfo.id) }), ]; @@ -559,31 +559,23 @@ export class ContextMenuManager { const menuItems = [ new MenuItem({ label: 'Expand', - click: async () => { - await CommandsManager.expandPlot(itemInfo); - }, + click: () => CommandsManager.expandPlot(itemInfo) }), new MenuItem({ label: 'Make Group Plot', - click: async () => {plot.makeDisplayPlot();} + click: () => plot.makeDisplayPlot() }), new MenuItem({ label: 'Options', - click: async () => { - await CommandsManager.openPlotWindowOptions(itemInfo); - }, + click: () => CommandsManager.openPlotWindowOptions(itemInfo) }), new MenuItem({ label: 'Set options as default', - click: async () => { - minsky.canvas.setDefaultPlotOptions(); - }, + click: () => minsky.canvas.setDefaultPlotOptions() }), new MenuItem({ label: 'Apply default options', - click: async () => { - minsky.canvas.applyDefaultPlotOptions(); - }, + click: () => minsky.canvas.applyDefaultPlotOptions() }), new MenuItem({ label: 'Pen Styles', @@ -598,43 +590,31 @@ export class ContextMenuManager { }), new MenuItem({ label: 'Export as CSV', - click: async () => { - await CommandsManager.exportItemAsCSV(plot); - }, + click: () => CommandsManager.exportItemAsCSV(plot) }), new MenuItem({ label: 'Export Image As', submenu: [ { label: 'SVG', - click: async () => { - CommandsManager.exportItemAsImageDialog(plot, 'svg', 'SVG'); - }, + click: () => CommandsManager.exportItemAsImageDialog(plot, 'svg', 'SVG') }, { label: 'PDF', - click: async () => { - CommandsManager.exportItemAsImageDialog(plot, 'pdf', 'PDF'); - }, + click: () => CommandsManager.exportItemAsImageDialog(plot, 'pdf', 'PDF') }, { label: 'PostScript', - click: async () => { - CommandsManager.exportItemAsImageDialog(plot, 'ps', 'PostScript'); - }, + click: () => CommandsManager.exportItemAsImageDialog(plot, 'ps', 'PostScript') }, { label: 'EMF', visible: Functions.isWindows(), - click: async () => { - CommandsManager.exportItemAsImageDialog(plot, 'emf', 'EMF'); - }, + click: () => CommandsManager.exportItemAsImageDialog(plot, 'emf', 'EMF') }, { label: 'Portable Network Graphics', - click: async () => { - CommandsManager.exportItemAsImageDialog(plot, 'png', 'Portable Network Graphics'); - }, + click: () => CommandsManager.exportItemAsImageDialog(plot, 'png', 'Portable Network Graphics') }, ], }), @@ -652,86 +632,66 @@ export class ContextMenuManager { const rowColButtonsChecked = await godley.buttonDisplay(); const editorModeChecked = await godley.editorMode(); - // build stock import menu for this column + const editorX = await godley.toEditorX(this.x) / await godley.editor.zoomFactor(); + const editorY = await godley.toEditorY(this.y) / await godley.editor.zoomFactor(); + const row=await godley.editor.rowY(editorY); + const col=await godley.editor.colX(editorX); + const clickType = await godley.editor.clickType(editorX, editorY); - const stockImportMenuItems=[]; - { - let x=this.x-await godley.x()+0.5*await godley.width(); - let col=await godley.editor.colXZoomed(x); - let importOptions=await godley.editor.matchingTableColumnsByCol(col); - for (let v of importOptions) - stockImportMenuItems.push({ - label: v, - click: (item) => { - godley.editor.importStockVarByCol(item.label, col); - }, - }); + if(editorModeChecked && clickType !== 'background') { + this.initContextMenuForGodley(godley, row, col, clickType, () => godley.editor.update()); + return; } - + const menuItems = [ new MenuItem({ label: 'Open Godley Table', - click: async () => { - await CommandsManager.openGodleyTable(itemInfo); - }, + click: () => CommandsManager.openGodleyTable(itemInfo) }), new MenuItem({ label: 'Title', - click: () => { - CommandsManager.editGodleyTitle(godley); - }, + click: () => CommandsManager.editGodleyTitle(godley), }), new MenuItem({ label: 'Set currency', - click: () => { - CommandsManager.setGodleyCurrency(); - }, + click: () => CommandsManager.setGodleyCurrency(), }), new MenuItem({ label: 'Editor mode', checked: editorModeChecked, type: 'checkbox', - click: async () => {godley.toggleEditorMode();} + click: () => godley.toggleEditorMode() }), new MenuItem({ label: 'Row/Col buttons', checked: rowColButtonsChecked, type: 'checkbox', - click: async () => {godley.toggleButtons();} + click: () => godley.toggleButtons() }), new MenuItem({ label: 'Display variables', type: 'checkbox', checked: displayVariableChecked, - click: async () => {godley.toggleVariableDisplay();} + click: () => godley.toggleVariableDisplay() }), new MenuItem({ label: 'Copy flow variables', - click: async () => {minsky.canvas.copyAllFlowVars();} + click: () => minsky.canvas.copyAllFlowVars() }), new MenuItem({ label: 'Copy stock variables', - click: async () => {minsky.canvas.copyAllStockVars();} - }), - new MenuItem({ - label: 'Import stock variables', - enabled: stockImportMenuItems.length>0, - submenu: Menu.buildFromTemplate(stockImportMenuItems), + click: () => minsky.canvas.copyAllStockVars() }), new MenuItem({ label: 'Export as', submenu: [ { label: 'CSV', - click: async () => { - await CommandsManager.exportGodleyAs('csv'); - }, + click: () => CommandsManager.exportGodleyAs('csv') }, { label: 'LaTeX', - click: async () => { - await CommandsManager.exportGodleyAs('tex'); - }, + click: () => CommandsManager.exportGodleyAs('tex') }, ], }), @@ -753,9 +713,7 @@ export class ContextMenuManager { new MenuItem({ label: `Port values ${portValues}` }), new MenuItem({ label: 'Edit', - click: async () => { - await CommandsManager.editItem(itemInfo.classType); - }, + click: () => CommandsManager.editItem(itemInfo.classType) }), ]; @@ -766,7 +724,7 @@ export class ContextMenuManager { label: 'Import Data', click: async () => { const filePath = await CommandsManager.getFilePathUsingSaveDialog(); - if (filePath) {(new DataOp(op)).readData(filePath);} + if (filePath) {await (new DataOp(op)).readData(filePath);} } }) ); @@ -776,13 +734,11 @@ export class ContextMenuManager { ...menuItems, new MenuItem({ label: 'Copy item', - click: async () => {minsky.canvas.copyItem();} + click: () => minsky.canvas.copyItem() }), new MenuItem({ label: 'Flip', - click: async () => { - await CommandsManager.flip(); - }, + click: () => CommandsManager.flip() }), ]; @@ -791,23 +747,21 @@ export class ContextMenuManager { new MenuItem({ label: 'Toggle var binding', click: async () => { - (new IntOp(op)).toggleCoupled(); - CommandsManager.requestRedraw(); + await (new IntOp(op)).toggleCoupled(); + await CommandsManager.requestRedraw(); }, }) ); menuItems.push( new MenuItem({ label: 'Select all instances', - click: () => {minsky.canvas.selectAllVariables();} + click: () => minsky.canvas.selectAllVariables() }) ); menuItems.push( new MenuItem({ label: 'Rename all instances', - click: async () => { - await CommandsManager.renameAllInstances(itemInfo); - }, + click: async () => CommandsManager.renameAllInstances(itemInfo), }) ); } @@ -819,70 +773,64 @@ export class ContextMenuManager { const menuItems = [ new MenuItem({ label: 'Edit', - click: async () => { - await CommandsManager.editItem(ClassType.Group); - }, + click: () => CommandsManager.editItem(ClassType.Group), }), new MenuItem({ label: 'Open in canvas', click: async () => { - minsky.openGroupInCanvas(); - BookmarkManager.updateBookmarkList(); + await minsky.openGroupInCanvas(); + await BookmarkManager.updateBookmarkList(); } }), new MenuItem({ label: 'Zoom to display', click: async () => { - minsky.canvas.zoomToDisplay(); + await minsky.canvas.zoomToDisplay(); await CommandsManager.requestRedraw(); }, }), new MenuItem({ label: 'Remove plot icon', click: async () => { - group.removeDisplayPlot(); + await group.removeDisplayPlot(); await CommandsManager.requestRedraw(); }, }), new MenuItem({ label: 'Copy', - click: () => {minsky.canvas.copyItem();} + click: () => minsky.canvas.copyItem() }), new MenuItem({ label: 'Make subroutine', - click: () => {group.makeSubroutine();} + click: () => group.makeSubroutine() }), new MenuItem({ label: 'Save group as', - click: async () => { - await CommandsManager.saveGroupAsFile(); - }, + click: () => CommandsManager.saveGroupAsFile(), }), new MenuItem({ label: 'Flip', - click: async () => { - await CommandsManager.flip(); - }, + click: () => CommandsManager.flip(), }), new MenuItem({ label: 'Flip Contents', click: async () => { - group.flipContents(); - CommandsManager.requestRedraw(); + await group.flipContents(); + await CommandsManager.requestRedraw(); }, }), new MenuItem({ label: 'Resize icon on contents', - click: () => { - group.resizeOnContents(); - CommandsManager.requestRedraw(); + click: async () => { + await group.resizeOnContents(); + await CommandsManager.requestRedraw(); }, }), new MenuItem({ label: 'Ungroup', click: async () => { - minsky.canvas.ungroupItem(); - CommandsManager.requestRedraw(); + await minsky.canvas.ungroupItem(); + await CommandsManager.requestRedraw(); }, }), ]; @@ -920,7 +868,7 @@ export class ContextMenuManager { label: 'Editor mode', type: 'checkbox', checked: editorMode, - click: () => {ravel.toggleEditorMode();} + click: () => ravel.toggleEditorMode() }), new MenuItem({ label: 'Connect to database', @@ -954,26 +902,24 @@ export class ContextMenuManager { label: 'Set next aggregation', submenu: aggregations.map(agg => ({ label: agg.label, - click: async () => {ravel.nextReduction(agg.value);} + click: () => ravel.nextReduction(agg.value) })) }), new MenuItem({ label: 'Collapse all handles', - click: async () => {ravel.collapseAllHandles(true);} + click: () => ravel.collapseAllHandles(true) }), new MenuItem({ label: 'Expand all handles', - click: async () => {ravel.collapseAllHandles(false);} + click: () => ravel.collapseAllHandles(false) }), new MenuItem({ label: 'Flip', - click: async () => {ravel.flipped(!await ravel.flipped());} + click: async () => {await ravel.flipped(!await ravel.flipped());} }), new MenuItem({ label: 'Link specific handles', - click: async () => { - CommandsManager.lockSpecificHandles(ravel); - }, + click: () => CommandsManager.lockSpecificHandles(ravel), enabled: linkHandlesAvailable }), new MenuItem({ @@ -984,8 +930,8 @@ export class ContextMenuManager { label: 'Unlink', enabled: unlinkAvailable, click: async () => { - ravel.leaveLockGroup(); - CommandsManager.requestRedraw(); + await ravel.leaveLockGroup(); + await CommandsManager.requestRedraw(); }, }) ]; @@ -998,8 +944,8 @@ export class ContextMenuManager { label: 'Toggle axis calipers', enabled: handleAvailable, click: async () => { - ravel.toggleDisplayFilterCaliper(); - ravel.broadcastStateToLockGroup(); + await ravel.toggleDisplayFilterCaliper(); + await ravel.broadcastStateToLockGroup(); } }), new MenuItem({ @@ -1009,10 +955,10 @@ export class ContextMenuManager { label: so, type: 'radio', checked: sortOrder == so, - click: () => { - ravel.setSortOrder(so); - ravel.broadcastStateToLockGroup(); - minsky.requestReset(); + click: async () => { + await ravel.setSortOrder(so); + await ravel.broadcastStateToLockGroup(); + await minsky.requestReset(); } })), }), @@ -1024,10 +970,10 @@ export class ContextMenuManager { label: 'none', type: 'radio', checked: sortOrder == 'none', - click: () => { - ravel.setSortOrder('none'); - ravel.broadcastStateToLockGroup(); - minsky.requestReset(); + click: async () => { + await ravel.setSortOrder('none'); + await ravel.broadcastStateToLockGroup(); + await minsky.requestReset(); }, }].concat( ['forward','reverse'].map(vso =>({ @@ -1035,10 +981,10 @@ export class ContextMenuManager { type: 'radio', checked: sortOrder == valueSort('static',vso), click: async () => { - ravel.sortByValue(vso); - ravel.setSortOrder(valueSort('static',vso)); - ravel.broadcastStateToLockGroup(); - minsky.reset(); + await ravel.sortByValue(vso); + await ravel.setSortOrder(valueSort('static',vso)); + await ravel.broadcastStateToLockGroup(); + await minsky.reset(); } }))).concat( ['forward','reverse'].map(vso =>({ @@ -1046,19 +992,17 @@ export class ContextMenuManager { type: 'radio', checked: sortOrder == valueSort('dynamic',vso), click: async () => { - ravel.sortByValue(vso); - ravel.setSortOrder(valueSort('dynamic',vso)); - ravel.broadcastStateToLockGroup(); - minsky.reset(); + await ravel.sortByValue(vso); + await ravel.setSortOrder(valueSort('dynamic',vso)); + await ravel.broadcastStateToLockGroup(); + await minsky.reset(); } }))), }), new MenuItem({ label: 'Pick axis slices', enabled: handleAvailable, - click: () => { - CommandsManager.pickSlices(ravel,handleIndex); - } + click: () => CommandsManager.pickSlices(ravel,handleIndex) }), new MenuItem({ label: 'Axis properties', @@ -1066,23 +1010,17 @@ export class ContextMenuManager { submenu: [ { label: 'Description', - click: async () => { - await CommandsManager.editHandleDescription(ravel,handleIndex); - } + click: async () => CommandsManager.editHandleDescription(ravel,handleIndex) }, { label: 'Dimension', - click: async () => { - await CommandsManager.editHandleDimension(ravel,handleIndex); - } + click: async () => CommandsManager.editHandleDimension(ravel,handleIndex) }, { label: 'Set aggregation', submenu: aggregations.map(agg => ({ label: agg.label, - click: () => { - ravel.handleSetReduction(handleIndex, agg.value); - } + click: () => ravel.handleSetReduction(handleIndex, agg.value) })) } ] @@ -1096,11 +1034,11 @@ export class ContextMenuManager { const menuItems = [ new MenuItem({ label: 'Add case', - click: async () => {switchIcon.setNumCases(await switchIcon.numCases()+1);}, + click: async () => {await switchIcon.setNumCases(await switchIcon.numCases()+1);}, }), new MenuItem({ label: 'Delete case', - click: async () => {switchIcon.setNumCases(await switchIcon.numCases()-1);}, + click: async () => {await switchIcon.setNumCases(await switchIcon.numCases()-1);}, }), new MenuItem({ label: 'Flip', @@ -1118,11 +1056,11 @@ export class ContextMenuManager { return [ new MenuItem({ label: (await ravel.locked())? 'Unlock': 'Lock', - click: () => {ravel.toggleLocked();} + click: () => ravel.toggleLocked() }), new MenuItem({ label: 'Apply state to Ravel', - click: () => {ravel.applyLockedStateToRavel();} + click: () => ravel.applyLockedStateToRavel() }), ]; @@ -1151,58 +1089,48 @@ export class ContextMenuManager { label: "Local", type: 'checkbox', checked: await v.local(), - click: async () => {v.toggleLocal();} + click: () => v.toggleLocal() }), new MenuItem({ label: 'Find definition', - click: async () => { - await CommandsManager.findDefinition(); - }, + click: () => CommandsManager.findDefinition() }), new MenuItem({ label: 'Select all instances', - click: async () => {minsky.canvas.selectAllVariables();} + click: () => minsky.canvas.selectAllVariables() }), new MenuItem({ label: 'Find all instances', - click: async () => { - await CommandsManager.findAllInstances(); - }, + click: () => CommandsManager.findAllInstances(), }), new MenuItem({ label: 'Rename all instances', - click: async () => { - await CommandsManager.renameAllInstances(itemInfo); - }, + click: () => CommandsManager.renameAllInstances(itemInfo), }), new MenuItem({ label: 'Edit', - click: async () => { - await CommandsManager.editVar(); - }, + click: () => CommandsManager.editVar(), }), new MenuItem({ label: 'Copy item', - click: () => {minsky.canvas.copyItem();} + click: () => minsky.canvas.copyItem() }), new MenuItem({ label: 'Add integral', - click: () => {minsky.addIntegral();} + click: () => minsky.addIntegral() }), new MenuItem({ label: 'Mini Plot', type: 'checkbox', checked: miniPlot, - click: () => {v.miniPlotEnabled(!miniPlot);} + click: () => v.miniPlotEnabled(!miniPlot) }), ]; menuItems.push( new MenuItem({ label: 'Flip', - click: async () => { - await CommandsManager.flip(); - }, + click: () => CommandsManager.flip(), }) ); @@ -1211,16 +1139,16 @@ export class ContextMenuManager { new MenuItem({ label: 'Import CSV', click: async () => { - CommandsManager.importCSV(minsky.variableValues.elem(await v.valueId())); + await CommandsManager.importCSV(minsky.variableValues.elem(await v.valueId())); }, }) ); menuItems.push( new MenuItem({ label: 'Reimport CSV', - click: () => { - v.reloadCSV(); - minsky.requestReset(); + click: async () => { + await v.reloadCSV(); + await minsky.requestReset(); }, }) ); @@ -1243,7 +1171,7 @@ export class ContextMenuManager { const refreshFunction = () => event.sender.send(events.GODLEY_POPUP_REFRESH); - this.initContextMenuForGodley(godley, r, c, parsed[1], refreshFunction); + this.initContextMenuForGodley(godley, r, c, parsed[1], async () => refreshFunction()); } private static async initContextMenuForGodleyPopup(namedItemSubCommand: string, x: number, y: number) @@ -1252,10 +1180,10 @@ export class ContextMenuManager { const r=await godley.popup.rowYZoomed(y); const c=await godley.popup.colXZoomed(x); const clickType = await godley.popup.clickTypeZoomed(x, y); - this.initContextMenuForGodley(godley, r, c, clickType, () => {}); + this.initContextMenuForGodley(godley, r, c, clickType, () => CommandsManager.requestRedraw()); } - private static async initContextMenuForGodley(godley: GodleyIcon, r: number, c: number, clickType: string, refreshFunction: () => void) + private static async initContextMenuForGodley(godley: GodleyIcon, r: number, c: number, clickType: string, refreshFunction: () => Promise) { var menu=new Menu(); @@ -1268,7 +1196,7 @@ export class ContextMenuManager { label: "Title", click: async ()=> { await CommandsManager.editGodleyTitle(godley); - refreshFunction(); + await refreshFunction(); }, })); @@ -1279,8 +1207,8 @@ export class ContextMenuManager { menu.append(new MenuItem({ label: "Add new stock variable", click: async ()=> { - godley.popup.addStockVarByCol(c); - refreshFunction(); + await godley.popup.addStockVarByCol(c); + await refreshFunction(); }, })); @@ -1290,8 +1218,8 @@ export class ContextMenuManager { importMenu.append(new MenuItem({ label: importables[i], click: async (item)=> { - godley.popup.importStockVarByCol(item.label, c); - refreshFunction(); + await godley.popup.importStockVarByCol(item.label, c); + await refreshFunction(); }, })); @@ -1303,8 +1231,8 @@ export class ContextMenuManager { menu.append(new MenuItem({ label: "Delete stock variable", click: async ()=> { - godley.popup.deleteStockVarByCol(c); - refreshFunction(); + await godley.popup.deleteStockVarByCol(c); + await refreshFunction(); }, })); break; @@ -1312,15 +1240,15 @@ export class ContextMenuManager { menu.append(new MenuItem({ label: "Add flow", click: async ()=> { - godley.popup.addFlowByRow(r); - refreshFunction(); + await godley.popup.addFlowByRow(r); + await refreshFunction(); }, })); menu.append(new MenuItem({ label: "Delete flow", click: async ()=> { - godley.popup.deleteFlowByRow(r); - refreshFunction(); + await godley.popup.deleteFlowByRow(r); + await refreshFunction(); }, })); break; @@ -1329,10 +1257,10 @@ export class ContextMenuManager { if (r!=await godley.popup.selectedRow() || c!=await godley.popup.selectedCol()) { - godley.popup.selectedRow(r); - godley.popup.selectedCol(c); - godley.popup.insertIdx(0); - godley.popup.selectIdx(0); + await godley.popup.selectedRow(r); + await godley.popup.selectedCol(c); + await godley.popup.insertIdx(0); + await godley.popup.selectIdx(0); } var cell=await godley.table.getCell(r,c); if (cell.length>0 && (r!=1 || c!=0)) @@ -1340,8 +1268,8 @@ export class ContextMenuManager { menu.append(new MenuItem({ label: "Cut", click: async ()=> { - godley.popup.cut(); - refreshFunction(); + await godley.popup.cut(); + await refreshFunction(); } })); menu.append(new MenuItem({ @@ -1356,9 +1284,9 @@ export class ContextMenuManager { menu.append(new MenuItem({ label: "Paste", enabled: !clip, - click: ()=> { - godley.popup.paste(); - refreshFunction(); + click: async ()=> { + await godley.popup.paste(); + await refreshFunction(); } })); } @@ -1370,16 +1298,16 @@ export class ContextMenuManager { submenu: [ { label: '+', - click: ()=>{ - godley.table.setCell(r,c,flows[i]); - refreshFunction(); + click: async ()=>{ + await godley.table.setCell(r,c,flows[i]); + await refreshFunction(); } }, { label: '-', - click: ()=>{ - godley.table.setCell(r,c,`-${flows[i]}`); - refreshFunction(); + click: async ()=>{ + await godley.table.setCell(r,c,`-${flows[i]}`); + await refreshFunction(); } }, ] @@ -1392,9 +1320,9 @@ export class ContextMenuManager { if ((await godley.table.assetClass())[c]==="equity") menu.append(new MenuItem({ label: 'Balance equity', - click: ()=>{ - godley.table.balanceEquity(c); - refreshFunction(); + click: async ()=>{ + await godley.table.balanceEquity(c); + await refreshFunction(); } })); @@ -1415,36 +1343,36 @@ export class ContextMenuManager { var menu=Menu.buildFromTemplate([ new MenuItem({ label: 'Set as header row', - click: ()=>{ - value.spec.headerRow(row); + click: async ()=>{ + await value.spec.headerRow(row); refresh(); }, }), new MenuItem({ label: 'Auto-classify columns as axis/data', click: async ()=>{ - value.classifyColumns(); + await value.classifyColumns(); refresh(); }, }), new MenuItem({ label: 'Populate column labels', click: async ()=>{ - value.populateHeaders(); + await value.populateHeaders(); refresh(); }, }), new MenuItem({ label: 'Populate current column label', - click: ()=>{ - value.populateHeader(col); + click: async ()=>{ + await value.populateHeader(col); refresh(); }, }), new MenuItem({ label: 'Set start of data row, and column', - click: ()=>{ - value.spec.setDataArea(row,col); + click: async ()=>{ + await value.spec.setDataArea(row,col); refresh(); }, }), @@ -1452,7 +1380,7 @@ export class ContextMenuManager { label: 'Set start of data row', click: async ()=>{ let c=await value.spec.nColAxes(); - value.spec.setDataArea(row,c); + await value.spec.setDataArea(row,c); refresh(); }, }), @@ -1460,7 +1388,7 @@ export class ContextMenuManager { label: 'Set start of data column', click: async ()=>{ let r=await value.spec.nRowAxes(); - value.spec.setDataArea(r,col); + await value.spec.setDataArea(r,col); refresh(); }, }), diff --git a/gui-js/libs/core/src/lib/services/communication/communication.service.ts b/gui-js/libs/core/src/lib/services/communication/communication.service.ts index bdd72a551..bf237362e 100644 --- a/gui-js/libs/core/src/lib/services/communication/communication.service.ts +++ b/gui-js/libs/core/src/lib/services/communication/communication.service.ts @@ -332,6 +332,7 @@ export class CommunicationService { x: this.mouseX, y: this.mouseY, type: "canvas", + leftClick: false }); return; } @@ -393,8 +394,16 @@ export class CommunicationService { case 'mousedown': if (message.ctrlKey || message.metaKey) canvas.controlMouseDown(this.mouseX, this.mouseY); - else + else { + this.electronService.send(events.CONTEXT_MENU, { + x: this.mouseX, + y: this.mouseY, + type: "canvas", + leftClick: true + }); + canvas.mouseDown(this.mouseX, this.mouseY); + } break; case 'mouseup': canvas.mouseUp(this.mouseX, this.mouseY); diff --git a/gui-js/libs/shared/src/lib/backend/minsky.ts b/gui-js/libs/shared/src/lib/backend/minsky.ts index 7539bb2e6..3e3468055 100644 --- a/gui-js/libs/shared/src/lib/backend/minsky.ts +++ b/gui-js/libs/shared/src/lib/backend/minsky.ts @@ -737,6 +737,8 @@ export class GodleyIcon extends Item { async stockVarUnits(a1: string,a2: boolean): Promise {return this.$callMethod('stockVarUnits',a1,a2);} async stockVars(): Promise> {return this.$callMethod('stockVars');} async summarise(): Promise {return this.$callMethod('summarise');} + async toEditorX(a1: number): Promise {return this.$callMethod('toEditorX',a1);} + async toEditorY(a1: number): Promise {return this.$callMethod('toEditorY',a1);} async toggleButtons(): Promise {return this.$callMethod('toggleButtons');} async toggleEditorMode(): Promise {return this.$callMethod('toggleEditorMode');} async toggleVariableDisplay(): Promise {return this.$callMethod('toggleVariableDisplay');} @@ -805,6 +807,7 @@ export class GodleyTableEditor extends CppClass { async adjustWidgets(): Promise {return this.$callMethod('adjustWidgets');} async clickType(a1: number,a2: number): Promise {return this.$callMethod('clickType',a1,a2);} async clickTypeZoomed(a1: number,a2: number): Promise {return this.$callMethod('clickTypeZoomed',a1,a2);} + async colX(a1: number): Promise {return this.$callMethod('colX',a1);} async colXZoomed(a1: number): Promise {return this.$callMethod('colXZoomed',a1);} async columnButtonsOffset(...args: number[]): Promise {return this.$callMethod('columnButtonsOffset',...args);} async copy(): Promise {return this.$callMethod('copy');} @@ -848,6 +851,7 @@ export class GodleyTableEditor extends CppClass { async pulldownHot(...args: number[]): Promise {return this.$callMethod('pulldownHot',...args);} async pushHistory(): Promise {return this.$callMethod('pushHistory');} async rowHeight(...args: number[]): Promise {return this.$callMethod('rowHeight',...args);} + async rowY(a1: number): Promise {return this.$callMethod('rowY',a1);} async rowYZoomed(a1: number): Promise {return this.$callMethod('rowYZoomed',a1);} async scrollColStart(...args: number[]): Promise {return this.$callMethod('scrollColStart',...args);} async scrollRowStart(...args: number[]): Promise {return this.$callMethod('scrollRowStart',...args);} @@ -885,6 +889,7 @@ export class GodleyTableWindow extends CppClass { async adjustWidgets(): Promise {return this.$callMethod('adjustWidgets');} async clickType(a1: number,a2: number): Promise {return this.$callMethod('clickType',a1,a2);} async clickTypeZoomed(a1: number,a2: number): Promise {return this.$callMethod('clickTypeZoomed',a1,a2);} + async colX(a1: number): Promise {return this.$callMethod('colX',a1);} async colXZoomed(a1: number): Promise {return this.$callMethod('colXZoomed',a1);} async columnButtonsOffset(...args: number[]): Promise {return this.$callMethod('columnButtonsOffset',...args);} async controlMouseDown(a1: number,a2: number): Promise {return this.$callMethod('controlMouseDown',a1,a2);} @@ -948,6 +953,7 @@ export class GodleyTableWindow extends CppClass { async requestRedrawCanvas(): Promise {return this.$callMethod('requestRedrawCanvas');} async resolutionScaleFactor(...args: any[]): Promise {return this.$callMethod('resolutionScaleFactor',...args);} async rowHeight(...args: number[]): Promise {return this.$callMethod('rowHeight',...args);} + async rowY(a1: number): Promise {return this.$callMethod('rowY',a1);} async rowYZoomed(a1: number): Promise {return this.$callMethod('rowYZoomed',a1);} async scaleFactor(): Promise {return this.$callMethod('scaleFactor');} async scrollColStart(...args: number[]): Promise {return this.$callMethod('scrollColStart',...args);} diff --git a/gui-js/libs/ui-components/src/lib/input-modal/input-modal.component.scss b/gui-js/libs/ui-components/src/lib/input-modal/input-modal.component.scss index ed665e8ec..86a0cac08 100644 --- a/gui-js/libs/ui-components/src/lib/input-modal/input-modal.component.scss +++ b/gui-js/libs/ui-components/src/lib/input-modal/input-modal.component.scss @@ -4,12 +4,13 @@ display: flex; justify-content: center; align-items: center; - margin-top: 1rem; + margin-top: 5px; } button { margin-right: $margin; } input { - width: 100%; + width: calc(100% - 10px); + margin: 5px; } diff --git a/model/godleyIcon.h b/model/godleyIcon.h index a16c588e4..933efd03c 100644 --- a/model/godleyIcon.h +++ b/model/godleyIcon.h @@ -167,6 +167,10 @@ namespace minsky const GodleyIcon* godleyIconCast() const override {return this;} GodleyIcon* godleyIconCast() override {return this;} + /// @{ convert mouse coordinates into editor coords + float toEditorX(float) const; + float toEditorY(float) const; + private: void updateVars(Variables& vars, const vector& varNames, @@ -174,10 +178,6 @@ namespace minsky /// move contained variables to correct locations within icon void positionVariables() const; Variables m_flowVars, m_stockVars; - - /// @{ convert mouse coordinates into editor coords - float toEditorX(float) const; - float toEditorY(float) const; }; } diff --git a/model/godleyTableWindow.cc b/model/godleyTableWindow.cc index 1cf4fcaf3..4201fba39 100644 --- a/model/godleyTableWindow.cc +++ b/model/godleyTableWindow.cc @@ -202,6 +202,13 @@ namespace minsky cairo_move_to(cairo, x, columnButtonsOffset); colWidgets[col].draw(cairo); } + + if (col>1) + { + cairo_move_to(cairo,x-pulldownHot,topTableOffset); + pango.setMarkup("▼"); + pango.show(); + } double y=topTableOffset; double colWidth=minColumnWidth; @@ -506,6 +513,8 @@ namespace minsky selectIdx=insertIdx=0; selectedCol=selectedRow=-1; break; + case importStock: + break; default: if (selectedRow>=0 && selectedCol>=0) { // if cell already selected, deselect to allow the chance to redraw diff --git a/model/godleyTableWindow.h b/model/godleyTableWindow.h index 0e594d4cc..f3fd541ef 100644 --- a/model/godleyTableWindow.h +++ b/model/godleyTableWindow.h @@ -88,7 +88,7 @@ namespace minsky /// offset of the table within the window double leftTableOffset=4*ButtonWidget::buttonSpacing; double topTableOffset=30; - static constexpr double pulldownHot=12; ///< space for ▼ in stackVar cells + static constexpr double pulldownHot=16; ///< space for ▼ in stackVar cells /// minimum column width (for eg empty columns) static constexpr double minColumnWidth=4*ButtonWidget::buttonSpacing; @@ -151,6 +151,11 @@ namespace minsky int colXZoomed(double x) const {return colX(x/zoomFactor);} int rowYZoomed(double y) const {return rowY(y/zoomFactor);} + + /// column at \a x in unzoomed coordinates + int colX(double x) const; + /// row at \a y in unzoomed coordinates + int rowY(double y) const; // warn user when a stock variable column is going to be moved to a different asset class on pressing a column button widget. For ticket 1072. std::string moveAssetClass(double x, double y); @@ -198,10 +203,6 @@ namespace minsky protected: std::vector> rowWidgets; std::vector> colWidgets; - /// column at \a x in unzoomed coordinates - int colX(double x) const; - /// row at \a y in unzoomed coordinates - int rowY(double y) const; int motionRow=-1, motionCol=-1; ///< current cell under mouse motion // Perform deep comparison of Godley tables in history to avoid spurious noAssetClass columns from arising during undo. For ticket 1118. std::deque history; From 87a5614b7e470dfec090bcedfe629d41a3f4e18c Mon Sep 17 00:00:00 2001 From: Niels Rademaker Date: Fri, 26 Dec 2025 22:49:46 +0100 Subject: [PATCH 2/3] replicated error handing from similar method --- .../src/app/managers/ContextMenuManager.ts | 53 +++++++++++-------- 1 file changed, 30 insertions(+), 23 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 2937ac59c..1641c862b 100644 --- a/gui-js/apps/minsky-electron/src/app/managers/ContextMenuManager.ts +++ b/gui-js/apps/minsky-electron/src/app/managers/ContextMenuManager.ts @@ -189,31 +189,38 @@ export class ContextMenuManager { } private static async leftMouseGodley(mainWindow: BrowserWindow) { - const itemInfo = await CommandsManager.getItemInfo(this.x, this.y); - if(itemInfo?.classType === ClassType.GodleyIcon) { - let godley=new GodleyIcon(minsky.canvas.item); - minsky.nameCurrentItem(await godley.id()); - const editorModeChecked = await godley.editorMode(); - - const editorX = await godley.toEditorX(this.x) / await godley.editor.zoomFactor(); - const editorY = await godley.toEditorY(this.y) / await godley.editor.zoomFactor(); - const col=await godley.editor.colX(editorX); - const clickType = await godley.editor.clickType(editorX, editorY); - - if(editorModeChecked && clickType === 'importStock') { - const stockImportMenuItems=[]; - let importOptions=await godley.editor.matchingTableColumnsByCol(col); - for (let v of importOptions) { - stockImportMenuItems.push({ - label: v, - click: (item) => godley.editor.importStockVarByCol(item.label, col) - }); + try { + const itemInfo = await CommandsManager.getItemInfo(this.x, this.y); + if(itemInfo?.classType === ClassType.GodleyIcon) { + let godley=new GodleyIcon(minsky.canvas.item); + minsky.nameCurrentItem(await godley.id()); + const editorModeChecked = await godley.editorMode(); + + const editorX = await godley.toEditorX(this.x) / await godley.editor.zoomFactor(); + const editorY = await godley.toEditorY(this.y) / await godley.editor.zoomFactor(); + const col=await godley.editor.colX(editorX); + const clickType = await godley.editor.clickType(editorX, editorY); + + if(editorModeChecked && clickType === 'importStock') { + const stockImportMenuItems=[]; + let importOptions=await godley.editor.matchingTableColumnsByCol(col); + for (let v of importOptions) { + stockImportMenuItems.push({ + label: v, + click: (item) => godley.editor.importStockVarByCol(item.label, col) + }); + } + ContextMenuManager.buildAndDisplayContextMenu( + stockImportMenuItems, + mainWindow + ); } - ContextMenuManager.buildAndDisplayContextMenu( - stockImportMenuItems, - mainWindow - ); } + } catch (error) { + console.error( + '🚀 ~ file: contextMenuManager.ts ~ line 221 ~ ContextMenuManager ~ mainWindow.webContents.on ~ error', + error + ); } } From c28ac62fb0f432fd63b5dbbd54cd449a6549466f Mon Sep 17 00:00:00 2001 From: Niels Rademaker Date: Sat, 27 Dec 2025 14:25:57 +0100 Subject: [PATCH 3/3] fixes based on review --- .../src/app/events/electron.events.ts | 8 +- .../src/app/managers/ContextMenuManager.ts | 247 +++++++++--------- .../communication/communication.service.ts | 8 +- .../shared/src/lib/constants/constants.ts | 1 + 4 files changed, 129 insertions(+), 135 deletions(-) diff --git a/gui-js/apps/minsky-electron/src/app/events/electron.events.ts b/gui-js/apps/minsky-electron/src/app/events/electron.events.ts index 75253c21a..74a64d139 100644 --- a/gui-js/apps/minsky-electron/src/app/events/electron.events.ts +++ b/gui-js/apps/minsky-electron/src/app/events/electron.events.ts @@ -230,8 +230,12 @@ ipcMain.handle( } ); -ipcMain.on(events.CONTEXT_MENU, async (event, { x, y, type, command, leftClick}) => { - await ContextMenuManager.initContextMenu(event, x, y, type, command, leftClick); +ipcMain.on(events.CONTEXT_MENU, async (event, { x, y, type, command}) => { + await ContextMenuManager.initContextMenu(event, x, y, type, command); +}); + +ipcMain.on(events.CLICK_MENU, async (event, { x, y, type, command}) => { + await ContextMenuManager.initClickMenu(event, x, y, type, command); }); ipcMain.on( 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 1641c862b..7a9d7ebb6 100644 --- a/gui-js/apps/minsky-electron/src/app/managers/ContextMenuManager.ts +++ b/gui-js/apps/minsky-electron/src/app/managers/ContextMenuManager.ts @@ -16,48 +16,52 @@ export class ContextMenuManager { private static showAllPlotsOnTabChecked = false; - public static async initContextMenu(event: IpcMainEvent, x: number, y: number, type: string, command: string, leftClick: boolean) { + public static async initContextMenu(event: IpcMainEvent, x: number, y: number, type: string, command: string) { const mainWindow = WindowManager.getMainWindow(); this.x = x; this.y = y; - if(leftClick) { - if(type === 'canvas' && WindowManager.currentTab.$equal(minsky.canvas)) { - this.leftMouseGodley(mainWindow); - } - } else { - switch (type) + switch (type) + { + case "canvas": { - case "canvas": - { - const currentTab = WindowManager.currentTab; - if (currentTab.$equal(minsky.canvas)) - this.initContextMenuForWiring(mainWindow); - else if (currentTab.$equal(minsky.phillipsDiagram)) - this.initContextMenuForPhillipsDiagram(mainWindow); - else - this.initContextMenuForPublication(event, mainWindow,currentTab); - } - break; - case 'publication-button': - this.initContextMenuForPublicationTabButton(event,command); - break; - case "godley": - this.initContextMenuForGodleyPopup(command,x,y); - break; - case "html-godley": - this.initContextMenuForGodleyHTMLPopup(event, command,x,y); - break; - case "ravel": - this.initContextMenuForRavelPopup(command,x,y); - break; - case "csv-import": - this.initContextMenuForCSVImport(event, command,x,y); - break; - default: - log.warn("Unknown context menu for ",type); - break; + const currentTab = WindowManager.currentTab; + if (currentTab.$equal(minsky.canvas)) + this.initContextMenuForWiring(mainWindow); + else if (currentTab.$equal(minsky.phillipsDiagram)) + this.initContextMenuForPhillipsDiagram(mainWindow); + else + this.initContextMenuForPublication(event, mainWindow,currentTab); } + break; + case 'publication-button': + this.initContextMenuForPublicationTabButton(event,command); + break; + case "godley": + this.initContextMenuForGodleyPopup(command,x,y); + break; + case "html-godley": + this.initContextMenuForGodleyHTMLPopup(event, command,x,y); + break; + case "ravel": + this.initContextMenuForRavelPopup(command,x,y); + break; + case "csv-import": + this.initContextMenuForCSVImport(event, command,x,y); + break; + default: + log.warn("Unknown context menu for ",type); + break; + } + } + + public static async initClickMenu(event: IpcMainEvent, x: number, y: number, type: string, command: string) { + const mainWindow = WindowManager.getMainWindow(); + this.x = x; + this.y = y; + + if(type === 'canvas' && WindowManager.currentTab.$equal(minsky.canvas)) { + this.leftMouseGodley(mainWindow); } } @@ -131,96 +135,84 @@ export class ContextMenuManager { } private static async initContextMenuForWiring(mainWindow: BrowserWindow) { - try { - const isWirePresent = await minsky.canvas.getWireAt(this.x, this.y); + const isWirePresent = await minsky.canvas.getWireAt(this.x, this.y); - const isWireVisible = await minsky.canvas.wire.visible(); - const itemInfo = await CommandsManager.getItemInfo(this.x, this.y); + const isWireVisible = await minsky.canvas.wire.visible(); + const itemInfo = await CommandsManager.getItemInfo(this.x, this.y); - if (isWirePresent && isWireVisible && (itemInfo?.classType != ClassType.Group || itemInfo?.displayContents)) { - ContextMenuManager.buildAndDisplayContextMenu( - ContextMenuManager.wireContextMenu(), - mainWindow - ); - return; - } + if (isWirePresent && isWireVisible && (itemInfo?.classType != ClassType.Group || itemInfo?.displayContents)) { + ContextMenuManager.buildAndDisplayContextMenu( + ContextMenuManager.wireContextMenu(), + mainWindow + ); + return; + } - if (itemInfo?.classType) { - switch (itemInfo?.classType) { - case ClassType.GodleyIcon: - ContextMenuManager.buildAndDisplayContextMenu( - await ContextMenuManager.rightMouseGodley(itemInfo), - mainWindow - ); - break; - - case ClassType.Group: - ContextMenuManager.buildAndDisplayContextMenu( - await ContextMenuManager.rightMouseGroup(itemInfo), - mainWindow - ); - break; - - default: - ContextMenuManager.buildAndDisplayContextMenu( - await ContextMenuManager.contextMenu(itemInfo), - mainWindow - ); - - break; - } + if (itemInfo?.classType) { + switch (itemInfo?.classType) { + case ClassType.GodleyIcon: + ContextMenuManager.buildAndDisplayContextMenu( + await ContextMenuManager.rightMouseGodley(itemInfo), + mainWindow + ); + break; + + case ClassType.Group: + ContextMenuManager.buildAndDisplayContextMenu( + await ContextMenuManager.rightMouseGroup(itemInfo), + mainWindow + ); + break; + + default: + ContextMenuManager.buildAndDisplayContextMenu( + await ContextMenuManager.contextMenu(itemInfo), + mainWindow + ); - return; + break; } - - ContextMenuManager.buildAndDisplayContextMenu( - await ContextMenuManager.canvasContext(), - mainWindow - ); return; - } catch (error) { - console.error( - '🚀 ~ file: contextMenuManager.ts ~ line 117 ~ ContextMenuManager ~ mainWindow.webContents.on ~ error', - error - ); } + + ContextMenuManager.buildAndDisplayContextMenu( + await ContextMenuManager.canvasContext(), + mainWindow + ); + + return; } private static async leftMouseGodley(mainWindow: BrowserWindow) { - try { - const itemInfo = await CommandsManager.getItemInfo(this.x, this.y); - if(itemInfo?.classType === ClassType.GodleyIcon) { - let godley=new GodleyIcon(minsky.canvas.item); - minsky.nameCurrentItem(await godley.id()); - const editorModeChecked = await godley.editorMode(); - - const editorX = await godley.toEditorX(this.x) / await godley.editor.zoomFactor(); - const editorY = await godley.toEditorY(this.y) / await godley.editor.zoomFactor(); - const col=await godley.editor.colX(editorX); - const clickType = await godley.editor.clickType(editorX, editorY); - - if(editorModeChecked && clickType === 'importStock') { - const stockImportMenuItems=[]; - let importOptions=await godley.editor.matchingTableColumnsByCol(col); - for (let v of importOptions) { - stockImportMenuItems.push({ - label: v, - click: (item) => godley.editor.importStockVarByCol(item.label, col) - }); - } - ContextMenuManager.buildAndDisplayContextMenu( - stockImportMenuItems, - mainWindow - ); + const itemInfo = await CommandsManager.getItemInfo(this.x, this.y); + if(itemInfo?.classType === ClassType.GodleyIcon) { + let godley=new GodleyIcon(minsky.canvas.item); + minsky.nameCurrentItem(await godley.id()); + const editorModeChecked = await godley.editorMode(); + + const zoomFactor = await godley.editor.zoomFactor(); + const editorX = await godley.toEditorX(this.x) / zoomFactor; + const editorY = await godley.toEditorY(this.y) / zoomFactor; + const col = await godley.editor.colX(editorX); + const clickType = await godley.editor.clickType(editorX, editorY); + + if(editorModeChecked && clickType === 'importStock') { + const stockImportMenuItems=[]; + let importOptions=await godley.editor.matchingTableColumnsByCol(col); + for (let v of importOptions) { + stockImportMenuItems.push({ + label: v, + click: (item) => godley.editor.importStockVarByCol(item.label, col) + }); } + ContextMenuManager.buildAndDisplayContextMenu( + stockImportMenuItems, + mainWindow, + false + ); } - } catch (error) { - console.error( - '🚀 ~ file: contextMenuManager.ts ~ line 221 ~ ContextMenuManager ~ mainWindow.webContents.on ~ error', - error - ); } } @@ -348,18 +340,18 @@ export class ContextMenuManager { private static buildAndDisplayContextMenu( menuItems: MenuItem[], - mainWindow: Electron.BrowserWindow + mainWindow: Electron.BrowserWindow, + withHelp = true ) { if (menuItems.length) { - const menu = Menu.buildFromTemplate([ + const menu = Menu.buildFromTemplate((withHelp ? [ new MenuItem({ label: 'Help', visible: WindowManager.currentTab.$equal(minsky.canvas), click: () => CommandsManager.help(this.x, this.y), - }), - ...menuItems, - ]); + })] : []).concat(menuItems), + ); menu.popup({ window: mainWindow, @@ -639,8 +631,9 @@ export class ContextMenuManager { const rowColButtonsChecked = await godley.buttonDisplay(); const editorModeChecked = await godley.editorMode(); - const editorX = await godley.toEditorX(this.x) / await godley.editor.zoomFactor(); - const editorY = await godley.toEditorY(this.y) / await godley.editor.zoomFactor(); + const zoomFactor = await godley.editor.zoomFactor(); + const editorX = await godley.toEditorX(this.x) / zoomFactor; + const editorY = await godley.toEditorY(this.y) / zoomFactor; const row=await godley.editor.rowY(editorY); const col=await godley.editor.colX(editorX); const clickType = await godley.editor.clickType(editorX, editorY); @@ -768,7 +761,7 @@ export class ContextMenuManager { menuItems.push( new MenuItem({ label: 'Rename all instances', - click: async () => CommandsManager.renameAllInstances(itemInfo), + click: () => CommandsManager.renameAllInstances(itemInfo), }) ); } @@ -1176,9 +1169,9 @@ export class ContextMenuManager { const parsed = JSON5.parse(command); const godley=new GodleyIcon(parsed[0]); - const refreshFunction = () => event.sender.send(events.GODLEY_POPUP_REFRESH); + const refreshFunction = async () => event.sender.send(events.GODLEY_POPUP_REFRESH); - this.initContextMenuForGodley(godley, r, c, parsed[1], async () => refreshFunction()); + this.initContextMenuForGodley(godley, r, c, parsed[1], refreshFunction); } private static async initContextMenuForGodleyPopup(namedItemSubCommand: string, x: number, y: number) @@ -1187,7 +1180,7 @@ export class ContextMenuManager { const r=await godley.popup.rowYZoomed(y); const c=await godley.popup.colXZoomed(x); const clickType = await godley.popup.clickTypeZoomed(x, y); - this.initContextMenuForGodley(godley, r, c, clickType, () => CommandsManager.requestRedraw()); + this.initContextMenuForGodley(godley, r, c, clickType, CommandsManager.requestRedraw); } private static async initContextMenuForGodley(godley: GodleyIcon, r: number, c: number, clickType: string, refreshFunction: () => Promise) @@ -1262,12 +1255,10 @@ export class ContextMenuManager { case "internal": break; } // switch clickType - if (r!=await godley.popup.selectedRow() || c!=await godley.popup.selectedCol()) + const selecteds = await Promise.all([godley.popup.selectedRow(),godley.popup.selectedCol()]); + if (r!=selecteds[0] || c!=selecteds[1]) { - await godley.popup.selectedRow(r); - await godley.popup.selectedCol(c); - await godley.popup.insertIdx(0); - await godley.popup.selectIdx(0); + await Promise.all([godley.popup.selectedRow(r), godley.popup.selectedCol(c), godley.popup.insertIdx(0), godley.popup.selectIdx(0)]); } var cell=await godley.table.getCell(r,c); if (cell.length>0 && (r!=1 || c!=0)) diff --git a/gui-js/libs/core/src/lib/services/communication/communication.service.ts b/gui-js/libs/core/src/lib/services/communication/communication.service.ts index bf237362e..e7088cad5 100644 --- a/gui-js/libs/core/src/lib/services/communication/communication.service.ts +++ b/gui-js/libs/core/src/lib/services/communication/communication.service.ts @@ -331,8 +331,7 @@ export class CommunicationService { this.electronService.send(events.CONTEXT_MENU, { x: this.mouseX, y: this.mouseY, - type: "canvas", - leftClick: false + type: "canvas" }); return; } @@ -395,11 +394,10 @@ export class CommunicationService { if (message.ctrlKey || message.metaKey) canvas.controlMouseDown(this.mouseX, this.mouseY); else { - this.electronService.send(events.CONTEXT_MENU, { + this.electronService.send(events.CLICK_MENU, { x: this.mouseX, y: this.mouseY, - type: "canvas", - leftClick: true + type: "canvas" }); canvas.mouseDown(this.mouseX, this.mouseY); diff --git a/gui-js/libs/shared/src/lib/constants/constants.ts b/gui-js/libs/shared/src/lib/constants/constants.ts index f3248810e..795aa5d15 100644 --- a/gui-js/libs/shared/src/lib/constants/constants.ts +++ b/gui-js/libs/shared/src/lib/constants/constants.ts @@ -23,6 +23,7 @@ export const events = { CHANGE_MAIN_TAB: 'change-main-tab', CLOSE_WINDOW: 'close-window', CONTEXT_MENU: 'context-menu', + CLICK_MENU: 'click-menu', CREATE_MENU_POPUP: 'create-menu-popup', CURRENT_TAB_MOVE_TO: 'current-tab-move-to', CURRENT_TAB_POSITION: 'current-tab-position',