diff --git a/build/lib/i18n.resources.json b/build/lib/i18n.resources.json index 5a1e2dc4d7ac1f..46234248d28fba 100644 --- a/build/lib/i18n.resources.json +++ b/build/lib/i18n.resources.json @@ -290,6 +290,10 @@ "name": "vs/workbench/contrib/welcomeWalkthrough", "project": "vscode-workbench" }, + { + "name": "vs/workbench/contrib/welcomeDialog", + "project": "vscode-workbench" + }, { "name": "vs/workbench/contrib/outline", "project": "vscode-workbench" diff --git a/src/vs/workbench/browser/web.api.ts b/src/vs/workbench/browser/web.api.ts index fe3397e05ecfc7..eb4f2497a91ddd 100644 --- a/src/vs/workbench/browser/web.api.ts +++ b/src/vs/workbench/browser/web.api.ts @@ -343,7 +343,7 @@ export interface IWorkbenchConstructionOptions { readonly initialColorTheme?: IInitialColorTheme; /** - * Welcome view dialog on first launch. Can be dismissed by the user. + * Welcome dialog. Can be dismissed by the user. */ readonly welcomeDialog?: IWelcomeDialog; @@ -639,14 +639,24 @@ export interface IWelcomeDialog { buttonText: string; /** - * Message text and icon for the welcome dialog. + * Button command to execute from the welcome dialog. */ - messages: { message: string; icon: string }[]; + buttonCommand: string; /** - * Optional action to appear as links at the bottom of the welcome dialog. + * Message text for the welcome dialog. */ - action?: IWelcomeLinkAction; + message: string; + + /** + * Context key expression to control the visibility of the welcome dialog. + */ + when: string; + + /** + * Media to include in the welcome dialog. + */ + media: { altText: string; path: string }; } export interface IDefaultView { diff --git a/src/vs/workbench/contrib/welcomeDialog/browser/media/welcomeDialog.css b/src/vs/workbench/contrib/welcomeDialog/browser/media/welcomeDialog.css deleted file mode 100644 index 63fb861a55916a..00000000000000 --- a/src/vs/workbench/contrib/welcomeDialog/browser/media/welcomeDialog.css +++ /dev/null @@ -1,38 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -.monaco-dialog-box { - border-radius: 6px; -} - -.monaco-dialog-box .dialog-message-row .dialog-message-container .dialog-message-text { - font-size: 25px; - min-width: max-content; -} - -#monaco-dialog-message-body > div > p > .codicon[class*='codicon-']::before{ - padding-right: 8px; - max-width: 30px; - max-height: 30px; - position: relative; - top: auto; - color: var(--vscode-textLink-foreground); - padding-right: 20px; - font-size: 25px; -} - -#monaco-dialog-message-body > .message-body > p { - display: flex; - font-size: 16px; - background: var(--vscode-welcomePage-tileHoverBackground); - border-radius: 6px; - padding: 20px; - min-height: auto; - word-wrap: break-word; - overflow-wrap:break-word; -} - -#monaco-dialog-message-body > .link > p { - font-size: 16px; -} diff --git a/src/vs/workbench/contrib/welcomeDialog/browser/media/welcomeWidget.css b/src/vs/workbench/contrib/welcomeDialog/browser/media/welcomeWidget.css new file mode 100644 index 00000000000000..d8faced6f19f6f --- /dev/null +++ b/src/vs/workbench/contrib/welcomeDialog/browser/media/welcomeWidget.css @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +.monaco-dialog-box { + border-radius: 6px; +} + +.welcome-widget { + height: min-content; + border-radius: 6px; +} + +.dialog-message-detail-title{ + height: 22px; + padding-bottom: 4px; + font-size: large; +} + +.monaco-dialog-box .monaco-action-bar .actions-container { + justify-content: flex-end; +} diff --git a/src/vs/workbench/contrib/welcomeDialog/browser/welcomeDialog.contribution.ts b/src/vs/workbench/contrib/welcomeDialog/browser/welcomeDialog.contribution.ts index 5a9d7d1eb0918b..0d8bf80483bdda 100644 --- a/src/vs/workbench/contrib/welcomeDialog/browser/welcomeDialog.contribution.ts +++ b/src/vs/workbench/contrib/welcomeDialog/browser/welcomeDialog.contribution.ts @@ -5,24 +5,40 @@ import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { Registry } from 'vs/platform/registry/common/platform'; -import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; -import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry, IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { IBrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; -import { IWelcomeDialogService as IWelcomeDialogService } from 'vs/workbench/contrib/welcomeDialog/browser/welcomeDialogService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { WelcomeWidget } from 'vs/workbench/contrib/welcomeDialog/browser/welcomeWidget'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; const configurationKey = 'welcome.experimental.dialog'; -class WelcomeDialogContribution { +class WelcomeDialogContribution extends Disposable implements IWorkbenchContribution { - private static readonly WELCOME_DIALOG_DISMISSED_KEY = 'workbench.dialog.welcome.dismissed'; + private contextKeysToWatch = new Set(); constructor( - @IWelcomeDialogService welcomeDialogService: IWelcomeDialogService, @IStorageService storageService: IStorageService, @IBrowserWorkbenchEnvironmentService environmentService: IBrowserWorkbenchEnvironmentService, - @IConfigurationService configurationService: IConfigurationService + @IConfigurationService configurationService: IConfigurationService, + @IContextKeyService readonly contextService: IContextKeyService, + @ICodeEditorService readonly codeEditorService: ICodeEditorService, + @IInstantiationService readonly instantiationService: IInstantiationService, + @ICommandService readonly commandService: ICommandService, + @ITelemetryService readonly telemetryService: ITelemetryService ) { + super(); + + if (!storageService.isNew(StorageScope.PROFILE)) { + return; // do not show if this is not the first session + } + const setting = configurationService.inspect(configurationKey); if (!setting.value) { return; @@ -33,19 +49,23 @@ class WelcomeDialogContribution { return; } - if (storageService.getBoolean(WelcomeDialogContribution.WELCOME_DIALOG_DISMISSED_KEY + '#' + welcomeDialog.id, StorageScope.PROFILE, false)) { - return; - } + this.contextKeysToWatch.add(welcomeDialog.when); - welcomeDialogService.show({ - title: welcomeDialog.title, - buttonText: welcomeDialog.buttonText, - messages: welcomeDialog.messages, - action: welcomeDialog.action, - onClose: () => { - storageService.store(WelcomeDialogContribution.WELCOME_DIALOG_DISMISSED_KEY + '#' + welcomeDialog.id, true, StorageScope.PROFILE, StorageTarget.USER); + this._register(this.contextService.onDidChangeContext(e => { + if (e.affectsSome(this.contextKeysToWatch) && + Array.from(this.contextKeysToWatch).every(value => this.contextService.contextMatchesRules(ContextKeyExpr.deserialize(value)))) { + const codeEditor = this.codeEditorService.getActiveCodeEditor(); + if (codeEditor?.hasModel()) { + const welcomeWidget = new WelcomeWidget(codeEditor, instantiationService, commandService, telemetryService); + welcomeWidget.render(welcomeDialog.title, + welcomeDialog.message, + welcomeDialog.buttonText, + welcomeDialog.buttonCommand, + welcomeDialog.media); + this.contextKeysToWatch.delete(welcomeDialog.when); + } } - }); + })); } } diff --git a/src/vs/workbench/contrib/welcomeDialog/browser/welcomeDialogService.ts b/src/vs/workbench/contrib/welcomeDialog/browser/welcomeDialogService.ts deleted file mode 100644 index fadc29bb782c7c..00000000000000 --- a/src/vs/workbench/contrib/welcomeDialog/browser/welcomeDialogService.ts +++ /dev/null @@ -1,77 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import 'vs/css!./media/welcomeDialog'; -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { ILinkDescriptor } from 'vs/platform/opener/browser/link'; -import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { MarkdownString } from 'vs/base/common/htmlContent'; -import { openLinkFromMarkdown } from 'vs/editor/contrib/markdownRenderer/browser/markdownRenderer'; -import { IOpenerService } from 'vs/platform/opener/common/opener'; - -interface IWelcomeDialogItem { - readonly title: string; - readonly messages: { message: string; icon: string }[]; - readonly buttonText: string; - readonly action?: ILinkDescriptor; - readonly onClose?: () => void; -} - -export const IWelcomeDialogService = createDecorator('welcomeDialogService'); - -export interface IWelcomeDialogService { - readonly _serviceBrand: undefined; - - show(item: IWelcomeDialogItem): void; -} - -export class WelcomeDialogService implements IWelcomeDialogService { - declare readonly _serviceBrand: undefined; - - constructor( - @IDialogService private readonly dialogService: IDialogService, - @IOpenerService private readonly openerService: IOpenerService) { - } - - async show(welcomeDialogItem: IWelcomeDialogItem): Promise { - - const renderBody = (icon: string, message: string): MarkdownString => { - const mds = new MarkdownString(undefined, { supportThemeIcons: true, supportHtml: true }); - mds.appendMarkdown(`$(${icon})`); - mds.appendMarkdown(message); - return mds; - }; - - const hr = new MarkdownString(undefined, { supportThemeIcons: true, supportHtml: true }); - hr.appendMarkdown('
'); - - await this.dialogService.prompt({ - type: 'none', - message: welcomeDialogItem.title, - cancelButton: welcomeDialogItem.buttonText, - buttons: welcomeDialogItem.action ? [{ - label: welcomeDialogItem.action.label as string, - run: () => { - openLinkFromMarkdown(this.openerService, welcomeDialogItem.action?.href!, true); - welcomeDialogItem.onClose?.(); - } - - }] : undefined, - custom: { - disableCloseAction: true, - markdownDetails: [ - { markdown: hr, classes: ['hr'] }, - ...welcomeDialogItem.messages.map(value => { return { markdown: renderBody(value.icon, value.message), classes: ['message-body'] }; }) - ] - } - }); - - welcomeDialogItem.onClose?.(); - } -} - -registerSingleton(IWelcomeDialogService, WelcomeDialogService, InstantiationType.Eager); - diff --git a/src/vs/workbench/contrib/welcomeDialog/browser/welcomeWidget.ts b/src/vs/workbench/contrib/welcomeDialog/browser/welcomeWidget.ts new file mode 100644 index 00000000000000..c5dc2eac289eb5 --- /dev/null +++ b/src/vs/workbench/contrib/welcomeDialog/browser/welcomeWidget.ts @@ -0,0 +1,177 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'vs/css!./media/welcomeWidget'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, OverlayWidgetPositionPreference } from 'vs/editor/browser/editorBrowser'; +import { $, hide } from 'vs/base/browser/dom'; import { RunOnceScheduler } from 'vs/base/common/async'; +import { MarkdownString } from 'vs/base/common/htmlContent'; +import { MarkdownRenderer } from 'vs/editor/contrib/markdownRenderer/browser/markdownRenderer'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ButtonBar } from 'vs/base/browser/ui/button/button'; +import { mnemonicButtonLabel } from 'vs/base/common/labels'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { defaultButtonStyles, defaultDialogStyles } from 'vs/platform/theme/browser/defaultStyles'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { Action, WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from 'vs/base/common/actions'; +import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; +import { localize } from 'vs/nls'; +import { ThemeIcon } from 'vs/base/common/themables'; +import { Codicon } from 'vs/base/common/codicons'; + +export class WelcomeWidget extends Disposable implements IOverlayWidget { + + private readonly _rootDomNode: HTMLElement; + private readonly element: HTMLElement; + private readonly messageContainer: HTMLElement; + private readonly markdownRenderer = this.instantiationService.createInstance(MarkdownRenderer, {}); + + constructor( + private readonly _editor: ICodeEditor, + private readonly instantiationService: IInstantiationService, + private readonly commandService: ICommandService, + private readonly telemetryService: ITelemetryService, + ) { + super(); + this._rootDomNode = document.createElement('div'); + this._rootDomNode.className = 'welcome-widget'; + + this.element = this._rootDomNode.appendChild($('.monaco-dialog-box')); + this.element.setAttribute('role', 'dialog'); + + hide(this._rootDomNode); + + this.messageContainer = this.element.appendChild($('.dialog-message-container')); + } + + async executeCommand(commandId: string, ...args: string[]) { + try { + await this.commandService.executeCommand(commandId, ...args); + this._hide(false); + this.telemetryService.publicLog2('workbenchActionExecuted', { + id: commandId, + from: 'welcomeWidget' + }); + } + catch (ex) { + } + } + + render(title: string, message: string, buttonText: string, buttonAction: string, media: { altText: string; path: string }): void { + if (!this._editor._getViewModel()) { + return; + } + + this.buildWidgetContent(title, message, buttonText, buttonAction, media); + this._editor.addOverlayWidget(this); + this._revealTemporarily(); + this.telemetryService.publicLog2('workbenchActionExecuted', { + id: 'welcomeWidgetRendered', + from: 'welcomeWidget' + }); + } + + buildWidgetContent(title: string, message: string, buttonText: string, buttonAction: string, media: { altText: string; path: string }): void { + + const actionBar = this._register(new ActionBar(this.element, {})); + + const action = this._register(new Action('dialog.close', localize('dialogClose', "Close Dialog"), ThemeIcon.asClassName(Codicon.dialogClose), true, async () => { + this._hide(true); + })); + actionBar.push(action, { icon: true, label: false }); + + + const messageTitleElement = this.messageContainer.appendChild($('.dialog-message-title')); + messageTitleElement.style.display = 'contents'; + messageTitleElement.style.alignContent = 'start'; + + const renderBody = (message: string): MarkdownString => { + const mds = new MarkdownString(undefined, { supportHtml: true }); + mds.appendMarkdown(message); + return mds; + }; + + const titleElement = this.messageContainer.appendChild($('#monaco-dialog-message-detail.dialog-message-detail-title')); + const titleElementMdt = this.markdownRenderer.render(renderBody(title)); + titleElement.appendChild(titleElementMdt.element); + + const messageElement = this.messageContainer.appendChild($('#monaco-dialog-message-detail.dialog-message-detail-message')); + const messageElementMd = this.markdownRenderer.render(renderBody(message)); + messageElement.appendChild(messageElementMd.element); + + const buttonsRowElement = this.messageContainer.appendChild($('.dialog-buttons-row')); + const buttonContainer = buttonsRowElement.appendChild($('.dialog-buttons')); + + const buttonBar = this._register(new ButtonBar(buttonContainer)); + const primaryButton = this._register(buttonBar.addButtonWithDescription({ title: true, secondary: false, ...defaultButtonStyles })); + primaryButton.label = mnemonicButtonLabel(buttonText, true); + + this._register(primaryButton.onDidClick(async () => { + await this.executeCommand(buttonAction); + })); + + buttonBar.buttons[0].focus(); + this.applyStyles(); + } + + getId(): string { + return 'editor.contrib.welcomeWidget'; + } + + getDomNode(): HTMLElement { + return this._rootDomNode; + } + + getPosition(): IOverlayWidgetPosition | null { + return { + preference: OverlayWidgetPositionPreference.TOP_RIGHT_CORNER + }; + } + + private _hideSoon = this._register(new RunOnceScheduler(() => this._hide(false), 30000)); + private _isVisible: boolean = false; + + private _revealTemporarily(): void { + this._show(); + this._hideSoon.schedule(); + } + + private _show(): void { + if (this._isVisible) { + return; + } + this._isVisible = true; + this._rootDomNode.style.display = 'block'; + } + + private _hide(isUserDismissed: boolean): void { + if (!this._isVisible) { + return; + } + + this._isVisible = false; + this._rootDomNode.style.display = 'none'; + this._editor.removeOverlayWidget(this); + this.telemetryService.publicLog2('workbenchActionExecuted', { + id: isUserDismissed ? 'welcomeWidgetDismissed' : 'welcomeWidgetHidden', + from: 'welcomeWidget' + }); + } + + private applyStyles(): void { + const style = defaultDialogStyles; + + const fgColor = style.dialogForeground; + const bgColor = style.dialogBackground; + const shadowColor = style.dialogShadow ? `0 0px 8px ${style.dialogShadow}` : ''; + const border = style.dialogBorder ? `1px solid ${style.dialogBorder}` : ''; + + this._rootDomNode.style.boxShadow = shadowColor; + + this._rootDomNode.style.color = fgColor ?? ''; + this._rootDomNode.style.backgroundColor = bgColor ?? ''; + this._rootDomNode.style.border = border; + } +}