Skip to content

Commit

Permalink
[webview] fix #5521: emulate webview focus when something is focused …
Browse files Browse the repository at this point in the history
…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 cb1d3d5 commit 3e7a640
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 3e7a640

Please sign in to comment.