From 9601c732084a3a762ecbfba4b4bad6062bb12986 Mon Sep 17 00:00:00 2001 From: Anton Kosyakov Date: Mon, 28 Oct 2019 07:56:32 +0000 Subject: [PATCH] [webview] fix #5648: integrate webviews with the application shell It requires to preserve webviews on reload and reconnection. Signed-off-by: Anton Kosyakov --- .../browser/plugin-ext-frontend-module.ts | 11 ++ .../src/main/browser/webview/webview.ts | 46 ++++++-- .../src/main/browser/webviews-main.ts | 106 +++++++++--------- packages/plugin-ext/src/plugin/webviews.ts | 5 +- 4 files changed, 102 insertions(+), 66 deletions(-) diff --git a/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts b/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts index 5c6dfc110ea9d..71c295e5f52fe 100644 --- a/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts +++ b/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts @@ -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) => { @@ -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, diff --git a/packages/plugin-ext/src/main/browser/webview/webview.ts b/packages/plugin-ext/src/main/browser/webview/webview.ts index 704b9e5d4c682..93bc5a065f788 100644 --- a/packages/plugin-ext/src/main/browser/webview/webview.ts +++ b/packages/plugin-ext/src/main/browser/webview/webview.ts @@ -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'; @@ -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; @@ -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; @@ -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': @@ -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(); } diff --git a/packages/plugin-ext/src/main/browser/webviews-main.ts b/packages/plugin-ext/src/main/browser/webviews-main.ts index 8c0909b17f28d..1746fd1b751e1 100644 --- a/packages/plugin-ext/src/main/browser/webviews-main.ts +++ b/packages/plugin-ext/src/main/browser/webviews-main.ts @@ -14,6 +14,7 @@ * 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'; @@ -21,26 +22,25 @@ 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(); 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(); private readonly viewsOptions = new Map(); - 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); @@ -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 { const toDisposeOnClose = new DisposableCollection(); const toDisposeOnLoad = new DisposableCollection(); - const view = new WebviewWidget(title, { + const view = await this.widgets.getOrCreateWidget(WebviewWidget.FACTORY_ID, { 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 = 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 = 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(styleElement, this.themeRulesService.getCurrentThemeRules()); contentDocument.body.className = `vscode-${ThemeService.get().getCurrentTheme().id}`; - toDisposeOnLoad.push(this.themeService.onThemeChange(() => { - this.themeRulesService.setRules(styleElement, this.themeRulesService.getCurrentThemeRules()); - contentDocument.body.className = `vscode-${ThemeService.get().getCurrentTheme().id}`; - })); - } - }, - this.mouseTracker); + })); + } + }; view.disposed.connect(() => { toDisposeOnClose.dispose(); this.proxy.$onDidDisposeWebviewPanel(panelId); @@ -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; } @@ -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(); } @@ -256,12 +256,12 @@ export class WebviewsMainImpl implements WebviewsMain, Disposable { this.revivers.delete(viewType); } - private async checkViewOptions(handler: string, viewColumn?: number | undefined): Promise { - const options = this.viewsOptions.get(handler); + private async checkViewOptions(handle: string, viewColumn?: number | undefined): Promise { + 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; } @@ -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.FACTORY_ID, { id }); + } + } diff --git a/packages/plugin-ext/src/plugin/webviews.ts b/packages/plugin-ext/src/plugin/webviews.ts index 7611998af941f..5a98442659f39 100644 --- a/packages/plugin-ext/src/plugin/webviews.ts +++ b/packages/plugin-ext/src/plugin/webviews.ts @@ -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(); private readonly serializers = new Map(); @@ -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);