diff --git a/src/vs/base/browser/ui/icons/iconSelectBox.css b/src/vs/base/browser/ui/icons/iconSelectBox.css index 4e2278d04bc84..399d577942395 100644 --- a/src/vs/base/browser/ui/icons/iconSelectBox.css +++ b/src/vs/base/browser/ui/icons/iconSelectBox.css @@ -35,4 +35,5 @@ .icon-select-box .icon-select-id-container .icon-select-id-label .highlight { color: var(--vscode-list-highlightForeground); + font-weight: bold; } diff --git a/src/vs/base/browser/ui/icons/iconSelectBox.ts b/src/vs/base/browser/ui/icons/iconSelectBox.ts index c592c37d3f7ab..c9ec383cb94cc 100644 --- a/src/vs/base/browser/ui/icons/iconSelectBox.ts +++ b/src/vs/base/browser/ui/icons/iconSelectBox.ts @@ -46,7 +46,7 @@ export class IconSelectBox extends Disposable { private scrollableElement: DomScrollableElement | undefined; private iconIdElement: HighlightedLabel | undefined; private readonly iconContainerWidth = 36; - private readonly iconContainerHeight = 32; + private readonly iconContainerHeight = 36; constructor( private readonly options: IIconSelectBoxOptions, diff --git a/src/vs/platform/userDataProfile/common/userDataProfile.ts b/src/vs/platform/userDataProfile/common/userDataProfile.ts index e42d012c995a8..f18de2499cd39 100644 --- a/src/vs/platform/userDataProfile/common/userDataProfile.ts +++ b/src/vs/platform/userDataProfile/common/userDataProfile.ts @@ -41,6 +41,7 @@ export interface IUserDataProfile { readonly isDefault: boolean; readonly name: string; readonly shortName?: string; + readonly icon?: string; readonly location: URI; readonly globalStorageHome: URI; readonly settingsResource: URI; @@ -84,12 +85,14 @@ export type WillRemoveProfileEvent = { export interface IUserDataProfileOptions { readonly shortName?: string; + readonly icon?: string; readonly useDefaultFlags?: UseDefaultProfileFlags; readonly transient?: boolean; } -export interface IUserDataProfileUpdateOptions extends IUserDataProfileOptions { +export interface IUserDataProfileUpdateOptions extends Omit { readonly name?: string; + readonly icon?: string | null; } export const IUserDataProfilesService = createDecorator('IUserDataProfilesService'); @@ -124,6 +127,7 @@ export function reviveProfile(profile: UriDto, scheme: string) isDefault: profile.isDefault, name: profile.name, shortName: profile.shortName, + icon: profile.icon, location: URI.revive(profile.location).with({ scheme }), globalStorageHome: URI.revive(profile.globalStorageHome).with({ scheme }), settingsResource: URI.revive(profile.settingsResource).with({ scheme }), @@ -144,6 +148,7 @@ export function toUserDataProfile(id: string, name: string, location: URI, profi location, isDefault: false, shortName: options?.shortName, + icon: options?.icon, globalStorageHome: defaultProfile && options?.useDefaultFlags?.globalState ? defaultProfile.globalStorageHome : joinPath(location, 'globalStorage'), settingsResource: defaultProfile && options?.useDefaultFlags?.settings ? defaultProfile.settingsResource : joinPath(location, 'settings.json'), keybindingsResource: defaultProfile && options?.useDefaultFlags?.keybindings ? defaultProfile.keybindingsResource : joinPath(location, 'keybindings.json'), @@ -166,6 +171,7 @@ export type StoredUserDataProfile = { name: string; location: URI; shortName?: string; + icon?: string; useDefaultFlags?: UseDefaultProfileFlags; }; @@ -246,7 +252,7 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf this.logService.warn('Skipping the invalid stored profile', storedProfile.location || storedProfile.name); continue; } - profiles.push(toUserDataProfile(basename(storedProfile.location), storedProfile.name, storedProfile.location, this.profilesCacheHome, { shortName: storedProfile.shortName, useDefaultFlags: storedProfile.useDefaultFlags }, defaultProfile)); + profiles.push(toUserDataProfile(basename(storedProfile.location), storedProfile.name, storedProfile.location, this.profilesCacheHome, { shortName: storedProfile.shortName, icon: storedProfile.icon, useDefaultFlags: storedProfile.useDefaultFlags }, defaultProfile)); } } catch (error) { this.logService.error(error); @@ -365,7 +371,12 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf throw new Error(`Profile '${profileToUpdate.name}' does not exist`); } - profile = toUserDataProfile(profile.id, options.name ?? profile.name, profile.location, this.profilesCacheHome, { shortName: options.shortName ?? profile.shortName, transient: options.transient ?? profile.isTransient, useDefaultFlags: options.useDefaultFlags ?? profile.useDefaultFlags }, this.defaultProfile); + profile = toUserDataProfile(profile.id, options.name ?? profile.name, profile.location, this.profilesCacheHome, { + shortName: options.shortName ?? profile.shortName, + icon: options.icon === null ? undefined : options.icon ?? profile.icon, + transient: options.transient ?? profile.isTransient, + useDefaultFlags: options.useDefaultFlags ?? profile.useDefaultFlags + }, this.defaultProfile); this.updateProfiles([], [], [profile]); return profile; @@ -516,7 +527,7 @@ export class UserDataProfilesService extends Disposable implements IUserDataProf if (profile.isTransient) { this.transientProfilesObject.profiles.push(profile); } else { - storedProfiles.push({ location: profile.location, name: profile.name, shortName: profile.shortName, useDefaultFlags: profile.useDefaultFlags }); + storedProfiles.push({ location: profile.location, name: profile.name, shortName: profile.shortName, icon: profile.icon, useDefaultFlags: profile.useDefaultFlags }); } } this.saveStoredProfiles(storedProfiles); diff --git a/src/vs/platform/userDataSync/common/userDataProfilesManifestMerge.ts b/src/vs/platform/userDataSync/common/userDataProfilesManifestMerge.ts index 072554b04f8a7..a6957351fc5c2 100644 --- a/src/vs/platform/userDataSync/common/userDataProfilesManifestMerge.ts +++ b/src/vs/platform/userDataSync/common/userDataProfilesManifestMerge.ts @@ -18,6 +18,7 @@ interface IUserDataProfileInfo { readonly id: string; readonly name: string; readonly shortName?: string; + readonly icon?: string; readonly useDefaultFlags?: UseDefaultProfileFlags; } @@ -119,7 +120,7 @@ function compare(from: IUserDataProfileInfo[] | null, to: IUserDataProfileInfo[] const removed = fromKeys.filter(key => !toKeys.includes(key)); const updated: string[] = []; - for (const { id, name, shortName, useDefaultFlags } of from) { + for (const { id, name, shortName, icon, useDefaultFlags } of from) { if (removed.includes(id)) { continue; } @@ -127,6 +128,7 @@ function compare(from: IUserDataProfileInfo[] | null, to: IUserDataProfileInfo[] if (!toProfile || toProfile.name !== name || toProfile.shortName !== shortName + || toProfile.icon !== icon || !equals(toProfile.useDefaultFlags, useDefaultFlags) ) { updated.push(id); diff --git a/src/vs/platform/userDataSync/common/userDataProfilesManifestSync.ts b/src/vs/platform/userDataSync/common/userDataProfilesManifestSync.ts index eee01fe850c05..5a532c8bae2f6 100644 --- a/src/vs/platform/userDataSync/common/userDataProfilesManifestSync.ts +++ b/src/vs/platform/userDataSync/common/userDataProfilesManifestSync.ts @@ -191,7 +191,7 @@ export class UserDataProfilesManifestSynchroniser extends AbstractSynchroniser i for (const profile of local.added) { promises.push((async () => { this.logService.trace(`${this.syncResourceLogLabel}: Creating '${profile.name}' profile...`); - await this.userDataProfilesService.createProfile(profile.id, profile.name, { shortName: profile.shortName, useDefaultFlags: profile.useDefaultFlags }); + await this.userDataProfilesService.createProfile(profile.id, profile.name, { shortName: profile.shortName, icon: profile.icon, useDefaultFlags: profile.useDefaultFlags }); this.logService.info(`${this.syncResourceLogLabel}: Created profile '${profile.name}'.`); })()); } @@ -207,7 +207,7 @@ export class UserDataProfilesManifestSynchroniser extends AbstractSynchroniser i if (localProfile) { promises.push((async () => { this.logService.trace(`${this.syncResourceLogLabel}: Updating '${profile.name}' profile...`); - await this.userDataProfilesService.updateProfile(localProfile, { name: profile.name, shortName: profile.shortName, useDefaultFlags: profile.useDefaultFlags }); + await this.userDataProfilesService.updateProfile(localProfile, { name: profile.name, shortName: profile.shortName, icon: profile.icon, useDefaultFlags: profile.useDefaultFlags }); this.logService.info(`${this.syncResourceLogLabel}: Updated profile '${profile.name}'.`); })()); } else { @@ -225,7 +225,7 @@ export class UserDataProfilesManifestSynchroniser extends AbstractSynchroniser i for (const profile of remote?.added || []) { const collection = await this.userDataSyncStoreService.createCollection(this.syncHeaders); addedCollections.push(collection); - remoteProfiles.push({ id: profile.id, name: profile.name, collection, shortName: profile.shortName, useDefaultFlags: profile.useDefaultFlags }); + remoteProfiles.push({ id: profile.id, name: profile.name, collection, shortName: profile.shortName, icon: profile.icon, useDefaultFlags: profile.useDefaultFlags }); } } else { this.logService.info(`${this.syncResourceLogLabel}: Could not create remote profiles as there are too many profiles.`); @@ -236,7 +236,7 @@ export class UserDataProfilesManifestSynchroniser extends AbstractSynchroniser i for (const profile of remote?.updated || []) { const profileToBeUpdated = remoteProfiles.find(({ id }) => profile.id === id); if (profileToBeUpdated) { - remoteProfiles.splice(remoteProfiles.indexOf(profileToBeUpdated), 1, { ...profileToBeUpdated, id: profile.id, name: profile.name, shortName: profile.shortName, useDefaultFlags: profile.useDefaultFlags }); + remoteProfiles.splice(remoteProfiles.indexOf(profileToBeUpdated), 1, { ...profileToBeUpdated, id: profile.id, name: profile.name, shortName: profile.shortName, icon: profile.icon, useDefaultFlags: profile.useDefaultFlags }); } } diff --git a/src/vs/platform/userDataSync/common/userDataSync.ts b/src/vs/platform/userDataSync/common/userDataSync.ts index 13f99fc4aa5ec..084755420cb32 100644 --- a/src/vs/platform/userDataSync/common/userDataSync.ts +++ b/src/vs/platform/userDataSync/common/userDataSync.ts @@ -342,6 +342,7 @@ export interface ISyncUserDataProfile { readonly collection: string; readonly name: string; readonly shortName?: string; + readonly icon?: string; readonly useDefaultFlags?: UseDefaultProfileFlags; } diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts index 6e8afba3bf558..3fc1bdc4e82bc 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts @@ -492,7 +492,6 @@ export class GlobalActivityActionViewItem extends MenuActivityActionViewItem { @IKeybindingService keybindingService: IKeybindingService, ) { super(MenuId.GlobalActivity, action, contextMenuActionsProvider, true, colors, activityHoverOptions, themeService, hoverService, menuService, contextMenuService, contextKeyService, configurationService, environmentService, keybindingService); - this._register(this.userDataProfileService.onDidChangeCurrentProfile(() => this.updateProfileBadge())); } override render(container: HTMLElement): void { diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts index e9aca0b760186..007ef5fc79036 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts @@ -43,6 +43,8 @@ import { StringSHA1 } from 'vs/base/common/hash'; import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; import { GestureEvent } from 'vs/base/browser/touch'; import { IPaneCompositePart, IPaneCompositeSelectorPart } from 'vs/workbench/browser/parts/paneCompositePart'; +import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; +import { IUserDataProfile } from 'vs/platform/userDataProfile/common/userDataProfile'; interface IPlaceholderViewContainer { readonly id: string; @@ -130,6 +132,7 @@ export class ActivitybarPart extends Part implements IPaneCompositeSelectorPart @IContextKeyService private readonly contextKeyService: IContextKeyService, @IConfigurationService private readonly configurationService: IConfigurationService, @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, ) { super(Parts.ACTIVITYBAR_PART, { hasTitle: false }, themeService, storageService, layoutService); @@ -523,10 +526,11 @@ export class ActivitybarPart extends Part implements IPaneCompositeSelectorPart preventLoopNavigation: true })); - this.globalActivityAction = this._register(new ActivityAction({ - id: 'workbench.actions.manage', - name: localize('manage', "Manage"), - classNames: ThemeIcon.asClassNameArray(ActivitybarPart.GEAR_ICON), + this.globalActivityAction = this._register(new ActivityAction(this.createGlobalActivity(this.userDataProfileService.currentProfile))); + this._register(this.userDataProfileService.onDidChangeCurrentProfile(e => { + if (this.globalActivityAction) { + this.globalActivityAction.activity = this.createGlobalActivity(e.profile); + } })); if (this.accountsVisibilityPreference) { @@ -542,6 +546,14 @@ export class ActivitybarPart extends Part implements IPaneCompositeSelectorPart this.globalActivityActionBar.push(this.globalActivityAction); } + private createGlobalActivity(profile: IUserDataProfile): IActivity { + return { + id: 'workbench.actions.manage', + name: localize('manage', "Manage"), + classNames: ThemeIcon.asClassNameArray(profile.icon ? ThemeIcon.fromId(profile.icon) : ActivitybarPart.GEAR_ICON), + }; + } + private toggleAccountsActivity() { if (!!this.accountsActivityAction === this.accountsVisibilityPreference) { return; diff --git a/src/vs/workbench/services/userDataProfile/browser/media/userDataProfileView.css b/src/vs/workbench/services/userDataProfile/browser/media/userDataProfileView.css index aa12a2746204c..b17aa5fb4d7df 100644 --- a/src/vs/workbench/services/userDataProfile/browser/media/userDataProfileView.css +++ b/src/vs/workbench/services/userDataProfile/browser/media/userDataProfileView.css @@ -62,24 +62,63 @@ padding: 0 4px; } -.profile-type-widget { +.profile-edit-widget { + padding: 4px 6px 0px 11px; +} + +.profile-edit-widget > .profile-icon-container { + display: flex; + margin-bottom: 8px; +} + +.profile-edit-widget > .profile-icon-container > .profile-icon { + cursor: pointer; + padding: 3px; + border-radius: 5px; +} + +.profile-edit-widget > .profile-icon-container > .profile-icon.codicon{ + font-size: 18px; +} + +.profile-edit-widget > .profile-icon-container > .profile-icon:hover { + outline: 1px dashed var(--vscode-toolbar-hoverOutline); + outline-offset: -1px; + background-color: var(--vscode-toolbar-hoverBackground); +} + +.profile-edit-widget > .profile-type-container { display: flex; - margin: 0px 6px 8px 11px; align-items: center; justify-content: space-between; font-size: 12px; + margin-bottom: 8px; +} + +.profile-edit-widget > .profile-icon-container > .profile-icon-label, +.profile-edit-widget > .profile-type-container > .profile-type-create-label { + width: 90px; + display: inline-flex; + align-items: center; +} + +.profile-edit-widget > .profile-icon-container > .profile-icon-id { + display: inline-flex; + align-items: center; + margin-left: 5px; + opacity: .8; + font-size: 0.9em; } -.profile-type-widget>.profile-type-select-container { +.profile-edit-widget > .profile-type-container > .profile-type-select-container { overflow: hidden; - padding-left: 10px; flex: 1; display: flex; align-items: center; justify-content: center; } -.profile-type-widget>.profile-type-select-container>.monaco-select-box { +.profile-edit-widget > .profile-type-container > .profile-type-select-container > .monaco-select-box { cursor: pointer; line-height: 17px; padding: 2px 23px 2px 8px; diff --git a/src/vs/workbench/services/userDataProfile/browser/userDataProfileImportExportService.ts b/src/vs/workbench/services/userDataProfile/browser/userDataProfileImportExportService.ts index 9dc107dd7fbfd..2823141ff4c68 100644 --- a/src/vs/workbench/services/userDataProfile/browser/userDataProfileImportExportService.ts +++ b/src/vs/workbench/services/userDataProfile/browser/userDataProfileImportExportService.ts @@ -40,7 +40,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { defaultButtonStyles, defaultSelectBoxStyles } from 'vs/platform/theme/browser/defaultStyles'; +import { defaultButtonStyles, defaultInputBoxStyles, defaultSelectBoxStyles } from 'vs/platform/theme/browser/defaultStyles'; import { generateUuid } from 'vs/base/common/uuid'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { EditorsOrder } from 'vs/workbench/common/editor'; @@ -71,10 +71,19 @@ import { MarkdownString } from 'vs/base/common/htmlContent'; import { renderMarkdown } from 'vs/base/browser/markdownRenderer'; import { showWindowLogActionId } from 'vs/workbench/services/log/common/logConstants'; import { ISelectOptionItem, SelectBox } from 'vs/base/browser/ui/selectBox/selectBox'; +import { ThemeIcon } from 'vs/base/common/themables'; +import { IHoverService } from 'vs/workbench/services/hover/browser/hover'; +import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; +import { DEFAULT_ICON, ICONS } from 'vs/workbench/services/userDataProfile/common/userDataProfileIcons'; +import { WorkbenchIconSelectBox } from 'vs/workbench/browser/iconSelectBox'; +import { IHoverWidget } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { KeyCode } from 'vs/base/common/keyCodes'; interface IUserDataProfileTemplate { readonly name: string; readonly shortName?: string; + readonly icon?: string; readonly settings?: string; readonly keybindings?: string; readonly tasks?: string; @@ -89,6 +98,7 @@ function isUserDataProfileTemplate(thing: unknown): thing is IUserDataProfileTem return !!(candidate && typeof candidate === 'object' && (candidate.name && typeof candidate.name === 'string') && (isUndefined(candidate.shortName) || typeof candidate.shortName === 'string') + && (isUndefined(candidate.icon) || typeof candidate.icon === 'string') && (isUndefined(candidate.settings) || typeof candidate.settings === 'string') && (isUndefined(candidate.globalState) || typeof candidate.globalState === 'string') && (isUndefined(candidate.extensions) || typeof candidate.extensions === 'string')); @@ -132,6 +142,7 @@ export class UserDataProfileImportExportService extends Disposable implements IU @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IContextViewService private readonly contextViewService: IContextViewService, + @IHoverService private readonly hoverService: IHoverService, @ILogService private readonly logService: ILogService, ) { super(); @@ -322,7 +333,14 @@ export class UserDataProfileImportExportService extends Disposable implements IU disposables.add(quickPick.onDidChangeValue(validate)); - let result: { name: string; items: ReadonlyArray } | undefined; + let icon = DEFAULT_ICON; + if (profile?.icon) { + icon = ThemeIcon.fromId(profile.icon); + } + if (isUserDataProfileTemplate(source) && source.icon) { + icon = ThemeIcon.fromId(source.icon); + } + let result: { name: string; items: ReadonlyArray; icon?: string | null } | undefined; disposables.add(Event.any(quickPick.onDidCustom, quickPick.onDidAccept)(() => { const name = quickPick.value.trim(); if (!name) { @@ -332,15 +350,73 @@ export class UserDataProfileImportExportService extends Disposable implements IU if (quickPick.validationMessage) { return; } - result = { name, items: quickPick.selectedItems }; + result = { name, items: quickPick.selectedItems, icon: icon.id === DEFAULT_ICON.id ? null : icon.id }; quickPick.hide(); quickPick.severity = Severity.Ignore; quickPick.validationMessage = undefined; })); + const domNode = DOM.$('.profile-edit-widget'); + + const profileIconContainer = DOM.$('.profile-icon-container'); + DOM.append(profileIconContainer, DOM.$('.profile-icon-label', undefined, localize('icon', "Icon:"))); + const profileIconElement = DOM.append(profileIconContainer, DOM.$(`.profile-icon${ThemeIcon.asCSSSelector(icon)}`)); + profileIconElement.tabIndex = 0; + profileIconElement.role = 'button'; + profileIconElement.ariaLabel = localize('select icon', "Icon: {0}", icon.id); + const iconSelectBox = disposables.add(this.instantiationService.createInstance(WorkbenchIconSelectBox, { icons: ICONS, inputBoxStyles: defaultInputBoxStyles })); + const dimension = new DOM.Dimension(496, 300); + iconSelectBox.layout(dimension); + let hoverWidget: IHoverWidget | undefined; + + const updateIcon = (updated: ThemeIcon | undefined) => { + icon = updated ?? DEFAULT_ICON; + profileIconElement.className = `profile-icon ${ThemeIcon.asClassName(icon)}`; + }; + disposables.add(iconSelectBox.onDidSelect(selectedIcon => { + if (icon.id !== selectedIcon.id) { + updateIcon(selectedIcon); + } + hoverWidget?.dispose(); + profileIconElement.focus(); + })); + const showIconSelectBox = () => { + hoverWidget = this.hoverService.showHover({ + content: iconSelectBox.domNode, + target: profileIconElement, + hoverPosition: HoverPosition.BELOW, + showPointer: true, + hideOnHover: false, + }, true); + if (hoverWidget) { + iconSelectBox.layout(dimension); + disposables.add(hoverWidget); + } + iconSelectBox.focus(); + }; + disposables.add(DOM.addDisposableListener(profileIconElement, DOM.EventType.CLICK, (e: MouseEvent) => { + DOM.EventHelper.stop(e, true); + showIconSelectBox(); + })); + disposables.add(DOM.addDisposableListener(profileIconElement, DOM.EventType.KEY_DOWN, e => { + const event = new StandardKeyboardEvent(e); + if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) { + DOM.EventHelper.stop(event, true); + showIconSelectBox(); + } + })); + disposables.add(DOM.addDisposableListener(iconSelectBox.domNode, DOM.EventType.KEY_DOWN, e => { + const event = new StandardKeyboardEvent(e); + if (event.equals(KeyCode.Escape)) { + DOM.EventHelper.stop(event, true); + hoverWidget?.dispose(); + profileIconElement.focus(); + } + })); + if (!profile && !isUserDataProfileTemplate(source)) { - const domNode = DOM.$('.profile-type-widget'); - DOM.append(domNode, DOM.$('.profile-type-create-label', undefined, localize('create from', "Copy from:"))); + const profileTypeContainer = DOM.append(domNode, DOM.$('.profile-type-container')); + DOM.append(profileTypeContainer, DOM.$('.profile-type-create-label', undefined, localize('create from', "Copy from:"))); const separator = { text: '\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500', isDisabled: true }; const profileOptions: (ISelectOptionItem & { id?: string; source?: IUserDataProfile | URI })[] = []; profileOptions.push({ text: localize('empty profile', "None") }); @@ -369,9 +445,17 @@ export class UserDataProfileImportExportService extends Disposable implements IU }; const initialIndex = findOptionIndex(); - const selectBox = disposables.add(this.instantiationService.createInstance(SelectBox, profileOptions, initialIndex, this.contextViewService, defaultSelectBoxStyles, { useCustomDrawn: true })); - selectBox.render(DOM.append(domNode, DOM.$('.profile-type-select-container'))); - quickPick.widget = domNode; + const selectBox = disposables.add(this.instantiationService.createInstance(SelectBox, + profileOptions, + initialIndex, + this.contextViewService, + defaultSelectBoxStyles, + { + useCustomDrawn: true, + ariaLabel: localize('copy profile from', "Copy profile from"), + } + )); + selectBox.render(DOM.append(profileTypeContainer, DOM.$('.profile-type-select-container'))); if (profileOptions[initialIndex].source) { quickPick.value = this.generateProfileName(profileOptions[initialIndex].text); @@ -382,6 +466,7 @@ export class UserDataProfileImportExportService extends Disposable implements IU for (const resource of resources) { resource.picked = option.source && !(option.source instanceof URI) ? !option.source?.useDefaultFlags?.[resource.id] : true; } + updateIcon(!(option.source instanceof URI) && option.source?.icon ? ThemeIcon.fromId(option.source.icon) : undefined); update(); }; @@ -392,6 +477,9 @@ export class UserDataProfileImportExportService extends Disposable implements IU })); } + DOM.append(domNode, profileIconContainer); + + quickPick.widget = domNode; quickPick.show(); await new Promise((c, e) => { @@ -421,21 +509,21 @@ export class UserDataProfileImportExportService extends Disposable implements IU extensions: !result.items.includes(extensions) }; if (profile) { - await this.userDataProfileManagementService.updateProfile(profile, { name: result.name, useDefaultFlags: profile.useDefaultFlags && !useDefaultFlags ? {} : useDefaultFlags }); + await this.userDataProfileManagementService.updateProfile(profile, { name: result.name, icon: result.icon, useDefaultFlags: profile.useDefaultFlags && !useDefaultFlags ? {} : useDefaultFlags }); } else { if (source instanceof URI) { this.telemetryService.publicLog2('userDataProfile.createFromTemplate', createProfileTelemetryData); - await this.importProfile(source, { mode: 'apply', name: result.name, useDefaultFlags }); + await this.importProfile(source, { mode: 'apply', name: result.name, useDefaultFlags, icon: result.icon ? result.icon : undefined }); } else if (isUserDataProfile(source)) { this.telemetryService.publicLog2('userDataProfile.createFromProfile', createProfileTelemetryData); - await this.createFromProfile(source, result.name, { useDefaultFlags }); + await this.createFromProfile(source, result.name, { useDefaultFlags, icon: result.icon ? result.icon : undefined }); } else if (isUserDataProfileTemplate(source)) { source.name = result.name; this.telemetryService.publicLog2('userDataProfile.createFromExternalTemplate', createProfileTelemetryData); - await this.createAndSwitch(source, false, true, { useDefaultFlags }, localize('create profile', "Create Profile")); + await this.createAndSwitch(source, false, true, { useDefaultFlags, icon: result.icon ? result.icon : undefined }, localize('create profile', "Create Profile")); } else { this.telemetryService.publicLog2('userDataProfile.createEmptyProfile', createProfileTelemetryData); - await this.userDataProfileManagementService.createAndEnterProfile(result.name, { useDefaultFlags }); + await this.userDataProfileManagementService.createAndEnterProfile(result.name, { useDefaultFlags, icon: result.icon ? result.icon : undefined }); } } } catch (error) { @@ -600,6 +688,10 @@ export class UserDataProfileImportExportService extends Disposable implements IU profileTemplate.name = options.name; } + if (options?.icon) { + profileTemplate.icon = options.icon; + } + return profileTemplate; } @@ -1315,6 +1407,7 @@ class UserDataProfileExportState extends UserDataProfileImportExportState { location: profile.location, isDefault: profile.isDefault, shortName: profile.shortName, + icon: profile.icon, globalStorageHome: profile.globalStorageHome, settingsResource: profile.settingsResource.with({ scheme: USER_DATA_PROFILE_EXPORT_SCHEME }), keybindingsResource: profile.keybindingsResource.with({ scheme: USER_DATA_PROFILE_EXPORT_SCHEME }), diff --git a/src/vs/workbench/services/userDataProfile/common/userDataProfile.ts b/src/vs/workbench/services/userDataProfile/common/userDataProfile.ts index 25347a80b98ed..95b1916713768 100644 --- a/src/vs/workbench/services/userDataProfile/common/userDataProfile.ts +++ b/src/vs/workbench/services/userDataProfile/common/userDataProfile.ts @@ -80,6 +80,7 @@ export function toUserDataProfileUri(path: string, productService: IProductServi export interface IProfileImportOptions extends IUserDataProfileOptions { readonly name?: string; + readonly icon?: string; readonly mode?: 'preview' | 'apply' | 'both'; } diff --git a/src/vs/workbench/services/userDataProfile/common/userDataProfileIcons.ts b/src/vs/workbench/services/userDataProfile/common/userDataProfileIcons.ts new file mode 100644 index 0000000000000..9b35a9221f092 --- /dev/null +++ b/src/vs/workbench/services/userDataProfile/common/userDataProfileIcons.ts @@ -0,0 +1,86 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from 'vs/nls'; +import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; +import { Codicon } from 'vs/base/common/codicons'; + +export const DEFAULT_ICON = Codicon.settingsGear; + +export const ICONS = [ + + /* Default */ + DEFAULT_ICON, + + /* hardware/devices */ + registerIcon('profile-icon-vm', Codicon.vm, localize('vm', 'Virtual Machine')), + registerIcon('profile-icon-server', Codicon.server, localize('server', 'Server')), + registerIcon('profile-icon-recordKeys', Codicon.recordKeys, localize('recordkeys', 'Record Keys')), + registerIcon('profile-icon-deviceMobile', Codicon.deviceMobile, localize('deviceMobile', 'Mobile Device')), + registerIcon('profile-icon-watch', Codicon.watch, localize('watch', 'Watch')), + + /* languages */ + registerIcon('profile-icon-ruby', Codicon.ruby, localize('ruby', 'Ruby Language')), + registerIcon('profile-icon-code', Codicon.code, localize('code', 'Coding')), + + /* project types */ + registerIcon('profile-icon-window', Codicon.window, localize('window', 'Window')), + registerIcon('profile-icon-library', Codicon.library, localize('library', 'Library')), + registerIcon('profile-icon-extensions', Codicon.extensions, localize('extensions', 'Extensions')), + registerIcon('profile-icon-terminal', Codicon.terminal, localize('terminal', 'Terminal')), + registerIcon('profile-icon-beaker', Codicon.beaker, localize('testing', 'Testing')), + registerIcon('profile-icon-package', Codicon.package, localize('package', 'Package')), + registerIcon('profile-icon-cloud', Codicon.cloud, localize('cloud', 'Cloud')), + registerIcon('profile-icon-book', Codicon.book, localize('book', 'Book')), + registerIcon('profile-icon-globe', Codicon.globe, localize('globe', 'Globe')), + registerIcon('profile-icon-database', Codicon.database, localize('database', 'Database')), + registerIcon('profile-icon-notebook', Codicon.notebook, localize('notebook', 'Notebook')), + + /* misc */ + registerIcon('profile-icon-gift', Codicon.gift, localize('gift', 'Gift')), + registerIcon('profile-icon-send', Codicon.send, localize('send', 'Send')), + registerIcon('profile-icon-briefcase', Codicon.briefcase, localize('briefcase', 'Briefcase')), + registerIcon('profile-icon-megaphone', Codicon.megaphone, localize('megaphone', 'Megaphone')), + registerIcon('profile-icon-comment', Codicon.comment, localize('comment', 'Comment')), + registerIcon('profile-icon-telescope', Codicon.telescope, localize('telescope', 'Telecope')), + registerIcon('profile-icon-creditCard', Codicon.creditCard, localize('creditcard', 'Credit Card')), + registerIcon('profile-icon-map', Codicon.map, localize('map', 'Map')), + registerIcon('profile-icon-deviceCameraVideo', Codicon.deviceCameraVideo, localize('cameraVideo', 'Video Camera')), + registerIcon('profile-icon-unmute', Codicon.unmute, localize('unmute', 'Unmute')), + registerIcon('profile-icon-law', Codicon.law, localize('law', 'Law')), + registerIcon('profile-icon-graphLine', Codicon.graphLine, localize('graphLine', 'Graph Line')), + registerIcon('profile-icon-heart', Codicon.heart, localize('heart', 'Heart')), + registerIcon('profile-icon-home', Codicon.home, localize('home', 'Home')), + registerIcon('profile-icon-inbox', Codicon.inbox, localize('inbox', 'Inbox')), + registerIcon('profile-icon-mortarBoard', Codicon.mortarBoard, localize('mortarBoard', 'Mortar Board')), + registerIcon('profile-icon-rocket', Codicon.rocket, localize('rocket', 'Rocket')), + registerIcon('profile-icon-magnet', Codicon.magnet, localize('magnet', 'Magnet')), + registerIcon('profile-icon-lock', Codicon.lock, localize('lock', 'Lock')), + registerIcon('profile-icon-milestone', Codicon.milestone, localize('milestone', 'Milestone')), + registerIcon('profile-icon-tag', Codicon.tag, localize('tag', 'Tag')), + registerIcon('profile-icon-pulse', Codicon.pulse, localize('pulse', 'Pulse')), + registerIcon('profile-icon-radioTower', Codicon.radioTower, localize('radioTower', 'Radio Tower')), + registerIcon('profile-icon-smiley', Codicon.smiley, localize('smiley', 'Smiley')), + registerIcon('profile-icon-symbolEvent', Codicon.symbolEvent, localize('symbol event', 'Event Symbol')), + registerIcon('profile-icon-squirrel', Codicon.squirrel, localize('squirrel', 'Squirrel')), + registerIcon('profile-icon-symbolColor', Codicon.symbolColor, localize('symbolColor', 'Color Symbol')), + registerIcon('profile-icon-mail', Codicon.mail, localize('mail', 'Mail')), + registerIcon('profile-icon-key', Codicon.key, localize('key', 'Key')), + registerIcon('profile-icon-pieChart', Codicon.pieChart, localize('pieChart', 'Pie Chart')), + registerIcon('profile-icon-organization', Codicon.organization, localize('organization', 'Organization')), + registerIcon('profile-icon-preview', Codicon.preview, localize('preview', 'Preview')), + registerIcon('profile-icon-wand', Codicon.wand, localize('wand', 'Wand')), + registerIcon('profile-icon-starEmpty', Codicon.starEmpty, localize('startEmpty', 'Empty Star')), + registerIcon('profile-icon-lightbulb', Codicon.lightbulb, localize('lightBulb', 'Idea')), + registerIcon('profile-icon-symbolRuler', Codicon.symbolRuler, localize('symbolRuler', 'Ruler Symbol')), + registerIcon('profile-icon-dashboard', Codicon.dashboard, localize('dashboard', 'Dashboard')), + registerIcon('profile-icon-calendar', Codicon.calendar, localize('calendar', 'Calendar')), + registerIcon('profile-icon-shield', Codicon.shield, localize('shield', 'Shield')), + registerIcon('profile-icon-flame', Codicon.flame, localize('flame', 'Flame')), + registerIcon('profile-icon-compass', Codicon.compass, localize('compass', 'Compass')), + registerIcon('profile-icon-paintcan', Codicon.paintcan, localize('paintcan', 'Paint Can')), + registerIcon('profile-icon-archive', Codicon.archive, localize('archive', 'Archive')), + +];