Skip to content

Commit

Permalink
[webview] fix eclipse-theia#5521: emulate webview focus when somethin…
Browse files Browse the repository at this point in the history
…g is focused in iframe

Otherwise the webview is not detected as active and title actions are not available.

Signed-off-by: Anton Kosyakov <anton.kosyakov@typefox.io>
  • Loading branch information
akosyakov committed Nov 11, 2019
1 parent 21e10bc commit ffb4dba
Show file tree
Hide file tree
Showing 6 changed files with 93 additions and 27 deletions.
5 changes: 5 additions & 0 deletions packages/core/src/browser/shell/application-shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1005,6 +1005,11 @@ export class ApplicationShell extends Widget {
private readonly toDisposeOnActivationCheck = new DisposableCollection();
private assertActivated(widget: Widget): void {
this.toDisposeOnActivationCheck.dispose();

const onDispose = () => this.toDisposeOnActivationCheck.dispose();
widget.disposed.connect(onDispose);
this.toDisposeOnActivationCheck.push(Disposable.create(() => widget.disposed.disconnect(onDispose)));

let start = 0;
const step: FrameRequestCallback = timestamp => {
if (document.activeElement && widget.node.contains(document.activeElement)) {
Expand Down
2 changes: 2 additions & 0 deletions packages/plugin-ext/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,15 @@
"@theia/terminal": "^0.12.0",
"@theia/workspace": "^0.12.0",
"@types/connect": "^3.4.32",
"@types/mime": "^2.0.1",
"@types/serve-static": "^1.13.3",
"connect": "^3.7.0",
"decompress": "^4.2.0",
"escape-html": "^1.0.3",
"jsonc-parser": "^2.0.2",
"lodash.clonedeep": "^4.5.0",
"macaddress": "^0.2.9",
"mime": "^2.4.4",
"ps-tree": "^1.2.0",
"request": "^2.82.0",
"serve-static": "^1.14.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { injectable, inject } from 'inversify';
import { MenuPath, ILogger, CommandRegistry, Command, Mutable, MenuAction, SelectionService, CommandHandler, Disposable, DisposableCollection } from '@theia/core';
import { EDITOR_CONTEXT_MENU, EditorWidget } from '@theia/editor/lib/browser';
import { MenuModelRegistry } from '@theia/core/lib/common';
import { Emitter } from '@theia/core/lib/common/event';
import { TabBarToolbarRegistry, TabBarToolbarItem } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { NAVIGATOR_CONTEXT_MENU } from '@theia/navigator/lib/browser/navigator-contribution';
import { QuickCommandService } from '@theia/core/lib/browser/quick-open/quick-command-service';
Expand All @@ -38,6 +39,7 @@ import { PluginViewWidget } from '../view/plugin-view-widget';
import { ViewContextKeyService } from '../view/view-context-key-service';
import { WebviewWidget } from '../webview/webview';
import { Navigatable } from '@theia/core/lib/browser/navigatable';
import { ContextKeyService } from '@theia/core/lib/browser/context-key-service';

type CodeEditorWidget = EditorWidget | WebviewWidget;
export namespace CodeEditorWidget {
Expand Down Expand Up @@ -80,6 +82,9 @@ export class MenusContributionPointHandler {
@inject(ViewContextKeyService)
protected readonly viewContextKeys: ViewContextKeyService;

@inject(ContextKeyService)
protected readonly contextKeyService: ContextKeyService;

handle(contributions: PluginContribution): Disposable {
const allMenus = contributions.menus;
if (!allMenus) {
Expand Down Expand Up @@ -194,6 +199,23 @@ export class MenusContributionPointHandler {
toDispose.push(this.commands.registerCommand(command, handler));

const { when } = action;
const whenKeys = when && this.contextKeyService.parseKeys(when);
let onDidChange;
if (whenKeys && whenKeys.size) {
const onDidChangeEmitter = new Emitter<void>();
toDispose.push(onDidChangeEmitter);
onDidChange = onDidChangeEmitter.event;
this.contextKeyService.onDidChange.maxListeners = this.contextKeyService.onDidChange.maxListeners + 1;
toDispose.push(this.contextKeyService.onDidChange(event => {
if (event.affects(whenKeys)) {
onDidChangeEmitter.fire(undefined);
}
}));
toDispose.push(Disposable.create(() => {
this.contextKeyService.onDidChange.maxListeners = this.contextKeyService.onDidChange.maxListeners - 1;
}));
}

// handle group and priority
// if group is empty or white space is will be set to navigation
// ' ' => ['navigation', 0]
Expand All @@ -202,7 +224,7 @@ export class MenusContributionPointHandler {
// if priority is not a number it will be set to 0
// navigation@test => ['navigation', 0]
const [group, sort] = (action.group || 'navigation').split('@');
const item: Mutable<TabBarToolbarItem> = { id, command: id, group: group.trim() || 'navigation', priority: ~~sort || undefined, when };
const item: Mutable<TabBarToolbarItem> = { id, command: id, group: group.trim() || 'navigation', priority: ~~sort || undefined, when, onDidChange };
toDispose.push(this.tabBarToolbar.registerItem(item));

toDispose.push(this.onDidRegisterCommand(action.command, pluginCommand => {
Expand Down
57 changes: 37 additions & 20 deletions packages/plugin-ext/src/main/browser/webview/webview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import * as mime from 'mime';
import { injectable, inject, postConstruct } from 'inversify';
import { ArrayExt } from '@phosphor/algorithm/lib/array';
import { WebviewPanelOptions, WebviewPortMapping } from '@theia/plugin';
import { BaseWidget, Message } from '@theia/core/lib/browser/widgets/widget';
import { Disposable } from '@theia/core/lib/common/disposable';
Expand All @@ -32,12 +32,15 @@ import { Emitter } from '@theia/core/lib/common/event';
import { open, OpenerService } from '@theia/core/lib/browser/opener-service';
import { KeybindingRegistry } from '@theia/core/lib/browser/keybinding';
import { Schemes } from '../../../common/uri-components';
import { JSONExt } from '@phosphor/coreutils';

// tslint:disable:no-any

export const enum WebviewMessageChannels {
onmessage = 'onmessage',
didClickLink = 'did-click-link',
didFocus = 'did-focus',
didBlur = 'did-blur',
doUpdateState = 'do-update-state',
doReload = 'do-reload',
loadResource = 'load-resource',
Expand Down Expand Up @@ -71,7 +74,7 @@ export class WebviewWidget extends BaseWidget implements StatefulWidget {

static FACTORY_ID = 'plugin-webview';

protected element: HTMLIFrameElement;
protected element: HTMLIFrameElement | undefined;

// tslint:disable-next-line:max-line-length
// XXX This is a hack to be able to tack the mouse events when drag and dropping the widgets.
Expand Down Expand Up @@ -106,8 +109,16 @@ export class WebviewWidget extends BaseWidget implements StatefulWidget {
};

protected html = '';
protected contentOptions: WebviewContentOptions = {};
state: any;

protected _contentOptions: WebviewContentOptions = {};
get contentOptions(): WebviewContentOptions {
return this._contentOptions;
}

protected _state: any;
get state(): any {
return this._state;
}

viewType: string;
options: WebviewPanelOptions = {};
Expand All @@ -132,12 +143,12 @@ export class WebviewWidget extends BaseWidget implements StatefulWidget {
this.node.appendChild(this.transparentOverlay);

this.toDispose.push(this.mouseTracker.onMousedown(() => {
if (this.element.style.display !== 'none') {
if (this.element && this.element.style.display !== 'none') {
this.transparentOverlay.style.display = 'block';
}
}));
this.toDispose.push(this.mouseTracker.onMouseup(() => {
if (this.element.style.display !== 'none') {
if (this.element && this.element.style.display !== 'none') {
this.transparentOverlay.style.display = 'none';
}
}));
Expand All @@ -151,6 +162,12 @@ export class WebviewWidget extends BaseWidget implements StatefulWidget {
element.style.height = '100%';
this.element = element;
this.node.appendChild(this.element);
this.toDispose.push(Disposable.create(() => {
if (this.element) {
this.element.remove();
this.element = undefined;
}
}));

const subscription = this.on(WebviewMessageChannels.webviewReady, () => {
subscription.dispose();
Expand All @@ -160,7 +177,14 @@ export class WebviewWidget extends BaseWidget implements StatefulWidget {
this.toDispose.push(this.on(WebviewMessageChannels.onmessage, (data: any) => this.onMessageEmitter.fire(data)));
this.toDispose.push(this.on(WebviewMessageChannels.didClickLink, (uri: string) => this.openLink(new URI(uri))));
this.toDispose.push(this.on(WebviewMessageChannels.doUpdateState, (state: any) => {
this.state = state;
this._state = state;
}));
this.toDispose.push(this.on(WebviewMessageChannels.didFocus, () =>
// emulate the webview focus without actually changing focus
this.node.dispatchEvent(new FocusEvent('focus'))
));
this.toDispose.push(this.on(WebviewMessageChannels.didBlur, () => {
/* no-op: webview loses focus only if another element gains focus in the main window */
}));
this.toDispose.push(this.on(WebviewMessageChannels.doReload, () => this.reload()));
this.toDispose.push(this.on(WebviewMessageChannels.loadResource, (entry: any) => {
Expand All @@ -178,10 +202,10 @@ export class WebviewWidget extends BaseWidget implements StatefulWidget {
}

setContentOptions(contentOptions: WebviewContentOptions): void {
if (WebviewWidget.compareWebviewContentOptions(this.contentOptions, contentOptions)) {
if (JSONExt.deepEqual(<any>this.contentOptions, <any>contentOptions)) {
return;
}
this.contentOptions = contentOptions;
this._contentOptions = contentOptions;
this.doUpdateContent();
}

Expand All @@ -206,6 +230,7 @@ export class WebviewWidget extends BaseWidget implements StatefulWidget {

protected onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
this.node.focus();
this.focus();
}

Expand Down Expand Up @@ -256,7 +281,7 @@ export class WebviewWidget extends BaseWidget implements StatefulWidget {
return this.doSend('did-load-resource', {
status: 200,
path: requestPath,
mime: 'text/plain', // TODO detect mimeType from URI extension
mime: mime.getType(normalizedUri.path.toString()) || 'application/octet-stream',
data: content
});
}
Expand Down Expand Up @@ -313,8 +338,8 @@ export class WebviewWidget extends BaseWidget implements StatefulWidget {
this.viewType = viewType;
this.title.label = title;
this.options = options;
this.contentOptions = contentOptions;
this.state = state;
this._contentOptions = contentOptions;
this._state = state;
}

protected async doSend(channel: string, data?: any): Promise<void> {
Expand Down Expand Up @@ -360,12 +385,4 @@ export namespace WebviewWidget {
state: any
// TODO: preserve icon class
}
export function compareWebviewContentOptions(a: WebviewContentOptions, b: WebviewContentOptions): boolean {
return a.enableCommandUris === b.enableCommandUris
&& a.allowScripts === b.allowScripts &&
ArrayExt.shallowEqual(a.localResourceRoots || [], b.localResourceRoots || [], (uri, uri2) => uri === uri2) &&
ArrayExt.shallowEqual(a.portMapping || [], b.portMapping || [], (m, m2) =>
m.extensionHostPort === m2.extensionHostPort && m.webviewPort === m2.webviewPort
);
}
}
22 changes: 16 additions & 6 deletions packages/plugin-ext/src/main/browser/webviews-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@
********************************************************************************/

import debounce = require('lodash.debounce');
import { WebviewsMain, MAIN_RPC_CONTEXT, WebviewsExt, WebviewPanelViewState } from '../../common/plugin-api-rpc';
import URI from 'vscode-uri';
import { interfaces } from 'inversify';
import { WebviewsMain, MAIN_RPC_CONTEXT, WebviewsExt, WebviewPanelViewState } from '../../common/plugin-api-rpc';
import { RPCProtocol } from '../../common/rpc-protocol';
import { WebviewOptions, WebviewPanelOptions, WebviewPanelShowOptions } from '@theia/plugin';
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
Expand All @@ -27,6 +28,7 @@ import { WidgetManager } from '@theia/core/lib/browser/widget-manager';
import { JSONExt } from '@phosphor/coreutils/lib/json';
import { Mutable } from '@theia/core/lib/common/types';
import { HostedPluginSupport } from '../../hosted/browser/hosted-plugin';

export class WebviewsMainImpl implements WebviewsMain, Disposable {

private readonly proxy: WebviewsExt;
Expand Down Expand Up @@ -75,10 +77,12 @@ export class WebviewsMainImpl implements WebviewsMain, Disposable {
protected hookWebview(view: WebviewWidget): void {
const handle = view.identifier.id;
this.toDispose.push(view.onMessage(data => this.proxy.$onMessage(handle, data)));

const onDispose = () => this.proxy.$onDidDisposeWebviewPanel(handle);
view.disposed.connect(onDispose);
this.toDispose.push(Disposable.create(() => view.disposed.disconnect(onDispose)));
view.disposed.connect(() => {
if (this.toDispose.disposed) {
return;
}
this.proxy.$onDidDisposeWebviewPanel(handle);
});
}

private async addOrReattachWidget(handle: string, showOptions: WebviewPanelShowOptions): Promise<void> {
Expand Down Expand Up @@ -197,9 +201,15 @@ export class WebviewsMainImpl implements WebviewsMain, Disposable {
const title = widget.title.label;
const state = widget.state;
const options = widget.options;
const { allowScripts, localResourceRoots, ...contentOptions } = widget.contentOptions;
this.viewColumnService.updateViewColumns();
const position = this.viewColumnService.getViewColumn(widget.id) || 0;
await this.proxy.$deserializeWebviewPanel(handle, widget.viewType, title, state, position, options);
await this.proxy.$deserializeWebviewPanel(handle, widget.viewType, title, state, position, {
enableScripts: allowScripts,
localResourceRoots: localResourceRoots && localResourceRoots.map(root => URI.parse(root)),
...contentOptions,
...options
});
}

protected readonly updateViewStates = debounce(() => {
Expand Down
10 changes: 10 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -989,6 +989,11 @@
version "2.0.0"
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.0.tgz#5a7306e367c539b9f6543499de8dd519fac37a8b"

"@types/mime@^2.0.1":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.1.tgz#dc488842312a7f075149312905b5e3c0b054c79d"
integrity sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw==

"@types/minimatch@*", "@types/minimatch@3.0.3":
version "3.0.3"
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
Expand Down Expand Up @@ -7389,6 +7394,11 @@ mime@^2.0.3:
version "2.3.1"
resolved "https://registry.yarnpkg.com/mime/-/mime-2.3.1.tgz#b1621c54d63b97c47d3cfe7f7215f7d64517c369"

mime@^2.4.4:
version "2.4.4"
resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.4.tgz#bd7b91135fc6b01cde3e9bae33d659b63d8857e5"
integrity sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==

mimic-fn@^1.0.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022"
Expand Down

0 comments on commit ffb4dba

Please sign in to comment.