Skip to content

Commit

Permalink
Focus revealed external widgets in electron
Browse files Browse the repository at this point in the history
Adds a new service `SecondaryWindowService` to handle creating and focussing external windows based on the platform.

Signed-off-by: Lucas Koehler <[email protected]>
  • Loading branch information
lucas-koehler committed May 16, 2022
1 parent dd58c2e commit ad86315
Show file tree
Hide file tree
Showing 7 changed files with 166 additions and 39 deletions.
33 changes: 5 additions & 28 deletions packages/core/src/browser/secondary-window-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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<Widget>();
Expand All @@ -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<Widget> {
Expand Down Expand Up @@ -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();
}
});
}

/**
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -243,8 +224,4 @@ export class SecondaryWindowHandler {
this.onDidRemoveWidgetEmitter.fire(widget);
}
}

protected nextWindowId(): string {
return `${this.prefix}-subwindow${this.nextId++}`;
}
}
3 changes: 3 additions & 0 deletions packages/core/src/browser/window/browser-window-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
Original file line number Diff line number Diff line change
@@ -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++}`;
}
}
30 changes: 30 additions & 0 deletions packages/core/src/browser/window/secondary-window-service.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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<string, BrowserWindow> = 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.`);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 =>
Expand All @@ -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();
});
12 changes: 1 addition & 11 deletions packages/core/src/electron-main/electron-main-application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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');
}

/**
Expand Down

0 comments on commit ad86315

Please sign in to comment.