Code Study: GitHub Desktop / main-process / app-window.ts

Source code: github-desktop/app/src/main-process/app-window.ts

This code file defines the AppWindow class that represents the main window of GitHub Desktop. The class encapsulates the Electron BrowserWindow instance, and provides additional window-related functions.

Private Fields

  1. window: Electron.BrowserWindow - the electron BrowserWindow instance encapsulated by the AppWindow.
  2. emitter: Emitter - the event emitter provided by the event-kit node package. For example, we implement onDidLoad on the AppWindow object, which will register callbacks to be invoked whenever the page has loaded and the renderer has signalled that it’s ready. To do so, we make use of this private emitter instance. We use emitter.on to subscribe the given callback in onDidLoad, and emitter.emit to notify subscribers. Finally, when the AppWindow instance is destroyed we call emitter.dispose to unsubscribe all subscribers.
  3. _loadTime: number | null - _loadTime records the time in milliseconds spent loading the page. It will be null until onDidLoad is called.
  4. _rendererReadyTime: number | null - _readererReadyTime records the time (in milliseconds) elapsed from the renderer being loaded to it signaling it was ready. This will be null until onDidLoad is called.
  5. minWidth = 960 - minimum width of the window.
  6. minHeight = 660 - minimum height of the window.

Constructor

  1. Try to get the saved window state by leveraging the electron-window-state node package. It’s a library to store and restore window sizes and positions for your Electron app. If the previous window state is not available (maybe because the app has never been opened before), it returns the minWidth and minHeight as the default width and height.

     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,
     })
    
  2. Construct the windowOptions object, which contains the parameters for creating the BrowserWindow. GitHub Desktop has a frameless main window. In order to create a frameless window, we need to set titleBarStyle as ‘hidden’ in macOS, and set frame to be false in Windows. The show parameter is set to false because we don’t want to show the window until it finishes loading see the call to window.onDidLoad method in main-process/main.ts.

     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')
     }
    
  3. It’s time to create the BrowserWindow, and let the window state manager manage the window state.

     this.window = new BrowserWindow(windowOptions)
     savedWindowState.manage(this.window)
    
  4. On macOS, when the user closes the window and the app is not quitting (no ‘before-quit’ or ‘will-quit’ message), we really just hide it. This lets us activate quickly and keep all our interesting logic in the renderer.

     let quitting = false
     app.on('before-quit', () => {
       quitting = true
     })
    
     ipcMain.on('will-quit', (event: Electron.IpcMessageEvent) => {
       quitting = true
       event.returnValue = true
     })
    
     if (__DARWIN__) {
       this.window.on('close', e => {
         if (!quitting) {
           e.preventDefault()
           Menu.sendActionToFirstResponder('hide:')
         }
       })
     }
    

Public Methods

load()

The function prepares various event handlers for load-related events, and calls window.loadURL to load ‘index.html’ at the end.

  1. Register the did-start-loading and did-finish-load event handlers. These events will start to trigger after the window.loadURL call - the last line in this method. When the events are emitted, we can calculate the window load time.

     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()
     })
    
  2. Make sure that the app window does not allow zoom in/out when it finshes loading.

     this.window.webContents.on('did-finish-load', () => {
       this.window.webContents.setVisualZoomLevelLimits(1, 1)
     })
    
  3. Register the did-fail-load event handler. If the load fails or is cancelled, we will open the devtool to troubleshoot the problem.

  4. Handle the ‘render-ready’ event emitted from the renderer to indicate that the loading is finished. If the renderer process enters idle period during the launch, renderer would send a ‘renderer-ready’ message indicating that it’s ready.

  5. Registers event handlers for all window state transition events and forwards those to the renderer process for a given window. Window state transition events include enter-full-screen, leave-full-screen, maximize, minimize, unmaximize, restore, hide, and show.

     registerWindowStateChangedEvents(this.window)
    
  6. Call window.loadURL to load the index.html file. (index.html will further load renderer.js). The actual loading begins from here.

     this.window.loadURL(encodePathAsUrl(__dirname, 'index.html'))
    

onClose(fn: () => void)

Register a function to call when the window (BrowserWindow) is closed. After you have received this event you should remove the reference to the window and avoid using it any more.

onDidLoad(fn: () => void)

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.

isMinimized()

Whether the window is minimized.

isVisible()

Whether the window is visible to the user.

restore()

Restores the window from minimized state to its previous state.

focus()

Focuses on the window.

show()

Shows and gives focus to the window.

sendMenuEvent(name: MenuEvent)

Send the menu event to the renderer. The name parameter could be any of the predefined menu events: ‘push’, ‘pull’, ‘show-changes’, etc. Based on the name, the renderer would do the actual work (push, pull, show-changes, etc.). See How Menu Event Works in GitHub Desktop to understand how menu event works between main process and renderer process.

public sendMenuEvent(name: MenuEvent) {
  this.show()
  this.window.webContents.send('menu-event', { name })
}

sendURLAction(action: URLActionType)

See How URL Action Works in GitHub Desktop

public sendURLAction(action: URLActionType) {
  this.show()
  this.window.webContents.send('url-action', { action })
}

sendLaunchTimingStats(stats: ILaunchStats)

It sends the app launch timing stats to the renderer.

sendAppMenu()

It sends the app menu to the renderer.

sendException(error: Error)

It reports the exception to the renderer.

tags: GitHub Desktop - Electron