mirror of
https://github.com/microsoft/vscode.git
synced 2025-12-25 04:36:23 +00:00
Initial welcome widget commit (#183446)
* Initial welcome widget commit * Update i18n.resource.json for welcomeDialog * Clean up code
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
177
src/vs/workbench/contrib/welcomeDialog/browser/welcomeWidget.ts
Normal file
177
src/vs/workbench/contrib/welcomeDialog/browser/welcomeWidget.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user