diff --git a/arduino-ide-extension/package.json b/arduino-ide-extension/package.json index 713c7868c..4e5ab0d16 100644 --- a/arduino-ide-extension/package.json +++ b/arduino-ide-extension/package.json @@ -150,6 +150,10 @@ "frontend": "lib/browser/theia/core/browser-menu-module", "frontendElectron": "lib/electron-browser/theia/core/electron-menu-module" }, + { + "frontend": "lib/browser/theia/core/browser-window-module", + "frontendElectron": "lib/electron-browser/theia/core/electron-window-module" + }, { "electronMain": "lib/electron-main/arduino-electron-main-module" } diff --git a/arduino-ide-extension/src/browser/contributions/first-startup-installer.ts b/arduino-ide-extension/src/browser/contributions/first-startup-installer.ts index 5010c5fc9..8f469ee7d 100644 --- a/arduino-ide-extension/src/browser/contributions/first-startup-installer.ts +++ b/arduino-ide-extension/src/browser/contributions/first-startup-installer.ts @@ -43,7 +43,7 @@ export class FirstStartupInstaller extends Contribution { // If arduino:avr installation fails because it's already installed we don't want to retry on next start-up console.error(e); } else { - // But if there is any other error (e.g.: no interntet cconnection), we want to retry next time + // But if there is any other error (e.g.: no Internet connection), we want to retry next time avrPackageError = e; } } @@ -64,7 +64,7 @@ export class FirstStartupInstaller extends Contribution { // If Arduino_BuiltIn installation fails because it's already installed we don't want to retry on next start-up console.log('error installing core', e); } else { - // But if there is any other error (e.g.: no interntet cconnection), we want to retry next time + // But if there is any other error (e.g.: no Internet connection), we want to retry next time builtInLibraryError = e; } } diff --git a/arduino-ide-extension/src/browser/theia/core/browser-window-module.ts b/arduino-ide-extension/src/browser/theia/core/browser-window-module.ts new file mode 100644 index 000000000..ac0862b75 --- /dev/null +++ b/arduino-ide-extension/src/browser/theia/core/browser-window-module.ts @@ -0,0 +1,10 @@ +import { DefaultWindowService as TheiaDefaultWindowService } from '@theia/core/lib/browser/window/default-window-service'; +import { ContainerModule } from '@theia/core/shared/inversify'; +import { DefaultWindowService } from './default-window-service'; +import { WindowServiceExt } from './window-service-ext'; + +export default new ContainerModule((bind, unbind, isBound, rebind) => { + bind(DefaultWindowService).toSelf().inSingletonScope(); + rebind(TheiaDefaultWindowService).toService(DefaultWindowService); + bind(WindowServiceExt).toService(DefaultWindowService); +}); diff --git a/arduino-ide-extension/src/browser/theia/core/default-window-service.ts b/arduino-ide-extension/src/browser/theia/core/default-window-service.ts new file mode 100644 index 000000000..145691727 --- /dev/null +++ b/arduino-ide-extension/src/browser/theia/core/default-window-service.ts @@ -0,0 +1,17 @@ +import { DefaultWindowService as TheiaDefaultWindowService } from '@theia/core/lib/browser/window/default-window-service'; +import { injectable } from '@theia/core/shared/inversify'; +import { WindowServiceExt } from './window-service-ext'; + +@injectable() +export class DefaultWindowService + extends TheiaDefaultWindowService + implements WindowServiceExt +{ + /** + * The default implementation always resolves to `true`. + * IDE2 does not use it. It's currently an electron-only app. + */ + async isFirstWindow(): Promise { + return true; + } +} diff --git a/arduino-ide-extension/src/browser/theia/core/window-service-ext.ts b/arduino-ide-extension/src/browser/theia/core/window-service-ext.ts new file mode 100644 index 000000000..f22e55cc4 --- /dev/null +++ b/arduino-ide-extension/src/browser/theia/core/window-service-ext.ts @@ -0,0 +1,7 @@ +export const WindowServiceExt = Symbol('WindowServiceExt'); +export interface WindowServiceExt { + /** + * Returns with a promise that resolves to `true` if the current window is the first window. + */ + isFirstWindow(): Promise; +} diff --git a/arduino-ide-extension/src/electron-browser/theia/core/electron-menu-module.ts b/arduino-ide-extension/src/electron-browser/theia/core/electron-menu-module.ts index bed0179ee..101e44b56 100644 --- a/arduino-ide-extension/src/electron-browser/theia/core/electron-menu-module.ts +++ b/arduino-ide-extension/src/electron-browser/theia/core/electron-menu-module.ts @@ -1,14 +1,7 @@ import { ContainerModule } from '@theia/core/shared/inversify'; -import { WindowService } from '@theia/core/lib/browser/window/window-service'; import { ElectronMainMenuFactory as TheiaElectronMainMenuFactory } from '@theia/core/lib/electron-browser/menu/electron-main-menu-factory'; import { ElectronMenuContribution as TheiaElectronMenuContribution } from '@theia/core/lib/electron-browser/menu/electron-menu-contribution'; -import { ElectronIpcConnectionProvider } from '@theia/core/lib/electron-browser/messaging/electron-ipc-connection-provider'; -import { - SplashService, - splashServicePath, -} from '../../../electron-common/splash-service'; import { MainMenuManager } from '../../../common/main-menu-manager'; -import { ElectronWindowService } from '../../electron-window-service'; import { ElectronMainMenuFactory } from './electron-main-menu-factory'; import { ElectronMenuContribution } from './electron-menu-contribution'; import { nls } from '@theia/core/lib/common/nls'; @@ -16,23 +9,25 @@ import { nls } from '@theia/core/lib/common/nls'; import * as remote from '@theia/core/electron-shared/@electron/remote'; import * as dialogs from '@theia/core/lib/browser/dialogs'; - Object.assign(dialogs, { confirmExit: async () => { const messageBoxResult = await remote.dialog.showMessageBox( remote.getCurrentWindow(), { - message: nls.localize('theia/core/quitMessage', 'Any unsaved changes will not be saved.'), - title: nls.localize('theia/core/quitTitle', 'Are you sure you want to quit?'), + message: nls.localize( + 'theia/core/quitMessage', + 'Any unsaved changes will not be saved.' + ), + title: nls.localize( + 'theia/core/quitTitle', + 'Are you sure you want to quit?' + ), type: 'question', - buttons: [ - dialogs.Dialog.CANCEL, - dialogs.Dialog.YES, - ], + buttons: [dialogs.Dialog.CANCEL, dialogs.Dialog.YES], } - ) + ); return messageBoxResult.response === 1; - } + }, }); export default new ContainerModule((bind, unbind, isBound, rebind) => { @@ -41,14 +36,4 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { rebind(TheiaElectronMenuContribution).toService(ElectronMenuContribution); bind(ElectronMainMenuFactory).toSelf().inSingletonScope(); rebind(TheiaElectronMainMenuFactory).toService(ElectronMainMenuFactory); - bind(ElectronWindowService).toSelf().inSingletonScope(); - rebind(WindowService).toService(ElectronWindowService); - bind(SplashService) - .toDynamicValue((context) => - ElectronIpcConnectionProvider.createProxy( - context.container, - splashServicePath - ) - ) - .inSingletonScope(); }); diff --git a/arduino-ide-extension/src/electron-browser/theia/core/electron-window-module.ts b/arduino-ide-extension/src/electron-browser/theia/core/electron-window-module.ts new file mode 100644 index 000000000..9328572b1 --- /dev/null +++ b/arduino-ide-extension/src/electron-browser/theia/core/electron-window-module.ts @@ -0,0 +1,32 @@ +import { WindowService } from '@theia/core/lib/browser/window/window-service'; +import { ElectronIpcConnectionProvider } from '@theia/core/lib/electron-browser/messaging/electron-ipc-connection-provider'; +import { ContainerModule } from '@theia/core/shared/inversify'; +import { WindowServiceExt } from '../../../browser/theia/core/window-service-ext'; +import { + ElectronMainWindowServiceExt, + electronMainWindowServiceExtPath, +} from '../../../electron-common/electron-main-window-service-ext'; +import { + SplashService, + splashServicePath, +} from '../../../electron-common/splash-service'; +import { ElectronWindowService } from './electron-window-service'; + +export default new ContainerModule((bind, unbind, isBound, rebind) => { + bind(ElectronWindowService).toSelf().inSingletonScope(); + rebind(WindowService).toService(ElectronWindowService); + bind(WindowServiceExt).toService(ElectronWindowService); + bind(ElectronMainWindowServiceExt) + .toDynamicValue(({ container }) => + ElectronIpcConnectionProvider.createProxy( + container, + electronMainWindowServiceExtPath + ) + ) + .inSingletonScope(); + bind(SplashService) + .toDynamicValue(({ container }) => + ElectronIpcConnectionProvider.createProxy(container, splashServicePath) + ) + .inSingletonScope(); +}); diff --git a/arduino-ide-extension/src/electron-browser/electron-window-service.ts b/arduino-ide-extension/src/electron-browser/theia/core/electron-window-service.ts similarity index 61% rename from arduino-ide-extension/src/electron-browser/electron-window-service.ts rename to arduino-ide-extension/src/electron-browser/theia/core/electron-window-service.ts index 1407ac425..2f571b419 100644 --- a/arduino-ide-extension/src/electron-browser/electron-window-service.ts +++ b/arduino-ide-extension/src/electron-browser/theia/core/electron-window-service.ts @@ -1,4 +1,8 @@ -import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; +import { + inject, + injectable, + postConstruct, +} from '@theia/core/shared/inversify'; import * as remote from '@theia/core/electron-shared/@electron/remote'; import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; import { @@ -6,19 +10,27 @@ import { ConnectionStatusService, } from '@theia/core/lib/browser/connection-status-service'; import { ElectronWindowService as TheiaElectronWindowService } from '@theia/core/lib/electron-browser/window/electron-window-service'; -import { SplashService } from '../electron-common/splash-service'; +import { SplashService } from '../../../electron-common/splash-service'; import { nls } from '@theia/core/lib/common'; +import { WindowServiceExt } from '../../../browser/theia/core/window-service-ext'; +import { ElectronMainWindowServiceExt } from '../../../electron-common/electron-main-window-service-ext'; @injectable() -export class ElectronWindowService extends TheiaElectronWindowService { +export class ElectronWindowService + extends TheiaElectronWindowService + implements WindowServiceExt +{ @inject(ConnectionStatusService) - protected readonly connectionStatusService: ConnectionStatusService; + private readonly connectionStatusService: ConnectionStatusService; @inject(SplashService) - protected readonly splashService: SplashService; + private readonly splashService: SplashService; @inject(FrontendApplicationStateService) - protected readonly appStateService: FrontendApplicationStateService; + private readonly appStateService: FrontendApplicationStateService; + + @inject(ElectronMainWindowServiceExt) + private readonly mainWindowServiceExt: ElectronMainWindowServiceExt; @postConstruct() protected override init(): void { @@ -55,4 +67,15 @@ export class ElectronWindowService extends TheiaElectronWindowService { }); return response === 0; // 'Yes', close the window. } + + private _firstWindow: boolean | undefined; + async isFirstWindow(): Promise { + if (this._firstWindow === undefined) { + const windowId = remote.getCurrentWindow().id; // This is expensive and synchronous so we check it once per FE. + this._firstWindow = await this.mainWindowServiceExt.isFirstWindow( + windowId + ); + } + return this._firstWindow; + } } diff --git a/arduino-ide-extension/src/electron-common/electron-main-window-service-ext.ts b/arduino-ide-extension/src/electron-common/electron-main-window-service-ext.ts new file mode 100644 index 000000000..c0e52d353 --- /dev/null +++ b/arduino-ide-extension/src/electron-common/electron-main-window-service-ext.ts @@ -0,0 +1,7 @@ +export const electronMainWindowServiceExtPath = '/services/electron-window-ext'; +export const ElectronMainWindowServiceExt = Symbol( + 'ElectronMainWindowServiceExt' +); +export interface ElectronMainWindowServiceExt { + isFirstWindow(windowId: number): Promise; +} diff --git a/arduino-ide-extension/src/electron-main/arduino-electron-main-module.ts b/arduino-ide-extension/src/electron-main/arduino-electron-main-module.ts index 7da0c7314..0146fd02a 100644 --- a/arduino-ide-extension/src/electron-main/arduino-electron-main-module.ts +++ b/arduino-ide-extension/src/electron-main/arduino-electron-main-module.ts @@ -1,31 +1,31 @@ -import { ContainerModule } from '@theia/core/shared/inversify'; import { JsonRpcConnectionHandler } from '@theia/core/lib/common/messaging/proxy-factory'; -import { ElectronConnectionHandler } from '@theia/core/lib/electron-common/messaging/electron-connection-handler'; import { ElectronMainWindowService } from '@theia/core/lib/electron-common/electron-main-window-service'; +import { ElectronConnectionHandler } from '@theia/core/lib/electron-common/messaging/electron-connection-handler'; import { ElectronMainApplication as TheiaElectronMainApplication, ElectronMainApplicationContribution, } from '@theia/core/lib/electron-main/electron-main-application'; +import { TheiaElectronWindow as DefaultTheiaElectronWindow } from '@theia/core/lib/electron-main/theia-electron-window'; +import { ContainerModule } from '@theia/core/shared/inversify'; +import { + IDEUpdater, + IDEUpdaterClient, + IDEUpdaterPath, +} from '../common/protocol/ide-updater'; +import { + ElectronMainWindowServiceExt, + electronMainWindowServiceExtPath, +} from '../electron-common/electron-main-window-service-ext'; import { SplashService, splashServicePath, } from '../electron-common/splash-service'; +import { ElectronMainWindowServiceExtImpl } from './electron-main-window-service-ext-impl'; +import { IDEUpdaterImpl } from './ide-updater/ide-updater-impl'; import { SplashServiceImpl } from './splash/splash-service-impl'; import { ElectronMainApplication } from './theia/electron-main-application'; import { ElectronMainWindowServiceImpl } from './theia/electron-main-window-service'; -import { - IDEUpdater, - IDEUpdaterClient, - IDEUpdaterPath, -} from '../common/protocol/ide-updater'; -import { IDEUpdaterImpl } from './ide-updater/ide-updater-impl'; import { TheiaElectronWindow } from './theia/theia-electron-window'; -import { TheiaElectronWindow as DefaultTheiaElectronWindow } from '@theia/core/lib/electron-main/theia-electron-window'; -import { SurveyNotificationServiceImpl } from '../node/survey-service-impl'; -import { - SurveyNotificationService, - SurveyNotificationServicePath, -} from '../common/protocol/survey-service'; export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(ElectronMainApplication).toSelf().inSingletonScope(); @@ -67,19 +67,14 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(TheiaElectronWindow).toSelf(); rebind(DefaultTheiaElectronWindow).toService(TheiaElectronWindow); - // Survey notification bindings - bind(SurveyNotificationServiceImpl).toSelf().inSingletonScope(); - bind(SurveyNotificationService).toService(SurveyNotificationServiceImpl); - bind(ElectronMainApplicationContribution).toService( - SurveyNotificationService - ); + bind(ElectronMainWindowServiceExt) + .to(ElectronMainWindowServiceExtImpl) + .inSingletonScope(); bind(ElectronConnectionHandler) .toDynamicValue( (context) => - new JsonRpcConnectionHandler(SurveyNotificationServicePath, () => - context.container.get( - SurveyNotificationService - ) + new JsonRpcConnectionHandler(electronMainWindowServiceExtPath, () => + context.container.get(ElectronMainWindowServiceExt) ) ) .inSingletonScope(); diff --git a/arduino-ide-extension/src/electron-main/electron-main-window-service-ext-impl.ts b/arduino-ide-extension/src/electron-main/electron-main-window-service-ext-impl.ts new file mode 100644 index 000000000..a0c872786 --- /dev/null +++ b/arduino-ide-extension/src/electron-main/electron-main-window-service-ext-impl.ts @@ -0,0 +1,15 @@ +import { inject, injectable } from '@theia/core/shared/inversify'; +import { ElectronMainWindowServiceExt } from '../electron-common/electron-main-window-service-ext'; +import { ElectronMainApplication } from './theia/electron-main-application'; + +@injectable() +export class ElectronMainWindowServiceExtImpl + implements ElectronMainWindowServiceExt +{ + @inject(ElectronMainApplication) + private readonly app: ElectronMainApplication; + + async isFirstWindow(windowId: number): Promise { + return this.app.firstWindowId === windowId; + } +} 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 4a3a3ade2..e5159a149 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 @@ -66,11 +66,12 @@ const APP_STARTED_WITH_CONTENT_TRACE = @injectable() export class ElectronMainApplication extends TheiaElectronMainApplication { - protected startup = false; - protected openFilePromise = new Deferred(); + private startup = false; + private _firstWindowId: number | undefined; + private openFilePromise = new Deferred(); @inject(SplashServiceImpl) - protected readonly splashService: SplashServiceImpl; + private readonly splashService: SplashServiceImpl; override async start(config: FrontendApplicationConfig): Promise { // Explicitly set the app name to have better menu items on macOS. ("About", "Hide", and "Quit") @@ -142,7 +143,7 @@ export class ElectronMainApplication extends TheiaElectronMainApplication { })(); } - attachFileAssociations() { + private attachFileAssociations(): void { // OSX: register open-file event if (os.isOSX) { app.on('open-file', async (event, uri) => { @@ -158,7 +159,7 @@ export class ElectronMainApplication extends TheiaElectronMainApplication { } } - protected async isValidSketchPath(uri: string): Promise { + private async isValidSketchPath(uri: string): Promise { return typeof uri === 'string' && (await fs.pathExists(uri)); } @@ -201,7 +202,7 @@ export class ElectronMainApplication extends TheiaElectronMainApplication { } } - protected async launchFromArgs( + private async launchFromArgs( params: ElectronMainExecutionParams ): Promise { // Copy to prevent manipulation of original array @@ -223,7 +224,7 @@ export class ElectronMainApplication extends TheiaElectronMainApplication { return false; } - protected async openSketch( + private async openSketch( workspace: WorkspaceOptions | string ): Promise { const options = await this.getLastWindowOptions(); @@ -257,7 +258,8 @@ export class ElectronMainApplication extends TheiaElectronMainApplication { } protected override getTitleBarStyle( - config: FrontendApplicationConfig + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _config: FrontendApplicationConfig ): 'native' | 'custom' { return 'native'; } @@ -354,6 +356,9 @@ export class ElectronMainApplication extends TheiaElectronMainApplication { electronWindow.webContents.openDevTools(); } this.attachListenersToWindow(electronWindow); + if (this._firstWindowId === undefined) { + this._firstWindowId = electronWindow.id; + } return electronWindow; } @@ -389,7 +394,7 @@ export class ElectronMainApplication extends TheiaElectronMainApplication { }); event.newGuest = new BrowserWindow(options); event.newGuest.setMenu(null); - event.newGuest?.on('closed', (e: any) => { + event.newGuest?.on('closed', () => { electronWindow?.webContents.send('CLOSE_CHILD_WINDOW'); }); event.newGuest?.loadURL(url); @@ -462,9 +467,9 @@ export class ElectronMainApplication extends TheiaElectronMainApplication { } } - protected closedWorkspaces: WorkspaceOptions[] = []; + private closedWorkspaces: WorkspaceOptions[] = []; - protected attachClosedWorkspace(window: BrowserWindow): void { + private attachClosedWorkspace(window: BrowserWindow): void { // Since the `before-quit` event is only fired when closing the *last* window // We need to keep track of recently closed windows/workspaces manually window.on('close', () => { @@ -522,4 +527,8 @@ export class ElectronMainApplication extends TheiaElectronMainApplication { get browserWindows(): BrowserWindow[] { return Array.from(this.windows.values()).map(({ window }) => window); } + + get firstWindowId(): number | undefined { + return this._firstWindowId; + } } diff --git a/arduino-ide-extension/src/node/arduino-ide-backend-module.ts b/arduino-ide-extension/src/node/arduino-ide-backend-module.ts index 6338b5320..b21f5d301 100644 --- a/arduino-ide-extension/src/node/arduino-ide-backend-module.ts +++ b/arduino-ide-extension/src/node/arduino-ide-backend-module.ts @@ -104,6 +104,11 @@ import { ClangFormatter } from './clang-formatter'; import { FormatterPath } from '../common/protocol/formatter'; import { LocalizationBackendContribution } from './i18n/localization-backend-contribution'; import { LocalizationBackendContribution as TheiaLocalizationBackendContribution } from '@theia/core/lib/node/i18n/localization-backend-contribution'; +import { SurveyNotificationServiceImpl } from './survey-service-impl'; +import { + SurveyNotificationService, + SurveyNotificationServicePath, +} from '../common/protocol/survey-service'; export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(BackendApplication).toSelf().inSingletonScope(); @@ -401,4 +406,17 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { rebind(TheiaLocalizationBackendContribution).toService( LocalizationBackendContribution ); + + // Survey notification bindings + // It's currently unused. https://github.com/arduino/arduino-ide/pull/1150 + bind(SurveyNotificationServiceImpl).toSelf().inSingletonScope(); + bind(SurveyNotificationService).toService(SurveyNotificationServiceImpl); + bind(ConnectionHandler) + .toDynamicValue( + ({ container }) => + new JsonRpcConnectionHandler(SurveyNotificationServicePath, () => + container.get(SurveyNotificationService) + ) + ) + .inSingletonScope(); });