Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement "secondary window" support for Electron #12481

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/electron/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"@theia/scm": "1.38.0",
"@theia/scm-extra": "1.38.0",
"@theia/search-in-workspace": "1.38.0",
"@theia/secondary-window": "1.38.0",
"@theia/task": "1.38.0",
"@theia/terminal": "1.38.0",
"@theia/timeline": "1.38.0",
Expand Down
3 changes: 3 additions & 0 deletions examples/electron/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@
{
"path": "../../packages/search-in-workspace"
},
{
"path": "../../packages/secondary-window"
},
{
"path": "../../packages/task"
},
Expand Down
33 changes: 19 additions & 14 deletions packages/core/src/browser/dialogs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,9 @@ export class DialogOverlayService implements FrontendApplicationContribution {

// eslint-disable-next-line @typescript-eslint/no-explicit-any
protected readonly dialogs: AbstractDialog<any>[] = [];
protected readonly documents: Document[] = [];

constructor() {
addKeyListener(document.body, Key.ENTER, e => this.handleEnter(e));
addKeyListener(document.body, Key.ESCAPE, e => this.handleEscape(e));
}

initialize(): void {
Expand All @@ -101,6 +100,11 @@ export class DialogOverlayService implements FrontendApplicationContribution {

// eslint-disable-next-line @typescript-eslint/no-explicit-any
push(dialog: AbstractDialog<any>): Disposable {
if (this.documents.findIndex(document => document === dialog.node.ownerDocument) < 0) {
addKeyListener(dialog.node.ownerDocument.body, Key.ENTER, e => this.handleEnter(e));
addKeyListener(dialog.node.ownerDocument.body, Key.ESCAPE, e => this.handleEscape(e));
this.documents.push(dialog.node.ownerDocument);
}
this.dialogs.unshift(dialog);
return Disposable.create(() => {
const index = this.dialogs.indexOf(dialog);
Expand Down Expand Up @@ -147,17 +151,18 @@ export abstract class AbstractDialog<T> extends BaseWidget {
protected activeElement: HTMLElement | undefined;

constructor(
@inject(DialogProps) protected readonly props: DialogProps
protected readonly props: DialogProps,
options?: Widget.IOptions
) {
super();
super(options);
this.id = 'theia-dialog-shell';
this.addClass('dialogOverlay');
this.toDispose.push(Disposable.create(() => {
if (this.reject) {
Widget.detach(this);
}
}));
const container = document.createElement('div');
const container = this.node.ownerDocument.createElement('div');
container.classList.add('dialogBlock');
if (props.maxWidth === undefined) {
container.setAttribute('style', 'max-width: none');
Expand All @@ -166,31 +171,31 @@ export abstract class AbstractDialog<T> extends BaseWidget {
}
this.node.appendChild(container);

const titleContentNode = document.createElement('div');
const titleContentNode = this.node.ownerDocument.createElement('div');
titleContentNode.classList.add('dialogTitle');
container.appendChild(titleContentNode);

this.titleNode = document.createElement('div');
this.titleNode = this.node.ownerDocument.createElement('div');
this.titleNode.textContent = props.title;
titleContentNode.appendChild(this.titleNode);

this.closeCrossNode = document.createElement('i');
this.closeCrossNode = this.node.ownerDocument.createElement('i');
this.closeCrossNode.classList.add(...codiconArray('close'));
this.closeCrossNode.classList.add('closeButton');
titleContentNode.appendChild(this.closeCrossNode);

this.contentNode = document.createElement('div');
this.contentNode = this.node.ownerDocument.createElement('div');
this.contentNode.classList.add('dialogContent');
if (props.wordWrap !== undefined) {
this.contentNode.setAttribute('style', `word-wrap: ${props.wordWrap}`);
}
container.appendChild(this.contentNode);

this.controlPanel = document.createElement('div');
this.controlPanel = this.node.ownerDocument.createElement('div');
this.controlPanel.classList.add('dialogControl');
container.appendChild(this.controlPanel);

this.errorMessageNode = document.createElement('div');
this.errorMessageNode = this.node.ownerDocument.createElement('div');
this.errorMessageNode.classList.add('error');
this.errorMessageNode.setAttribute('style', 'flex: 2');
this.controlPanel.appendChild(this.errorMessageNode);
Expand Down Expand Up @@ -255,7 +260,7 @@ export abstract class AbstractDialog<T> extends BaseWidget {
if (this.resolve) {
return Promise.reject(new Error('The dialog is already opened.'));
}
this.activeElement = window.document.activeElement as HTMLElement;
this.activeElement = this.node.ownerDocument.activeElement as HTMLElement;
return new Promise<T | undefined>((resolve, reject) => {
this.resolve = resolve;
this.reject = reject;
Expand All @@ -264,7 +269,7 @@ export abstract class AbstractDialog<T> extends BaseWidget {
this.reject = undefined;
}));

Widget.attach(this, document.body);
Widget.attach(this, this.node.ownerDocument.body);
this.activate();
});
}
Expand Down Expand Up @@ -388,7 +393,7 @@ export class ConfirmDialog extends AbstractDialog<boolean> {

protected createMessageNode(msg: string | HTMLElement): HTMLElement {
if (typeof msg === 'string') {
const messageNode = document.createElement('div');
const messageNode = this.node.ownerDocument.createElement('div');
messageNode.textContent = msg;
return messageNode;
}
Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/browser/saveable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,9 +287,11 @@ export class ShouldSaveDialog extends AbstractDialog<boolean> {
constructor(widget: Widget) {
super({
title: nls.localizeByDefault('Do you want to save the changes you made to {0}?', widget.title.label || widget.title.caption)
}, {
node: widget.node.ownerDocument.createElement('div')
});

const messageNode = document.createElement('div');
const messageNode = this.node.ownerDocument.createElement('div');
messageNode.textContent = nls.localizeByDefault("Your changes will be lost if you don't save them.");
messageNode.setAttribute('style', 'flex: 1 100%; padding-bottom: calc(var(--theia-ui-padding)*3);');
this.contentNode.appendChild(messageNode);
Expand Down
45 changes: 7 additions & 38 deletions packages/core/src/browser/secondary-window-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { Emitter } from '../common/event';
import { SecondaryWindowService } from './window/secondary-window-service';
import { KeybindingRegistry } from './keybinding';
import { ColorApplicationContribution } from './color-application-contribution';
import { StylingService } from './styling-service';

/** Widget to be contained directly in a secondary window. */
class SecondaryWindowRootWidget extends Widget {
Expand Down Expand Up @@ -50,8 +51,6 @@ class SecondaryWindowRootWidget extends Widget {
*/
@injectable()
export class SecondaryWindowHandler {
/** List of currently open secondary windows. Window references should be removed once the window is closed. */
protected readonly secondaryWindows: Window[] = [];
/** List of widgets in secondary windows. */
protected readonly _widgets: ExtractableWidget[] = [];

Expand All @@ -63,6 +62,9 @@ export class SecondaryWindowHandler {
@inject(ColorApplicationContribution)
protected colorAppContribution: ColorApplicationContribution;

@inject(StylingService)
protected stylingService: StylingService;

protected readonly onDidAddWidgetEmitter = new Emitter<Widget>();
/** Subscribe to get notified when a widget is added to this handler, i.e. the widget was moved to an secondary window . */
readonly onDidAddWidget = this.onDidAddWidgetEmitter.event;
Expand Down Expand Up @@ -95,33 +97,6 @@ export class SecondaryWindowHandler {
return;
}
this.applicationShell = shell;

// Set up messaging with secondary windows
window.addEventListener('message', (event: MessageEvent) => {
console.trace('Message on main window', event);
if (event.data.fromSecondary) {
console.trace('Message comes from secondary window');
return;
}
if (event.data.fromMain) {
console.trace('Message has mainWindow marker, therefore ignore it');
return;
}

// Filter setImmediate messages. Do not forward because these come in with very high frequency.
// They are not needed in secondary windows because these messages are just a work around
// to make setImmediate work in the main window: https://developer.mozilla.org/en-US/docs/Web/API/Window/setImmediate
if (typeof event.data === 'string' && event.data.startsWith('setImmediate')) {
return;
}

console.trace('Delegate main window message to secondary windows', event);
this.secondaryWindows.forEach(secondaryWindow => {
if (!secondaryWindow.window.closed) {
secondaryWindow.window.postMessage({ ...event.data, fromMain: true }, '*');
}
});
});
}

/**
Expand All @@ -139,21 +114,13 @@ export class SecondaryWindowHandler {
return;
}

const newWindow = this.secondaryWindowService.createSecondaryWindow(closed => {
this.applicationShell.closeWidget(widget.id);
const extIndex = this.secondaryWindows.indexOf(closed);
if (extIndex > -1) {
this.secondaryWindows.splice(extIndex, 1);
}
});
const newWindow = this.secondaryWindowService.createSecondaryWindow(widget, this.applicationShell);

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;
}

this.secondaryWindows.push(newWindow);

const mainWindowTitle = document.title;
newWindow.onload = () => {
this.keybindings.registerEventListeners(newWindow);
Expand All @@ -168,6 +135,7 @@ export class SecondaryWindowHandler {
return;
}
const unregisterWithColorContribution = this.colorAppContribution.registerWindow(newWindow);
const unregisterWithStylingService = this.stylingService.registerWindow(newWindow);

widget.secondaryWindow = newWindow;
const rootWidget = new SecondaryWindowRootWidget();
Expand All @@ -182,6 +150,7 @@ export class SecondaryWindowHandler {
// Close the window if the widget is disposed, e.g. by a command closing all widgets.
widget.disposed.connect(() => {
unregisterWithColorContribution.dispose();
unregisterWithStylingService.dispose();
this.removeWidget(widget);
if (!newWindow.closed) {
newWindow.close();
Expand Down
23 changes: 17 additions & 6 deletions packages/core/src/browser/styling-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { ColorRegistry } from './color-registry';
import { DecorationStyle } from './decoration-style';
import { FrontendApplicationContribution } from './frontend-application';
import { ThemeService } from './theming';
import { Disposable } from '../common';

export const StylingParticipant = Symbol('StylingParticipant');

Expand All @@ -40,8 +41,7 @@ export interface CssStyleCollector {

@injectable()
export class StylingService implements FrontendApplicationContribution {

protected cssElement = DecorationStyle.createStyleElement('contributedColorTheme');
protected cssElements = new Map<Window, HTMLStyleElement>();

@inject(ThemeService)
protected readonly themeService: ThemeService;
Expand All @@ -53,11 +53,22 @@ export class StylingService implements FrontendApplicationContribution {
protected readonly themingParticipants: ContributionProvider<StylingParticipant>;

onStart(): void {
this.applyStyling(this.themeService.getCurrentTheme());
this.themeService.onDidColorThemeChange(e => this.applyStyling(e.newTheme));
this.registerWindow(window);
this.themeService.onDidColorThemeChange(e => this.applyStylingToWindows(e.newTheme));
}

registerWindow(win: Window): Disposable {
const cssElement = DecorationStyle.createStyleElement('contributedColorTheme', win.document.head);
this.cssElements.set(win, cssElement);
this.applyStyling(this.themeService.getCurrentTheme(), cssElement);
return Disposable.create(() => this.cssElements.delete(win));
}

protected applyStylingToWindows(theme: Theme): void {
this.cssElements.forEach(cssElement => this.applyStyling(theme, cssElement));
}

protected applyStyling(theme: Theme): void {
protected applyStyling(theme: Theme, cssElement: HTMLStyleElement): void {
const rules: string[] = [];
const colorTheme: ColorTheme = {
type: theme.type,
Expand All @@ -71,6 +82,6 @@ export class StylingService implements FrontendApplicationContribution {
themingParticipant.registerThemeStyle(colorTheme, styleCollector);
}
const fullCss = rules.join('\n');
this.cssElement.innerText = fullCss;
cssElement.innerText = fullCss;
}
}
4 changes: 4 additions & 0 deletions packages/core/src/browser/widgets/widget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ export class BaseWidget extends Widget {
protected scrollBar?: PerfectScrollbar;
protected scrollOptions?: PerfectScrollbar.Options;

constructor(options?: Widget.IOptions) {
super(options);
}

override dispose(): void {
if (this.isDisposed) {
return;
Expand Down
Loading