Skip to content

Commit bcb7f7d

Browse files
author
Akos Kitta
committed
fix: workaround for menus not working at startup
Signed-off-by: Akos Kitta <[email protected]>
1 parent ed60048 commit bcb7f7d

File tree

5 files changed

+211
-12
lines changed

5 files changed

+211
-12
lines changed

Diff for: arduino-ide-extension/src/electron-browser/preload.ts

+57-2
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,71 @@ import {
33
ipcRenderer,
44
} from '@theia/core/electron-shared/electron';
55
import { Disposable } from '@theia/core/lib/common/disposable';
6-
import { CHANNEL_REQUEST_RELOAD } from '@theia/core/lib/electron-common/electron-api';
6+
import {
7+
CHANNEL_REQUEST_RELOAD,
8+
MenuDto,
9+
} from '@theia/core/lib/electron-common/electron-api';
10+
import { v4 } from 'uuid';
711
import type { Sketch } from '../common/protocol/sketches-service';
812
import {
913
CHANNEL_APP_VERSION,
1014
CHANNEL_IS_FIRST_WINDOW,
15+
CHANNEL_MAIN_MENU_ITEM_DID_CLICK,
1116
CHANNEL_OPEN_PATH,
1217
CHANNEL_PLOTTER_WINDOW_DID_CLOSE,
1318
CHANNEL_QUIT_APP,
1419
CHANNEL_SCHEDULE_DELETION,
1520
CHANNEL_SEND_STARTUP_TASKS,
21+
CHANNEL_SET_MENU_WITH_NODE_ID,
1622
CHANNEL_SET_REPRESENTED_FILENAME,
1723
CHANNEL_SHOW_MESSAGE_BOX,
1824
CHANNEL_SHOW_OPEN_DIALOG,
1925
CHANNEL_SHOW_PLOTTER_WINDOW,
2026
CHANNEL_SHOW_SAVE_DIALOG,
2127
ElectronArduino,
28+
InternalMenuDto,
2229
MessageBoxOptions,
2330
OpenDialogOptions,
2431
SaveDialogOptions,
2532
} from '../electron-common/electron-arduino';
26-
import { StartupTasks, hasStartupTasks } from '../electron-common/startup-task';
33+
import { hasStartupTasks, StartupTasks } from '../electron-common/startup-task';
34+
35+
let mainMenuHandlers: Map<string, () => void> = new Map();
36+
37+
function convertMenu(
38+
menu: MenuDto[] | undefined,
39+
handlerMap: Map<string, () => void>
40+
): InternalMenuDto[] | undefined {
41+
if (!menu) {
42+
return undefined;
43+
}
44+
45+
return menu.map((item) => {
46+
let nodeId = v4();
47+
if (item.execute) {
48+
if (!item.id) {
49+
throw new Error(
50+
"A menu item having the 'execute' property must have an 'id' too."
51+
);
52+
}
53+
nodeId = item.id;
54+
handlerMap.set(nodeId, item.execute);
55+
}
56+
57+
return {
58+
id: item.id,
59+
submenu: convertMenu(item.submenu, handlerMap),
60+
accelerator: item.accelerator,
61+
label: item.label,
62+
nodeId,
63+
checked: item.checked,
64+
enabled: item.enabled,
65+
role: item.role,
66+
type: item.type,
67+
visible: item.visible,
68+
};
69+
});
70+
}
2771

2872
const api: ElectronArduino = {
2973
showMessageBox: (options: MessageBoxOptions) =>
@@ -68,9 +112,20 @@ const api: ElectronArduino = {
68112
);
69113
},
70114
openPath: (fsPath: string) => ipcRenderer.send(CHANNEL_OPEN_PATH, fsPath),
115+
setMenu: (menu: MenuDto[] | undefined): void => {
116+
mainMenuHandlers = new Map();
117+
const internalMenu = convertMenu(menu, mainMenuHandlers);
118+
ipcRenderer.send(CHANNEL_SET_MENU_WITH_NODE_ID, internalMenu);
119+
},
71120
};
72121

73122
export function preload(): void {
74123
contextBridge.exposeInMainWorld('electronArduino', api);
124+
ipcRenderer.on(CHANNEL_MAIN_MENU_ITEM_DID_CLICK, (_, nodeId: string) => {
125+
const handler = mainMenuHandlers.get(nodeId);
126+
if (handler) {
127+
handler();
128+
}
129+
});
75130
console.log('Exposed Arduino IDE electron API');
76131
}

