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 0a9af849679c5..a3218434d0b38 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 @@ -59,6 +59,7 @@ import { DebugService } from '@theia/debug/lib/common/debug-service'; import { PluginSharedStyle } from './plugin-shared-style'; import { FSResourceResolver } from './file-system-main'; import { SelectionProviderCommandContribution } from './selection-provider-command'; +import { ViewColumnService } from './view-column-service'; export default new ContainerModule((bind, unbind, isBound, rebind) => { bindHostedPluginPreferences(bind); @@ -139,4 +140,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { rebind(DebugService).toService(PluginDebugService); bind(PluginDebugSessionContributionRegistry).toSelf().inSingletonScope(); rebind(DebugSessionContributionRegistry).toService(PluginDebugSessionContributionRegistry); + + bind(ViewColumnService).toSelf().inSingletonScope(); }); diff --git a/packages/plugin-ext/src/main/browser/view-column-service.ts b/packages/plugin-ext/src/main/browser/view-column-service.ts new file mode 100644 index 0000000000000..192015e6add68 --- /dev/null +++ b/packages/plugin-ext/src/main/browser/view-column-service.ts @@ -0,0 +1,81 @@ +/******************************************************************************** + * Copyright (C) 2019 Red Hat, Inc. 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 { injectable, inject } from 'inversify'; +import { Emitter, Event } from '@theia/core'; +import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell'; + +@injectable() +export class ViewColumnService { + private panelNode: HTMLElement; + private viewColumns = new Map(); + private viewColumnIds = new Map(); + + constructor( + @inject(ApplicationShell) private readonly shell: ApplicationShell, + ) { + this.panelNode = this.shell.mainPanel.node; + + this.panelNode.addEventListener('p-drop', async () => { + await new Promise((resolve => setTimeout(() => resolve()))); + this.updateViewColumnIds(); + const viewColumns = new Map(); + this.viewColumnIds.forEach((ids: string[], viewColumn: number) => { + ids.forEach((id: string) => { + viewColumns.set(id, viewColumn); + if (!this.viewColumns.has(id) || this.viewColumns.get(id) !== viewColumn) { + this.onViewColumnChangedEmitter.fire({ id, viewColumn }); + } + }); + }); + this.viewColumns = viewColumns; + }, true); + } + + updateViewColumnIds(): void { + const dockPanelElements = this.panelNode.getElementsByClassName('p-DockPanel-widget'); + const positionIds = new Map(); + for (let i = 0; i < dockPanelElements.length; i++) { + // tslint:disable-next-line:no-any + const dockPanel = dockPanelElements[i]; + if (dockPanel && dockPanel.style && dockPanel.style.left) { + const pos = parseInt(dockPanel.style.left); + if (!positionIds.has(pos)) { + positionIds.set(pos, []); + } + positionIds.get(pos)!.push(dockPanelElements[i].id); + } + } + this.viewColumnIds.clear(); + [...positionIds.keys()].sort().forEach((key: number, viewColumn: number) => { + positionIds.get(key)!.forEach((id: string) => { + if (!this.viewColumnIds.has(viewColumn)) { + this.viewColumnIds.set(viewColumn, []); + } + this.viewColumnIds.get(viewColumn)!.push(id); + }); + }); + } + + getViewColumnIds(viewColumn: number): string[] { + return this.viewColumnIds.get(viewColumn) || []; + } + + protected readonly onViewColumnChangedEmitter = new Emitter<{ id: string, viewColumn: number }>(); + get onViewColumnChanged(): Event<{ id: string, viewColumn: number }> { + return this.onViewColumnChangedEmitter.event; + } +} diff --git a/packages/plugin-ext/src/main/browser/webview/webview.ts b/packages/plugin-ext/src/main/browser/webview/webview.ts index c40334db4121c..802e2286708b4 100644 --- a/packages/plugin-ext/src/main/browser/webview/webview.ts +++ b/packages/plugin-ext/src/main/browser/webview/webview.ts @@ -33,15 +33,18 @@ export class WebviewWidget extends BaseWidget { private static readonly ID = new IdGenerator('webview-widget-'); protected readonly toDispose = new DisposableCollection(); private iframe: HTMLIFrameElement; - private state: string | undefined = undefined; + private name: string; + private group: number | undefined; + private state: { [key: string]: any } | undefined = undefined; private loadTimeout: number | undefined; - constructor(title: string, private options: WebviewWidgetOptions, private eventDelegate: WebviewEvents) { + constructor(name: string, private options: WebviewWidgetOptions, private eventDelegate: WebviewEvents) { super(); this.node.tabIndex = 0; this.id = WebviewWidget.ID.nextId(); + this.name = name; this.title.closable = true; - this.title.label = title; + this.title.label = this.name; this.addClass(WebviewWidget.Styles.WEBVIEW); } @@ -72,6 +75,34 @@ export class WebviewWidget extends BaseWidget { this.title.iconClass = iconClass; } + getOptions(): WebviewWidgetOptions { + return this.options; + } + + getName(): string { + return this.name; + } + + getTitle() { + return this.getName(); + } + + getState(): { [key: string]: any } | undefined { + return this.state; + } + + setState(state: { [key: string]: any } | undefined) { + this.state = state; + } + + getGroup(): number | undefined { + return this.group; + } + + public updateGroup(value: number | undefined): void { + this.group = value; + } + setHTML(html: string) { html = html.replace(/theia-resource:/g, '/webview/'); const newDocument = new DOMParser().parseFromString(html, 'text/html'); diff --git a/packages/plugin-ext/src/main/browser/webviews-main.ts b/packages/plugin-ext/src/main/browser/webviews-main.ts index 2db2002c573aa..1917ad2045128 100644 --- a/packages/plugin-ext/src/main/browser/webviews-main.ts +++ b/packages/plugin-ext/src/main/browser/webviews-main.ts @@ -25,20 +25,49 @@ import { WebviewWidget } from './webview/webview'; import { ThemeService } from '@theia/core/lib/browser/theming'; import { ThemeRulesService } from './webview/theme-rules-service'; import { DisposableCollection } from '@theia/core'; +import { ViewColumnService } from './view-column-service'; -export class WebviewsMainImpl implements WebviewsMain { +export interface WebviewReviver { + canRevive(webview: WebviewWidget): boolean; + reviveWebview(webview: WebviewWidget): Thenable; +} + +export class WebviewsMainImpl implements WebviewsMain, WebviewReviver { + private static revivalPool = 0; + + private readonly revivers = new Set(); private readonly proxy: WebviewsExt; protected readonly shell: ApplicationShell; + protected readonly viewColumnService: ViewColumnService; protected readonly keybindingRegistry: KeybindingRegistry; protected readonly themeService = ThemeService.get(); protected readonly themeRulesService = ThemeRulesService.get(); private readonly views = new Map(); + private readonly viewsPanelOptions = new Map(); constructor(rpc: RPCProtocol, container: interfaces.Container) { this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.WEBVIEWS_EXT); this.shell = container.get(ApplicationShell); this.keybindingRegistry = container.get(KeybindingRegistry); + this.viewColumnService = container.get(ViewColumnService); + + this.viewColumnService.onViewColumnChanged(e => { + this.updatePanelViewState(e.id, e.viewColumn); + }); + let timeoutHandle: NodeJS.Timer | undefined; + const updateOptions = () => { + if (timeoutHandle) { + clearTimeout(timeoutHandle); + } + timeoutHandle = setTimeout(() => { + for (const key of this.viewsPanelOptions.keys()) { + this.updatePanelViewState(key); + } + }, 100); + }; + this.shell.activeChanged.connect(() => updateOptions()); + this.shell.currentChanged.connect(() => updateOptions()); } $createWebviewPanel( @@ -85,12 +114,26 @@ export class WebviewsMainImpl implements WebviewsMain { this.onCloseView(viewId); }); this.views.set(viewId, view); + this.viewsPanelOptions.set(view.node.id, { panelOptions: showOptions, panelId: viewId, active: true, visible: true }); const widgetOptions: ApplicationShell.WidgetOptions = { area: showOptions.area ? showOptions.area : 'main' }; - // FIXME translate all view columns properly + + let mode = 'open-to-right'; if (showOptions.viewColumn === -2) { const ref = this.shell.currentWidget; if (ref && this.shell.getAreaFor(ref) === widgetOptions.area) { - Object.assign(widgetOptions, { ref, mode: 'open-to-right' }); + Object.assign(widgetOptions, { ref, mode }); + } + } else if (widgetOptions.area === 'main' && showOptions.viewColumn !== undefined) { + this.viewColumnService.updateViewColumnIds(); + let widgetIds = this.viewColumnService.getViewColumnIds(showOptions.viewColumn); + if (widgetIds.length > 0) { + mode = 'tab-after'; + } else if (showOptions.viewColumn > 0) { + widgetIds = this.viewColumnService.getViewColumnIds(showOptions.viewColumn - 1); + } + const ref = this.shell.getWidgets(widgetOptions.area).find(widget => widget.isVisible && widgetIds.indexOf(widget.node.id) !== -1); + if (ref) { + Object.assign(widgetOptions, { ref, mode }); } } this.shell.addWidget(view, widgetOptions); @@ -144,10 +187,61 @@ export class WebviewsMainImpl implements WebviewsMain { return Promise.resolve(webview !== undefined); } $registerSerializer(viewType: string): void { - throw new Error('Method not implemented.'); + this.revivers.add(viewType); } $unregisterSerializer(viewType: string): void { - throw new Error('Method not implemented.'); + this.revivers.delete(viewType); + } + + canRevive(webview: WebviewWidget): boolean { + if (webview.isDisposed || !webview.getState() || !webview.getState()!.viewType) { + return false; + } + + return !this.revivers.has(webview.getState()!.viewType); + } + + reviveWebview(webview: WebviewWidget): Thenable { + const viewType = webview.getState() ? webview.getState()!.viewType : undefined; + return Promise.resolve().then(() => { + const handle = 'revival-' + WebviewsMainImpl.revivalPool++; + this.views.set(handle, webview); + + let state = undefined; + if (webview.getState() && webview.getState()!.state) { + try { + state = JSON.parse(webview.getState()!.state); + } catch { + // noop + } + } + const group = webview.getGroup() ? webview.getGroup() : -2; + const options = webview.getOptions(); + return this.proxy.$deserializeWebviewPanel(handle, viewType, webview.getTitle(), state, group, options).then(undefined, () => { + webview.setHTML(` + An error occurred while restoring view:${viewType}`); + }); + }); + } + + private updatePanelViewState(handler: string, position?: number): void { + const option = this.viewsPanelOptions.get(handler); + if (!option || !option.panelOptions) { + return; + } + const view = this.views.get(option.panelId); + const active: boolean = !!this.shell.activeWidget && !!view && (this.shell.activeWidget.id === view.id); + const visible = !!view && view.isVisible; + if ((position === undefined || option.panelOptions.viewColumn === position) && option.visible === visible && option.active === active) { + return; + } + if (position !== undefined) { + option.panelOptions.viewColumn = position; + } + option.active = active; + option.visible = visible; + this.viewsPanelOptions.set(handler, option); + this.proxy.$onDidChangeWebviewPanelViewState(option.panelId, { active, visible, position: option.panelOptions.viewColumn }); } private getWebview(viewId: string): WebviewWidget { @@ -165,6 +259,7 @@ export class WebviewsMainImpl implements WebviewsMain { } const cleanUp = () => { this.views.delete(viewId); + this.viewsPanelOptions.delete(viewId); }; this.proxy.$onDidDisposeWebviewPanel(viewId).then(cleanUp, cleanUp); }