refactor: main window on events

This commit is contained in:
tiddlygit-test 2021-01-17 19:03:58 +08:00
parent 288415f31b
commit adfb4cd0db
4 changed files with 289 additions and 318 deletions

View file

@ -102,6 +102,7 @@ export interface WindowMeta extends Record<WindowNames, Record<string, unknown>
[WindowNames.codeInjection]: { codeInjectionType?: CodeInjectionType };
[WindowNames.editWorkspace]: { workspaceID?: string };
[WindowNames.openUrlWith]: { incomingUrl?: string };
[WindowNames.main]: { forceClose?: boolean };
}
/**

View file

@ -0,0 +1,106 @@
/* eslint-disable @typescript-eslint/strict-boolean-expressions */
import { Menu, Tray, ipcMain, nativeImage } from 'electron';
import windowStateKeeper from 'electron-window-state';
import { menubar, Menubar } from 'menubar';
import path from 'path';
import { REACT_PATH, isDev as isDevelopment, buildResourcePath } from '@services/constants/paths';
export default async function handleAttachToMenuBar(): Promise<Menubar> {
const menubarWindowState = windowStateKeeper({
file: 'window-state-menubar.json',
defaultWidth: 400,
defaultHeight: 400,
});
// setImage after Tray instance is created to avoid
// "Segmentation fault (core dumped)" bug on Linux
// https://github.com/electron/electron/issues/22137#issuecomment-586105622
// https://github.com/atomery/translatium/issues/164
const tray = new Tray(nativeImage.createEmpty());
// icon template is not supported on Windows & Linux
const iconPath = path.resolve(buildResourcePath, process.platform === 'darwin' ? 'menubarTemplate.png' : 'menubar.png');
tray.setImage(iconPath);
const menuBar = menubar({
index: REACT_PATH,
tray,
preloadWindow: true,
tooltip: 'TiddlyGit',
browserWindow: {
x: menubarWindowState.x,
y: menubarWindowState.y,
width: menubarWindowState.width,
height: menubarWindowState.height,
minHeight: 100,
minWidth: 250,
webPreferences: {
nodeIntegration: false,
enableRemoteModule: true,
webSecurity: !isDevelopment,
contextIsolation: true,
preload: path.join(__dirname, '..', 'preload', 'menubar.js'),
},
},
});
menuBar.on('after-create-window', () => {
if (menuBar.window) {
menubarWindowState.manage(menuBar.window);
menuBar.window.on('focus', () => {
const view = menuBar.window?.getBrowserView();
if (view?.webContents) {
view.webContents.focus();
}
});
}
});
return await new Promise<Menubar>((resolve) => {
menuBar.on('ready', () => {
menuBar.tray.on('right-click', () => {
// TODO: restore updater options here
const contextMenu = Menu.buildFromTemplate([
{
label: 'Open TiddlyGit',
click: async () => await menuBar.showWindow(),
},
{
type: 'separator',
},
{
label: 'About TiddlyGit',
click: () => ipcMain.emit('request-show-about-window'),
},
{ type: 'separator' },
{
label: 'Preferences...',
click: () => ipcMain.emit('request-show-preferences-window'),
},
{ type: 'separator' },
{
label: 'Notifications...',
click: () => ipcMain.emit('request-show-notifications-window'),
},
{ type: 'separator' },
{
label: 'Clear Browsing Data...',
click: () => ipcMain.emit('request-clear-browsing-data'),
},
{ type: 'separator' },
{
role: 'quit',
click: () => {
menuBar.app.quit();
},
},
]);
menuBar.tray.popUpContextMenu(contextMenu);
});
resolve(menuBar);
});
});
}

View file

