From 38585e8415c17e6e97522ca2af80441ebdcb8888 Mon Sep 17 00:00:00 2001 From: Mark Sujew Date: Mon, 14 Feb 2022 16:40:21 +0100 Subject: [PATCH 1/2] Enable opening the IDE from finder/explorer --- .../theia/electron-main-application.ts | 100 +++++++++++++++--- electron/build/template-package.json | 6 ++ 2 files changed, 94 insertions(+), 12 deletions(-) diff --git a/arduino-ide-extension/src/electron-main/theia/electron-main-application.ts b/arduino-ide-extension/src/electron-main/theia/electron-main-application.ts index 9a26d81fd..0314ac372 100644 --- a/arduino-ide-extension/src/electron-main/theia/electron-main-application.ts +++ b/arduino-ide-extension/src/electron-main/theia/electron-main-application.ts @@ -1,8 +1,8 @@ import { inject, injectable } from 'inversify'; -import { app, BrowserWindow, BrowserWindowConstructorOptions, ipcMain, screen } from '@theia/core/electron-shared/electron'; +import { app, BrowserWindow, BrowserWindowConstructorOptions, ipcMain, screen, Event as ElectronEvent } from '@theia/core/electron-shared/electron'; import { fork } from 'child_process'; import { AddressInfo } from 'net'; -import { join } from 'path'; +import { join, dirname } from 'path'; import * as fs from 'fs-extra'; import { initSplashScreen } from '../splash/splash-screen'; import { MaybePromise } from '@theia/core/lib/common/types'; @@ -16,6 +16,8 @@ import { import { SplashServiceImpl } from '../splash/splash-service-impl'; import { URI } from '@theia/core/shared/vscode-uri'; import * as electronRemoteMain from '@theia/core/electron-shared/@electron/remote/main'; +import { Deferred } from '@theia/core/lib/common/promise-util'; +import * as os from '@theia/core/lib/common/os'; app.commandLine.appendSwitch('disable-http-cache'); @@ -36,6 +38,7 @@ const WORKSPACES = 'workspaces'; export class ElectronMainApplication extends TheiaElectronMainApplication { protected _windows: BrowserWindow[] = []; protected startup = false; + protected openFilePromise = new Deferred(); @inject(SplashServiceImpl) protected readonly splashService: SplashServiceImpl; @@ -45,17 +48,52 @@ export class ElectronMainApplication extends TheiaElectronMainApplication { // See: https://github.com/electron-userland/electron-builder/issues/2468 // Regression in Theia: https://github.com/eclipse-theia/theia/issues/8701 app.on('ready', () => app.setName(config.applicationName)); + this.attachFileAssociations(); return super.start(config); } + attachFileAssociations() { + // OSX: register open-file event + if (os.isOSX) { + app.on('open-file', async (event, uri) => { + event.preventDefault(); + if (uri.endsWith('.ino') && await fs.pathExists(uri)) { + this.openFilePromise.reject(); + await this.openSketch(dirname(uri)); + } + }); + setTimeout(() => this.openFilePromise.resolve(), 500); + } else { + this.openFilePromise.resolve(); + } + } + + protected async isValidSketchPath(uri: string): Promise { + return typeof uri === 'string' && await fs.pathExists(uri); + } + protected async launch(params: ElectronMainExecutionParams): Promise { + try { + // When running on MacOS, we either have to wait until + // 1. The `open-file` command has been received by the app, rejecting the promise + // 2. A short timeout resolves the promise automatically, falling back to the usual app launch + await this.openFilePromise.promise; + } catch { + // Application has received the `open-file` event and will skip the default application launch + return; + } + + if (!os.isOSX && await this.launchWindowsOpen(params)) { + // Application has received a file in its arguments and will skip the default application launch + return; + } + this.startup = true; const workspaces: WorkspaceOptions[] | undefined = this.electronStore.get(WORKSPACES); let useDefault = true; if (workspaces && workspaces.length > 0) { for (const workspace of workspaces) { - const file = workspace.file; - if (typeof file === 'string' && await fs.pathExists(file)) { + if (await this.isValidSketchPath(workspace.file)) { useDefault = false; await this.openSketch(workspace); } @@ -67,16 +105,46 @@ export class ElectronMainApplication extends TheiaElectronMainApplication { } } - protected async openSketch(workspace: WorkspaceOptions): Promise { + protected async launchWindowsOpen(params: ElectronMainExecutionParams): Promise { + // Copy to prevent manipulation of original array + const argCopy = [...params.argv]; + if (app.isPackaged) { + // workaround for missing executable argument when app is packaged + argCopy.unshift('packaged'); + } + const possibleUris = argCopy.slice(2) || null; + if (possibleUris) { + let uri: string | undefined; + for (const possibleUri of possibleUris) { + if (possibleUri.endsWith('.ino') && await this.isValidSketchPath(possibleUri)) { + uri = possibleUri; + break; + } + } + if (uri) { + await this.openSketch(dirname(uri)); + return true; + } + } + return false; + } + + protected async openSketch(workspace: WorkspaceOptions | string): Promise { const options = await this.getLastWindowOptions(); - options.x = workspace.x; - options.y = workspace.y; - options.width = workspace.width; - options.height = workspace.height; - options.isMaximized = workspace.isMaximized; - options.isFullScreen = workspace.isFullScreen; + let file: string; + if (typeof workspace === 'object') { + options.x = workspace.x; + options.y = workspace.y; + options.width = workspace.width; + options.height = workspace.height; + options.isMaximized = workspace.isMaximized; + options.isFullScreen = workspace.isFullScreen; + file = workspace.file; + } else { + file = workspace; + } const [uri, electronWindow] = await Promise.all([this.createWindowUri(), this.createWindow(options)]); - electronWindow.loadURL(uri.withFragment(encodeURI(workspace.file)).toString(true)); + electronWindow.loadURL(uri.withFragment(encodeURI(file)).toString(true)); return electronWindow; } @@ -101,6 +169,14 @@ export class ElectronMainApplication extends TheiaElectronMainApplication { }); } + protected async onSecondInstance(event: ElectronEvent, argv: string[], cwd: string): Promise { + if (!os.isOSX && await this.launchWindowsOpen({ cwd, argv, secondInstance: true })) { + // Application has received a file in its arguments + return; + } + super.onSecondInstance(event, argv, cwd); + } + /** * Use this rather than creating `BrowserWindow` instances from scratch, since some security parameters need to be set, this method will do it. * diff --git a/electron/build/template-package.json b/electron/build/template-package.json index 4f21f78ff..0a5a848cf 100644 --- a/electron/build/template-package.json +++ b/electron/build/template-package.json @@ -60,6 +60,12 @@ "directories": { "buildResources": "resources" }, + "fileAssociations": [ + { + "ext": "ino", + "role": "Editor" + } + ], "files": [ "src-gen", "lib", From fe0ca6366432511f755b70a4149aef3511075df4 Mon Sep 17 00:00:00 2001 From: Mark Sujew Date: Wed, 23 Feb 2022 11:07:34 +0100 Subject: [PATCH 2/2] Make opening windows from args a bit more lenient --- .../theia/electron-main-application.ts | 31 +++++++------------ 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/arduino-ide-extension/src/electron-main/theia/electron-main-application.ts b/arduino-ide-extension/src/electron-main/theia/electron-main-application.ts index 0314ac372..dc2cdeaed 100644 --- a/arduino-ide-extension/src/electron-main/theia/electron-main-application.ts +++ b/arduino-ide-extension/src/electron-main/theia/electron-main-application.ts @@ -83,7 +83,7 @@ export class ElectronMainApplication extends TheiaElectronMainApplication { return; } - if (!os.isOSX && await this.launchWindowsOpen(params)) { + if (!os.isOSX && await this.launchFromArgs(params)) { // Application has received a file in its arguments and will skip the default application launch return; } @@ -105,27 +105,20 @@ export class ElectronMainApplication extends TheiaElectronMainApplication { } } - protected async launchWindowsOpen(params: ElectronMainExecutionParams): Promise { + protected async launchFromArgs(params: ElectronMainExecutionParams): Promise { // Copy to prevent manipulation of original array const argCopy = [...params.argv]; - if (app.isPackaged) { - // workaround for missing executable argument when app is packaged - argCopy.unshift('packaged'); - } - const possibleUris = argCopy.slice(2) || null; - if (possibleUris) { - let uri: string | undefined; - for (const possibleUri of possibleUris) { - if (possibleUri.endsWith('.ino') && await this.isValidSketchPath(possibleUri)) { - uri = possibleUri; - break; - } - } - if (uri) { - await this.openSketch(dirname(uri)); - return true; + let uri: string | undefined; + for (const possibleUri of argCopy) { + if (possibleUri.endsWith('.ino') && await this.isValidSketchPath(possibleUri)) { + uri = possibleUri; + break; } } + if (uri) { + await this.openSketch(dirname(uri)); + return true; + } return false; } @@ -170,7 +163,7 @@ export class ElectronMainApplication extends TheiaElectronMainApplication { } protected async onSecondInstance(event: ElectronEvent, argv: string[], cwd: string): Promise { - if (!os.isOSX && await this.launchWindowsOpen({ cwd, argv, secondInstance: true })) { + if (!os.isOSX && await this.launchFromArgs({ cwd, argv, secondInstance: true })) { // Application has received a file in its arguments return; }