Diff for: arduino-ide-extension/src/electron-browser/theia/core/electron-main-menu-factory.ts

+60-2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
ArduinoMenus,
2424
PlaceholderMenuNode,
2525
} from '../../../browser/menu/arduino-menus';
26+
import debounce from 'lodash.debounce';
2627

2728
@injectable()
2829
export class ElectronMainMenuFactory extends TheiaElectronMainMenuFactory {
@@ -33,7 +34,27 @@ export class ElectronMainMenuFactory extends TheiaElectronMainMenuFactory {
3334
private updateWhenReady = false;
3435

3536
override postConstruct(): void {
36-
super.postConstruct();
37+
// #region Theia `postConstruct` customizations with calling IDE2 `setMenu`
38+
this.preferencesService.onPreferenceChanged(
39+
debounce((e) => {
40+
if (e.preferenceName === 'window.menuBarVisibility') {
41+
this.setMenuBar();
42+
}
43+
if (this._menu) {
44+
for (const cmd of this._toggledCommands) {
45+
const menuItem = this.findMenuById(this._menu, cmd);
46+
if (menuItem) {
47+
menuItem.checked = this.commandRegistry.isToggled(cmd);
48+
}
49+
}
50+
window.electronArduino.setMenu(this._menu); // calls the IDE2-specific implementation
51+
}
52+
}, 10)
53+
);
54+
this.keybindingRegistry.onKeybindingsChanged(() => {
55+
this.setMenuBar();
56+
});
57+
// #endregion Theia `postConstruct`
3758
this.appStateService.reachedState('ready').then(() => {
3859
this.appReady = true;
3960
if (this.updateWhenReady) {
@@ -65,7 +86,8 @@ export class ElectronMainMenuFactory extends TheiaElectronMainMenuFactory {
6586
return;
6687
}
6788
await this.preferencesService.ready;
68-
return super.setMenuBar();
89+
const createdMenuBar = this.createElectronMenuBar();
90+
window.electronArduino.setMenu(createdMenuBar);
6991
}
7092

7193
override createElectronContextMenu(
@@ -85,6 +107,42 @@ export class ElectronMainMenuFactory extends TheiaElectronMainMenuFactory {
85107
});
86108
}
87109

110+
protected override async execute(
111+
commandId: string,
112+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
113+
args: any[],
114+
menuPath: MenuPath
115+
): Promise<void> {
116+
try {
117+
// This is workaround for https://github.com/eclipse-theia/theia/issues/446.
118+
// Electron menus do not update based on the `isEnabled`, `isVisible` property of the command.
119+
// We need to check if we can execute it.
120+
if (this.menuCommandExecutor.isEnabled(menuPath, commandId, ...args)) {
121+
await this.menuCommandExecutor.executeCommand(
122+
menuPath,
123+
commandId,
124+
...args
125+
);
126+
if (
127+
this._menu &&
128+
this.menuCommandExecutor.isVisible(menuPath, commandId, ...args)
129+
) {
130+
const item = this.findMenuById(this._menu, commandId);
131+
if (item) {
132+
item.checked = this.menuCommandExecutor.isToggled(
133+
menuPath,
134+
commandId,
135+
...args
136+
);
137+
window.electronArduino.setMenu(this._menu); // overridden to call the IDE2-specific implementation.
138+
}
139+
}
140+
}
141+
} catch {
142+
// no-op
143+
}
144+
}
145+
88146
// TODO: remove after https://github.com/eclipse-theia/theia/pull/9231
89147
private escapeAmpersand(template: MenuDto[]): MenuDto[] {
90148
for (const option of template) {

Diff for: arduino-ide-extension/src/electron-browser/theia/core/electron-menu-contribution.ts

+24-7
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1-
import { inject, injectable } from '@theia/core/shared/inversify';
2-
import { CommandRegistry } from '@theia/core/lib/common/command';
3-
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
4-
import { KeybindingRegistry } from '@theia/core/lib/browser/keybinding';
1+
import type { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
2+
import type { KeybindingRegistry } from '@theia/core/lib/browser/keybinding';
3+
import type { CommandRegistry } from '@theia/core/lib/common/command';
4+
import type { MenuModelRegistry } from '@theia/core/lib/common/menu';
5+
import { isOSX } from '@theia/core/lib/common/os';
56
import {
6-
ElectronMenuContribution as TheiaElectronMenuContribution,
77
ElectronCommands,
8+
ElectronMenuContribution as TheiaElectronMenuContribution,
89
} from '@theia/core/lib/electron-browser/menu/electron-menu-contribution';
9-
import { MainMenuManager } from '../../../common/main-menu-manager';
10+
import type { MenuDto } from '@theia/core/lib/electron-common/electron-api';
11+
import { inject, injectable } from '@theia/core/shared/inversify';
12+
import type { MainMenuManager } from '../../../common/main-menu-manager';
1013
import { ElectronMainMenuFactory } from './electron-main-menu-factory';
1114

1215
@injectable()
@@ -19,7 +22,7 @@ export class ElectronMenuUpdater implements MainMenuManager {
1922
}
2023

2124
private setMenu(): void {
22-
window.electronTheiaCore.setMenu(this.factory.createElectronMenuBar());
25+
window.electronArduino.setMenu(this.factory.createElectronMenuBar());
2326
}
2427
}
2528

@@ -46,4 +49,18 @@ export class ElectronMenuContribution extends TheiaElectronMenuContribution {
4649
registry.unregisterKeybinding(ElectronCommands.ZOOM_IN.id);
4750
registry.unregisterKeybinding(ElectronCommands.ZOOM_OUT.id);
4851
}
52+
53+
protected override setMenu(
54+
app: FrontendApplication,
55+
electronMenu: MenuDto[] | undefined = this.factory.createElectronMenuBar()
56+
): void {
57+
if (!isOSX) {
58+
this.hideTopPanel(); // no app args. the overridden method is noop in IDE2.
59+
if (this.titleBarStyle === 'custom' && !this.menuBar) {
60+
this.createCustomTitleBar(app);
61+
return;
62+
}
63+
}
64+
window.electronArduino.setMenu(electronMenu); // overridden to call the IDE20-specific implementation.
65+
}
4966
}

Diff for: arduino-ide-extension/src/electron-common/electron-arduino.ts

+16
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,20 @@ import type {
77
SaveDialogReturnValue as ElectronSaveDialogReturnValue,
88
} from '@theia/core/electron-shared/electron';
99
import type { Disposable } from '@theia/core/lib/common/disposable';
10+
import type {
11+
InternalMenuDto as TheiaInternalMenuDto,
12+
MenuDto,
13+
} from '@theia/core/lib/electron-common/electron-api';
1014
import type { Sketch } from '../common/protocol/sketches-service';
1115
import type { StartupTasks } from './startup-task';
1216

17+
export interface InternalMenuDto
18+
extends Omit<TheiaInternalMenuDto, 'handlerId'> {
19+
// Theia handles the menus with a running-index handler ID. https://github.com/eclipse-theia/theia/issues/12493
20+
// IDE2 keeps the menu `nodeId` instead of the running-index.
21+
nodeId?: string;
22+
}
23+
1324
export type MessageBoxOptions = Omit<
1425
ElectronMessageBoxOptions,
1526
'icon' | 'signal'
@@ -51,6 +62,9 @@ export interface ElectronArduino {
5162
showPlotterWindow(params: { url: string; forceReload?: boolean }): void;
5263
registerPlotterWindowCloseHandler(handler: () => void): Disposable;
5364
openPath(fsPath: string): void;
65+
// Unlike the Theia implementation, IDE2 uses the command IDs, and not the running-index handler IDs.
66+
// https://github.com/eclipse-theia/theia/issues/12493
67+
setMenu(menu: MenuDto[] | undefined): void;
5468
}
5569

5670
declare global {
@@ -71,6 +85,8 @@ export const CHANNEL_SET_REPRESENTED_FILENAME =
7185
'Arduino:SetRepresentedFilename';
7286
export const CHANNEL_SHOW_PLOTTER_WINDOW = 'Arduino:ShowPlotterWindow';
7387
export const CHANNEL_OPEN_PATH = 'Arduino:OpenPath';
88+
export const CHANNEL_SET_MENU_WITH_NODE_ID = 'Arduino:SetMenuWithNodeId';
7489
// main to renderer
7590
export const CHANNEL_SEND_STARTUP_TASKS = 'Arduino:SendStartupTasks';
7691
export const CHANNEL_PLOTTER_WINDOW_DID_CLOSE = 'Arduino:PlotterWindowDidClose';
92+
export const CHANNEL_MAIN_MENU_ITEM_DID_CLICK = 'Arduino:MainMenuItemDidClick';

Diff for: arduino-ide-extension/src/electron-main/electron-arduino.ts

+54-1
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,33 @@ import {
33
dialog,
44
ipcMain,
55
IpcMainEvent,
6+
Menu,
7+
MenuItemConstructorOptions,
68
shell,
79
} from '@theia/core/electron-shared/electron';
810
import { Disposable } from '@theia/core/lib/common/disposable';
11+
import { isOSX } from '@theia/core/lib/common/os';
912
import { CHANNEL_REQUEST_RELOAD } from '@theia/core/lib/electron-common/electron-api';
1013
import {
11-
ElectronMainApplicationContribution,
1214
ElectronMainApplication as TheiaElectronMainApplication,
15+
ElectronMainApplicationContribution,
1316
} from '@theia/core/lib/electron-main/electron-main-application';
1417
import { createDisposableListener } from '@theia/core/lib/electron-main/event-utils';
1518
import { injectable } from '@theia/core/shared/inversify';
1619
import { WebContents } from '@theia/electron/shared/electron';
1720
import {
1821
CHANNEL_APP_VERSION,
1922
CHANNEL_IS_FIRST_WINDOW,
23+
CHANNEL_MAIN_MENU_ITEM_DID_CLICK,
2024
CHANNEL_OPEN_PATH,
2125
CHANNEL_QUIT_APP,
2226
CHANNEL_SEND_STARTUP_TASKS,
27+
CHANNEL_SET_MENU_WITH_NODE_ID,
2328
CHANNEL_SET_REPRESENTED_FILENAME,
2429
CHANNEL_SHOW_MESSAGE_BOX,
2530
CHANNEL_SHOW_OPEN_DIALOG,
2631
CHANNEL_SHOW_SAVE_DIALOG,
32+
InternalMenuDto,
2733
MessageBoxOptions,
2834
MessageBoxReturnValue,
2935
OpenDialogOptions,
@@ -99,9 +105,56 @@ export class ElectronArduino implements ElectronMainApplicationContribution {
99105
ipcMain.on(CHANNEL_OPEN_PATH, (_, fsPath: string) => {
100106
shell.openPath(fsPath);
101107
});
108+
ipcMain.on(
109+
CHANNEL_SET_MENU_WITH_NODE_ID,
110+
(event, internalMenu: InternalMenuDto[] | undefined) => {
111+
const electronMenu = internalMenu
112+
? Menu.buildFromTemplate(fromMenuDto(event.sender, internalMenu))
113+
: null;
114+
if (isOSX) {
115+
Menu.setApplicationMenu(electronMenu);
116+
} else {
117+
const window = BrowserWindow.fromWebContents(event.sender);
118+
if (!window) {
119+
console.warn(
120+
`Failed to set the application menu. Could not find the browser window from the webContents. Sender ID: ${event.sender.id}`
121+
);
122+
return;
123+
}
124+
window.setMenu(electronMenu);
125+
}
126+
}
127+
);
102128
}
103129
}
104130

131+
function fromMenuDto(
132+
sender: WebContents,
133+
menuDto: InternalMenuDto[]
134+
): MenuItemConstructorOptions[] {
135+
return menuDto.map((dto) => {
136+
const result: MenuItemConstructorOptions = {
137+
id: dto.id,
138+
label: dto.label,
139+
type: dto.type,
140+
checked: dto.checked,
141+
enabled: dto.enabled,
142+
visible: dto.visible,
143+
role: dto.role,
144+
accelerator: dto.accelerator,
145+
};
146+
if (dto.submenu) {
147+
result.submenu = fromMenuDto(sender, dto.submenu);
148+
}
149+
if (dto.nodeId) {
150+
result.click = () => {
151+
sender.send(CHANNEL_MAIN_MENU_ITEM_DID_CLICK, dto.nodeId);
152+
};
153+
}
154+
return result;
155+
});
156+
}
157+
105158
export namespace ElectronArduinoRenderer {
106159
export function sendStartupTasks(
107160
webContents: WebContents,

0 commit comments

Comments
 (0)