Skip to content

Commit 72ea582

Browse files
authored
Workspace app menu (#1423)
Adds a new app menu for creating a new workspace or switching to an existing one. This required adding a new WPS event any time a workspace gets updated, since the Electron app menus are static. This also fixes a bug where closing a workspace could delete it if it didn't have both a pinned and an unpinned tab.
1 parent 66d1686 commit 72ea582

15 files changed

Lines changed: 301 additions & 154 deletions

File tree

emain/emain-window.ts

Lines changed: 106 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
11
// Copyright 2024, Command Line Inc.
22
// SPDX-License-Identifier: Apache-2.0
33

4-
import { ClientService, FileService, WindowService, WorkspaceService } from "@/app/store/services";
4+
import { ClientService, FileService, ObjectService, WindowService, WorkspaceService } from "@/app/store/services";
55
import { fireAndForget } from "@/util/util";
66
import { BaseWindow, BaseWindowConstructorOptions, dialog, ipcMain, screen } from "electron";
77
import path from "path";
88
import { debounce } from "throttle-debounce";
9-
import { getGlobalIsQuitting, getGlobalIsRelaunching, setWasActive, setWasInFg } from "./emain-activity";
9+
import {
10+
getGlobalIsQuitting,
11+
getGlobalIsRelaunching,
12+
setGlobalIsRelaunching,
13+
setWasActive,
14+
setWasInFg,
15+
} from "./emain-activity";
1016
import { getOrCreateWebViewForTab, getWaveTabViewByWebContentsId, WaveTabView } from "./emain-tabview";
1117
import { delay, ensureBoundsAreVisible } from "./emain-util";
18+
import { log } from "./log";
1219
import { getElectronAppBasePath, unamePlatform } from "./platform";
1320
import { updater } from "./updater";
1421
export type WindowOpts = {
@@ -272,6 +279,10 @@ export class WaveBrowserWindow extends BaseWindow {
272279

273280
async switchWorkspace(workspaceId: string) {
274281
console.log("switchWorkspace", workspaceId, this.waveWindowId);
282+
if (workspaceId == this.workspaceId) {
283+
console.log("switchWorkspace already on this workspace", this.waveWindowId);
284+
return;
285+
}
275286
const curWorkspace = await WorkspaceService.GetWorkspace(this.workspaceId);
276287
if (curWorkspace.tabids.length > 1 && (!curWorkspace.name || !curWorkspace.icon)) {
277288
const choice = dialog.showMessageBoxSync(this, {
@@ -603,19 +614,100 @@ ipcMain.on("close-tab", async (event, workspaceId, tabId) => {
603614
return null;
604615
});
605616

606-
ipcMain.on("switch-workspace", async (event, workspaceId) => {
607-
const ww = getWaveWindowByWebContentsId(event.sender.id);
608-
console.log("switch-workspace", workspaceId, ww?.waveWindowId);
609-
await ww?.switchWorkspace(workspaceId);
617+
ipcMain.on("switch-workspace", (event, workspaceId) => {
618+
fireAndForget(async () => {
619+
const ww = getWaveWindowByWebContentsId(event.sender.id);
620+
console.log("switch-workspace", workspaceId, ww?.waveWindowId);
621+
await ww?.switchWorkspace(workspaceId);
622+
});
610623
});
611624

612-
ipcMain.on("delete-workspace", async (event, workspaceId) => {
613-
const ww = getWaveWindowByWebContentsId(event.sender.id);
614-
console.log("delete-workspace", workspaceId, ww?.waveWindowId);
615-
await WorkspaceService.DeleteWorkspace(workspaceId);
616-
console.log("delete-workspace done", workspaceId, ww?.waveWindowId);
617-
if (ww?.workspaceId == workspaceId) {
618-
console.log("delete-workspace closing window", workspaceId, ww?.waveWindowId);
619-
ww.destroy();
625+
export async function createWorkspace(window: WaveBrowserWindow) {
626+
if (!window) {
627+
return;
628+
}
629+
const newWsId = await WorkspaceService.CreateWorkspace();
630+
if (newWsId) {
631+
await window.switchWorkspace(newWsId);
620632
}
633+
}
634+
635+
ipcMain.on("create-workspace", (event) => {
636+
fireAndForget(async () => {
637+
const ww = getWaveWindowByWebContentsId(event.sender.id);
638+
console.log("create-workspace", ww?.waveWindowId);
639+
await createWorkspace(ww);
640+
});
641+
});
642+
643+
ipcMain.on("delete-workspace", (event, workspaceId) => {
644+
fireAndForget(async () => {
645+
const ww = getWaveWindowByWebContentsId(event.sender.id);
646+
console.log("delete-workspace", workspaceId, ww?.waveWindowId);
647+
await WorkspaceService.DeleteWorkspace(workspaceId);
648+
console.log("delete-workspace done", workspaceId, ww?.waveWindowId);
649+
if (ww?.workspaceId == workspaceId) {
650+
console.log("delete-workspace closing window", workspaceId, ww?.waveWindowId);
651+
ww.destroy();
652+
}
653+
});
621654
});
655+
656+
export async function createNewWaveWindow() {
657+
log("createNewWaveWindow");
658+
const clientData = await ClientService.GetClientData();
659+
const fullConfig = await FileService.GetFullConfig();
660+
let recreatedWindow = false;
661+
const allWindows = getAllWaveWindows();
662+
if (allWindows.length === 0 && clientData?.windowids?.length >= 1) {
663+
console.log("no windows, but clientData has windowids, recreating first window");
664+
// reopen the first window
665+
const existingWindowId = clientData.windowids[0];
666+
const existingWindowData = (await ObjectService.GetObject("window:" + existingWindowId)) as WaveWindow;
667+
if (existingWindowData != null) {
668+
const win = await createBrowserWindow(existingWindowData, fullConfig, { unamePlatform });
669+
await win.waveReadyPromise;
670+
win.show();
671+
recreatedWindow = true;
672+
}
673+
}
674+
if (recreatedWindow) {
675+
console.log("recreated window, returning");
676+
return;
677+
}
678+
console.log("creating new window");
679+
const newBrowserWindow = await createBrowserWindow(null, fullConfig, { unamePlatform });
680+
await newBrowserWindow.waveReadyPromise;
681+
newBrowserWindow.show();
682+
}
683+
684+
export async function relaunchBrowserWindows() {
685+
console.log("relaunchBrowserWindows");
686+
setGlobalIsRelaunching(true);
687+
const windows = getAllWaveWindows();
688+
for (const window of windows) {
689+
console.log("relaunch -- closing window", window.waveWindowId);
690+
window.close();
691+
}
692+
setGlobalIsRelaunching(false);
693+
694+
const clientData = await ClientService.GetClientData();
695+
const fullConfig = await FileService.GetFullConfig();
696+
const wins: WaveBrowserWindow[] = [];
697+
for (const windowId of clientData.windowids.slice().reverse()) {
698+
const windowData: WaveWindow = await WindowService.GetWindow(windowId);
699+
if (windowData == null) {
700+
console.log("relaunch -- window data not found, closing window", windowId);
701+
await WindowService.CloseWindow(windowId, true);
702+
continue;
703+
}
704+
console.log("relaunch -- creating window", windowId, windowData);
705+
const win = await createBrowserWindow(windowData, fullConfig, { unamePlatform });
706+
wins.push(win);
707+
}
708+
for (const win of wins) {
709+
await win.waveReadyPromise;
710+
console.log("show window", win.waveWindowId);
711+
win.show();
712+
}
713+
}

emain/emain-wsh.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,16 @@ export class ElectronWshClientType extends WshClient {
5555
}
5656
ww.focus();
5757
}
58+
59+
// async handle_workspaceupdate(rh: RpcResponseHelper) {
60+
// console.log("workspaceupdate");
61+
// fireAndForget(async () => {
62+
// console.log("workspace menu clicked");
63+
// const updatedWorkspaceMenu = await getWorkspaceMenu();
64+
// const workspaceMenu = Menu.getApplicationMenu().getMenuItemById("workspace-menu");
65+
// workspaceMenu.submenu = Menu.buildFromTemplate(updatedWorkspaceMenu);
66+
// });
67+
// }
5868
}
5969

6070
export let ElectronWshClient: ElectronWshClientType;

emain/emain.ts

Lines changed: 9 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ import * as path from "path";
1010
import { PNG } from "pngjs";
1111
import { sprintf } from "sprintf-js";
1212
import { Readable } from "stream";
13-
import * as util from "util";
14-
import winston from "winston";
1513
import * as services from "../frontend/app/store/services";
1614
import { initElectronWshrpc, shutdownWshrpc } from "../frontend/app/store/wshrpcutil";
1715
import { getWebServerEndpoint } from "../frontend/util/endpoints";
@@ -25,7 +23,6 @@ import {
2523
getGlobalIsRelaunching,
2624
setForceQuit,
2725
setGlobalIsQuitting,
28-
setGlobalIsRelaunching,
2926
setGlobalIsStarting,
3027
setWasActive,
3128
setWasInFg,
@@ -35,16 +32,19 @@ import { handleCtrlShiftState } from "./emain-util";
3532
import { getIsWaveSrvDead, getWaveSrvProc, getWaveSrvReady, getWaveVersion, runWaveSrv } from "./emain-wavesrv";
3633
import {
3734
createBrowserWindow,
35+
createNewWaveWindow,
3836
focusedWaveWindow,
3937
getAllWaveWindows,
4038
getWaveWindowById,
4139
getWaveWindowByWebContentsId,
4240
getWaveWindowByWorkspaceId,
41+
relaunchBrowserWindows,
4342
WaveBrowserWindow,
4443
} from "./emain-window";
4544
import { ElectronWshClient, initElectronWshClient } from "./emain-wsh";
4645
import { getLaunchSettings } from "./launchsettings";
47-
import { getAppMenu } from "./menu";
46+
import { log } from "./log";
47+
import { instantiateAppMenu, makeAppMenu } from "./menu";
4848
import {
4949
getElectronAppBasePath,
5050
getElectronAppUnpackedBasePath,
@@ -65,30 +65,7 @@ electron.nativeTheme.themeSource = "dark";
6565

6666
let webviewFocusId: number = null; // set to the getWebContentsId of the webview that has focus (null if not focused)
6767
let webviewKeys: string[] = []; // the keys to trap when webview has focus
68-
const oldConsoleLog = console.log;
6968

70-
const loggerTransports: winston.transport[] = [
71-
new winston.transports.File({ filename: path.join(waveDataDir, "waveapp.log"), level: "info" }),
72-
];
73-
if (isDev) {
74-
loggerTransports.push(new winston.transports.Console());
75-
}
76-
const loggerConfig = {
77-
level: "info",
78-
format: winston.format.combine(
79-
winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss.SSS" }),
80-
winston.format.printf((info) => `${info.timestamp} ${info.message}`)
81-
),
82-
transports: loggerTransports,
83-
};
84-
const logger = winston.createLogger(loggerConfig);
85-
function log(...msg: any[]) {
86-
try {
87-
logger.info(util.format(...msg));
88-
} catch (e) {
89-
oldConsoleLog(...msg);
90-
}
91-
}
9269
console.log = log;
9370
console.log(
9471
sprintf(
@@ -375,34 +352,6 @@ electron.ipcMain.on("open-native-path", (event, filePath: string) => {
375352
);
376353
});
377354

378-
async function createNewWaveWindow(): Promise<void> {
379-
log("createNewWaveWindow");
380-
const clientData = await services.ClientService.GetClientData();
381-
const fullConfig = await services.FileService.GetFullConfig();
382-
let recreatedWindow = false;
383-
const allWindows = getAllWaveWindows();
384-
if (allWindows.length === 0 && clientData?.windowids?.length >= 1) {
385-
console.log("no windows, but clientData has windowids, recreating first window");
386-
// reopen the first window
387-
const existingWindowId = clientData.windowids[0];
388-
const existingWindowData = (await services.ObjectService.GetObject("window:" + existingWindowId)) as WaveWindow;
389-
if (existingWindowData != null) {
390-
const win = await createBrowserWindow(existingWindowData, fullConfig, { unamePlatform });
391-
await win.waveReadyPromise;
392-
win.show();
393-
recreatedWindow = true;
394-
}
395-
}
396-
if (recreatedWindow) {
397-
console.log("recreated window, returning");
398-
return;
399-
}
400-
console.log("creating new window");
401-
const newBrowserWindow = await createBrowserWindow(null, fullConfig, { unamePlatform });
402-
await newBrowserWindow.waveReadyPromise;
403-
newBrowserWindow.show();
404-
}
405-
406355
electron.ipcMain.on("set-window-init-status", (event, status: "ready" | "wave-ready") => {
407356
const tabView = getWaveTabViewByWebContentsId(event.sender.id);
408357
if (tabView == null || tabView.initResolve == null) {
@@ -481,10 +430,10 @@ electron.ipcMain.on("contextmenu-show", (event, menuDefArr?: ElectronContextMenu
481430
if (menuDefArr?.length === 0) {
482431
return;
483432
}
484-
const menu = menuDefArr ? convertMenuDefArrToMenu(menuDefArr) : instantiateAppMenu();
485-
// const { x, y } = electron.screen.getCursorScreenPoint();
486-
// const windowPos = window.getPosition();
487-
menu.popup();
433+
fireAndForget(async () => {
434+
const menu = menuDefArr ? convertMenuDefArrToMenu(menuDefArr) : await instantiateAppMenu();
435+
menu.popup();
436+
});
488437
event.returnValue = true;
489438
});
490439

@@ -561,18 +510,6 @@ function convertMenuDefArrToMenu(menuDefArr: ElectronContextMenuItem[]): electro
561510
return electron.Menu.buildFromTemplate(menuItems);
562511
}
563512

564-
function instantiateAppMenu(): electron.Menu {
565-
return getAppMenu({
566-
createNewWaveWindow,
567-
relaunchBrowserWindows,
568-
});
569-
}
570-
571-
function makeAppMenu() {
572-
const menu = instantiateAppMenu();
573-
electron.Menu.setApplicationMenu(menu);
574-
}
575-
576513
function hideWindowWithCatch(window: WaveBrowserWindow) {
577514
if (window == null) {
578515
return;
@@ -649,37 +586,6 @@ process.on("uncaughtException", (error) => {
649586
electronApp.quit();
650587
});
651588

652-
async function relaunchBrowserWindows(): Promise<void> {
653-
console.log("relaunchBrowserWindows");
654-
setGlobalIsRelaunching(true);
655-
const windows = getAllWaveWindows();
656-
for (const window of windows) {
657-
console.log("relaunch -- closing window", window.waveWindowId);
658-
window.close();
659-
}
660-
setGlobalIsRelaunching(false);
661-
662-
const clientData = await services.ClientService.GetClientData();
663-
const fullConfig = await services.FileService.GetFullConfig();
664-
const wins: WaveBrowserWindow[] = [];
665-
for (const windowId of clientData.windowids.slice().reverse()) {
666-
const windowData: WaveWindow = await services.WindowService.GetWindow(windowId);
667-
if (windowData == null) {
668-
console.log("relaunch -- window data not found, closing window", windowId);
669-
await services.WindowService.CloseWindow(windowId, true);
670-
continue;
671-
}
672-
console.log("relaunch -- creating window", windowId, windowData);
673-
const win = await createBrowserWindow(windowData, fullConfig, { unamePlatform });
674-
wins.push(win);
675-
}
676-
for (const win of wins) {
677-
await win.waveReadyPromise;
678-
console.log("show window", win.waveWindowId);
679-
win.show();
680-
}
681-
}
682-
683589
async function appMain() {
684590
// Set disableHardwareAcceleration as early as possible, if required.
685591
const launchSettings = getLaunchSettings();
@@ -694,7 +600,6 @@ async function appMain() {
694600
electronApp.quit();
695601
return;
696602
}
697-
makeAppMenu();
698603
try {
699604
await runWaveSrv(handleWSEvent);
700605
} catch (e) {
@@ -715,6 +620,7 @@ async function appMain() {
715620
} catch (e) {
716621
console.log("error initializing wshrpc", e);
717622
}
623+
makeAppMenu();
718624
await configureAutoUpdater();
719625
setGlobalIsStarting(false);
720626
if (fullConfig?.settings?.["window:maxtabcachesize"] != null) {

emain/log.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import path from "path";
2+
import { format } from "util";
3+
import winston from "winston";
4+
import { getWaveDataDir, isDev } from "./platform";
5+
6+
const oldConsoleLog = console.log;
7+
8+
const loggerTransports: winston.transport[] = [
9+
new winston.transports.File({ filename: path.join(getWaveDataDir(), "waveapp.log"), level: "info" }),
10+
];
11+
if (isDev) {
12+
loggerTransports.push(new winston.transports.Console());
13+
}
14+
const loggerConfig = {
15+
level: "info",
16+
format: winston.format.combine(
17+
winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss.SSS" }),
18+
winston.format.printf((info) => `${info.timestamp} ${info.message}`)
19+
),
20+
transports: loggerTransports,
21+
};
22+
const logger = winston.createLogger(loggerConfig);
23+
function log(...msg: any[]) {
24+
try {
25+
logger.info(format(...msg));
26+
} catch (e) {
27+
oldConsoleLog(...msg);
28+
}
29+
}
30+
31+
export { log };

0 commit comments

Comments
 (0)