Skip to content

Commit 4c366ec

Browse files
authored
simplify theme configuration with auto detect (microsoft#209985)
1 parent 4db3075 commit 4c366ec

File tree

5 files changed

+200
-165
lines changed

5 files changed

+200
-165
lines changed

src/vs/workbench/contrib/themes/browser/themes.contribution.ts

Lines changed: 131 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { Color } from 'vs/base/common/color';
1818
import { ColorScheme, isHighContrast } from 'vs/platform/theme/common/theme';
1919
import { colorThemeSchemaId } from 'vs/workbench/services/themes/common/colorThemeSchema';
2020
import { isCancellationError, onUnexpectedError } from 'vs/base/common/errors';
21-
import { IQuickInputButton, IQuickInputService, IQuickPickItem, QuickPickInput } from 'vs/platform/quickinput/common/quickInput';
21+
import { IQuickInputButton, IQuickInputService, IQuickInputToggle, IQuickPick, IQuickPickItem, QuickPickInput } from 'vs/platform/quickinput/common/quickInput';
2222
import { DEFAULT_PRODUCT_ICON_THEME_ID, ProductIconThemeData } from 'vs/workbench/services/themes/browser/productIconThemeData';
2323
import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite';
2424
import { ViewContainerLocation } from 'vs/workbench/common/views';
@@ -45,11 +45,22 @@ import { isWeb } from 'vs/base/common/platform';
4545
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
4646
import { IHostService } from 'vs/workbench/services/host/browser/host';
4747
import { mainWindow } from 'vs/base/browser/window';
48+
import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences';
49+
import { Toggle } from 'vs/base/browser/ui/toggle/toggle';
50+
import { defaultToggleStyles } from 'vs/platform/theme/browser/defaultStyles';
51+
import { DisposableStore } from 'vs/base/common/lifecycle';
52+
import { COLOR_THEME_CONFIGURATION_SETTINGS_TAG } from 'vs/workbench/services/themes/common/themeConfiguration';
4853

4954
export const manageExtensionIcon = registerIcon('theme-selection-manage-extension', Codicon.gear, localize('manageExtensionIcon', 'Icon for the \'Manage\' action in the theme selection quick pick.'));
5055

5156
type PickerResult = 'back' | 'selected' | 'cancelled';
5257

58+
enum ConfigureItem {
59+
BROWSE_GALLERY = 'marketplace',
60+
EXTENSIONS_VIEW = 'extensions',
61+
CUSTOM_TOP_ENTRY = 'customTopEntry'
62+
}
63+
5364
class MarketplaceThemesPicker {
5465
private readonly _installedExtensions: Promise<Set<string>>;
5566
private readonly _marketplaceExtensions: Set<string> = new Set();
@@ -273,13 +284,20 @@ class MarketplaceThemesPicker {
273284
}
274285
}
275286

287+
interface InstalledThemesPickerOptions {
288+
readonly installMessage: string;
289+
readonly browseMessage?: string;
290+
readonly placeholderMessage: string;
291+
readonly marketplaceTag: string;
292+
readonly title?: string;
293+
readonly description?: string;
294+
readonly toggles?: IQuickInputToggle[];
295+
readonly onToggle?: (toggle: IQuickInputToggle, quickInput: IQuickPick<ThemeItem>) => Promise<void>;
296+
}
276297

277298
class InstalledThemesPicker {
278299
constructor(
279-
private readonly installMessage: string,
280-
private readonly browseMessage: string | undefined,
281-
private readonly placeholderMessage: string,
282-
private readonly marketplaceTag: string,
300+
private readonly options: InstalledThemesPickerOptions,
283301
private readonly setTheme: (theme: IWorkbenchTheme | undefined, settingsTarget: ThemeSettingTarget) => Promise<any>,
284302
private readonly getMarketplaceColorThemes: (publisher: string, name: string, version: string) => Promise<IWorkbenchTheme[]>,
285303
@IQuickInputService private readonly quickInputService: IQuickInputService,
@@ -291,13 +309,14 @@ class InstalledThemesPicker {
291309
}
292310

293311
public async openQuickPick(picks: QuickPickInput<ThemeItem>[], currentTheme: IWorkbenchTheme) {
312+
294313
let marketplaceThemePicker: MarketplaceThemesPicker | undefined;
295314
if (this.extensionGalleryService.isEnabled()) {
296-
if (this.extensionResourceLoaderService.supportsExtensionGalleryResources && this.browseMessage) {
297-
marketplaceThemePicker = this.instantiationService.createInstance(MarketplaceThemesPicker, this.getMarketplaceColorThemes.bind(this), this.marketplaceTag);
298-
picks = [...configurationEntries(this.browseMessage), ...picks];
315+
if (this.extensionResourceLoaderService.supportsExtensionGalleryResources && this.options.browseMessage) {
316+
marketplaceThemePicker = this.instantiationService.createInstance(MarketplaceThemesPicker, this.getMarketplaceColorThemes.bind(this), this.options.marketplaceTag);
317+
picks = [configurationEntry(this.options.browseMessage, ConfigureItem.BROWSE_GALLERY), ...picks];
299318
} else {
300-
picks = [...picks, ...configurationEntries(this.installMessage)];
319+
picks = [...picks, { type: 'separator' }, configurationEntry(this.options.installMessage, ConfigureItem.EXTENSIONS_VIEW)];
301320
}
302321
}
303322

@@ -322,25 +341,34 @@ class InstalledThemesPicker {
322341
const pickInstalledThemes = (activeItemId: string | undefined) => {
323342
return new Promise<void>((s, _) => {
324343
let isCompleted = false;
344+
const disposables = new DisposableStore();
325345

326346
const autoFocusIndex = picks.findIndex(p => isItem(p) && p.id === activeItemId);
327347
const quickpick = this.quickInputService.createQuickPick<ThemeItem>();
328348
quickpick.items = picks;
329-
quickpick.placeholder = this.placeholderMessage;
349+
quickpick.title = this.options.title;
350+
quickpick.description = this.options.description;
351+
quickpick.placeholder = this.options.placeholderMessage;
330352
quickpick.activeItems = [picks[autoFocusIndex] as ThemeItem];
331353
quickpick.canSelectMany = false;
354+
quickpick.toggles = this.options.toggles;
355+
quickpick.toggles?.forEach(toggle => {
356+
toggle.onChange(() => this.options.onToggle?.(toggle, quickpick), undefined, disposables);
357+
});
332358
quickpick.matchOnDescription = true;
333359
quickpick.onDidAccept(async _ => {
334360
isCompleted = true;
335361
const theme = quickpick.selectedItems[0];
336-
if (!theme || typeof theme.id === 'undefined') { // 'pick in marketplace' entry
337-
if (marketplaceThemePicker) {
338-
const res = await marketplaceThemePicker.openQuickPick(quickpick.value, currentTheme, selectTheme);
339-
if (res === 'back') {
340-
await pickInstalledThemes(undefined);
362+
if (!theme || theme.configureItem) { // 'pick in marketplace' entry
363+
if (!theme || theme.configureItem === ConfigureItem.EXTENSIONS_VIEW) {
364+
openExtensionViewlet(this.paneCompositeService, `${this.options.marketplaceTag} ${quickpick.value}`);
365+
} else if (theme.configureItem === ConfigureItem.BROWSE_GALLERY) {
366+
if (marketplaceThemePicker) {
367+
const res = await marketplaceThemePicker.openQuickPick(quickpick.value, currentTheme, selectTheme);
368+
if (res === 'back') {
369+
await pickInstalledThemes(undefined);
370+
}
341371
}
342-
} else {
343-
openExtensionViewlet(this.paneCompositeService, `${this.marketplaceTag} ${quickpick.value}`);
344372
}
345373
} else {
346374
selectTheme(theme.theme, true);
@@ -356,14 +384,15 @@ class InstalledThemesPicker {
356384
s();
357385
}
358386
quickpick.dispose();
387+
disposables.dispose();
359388
});
360389
quickpick.onDidTriggerItemButton(e => {
361390
if (isItem(e.item)) {
362391
const extensionId = e.item.theme?.extensionData?.extensionId;
363392
if (extensionId) {
364393
openExtensionViewlet(this.paneCompositeService, `@id:${extensionId}`);
365394
} else {
366-
openExtensionViewlet(this.paneCompositeService, `${this.marketplaceTag} ${quickpick.value}`);
395+
openExtensionViewlet(this.paneCompositeService, `${this.options.marketplaceTag} ${quickpick.value}`);
367396
}
368397
}
369398
});
@@ -394,28 +423,80 @@ registerAction2(class extends Action2 {
394423
});
395424
}
396425

426+
private getTitle(colorScheme: ColorScheme | undefined): string | undefined {
427+
switch (colorScheme) {
428+
case ColorScheme.DARK: return localize('themes.selectTheme.darkScheme', "Select Color Theme for Dark Mode");
429+
case ColorScheme.LIGHT: return localize('themes.selectTheme.lightScheme', "Select Color Theme for Light Mode");
430+
case ColorScheme.HIGH_CONTRAST_DARK: return localize('themes.selectTheme.darkHC', "Select Color Theme for Dark High Contrast Mode");
431+
case ColorScheme.HIGH_CONTRAST_LIGHT: return localize('themes.selectTheme.lightHC', "Select Color Theme for Light High Contrast Mode");
432+
default:
433+
return undefined;
434+
}
435+
}
436+
397437
override async run(accessor: ServicesAccessor) {
398438
const themeService = accessor.get(IWorkbenchThemeService);
439+
const preferencesService = accessor.get(IPreferencesService);
399440

400-
const installMessage = localize('installColorThemes', "Install Additional Color Themes...");
401-
const browseMessage = '$(plus) ' + localize('browseColorThemes', "Browse Additional Color Themes...");
402-
const placeholderMessage = localize('themes.selectTheme', "Select Color Theme (Up/Down Keys to Preview)");
403-
const marketplaceTag = 'category:themes';
441+
const preferredColorScheme = themeService.getPreferredColorScheme();
442+
443+
let modeConfigureToggle;
444+
if (preferredColorScheme) {
445+
modeConfigureToggle = new Toggle({
446+
title: 'Automatic Mode Switching is Enabled. Click to configure.',
447+
icon: Codicon.colorMode,
448+
isChecked: false,
449+
...defaultToggleStyles
450+
});
451+
} else {
452+
modeConfigureToggle = new Toggle({
453+
title: 'Click to configure automatic mode switching.',
454+
icon: Codicon.gear,
455+
isChecked: false,
456+
...defaultToggleStyles
457+
});
458+
}
459+
460+
const options = {
461+
installMessage: localize('installColorThemes', "Install Additional Color Themes..."),
462+
browseMessage: '$(plus) ' + localize('browseColorThemes', "Browse Additional Color Themes..."),
463+
placeholderMessage: this.getTitle(preferredColorScheme) ?? 'Select Color Theme (Mode Switching Disabled)',
464+
marketplaceTag: 'category:themes',
465+
toggles: [modeConfigureToggle],
466+
onToggle: async (toggle, picker) => {
467+
picker.hide();
468+
await preferencesService.openSettings({ query: `@tag:${COLOR_THEME_CONFIGURATION_SETTINGS_TAG}` });
469+
}
470+
} satisfies InstalledThemesPickerOptions;
404471
const setTheme = (theme: IWorkbenchTheme | undefined, settingsTarget: ThemeSettingTarget) => themeService.setColorTheme(theme as IWorkbenchColorTheme, settingsTarget);
405472
const getMarketplaceColorThemes = (publisher: string, name: string, version: string) => themeService.getMarketplaceColorThemes(publisher, name, version);
406473

407474
const instantiationService = accessor.get(IInstantiationService);
408-
const picker = instantiationService.createInstance(InstalledThemesPicker, installMessage, browseMessage, placeholderMessage, marketplaceTag, setTheme, getMarketplaceColorThemes);
475+
const picker = instantiationService.createInstance(InstalledThemesPicker, options, setTheme, getMarketplaceColorThemes);
409476

410477
const themes = await themeService.getColorThemes();
411478
const currentTheme = themeService.getColorTheme();
412479

413-
const picks: QuickPickInput<ThemeItem>[] = [
414-
...toEntries(themes.filter(t => t.type === ColorScheme.LIGHT), localize('themes.category.light', "light themes")),
415-
...toEntries(themes.filter(t => t.type === ColorScheme.DARK), localize('themes.category.dark', "dark themes")),
416-
...toEntries(themes.filter(t => isHighContrast(t.type)), localize('themes.category.hc', "high contrast themes")),
417-
];
480+
const lightEntries = toEntries(themes.filter(t => t.type === ColorScheme.LIGHT), localize('themes.category.light', "light themes"));
481+
const darkEntries = toEntries(themes.filter(t => t.type === ColorScheme.DARK), localize('themes.category.dark', "dark themes"));
482+
const hcEntries = toEntries(themes.filter(t => isHighContrast(t.type)), localize('themes.category.hc', "high contrast themes"));
483+
484+
let picks;
485+
switch (preferredColorScheme) {
486+
case ColorScheme.DARK:
487+
picks = [...darkEntries, ...lightEntries, ...hcEntries];
488+
break;
489+
case ColorScheme.HIGH_CONTRAST_DARK:
490+
case ColorScheme.HIGH_CONTRAST_LIGHT:
491+
picks = [...hcEntries, ...lightEntries, ...darkEntries];
492+
break;
493+
case ColorScheme.LIGHT:
494+
default:
495+
picks = [...lightEntries, ...darkEntries, ...hcEntries];
496+
break;
497+
}
418498
await picker.openQuickPick(picks, currentTheme);
499+
419500
}
420501
});
421502

@@ -435,14 +516,16 @@ registerAction2(class extends Action2 {
435516
override async run(accessor: ServicesAccessor) {
436517
const themeService = accessor.get(IWorkbenchThemeService);
437518

438-
const installMessage = localize('installIconThemes', "Install Additional File Icon Themes...");
439-
const placeholderMessage = localize('themes.selectIconTheme', "Select File Icon Theme (Up/Down Keys to Preview)");
440-
const marketplaceTag = 'tag:icon-theme';
519+
const options = {
520+
installMessage: localize('installIconThemes', "Install Additional File Icon Themes..."),
521+
placeholderMessage: localize('themes.selectIconTheme', "Select File Icon Theme (Up/Down Keys to Preview)"),
522+
marketplaceTag: 'tag:icon-theme'
523+
};
441524
const setTheme = (theme: IWorkbenchTheme | undefined, settingsTarget: ThemeSettingTarget) => themeService.setFileIconTheme(theme as IWorkbenchFileIconTheme, settingsTarget);
442525
const getMarketplaceColorThemes = (publisher: string, name: string, version: string) => themeService.getMarketplaceFileIconThemes(publisher, name, version);
443526

444527
const instantiationService = accessor.get(IInstantiationService);
445-
const picker = instantiationService.createInstance(InstalledThemesPicker, installMessage, undefined, placeholderMessage, marketplaceTag, setTheme, getMarketplaceColorThemes);
528+
const picker = instantiationService.createInstance(InstalledThemesPicker, options, setTheme, getMarketplaceColorThemes);
446529

447530
const picks: QuickPickInput<ThemeItem>[] = [
448531
{ type: 'separator', label: localize('fileIconThemeCategory', 'file icon themes') },
@@ -470,15 +553,17 @@ registerAction2(class extends Action2 {
470553
override async run(accessor: ServicesAccessor) {
471554
const themeService = accessor.get(IWorkbenchThemeService);
472555

473-
const installMessage = localize('installProductIconThemes', "Install Additional Product Icon Themes...");
474-
const browseMessage = '$(plus) ' + localize('browseProductIconThemes', "Browse Additional Product Icon Themes...");
475-
const placeholderMessage = localize('themes.selectProductIconTheme', "Select Product Icon Theme (Up/Down Keys to Preview)");
476-
const marketplaceTag = 'tag:product-icon-theme';
556+
const options = {
557+
installMessage: localize('installProductIconThemes', "Install Additional Product Icon Themes..."),
558+
browseMessage: '$(plus) ' + localize('browseProductIconThemes', "Browse Additional Product Icon Themes..."),
559+
placeholderMessage: localize('themes.selectProductIconTheme', "Select Product Icon Theme (Up/Down Keys to Preview)"),
560+
marketplaceTag: 'tag:product-icon-theme'
561+
};
477562
const setTheme = (theme: IWorkbenchTheme | undefined, settingsTarget: ThemeSettingTarget) => themeService.setProductIconTheme(theme as IWorkbenchProductIconTheme, settingsTarget);
478563
const getMarketplaceColorThemes = (publisher: string, name: string, version: string) => themeService.getMarketplaceProductIconThemes(publisher, name, version);
479564

480565
const instantiationService = accessor.get(IInstantiationService);
481-
const picker = instantiationService.createInstance(InstalledThemesPicker, installMessage, browseMessage, placeholderMessage, marketplaceTag, setTheme, getMarketplaceColorThemes);
566+
const picker = instantiationService.createInstance(InstalledThemesPicker, options, setTheme, getMarketplaceColorThemes);
482567

483568
const picks: QuickPickInput<ThemeItem>[] = [
484569
{ type: 'separator', label: localize('productIconThemeCategory', 'product icon themes') },
@@ -510,19 +595,14 @@ function findBuiltInThemes(themes: IWorkbenchColorTheme[], extension: { publishe
510595
return themes.filter(({ extensionData }) => extensionData && extensionData.extensionIsBuiltin && equalsIgnoreCase(extensionData.extensionPublisher, extension.publisher) && equalsIgnoreCase(extensionData.extensionName, extension.name));
511596
}
512597

513-
function configurationEntries(label: string): QuickPickInput<ThemeItem>[] {
514-
return [
515-
{
516-
type: 'separator'
517-
},
518-
{
519-
id: undefined,
520-
label: label,
521-
alwaysShow: true,
522-
buttons: [configureButton]
523-
}
524-
];
525-
598+
function configurationEntry(label: string, configureItem: ConfigureItem): QuickPickInput<ThemeItem> {
599+
return {
600+
id: undefined,
601+
label: label,
602+
alwaysShow: true,
603+
buttons: [configureButton],
604+
configureItem: configureItem
605+
};
526606
}
527607

528608
function openExtensionViewlet(paneCompositeService: IPaneCompositePartService, query: string) {
@@ -540,6 +620,7 @@ interface ThemeItem extends IQuickPickItem {
540620
readonly label: string;
541621
readonly description?: string;
542622
readonly alwaysShow?: boolean;
623+
readonly configureItem?: ConfigureItem;
543624
}
544625

545626
function isItem(i: QuickPickInput<ThemeItem>): i is ThemeItem {

src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ import { IStorageService, StorageScope, StorageTarget, WillSaveStateReason } fro
4848
import { ITelemetryService, TelemetryLevel, firstSessionDateStorageKey } from 'vs/platform/telemetry/common/telemetry';
4949
import { getTelemetryLevel } from 'vs/platform/telemetry/common/telemetryUtils';
5050
import { defaultButtonStyles, defaultToggleStyles } from 'vs/platform/theme/browser/defaultStyles';
51-
import { IThemeService } from 'vs/platform/theme/common/themeService';
5251
import { IWindowOpenable } from 'vs/platform/window/common/window';
5352
import { IWorkspaceContextService, UNKNOWN_EMPTY_WINDOW_WORKSPACE } from 'vs/platform/workspace/common/workspace';
5453
import { IRecentFolder, IRecentWorkspace, IRecentlyOpened, IWorkspacesService, isRecentFolder, isRecentWorkspace } from 'vs/platform/workspaces/common/workspaces';
@@ -68,7 +67,7 @@ import { startEntries } from 'vs/workbench/contrib/welcomeGettingStarted/common/
6867
import { GroupDirection, GroupsOrder, IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
6968
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
7069
import { IHostService } from 'vs/workbench/services/host/browser/host';
71-
import { ThemeSettings } from 'vs/workbench/services/themes/common/workbenchThemeService';
70+
import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
7271
import { GettingStartedIndexList } from './gettingStartedList';
7372
import { IWorkbenchAssignmentService } from 'vs/workbench/services/assignment/common/assignmentService';
7473

@@ -175,7 +174,7 @@ export class GettingStartedPage extends EditorPane {
175174
@ILanguageService private readonly languageService: ILanguageService,
176175
@IFileService private readonly fileService: IFileService,
177176
@IOpenerService private readonly openerService: IOpenerService,
178-
@IThemeService themeService: IThemeService,
177+
@IWorkbenchThemeService public override readonly themeService: IWorkbenchThemeService,
179178
@IStorageService private storageService: IStorageService,
180179
@IExtensionService private readonly extensionService: IExtensionService,
181180
@IInstantiationService private readonly instantiationService: IInstantiationService,
@@ -690,12 +689,16 @@ export class GettingStartedPage extends EditorPane {
690689

691690
postTrueKeysMessage();
692691

693-
this.stepDisposables.add(this.webview.onMessage(e => {
692+
this.stepDisposables.add(this.webview.onMessage(async e => {
694693
const message: string = e.message as string;
695694
if (message.startsWith('command:')) {
696695
this.openerService.open(message, { allowCommands: true });
697696
} else if (message.startsWith('setTheme:')) {
698-
this.configurationService.updateValue(ThemeSettings.COLOR_THEME, message.slice('setTheme:'.length), ConfigurationTarget.USER);
697+
const themeId = message.slice('setTheme:'.length);
698+
const theme = (await this.themeService.getColorThemes()).find(theme => theme.settingsId === themeId);
699+
if (theme) {
700+
this.themeService.setColorTheme(theme.id, ConfigurationTarget.USER);
701+
}
699702
} else {
700703
console.error('Unexpected message', message);
701704
}

0 commit comments

Comments
 (0)