diff --git a/packages/core/src/browser/secondary-window-handler.ts b/packages/core/src/browser/secondary-window-handler.ts index c1aa87e505ad3..41d595ccfe74a 100644 --- a/packages/core/src/browser/secondary-window-handler.ts +++ b/packages/core/src/browser/secondary-window-handler.ts @@ -20,7 +20,7 @@ import { BoxLayout, BoxPanel, ExtractableWidget, Widget } from './widgets'; import { MessageService } from '../common/message-service'; import { ApplicationShell } from './shell/application-shell'; import { Emitter } from '../common/event'; -import { WindowService } from './window/window-service'; +import { SecondaryWindowService } from './window/secondary-window-service'; /** Widget to be contained directly in a secondary window. */ class SecondaryWindowRootWidget extends Widget { @@ -54,14 +54,6 @@ export class SecondaryWindowHandler { /** List of widgets in secondary windows. */ protected readonly _widgets: ExtractableWidget[] = []; - /** - * Randomized prefix to be included in opened windows' ids. - * This avoids conflicts when creating sub-windows from multiple theia instances (e.g. by opening Theia multiple times in the same browser) - */ - protected readonly prefix = crypto.getRandomValues(new Uint16Array(1))[0]; - /** Unique id. Increase after every access. */ - private nextId = 0; - protected applicationShell: ApplicationShell; protected readonly onDidAddWidgetEmitter = new Emitter(); @@ -75,8 +67,8 @@ export class SecondaryWindowHandler { @inject(MessageService) protected readonly messageService: MessageService; - @inject(WindowService) - protected readonly windowService: WindowService; + @inject(SecondaryWindowService) + protected readonly secondaryWindowService: SecondaryWindowService; /** @returns List of widgets in secondary windows. */ get widgets(): ReadonlyArray { @@ -123,14 +115,6 @@ export class SecondaryWindowHandler { } }); }); - - // Close all open windows when the main window is closed. - this.windowService.onUnload(() => { - // Iterate backwards because calling window.close might remove the window from the array - for (let i = this.secondaryWindows.length - 1; i >= 0; i--) { - this.secondaryWindows[i].close(); - } - }); } /** @@ -148,9 +132,7 @@ export class SecondaryWindowHandler { return; } - // secondary-window.html is part of Theia's generated code. It is generated by dev-packages/application-manager/src/generator/frontend-generator.ts - const newWindow = window.open('secondary-window.html', this.nextWindowId(), 'popup'); - + const newWindow = this.secondaryWindowService.createSecondaryWindow(); if (!newWindow) { this.messageService.error('The widget could not be moved to a secondary window because the window creation failed. Please make sure to allow popups.'); return; @@ -222,8 +204,7 @@ export class SecondaryWindowHandler { revealWidget(widgetId: string): ExtractableWidget | undefined { const trackedWidget = this._widgets.find(w => w.id === widgetId); if (trackedWidget) { - // TODO This is not sufficient for electron - trackedWidget.secondaryWindow?.focus(); + this.secondaryWindowService.focus(trackedWidget.secondaryWindow!); return trackedWidget; } return undefined; @@ -243,8 +224,4 @@ export class SecondaryWindowHandler { this.onDidRemoveWidgetEmitter.fire(widget); } } - - protected nextWindowId(): string { - return `${this.prefix}-subwindow${this.nextId++}`; - } } diff --git a/packages/core/src/browser/window/browser-window-module.ts b/packages/core/src/browser/window/browser-window-module.ts index 5a8d5d70efb8c..29abfd81789d6 100644 --- a/packages/core/src/browser/window/browser-window-module.ts +++ b/packages/core/src/browser/window/browser-window-module.ts @@ -20,10 +20,13 @@ import { DefaultWindowService } from '../../browser/window/default-window-servic import { FrontendApplicationContribution } from '../frontend-application'; import { ClipboardService } from '../clipboard-service'; import { BrowserClipboardService } from '../browser-clipboard-service'; +import { SecondaryWindowService } from './secondary-window-service'; +import { DefaultSecondaryWindowService } from './default-secondary-window-service'; export default new ContainerModule(bind => { bind(DefaultWindowService).toSelf().inSingletonScope(); bind(WindowService).toService(DefaultWindowService); bind(FrontendApplicationContribution).toService(DefaultWindowService); bind(ClipboardService).to(BrowserClipboardService).inSingletonScope(); + bind(SecondaryWindowService).to(DefaultSecondaryWindowService).inSingletonScope(); }); diff --git a/packages/core/src/browser/window/default-secondary-window-service.ts b/packages/core/src/browser/window/default-secondary-window-service.ts new file mode 100644 index 0000000000000..8e31be0795cee --- /dev/null +++ b/packages/core/src/browser/window/default-secondary-window-service.ts @@ -0,0 +1,74 @@ +// ***************************************************************************** +// Copyright (C) 2022 STMicroelectronics, Ericsson, ARM, EclipseSource and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +// ***************************************************************************** +import { inject, injectable, postConstruct } from 'inversify'; +import { SecondaryWindowService } from './secondary-window-service'; +import { WindowService } from './window-service'; + +@injectable() +export class DefaultSecondaryWindowService implements SecondaryWindowService { + // secondary-window.html is part of Theia's generated code. It is generated by dev-packages/application-manager/src/generator/frontend-generator.ts + protected static SECONDARY_WINDOW_URL = 'secondary-window.html'; + + /** + * Randomized prefix to be included in opened windows' ids. + * This avoids conflicts when creating sub-windows from multiple theia instances (e.g. by opening Theia multiple times in the same browser) + */ + protected readonly prefix = crypto.getRandomValues(new Uint32Array(1))[0]; + /** Unique id. Increase after every access. */ + private nextId = 0; + + protected secondaryWindows: Window[] = []; + + @inject(WindowService) + protected readonly windowService: WindowService; + + @postConstruct() + init(): void { + // Close all open windows when the main window is closed. + this.windowService.onUnload(() => { + // Iterate backwards because calling window.close might remove the window from the array + for (let i = this.secondaryWindows.length - 1; i >= 0; i--) { + this.secondaryWindows[i].close(); + } + }); + } + + createSecondaryWindow(): Window | undefined { + const win = this.doCreateSecondaryWindow(); + if (win) { + this.secondaryWindows.push(win); + win.addEventListener('beforeunload', () => { + const extIndex = this.secondaryWindows.indexOf(win); + if (extIndex > -1) { + this.secondaryWindows.splice(extIndex, 1); + } + }); + } + return win ?? undefined; + } + + protected doCreateSecondaryWindow(): Window | undefined { + return window.open(DefaultSecondaryWindowService.SECONDARY_WINDOW_URL, this.nextWindowId(), 'popup') ?? undefined; + } + + focus(win: Window): void { + win.focus(); + } + + protected nextWindowId(): string { + return `${this.prefix}-secondaryWindow-${this.nextId++}`; + } +} diff --git a/packages/core/src/browser/window/secondary-window-service.ts b/packages/core/src/browser/window/secondary-window-service.ts new file mode 100644 index 0000000000000..0c61d59c993e1 --- /dev/null +++ b/packages/core/src/browser/window/secondary-window-service.ts @@ -0,0 +1,30 @@ +// ***************************************************************************** +// Copyright (C) 2022 STMicroelectronics, Ericsson, ARM, EclipseSource and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +// ***************************************************************************** +export const SecondaryWindowService = Symbol('SecondaryWindowService'); + +/** Service for opening new secondary windows to contain widgets extracted from the application shell. */ +export interface SecondaryWindowService { + /** + * Creates a new secondary window for a widget to be extracted from the application shell. + * The created window is closed automatically when the current theia instance is closed. + * + * @returns the created window or `undefined` if it could not be created + */ + createSecondaryWindow(): Window | undefined; + + /** Handles focussing the given secondary window in the browser and on Electron. */ + focus(win: Window): void; +} diff --git a/packages/core/src/electron-browser/window/electron-secondary-window-service.ts b/packages/core/src/electron-browser/window/electron-secondary-window-service.ts new file mode 100644 index 0000000000000..fa513de2af5ab --- /dev/null +++ b/packages/core/src/electron-browser/window/electron-secondary-window-service.ts @@ -0,0 +1,50 @@ +// ***************************************************************************** +// Copyright (C) 2022 STMicroelectronics, Ericsson, ARM, EclipseSource and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +// ***************************************************************************** +import { BrowserWindow } from '../../../electron-shared/electron'; +import * as electronRemote from '../../../electron-shared/@electron/remote'; +import { injectable } from 'inversify'; +import { DefaultSecondaryWindowService } from '../../browser/window/default-secondary-window-service'; + +@injectable() +export class ElectronSecondaryWindowService extends DefaultSecondaryWindowService { + protected electronWindows: Map = new Map(); + + protected override doCreateSecondaryWindow(): Window | undefined { + const id = this.nextWindowId(); + electronRemote.getCurrentWindow().webContents.once('did-create-window', newElectronWindow => { + newElectronWindow.setMenuBarVisibility(false); + this.electronWindows.set(id, newElectronWindow); + }); + const win = window.open(DefaultSecondaryWindowService.SECONDARY_WINDOW_URL, id); + win?.addEventListener('beforeunload', () => { + this.electronWindows.delete(id); + }); + return win ?? undefined; + } + + override focus(win: Window): void { + // window.name is the target name given to the window.open call as the second parameter. + const electronWindow = this.electronWindows.get(win.name); + if (electronWindow) { + if (electronWindow.isMinimized()) { + electronWindow.restore(); + } + electronWindow.focus(); + } else { + console.warn(`There is no known secondary window '${win.name}'. Thus, the window could not be focussed.`); + } + } +} diff --git a/packages/core/src/electron-browser/window/electron-window-module.ts b/packages/core/src/electron-browser/window/electron-window-module.ts index 0318d184aabcd..717d51aa7f60d 100644 --- a/packages/core/src/electron-browser/window/electron-window-module.ts +++ b/packages/core/src/electron-browser/window/electron-window-module.ts @@ -25,6 +25,8 @@ import { ElectronIpcConnectionProvider } from '../messaging/electron-ipc-connect import { bindWindowPreferences } from './electron-window-preferences'; import { FrontendApplicationStateService } from '../../browser/frontend-application-state'; import { ElectronFrontendApplicationStateService } from './electron-frontend-application-state'; +import { ElectronSecondaryWindowService } from './electron-secondary-window-service'; +import { SecondaryWindowService } from '../../browser/window/secondary-window-service'; export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(ElectronMainWindowService).toDynamicValue(context => @@ -35,4 +37,5 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(FrontendApplicationContribution).toService(WindowService); bind(ClipboardService).to(ElectronClipboardService).inSingletonScope(); rebind(FrontendApplicationStateService).to(ElectronFrontendApplicationStateService).inSingletonScope(); + bind(SecondaryWindowService).to(ElectronSecondaryWindowService).inSingletonScope(); }); diff --git a/packages/core/src/electron-main/electron-main-application.ts b/packages/core/src/electron-main/electron-main-application.ts index c76d083188df3..27d395fa7d3db 100644 --- a/packages/core/src/electron-main/electron-main-application.ts +++ b/packages/core/src/electron-main/electron-main-application.ts @@ -16,7 +16,7 @@ import { inject, injectable, named } from 'inversify'; import * as electronRemoteMain from '../../electron-shared/@electron/remote/main'; -import { screen, ipcMain, app, BrowserWindow, Event as ElectronEvent, DidCreateWindowDetails, BrowserWindowConstructorOptions, nativeImage } from '../../electron-shared/electron'; +import { screen, ipcMain, app, BrowserWindow, Event as ElectronEvent, BrowserWindowConstructorOptions, nativeImage } from '../../electron-shared/electron'; import * as path from 'path'; import { Argv } from 'yargs'; import { AddressInfo } from 'net'; @@ -339,16 +339,6 @@ export class ElectronMainApplication { overrideBrowserWindowOptions: options, }; }); - electronWindow.webContents.on('did-create-window', (newWindow: BrowserWindow, details: DidCreateWindowDetails) => { - if (this.isSecondaryWindowUrl(details.url)) { - newWindow.setMenuBarVisibility(false); - } - }); - } - - /** @returns whether the given url references the html file for creating a secondary window for an extracted widget. */ - protected isSecondaryWindowUrl(url: string): boolean { - return !!url && url.endsWith('secondary-window.html'); } /**