app/src/electron/app-window.ts
crusader 7c915f6d5f ing
2018-08-17 15:11:55 +09:00

297 lines
8.1 KiB
TypeScript

import { BrowserWindow, ipcMain, Menu, app, dialog } from 'electron';
import * as path from 'path';
import * as URL from 'url';
import { Subscription, PartialObserver } from 'rxjs';
import { Emitter } from '@overflow/core/emitter';
import { encodePathAsUrl } from '@overflow/core/path';
import { registerWindowStateChangedEvents } from '../commons/model/window-state';
import { URLActionType } from '@overflow/core/parse-app-url';
import { now } from '@overflow/core/now';
import { MenuEvent } from '../commons/type';
import { LaunchState } from '../commons/model';
import { menuFromElectronMenu } from '../commons/model/app-menu';
let windowStateKeeper: any | null = null;
export class AppWindow {
private window: Electron.BrowserWindow;
private emitter = new Emitter();
private _loadTime: number | null = null;
private _rendererReadyTime: number | null = null;
private minWidth = 960;
private minHeight = 660;
public constructor() {
if (!windowStateKeeper) {
// `electron-window-state` requires Electron's `screen` module, which can
// only be required after the app has emitted `ready`. So require it
// lazily.
windowStateKeeper = require('electron-window-state');
}
const savedWindowState = windowStateKeeper({
defaultWidth: this.minWidth,
defaultHeight: this.minHeight,
});
const windowOptions: Electron.BrowserWindowConstructorOptions = {
x: savedWindowState.x,
y: savedWindowState.y,
width: savedWindowState.width,
height: savedWindowState.height,
minWidth: this.minWidth,
minHeight: this.minHeight,
show: false,
// This fixes subpixel aliasing on Windows
// See https://github.com/atom/atom/commit/683bef5b9d133cb194b476938c77cc07fd05b972
backgroundColor: '#fff',
webPreferences: {
// Disable auxclick event
// See https://developers.google.com/web/updates/2016/10/auxclick
disableBlinkFeatures: 'Auxclick',
// Enable, among other things, the ResizeObserver
experimentalFeatures: true,
},
acceptFirstMouse: true,
};
if (__DARWIN__) {
windowOptions.titleBarStyle = 'hidden';
} else if (__WIN32__) {
windowOptions.frame = false;
} else if (__LINUX__) {
windowOptions.icon = path.join(__dirname, 'static', 'icon-logo.png');
}
this.window = new BrowserWindow(windowOptions);
savedWindowState.manage(this.window);
let quitting = false;
app.on('before-quit', () => {
quitting = true;
});
ipcMain.on('will-quit', (event: Electron.IpcMessageEvent) => {
quitting = true;
event.returnValue = true;
});
// on macOS, when the user closes the window we really just hide it. This
// lets us activate quickly and keep all our interesting logic in the
// renderer.
if (__DARWIN__) {
this.window.on('close', e => {
if (!quitting) {
e.preventDefault();
Menu.sendActionToFirstResponder('hide:');
}
});
}
}
public load() {
let startLoad = 0;
// We only listen for the first of the loading events to avoid a bug in
// Electron/Chromium where they can sometimes fire more than once. See
// See
// https://github.com/desktop/desktop/pull/513#issuecomment-253028277. This
// shouldn't really matter as in production builds loading _should_ only
// happen once.
this.window.webContents.once('did-start-loading', () => {
this._rendererReadyTime = null;
this._loadTime = null;
startLoad = now();
});
this.window.webContents.once('did-finish-load', () => {
if (process.env.NODE_ENV === 'development') {
this.window.webContents.openDevTools();
}
this._loadTime = now() - startLoad;
this.maybeEmitDidLoad();
});
this.window.webContents.on('did-finish-load', () => {
this.window.webContents.setVisualZoomLevelLimits(1, 1);
});
this.window.webContents.on('did-fail-load', () => {
this.window.webContents.openDevTools();
this.window.show();
});
// TODO: This should be scoped by the window.
ipcMain.once(
'renderer-ready',
(event: Electron.IpcMessageEvent, readyTime: number) => {
this._rendererReadyTime = readyTime;
this.maybeEmitDidLoad();
}
);
this.window.on('focus', () => this.window.webContents.send('focus'));
this.window.on('blur', () => this.window.webContents.send('blur'));
registerWindowStateChangedEvents(this.window);
// this.window.loadURL(encodePathAsUrl(__dirname, 'index.html'));
const indexUrl = URL.format({
pathname: path.join('//localhost:4200'),
protocol: 'http:',
slashes: true
});
this.window.loadURL(indexUrl);
}
/**
* Emit the `onDidLoad` event if the page has loaded and the renderer has
* signalled that it's ready.
*/
private maybeEmitDidLoad() {
if (!this.rendererLoaded) {
return;
}
this.emitter.emit('did-load');
}
/** Is the page loaded and has the renderer signalled it's ready? */
private get rendererLoaded(): boolean {
return !!this.loadTime && !!this.rendererReadyTime;
}
public onClose(fn: () => void) {
this.window.on('closed', fn);
}
/**
* Register a function to call when the window is done loading. At that point
* the page has loaded and the renderer has signalled that it is ready.
*/
public onDidLoad(fn: (data?: any) => void): Subscription {
return this.emitter.listen('did-load', fn);
}
public isMinimized() {
return this.window.isMinimized();
}
/** Is the window currently visible? */
public isVisible() {
return this.window.isVisible();
}
public restore() {
this.window.restore();
}
public focus() {
this.window.focus();
}
/** Show the window. */
public show() {
this.window.show();
}
/** Send the menu event to the renderer. */
public sendMenuEvent(name: MenuEvent) {
this.show();
this.window.webContents.send('menu-event', { name });
}
/** Send the URL action to the renderer. */
public sendURLAction(action: URLActionType) {
this.show();
this.window.webContents.send('url-action', { action });
}
/** Send the app launch timing stats to the renderer. */
public sendLaunchTimingStats(state: LaunchState) {
this.window.webContents.send('launch-timing-stats', { state });
}
/** Send the app menu to the renderer. */
public sendAppMenu() {
const appMenu = Menu.getApplicationMenu();
if (appMenu) {
const menu = menuFromElectronMenu(appMenu);
this.window.webContents.send('app-menu', { menu });
}
}
/** Send a certificate error to the renderer. */
public sendCertificateError(
certificate: Electron.Certificate,
error: string,
url: string
) {
this.window.webContents.send('certificate-error', {
certificate,
error,
url,
});
}
public showCertificateTrustDialog(
certificate: Electron.Certificate,
message: string
) {
// The Electron type definitions don't include `showCertificateTrustDialog`
// yet.
const d = dialog as any;
d.showCertificateTrustDialog(
this.window,
{ certificate, message },
() => { }
);
}
/** Report the exception to the renderer. */
public sendException(error: Error) {
// `Error` can't be JSONified so it doesn't transport nicely over IPC. So
// we'll just manually copy the properties we care about.
const friendlyError = {
stack: error.stack,
message: error.message,
name: error.name,
};
this.window.webContents.send('main-process-exception', friendlyError);
}
/**
* Get the time (in milliseconds) spent loading the page.
*
* This will be `null` until `onDidLoad` is called.
*/
public get loadTime(): number | null {
return this._loadTime;
}
/**
* Get the time (in milliseconds) elapsed from the renderer being loaded to it
* signaling it was ready.
*
* This will be `null` until `onDidLoad` is called.
*/
public get rendererReadyTime(): number | null {
return this._rendererReadyTime;
}
public destroy() {
this.window.destroy();
}
}