Skip to content

Commit

Permalink
[desktop]: Open dialogs using Electron's BrowserWindow as modal
Browse files Browse the repository at this point in the history
  • Loading branch information
tiagohm committed Jan 10, 2024
1 parent 86a6461 commit 472c831
Show file tree
Hide file tree
Showing 13 changed files with 169 additions and 145 deletions.
80 changes: 55 additions & 25 deletions desktop/app/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@ import * as path from 'path'

import { WebSocket } from 'ws'
import { MessageEvent } from '../src/shared/types/api.types'
import { InternalEventType, JsonFile, NotificationEvent, OpenDirectory, OpenFile, OpenWindow } from '../src/shared/types/app.types'
import { CloseWindow, InternalEventType, JsonFile, NotificationEvent, OpenDirectory, OpenFile, OpenWindow } from '../src/shared/types/app.types'
Object.assign(global, { WebSocket })

const store = new ElectronStore()

const browserWindows = new Map<string, BrowserWindow>()
const modalWindows = new Map<string, { window: BrowserWindow, resolve: (data: any) => void }>()
let api: ChildProcessWithoutNullStreams | null = null
let apiPort = 7000
let wsClient: Client
Expand All @@ -23,8 +24,7 @@ const serve = args.some(e => e === '--serve')
app.commandLine.appendSwitch('disable-http-cache')

