Skip to content

Commit 76abc28

Browse files
committed
Add repo-list action to open repositories in new windows
Add an "Open Repository in New Window" action to the repository list context menu and the minimum multi-window plumbing needed to support it safely.\n\nAlso updates native window titles with the selected repository name and reveals hidden/minimized windows when notification clicks focus them.
1 parent 6aa2179 commit 76abc28

12 files changed

Lines changed: 578 additions & 137 deletions

File tree

app/src/lib/cli-action.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export type CLIAction =
22
| {
33
readonly kind: 'open-repository'
44
readonly path: string
5+
readonly persistSelection?: boolean
56
}
67
| {
78
readonly kind: 'clone-url'

app/src/lib/ipc-shared.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ export type RequestChannels = {
6969
'update-accounts': (accounts: ReadonlyArray<EndpointToken>) => void
7070
'quit-and-install-updates': () => void
7171
'quit-app': () => void
72+
'open-repository-in-new-window': (path: string) => void
73+
'set-window-title': (title: string) => void
7274
'minimize-window': () => void
7375
'maximize-window': () => void
7476
'unmaximize-window': () => void

app/src/lib/stores/app-store.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1897,7 +1897,8 @@ export class AppStore extends TypedBaseStore<IAppState> {
18971897

18981898
/** This shouldn't be called directly. See `Dispatcher`. */
18991899
public async _selectRepository(
1900-
repository: Repository | CloningRepository | null
1900+
repository: Repository | CloningRepository | null,
1901+
persistSelection: boolean = true
19011902
): Promise<Repository | null> {
19021903
const previouslySelectedRepository = this.selectedRepository
19031904

@@ -1929,7 +1930,9 @@ export class AppStore extends TypedBaseStore<IAppState> {
19291930
return Promise.resolve(null)
19301931
}
19311932

1932-
setNumber(LastSelectedRepositoryIDKey, repository.id)
1933+
if (persistSelection) {
1934+
setNumber(LastSelectedRepositoryIDKey, repository.id)
1935+
}
19331936

19341937
const previousRepositoryId = previouslySelectedRepository
19351938
? previouslySelectedRepository.id
@@ -6388,6 +6391,7 @@ export class AppStore extends TypedBaseStore<IAppState> {
63886391
}
63896392

63906393
if (this.appIsFocused) {
6394+
this.updateMenuLabelsForSelectedRepository()
63916395
this.repositoryIndicatorUpdater.resume()
63926396
if (this.selectedRepository instanceof Repository) {
63936397
this.startPullRequestUpdater(this.selectedRepository)

app/src/main-process/app-window.ts

Lines changed: 129 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import {
55
BrowserWindow,
66
autoUpdater,
77
nativeTheme,
8+
ipcMain as electronIpcMain,
89
} from 'electron'
10+
import { IpcMainEvent } from 'electron/main'
911
import { shell } from '../lib/app-shell'
1012
import { Emitter, Disposable } from 'event-kit'
1113
import { encodePathAsUrl } from '../lib/path'
@@ -20,19 +22,16 @@ import { menuFromElectronMenu } from '../models/app-menu'
2022
import { now } from './now'
2123
import * as path from 'path'
2224
import windowStateKeeper from 'electron-window-state'
23-
import * as ipcMain from './ipc-main'
2425
import * as ipcWebContents from './ipc-webcontents'
25-
import {
26-
installNotificationCallback,
27-
terminateDesktopNotifications,
28-
} from './notifications'
26+
import { installNotificationCallback } from './notifications'
2927
import { addTrustedIPCSender } from './trusted-ipc-sender'
3028
import { getUpdaterGUID } from '../lib/get-updater-guid'
3129
import { CLIAction } from '../lib/cli-action'
3230

3331
export class AppWindow {
3432
private window: Electron.BrowserWindow
3533
private emitter = new Emitter()
34+
private readonly cleanupTasks = new Array<() => void>()
3635

3736
private _loadTime: number | null = null
3837
private _rendererReadyTime: number | null = null
@@ -43,6 +42,8 @@ export class AppWindow {
4342

4443
// See https://github.com/desktop/desktop/pull/11162
4544
private shouldMaximizeOnShow = false
45+
private quitting = false
46+
private quittingEvenIfUpdating = false
4647

4748
public constructor() {
4849
const savedWindowState = windowStateKeeper({
@@ -84,41 +85,24 @@ export class AppWindow {
8485
this.window = new BrowserWindow(windowOptions)
8586
addTrustedIPCSender(this.window.webContents)
8687

87-
installNotificationCallback(this.window)
88+
this.addCleanupTask(installNotificationCallback(this.window))
8889

8990
savedWindowState.manage(this.window)
9091
this.shouldMaximizeOnShow = savedWindowState.isMaximized
9192

92-
let quitting = false
93-
let quittingEvenIfUpdating = false
94-
app.on('before-quit', () => {
95-
quitting = true
96-
})
97-
98-
ipcMain.on('will-quit', event => {
99-
quitting = true
100-
event.returnValue = true
101-
})
102-
103-
ipcMain.on('will-quit-even-if-updating', event => {
104-
quitting = true
105-
quittingEvenIfUpdating = true
106-
event.returnValue = true
107-
})
108-
109-
ipcMain.on('cancel-quitting', event => {
110-
quitting = false
111-
quittingEvenIfUpdating = false
112-
event.returnValue = true
113-
})
93+
const onBeforeQuit = () => {
94+
this.quitting = true
95+
}
96+
app.on('before-quit', onBeforeQuit)
97+
this.addCleanupTask(() => app.removeListener('before-quit', onBeforeQuit))
11498

11599
this.window.on('close', e => {
116100
// On macOS, closing the window doesn't mean the app is quitting. If the
117101
// app is updating, we will prevent the window from closing only when the
118102
// app is also quitting.
119103
if (
120-
(!__DARWIN__ || quitting) &&
121-
!quittingEvenIfUpdating &&
104+
(!__DARWIN__ || this.quitting) &&
105+
!this.quittingEvenIfUpdating &&
122106
this.isDownloadingUpdate
123107
) {
124108
e.preventDefault()
@@ -136,7 +120,7 @@ export class AppWindow {
136120
// on macOS, when the user closes the window we really just hide it. This
137121
// lets us activate quickly and keep all our interesting logic in the
138122
// renderer.
139-
if (__DARWIN__ && !quitting) {
123+
if (__DARWIN__ && !this.quitting) {
140124
e.preventDefault()
141125
// https://github.com/desktop/desktop/issues/12838
142126
if (this.window.isFullScreen()) {
@@ -147,10 +131,9 @@ export class AppWindow {
147131
}
148132
return
149133
}
150-
nativeTheme.removeAllListeners()
151-
autoUpdater.removeAllListeners()
152-
terminateDesktopNotifications()
153134
})
135+
136+
this.window.on('closed', () => this.cleanup())
154137
}
155138

156139
public load() {
@@ -187,11 +170,37 @@ export class AppWindow {
187170
this.window.show()
188171
})
189172

190-
// TODO: This should be scoped by the window.
191-
ipcMain.once('renderer-ready', (_, readyTime) => {
173+
const onRendererReady = (event: IpcMainEvent, readyTime: number) => {
174+
if (event.sender !== this.window.webContents) {
175+
return
176+
}
177+
192178
this._rendererReadyTime = readyTime
193179
this.maybeEmitDidLoad()
194-
})
180+
electronIpcMain.removeListener('renderer-ready', onRendererReady)
181+
}
182+
electronIpcMain.on('renderer-ready', onRendererReady)
183+
this.addCleanupTask(() =>
184+
electronIpcMain.removeListener('renderer-ready', onRendererReady)
185+
)
186+
187+
const onBackgroundColorUpdated = (event: IpcMainEvent, color: string) => {
188+
if (event.sender !== this.window.webContents) {
189+
return
190+
}
191+
192+
this.window.setBackgroundColor(color)
193+
}
194+
electronIpcMain.on(
195+
'update-window-background-color',
196+
onBackgroundColorUpdated
197+
)
198+
this.addCleanupTask(() =>
199+
electronIpcMain.removeListener(
200+
'update-window-background-color',
201+
onBackgroundColorUpdated
202+
)
203+
)
195204

196205
this.window.on('focus', () =>
197206
ipcWebContents.send(this.window.webContents, 'focus')
@@ -203,13 +212,13 @@ export class AppWindow {
203212
registerWindowStateChangedEvents(this.window)
204213
this.window.loadURL(encodePathAsUrl(__dirname, 'index.html'))
205214

206-
nativeTheme.addListener('updated', () => {
215+
const onNativeThemeUpdated = () => {
207216
ipcWebContents.send(this.window.webContents, 'native-theme-updated')
208-
})
209-
210-
ipcMain.on('update-window-background-color', (_, color) => {
211-
this.window.setBackgroundColor(color)
212-
})
217+
}
218+
nativeTheme.addListener('updated', onNativeThemeUpdated)
219+
this.addCleanupTask(() =>
220+
nativeTheme.removeListener('updated', onNativeThemeUpdated)
221+
)
213222

214223
this.setupAutoUpdater()
215224
}
@@ -231,6 +240,10 @@ export class AppWindow {
231240
return !!this.loadTime && !!this.rendererReadyTime
232241
}
233242

243+
public get isLoaded(): boolean {
244+
return this.rendererLoaded
245+
}
246+
234247
public onClosed(fn: () => void) {
235248
this.window.on('closed', fn)
236249
}
@@ -260,10 +273,31 @@ export class AppWindow {
260273
return this.window.isFocused()
261274
}
262275

276+
public get id() {
277+
return this.window.id
278+
}
279+
263280
public focus() {
264281
this.window.focus()
265282
}
266283

284+
public revealAndFocus() {
285+
if (this.window.isMinimized()) {
286+
this.window.restore()
287+
}
288+
289+
if (!this.window.isVisible()) {
290+
this.show()
291+
return
292+
}
293+
294+
this.window.focus()
295+
}
296+
297+
public setTitle(title: string) {
298+
this.window.setTitle(title)
299+
}
300+
267301
/** Selects all the windows web contents */
268302
public selectAllWindowContents() {
269303
this.window.webContents.selectAll()
@@ -394,42 +428,60 @@ export class AppWindow {
394428
}
395429

396430
public setupAutoUpdater() {
397-
autoUpdater.on('error', (error: Error) => {
431+
const onAutoUpdaterError = (error: Error) => {
398432
this.isDownloadingUpdate = false
399433
ipcWebContents.send(this.window.webContents, 'auto-updater-error', error)
400-
})
434+
}
435+
autoUpdater.on('error', onAutoUpdaterError)
436+
this.addCleanupTask(() => autoUpdater.removeListener('error', onAutoUpdaterError))
401437

402-
autoUpdater.on('checking-for-update', () => {
438+
const onCheckingForUpdate = () => {
403439
this.isDownloadingUpdate = false
404440
ipcWebContents.send(
405441
this.window.webContents,
406442
'auto-updater-checking-for-update'
407443
)
408-
})
444+
}
445+
autoUpdater.on('checking-for-update', onCheckingForUpdate)
446+
this.addCleanupTask(() =>
447+
autoUpdater.removeListener('checking-for-update', onCheckingForUpdate)
448+
)
409449

410-
autoUpdater.on('update-available', () => {
450+
const onUpdateAvailable = () => {
411451
this.isDownloadingUpdate = true
412452
ipcWebContents.send(
413453
this.window.webContents,
414454
'auto-updater-update-available'
415455
)
416-
})
456+
}
457+
autoUpdater.on('update-available', onUpdateAvailable)
458+
this.addCleanupTask(() =>
459+
autoUpdater.removeListener('update-available', onUpdateAvailable)
460+
)
417461

418-
autoUpdater.on('update-not-available', () => {
462+
const onUpdateNotAvailable = () => {
419463
this.isDownloadingUpdate = false
420464
ipcWebContents.send(
421465
this.window.webContents,
422466
'auto-updater-update-not-available'
423467
)
424-
})
468+
}
469+
autoUpdater.on('update-not-available', onUpdateNotAvailable)
470+
this.addCleanupTask(() =>
471+
autoUpdater.removeListener('update-not-available', onUpdateNotAvailable)
472+
)
425473

426-
autoUpdater.on('update-downloaded', () => {
474+
const onUpdateDownloaded = () => {
427475
this.isDownloadingUpdate = false
428476
ipcWebContents.send(
429477
this.window.webContents,
430478
'auto-updater-update-downloaded'
431479
)
432-
})
480+
}
481+
autoUpdater.on('update-downloaded', onUpdateDownloaded)
482+
this.addCleanupTask(() =>
483+
autoUpdater.removeListener('update-downloaded', onUpdateDownloaded)
484+
)
433485
}
434486

435487
public async checkForUpdates(url: string) {
@@ -496,6 +548,30 @@ export class AppWindow {
496548
const { filePaths } = await dialog.showOpenDialog(this.window, options)
497549
return filePaths.length > 0 ? filePaths[0] : null
498550
}
551+
552+
public markWillQuit() {
553+
this.quitting = true
554+
}
555+
556+
public markWillQuitEvenIfUpdating() {
557+
this.quitting = true
558+
this.quittingEvenIfUpdating = true
559+
}
560+
561+
public cancelQuitting() {
562+
this.quitting = false
563+
this.quittingEvenIfUpdating = false
564+
}
565+
566+
private addCleanupTask(task: () => void) {
567+
this.cleanupTasks.push(task)
568+
}
569+
570+
private cleanup() {
571+
for (const task of this.cleanupTasks.splice(0).reverse()) {
572+
task()
573+
}
574+
}
499575
}
500576

501577
const trySetUpdaterGuid = async (url: string) => {

0 commit comments

Comments
 (0)