waveterm/emain/emain-menu.ts
2025-11-14 16:35:37 -08:00

487 lines
16 KiB
TypeScript

// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { waveEventSubscribe } from "@/app/store/wps";
import { RpcApi } from "@/app/store/wshclientapi";
import * as electron from "electron";
import { fireAndForget } from "../frontend/util/util";
import { focusedBuilderWindow, getBuilderWindowById } from "./emain-builder";
import { openBuilderWindow } from "./emain-ipc";
import { isDev, unamePlatform } from "./emain-platform";
import { clearTabCache } from "./emain-tabview";
import {
createNewWaveWindow,
createWorkspace,
focusedWaveWindow,
getAllWaveWindows,
getWaveWindowByWorkspaceId,
relaunchBrowserWindows,
WaveBrowserWindow,
} from "./emain-window";
import { ElectronWshClient } from "./emain-wsh";
import { updater } from "./updater";
type AppMenuCallbacks = {
createNewWaveWindow: () => Promise<void>;
relaunchBrowserWindows: () => Promise<void>;
};
function getWindowWebContents(window: electron.BaseWindow): electron.WebContents {
if (window == null) {
return null;
}
// Check BrowserWindow first (for Tsunami Builder windows)
if (window instanceof electron.BrowserWindow) {
return window.webContents;
}
// Check WaveBrowserWindow (for main Wave windows with tab views)
if (window instanceof WaveBrowserWindow) {
if (window.activeTabView) {
return window.activeTabView.webContents;
}
return null;
}
return null;
}
async function getWorkspaceMenu(ww?: WaveBrowserWindow): Promise<Electron.MenuItemConstructorOptions[]> {
const workspaceList = await RpcApi.WorkspaceListCommand(ElectronWshClient);
const workspaceMenu: Electron.MenuItemConstructorOptions[] = [
{
label: "Create Workspace",
click: (_, window) => fireAndForget(() => createWorkspace((window as WaveBrowserWindow) ?? ww)),
},
];
function getWorkspaceSwitchAccelerator(i: number): string {
if (i < 9) {
return unamePlatform == "darwin" ? `Command+Control+${i + 1}` : `Alt+Control+${i + 1}`;
}
}
workspaceList?.length &&
workspaceMenu.push(
{ type: "separator" },
...workspaceList.map<Electron.MenuItemConstructorOptions>((workspace, i) => {
return {
label: `${workspace.workspacedata.name}`,
click: (_, window) => {
((window as WaveBrowserWindow) ?? ww)?.switchWorkspace(workspace.workspacedata.oid);
},
accelerator: getWorkspaceSwitchAccelerator(i),
};
})
);
return workspaceMenu;
}
function makeEditMenu(): Electron.MenuItemConstructorOptions[] {
return [
{
role: "undo",
accelerator: unamePlatform === "darwin" ? "Command+Z" : "",
},
{
role: "redo",
accelerator: unamePlatform === "darwin" ? "Command+Shift+Z" : "",
},
{ type: "separator" },
{
role: "cut",
accelerator: unamePlatform === "darwin" ? "Command+X" : "",
},
{
role: "copy",
accelerator: unamePlatform === "darwin" ? "Command+C" : "",
},
{
role: "paste",
accelerator: unamePlatform === "darwin" ? "Command+V" : "",
},
{
role: "pasteAndMatchStyle",
accelerator: unamePlatform === "darwin" ? "Command+Shift+V" : "",
},
{
role: "delete",
},
{
role: "selectAll",
accelerator: unamePlatform === "darwin" ? "Command+A" : "",
},
];
}
function makeFileMenu(numWaveWindows: number, callbacks: AppMenuCallbacks, fullConfig: FullConfigType): Electron.MenuItemConstructorOptions[] {
const fileMenu: Electron.MenuItemConstructorOptions[] = [
{
label: "New Window",
accelerator: "CommandOrControl+Shift+N",
click: () => fireAndForget(callbacks.createNewWaveWindow),
},
{
role: "close",
accelerator: "",
click: () => {
focusedWaveWindow?.close();
},
},
];
const featureWaveAppBuilder = fullConfig?.settings?.["feature:waveappbuilder"];
if (isDev || featureWaveAppBuilder) {
fileMenu.splice(1, 0, {
label: "New WaveApp Builder Window",
accelerator: unamePlatform === "darwin" ? "Command+Shift+B" : "Alt+Shift+B",
click: () => openBuilderWindow(""),
});
}
if (numWaveWindows == 0) {
fileMenu.push({
label: "New Window (hidden-1)",
accelerator: unamePlatform === "darwin" ? "Command+N" : "Alt+N",
acceleratorWorksWhenHidden: true,
visible: false,
click: () => fireAndForget(callbacks.createNewWaveWindow),
});
fileMenu.push({
label: "New Window (hidden-2)",
accelerator: unamePlatform === "darwin" ? "Command+T" : "Alt+T",
acceleratorWorksWhenHidden: true,
visible: false,
click: () => fireAndForget(callbacks.createNewWaveWindow),
});
}
return fileMenu;
}
function makeAppMenuItems(webContents: electron.WebContents): Electron.MenuItemConstructorOptions[] {
const appMenuItems: Electron.MenuItemConstructorOptions[] = [
{
label: "About Wave Terminal",
click: (_, window) => {
(getWindowWebContents(window) ?? webContents)?.send("menu-item-about");
},
},
{
label: "Check for Updates",
click: () => {
fireAndForget(() => updater?.checkForUpdates(true));
},
},
{ type: "separator" },
];
if (unamePlatform === "darwin") {
appMenuItems.push(
{ role: "services" },
{ type: "separator" },
{ role: "hide" },
{ role: "hideOthers" },
{ type: "separator" }
);
}
appMenuItems.push({ role: "quit" });
return appMenuItems;
}
function makeViewMenu(
webContents: electron.WebContents,
callbacks: AppMenuCallbacks,
isBuilderWindowFocused: boolean,
fullscreenOnLaunch: boolean
): Electron.MenuItemConstructorOptions[] {
const devToolsAccel = unamePlatform === "darwin" ? "Option+Command+I" : "Alt+Shift+I";
return [
{
label: isBuilderWindowFocused ? "Reload Window" : "Reload Tab",
accelerator: "Shift+CommandOrControl+R",
click: (_, window) => {
(getWindowWebContents(window) ?? webContents)?.reloadIgnoringCache();
},
},
{
label: "Relaunch All Windows",
click: () => callbacks.relaunchBrowserWindows(),
},
{
label: "Clear Tab Cache",
click: () => clearTabCache(),
},
{
label: "Toggle DevTools",
accelerator: devToolsAccel,
click: (_, window) => {
let wc = getWindowWebContents(window) ?? webContents;
wc?.toggleDevTools();
},
},
{ type: "separator" },
{
label: "Reset Zoom",
accelerator: "CommandOrControl+0",
click: (_, window) => {
const wc = getWindowWebContents(window) ?? webContents;
if (wc) {
wc.setZoomFactor(1);
wc.send("zoom-factor-change", 1);
}
},
},
{
label: "Zoom In",
accelerator: "CommandOrControl+=",
click: (_, window) => {
const wc = getWindowWebContents(window) ?? webContents;
if (wc == null) {
return;
}
const newZoom = Math.min(5, wc.getZoomFactor() + 0.2);
wc.setZoomFactor(newZoom);
wc.send("zoom-factor-change", newZoom);
},
},
{
label: "Zoom In (hidden)",
accelerator: "CommandOrControl+Shift+=",
click: (_, window) => {
const wc = getWindowWebContents(window) ?? webContents;
if (wc == null) {
return;
}
const newZoom = Math.min(5, wc.getZoomFactor() + 0.2);
wc.setZoomFactor(newZoom);
wc.send("zoom-factor-change", newZoom);
},
visible: false,
acceleratorWorksWhenHidden: true,
},
{
label: "Zoom Out",
accelerator: "CommandOrControl+-",
click: (_, window) => {
const wc = getWindowWebContents(window) ?? webContents;
if (wc == null) {
return;
}
const newZoom = Math.max(0.2, wc.getZoomFactor() - 0.2);
wc.setZoomFactor(newZoom);
wc.send("zoom-factor-change", newZoom);
},
},
{
label: "Zoom Out (hidden)",
accelerator: "CommandOrControl+Shift+-",
click: (_, window) => {
const wc = getWindowWebContents(window) ?? webContents;
if (wc == null) {
return;
}
const newZoom = Math.max(0.2, wc.getZoomFactor() - 0.2);
wc.setZoomFactor(newZoom);
wc.send("zoom-factor-change", newZoom);
},
visible: false,
acceleratorWorksWhenHidden: true,
},
{
label: "Launch On Full Screen",
submenu: [
{
label: "On",
type: "radio",
checked: fullscreenOnLaunch,
click: () => {
RpcApi.SetConfigCommand(ElectronWshClient, { "window:fullscreenonlaunch": true });
},
},
{
label: "Off",
type: "radio",
checked: !fullscreenOnLaunch,
click: () => {
RpcApi.SetConfigCommand(ElectronWshClient, { "window:fullscreenonlaunch": false });
},
},
],
},
{ type: "separator" },
{
role: "togglefullscreen",
},
];
}
async function makeFullAppMenu(callbacks: AppMenuCallbacks, workspaceOrBuilderId?: string): Promise<Electron.Menu> {
const numWaveWindows = getAllWaveWindows().length;
const webContents = workspaceOrBuilderId && getWebContentsByWorkspaceOrBuilderId(workspaceOrBuilderId);
const appMenuItems = makeAppMenuItems(webContents);
const editMenu = makeEditMenu();
const isBuilderWindowFocused = focusedBuilderWindow != null;
let fullscreenOnLaunch = false;
let fullConfig: FullConfigType = null;
try {
fullConfig = await RpcApi.GetFullConfigCommand(ElectronWshClient);
fullscreenOnLaunch = fullConfig?.settings["window:fullscreenonlaunch"];
} catch (e) {
console.error("Error fetching config:", e);
}
const fileMenu = makeFileMenu(numWaveWindows, callbacks, fullConfig);
const viewMenu = makeViewMenu(webContents, callbacks, isBuilderWindowFocused, fullscreenOnLaunch);
let workspaceMenu: Electron.MenuItemConstructorOptions[] = null;
try {
workspaceMenu = await getWorkspaceMenu();
} catch (e) {
console.error("getWorkspaceMenu error:", e);
}
const windowMenu: Electron.MenuItemConstructorOptions[] = [
{ role: "minimize", accelerator: "" },
{ role: "zoom" },
{ type: "separator" },
{ role: "front" },
];
const menuTemplate: Electron.MenuItemConstructorOptions[] = [
{ role: "appMenu", submenu: appMenuItems },
{ role: "fileMenu", submenu: fileMenu },
{ role: "editMenu", submenu: editMenu },
{ role: "viewMenu", submenu: viewMenu },
];
if (workspaceMenu != null && !isBuilderWindowFocused) {
menuTemplate.push({
label: "Workspace",
id: "workspace-menu",
submenu: workspaceMenu,
});
}
menuTemplate.push({
role: "windowMenu",
submenu: windowMenu,
});
return electron.Menu.buildFromTemplate(menuTemplate);
}
export function instantiateAppMenu(workspaceOrBuilderId?: string): Promise<electron.Menu> {
return makeFullAppMenu(
{
createNewWaveWindow,
relaunchBrowserWindows,
},
workspaceOrBuilderId
);
}
export function makeAppMenu() {
fireAndForget(async () => {
const menu = await instantiateAppMenu();
electron.Menu.setApplicationMenu(menu);
});
}
waveEventSubscribe({
eventType: "workspace:update",
handler: makeAppMenu,
});
function getWebContentsByWorkspaceOrBuilderId(workspaceOrBuilderId: string): electron.WebContents {
const ww = getWaveWindowByWorkspaceId(workspaceOrBuilderId);
if (ww) {
return ww.activeTabView?.webContents;
}
const bw = getBuilderWindowById(workspaceOrBuilderId);
if (bw) {
return bw.webContents;
}
return null;
}
function convertMenuDefArrToMenu(
webContents: electron.WebContents,
menuDefArr: ElectronContextMenuItem[]
): electron.Menu {
const menuItems: electron.MenuItem[] = [];
for (const menuDef of menuDefArr) {
const menuItemTemplate: electron.MenuItemConstructorOptions = {
role: menuDef.role as any,
label: menuDef.label,
type: menuDef.type,
click: (_, window) => {
const wc = getWindowWebContents(window) ?? webContents;
if (!wc) {
console.error("invalid window for context menu click handler:", window);
return;
}
wc.send("contextmenu-click", menuDef.id);
},
checked: menuDef.checked,
};
if (menuDef.submenu != null) {
menuItemTemplate.submenu = convertMenuDefArrToMenu(webContents, menuDef.submenu);
}
const menuItem = new electron.MenuItem(menuItemTemplate);
menuItems.push(menuItem);
}
return electron.Menu.buildFromTemplate(menuItems);
}
electron.ipcMain.on(
"contextmenu-show",
(event, workspaceOrBuilderId: string, menuDefArr: ElectronContextMenuItem[]) => {
if (menuDefArr.length === 0) {
event.returnValue = true;
return;
}
fireAndForget(async () => {
const webContents = getWebContentsByWorkspaceOrBuilderId(workspaceOrBuilderId);
if (!webContents) {
console.error("invalid window for context menu:", workspaceOrBuilderId);
return;
}
const menu = convertMenuDefArrToMenu(webContents, menuDefArr);
menu.popup();
});
event.returnValue = true;
}
);
electron.ipcMain.on("workspace-appmenu-show", (event, workspaceId: string) => {
fireAndForget(async () => {
const webContents = getWebContentsByWorkspaceOrBuilderId(workspaceId);
if (!webContents) {
console.error("invalid window for workspace app menu:", workspaceId);
return;
}
const menu = await instantiateAppMenu(workspaceId);
menu.popup();
});
event.returnValue = true;
});
electron.ipcMain.on("builder-appmenu-show", (event, builderId: string) => {
fireAndForget(async () => {
const webContents = getWebContentsByWorkspaceOrBuilderId(builderId);
if (!webContents) {
console.error("invalid window for builder app menu:", builderId);
return;
}
const menu = await instantiateAppMenu(builderId);
menu.popup();
});
event.returnValue = true;
});
const dockMenu = electron.Menu.buildFromTemplate([
{
label: "New Window",
click() {
fireAndForget(createNewWaveWindow);
},
},
]);
function makeDockTaskbar() {
if (unamePlatform == "darwin") {
electron.app.dock.setMenu(dockMenu);
}
}
export { makeDockTaskbar };