diff --git a/src/sql/base/browser/ui/modal/modal.ts b/src/sql/base/browser/ui/modal/modal.ts index 62df73f890..bd0e6d47e4 100644 --- a/src/sql/base/browser/ui/modal/modal.ts +++ b/src/sql/base/browser/ui/modal/modal.ts @@ -410,6 +410,10 @@ export abstract class Modal extends Disposable implements IThemable { } } + protected get title(): string { + return this._title; + } + /** * Set the icon title class name * @param iconClassName diff --git a/src/sql/base/browser/ui/modal/webViewDialog.ts b/src/sql/base/browser/ui/modal/webViewDialog.ts new file mode 100644 index 0000000000..31c6e0f11f --- /dev/null +++ b/src/sql/base/browser/ui/modal/webViewDialog.ts @@ -0,0 +1,173 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import 'vs/css!sql/media/icons/common-icons'; +import { Button } from 'sql/base/browser/ui/button/button'; +import { Modal } from 'sql/base/browser/ui/modal/modal'; +import * as TelemetryKeys from 'sql/common/telemetryKeys'; +import { attachButtonStyler, attachModalDialogStyler } from 'sql/common/theme/styler'; +import { Builder } from 'vs/base/browser/builder'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { IPartService, Parts } from 'vs/workbench/services/part/common/partService'; +import Event, { Emitter } from 'vs/base/common/event'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; +import { localize } from 'vs/nls'; +import WebView from 'vs/workbench/parts/html/browser/webview'; +import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import data = require('data'); + +export class WebViewDialog extends Modal { + + private _body: HTMLElement; + private _okButton: Button; + private _okLabel: string; + private _closeLabel: string; + private _webview: WebView; + private _html: string; + private _headerTitle: string; + + private _onOk = new Emitter(); + public onOk: Event = this._onOk.event; + private _onClosed = new Emitter(); + public onClosed: Event = this._onClosed.event; + private contentDisposables: IDisposable[] = []; + private _onMessage = new Emitter(); + + constructor( + @IThemeService private _themeService: IThemeService, + @IClipboardService private _clipboardService: IClipboardService, + @IPartService private _webViewPartService: IPartService, + @ITelemetryService telemetryService: ITelemetryService, + @IContextKeyService contextKeyService: IContextKeyService, + @IContextViewService private _contextViewService: IContextViewService, + @IEnvironmentService private _environmentService: IEnvironmentService, + ) { + super('', TelemetryKeys.WebView, _webViewPartService, telemetryService, contextKeyService, { isFlyout: false, hasTitleIcon: true }); + this._okLabel = localize('OK', 'OK'); + this._closeLabel = localize('close', 'Close'); + } + + public set html(value: string) { + this._html = value; + } + + public get html(): string { + return this._html; + } + + public set okTitle(value: string) { + this._okLabel = value; + } + + public get okTitle(): string { + return this._okLabel; + } + + public set closeTitle(value: string) { + this._closeLabel = value; + } + + public get closeTitle(): string { + return this._closeLabel; + } + + public set headerTitle(value: string) { + this._headerTitle = value + } + + public get headerTitle(): string { + return this._headerTitle; + } + + protected renderBody(container: HTMLElement) { + new Builder(container).div({ 'class': 'webview-dialog' }, (bodyBuilder) => { + this._body = bodyBuilder.getHTMLElement(); + this._webview = new WebView(this._body, this._webViewPartService.getContainer(Parts.EDITOR_PART), + this._contextViewService, + undefined, + undefined, + { + allowScripts: true, + enableWrappedPostMessage: true, + hideFind: true + } + ); + + this._webview.style(this._themeService.getTheme()); + + this._webview.onMessage(message => { + this._onMessage.fire(message); + }, null, this.contentDisposables); + + this._themeService.onThemeChange(theme => this._webview.style(theme), null, this.contentDisposables); + + this.contentDisposables.push(this._webview); + this.contentDisposables.push(toDisposable(() => this._webview = null)); + }); + } + + get onMessage(): Event { + return this._onMessage.event; + } + + public render() { + super.render(); + this._register(attachModalDialogStyler(this, this._themeService)); + + this._okButton = this.addFooterButton(this._okLabel, () => this.ok()); + this._register(attachButtonStyler(this._okButton, this._themeService)); + } + + protected layout(height?: number): void { + // Nothing to re-layout + } + + private updateDialogBody(): void { + this._webview.contents = [this.html]; + } + + /* espace key */ + protected onClose() { + this.ok(); + } + + /* enter key */ + protected onAccept() { + this.ok(); + } + + public ok(): void { + this._onOk.fire(); + this.close(); + } + + public close() { + this.hide(); + this._onClosed.fire(); + } + + public sendMessage(message: any): void { + this._webview.sendMessage(message); + } + + public open() { + this.title = this.headerTitle; + this.updateDialogBody(); + this.show(); + this._okButton.focus(); + } + + public dispose(): void { + this.contentDisposables.forEach(element => { + element.dispose(); + }); + } +} \ No newline at end of file diff --git a/src/sql/common/telemetryKeys.ts b/src/sql/common/telemetryKeys.ts index 4590cd77b9..f51b350af5 100644 --- a/src/sql/common/telemetryKeys.ts +++ b/src/sql/common/telemetryKeys.ts @@ -29,6 +29,7 @@ export const FirewallRuleRequested = 'FirewallRuleCreated'; // Modal Dialogs: export const ErrorMessage = 'ErrorMessage'; +export const WebView = 'WebView'; export const ConnectionAdvancedProperties = 'ConnectionAdvancedProperties'; export const Connection = 'Connection'; export const Backup = 'Backup'; diff --git a/src/sql/data.d.ts b/src/sql/data.d.ts index 7763227a2f..b676b38738 100644 --- a/src/sql/data.d.ts +++ b/src/sql/data.d.ts @@ -1360,4 +1360,63 @@ declare module 'data' { result: boolean; ipAddress: string; } + + export interface ModalDialog { + /** + * Title of the webview. + */ + title: string; + + /** + * Contents of the dialog body. + */ + html: string; + + /** + * The caption of the OK button. + */ + okTitle: string; + + /** + * The caption of the Close button. + */ + closeTitle: string; + + /** + * Opens the dialog. + */ + open(): void; + + /** + * Closes the dialog. + */ + close(): void; + + /** + * Raised when the webview posts a message. + */ + readonly onMessage: vscode.Event; + + /** + * Raised when dialog closed. + */ + readonly onClosed: vscode.Event; + + /** + * Post a message to the dialog. + * + * @param message Body of the message. + */ + postMessage(message: any): Thenable; + } + + export namespace window { + /** + * creates a dialog + * @param title + */ + export function createDialog( + title: string + ): ModalDialog; + } } diff --git a/src/sql/workbench/api/electron-browser/mainThreadModalDialog.ts b/src/sql/workbench/api/electron-browser/mainThreadModalDialog.ts new file mode 100644 index 0000000000..6aa5a0a99e --- /dev/null +++ b/src/sql/workbench/api/electron-browser/mainThreadModalDialog.ts @@ -0,0 +1,71 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; +import { WebViewDialog } from 'sql/base/browser/ui/modal/webViewDialog'; + +import { MainThreadModalDialogShape, SqlMainContext, SqlExtHostContext, ExtHostModalDialogsShape } from 'sql/workbench/api/node/sqlextHost.protocol'; +import { IExtHostContext } from 'vs/workbench/api/node/extHost.protocol'; +import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers'; +import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import 'vs/css!sql/media/icons/common-icons'; + +@extHostNamedCustomer(SqlMainContext.MainThreadModalDialog) +export class MainThreadModalDialog implements MainThreadModalDialogShape { + private readonly _proxy: ExtHostModalDialogsShape; + private readonly _dialogs = new Map(); + + constructor( + context: IExtHostContext, + @IWorkbenchEditorService private readonly _editorService: IWorkbenchEditorService, + @IInstantiationService private readonly _instantiationService: IInstantiationService + ) { + this._proxy = context.get(SqlExtHostContext.ExtHostModalDialogs); + } + + dispose(): void { + throw new Error('Method not implemented.'); + } + + $createDialog(handle: number): void { + const dialog = this._instantiationService.createInstance(WebViewDialog); + + this._dialogs.set(handle, dialog); + dialog.onMessage(args => { + this._proxy.$onMessage(handle, args); + }); + + dialog.onClosed(args => { + this._proxy.$onClosed(handle); + }); + } + + $disposeDialog(handle: number): void { + const dialog = this._dialogs.get(handle); + dialog.close(); + } + + $setTitle(handle: number, value: string): void { + const dialog = this._dialogs.get(handle); + dialog.headerTitle = value; + } + + $setHtml(handle: number, value: string): void { + const dialog = this._dialogs.get(handle); + dialog.html = value; + } + + $show(handle: number): void { + const dialog = this._dialogs.get(handle); + dialog.render(); + dialog.open(); + } + + async $sendMessage(handle: number, message: any): Promise { + const dialog = this._dialogs.get(handle); + dialog.sendMessage(message); + return Promise.resolve(true); + } +} diff --git a/src/sql/workbench/api/electron-browser/sqlExtensionHost.contribution.ts b/src/sql/workbench/api/electron-browser/sqlExtensionHost.contribution.ts new file mode 100644 index 0000000000..0792645f41 --- /dev/null +++ b/src/sql/workbench/api/electron-browser/sqlExtensionHost.contribution.ts @@ -0,0 +1,7 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import 'sql/workbench/api/electron-browser/mainThreadModalDialog'; \ No newline at end of file diff --git a/src/sql/workbench/api/node/extHostModalDialog.ts b/src/sql/workbench/api/node/extHostModalDialog.ts new file mode 100644 index 0000000000..aac7c5f013 --- /dev/null +++ b/src/sql/workbench/api/node/extHostModalDialog.ts @@ -0,0 +1,122 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import { SqlMainContext, MainThreadModalDialogShape, ExtHostModalDialogsShape } from 'sql/workbench/api/node/sqlextHost.protocol'; +import { IMainContext } from 'vs/workbench/api/node/extHost.protocol'; +import * as vscode from 'vscode'; +import * as data from 'data'; +import { Emitter } from 'vs/base/common/event'; + +class ExtHostDialog implements data.ModalDialog { + private _title: string; + private _html: string; + private _okTitle: string; + private _closeTitle: string; + public onMessageEmitter = new Emitter(); + public onClosedEmitter = new Emitter(); + + constructor( + private readonly _proxy: MainThreadModalDialogShape, + private readonly _handle: number, + ) { } + + get title(): string { + return this._title; + } + + set title(value: string) { + if (this._title !== value) { + this._title = value; + this._proxy.$setTitle(this._handle, value); + } + } + + get html(): string { + return this._html; + } + + set html(value: string) { + if (this._html !== value) { + this._html = value; + this._proxy.$setHtml(this._handle, value); + } + } + + public set okTitle(value: string) { + this._okTitle = value; + } + + public get okTitle(): string { + return this._okTitle; + } + + public set closeTitle(value: string) { + this._closeTitle = value; + } + + public get closeTitle(): string { + return this._closeTitle; + } + + public open(): void { + this._proxy.$show(this._handle); + } + + public close(): void { + this._proxy.$disposeDialog(this._handle); + } + + public postMessage(message: any): Thenable { + return this._proxy.$sendMessage(this._handle, message); + } + + public get onMessage(): vscode.Event { + return this.onMessageEmitter.event; + } + + public get onClosed(): vscode.Event { + return this.onClosedEmitter.event; + } +} + +export class ExtHostModalDialogs implements ExtHostModalDialogsShape { + private static _handlePool = 0; + + private readonly _proxy: MainThreadModalDialogShape; + + private readonly _webviews = new Map(); + + constructor( + mainContext: IMainContext + ) { + this._proxy = mainContext.get(SqlMainContext.MainThreadModalDialog); + } + + createDialog( + title: string + ): data.ModalDialog { + console.log(title); + const handle = ExtHostModalDialogs._handlePool++; + this._proxy.$createDialog(handle); + + const webview = new ExtHostDialog(this._proxy, handle); + this._webviews.set(handle, webview); + webview.title = title; + //webview.options = options; + //this._proxy.$show(handle); + return webview; + } + + $onMessage(handle: number, message: any): void { + const webview = this._webviews.get(handle); + webview.onMessageEmitter.fire(message); + } + + $onClosed(handle: number): void { + const webview = this._webviews.get(handle); + webview.onClosedEmitter.fire(); + } +} \ No newline at end of file diff --git a/src/sql/workbench/api/node/sqlExtHost.api.impl.ts b/src/sql/workbench/api/node/sqlExtHost.api.impl.ts index 7aeac90dd0..aa5de092e6 100644 --- a/src/sql/workbench/api/node/sqlExtHost.api.impl.ts +++ b/src/sql/workbench/api/node/sqlExtHost.api.impl.ts @@ -25,6 +25,7 @@ import { ExtHostThreadService } from 'vs/workbench/services/thread/node/extHostT import * as sqlExtHostTypes from 'sql/workbench/api/common/sqlExtHostTypes'; import { ExtHostWorkspace } from 'vs/workbench/api/node/extHostWorkspace'; import { ExtHostConfiguration } from 'vs/workbench/api/node/extHostConfiguration'; +import { ExtHostModalDialogs } from 'sql/workbench/api/node/extHostModalDialog'; import { ILogService } from 'vs/platform/log/common/log'; import { IExtensionApiFactory } from 'vs/workbench/api/node/extHost.api.impl'; @@ -52,6 +53,7 @@ export function createApiFactory( const extHostDataProvider = threadService.set(SqlExtHostContext.ExtHostDataProtocol, new ExtHostDataProtocol(threadService)); const extHostSerializationProvider = threadService.set(SqlExtHostContext.ExtHostSerializationProvider, new ExtHostSerializationProvider(threadService)); const extHostResourceProvider = threadService.set(SqlExtHostContext.ExtHostResourceProvider, new ExtHostResourceProvider(threadService)); + const extHostModalDialogs = threadService.set(SqlExtHostContext.ExtHostModalDialogs, new ExtHostModalDialogs(threadService)); return { vsCodeFactory: vsCodeFactory, @@ -236,6 +238,12 @@ export function createApiFactory( } }; + const window = { + createDialog(name: string) { + return extHostModalDialogs.createDialog(name); + } + }; + return { accounts, credentials, @@ -248,7 +256,8 @@ export function createApiFactory( MetadataType: sqlExtHostTypes.MetadataType, TaskStatus: sqlExtHostTypes.TaskStatus, TaskExecutionMode: sqlExtHostTypes.TaskExecutionMode, - ScriptOperation: sqlExtHostTypes.ScriptOperation + ScriptOperation: sqlExtHostTypes.ScriptOperation, + window }; } }; diff --git a/src/sql/workbench/api/node/sqlExtHost.protocol.ts b/src/sql/workbench/api/node/sqlExtHost.protocol.ts index 02d711c2f8..a5025fb1ea 100644 --- a/src/sql/workbench/api/node/sqlExtHost.protocol.ts +++ b/src/sql/workbench/api/node/sqlExtHost.protocol.ts @@ -409,7 +409,8 @@ export const SqlMainContext = { MainThreadCredentialManagement: createMainId('MainThreadCredentialManagement'), MainThreadDataProtocol: createMainId('MainThreadDataProtocol'), MainThreadSerializationProvider: createMainId('MainThreadSerializationProvider'), - MainThreadResourceProvider: createMainId('MainThreadResourceProvider') + MainThreadResourceProvider: createMainId('MainThreadResourceProvider'), + MainThreadModalDialog: createMainId('MainThreadModalDialog'), }; export const SqlExtHostContext = { @@ -417,5 +418,19 @@ export const SqlExtHostContext = { ExtHostCredentialManagement: createExtId('ExtHostCredentialManagement'), ExtHostDataProtocol: createExtId('ExtHostDataProtocol'), ExtHostSerializationProvider: createExtId('ExtHostSerializationProvider'), - ExtHostResourceProvider: createExtId('ExtHostResourceProvider') + ExtHostResourceProvider: createExtId('ExtHostResourceProvider'), + ExtHostModalDialogs: createExtId('ExtHostModalDialogs') }; + +export interface MainThreadModalDialogShape extends IDisposable { + $createDialog(handle: number): void; + $disposeDialog(handle: number): void; + $show(handle: number): void; + $setTitle(handle: number, value: string): void; + $setHtml(handle: number, value: string): void; + $sendMessage(handle: number, value: any): Thenable; +} +export interface ExtHostModalDialogsShape { + $onMessage(handle: number, message: any): void; + $onClosed(handle: number): void; +} \ No newline at end of file diff --git a/src/vs/workbench/parts/html/browser/webview-pre.js b/src/vs/workbench/parts/html/browser/webview-pre.js index e497800b13..e73c007641 100644 --- a/src/vs/workbench/parts/html/browser/webview-pre.js +++ b/src/vs/workbench/parts/html/browser/webview-pre.js @@ -12,6 +12,8 @@ var firstLoad = true; var loadTimeout; var pendingMessages = []; + // {{SQL CARBON EDIT}} + var enableWrappedPostMessage = false; const initData = { initialScrollProgress: undefined @@ -124,6 +126,8 @@ // update iframe-contents ipcRenderer.on('content', function (_event, data) { const options = data.options; + // {{SQL CARBON EDIT}} + enableWrappedPostMessage = options && options.enableWrappedPostMessage; const text = data.contents.join('\n'); const newDocument = new DOMParser().parseFromString(text, 'text/html'); @@ -301,8 +305,16 @@ }); // forward messages from the embedded iframe + window.onmessage = function (message) { - ipcRenderer.sendToHost(message.data.command, message.data.data); + // {{SQL CARBON EDIT}} + if (enableWrappedPostMessage) { + // Modern webview. Forward wrapped message + ipcRenderer.sendToHost('onmessage', message.data); + } else { + // Old school webview. Forward exact message + ipcRenderer.sendToHost(message.data.command, message.data.data); + } }; // signal ready diff --git a/src/vs/workbench/parts/html/browser/webview.ts b/src/vs/workbench/parts/html/browser/webview.ts index 798326e6ad..689991871c 100644 --- a/src/vs/workbench/parts/html/browser/webview.ts +++ b/src/vs/workbench/parts/html/browser/webview.ts @@ -37,6 +37,9 @@ export interface WebviewOptions { allowScripts?: boolean; allowSvgs?: boolean; svgWhiteList?: string[]; + // {{SQL CARBON EDIT}} + enableWrappedPostMessage?: boolean; + hideFind?: boolean; } export default class Webview { @@ -49,6 +52,8 @@ export default class Webview { private _onDidScroll = new Emitter<{ scrollYPercentage: number }>(); private _onFoundInPageResults = new Emitter(); + // {{SQL CARBON EDIT}} + private _onMessage = new Emitter(); private _webviewFindWidget: WebviewFindWidget; private _findStarted: boolean = false; @@ -141,6 +146,13 @@ export default class Webview { console.error('embedded page crashed'); }), addDisposableListener(this._webview, 'ipc-message', (event) => { + // {{SQL CARBON EDIT}} + if (event.channel === 'onmessage') { + if (this._options.enableWrappedPostMessage && event.args && event.args.length) { + this._onMessage.fire(event.args[0]); + } + return; + } if (event.channel === 'did-click-link') { let [uri] = event.args; this._onDidClickLink.fire(URI.parse(uri)); @@ -181,7 +193,10 @@ export default class Webview { this._disposables.push(this._webviewFindWidget); if (parent) { + // {{SQL CARBON EDIT}} + if (!this._options.hideFind) { parent.appendChild(this._webviewFindWidget.getDomNode()); + } parent.appendChild(this._webview); } } @@ -217,6 +232,11 @@ export default class Webview { return this._onFoundInPageResults.event; } + // {{SQL CARBON EDIT}} + get onMessage(): Event { + return this._onMessage.event; + } + private _send(channel: string, ...args: any[]): void { this._ready .then(() => this._webview.send(channel, ...args)) diff --git a/src/vs/workbench/workbench.main.ts b/src/vs/workbench/workbench.main.ts index 473f59b586..7b2a39e0a0 100644 --- a/src/vs/workbench/workbench.main.ts +++ b/src/vs/workbench/workbench.main.ts @@ -160,3 +160,5 @@ import 'sql/parts/dashboard/widgets/tasks/tasksWidget.contribution'; import 'sql/parts/dashboard/dashboardConfig.contribution'; /* Tasks */ import 'sql/workbench/common/actions.contribution'; +/* Extension Host */ +import 'sql/workbench/api/electron-browser/sqlExtensionHost.contribution';