Skip to content

Commit

Permalink
[webview] fix #5648: integrate webviews with the application shell
Browse files Browse the repository at this point in the history
It requires to preserve webviews on reload and reconnection.

Signed-off-by: Anton Kosyakov <anton.kosyakov@typefox.io>
  • Loading branch information
akosyakov committed Nov 16, 2019
1 parent 6462d0a commit 9ebf180
Show file tree
Hide file tree
Showing 4 changed files with 102 additions and 66 deletions.
11 changes: 11 additions & 0 deletions packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ import { LanguagesMainFactory, OutputChannelRegistryFactory } from '../../common
import { LanguagesMainImpl } from './languages-main';
import { OutputChannelRegistryMainImpl } from './output-channel-registry-main';
import { InPluginFileSystemWatcherManager } from './in-plugin-filesystem-watcher-manager';
import { WebviewWidget, WebviewWidgetIdentifier } from './webview/webview';

export default new ContainerModule((bind, unbind, isBound, rebind) => {

Expand Down Expand Up @@ -146,6 +147,16 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
}
})).inSingletonScope();

bind(WebviewWidget).toSelf();
bind(WidgetFactory).toDynamicValue(({ container }) => ({
id: WebviewWidget.FACTORY_ID,
createWidget: (identifier: WebviewWidgetIdentifier) => {
const child = container.createChild();
child.bind(WebviewWidgetIdentifier).toConstantValue(identifier);
return child.get(WebviewWidget);
}
})).inSingletonScope();

