diff --git a/src/vs/workbench/contrib/userDataProfile/browser/media/userDataProfilesEditor.css b/src/vs/workbench/contrib/userDataProfile/browser/media/userDataProfilesEditor.css index 5ed42a2fdca29..c0674e54314b1 100644 --- a/src/vs/workbench/contrib/userDataProfile/browser/media/userDataProfilesEditor.css +++ b/src/vs/workbench/contrib/userDataProfile/browser/media/userDataProfilesEditor.css @@ -78,14 +78,17 @@ height: 28px; } -.profiles-editor .contents-container .profile-header .profile-actions-container .profile-button-container { - margin-left: 5px; - min-width: 120px; +.profiles-editor .contents-container .profile-header .profile-actions-container .actions-container { + gap: 4px; +} + +.profiles-editor .contents-container .profile-header .profile-actions-container .actions-container .codicon { + font-size: 18px; } -.profiles-editor .contents-container .profile-header .profile-actions-container .profile-button-container .monaco-button.error, -.profiles-editor .contents-container .profile-header .profile-actions-container .profile-button-container.error { - border: 1px solid var(--vscode-inputValidation-errorBorder, transparent); +.profiles-editor .contents-container .profile-header .profile-actions-container .profile-button-container { + margin-right: 5px; + min-width: 120px; } .profiles-editor .contents-container .profile-header .profile-actions-container .profile-button-container .monaco-button { diff --git a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditor.ts b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditor.ts index c401df78f1726..e14f9af5973f1 100644 --- a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditor.ts +++ b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditor.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/userDataProfilesEditor'; -import { $, addDisposableListener, append, Dimension, EventHelper, EventType, IDomPosition } from 'vs/base/browser/dom'; +import { $, addDisposableListener, append, Dimension, EventHelper, EventType, IDomPosition, trackFocus } from 'vs/base/browser/dom'; import { Action, IAction, Separator, SubmenuAction } from 'vs/base/common/actions'; import { Event } from 'vs/base/common/event'; import { ThemeIcon } from 'vs/base/common/themables'; @@ -32,7 +32,7 @@ import { IAsyncDataSource, IObjectTreeElement, ITreeNode, ITreeRenderer, ObjectT import { CancellationToken } from 'vs/base/common/cancellation'; import { IEditorOptions } from 'vs/platform/editor/common/editor'; import { Disposable, DisposableStore, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; -import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; +import { InputBox, MessageType } from 'vs/base/browser/ui/inputbox/inputBox'; import { Checkbox } from 'vs/base/browser/ui/toggle/toggle'; import { DEFAULT_ICON, ICONS } from 'vs/workbench/services/userDataProfile/common/userDataProfileIcons'; import { WorkbenchIconSelectBox } from 'vs/workbench/services/userDataProfile/browser/iconSelectBox'; @@ -433,11 +433,9 @@ class ProfileWidget extends Disposable { append(title, $('span', undefined, localize('profile', "Profile: "))); this.profileTitle = append(title, $('span')); const actionsContainer = append(header, $('.profile-actions-container')); - this.actionbar = new ActionBar(actionsContainer, { - focusOnlyEnabledItems: true - }); - this.actionbar.setFocusable(false); this.buttonContainer = append(actionsContainer, $('.profile-button-container')); + this.actionbar = new ActionBar(actionsContainer); + this.actionbar.setFocusable(true); const body = append(parent, $('.profile-body')); @@ -452,13 +450,37 @@ class ProfileWidget extends Disposable { inputBoxStyles: defaultInputBoxStyles, ariaLabel: localize('profileName', "Profile Name"), placeholder: localize('profileName', "Profile Name"), + validationOptions: { + validation: (value) => { + if (!value) { + return { + content: localize('name required', "Profile name is required and must be a non-empty value."), + type: MessageType.ERROR + }; + } + const initialName = this._profileElement.value?.element instanceof UserDataProfileElement ? this._profileElement.value.element.profile.name : undefined; + if (initialName !== value && this.userDataProfilesService.profiles.some(p => p.name === value)) { + return { + content: localize('profileExists', "Profile with name {0} already exists.", value), + type: MessageType.ERROR + }; + } + return null; + } + } } )); this.nameInput.onDidChange(value => { - if (this._profileElement.value) { + if (this._profileElement.value && value) { this._profileElement.value.element.name = value; } }); + const focusTracker = this._register(trackFocus(this.nameInput.inputElement)); + this._register(focusTracker.onDidBlur(() => { + if (this._profileElement.value && !this.nameInput.value) { + this.nameInput.value = this._profileElement.value.element.name; + } + })); this.copyFromContainer = append(body, $('.profile-copy-from-container')); append(this.copyFromContainer, $('.profile-copy-from-label', undefined, localize('create from', "Copy from:"))); @@ -639,24 +661,29 @@ class ProfileWidget extends Disposable { } })); - const button = disposables.add(new Button(this.buttonContainer, { - supportIcons: true, - ...defaultButtonStyles - })); - button.label = profileElement.primaryAction.label; - button.enabled = profileElement.primaryAction.enabled; - disposables.add(button.onDidClick(() => this.editorProgressService.showWhile(profileElement.primaryAction.run()))); - disposables.add(profileElement.primaryAction.onDidChange((e) => { - if (!isUndefined(e.enabled)) { - button.enabled = profileElement.primaryAction.enabled; - } - })); - disposables.add(profileElement.onDidChange(e => { - if (e.message) { - button.setTitle(profileElement.message ?? profileElement.primaryAction.label); - button.element.classList.toggle('error', !!profileElement.message); - } - })); + if (profileElement.primaryAction) { + this.buttonContainer.classList.remove('hide'); + const button = disposables.add(new Button(this.buttonContainer, { + supportIcons: true, + ...defaultButtonStyles + })); + button.label = profileElement.primaryAction.label; + button.enabled = profileElement.primaryAction.enabled; + disposables.add(button.onDidClick(() => this.editorProgressService.showWhile(profileElement.primaryAction!.run()))); + disposables.add(profileElement.primaryAction.onDidChange((e) => { + if (!isUndefined(e.enabled)) { + button.enabled = profileElement.primaryAction!.enabled; + } + })); + disposables.add(profileElement.onDidChange(e => { + if (e.message) { + button.setTitle(profileElement.message ?? profileElement.primaryAction!.label); + button.element.classList.toggle('error', !!profileElement.message); + } + })); + } else { + this.buttonContainer.classList.add('hide'); + } this.actionbar.clear(); if (profileElement.secondaryActions.length > 0) { @@ -987,12 +1014,21 @@ export class UserDataProfilesEditorInput extends EditorInput { private readonly model: UserDataProfilesEditorModel; + private _dirty: boolean = false; + get dirty(): boolean { return this._dirty; } + set dirty(dirty: boolean) { + if (this._dirty !== dirty) { + this._dirty = dirty; + this._onDidChangeDirty.fire(); + } + } + constructor( @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(); this.model = UserDataProfilesEditorModel.getInstance(this.instantiationService); - this._register(this.model.onDidChangeDirty(e => this._onDidChangeDirty.fire())); + this._register(this.model.onDidChange(e => this.dirty = this.model.profiles.some(profile => profile instanceof NewProfileElement))); } override get typeId(): string { return UserDataProfilesEditorInput.ID; } @@ -1004,10 +1040,11 @@ export class UserDataProfilesEditorInput extends EditorInput { } override isDirty(): boolean { - return this.model.isDirty(); + return this.dirty; } override async save(): Promise { + await this.model.saveNewProfile(); return this; } diff --git a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditorModel.ts b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditorModel.ts index 0650951cf6481..63872690376da 100644 --- a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditorModel.ts +++ b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditorModel.ts @@ -25,13 +25,13 @@ import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; import { IFileService } from 'vs/platform/files/common/files'; import { generateUuid } from 'vs/base/common/uuid'; +import { RunOnceScheduler } from 'vs/base/common/async'; export type ChangeEvent = { readonly name?: boolean; readonly icon?: boolean; readonly flags?: boolean; readonly active?: boolean; - readonly dirty?: boolean; readonly message?: boolean; readonly copyFrom?: boolean; readonly copyFlags?: boolean; @@ -43,7 +43,6 @@ export interface IProfileElement { readonly icon?: string; readonly flags?: UseDefaultProfileFlags; readonly active?: boolean; - readonly dirty?: boolean; readonly message?: string; } @@ -57,7 +56,6 @@ export abstract class AbstractUserDataProfileElement extends Disposable { icon: string | undefined, flags: UseDefaultProfileFlags | undefined, isActive: boolean, - isDirty: boolean, @IUserDataProfilesService protected readonly userDataProfilesService: IUserDataProfilesService, @IInstantiationService protected readonly instantiationService: IInstantiationService, ) { @@ -66,15 +64,13 @@ export abstract class AbstractUserDataProfileElement extends Disposable { this._icon = icon; this._flags = flags; this._active = isActive; - this._dirty = isDirty; this._register(this.onDidChange(e => { - if (!e.dirty) { - this.dirty = this.hasUnsavedChanges(); - } if (!e.message) { this.validate(); } - this.primaryAction.enabled = !this.message && this.dirty; + if (this.primaryAction) { + this.primaryAction.enabled = !this.message; + } })); } @@ -114,15 +110,6 @@ export abstract class AbstractUserDataProfileElement extends Disposable { } } - private _dirty: boolean = false; - get dirty(): boolean { return this._dirty; } - set dirty(isDirty: boolean) { - if (this._dirty !== isDirty) { - this._dirty = isDirty; - this._onDidChange.fire({ dirty: true }); - } - } - private _message: string | undefined; get message(): string | undefined { return this._message; } set message(message: string | undefined) { @@ -147,10 +134,6 @@ export abstract class AbstractUserDataProfileElement extends Disposable { } validate(): void { - if (!this.dirty) { - this.message = undefined; - return; - } if (!this.name) { this.message = localize('profileNameRequired', "Profile name is required."); return; @@ -172,8 +155,6 @@ export abstract class AbstractUserDataProfileElement extends Disposable { return []; } - reset(): void { } - protected async getChildrenFromProfile(profile: IUserDataProfile, resourceType: ProfileResourceType): Promise { profile = this.getFlag(resourceType) ? this.userDataProfilesService.defaultProfile : profile; switch (resourceType) { @@ -195,16 +176,17 @@ export abstract class AbstractUserDataProfileElement extends Disposable { return ''; } - abstract readonly primaryAction: Action; + abstract readonly primaryAction?: Action; abstract readonly secondaryActions: IAction[]; - protected abstract hasUnsavedChanges(): boolean; } export class UserDataProfileElement extends AbstractUserDataProfileElement implements IProfileElement { get profile(): IUserDataProfile { return this._profile; } - readonly primaryAction = new Action('userDataProfile.save', localize('save', "Save"), undefined, this.dirty, () => this.save()); + readonly primaryAction = undefined; + + private readonly saveScheduler = this._register(new RunOnceScheduler(() => this.doSave(), 500)); constructor( private _profile: IUserDataProfile, @@ -219,7 +201,6 @@ export class UserDataProfileElement extends AbstractUserDataProfileElement imple _profile.icon, _profile.useDefaultFlags, userDataProfileService.currentProfile.id === _profile.id, - false, userDataProfilesService, instantiationService, ); @@ -233,9 +214,12 @@ export class UserDataProfileElement extends AbstractUserDataProfileElement imple this.flags = profile.useDefaultFlags; } })); + this._register(this.onDidChange(e => { + this.save(); + })); } - protected hasUnsavedChanges(): boolean { + private hasUnsavedChanges(): boolean { if (this.name !== this.profile.name) { return true; } @@ -248,8 +232,12 @@ export class UserDataProfileElement extends AbstractUserDataProfileElement imple return false; } - private async save(): Promise { - if (!this.dirty) { + save(): void { + this.saveScheduler.schedule(); + } + + private async doSave(): Promise { + if (!this.hasUnsavedChanges()) { return; } this.validate(); @@ -265,20 +253,12 @@ export class UserDataProfileElement extends AbstractUserDataProfileElement imple icon: this.icon, useDefaultFlags: this.profile.useDefaultFlags && !useDefaultFlags ? {} : useDefaultFlags }); - - this.dirty = false; } override async getChildren(resourceType: ProfileResourceType): Promise { return this.getChildrenFromProfile(this.profile, resourceType); } - override reset(): void { - this.name = this.profile.name; - this.icon = this.profile.icon; - this.flags = this.profile.useDefaultFlags; - } - protected override getInitialName(): string { return this.profile.name; } @@ -304,7 +284,6 @@ export class NewProfileElement extends AbstractUserDataProfileElement implements undefined, undefined, false, - true, userDataProfilesService, instantiationService, ); @@ -353,10 +332,6 @@ export class NewProfileElement extends AbstractUserDataProfileElement implements this.copyFlags = flags; } - protected hasUnsavedChanges(): boolean { - return true; - } - override async getChildren(resourceType: ProfileResourceType): Promise { if (!this.getCopyFlag(resourceType)) { return []; @@ -445,9 +420,6 @@ export class UserDataProfilesEditorModel extends EditorModel { private _onDidChange = this._register(new Emitter()); readonly onDidChange = this._onDidChange.event; - private _onDidChangeDirty = this._register(new Emitter()); - readonly onDidChangeDirty = this._onDidChangeDirty.event; - constructor( @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, @IUserDataProfileManagementService private readonly userDataProfileManagementService: IUserDataProfileManagementService, @@ -492,11 +464,6 @@ export class UserDataProfilesEditorModel extends EditorModel { profile, actions )); - disposables.add(profileElement.onDidChange(e => { - if (e.dirty) { - this._onDidChangeDirty.fire(this.isDirty()); - } - })); return [profileElement, disposables]; } @@ -508,7 +475,7 @@ export class UserDataProfilesEditorModel extends EditorModel { copyFrom, new Action('userDataProfile.create', localize('create', "Create & Apply"), undefined, true, () => this.saveNewProfile()), [ - new Action('userDataProfile.discard', localize('discard', "Discard"), ThemeIcon.asClassName(Codicon.trash), true, () => { + new Action('userDataProfile.discard', localize('discard', "Discard"), ThemeIcon.asClassName(Codicon.close), true, () => { this.removeNewProfile(); this._onDidChange.fire(undefined); }) @@ -520,16 +487,8 @@ export class UserDataProfilesEditorModel extends EditorModel { return this.newProfileElement; } - isDirty(): boolean { - return this._profiles.some(([p]) => p.dirty); - } - revert(): void { this.removeNewProfile(); - for (const [profile] of this._profiles) { - profile.reset(); - } - this._onDidChangeDirty.fire(false); this._onDidChange.fire(undefined); } @@ -543,7 +502,7 @@ export class UserDataProfilesEditorModel extends EditorModel { } } - private async saveNewProfile(): Promise { + async saveNewProfile(): Promise { if (!this.newProfileElement) { return; }