Skip to content

Commit

Permalink
fix: workaround for menus not working at startup
Browse files Browse the repository at this point in the history
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
  • Loading branch information
Akos Kitta committed May 8, 2023
1 parent ed60048 commit bcb7f7d
Show file tree
Hide file tree
Showing 5 changed files with 211 additions and 12 deletions.
59 changes: 57 additions & 2 deletions arduino-ide-extension/src/electron-browser/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,71 @@ import {
ipcRenderer,
} from '@theia/core/electron-shared/electron';
import { Disposable } from '@theia/core/lib/common/disposable';
import { CHANNEL_REQUEST_RELOAD } from '@theia/core/lib/electron-common/electron-api';
import {
CHANNEL_REQUEST_RELOAD,
MenuDto,
} from '@theia/core/lib/electron-common/electron-api';
import { v4 } from 'uuid';
import type { Sketch } from '../common/protocol/sketches-service';
import {
CHANNEL_APP_VERSION,
CHANNEL_IS_FIRST_WINDOW,
CHANNEL_MAIN_MENU_ITEM_DID_CLICK,
CHANNEL_OPEN_PATH,
CHANNEL_PLOTTER_WINDOW_DID_CLOSE,
CHANNEL_QUIT_APP,
CHANNEL_SCHEDULE_DELETION,
CHANNEL_SEND_STARTUP_TASKS,
CHANNEL_SET_MENU_WITH_NODE_ID,
CHANNEL_SET_REPRESENTED_FILENAME,
CHANNEL_SHOW_MESSAGE_BOX,
CHANNEL_SHOW_OPEN_DIALOG,
CHANNEL_SHOW_PLOTTER_WINDOW,
CHANNEL_SHOW_SAVE_DIALOG,
ElectronArduino,
InternalMenuDto,
MessageBoxOptions,
OpenDialogOptions,
SaveDialogOptions,
} from '../electron-common/electron-arduino';
import { StartupTasks, hasStartupTasks } from '../electron-common/startup-task';
import { hasStartupTasks, StartupTasks } from '../electron-common/startup-task';

let mainMenuHandlers: Map<string, () => void> = new Map();

function convertMenu(
menu: MenuDto[] | undefined,
handlerMap: Map<string, () => void>
): InternalMenuDto[] | undefined {
if (!menu) {
return undefined;
}

return menu.map((item) => {
let nodeId = v4();
if (item.execute) {
if (!item.id) {
throw new Error(
"A menu item having the 'execute' property must have an 'id' too."
);
}
nodeId = item.id;
handlerMap.set(nodeId, item.execute);
}

return {
id: item.id,
submenu: convertMenu(item.submenu, handlerMap),
accelerator: item.accelerator,
label: item.label,
nodeId,
checked: item.checked,
enabled: item.enabled,
role: item.role,
type: item.type,
visible: item.visible,
};
});
}

const api: ElectronArduino = {
showMessageBox: (options: MessageBoxOptions) =>
Expand Down Expand Up @@ -68,9 +112,20 @@ const api: ElectronArduino = {
);
},
openPath: (fsPath: string) => ipcRenderer.send(CHANNEL_OPEN_PATH, fsPath),
setMenu: (menu: MenuDto[] | undefined): void => {
mainMenuHandlers = new Map();
const internalMenu = convertMenu(menu, mainMenuHandlers);
ipcRenderer.send(CHANNEL_SET_MENU_WITH_NODE_ID, internalMenu);
},
};

export function preload(): void {
contextBridge.exposeInMainWorld('electronArduino', api);
ipcRenderer.on(CHANNEL_MAIN_MENU_ITEM_DID_CLICK, (_, nodeId: string) => {
const handler = mainMenuHandlers.get(nodeId);
if (handler) {
handler();
}
});
console.log('Exposed Arduino IDE electron API');
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
ArduinoMenus,
PlaceholderMenuNode,
} from '../../../browser/menu/arduino-menus';
import debounce from 'lodash.debounce';

