From aac4eb1a3efc02e2a462eccbfae8fdf6e776ddca Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 7 Oct 2020 21:09:54 +0300 Subject: [PATCH] allow to close main window and re-open from dock or tray icon Signed-off-by: Roman --- src/main/index.ts | 64 ++++++++------ src/main/menu.ts | 14 ++- src/main/tray.ts | 30 +++---- src/main/window-manager.ts | 171 ++++++++++++++++++++++++------------- 4 files changed, 179 insertions(+), 100 deletions(-) diff --git a/src/main/index.ts b/src/main/index.ts index bdcd5c1f44..8e09e6dbb6 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -20,23 +20,24 @@ import { tracker } from "../common/tracker"; import logger from "./logger" const workingDir = path.join(app.getPath("appData"), appName); +let proxyPort: number; +let proxyServer: LensProxy; +let windowManager: WindowManager; +let clusterManager: ClusterManager; + app.setName(appName); if (!process.env.CICD) { app.setPath("userData", workingDir); } -let windowManager: WindowManager; -let clusterManager: ClusterManager; -let proxyServer: LensProxy; - mangleProxyEnv() if (app.commandLine.getSwitchValue("proxy-server") !== "") { process.env.HTTPS_PROXY = app.commandLine.getSwitchValue("proxy-server") } -async function main() { - await shellSync(); +app.on("ready", async () => { logger.info(`🚀 Starting Lens from "${workingDir}"`) + await shellSync(); tracker.event("app", "start"); const updater = new AppUpdater() @@ -44,23 +45,22 @@ async function main() { registerFileProtocol("static", __static); - // find free port - let proxyPort: number - try { - proxyPort = await getFreePort() - } catch (error) { - logger.error(error) - dialog.showErrorBox("Lens Error", "Could not find a free port for the cluster proxy") - app.quit(); - } - - // preload configuration from stores + // preload isomorphic stores await Promise.all([ userStore.load(), clusterStore.load(), workspaceStore.load(), ]); + // find free port + try { + proxyPort = await getFreePort() + } catch (error) { + logger.error(error) + dialog.showErrorBox("Lens Error", "Could not find a free port for the cluster proxy") + app.exit(); + } + // create cluster manager clusterManager = new ClusterManager(proxyPort); @@ -70,18 +70,30 @@ async function main() { } catch (error) { logger.error(`Could not start proxy (127.0.0:${proxyPort}): ${error.message}`) dialog.showErrorBox("Lens Error", `Could not start proxy (127.0.0:${proxyPort}): ${error.message || "unknown error"}`) - app.quit(); + app.exit(); } - // create window manager and open app windowManager = new WindowManager(proxyPort); -} +}); -app.on("ready", main); +app.on("activate", (event, hasVisibleWindows) => { + logger.info('APP:ACTIVATE', { hasVisibleWindows }) + if (!hasVisibleWindows) { + windowManager.initMainWindow(); + } +}); -app.on("will-quit", async (event) => { - event.preventDefault(); // To allow mixpanel sending to be executed - if (proxyServer) proxyServer.close() - if (clusterManager) clusterManager.stop() - app.exit(); +// Quit app on Cmd+Q (MacOS) +app.on("will-quit", (event) => { + logger.info('APP:QUIT'); + event.preventDefault(); // prevent app's default shutdown (e.g. required for mixpanel, GA, etc.) + + if (userStore.preferences.trayEnabled) { + return; // with tray the app remains open + } else { + windowManager?.destroy(); + clusterManager?.stop(); + proxyServer?.close(); + app.exit(); // force quite + } }) diff --git a/src/main/menu.ts b/src/main/menu.ts index 9fab6947f1..64b111046b 100644 --- a/src/main/menu.ts +++ b/src/main/menu.ts @@ -76,7 +76,15 @@ export function buildMenu(windowManager: WindowManager) { { role: 'hideOthers' }, { role: 'unhide' }, { type: 'separator' }, - { role: 'quit' } + { + label: 'Force Quit', + accelerator: 'Cmd+Shift+Q', + click() { + app.exit(0); // force quit since might be blocked within app.on("will-quit") + } + }, + { type: 'separator' }, + { role: 'quit' }, ] }; @@ -118,7 +126,9 @@ export function buildMenu(windowManager: WindowManager) { }, { type: 'separator' }, { role: 'quit' } - ]) + ]), + { type: 'separator' }, + { role: 'close' } // close current window ] }; mt.push(fileMenu) diff --git a/src/main/tray.ts b/src/main/tray.ts index 49316f38fb..b081f6d055 100644 --- a/src/main/tray.ts +++ b/src/main/tray.ts @@ -82,14 +82,13 @@ export function createTrayMenu(windowManager: WindowManager): Menu { label: "About Lens", click() { // note: argument[1] (browserWindow) not available when app is not focused / hidden - windowManager.bringToTop(); - showAbout(windowManager.mainView); + windowManager.runInContextWindow(showAbout); }, }, { label: "Preferences", - click() { - windowManager.bringToTop(); + async click() { + await windowManager.ensureMainWindow() windowManager.navigate(preferencesURL()); }, }, @@ -107,10 +106,10 @@ export function createTrayMenu(windowManager: WindowManager): Menu { return { label: `${online ? '✓' : '\x20'.repeat(3)/*offset*/}${label}`, toolTip: clusterId, - click() { + async click() { workspaceStore.setActive(workspace); clusterStore.setActive(clusterId); - windowManager.bringToTop(); + await windowManager.ensureMainWindow() windowManager.navigate(clusterViewURL({ params: { clusterId } })); } } @@ -120,15 +119,16 @@ export function createTrayMenu(windowManager: WindowManager): Menu { }, { label: "Check for updates", - async click() { - const result = await AppUpdater.checkForUpdates(); - if (!result) { - windowManager.bringToTop(); - dialog.showMessageBoxSync({ - message: "No updates available", - type: "info", - }) - } + click() { + windowManager.runInContextWindow(async window => { + const result = await AppUpdater.checkForUpdates(); + if (!result) { + dialog.showMessageBoxSync(window, { + message: "No updates available", + type: "info", + }) + } + }) }, }, ]); diff --git a/src/main/window-manager.ts b/src/main/window-manager.ts index cd2a1af98f..8bcdd8d6f9 100644 --- a/src/main/window-manager.ts +++ b/src/main/window-manager.ts @@ -2,81 +2,148 @@ import type { ClusterId } from "../common/cluster-store"; import { clusterStore } from "../common/cluster-store"; import { userStore } from "../common/user-store"; import { observable, reaction } from "mobx"; -import { BrowserWindow, dialog, ipcMain, shell, webContents } from "electron" +import { app, BrowserWindow, dialog, ipcMain, shell, webContents } from "electron" import windowStateKeeper from "electron-window-state" import { initMenu } from "./menu"; import { initTray } from "./tray"; export class WindowManager { - public mainView: BrowserWindow; + protected mainWindow: BrowserWindow; protected splashWindow: BrowserWindow; + protected trayWindow: BrowserWindow; protected windowState: windowStateKeeper.State; protected disposers: Record = {}; @observable activeClusterId: ClusterId; constructor(protected proxyPort: number) { - // Manage main window size and position with state persistence - this.windowState = windowStateKeeper({ - defaultHeight: 900, - defaultWidth: 1440, - }); - - const { width, height, x, y } = this.windowState; - this.mainView = new BrowserWindow({ - x, y, width, height, - show: false, - minWidth: 700, // accommodate 800 x 600 display minimum - minHeight: 500, // accommodate 800 x 600 display minimum - titleBarStyle: "hidden", - backgroundColor: "#1e2124", - webPreferences: { - nodeIntegration: true, - nodeIntegrationInSubFrames: true, - enableRemoteModule: true, - }, - }); - this.windowState.manage(this.mainView); - - // open external links in default browser (target=_blank, window.open) - this.mainView.webContents.on("new-window", (event, url) => { - event.preventDefault(); - shell.openExternal(url); - }); - - // track visible cluster from ui - ipcMain.on("cluster-view:current-id", (event, clusterId: ClusterId) => { - this.activeClusterId = clusterId; - }); - - // load & show app - this.showMain(); - this.initMenus(); + this.bindEvents(); + this.initMenu(); + this.initTray(); + this.initMainWindow(); } - protected async initMenus() { + get mainUrl() { + return `http://localhost:${this.proxyPort}` + } + + async initMainWindow(showSplash = true) { + // Manage main window size and position with state persistence + if (!this.windowState) { + this.windowState = windowStateKeeper({ + defaultHeight: 900, + defaultWidth: 1440, + }); + } + if (!this.mainWindow) { + const { width, height, x, y } = this.windowState; + this.mainWindow = new BrowserWindow({ + x, y, width, height, + show: false, + minWidth: 700, // accommodate 800 x 600 display minimum + minHeight: 500, // accommodate 800 x 600 display minimum + titleBarStyle: "hidden", + backgroundColor: "#1e2124", + webPreferences: { + nodeIntegration: true, + nodeIntegrationInSubFrames: true, + enableRemoteModule: true, + }, + }); + this.windowState.manage(this.mainWindow); + + // open external links in default browser (target=_blank, window.open) + this.mainWindow.webContents.on("new-window", (event, url) => { + event.preventDefault(); + shell.openExternal(url); + }); + + // clean up + this.mainWindow.on("closed", () => { + this.windowState.unmanage(); + this.mainWindow = null; + this.splashWindow = null; + this.trayWindow = null; + }) + } + try { + if (showSplash) await this.showSplash(); + await this.mainWindow.loadURL(this.mainUrl); + this.mainWindow.show(); + this.splashWindow.hide() + } catch (err) { + dialog.showErrorBox("ERROR!", err.toString()) + } + } + + protected async initMenu() { this.disposers.menuAutoUpdater = initMenu(this); + } + + protected async initTray() { this.disposers.trayAutoBind = reaction(() => userStore.preferences.trayEnabled, async isEnabled => { if (isEnabled) { + this.ensureTrayWindow(); this.disposers.trayAutoUpdater = await initTray(this); } else if (this.disposers.trayAutoUpdater) { this.disposers.trayAutoUpdater(); - this.disposers.trayAutoUpdater = null; + this.trayWindow.destroy(); + this.trayWindow = null; } }, { fireImmediately: true }); } - bringToTop() { - this.mainView.show(); + protected bindEvents() { + // track visible cluster from ui + ipcMain.on("cluster-view:current-id", (event, clusterId: ClusterId) => { + this.activeClusterId = clusterId; + }); + } + + async ensureMainWindow({ bringToTop = true, showSplash = true } = {}) { + if (!this.mainWindow) { + await this.initMainWindow(showSplash); + } + if (bringToTop) { + this.mainWindow.show(); + } else { + this.mainWindow.hide(); + } + } + + ensureTrayWindow() { + if (!this.trayWindow) { + this.trayWindow = new BrowserWindow({ + show: false, + transparent: true, + titleBarStyle: "hidden", + }); + } + } + + async runInContextWindow(callback: (window: BrowserWindow) => any | Promise) { + const isMainVisible = this.mainWindow?.isVisible(); // is open, but might be not on the top + if (isMainVisible) { + this.mainWindow.show(); + await callback(this.mainWindow); + } else { + this.ensureTrayWindow(); + if (this.mainWindow) this.mainWindow.hide(); + this.trayWindow.show(); + await callback(this.trayWindow); + this.trayWindow.hide(); + if (this.mainWindow) this.mainWindow.hide(); + app.hide(); + } } sendToView({ channel, frameId, data = [] }: { channel: string, frameId?: number, data?: any[] }) { if (frameId) { - this.mainView.webContents.sendToFrame(frameId, channel, ...data); + this.mainWindow.webContents.sendToFrame(frameId, channel, ...data); } else { - this.mainView.webContents.send(channel, ...data); + this.mainWindow.webContents.send(channel, ...data); } } @@ -97,17 +164,6 @@ export class WindowManager { } } - async showMain() { - try { - await this.showSplash(); - await this.mainView.loadURL(`http://localhost:${this.proxyPort}`) - this.mainView.show(); - this.splashWindow.close(); - } catch (err) { - dialog.showErrorBox("ERROR!", err.toString()) - } - } - async showSplash() { if (!this.splashWindow) { this.splashWindow = new BrowserWindow({ @@ -128,9 +184,10 @@ export class WindowManager { } destroy() { - this.windowState.unmanage(); + this.mainWindow.destroy(); this.splashWindow.destroy(); - this.mainView.destroy(); + this.mainWindow = null; + this.splashWindow = null; Object.entries(this.disposers).forEach(([name, dispose]) => { dispose(); delete this.disposers[name]