diff --git a/src/vs/base/browser/ui/dialog/dialog.ts b/src/vs/base/browser/ui/dialog/dialog.ts index 6392ec535e788..d7fc96598f137 100644 --- a/src/vs/base/browser/ui/dialog/dialog.ts +++ b/src/vs/base/browser/ui/dialog/dialog.ts @@ -375,6 +375,8 @@ export class Dialog extends Disposable { this.iconElement.classList.add(...ThemeIcon.asClassNameArray(Codicon.loading), spinModifierClassName); break; case 'none': + this.iconElement.classList.add('no-codicon'); + break; case 'info': case 'question': default: diff --git a/src/vs/workbench/browser/web.api.ts b/src/vs/workbench/browser/web.api.ts index dae4352cc13a4..21415c69b9533 100644 --- a/src/vs/workbench/browser/web.api.ts +++ b/src/vs/workbench/browser/web.api.ts @@ -314,6 +314,11 @@ export interface IWorkbenchConstructionOptions { */ readonly initialColorTheme?: IInitialColorTheme; + /** + * Welcome view dialog on first launch. Can be dismissed by the user. + */ + readonly welcomeDialog?: IWelcomeDialog; + //#endregion @@ -510,10 +515,10 @@ export interface IWelcomeBanner { /** * Optional actions to appear as links after the welcome banner message. */ - actions?: IWelcomeBannerAction[]; + actions?: IWelcomeLinkAction[]; } -export interface IWelcomeBannerAction { +export interface IWelcomeLinkAction { /** * The link to open when clicking. Supports command invocation when @@ -578,6 +583,35 @@ export interface IInitialColorTheme { readonly colors?: { [colorId: string]: string }; } +export interface IWelcomeDialog { + + /** + * Unique identifier of the welcome dialog. The identifier will be used to determine + * if the dialog has been previously displayed. + */ + id: string; + + /** + * Title of the welcome dialog. + */ + title: string; + + /** + * Button text of the welcome dialog. + */ + buttonText: string; + + /** + * Message text and icon for the welcome dialog. + */ + messages: { message: string; icon: string }[]; + + /** + * Optional action to appear as links at the bottom of the welcome dialog. + */ + action?: IWelcomeLinkAction; +} + 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 new file mode 100644 index 0000000000000..1e076325ce15e --- /dev/null +++ b/src/vs/workbench/contrib/welcomeDialog/browser/media/welcomeDialog.css @@ -0,0 +1,54 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { + padding-left: 0px; +} + +.monaco-dialog-box .dialog-message-row .dialog-message-container .dialog-message-text { + font-size: 25px; + width: max-content; +} + +.monaco-dialog-box .dialog-message-row .dialog-message-container .dialog-message-body .dialog-message { + display: flex; + padding: 20px; + min-height: auto; + color: var(--vscode-descriptionForeground); + border-radius: 6px; + overflow: hidden; + margin-bottom: 12px; + background: var(--vscode-welcomePage-tileHoverBackground); + align-items: center; +} + +.monaco-dialog-box .dialog-message-row .dialog-message-container .dialog-message-body>.dialog-message { + grid-area: title; + align-self: flex-end; +} + +.monaco-dialog-box .dialog-message-row .dialog-message-container .dialog-message-body .dialog-message hr { + border-color: var(--vscode-descriptionForeground); + margin-bottom: 12px; + border-width: thin; +} + +.monaco-dialog-box .dialog-message-row .dialog-message-container .dialog-message-body .dialog-message .icon-widget { + 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-box .dialog-message-row .dialog-message-container .dialog-message-body .dialog-message .description-container { + font-size: 16px; +} diff --git a/src/vs/workbench/contrib/welcomeDialog/browser/welcomeDialog.contribution.ts b/src/vs/workbench/contrib/welcomeDialog/browser/welcomeDialog.contribution.ts new file mode 100644 index 0000000000000..15db69ea55119 --- /dev/null +++ b/src/vs/workbench/contrib/welcomeDialog/browser/welcomeDialog.contribution.ts @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +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 { IBrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; +import { IWelcomeDialogService as IWelcomeDialogService } from 'vs/workbench/contrib/welcomeDialog/browser/welcomeDialogService'; + +class WelcomeDialogContribution { + + private static readonly WELCOME_DIALOG_DISMISSED_KEY = 'workbench.dialog.welcome.dismissed'; + + constructor( + @IWelcomeDialogService welcomeDialogService: IWelcomeDialogService, + @IStorageService storageService: IStorageService, + @IBrowserWorkbenchEnvironmentService environmentService: IBrowserWorkbenchEnvironmentService + ) { + const welcomeDialog = environmentService.options?.welcomeDialog; + if (!welcomeDialog) { + return; + } + + if (storageService.getBoolean(WelcomeDialogContribution.WELCOME_DIALOG_DISMISSED_KEY + '#' + welcomeDialog.id, StorageScope.PROFILE, false)) { + return; + } + + 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); + } + }); + } +} + +Registry.as(WorkbenchExtensions.Workbench) + .registerWorkbenchContribution(WelcomeDialogContribution, LifecyclePhase.Restored); diff --git a/src/vs/workbench/contrib/welcomeDialog/browser/welcomeDialogService.ts b/src/vs/workbench/contrib/welcomeDialog/browser/welcomeDialogService.ts new file mode 100644 index 0000000000000..0d1a1ff00f4a8 --- /dev/null +++ b/src/vs/workbench/contrib/welcomeDialog/browser/welcomeDialogService.ts @@ -0,0 +1,103 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { IInstantiationService, createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { ThemeIcon } from 'vs/base/common/themables'; +import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { Dialog } from 'vs/base/browser/ui/dialog/dialog'; +import { defaultButtonStyles, defaultCheckboxStyles, defaultDialogStyles, defaultInputBoxStyles } from 'vs/platform/theme/browser/defaultStyles'; +import { $ } from 'vs/base/browser/dom'; +import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; +import { ILinkDescriptor, Link } from 'vs/platform/opener/browser/link'; + +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; + + private dialog: Dialog | undefined; + private disposableStore: DisposableStore = new DisposableStore(); + + constructor( + @ILayoutService private readonly layoutService: ILayoutService, + @IInstantiationService private readonly instantiationService: IInstantiationService,) { + } + + private static iconWidgetFor(icon: string) { + const themeIcon = ThemeIcon.fromId(icon); + if (themeIcon) { + const widget = $(ThemeIcon.asCSSSelector(themeIcon)); + widget.classList.add('icon-widget'); + return widget; + } + return ''; + } + + async show(welcomeDialogItem: IWelcomeDialogItem): Promise { + + this.disposableStore.clear(); + + const renderBody = (parent: HTMLElement) => { + + parent.classList.add(...('dialog-items')); + parent.appendChild(document.createElement('hr')); + + for (const message of welcomeDialogItem.messages) { + const descriptorComponent = + $('.dialog-message', + {}, + WelcomeDialogService.iconWidgetFor(message.icon), + $('.description-container', {}, + $('.description.description.max-lines-3', { 'x-description-for': 'description' }, ...renderLabelWithIcons(message.message)))); + parent.appendChild(descriptorComponent); + } + + const actionsContainer = $('div.dialog-action-container'); + parent.appendChild(actionsContainer); + if (welcomeDialogItem.action) { + this.disposableStore.add(this.instantiationService.createInstance(Link, actionsContainer, welcomeDialogItem.action, {})); + } + }; + + this.dialog = new Dialog( + this.layoutService.container, + welcomeDialogItem.title, + [welcomeDialogItem.buttonText], + { + detail: '', + type: 'none', + renderBody: renderBody, + disableCloseAction: true, + buttonStyles: defaultButtonStyles, + checkboxStyles: defaultCheckboxStyles, + inputBoxStyles: defaultInputBoxStyles, + dialogStyles: defaultDialogStyles + }); + + this.disposableStore.add(this.dialog); + await this.dialog.show(); + this.disposableStore.dispose(); + } +} + +registerSingleton(IWelcomeDialogService, WelcomeDialogService, InstantiationType.Eager); + diff --git a/src/vs/workbench/workbench.web.main.ts b/src/vs/workbench/workbench.web.main.ts index b96b78442c86f..caaabcf3778f6 100644 --- a/src/vs/workbench/workbench.web.main.ts +++ b/src/vs/workbench/workbench.web.main.ts @@ -134,6 +134,9 @@ import 'vs/workbench/contrib/debug/browser/extensionHostDebugService'; // Welcome Banner import 'vs/workbench/contrib/welcomeBanner/browser/welcomeBanner.contribution'; +// Welcome Dialog +import 'vs/workbench/contrib/welcomeDialog/browser/welcomeDialog.contribution'; + // Webview import 'vs/workbench/contrib/webview/browser/webview.web.contribution';