@injectable()
export class ElectronMainMenuFactory extends TheiaElectronMainMenuFactory {
Expand All @@ -33,7 +34,27 @@ export class ElectronMainMenuFactory extends TheiaElectronMainMenuFactory {
private updateWhenReady = false;

override postConstruct(): void {
super.postConstruct();
// #region Theia `postConstruct` customizations with calling IDE2 `setMenu`
this.preferencesService.onPreferenceChanged(
debounce((e) => {
if (e.preferenceName === 'window.menuBarVisibility') {
this.setMenuBar();
}
if (this._menu) {
for (const cmd of this._toggledCommands) {
const menuItem = this.findMenuById(this._menu, cmd);
if (menuItem) {
menuItem.checked = this.commandRegistry.isToggled(cmd);
}
}
window.electronArduino.setMenu(this._menu); // calls the IDE2-specific implementation
}
}, 10)
);
this.keybindingRegistry.onKeybindingsChanged(() => {
this.setMenuBar();
});
// #endregion Theia `postConstruct`
this.appStateService.reachedState('ready').then(() => {
this.appReady = true;
if (this.updateWhenReady) {
Expand Down Expand Up @@ -65,7 +86,8 @@ export class ElectronMainMenuFactory extends TheiaElectronMainMenuFactory {
return;
}
await this.preferencesService.ready;
return super.setMenuBar();
const createdMenuBar = this.createElectronMenuBar();
window.electronArduino.setMenu(createdMenuBar);
}

override createElectronContextMenu(
Expand All @@ -85,6 +107,42 @@ export class ElectronMainMenuFactory extends TheiaElectronMainMenuFactory {
});
}

protected override async execute(
commandId: string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
args: any[],
menuPath: MenuPath
): Promise<void> {
try {
// This is workaround for https://github.com/eclipse-theia/theia/issues/446.
// Electron menus do not update based on the `isEnabled`, `isVisible` property of the command.
// We need to check if we can execute it.
if (this.menuCommandExecutor.isEnabled(menuPath, commandId, ...args)) {
await this.menuCommandExecutor.executeCommand(
menuPath,
commandId,
...args
);
if (
this._menu &&
this.menuCommandExecutor.isVisible(menuPath, commandId, ...args)
) {
const item = this.findMenuById(this._menu, commandId);
if (item) {
item.checked = this.menuCommandExecutor.isToggled(
menuPath,
commandId,
...args
);
window.electronArduino.setMenu(this._menu); // overridden to call the IDE2-specific implementation.
}
}
}
} catch {
// no-op
}
}

// TODO: remove after https://github.com/eclipse-theia/theia/pull/9231
private escapeAmpersand(template: MenuDto[]): MenuDto[] {
for (const option of template) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { inject, injectable } from '@theia/core/shared/inversify';
import { CommandRegistry } from '@theia/core/lib/common/command';
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
import { KeybindingRegistry } from '@theia/core/lib/browser/keybinding';
import type { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
import type { KeybindingRegistry } from '@theia/core/lib/browser/keybinding';
import type { CommandRegistry } from '@theia/core/lib/common/command';
import type { MenuModelRegistry } from '@theia/core/lib/common/menu';
import { isOSX } from '@theia/core/lib/common/os';
import {
ElectronMenuContribution as TheiaElectronMenuContribution,
ElectronCommands,
ElectronMenuContribution as TheiaElectronMenuContribution,
} from '@theia/core/lib/electron-browser/menu/electron-menu-contribution';
import { MainMenuManager } from '../../../common/main-menu-manager';
import type { MenuDto } from '@theia/core/lib/electron-common/electron-api';
import { inject, injectable } from '@theia/core/shared/inversify';
import type { MainMenuManager } from '../../../common/main-menu-manager';
import { ElectronMainMenuFactory } from './electron-main-menu-factory';

@injectable()
Expand All @@ -19,7 +22,7 @@ export class ElectronMenuUpdater implements MainMenuManager {
}

private setMenu(): void {
window.electronTheiaCore.setMenu(this.factory.createElectronMenuBar());
window.electronArduino.setMenu(this.factory.createElectronMenuBar());
}
}

Expand All @@ -46,4 +49,18 @@ export class ElectronMenuContribution extends TheiaElectronMenuContribution {
registry.unregisterKeybinding(ElectronCommands.ZOOM_IN.id);
registry.unregisterKeybinding(ElectronCommands.ZOOM_OUT.id);
}

protected override setMenu(
app: FrontendApplication,
electronMenu: MenuDto[] | undefined = this.factory.createElectronMenuBar()
): void {
if (!isOSX) {
this.hideTopPanel(); // no app args. the overridden method is noop in IDE2.
if (this.titleBarStyle === 'custom' && !this.menuBar) {
this.createCustomTitleBar(app);
return;
}
}
window.electronArduino.setMenu(electronMenu); // overridden to call the IDE20-specific implementation.
}
}
16 changes: 16 additions & 0 deletions arduino-ide-extension/src/electron-common/electron-arduino.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,20 @@ import type {
SaveDialogReturnValue as ElectronSaveDialogReturnValue,
} from '@theia/core/electron-shared/electron';
import type { Disposable } from '@theia/core/lib/common/disposable';
import type {
InternalMenuDto as TheiaInternalMenuDto,
MenuDto,
} from '@theia/core/lib/electron-common/electron-api';
import type { Sketch } from '../common/protocol/sketches-service';
import type { StartupTasks } from './startup-task';

export interface InternalMenuDto
extends Omit<TheiaInternalMenuDto, 'handlerId'> {
// Theia handles the menus with a running-index handler ID. https://github.com/eclipse-theia/theia/issues/12493
// IDE2 keeps the menu `nodeId` instead of the running-index.
nodeId?: string;
}

export type MessageBoxOptions = Omit<
ElectronMessageBoxOptions,
'icon' | 'signal'
Expand Down Expand Up @@ -51,6 +62,9 @@ export interface ElectronArduino {
showPlotterWindow(params: { url: string; forceReload?: boolean }): void;
registerPlotterWindowCloseHandler(handler: () => void): Disposable;
openPath(fsPath: string): void;
// Unlike the Theia implementation, IDE2 uses the command IDs, and not the running-index handler IDs.
// https://github.com/eclipse-theia/theia/issues/12493
setMenu(menu: MenuDto[] | undefined): void;
}

declare global {
Expand All @@ -71,6 +85,8 @@ export const CHANNEL_SET_REPRESENTED_FILENAME =
'Arduino:SetRepresentedFilename';
export const CHANNEL_SHOW_PLOTTER_WINDOW = 'Arduino:ShowPlotterWindow';
export const CHANNEL_OPEN_PATH = 'Arduino:OpenPath';
export const CHANNEL_SET_MENU_WITH_NODE_ID = 'Arduino:SetMenuWithNodeId';
// main to renderer
export const CHANNEL_SEND_STARTUP_TASKS = 'Arduino:SendStartupTasks';
export const CHANNEL_PLOTTER_WINDOW_DID_CLOSE = 'Arduino:PlotterWindowDidClose';
export const CHANNEL_MAIN_MENU_ITEM_DID_CLICK = 'Arduino:MainMenuItemDidClick';
55 changes: 54 additions & 1 deletion arduino-ide-extension/src/electron-main/electron-arduino.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,33 @@ import {
dialog,
ipcMain,
IpcMainEvent,
Menu,
MenuItemConstructorOptions,
shell,
} from '@theia/core/electron-shared/electron';
import { Disposable } from '@theia/core/lib/common/disposable';
import { isOSX } from '@theia/core/lib/common/os';
import { CHANNEL_REQUEST_RELOAD } from '@theia/core/lib/electron-common/electron-api';
import {
ElectronMainApplicationContribution,
ElectronMainApplication as TheiaElectronMainApplication,
ElectronMainApplicationContribution,
} from '@theia/core/lib/electron-main/electron-main-application';
import { createDisposableListener } from '@theia/core/lib/electron-main/event-utils';
import { injectable } from '@theia/core/shared/inversify';
import { WebContents } from '@theia/electron/shared/electron';
import {
CHANNEL_APP_VERSION,
CHANNEL_IS_FIRST_WINDOW,
CHANNEL_MAIN_MENU_ITEM_DID_CLICK,
CHANNEL_OPEN_PATH,
CHANNEL_QUIT_APP,
CHANNEL_SEND_STARTUP_TASKS,
CHANNEL_SET_MENU_WITH_NODE_ID,
CHANNEL_SET_REPRESENTED_FILENAME,
CHANNEL_SHOW_MESSAGE_BOX,
CHANNEL_SHOW_OPEN_DIALOG,
CHANNEL_SHOW_SAVE_DIALOG,
InternalMenuDto,
MessageBoxOptions,
MessageBoxReturnValue,
OpenDialogOptions,
Expand Down Expand Up @@ -99,9 +105,56 @@ export class ElectronArduino implements ElectronMainApplicationContribution {
ipcMain.on(CHANNEL_OPEN_PATH, (_, fsPath: string) => {
shell.openPath(fsPath);
});
ipcMain.on(
CHANNEL_SET_MENU_WITH_NODE_ID,
(event, internalMenu: InternalMenuDto[] | undefined) => {
const electronMenu = internalMenu
? Menu.buildFromTemplate(fromMenuDto(event.sender, internalMenu))
: null;
if (isOSX) {
Menu.setApplicationMenu(electronMenu);
} else {
const window = BrowserWindow.fromWebContents(event.sender);
if (!window) {
console.warn(
`Failed to set the application menu. Could not find the browser window from the webContents. Sender ID: ${event.sender.id}`
);
return;
}
window.setMenu(electronMenu);
}
}
);
}
}

function fromMenuDto(
sender: WebContents,
menuDto: InternalMenuDto[]
): MenuItemConstructorOptions[] {
return menuDto.map((dto) => {
const result: MenuItemConstructorOptions = {
id: dto.id,
label: dto.label,
type: dto.type,
checked: dto.checked,
enabled: dto.enabled,
visible: dto.visible,
role: dto.role,
accelerator: dto.accelerator,
};
if (dto.submenu) {
result.submenu = fromMenuDto(sender, dto.submenu);
}
if (dto.nodeId) {
result.click = () => {
sender.send(CHANNEL_MAIN_MENU_ITEM_DID_CLICK, dto.nodeId);
};
}
return result;
});
}

export namespace ElectronArduinoRenderer {
export function sendStartupTasks(
webContents: WebContents,
Expand Down

0 comments on commit bcb7f7d

Please sign in to comment.