Initial welcome widget commit (#183446)

* Initial welcome widget commit

* Update i18n.resource.json for welcomeDialog

* Clean up code
This commit is contained in:
Bhavya U 2023-05-25 15:03:23 -07:00 committed by GitHub
parent 731d08f2bb
commit 049ee36265
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 256 additions and 138 deletions

View file

@ -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"

View file

@ -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 {

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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<string>();
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<boolean>(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);
}
}
});
}));
}
}

View file

@ -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<IWelcomeDialogService>('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<void> {
const renderBody = (icon: string, message: string): MarkdownString => {
const mds = new MarkdownString(undefined, { supportThemeIcons: true, supportHtml: true });
mds.appendMarkdown(`<a>$(${icon})</a>`);
mds.appendMarkdown(message);
return mds;
};
const hr = new MarkdownString(undefined, { supportThemeIcons: true, supportHtml: true });
hr.appendMarkdown('<hr>');
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);

View file

@ -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<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('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<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('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<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('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;
}
}