@ -1,18 +1,21 @@
/* eslint-disable @typescript-eslint/consistent-type-assertions */
import { BrowserWindow, ipcMain, dialog, app, App, remote, clipboard } from 'electron';
import { BrowserWindow, ipcMain, dialog, app, App, remote, clipboard, BrowserWindowConstructorOptions } from 'electron';
import isDevelopment from 'electron-is-dev';
import { injectable, inject } from 'inversify';
import windowStateKeeper, { State as windowStateKeeperState } from 'electron-window-state';
import { IBrowserViewMetaData, WindowNames, windowDimension, WindowMeta, CodeInjectionType } from '@services/windows/WindowProperties';
import serviceIdentifiers from '@services/serviceIdentifier';
import { Preference } from '@services/preferences';
import { Workspace } from '@services/workspaces';
import { WorkspaceView } from '@services/workspacesView';
import { MenuService } from '@services/menu';
import { Channels, WindowChannel, MetaDataChannel } from '@/constants/channels';
import i18n from '@services/libs/i18n';
import getViewBounds from '@services/libs/get-view-bounds';
import getFromRenderer from '@services/libs/getFromRenderer';
import handleAttachToMenuBar from './handleAttachToMenuBar';
@injectable()
export class Window {
@ -22,6 +25,7 @@ export class Window {
constructor(
@inject(serviceIdentifiers.Preference) private readonly preferenceService: Preference,
@inject(serviceIdentifiers.Workspace) private readonly workspaceService: Workspace,
@inject(serviceIdentifiers.WorkspaceView) private readonly workspaceViewService: WorkspaceView,
@inject(serviceIdentifiers.MenuService) private readonly menuService: MenuService,
) {
this.initIPCHandlers();
@ -29,53 +33,6 @@ export class Window {
}
initIPCHandlers(): void {
ipcMain.handle('request-go-home', async (_event: Electron.IpcMainInvokeEvent, windowName: WindowNames = WindowNames.main) => {
const win = this.get(windowName);
const contents = win?.getBrowserView()?.webContents;
const activeWorkspace = this.workspaceService.getActiveWorkspace();
if (contents !== undefined && activeWorkspace !== undefined && win !== undefined) {
await contents.loadURL(activeWorkspace.homeUrl);
contents.send('update-can-go-back', contents.canGoBack());
contents.send('update-can-go-forward', contents.canGoForward());
}
});
ipcMain.handle('request-go-back', (_event: Electron.IpcMainInvokeEvent, windowName: WindowNames = WindowNames.main) => {
const win = this.get(windowName);
const contents = win?.getBrowserView()?.webContents;
if (contents?.canGoBack() === true) {
contents.goBack();
contents.send('update-can-go-back', contents.canGoBack());
contents.send('update-can-go-forward', contents.canGoForward());
}
});
ipcMain.handle('request-go-forward', (_event: Electron.IpcMainInvokeEvent, windowName: WindowNames = WindowNames.main) => {
const win = this.get(windowName);
const contents = win?.getBrowserView()?.webContents;
if (contents?.canGoForward() === true) {
contents.goForward();
contents.send('update-can-go-back', contents.canGoBack());
contents.send('update-can-go-forward', contents.canGoForward());
}
});
ipcMain.handle('request-reload', (_event: Electron.IpcMainInvokeEvent, windowName: WindowNames = WindowNames.main) => {
const win = this.get(windowName);
win?.getBrowserView()?.webContents?.reload();
});
ipcMain.handle('request-show-message-box', (_event, message: Electron.MessageBoxOptions['message'], type?: Electron.MessageBoxOptions['type']) => {
const mainWindow = this.get(WindowNames.main);
if (mainWindow !== undefined) {
dialog
.showMessageBox(mainWindow, {
type: type ?? 'error',
message,
buttons: ['OK'],
cancelId: 0,
defaultId: 0,
})
.catch(console.log);
}
});
ipcMain.handle(WindowChannel.requestShowRequireRestartDialog, () => {
const availableWindowToShowDialog = this.get(WindowNames.preferences) ?? this.get(WindowNames.main);
if (availableWindowToShowDialog !== undefined) {
@ -180,16 +137,47 @@ export class Window {
const existedWindow = this.windows[windowName];
const existedWindowMeta = this.windowMeta[windowName];
if (existedWindow !== undefined) {
// TODO: handle this menubar logic
// if (attachToMenubar) {
// if (menuBar == undefined) {
// createAsync();
// } else {
// menuBar.on('ready', () => {
// menuBar.showWindow();
// });
// }
// }
if (recreate === true || (typeof recreate === 'function' && existedWindowMeta !== undefined && recreate(existedWindowMeta))) {
existedWindow.close();
} else {
return existedWindow.show();
}
}
const attachToMenubar: boolean = this.preferenceService.get('attachToMenubar');
let mainWindowConfig: Partial<BrowserWindowConstructorOptions> = {};
let mainWindowState: windowStateKeeperState | undefined;
const isMainWindow = windowName === WindowNames.main;
if (isMainWindow) {
if (attachToMenubar) {
await handleAttachToMenuBar();
}
mainWindowState = windowStateKeeper({
defaultWidth: windowDimension[WindowNames.main].width,
defaultHeight: windowDimension[WindowNames.main].height,
});
mainWindowConfig = {
x: mainWindowState.x,
y: mainWindowState.y,
width: mainWindowState.width,
height: mainWindowState.height,
};
}
const newWindow = new BrowserWindow({
...windowDimension[windowName],
...mainWindowConfig,
resizable: false,
maximizable: false,
minimizable: false,
@ -205,15 +193,107 @@ export class Window {
},
parent: windowName === WindowNames.main || attachToMenubar ? undefined : this.get(WindowNames.main),
});
newWindow.setMenuBarVisibility(false);
this.windows[windowName] = newWindow;
if (isMainWindow) {
mainWindowState?.manage(newWindow);
await this.registerMainWindowListeners(newWindow);
} else {
newWindow.setMenuBarVisibility(false);
}
newWindow.on('closed', () => {
this.windows[windowName] = undefined;
});
this.windows[windowName] = newWindow;
return newWindow.loadURL(MAIN_WINDOW_WEBPACK_ENTRY);
}
private async registerMainWindowListeners(newWindow: BrowserWindow): Promise<void> {
const { wasOpenedAsHidden } = app.getLoginItemSettings();
// Enable swipe to navigate
const swipeToNavigate = this.preferenceService.get('swipeToNavigate');
if (swipeToNavigate) {
const mainWindow = this.get(WindowNames.main);
if (mainWindow === undefined) return;
mainWindow.on('swipe', (_event, direction) => {
const view = mainWindow?.getBrowserView();
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
if (view) {
if (direction === 'left') {
view.webContents.goBack();
} else if (direction === 'right') {
view.webContents.goForward();
}
}
});
}
// Hide window instead closing on macos
newWindow.on('close', (event) => {
const mainWindow = this.get(WindowNames.main);
if (mainWindow === undefined) return;
if (process.platform === 'darwin' && this.getWindowMeta(WindowNames.main).forceClose !== true) {
event.preventDefault();
// https://github.com/electron/electron/issues/6033#issuecomment-242023295
if (mainWindow.isFullScreen()) {
mainWindow.once('leave-full-screen', () => {
const mainWindow = this.get(WindowNames.main);
if (mainWindow !== undefined) {
mainWindow.hide();
}
});
mainWindow.setFullScreen(false);
} else {
mainWindow.hide();
}
}
});
newWindow.on('focus', () => {
const mainWindow = this.get(WindowNames.main);
if (mainWindow === undefined) return;
const view = mainWindow?.getBrowserView();
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
view?.webContents?.focus();
});
newWindow.once('ready-to-show', () => {
const mainWindow = this.get(WindowNames.main);
if (mainWindow === undefined) return;
if (!wasOpenedAsHidden) {
mainWindow.show();
}
// calling this to redundantly setBounds BrowserView
// after the UI is fully loaded
// if not, BrowserView mouseover event won't work correctly
// https://github.com/atomery/webcatalog/issues/812
this.workspaceViewService.realignActiveWorkspace();
});
newWindow.on('enter-full-screen', () => {
const mainWindow = this.get(WindowNames.main);
if (mainWindow === undefined) return;
mainWindow?.webContents.send('is-fullscreen-updated', true);
this.workspaceViewService.realignActiveWorkspace();
});
newWindow.on('leave-full-screen', () => {
const mainWindow = this.get(WindowNames.main);
if (mainWindow === undefined) return;
mainWindow?.webContents.send('is-fullscreen-updated', false);
this.workspaceViewService.realignActiveWorkspace();
});
return await new Promise<void>((resolve) => {
const mainWindow = this.get(WindowNames.main);
if (mainWindow === undefined) return;
// ensure redux is loaded first
// if not, redux might not be able catch changes sent from ipcMain
mainWindow.webContents.once('did-stop-loading', () => {
resolve();
});
});
}
public setWindowMeta<N extends WindowNames>(windowName: N, meta: WindowMeta[N]): void {
this.windowMeta[windowName] = meta;
}
@ -238,6 +318,57 @@ export class Window {
});
};
public async goHome(windowName: WindowNames = WindowNames.main): Promise<void> {
const win = this.get(windowName);
const contents = win?.getBrowserView()?.webContents;
const activeWorkspace = this.workspaceService.getActiveWorkspace();
if (contents !== undefined && activeWorkspace !== undefined && win !== undefined) {
await contents.loadURL(activeWorkspace.homeUrl);
contents.send('update-can-go-back', contents.canGoBack());
contents.send('update-can-go-forward', contents.canGoForward());
}
}
public goBack(windowName: WindowNames = WindowNames.main): void {
const win = this.get(windowName);
const contents = win?.getBrowserView()?.webContents;
if (contents?.canGoBack() === true) {
contents.goBack();
contents.send('update-can-go-back', contents.canGoBack());
contents.send('update-can-go-forward', contents.canGoForward());
}
}
public goForward(windowName: WindowNames = WindowNames.main): void {
const win = this.get(windowName);
const contents = win?.getBrowserView()?.webContents;
if (contents?.canGoForward() === true) {
contents.goForward();
contents.send('update-can-go-back', contents.canGoBack());
contents.send('update-can-go-forward', contents.canGoForward());
}
}
public reload(windowName: WindowNames = WindowNames.main): void {
const win = this.get(windowName);
win?.getBrowserView()?.webContents?.reload();
}
public showMessageBox(message: Electron.MessageBoxOptions['message'], type?: Electron.MessageBoxOptions['type']): void {
const mainWindow = this.get(WindowNames.main);
if (mainWindow !== undefined) {
dialog
.showMessageBox(mainWindow, {
type: type ?? 'error',
message,
buttons: ['OK'],
cancelId: 0,
defaultId: 0,
})
.catch(console.log);
}
}
private registerMenu(): void {
this.menuService.insertMenu(
'window',
@ -368,6 +499,7 @@ export class Window {
]);
if (process.platform === 'darwin') {
// TODO: restore updater options here
this.menuService.insertMenu('TiddlyGit', [
{
label: i18n.t('ContextMenu.About'),

View file

@ -1,268 +0,0 @@
import { BrowserWindow, Menu, Tray, app, ipcMain, nativeImage } from 'electron';
import windowStateKeeper from 'electron-window-state';
import { menubar, Menubar } from 'menubar';
import path from 'path';
import { REACT_PATH, isDev as isDevelopment, buildResourcePath } from '@services/constants/paths';
import { getPreference } from '../libs/preferences';
import formatBytes from '../libs/format-bytes';
let win: BrowserWindow | undefined;
let menuBar: Menubar;
let attachToMenubar = false;
export const get = (): BrowserWindow | undefined => {
if (attachToMenubar && menuBar) return menuBar.window;
return win;
};
export const createAsync = async (): Promise<void> =>
await new Promise<void>((resolve) => {
attachToMenubar = getPreference('attachToMenubar');
if (attachToMenubar) {
const menubarWindowState = windowStateKeeper({
file: 'window-state-menubar.json',
defaultWidth: 400,
defaultHeight: 400,
});
// setImage after Tray instance is created to avoid
// "Segmentation fault (core dumped)" bug on Linux
// https://github.com/electron/electron/issues/22137#issuecomment-586105622
// https://github.com/atomery/translatium/issues/164
const tray = new Tray(nativeImage.createEmpty());
// icon template is not supported on Windows & Linux
const iconPath = path.resolve(buildResourcePath, process.platform === 'darwin' ? 'menubarTemplate.png' : 'menubar.png');
tray.setImage(iconPath);
menuBar = menubar({
index: REACT_PATH,
tray,
preloadWindow: true,
tooltip: 'TiddlyGit',
browserWindow: {
x: menubarWindowState.x,
y: menubarWindowState.y,
width: menubarWindowState.width,
height: menubarWindowState.height,
minHeight: 100,
minWidth: 250,
webPreferences: {
nodeIntegration: false,
enableRemoteModule: true,
webSecurity: !isDevelopment,
contextIsolation: true,
preload: path.join(__dirname, '..', 'preload', 'menubar.js'),
},
},
});
menuBar.on('after-create-window', () => {
if (menuBar.window) {
menubarWindowState.manage(menuBar.window);
menuBar.window.on('focus', () => {
const view = menuBar.window?.getBrowserView();
if (view && view.webContents) {
view.webContents.focus();
}
});
}
});
menuBar.on('ready', () => {
menuBar.tray.on('right-click', () => {
const updaterEnabled = process.env.SNAP == undefined && !process.mas && !process.windowsStore;
const updaterMenuItem = {
label: 'Check for Updates...',
click: () => ipcMain.emit('request-check-for-updates'),
visible: updaterEnabled,
enabled: true,
};
if (global.updaterObj && global.updaterObj.status === 'update-downloaded') {
updaterMenuItem.label = 'Restart to Apply Updates...';
} else if (global.updaterObj && global.updaterObj.status === 'update-available') {
updaterMenuItem.label = 'Downloading Updates...';
updaterMenuItem.enabled = false;
} else if (global.updaterObj && global.updaterObj.status === 'download-progress') {
const { transferred, total, bytesPerSecond } = global.updaterObj.info;
updaterMenuItem.label = `Downloading Updates (${formatBytes(transferred)}/${formatBytes(total)} at ${formatBytes(bytesPerSecond)}/s)...`;
updaterMenuItem.enabled = false;
} else if (global.updaterObj && global.updaterObj.status === 'checking-for-update') {
updaterMenuItem.label = 'Checking for Updates...';
updaterMenuItem.enabled = false;
}
const contextMenu = Menu.buildFromTemplate([
{
label: 'Open TiddlyGit',
click: () => menuBar.showWindow(),
},
{
type: 'separator',
},
{
label: 'About TiddlyGit',
click: () => ipcMain.emit('request-show-about-window'),
},
{ type: 'separator' },
updaterMenuItem,
{ type: 'separator' },
{
label: 'Preferences...',
click: () => ipcMain.emit('request-show-preferences-window'),
},
{ type: 'separator' },
{
label: 'Notifications...',
click: () => ipcMain.emit('request-show-notifications-window'),
},
{ type: 'separator' },
{
label: 'Clear Browsing Data...',
click: () => ipcMain.emit('request-clear-browsing-data'),
},
{ type: 'separator' },
{
role: 'quit',
click: () => {
menuBar.app.quit();
},
},
]);
menuBar.tray.popUpContextMenu(contextMenu);
});
resolve();
});
return;
}
const { wasOpenedAsHidden } = app.getLoginItemSettings();
const mainWindowState = windowStateKeeper({
defaultWidth: 1000,
defaultHeight: 768,
});
win = new BrowserWindow({
x: mainWindowState.x,
y: mainWindowState.y,
width: mainWindowState.width,
height: mainWindowState.height,
minHeight: 100,
minWidth: 350,
title: 'TiddlyGit',
titleBarStyle: 'hidden',
show: false,
// manually set dock icon for AppImage
// Snap icon is set correct already so no need to intervene
icon: process.platform === 'linux' && process.env.SNAP == undefined ? path.resolve(buildResourcePath, 'icon.png') : undefined,
webPreferences: {
nodeIntegration: false,
enableRemoteModule: true,
webSecurity: !isDevelopment,
contextIsolation: true,
// @ts-expect-error ts-migrate(2304) FIXME: Cannot find name 'MAIN_WINDOW_PRELOAD_WEBPACK_ENTR... Remove this comment to see the full error message
preload: MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY,
},
});
if (getPreference('hideMenuBar')) {
win.setMenuBarVisibility(false);
}
mainWindowState.manage(win);
// Enable swipe to navigate
const swipeToNavigate = getPreference('swipeToNavigate');
if (swipeToNavigate) {
win.on('swipe', (e, direction) => {
const view = win?.getBrowserView();
if (view) {
if (direction === 'left') {
view.webContents.goBack();
} else if (direction === 'right') {
view.webContents.goForward();
}
}
});
}
// Hide window instead closing on macos
win.on('close', (e) => {
// FIXME: use custom property
if (win && process.platform === 'darwin' && !(win as any).forceClose) {
e.preventDefault();
// https://github.com/electron/electron/issues/6033#issuecomment-242023295
if (win.isFullScreen()) {
win.once('leave-full-screen', () => {
if (win) {
win.hide();
}
});
win.setFullScreen(false);
} else {
win.hide();
}
}
});
win.on('closed', () => {
win = undefined;
});
win.on('focus', () => {
const view = win?.getBrowserView();
if (view && view.webContents) {
view.webContents.focus();
}
});
win.once('ready-to-show', () => {
if (win && !wasOpenedAsHidden) {
win.show();
}
// calling this to redundantly setBounds BrowserView
// after the UI is fully loaded
// if not, BrowserView mouseover event won't work correctly
// https://github.com/atomery/webcatalog/issues/812
this.workspaceViewService.realignActiveWorkspace();
});
win.on('enter-full-screen', () => {
win?.webContents.send('is-fullscreen-updated', true);
this.workspaceViewService.realignActiveWorkspace();
});
win.on('leave-full-screen', () => {
win?.webContents.send('is-fullscreen-updated', false);
this.workspaceViewService.realignActiveWorkspace();
});
// ensure redux is loaded first
// if not, redux might not be able catch changes sent from ipcMain
win.webContents.once('did-stop-loading', () => {
resolve();
});
// @ts-expect-error ts-migrate(2304) FIXME: Cannot find name 'MAIN_WINDOW_WEBPACK_ENTRY'.
void win.loadURL(MAIN_WINDOW_WEBPACK_ENTRY);
});
export const show = () => {
if (attachToMenubar) {
if (menuBar == undefined) {
createAsync();
} else {
menuBar.on('ready', () => {
menuBar.showWindow();
});
}
} else if (win == undefined) {
createAsync();
} else {
win.show();
}
};