diff options
Diffstat (limited to 'gui/src/main/window-controller.ts')
| -rw-r--r-- | gui/src/main/window-controller.ts | 253 |
1 files changed, 253 insertions, 0 deletions
diff --git a/gui/src/main/window-controller.ts b/gui/src/main/window-controller.ts new file mode 100644 index 0000000000..3822ecb23f --- /dev/null +++ b/gui/src/main/window-controller.ts @@ -0,0 +1,253 @@ +import { BrowserWindow, Display, screen, Tray, WebContents } from 'electron'; + +interface IPosition { + x: number; + y: number; +} + +export interface IWindowShapeParameters { + arrowPosition?: number; +} + +interface IWindowPositioning { + getPosition(window: BrowserWindow): IPosition; + getWindowShapeParameters(window: BrowserWindow): IWindowShapeParameters; +} + +class StandaloneWindowPositioning implements IWindowPositioning { + public getPosition(window: BrowserWindow): IPosition { + const windowBounds = window.getBounds(); + + const primaryDisplay = screen.getPrimaryDisplay(); + const workArea = primaryDisplay.workArea; + const maxX = workArea.x + workArea.width - windowBounds.width; + const maxY = workArea.y + workArea.height - windowBounds.height; + + const x = Math.min(Math.max(windowBounds.x, workArea.x), maxX); + const y = Math.min(Math.max(windowBounds.y, workArea.y), maxY); + + return { x, y }; + } + + public getWindowShapeParameters(_window: BrowserWindow): IWindowShapeParameters { + return {}; + } +} + +class AttachedToTrayWindowPositioning implements IWindowPositioning { + private tray: Tray; + + constructor(tray: Tray) { + this.tray = tray; + } + + public getPosition(window: BrowserWindow): IPosition { + const windowBounds = window.getBounds(); + const trayBounds = this.tray.getBounds(); + + const activeDisplay = screen.getDisplayNearestPoint({ + x: trayBounds.x, + y: trayBounds.y, + }); + const workArea = activeDisplay.workArea; + const placement = this.getTrayPlacement(); + const maxX = workArea.x + workArea.width - windowBounds.width; + const maxY = workArea.y + workArea.height - windowBounds.height; + + let x = 0; + let y = 0; + + switch (placement) { + case 'top': + x = trayBounds.x + (trayBounds.width - windowBounds.width) * 0.5; + y = workArea.y; + break; + + case 'bottom': + x = trayBounds.x + (trayBounds.width - windowBounds.width) * 0.5; + y = workArea.y + workArea.height - windowBounds.height; + break; + + case 'left': + x = workArea.x; + y = trayBounds.y + (trayBounds.height - windowBounds.height) * 0.5; + break; + + case 'right': + x = workArea.width - windowBounds.width; + y = trayBounds.y + (trayBounds.height - windowBounds.height) * 0.5; + break; + + case 'none': + x = workArea.x + (workArea.width - windowBounds.width) * 0.5; + y = workArea.y + (workArea.height - windowBounds.height) * 0.5; + break; + } + + x = Math.min(Math.max(x, workArea.x), maxX); + y = Math.min(Math.max(y, workArea.y), maxY); + + return { + x: Math.round(x), + y: Math.round(y), + }; + } + + public getWindowShapeParameters(window: BrowserWindow): IWindowShapeParameters { + const trayBounds = this.tray.getBounds(); + const windowBounds = window.getBounds(); + const arrowPosition = trayBounds.x - windowBounds.x + trayBounds.width * 0.5; + return { + arrowPosition, + }; + } + + private getTrayPlacement() { + switch (process.platform) { + case 'darwin': + // macOS has menubar always placed at the top + return 'top'; + + case 'win32': { + // taskbar occupies some part of the screen excluded from work area + const primaryDisplay = screen.getPrimaryDisplay(); + const displaySize = primaryDisplay.size; + const workArea = primaryDisplay.workArea; + + if (workArea.width < displaySize.width) { + return workArea.x > 0 ? 'left' : 'right'; + } else if (workArea.height < displaySize.height) { + return workArea.y > 0 ? 'top' : 'bottom'; + } else { + return 'none'; + } + } + + default: + return 'none'; + } + } +} + +export default class WindowController { + private width: number; + private height: number; + private windowPositioning: IWindowPositioning; + private isWindowReady = false; + + get window(): BrowserWindow { + return this.windowValue; + } + + get webContents(): WebContents { + return this.windowValue.webContents; + } + + constructor(private windowValue: BrowserWindow, tray: Tray) { + const [width, height] = windowValue.getSize(); + this.width = width; + this.height = height; + this.windowPositioning = + process.platform === 'linux' + ? new StandaloneWindowPositioning() + : new AttachedToTrayWindowPositioning(tray); + + this.installDisplayMetricsHandler(); + this.installWindowReadyHandlers(); + } + + public show(whenReady: boolean = true) { + if (whenReady) { + this.executeWhenWindowIsReady(() => this.showImmediately()); + } else { + this.showImmediately(); + } + } + + public hide() { + this.windowValue.hide(); + } + + public toggle() { + if (this.windowValue.isVisible()) { + this.hide(); + } else { + this.show(); + } + } + + public isVisible(): boolean { + return this.windowValue.isVisible(); + } + + public send(event: string, ...data: any[]): void { + this.windowValue.webContents.send(event, ...data); + } + + private showImmediately() { + const window = this.windowValue; + + this.updatePosition(); + this.notifyUpdateWindowShape(); + + window.show(); + window.focus(); + } + + private updatePosition() { + const { x, y } = this.windowPositioning.getPosition(this.windowValue); + this.windowValue.setPosition(x, y, false); + } + + private notifyUpdateWindowShape() { + const shapeParameters = this.windowPositioning.getWindowShapeParameters(this.windowValue); + this.windowValue.webContents.send('update-window-shape', shapeParameters); + } + + // Installs display event handlers to update the window position on any changes in the display or + // workarea dimensions. + private installDisplayMetricsHandler() { + screen.addListener('display-metrics-changed', this.onDisplayMetricsChanged); + this.windowValue.once('closed', () => { + screen.removeListener('display-metrics-changed', this.onDisplayMetricsChanged); + }); + } + + private onDisplayMetricsChanged = ( + _event: Electron.Event, + _display: Display, + changedMetrics: string[], + ) => { + if (changedMetrics.includes('workArea') && this.windowValue.isVisible()) { + this.updatePosition(); + this.notifyUpdateWindowShape(); + } + + // On linux, the window won't be properly rescaled back to it's original + // size if the DPI scaling factor is changed. + // https://github.com/electron/electron/issues/11050 + if (process.platform === 'linux' && changedMetrics.includes('scaleFactor')) { + this.forceResizeWindow(); + } + }; + + private forceResizeWindow() { + this.windowValue.setSize(this.width, this.height); + } + + private installWindowReadyHandlers() { + this.windowValue.once('ready-to-show', () => { + this.isWindowReady = true; + }); + } + + private executeWhenWindowIsReady(closure: () => void) { + if (this.isWindowReady) { + closure(); + } else { + this.windowValue.once('ready-to-show', () => { + closure(); + }); + } + } +} |
