import { app , BrowserWindow , screen } from "electron" ; import * as path from "path" ; import * as url from "url" ; import { LogService } from "jslib-common/abstractions/log.service" ; import { StateService } from "jslib-common/abstractions/state.service" ; import { cleanUserAgent , isDev , isMacAppStore , isSnapStore } from "./utils" ; const mainWindowSizeKey = "mainWindowSize" ; const WindowEventHandlingDelay = 100 ; export class WindowMain { win : BrowserWindow ; isQuitting : boolean = false ; private windowStateChangeTimer : NodeJS . Timer ; private windowStates : { [ key ] : any ; } = { } ; private enableAlwaysOnTop : boolean = false ; constructor ( private stateService , private logService , private hideTitleBar = false , private defaultWidth = 950 , private defaultHeight = 600 , private argvCallback = null , private createWindowCallback ) { } init ( ) { return new Promise < void > ( ( resolve , reject ) => { try { if ( ! isMacAppStore ( ) && ! isSnapStore ( ) ) { const gotTheLock = app . requestSingleInstanceLock ( ) ; if ( ! gotTheLock ) { app . quit ( ) ; return ; } else { app . on ( "second-instance" , ( event , argv , workingDirectory ) => { if ( this . win != null ) { if ( this . win . isMinimized ( ) || ! this . win . isVisible ( ) ) { this . win . show ( ) ; } this . win . focus ( ) ; } if ( process . platform === "win32" || process . platform === "linux" ) { if ( this . argvCallback != null ) { this . argvCallback ( argv ) ; } } } ) ; } } app . on ( "before-quit" , ( ) => { this . isQuitting = true ; } ) ; app . on ( "ready" , async ( ) => { await this . createWindow ( ) ; resolve ( ) ; if ( this . argvCallback != null ) { this . argvCallback ( process . argv ) ; } } ) ; app . on ( "window-all-closed" , ( ) => { if ( process . platform !== "darwin" || this . isQuitting || isMacAppStore ( ) ) { app . quit ( ) ; } } ) ; app . on ( "activate" , async ( ) => { if ( this . win === null ) { await this . createWindow ( ) ; } else { this . win . show ( ) ; } } ) ; } catch ( e ) { reject ( e ) ; } } ) ; } async createWindow ( ) { this . windowStates [ mainWindowSizeKey ] = await this . getWindowState ( mainWindowSizeKey , this . defaultWidth , this . defaultHeight ) ; this . enableAlwaysOnTop = await this . stateService . getEnableAlwaysOnTop ( ) ; this . win = new BrowserWindow ( { width : this . windowStates [ mainWindowSizeKey ] . width , height : this . windowStates [ mainWindowSizeKey ] . height , minWidth : 680 , minHeight : 500 , x : this . windowStates [ mainWindowSizeKey ] . x , y : this . windowStates [ mainWindowSizeKey ] . y , title : app . name , icon : process . platform === "linux" ? path . join ( __dirname , "/images/icon.png" ) : undefined , titleBarStyle : this . hideTitleBar && process . platform === "darwin" ? "hiddenInset" : undefined , show : false , backgroundColor : "#fff" , alwaysOnTop : this . enableAlwaysOnTop , webPreferences : { spellcheck : false , nodeIntegration : true , backgroundThrottling : false , contextIsolation : false , } , } ) ; if ( this . windowStates [ mainWindowSizeKey ] . isMaximized ) { this . win . maximize ( ) ; } this . win . show ( ) ; this . win . loadURL ( url . format ( { protocol : "file:" , pathname : path . join ( __dirname , "/index.html" ) , slashes : true , } ) , { userAgent : cleanUserAgent ( this . win . webContents . userAgent ) , } ) ; if ( isDev ( ) ) { this . win . webContents . openDevTools ( ) ; } this . win . on ( "closed" , async ( ) => { await this . updateWindowState ( mainWindowSizeKey , this . win ) ; this . win = null ; } ) ; this . win . on ( "close" , async ( ) => { await this . updateWindowState ( mainWindowSizeKey , this . win ) ; } ) ; this . win . on ( "maximize" , async ( ) => { await this . updateWindowState ( mainWindowSizeKey , this . win ) ; } ) ; this . win . on ( "unmaximize" , async ( ) => { await this . updateWindowState ( mainWindowSizeKey , this . win ) ; } ) ; this . win . on ( "resize" , ( ) => { this . windowStateChangeHandler ( mainWindowSizeKey , this . win ) ; } ) ; this . win . on ( "move" , ( ) => { this . windowStateChangeHandler ( mainWindowSizeKey , this . win ) ; } ) ; this . win . on ( "focus" , ( ) => { this . win . webContents . send ( "messagingService" , { command : "windowIsFocused" , windowIsFocused : true , } ) ; } ) ; if ( this . createWindowCallback ) { this . createWindowCallback ( this . win ) ; } } async toggleAlwaysOnTop ( ) { this . enableAlwaysOnTop = ! this . win . isAlwaysOnTop ( ) ; this . win . setAlwaysOnTop ( this . enableAlwaysOnTop ) ; await this . stateService . setEnableAlwaysOnTop ( this . enableAlwaysOnTop ) ; } private windowStateChangeHandler ( configKey , win ) { global . clearTimeout ( this . windowStateChangeTimer ) ; this . windowStateChangeTimer = global . setTimeout ( async ( ) => { await this . updateWindowState ( configKey , win ) ; } , WindowEventHandlingDelay ) ; } private async updateWindowState ( configKey , win ) { if ( win == null ) { return ; } try { const bounds = win . getBounds ( ) ; if ( this . windowStates [ configKey ] == null ) { this . windowStates [ configKey ] = ( await this . stateService . getWindow ( ) ) . get ( configKey ) ; if ( this . windowStates [ configKey ] == null ) { this . windowStates [ configKey ] = { } ; } } this . windowStates [ configKey ] . isMaximized = win . isMaximized ( ) ; this . windowStates [ configKey ] . displayBounds = screen . getDisplayMatching ( bounds ) . bounds ; if ( ! win . isMaximized ( ) && ! win . isMinimized ( ) && ! win . isFullScreen ( ) ) { this . windowStates [ configKey ] . x = bounds . x ; this . windowStates [ configKey ] . y = bounds . y ; this . windowStates [ configKey ] . width = bounds . width ; this . windowStates [ configKey ] . height = bounds . height ; } const cachedWindow = ( await this . stateService . getWindow ( ) ) ?? new Map < string , any > ( ) ; cachedWindow . set ( configKey , this . windowStates [ configKey ] ) ; await this . stateService . setWindow ( cachedWindow ) ; } catch ( e ) { this . logService . error ( e ) ; } } private async getWindowState ( configKey , defaultWidth , defaultHeight ) { const windowState = ( await this . stateService . getWindow ( ) ) ?? new Map < string , any > ( ) ; let state = windowState . has ( configKey ) ? windowState . get ( configKey ) : null ; const isValid = state != null && ( this . stateHasBounds ( state ) || state . isMaximized ) ; let displayBounds = null ; if ( ! isValid ) { state = { width : defaultWidth , height : defaultHeight , } ; displayBounds = screen . getPrimaryDisplay ( ) . bounds ; } else if ( this . stateHasBounds ( state ) && state . displayBounds ) { displayBounds = screen . getDisplayMatching ( state . displayBounds ) . bounds ; if ( displayBounds . width !== state . displayBounds . width || displayBounds . height !== state . displayBounds . height || displayBounds . x !== state . displayBounds . x || displayBounds . y !== state . displayBounds . y ) { state . x = undefined ; state . y = undefined ; displayBounds = screen . getPrimaryDisplay ( ) . bounds ; } } if ( displayBounds != null ) { if ( state . width > displayBounds . width && state . height > displayBounds . height ) { state . isMaximized = true ; } if ( state . width > displayBounds . width ) { state . width = displayBounds . width - 10 ; } if ( state . height > displayBounds . height ) { state . height = displayBounds . height - 10 ; } } return state ; } private stateHasBounds ( state ) { return ( state != null && Number . isInteger ( state . x ) && Number . isInteger ( state . y ) && Number . isInteger ( state . width ) && state . width > 0 && Number . isInteger ( state . height ) && state . height > 0 ) ; } }