function createMainWindow() {
const splashWindow = browserWindows.get('splash')
splashWindow?.close()
browserWindows.get('splash')?.close()
browserWindows.delete('splash')

createWindow({ id: 'home', path: 'home', data: undefined })
Expand Down Expand Up @@ -62,10 +62,10 @@ function createMainWindow() {
wsClient.activate()
}

function createWindow(options: OpenWindow<any>) {
function createWindow(options: OpenWindow<any>, parent?: BrowserWindow) {
let window = browserWindows.get(options.id)

if (window) {
if (window && !options.modal) {
if (options.data) {
console.info('window data changed. id=%s, data=%s', options.id, options.data)
window.webContents.send('DATA.CHANGED', options.data)
Expand Down Expand Up @@ -106,7 +106,7 @@ function createWindow(options: OpenWindow<any>) {
const icon = options.icon ?? 'nebulosa'
const data = encodeURIComponent(JSON.stringify(options.data || {}))

const position = store.get(`window.${options.id}.position`, undefined) as { x: number, y: number } | undefined
const position = !options.modal ? store.get(`window.${options.id}.position`, undefined) as { x: number, y: number } | undefined : undefined

if (position) {
position.x = Math.max(0, Math.min(position.x, size.width))
Expand All @@ -116,6 +116,8 @@ function createWindow(options: OpenWindow<any>) {
window = new BrowserWindow({
title: 'Nebulosa',
frame: false,
modal: options.modal,
parent,
width, height,
x: position?.x ?? undefined,
y: position?.y ?? undefined,
Expand All @@ -126,7 +128,7 @@ function createWindow(options: OpenWindow<any>) {
nodeIntegration: true,
allowRunningInsecureContent: serve,
contextIsolation: false,
additionalArguments: [`--port=${apiPort}`],
additionalArguments: [`--port=${apiPort}`, `--id=${options.id}`, `--modal=${options.modal ?? false}`],
preload: path.join(__dirname, 'preload.js'),
devTools: serve,
},
Expand Down Expand Up @@ -179,6 +181,13 @@ function createWindow(options: OpenWindow<any>) {
break
}
}

for (const [key, value] of modalWindows) {
if (value.window === window) {
modalWindows.delete(key)
break
}
}
}
})

Expand Down Expand Up @@ -219,7 +228,8 @@ function showNotification(event: NotificationEvent) {
}

function findWindowById(id: number) {
for (const [_, window] of browserWindows) if (window.id === id) return window
for (const [key, window] of browserWindows) if (window.id === id) return { window, key }
for (const [key, window] of modalWindows) if (window.window.id === id) return { window: window.window, key }
return undefined
}

Expand Down Expand Up @@ -281,8 +291,24 @@ try {
}
})

ipcMain.handle('WINDOW.OPEN', async (_, data: OpenWindow<any>) => {
const newWindow = !browserWindows.has(data.id)
ipcMain.handle('WINDOW.OPEN', async (event, data: OpenWindow<any>) => {
if (data.modal) {
const parent = findWindowById(event.sender.id)
const window = createWindow(data, parent?.window)

const promise = new Promise<any>((resolve) => {
modalWindows.set(data.id, {
window, resolve: (value) => {
window.close()
resolve(value)
}
})
})

return promise
}

const isNew = !browserWindows.has(data.id)

const window = createWindow(data)

Expand All @@ -293,7 +319,7 @@ try {
}

return new Promise<boolean>((resolve) => {
if (newWindow) {
if (isNew) {
window.webContents.once('did-finish-load', () => {
resolve(true)
})
Expand All @@ -306,7 +332,7 @@ try {
ipcMain.handle('FILE.OPEN', async (event, data?: OpenFile) => {
const ownerWindow = findWindowById(event.sender.id)

const value = await dialog.showOpenDialog(ownerWindow!, {
const value = await dialog.showOpenDialog(ownerWindow!.window, {
filters: data?.filters,
properties: ['openFile'],
defaultPath: data?.defaultPath || undefined,
Expand All @@ -317,7 +343,7 @@ try {

ipcMain.handle('FILE.SAVE', async (event, data?: OpenFile) => {
const ownerWindow = findWindowById(event.sender.id)
const value = await dialog.showSaveDialog(ownerWindow!, {
const value = await dialog.showSaveDialog(ownerWindow!.window, {
filters: data?.filters,
properties: ['createDirectory', 'showOverwriteConfirmation'],
defaultPath: data?.defaultPath || undefined,
Expand Down Expand Up @@ -352,7 +378,7 @@ try {

ipcMain.handle('DIRECTORY.OPEN', async (event, data?: OpenDirectory) => {
const ownerWindow = findWindowById(event.sender.id)
const value = await dialog.showOpenDialog(ownerWindow!, {
const value = await dialog.showOpenDialog(ownerWindow!.window, {
properties: ['openDirectory'],
defaultPath: data?.defaultPath || undefined,
})
Expand All @@ -361,47 +387,51 @@ try {
})

ipcMain.handle('WINDOW.PIN', (event) => {
const window = findWindowById(event.sender.id)
const window = findWindowById(event.sender.id)?.window
window?.setAlwaysOnTop(true)
return !!window
})

ipcMain.handle('WINDOW.UNPIN', (event) => {
const window = findWindowById(event.sender.id)
const window = findWindowById(event.sender.id)?.window
window?.setAlwaysOnTop(false)
return !!window
})

ipcMain.handle('WINDOW.MINIMIZE', (event) => {
const window = findWindowById(event.sender.id)
const window = findWindowById(event.sender.id)?.window
window?.minimize()
return !!window
})

ipcMain.handle('WINDOW.MAXIMIZE', (event) => {
const window = findWindowById(event.sender.id)
const window = findWindowById(event.sender.id)?.window

if (window?.isMaximized()) window.unmaximize()
else window?.maximize()

return window?.isMaximized() ?? false
})

ipcMain.handle('WINDOW.CLOSE', (event, id?: string) => {
if (id) {
ipcMain.handle('WINDOW.CLOSE', (event, data: CloseWindow<any>) => {
if (data.id) {
for (const [key, value] of browserWindows) {
if (key === id) {
if (key === data.id) {
value.close()
return true
}
}

return false
} else {
const window = findWindowById(event.sender.id)
window?.close()
return !!window

if (window) {
modalWindows.get(window.key)?.resolve(data.data)
window.window.close()
return true
}
}

return false
})

const events: InternalEventType[] = ['WHEEL.RENAMED', 'LOCATION.CHANGED']
Expand Down
8 changes: 6 additions & 2 deletions desktop/app/preload.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
const port = process.argv.find(e => e.startsWith('--port=')).split('=')[1]
function argWith(name) {
return process.argv.find(e => e.startsWith(`--${name}=`))?.split('=')?.[1]
}

window.apiPort = parseInt(port)
window.apiPort = parseInt(argWith('port'))
window.id = argWith('id')
window.modal = argWith('modal') === 'true'
10 changes: 5 additions & 5 deletions desktop/src/app/app.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@

<span class="separator flex justify-content-center" *ngIf="topMenu.length"></span>

<p-button *ngIf="pinned" [text]="true" [rounded]="true" size="small" icon="mdi mdi-pin" (onClick)="pin()"
<p-button *ngIf="pinned && !modal" [text]="true" [rounded]="true" size="small" icon="mdi mdi-pin" (onClick)="pin()"
styleClass="select-none cursor-pointer"></p-button>
<p-button *ngIf="!pinned" [text]="true" [rounded]="true" size="small" icon="mdi mdi-pin-off" (onClick)="pin()"
<p-button *ngIf="!pinned && !modal" [text]="true" [rounded]="true" size="small" icon="mdi mdi-pin-off" (onClick)="pin()"
styleClass="select-none cursor-pointer"></p-button>
<p-button [text]="true" [rounded]="true" size="small" icon="mdi mdi-window-minimize" (onClick)="minimize()"
styleClass="select-none cursor-pointer"></p-button>
<p-button [disabled]="!maximizable" [text]="true" [rounded]="true" size="small" icon="mdi mdi-window-maximize" (onClick)="maximize()"
<p-button *ngIf="!modal" [text]="true" [rounded]="true" size="small" icon="mdi mdi-window-minimize" (onClick)="minimize()"
styleClass="select-none cursor-pointer"></p-button>
<p-button *ngIf="!modal" [disabled]="!maximizable" [text]="true" [rounded]="true" size="small" icon="mdi mdi-window-maximize"
(onClick)="maximize()" styleClass="select-none cursor-pointer"></p-button>
<p-button [text]="true" [rounded]="true" size="small" icon="mdi mdi-window-close" (onClick)="close()"
styleClass="select-none cursor-pointer mr-1"></p-button>
</section>
Expand Down
5 changes: 3 additions & 2 deletions desktop/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export class AppComponent implements AfterViewInit {

pinned = false
maximizable = false
readonly modal = window.modal
subTitle? = ''
backgroundColor = '#212121'
topMenu: ExtendedMenuItem[] = []
Expand Down Expand Up @@ -64,7 +65,7 @@ export class AppComponent implements AfterViewInit {
this.electron.send('WINDOW.MAXIMIZE')
}

close() {
this.electron.send('WINDOW.CLOSE')
close(data?: any) {
this.electron.closeWindow({ data })
}
}
26 changes: 12 additions & 14 deletions desktop/src/app/camera/camera.component.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import { AfterContentInit, Component, HostListener, NgZone, OnDestroy, Optional, ViewChild } from '@angular/core'
import { AfterContentInit, Component, HostListener, NgZone, OnDestroy, ViewChild } from '@angular/core'
import { ActivatedRoute } from '@angular/router'
import { MenuItem } from 'primeng/api'
import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'
import { CameraExposureComponent } from '../../shared/components/camera-exposure/camera-exposure.component'
import { ApiService } from '../../shared/services/api.service'
import { BrowserWindowService } from '../../shared/services/browser-window.service'
import { ElectronService } from '../../shared/services/electron.service'
import { LocalStorageService } from '../../shared/services/local-storage.service'
import { PrimeService } from '../../shared/services/prime.service'
import { Camera, CameraDialogInput, CameraDialogMode, CameraPreference, CameraStartCapture, EMPTY_CAMERA, EMPTY_CAMERA_START_CAPTURE, ExposureMode, ExposureTimeUnit, FrameType, cameraPreferenceKey } from '../../shared/types/camera.types'
import { FilterWheel } from '../../shared/types/wheel.types'
import { AppComponent } from '../app.component'
Expand Down Expand Up @@ -120,15 +118,13 @@ export class CameraComponent implements AfterContentInit, OnDestroy {
private readonly cameraExposure!: CameraExposureComponent

constructor(
private app: AppComponent,
private api: ApiService,
private browserWindow: BrowserWindowService,
private electron: ElectronService,
private storage: LocalStorageService,
private route: ActivatedRoute,
ngZone: NgZone,
@Optional() private app?: AppComponent,
@Optional() private dialogRef?: DynamicDialogRef,
@Optional() config?: DynamicDialogConfig<CameraDialogInput>,
) {
if (app) app.title = 'Camera'

Expand Down Expand Up @@ -156,14 +152,17 @@ export class CameraComponent implements AfterContentInit, OnDestroy {
})
}
})

this.loadCameraStartCaptureForDialogMode(config?.data)
}

async ngAfterContentInit() {
this.route.queryParams.subscribe(e => {
const camera = JSON.parse(decodeURIComponent(e.data)) as Camera
this.cameraChanged(camera)
const decodedData = JSON.parse(decodeURIComponent(e.data))

if (this.app.modal) {
this.loadCameraStartCaptureForDialogMode(decodedData)
} else {
this.cameraChanged(decodedData)
}
})
}

Expand Down Expand Up @@ -363,7 +362,7 @@ export class CameraComponent implements AfterContentInit, OnDestroy {
}

apply() {
this.dialogRef?.close(this.makeCameraStartCapture())
this.app.close(this.makeCameraStartCapture())
}

private loadPreference() {
Expand Down Expand Up @@ -428,9 +427,8 @@ export class CameraComponent implements AfterContentInit, OnDestroy {
}
}

static async showAsDialog(prime: PrimeService, mode: CameraDialogMode, request: CameraStartCapture) {
const data: CameraDialogInput = { mode, request }
const result = await prime.open<CameraDialogInput, CameraStartCapture>(CameraComponent, { header: 'Camera', width: 'calc(400px + 2.5rem)', data })
static async showAsDialog(window: BrowserWindowService, mode: CameraDialogMode, request: CameraStartCapture) {
const result = await window.openCameraDialog({ data: { mode, request } })

if (result) {
Object.assign(request, result)
Expand Down
Loading

0 comments on commit 472c831

Please sign in to comment.