bind(PluginViewWidget).toSelf();
bind(WidgetFactory).toDynamicValue(({ container }) => ({
id: PLUGIN_VIEW_FACTORY_ID,
Expand Down
46 changes: 34 additions & 12 deletions packages/plugin-ext/src/main/browser/webview/webview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import { injectable, inject, postConstruct } from 'inversify';
import { BaseWidget, Message } from '@theia/core/lib/browser/widgets/widget';
import { IdGenerator } from '../../../common/id-generator';
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
// TODO: get rid of dependencies to the mini browser
import { MiniBrowserContentStyle } from '@theia/mini-browser/lib/browser/mini-browser-content-style';
import { ApplicationShellMouseTracker } from '@theia/core/lib/browser/shell/application-shell-mouse-tracker';

Expand All @@ -31,8 +33,16 @@ export interface WebviewEvents {
onLoad?(contentDocument: Document): void;
}

@injectable()
export class WebviewWidgetIdentifier {
id: string;
}

@injectable()
export class WebviewWidget extends BaseWidget {
private static readonly ID = new IdGenerator('webview-widget-');

static FACTORY_ID = 'plugin-webview';

private iframe: HTMLIFrameElement;
private state: { [key: string]: any } | undefined = undefined;
private loadTimeout: number | undefined;
Expand All @@ -42,15 +52,19 @@ export class WebviewWidget extends BaseWidget {
// XXX This is a hack to be able to tack the mouse events when drag and dropping the widgets. On `mousedown` we put a transparent div over the `iframe` to avoid losing the mouse tacking.
protected readonly transparentOverlay: HTMLElement;

constructor(title: string,
private options: WebviewWidgetOptions,
private eventDelegate: WebviewEvents,
protected readonly mouseTracker: ApplicationShellMouseTracker) {
@inject(WebviewWidgetIdentifier)
protected readonly identifier: WebviewWidgetIdentifier;

@inject(ApplicationShellMouseTracker)
protected readonly mouseTracker: ApplicationShellMouseTracker;

private options: WebviewWidgetOptions = {};
eventDelegate: WebviewEvents = {};

constructor() {
super();
this.node.tabIndex = 0;
this.id = WebviewWidget.ID.nextId();
this.title.closable = true;
this.title.label = title;
this.addClass(WebviewWidget.Styles.WEBVIEW);
this.scrollY = 0;

Expand All @@ -59,18 +73,23 @@ export class WebviewWidget extends BaseWidget {
this.transparentOverlay.style.display = 'none';
this.node.appendChild(this.transparentOverlay);

this.toDispose.push(this.mouseTracker.onMousedown(e => {
this.toDispose.push(this.mouseTracker.onMousedown(() => {
if (this.iframe.style.display !== 'none') {
this.transparentOverlay.style.display = 'block';
}
}));
this.toDispose.push(this.mouseTracker.onMouseup(e => {
this.toDispose.push(this.mouseTracker.onMouseup(() => {
if (this.iframe.style.display !== 'none') {
this.transparentOverlay.style.display = 'none';
}
}));
}

@postConstruct()
protected init(): void {
this.id = WebviewWidget.FACTORY_ID + ':' + this.identifier.id;
}

protected handleMessage(message: any): void {
switch (message.command) {
case 'onmessage':
Expand All @@ -88,11 +107,14 @@ export class WebviewWidget extends BaseWidget {
}

setOptions(options: WebviewWidgetOptions): void {
if (!this.iframe || this.options.allowScripts === options.allowScripts) {
if (this.options.allowScripts === options.allowScripts) {
return;
}
this.updateSandboxAttribute(this.iframe, options.allowScripts);
this.options = options;
if (!this.iframe) {
return;
}
this.updateSandboxAttribute(this.iframe, options.allowScripts);
this.reloadFrame();
}

Expand Down
106 changes: 55 additions & 51 deletions packages/plugin-ext/src/main/browser/webviews-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,33 +14,33 @@
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import debounce = require('lodash.debounce');
import { WebviewsMain, MAIN_RPC_CONTEXT, WebviewsExt } from '../../common/plugin-api-rpc';
import { interfaces } from 'inversify';
import { RPCProtocol } from '../../common/rpc-protocol';
import { UriComponents } from '../../common/uri-components';
import { WebviewOptions, WebviewPanelOptions, WebviewPanelShowOptions } from '@theia/plugin';
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
import { KeybindingRegistry } from '@theia/core/lib/browser/keybinding';
import { WebviewWidget } from './webview/webview';
import { WebviewWidget, WebviewWidgetIdentifier } from './webview/webview';
import { ThemeService } from '@theia/core/lib/browser/theming';
import { ThemeRulesService } from './webview/theme-rules-service';
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
import { ViewColumnService } from './view-column-service';
import { ApplicationShellMouseTracker } from '@theia/core/lib/browser/shell/application-shell-mouse-tracker';

import debounce = require('lodash.debounce');
import { WidgetManager } from '@theia/core/lib/browser/widget-manager';

export class WebviewsMainImpl implements WebviewsMain, Disposable {

private readonly revivers = new Set<string>();
private readonly proxy: WebviewsExt;
protected readonly shell: ApplicationShell;
protected readonly widgets: WidgetManager;
protected readonly viewColumnService: ViewColumnService;
protected readonly keybindingRegistry: KeybindingRegistry;
protected readonly themeService = ThemeService.get();
protected readonly themeRulesService = ThemeRulesService.get();
protected readonly updateViewOptions: () => void;

private readonly views = new Map<string, WebviewWidget>();
private readonly viewsOptions = new Map<string, {
panelOptions: WebviewPanelShowOptions;
options: (WebviewPanelOptions & WebviewOptions) | undefined;
Expand All @@ -49,16 +49,14 @@ export class WebviewsMainImpl implements WebviewsMain, Disposable {
visible: boolean;
}>();

protected readonly mouseTracker: ApplicationShellMouseTracker;

private readonly toDispose = new DisposableCollection();

constructor(rpc: RPCProtocol, container: interfaces.Container) {
this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.WEBVIEWS_EXT);
this.shell = container.get(ApplicationShell);
this.mouseTracker = container.get(ApplicationShellMouseTracker);
this.keybindingRegistry = container.get(KeybindingRegistry);
this.viewColumnService = container.get(ViewColumnService);
this.widgets = container.get(WidgetManager);
this.updateViewOptions = debounce<() => void>(() => {
for (const key of this.viewsOptions.keys()) {
this.checkViewOptions(key);
Expand All @@ -73,50 +71,54 @@ export class WebviewsMainImpl implements WebviewsMain, Disposable {
this.toDispose.dispose();
}

$createWebviewPanel(
async $createWebviewPanel(
panelId: string,
// TODO check webview API completness, implement or get rid of missing APIs
viewType: string,
title: string,
showOptions: WebviewPanelShowOptions,
options: (WebviewPanelOptions & WebviewOptions) | undefined,
// TODO check webview API completness, implement or get rid of missing APIs
extensionLocation: UriComponents
): void {
): Promise<void> {
const toDisposeOnClose = new DisposableCollection();
const toDisposeOnLoad = new DisposableCollection();
const view = new WebviewWidget(title, {
const view = await this.widgets.getOrCreateWidget<WebviewWidget>(WebviewWidget.FACTORY_ID, <WebviewWidgetIdentifier>{ id: panelId });
view.title.label = title;
view.setOptions({
allowScripts: options ? options.enableScripts : false
}, {
onMessage: m => {
this.proxy.$onMessage(panelId, m);
},
onKeyboardEvent: e => {
this.keybindingRegistry.run(e);
},
onLoad: contentDocument => {
const styleId = 'webview-widget-theme';
let styleElement: HTMLStyleElement | null | undefined;
if (!toDisposeOnLoad.disposed) {
// if reload the frame
toDisposeOnLoad.dispose();
styleElement = <HTMLStyleElement>contentDocument.getElementById(styleId);
}
toDisposeOnClose.push(toDisposeOnLoad);
if (!styleElement) {
const parent = contentDocument.head ? contentDocument.head : contentDocument.body;
styleElement = this.themeRulesService.createStyleSheet(parent);
styleElement.id = styleId;
parent.appendChild(styleElement);
}
});
view.eventDelegate = {
onMessage: m => {
this.proxy.$onMessage(panelId, m);
},
onKeyboardEvent: e => {
this.keybindingRegistry.run(e);
},
onLoad: contentDocument => {
const styleId = 'webview-widget-theme';
let styleElement: HTMLStyleElement | null | undefined;
if (!toDisposeOnLoad.disposed) {
// if reload the frame
toDisposeOnLoad.dispose();
styleElement = <HTMLStyleElement>contentDocument.getElementById(styleId);
}
toDisposeOnClose.push(toDisposeOnLoad);
if (!styleElement) {
const parent = contentDocument.head ? contentDocument.head : contentDocument.body;
styleElement = this.themeRulesService.createStyleSheet(parent);
styleElement.id = styleId;
parent.appendChild(styleElement);
}

this.themeRulesService.setRules(styleElement, this.themeRulesService.getCurrentThemeRules());
this.themeRulesService.setRules(styleElement, this.themeRulesService.getCurrentThemeRules());
contentDocument.body.className = `vscode-${ThemeService.get().getCurrentTheme().id}`;
toDisposeOnLoad.push(this.themeService.onThemeChange(() => {
this.themeRulesService.setRules(<HTMLElement>styleElement, this.themeRulesService.getCurrentThemeRules());
contentDocument.body.className = `vscode-${ThemeService.get().getCurrentTheme().id}`;
toDisposeOnLoad.push(this.themeService.onThemeChange(() => {
this.themeRulesService.setRules(<HTMLElement>styleElement, this.themeRulesService.getCurrentThemeRules());
contentDocument.body.className = `vscode-${ThemeService.get().getCurrentTheme().id}`;
}));
}
},
this.mouseTracker);
}));
}
};
view.disposed.connect(() => {
toDisposeOnClose.dispose();
this.proxy.$onDidDisposeWebviewPanel(panelId);
Expand All @@ -126,16 +128,14 @@ export class WebviewsMainImpl implements WebviewsMain, Disposable {
const viewId = view.id;
toDisposeOnClose.push(Disposable.create(() => this.themeRulesService.setIconPath(viewId, undefined)));

this.views.set(panelId, view);
toDisposeOnClose.push(Disposable.create(() => this.views.delete(panelId)));

this.viewsOptions.set(viewId, { panelOptions: showOptions, options: options, panelId, visible: false, active: false });
toDisposeOnClose.push(Disposable.create(() => this.viewsOptions.delete(viewId)));

this.addOrReattachWidget(panelId, showOptions);
}
private addOrReattachWidget(handler: string, showOptions: WebviewPanelShowOptions): void {
const view = this.views.get(handler);

private addOrReattachWidget(handle: string, showOptions: WebviewPanelShowOptions): void {
const view = this.tryGetWebview(handle);
if (!view) {
return;
}
Expand Down Expand Up @@ -184,7 +184,7 @@ export class WebviewsMainImpl implements WebviewsMain, Disposable {
options.active = active;
}
$disposeWebview(handle: string): void {
const view = this.views.get(handle);
const view = this.tryGetWebview(handle);
if (view) {
view.dispose();
}
Expand Down Expand Up @@ -256,12 +256,12 @@ export class WebviewsMainImpl implements WebviewsMain, Disposable {
this.revivers.delete(viewType);
}

private async checkViewOptions(handler: string, viewColumn?: number | undefined): Promise<void> {
const options = this.viewsOptions.get(handler);
private async checkViewOptions(handle: string, viewColumn?: number | undefined): Promise<void> {
const options = this.viewsOptions.get(handle);
if (!options || !options.panelOptions) {
return;
}
const view = this.views.get(options.panelId);
const view = this.tryGetWebview(handle);
if (!view) {
return;
}
Expand All @@ -281,11 +281,15 @@ export class WebviewsMainImpl implements WebviewsMain, Disposable {
}

private getWebview(viewId: string): WebviewWidget {
const webview = this.views.get(viewId);
const webview = this.tryGetWebview(viewId);
if (!webview) {
throw new Error(`Unknown Webview: ${viewId}`);
}
return webview;
}

private tryGetWebview(id: string): WebviewWidget | undefined {
return this.widgets.tryGetWidget<WebviewWidget>(WebviewWidget.FACTORY_ID, <WebviewWidgetIdentifier>{ id });
}

}
5 changes: 2 additions & 3 deletions packages/plugin-ext/src/plugin/webviews.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,17 @@
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import { v4 } from 'uuid';
import { WebviewsExt, WebviewPanelViewState, WebviewsMain, PLUGIN_RPC_CONTEXT, /* WebviewsMain, PLUGIN_RPC_CONTEXT */ } from '../common/plugin-api-rpc';
import * as theia from '@theia/plugin';
import { RPCProtocol } from '../common/rpc-protocol';
import URI from 'vscode-uri/lib/umd';
import { Emitter, Event } from '@theia/core/lib/common/event';
import { fromViewColumn, toViewColumn, toWebviewPanelShowOptions } from './type-converters';
import { IdGenerator } from '../common/id-generator';
import { Disposable, WebviewPanelTargetArea } from './types-impl';

export class WebviewsExtImpl implements WebviewsExt {
private readonly proxy: WebviewsMain;
private readonly idGenerator = new IdGenerator('v');
private readonly webviewPanels = new Map<string, WebviewPanelImpl>();
private readonly serializers = new Map<string, theia.WebviewPanelSerializer>();

Expand Down Expand Up @@ -85,7 +84,7 @@ export class WebviewsExtImpl implements WebviewsExt {
extensionLocation: URI): theia.WebviewPanel {

const webviewShowOptions = toWebviewPanelShowOptions(showOptions);
const viewId = this.idGenerator.nextId();
const viewId = v4();
this.proxy.$createWebviewPanel(viewId, viewType, title, webviewShowOptions, options, extensionLocation);

const webview = new WebviewImpl(viewId, this.proxy, options);
Expand Down

0 comments on commit 9ebf180

Please sign in to comment.