diff --git a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts index 74cdf94ddaf..ed8db767922 100644 --- a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts +++ b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts @@ -6,7 +6,7 @@ import * as nls from 'vs/nls'; import * as types from 'vs/base/common/types'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { IWorkbenchThemeService, IColorTheme, ITokenColorCustomizations, IFileIconTheme, ExtensionData, VS_LIGHT_THEME, VS_DARK_THEME, VS_HC_THEME, COLOR_THEME_SETTING, ICON_THEME_SETTING, CUSTOM_WORKBENCH_COLORS_SETTING, CUSTOM_EDITOR_COLORS_SETTING, DETECT_HC_SETTING, HC_THEME_ID, IColorCustomizations, CUSTOM_EDITOR_TOKENSTYLES_SETTING, IExperimentalTokenStyleCustomizations } from 'vs/workbench/services/themes/common/workbenchThemeService'; +import { IWorkbenchThemeService, IColorTheme, ITokenColorCustomizations, IFileIconTheme, ExtensionData, VS_LIGHT_THEME, VS_DARK_THEME, VS_HC_THEME, COLOR_THEME_SETTING, ICON_THEME_SETTING, CUSTOM_WORKBENCH_COLORS_SETTING, CUSTOM_EDITOR_COLORS_SETTING, IColorCustomizations, CUSTOM_EDITOR_TOKENSTYLES_SETTING, IExperimentalTokenStyleCustomizations } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -14,7 +14,7 @@ import * as errors from 'vs/base/common/errors'; import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, IConfigurationPropertySchema, IConfigurationNode } from 'vs/platform/configuration/common/configurationRegistry'; import { ColorThemeData } from 'vs/workbench/services/themes/common/colorThemeData'; -import { ITheme, Extensions as ThemingExtensions, IThemingRegistry } from 'vs/platform/theme/common/themeService'; +import { ITheme, Extensions as ThemingExtensions, IThemingRegistry, ThemeType, LIGHT, DARK, HIGH_CONTRAST } from 'vs/platform/theme/common/themeService'; import { Event, Emitter } from 'vs/base/common/event'; import { registerFileIconThemeSchemas } from 'vs/workbench/services/themes/common/fileIconThemeSchema'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; @@ -35,13 +35,25 @@ import { getRemoteAuthority } from 'vs/platform/remote/common/remoteHosts'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { IExtensionResourceLoaderService } from 'vs/workbench/services/extensionResourceLoader/common/extensionResourceLoader'; +// settings + +const PREFERRED_DARK_THEME_SETTING = 'workbench.preferredDarkColorTheme'; +const PREFERRED_LIGHT_THEME_SETTING = 'workbench.preferredLightColorTheme'; +const PREFERRED_HC_THEME_SETTING = 'workbench.preferredHighContrastColorTheme'; +const DETECT_COLOR_SCHEME_SETTING = 'workbench.autoDetectColorScheme'; +const DETECT_HC_SETTING = 'window.autoDetectHighContrast'; + // implementation const DEFAULT_THEME_ID = 'vs-dark vscode-theme-defaults-themes-dark_plus-json'; const DEFAULT_THEME_SETTING_VALUE = 'Default Dark+'; +const DEFAULT_THEME_DARK_SETTING_VALUE = 'Default Dark+'; +const DEFAULT_THEME_LIGHT_SETTING_VALUE = 'Default Light+'; +const DEFAULT_THEME_HC_SETTING_VALUE = 'Default High Contrast'; const PERSISTED_THEME_STORAGE_KEY = 'colorThemeData'; const PERSISTED_ICON_THEME_STORAGE_KEY = 'iconThemeData'; +const PERSISTED_OS_COLOR_SCHEME = 'osColorScheme'; const defaultThemeExtensionId = 'vscode-theme-defaults'; const oldDefaultThemeExtensionId = 'vscode-theme-colorful-defaults'; @@ -148,6 +160,7 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { this.initialize().then(undefined, errors.onUnexpectedError).then(_ => { this.installConfigurationListener(); + this.installPreferredSchemeListener(); }); let prevColorId: string | undefined = undefined; @@ -155,8 +168,8 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { // update settings schema setting for theme specific settings this.colorThemeStore.onDidChange(async event => { // updates enum for the 'workbench.colorTheme` setting - colorThemeSettingSchema.enum = event.themes.map(t => t.settingsId); - colorThemeSettingSchema.enumDescriptions = event.themes.map(t => t.description || ''); + colorThemeSettingEnum.splice(0, colorThemeSettingEnum.length, ...event.themes.map(t => t.settingsId)); + colorThemeSettingEnumDescriptions.splice(0, colorThemeSettingEnumDescriptions.length, ...event.themes.map(t => t.description || '')); const themeSpecificWorkbenchColors: IJSONSchema = { properties: {} }; const themeSpecificTokenColors: IJSONSchema = { properties: {} }; @@ -248,44 +261,40 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { } private initialize(): Promise<[IColorTheme | null, IFileIconTheme | null]> { - let detectHCThemeSetting = this.configurationService.getValue(DETECT_HC_SETTING); - - let colorThemeSetting: string; - if (this.environmentService.configuration.highContrast && detectHCThemeSetting) { - colorThemeSetting = HC_THEME_ID; - } else { - colorThemeSetting = this.configurationService.getValue(COLOR_THEME_SETTING); - } - - let iconThemeSetting = this.configurationService.getValue(ICON_THEME_SETTING); + const colorThemeSetting = this.configurationService.getValue(COLOR_THEME_SETTING); + const iconThemeSetting = this.configurationService.getValue(ICON_THEME_SETTING); const extDevLocs = this.environmentService.extensionDevelopmentLocationURI; - let uri: URI | undefined; - if (extDevLocs && extDevLocs.length > 0) { - // if there are more than one ext dev paths, use first - uri = extDevLocs[0]; - } - return Promise.all([ - this.colorThemeStore.findThemeDataBySettingsId(colorThemeSetting, DEFAULT_THEME_ID).then(theme => { - return this.colorThemeStore.findThemeDataByParentLocation(uri).then(devThemes => { - if (devThemes.length) { - return this.setColorTheme(devThemes[0].id, ConfigurationTarget.MEMORY); - } else { - return this.setColorTheme(theme && theme.id, undefined); - } - }); - }), - this.iconThemeStore.findThemeBySettingsId(iconThemeSetting).then(theme => { - return this.iconThemeStore.findThemeDataByParentLocation(uri).then(devThemes => { - if (devThemes.length) { - return this.setFileIconTheme(devThemes[0].id, ConfigurationTarget.MEMORY); - } else { - return this.setFileIconTheme(theme ? theme.id : DEFAULT_ICON_THEME_ID, undefined); - } - }); - }), - ]); + const initializeColorTheme = async () => { + if (extDevLocs && extDevLocs.length > 0) { // in dev mode, switch to a theme provided by the extension under dev. + const devThemes = await this.colorThemeStore.findThemeDataByParentLocation(extDevLocs[0]); + if (devThemes.length) { + return this.setColorTheme(devThemes[0].id, ConfigurationTarget.MEMORY); + } + } + let theme = await this.colorThemeStore.findThemeDataBySettingsId(colorThemeSetting, DEFAULT_THEME_ID); + + const persistedColorScheme = this.storageService.get(PERSISTED_OS_COLOR_SCHEME, StorageScope.GLOBAL); + const preferredColorScheme = this.getPreferredColorScheme(); + if (persistedColorScheme && preferredColorScheme && persistedColorScheme !== preferredColorScheme) { + return this.applyPreferredColorTheme(preferredColorScheme); + } + return this.setColorTheme(theme && theme.id, undefined); + }; + + const initializeIconTheme = async () => { + if (extDevLocs && extDevLocs.length > 0) { // in dev mode, switch to a theme provided by the extension under dev. + const devThemes = await this.iconThemeStore.findThemeDataByParentLocation(extDevLocs[0]); + if (devThemes.length) { + return this.setFileIconTheme(devThemes[0].id, ConfigurationTarget.MEMORY); + } + } + const theme = await this.iconThemeStore.findThemeBySettingsId(iconThemeSetting); + return this.setFileIconTheme(theme ? theme.id : DEFAULT_ICON_THEME_ID, undefined); + }; + + return Promise.all([initializeColorTheme(), initializeIconTheme()]); } private installConfigurationListener() { @@ -300,6 +309,18 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { }); } } + if (e.affectsConfiguration(DETECT_COLOR_SCHEME_SETTING)) { + this.handlePreferredSchemeUpdated(); + } + if (e.affectsConfiguration(PREFERRED_DARK_THEME_SETTING) && this.getPreferredColorScheme() === DARK) { + this.applyPreferredColorTheme(DARK); + } + if (e.affectsConfiguration(PREFERRED_LIGHT_THEME_SETTING) && this.getPreferredColorScheme() === LIGHT) { + this.applyPreferredColorTheme(LIGHT); + } + if (e.affectsConfiguration(PREFERRED_HC_THEME_SETTING) && this.getPreferredColorScheme() === HIGH_CONTRAST) { + this.applyPreferredColorTheme(HIGH_CONTRAST); + } if (e.affectsConfiguration(ICON_THEME_SETTING)) { let iconThemeSetting = this.configurationService.getValue(ICON_THEME_SETTING); if (iconThemeSetting !== this.currentIconTheme.settingsId) { @@ -330,6 +351,48 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { }); } + // preferred scheme handling + + private installPreferredSchemeListener() { + window.matchMedia('(prefers-color-scheme: dark)').addListener(async () => this.handlePreferredSchemeUpdated()); + } + + private async handlePreferredSchemeUpdated() { + const scheme = this.getPreferredColorScheme(); + this.storageService.store(PERSISTED_OS_COLOR_SCHEME, scheme, StorageScope.GLOBAL); + if (scheme) { + return this.applyPreferredColorTheme(scheme); + } + return undefined; + } + + private getPreferredColorScheme(): ThemeType | undefined { + let detectHCThemeSetting = this.configurationService.getValue(DETECT_HC_SETTING); + if (this.environmentService.configuration.highContrast && detectHCThemeSetting) { + return HIGH_CONTRAST; + } + if (this.configurationService.getValue(DETECT_COLOR_SCHEME_SETTING)) { + if (window.matchMedia(`(prefers-color-scheme: light)`).matches) { + return LIGHT; + } else if (window.matchMedia(`(prefers-color-scheme: dark)`).matches) { + return DARK; + } + } + return undefined; + } + + private async applyPreferredColorTheme(type: ThemeType): Promise { + const settingId = type === DARK ? PREFERRED_DARK_THEME_SETTING : type === LIGHT ? PREFERRED_LIGHT_THEME_SETTING : PREFERRED_HC_THEME_SETTING; + const themeSettingId = this.configurationService.getValue(settingId); + if (themeSettingId) { + const theme = await this.colorThemeStore.findThemeDataBySettingsId(themeSettingId, undefined); + if (theme) { + return this.setColorTheme(theme.id, 'auto'); + } + } + return null; + } + public getColorTheme(): IColorTheme { return this.currentColorTheme; } @@ -352,11 +415,10 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { themeId = validateThemeId(themeId); // migrate theme ids - return this.colorThemeStore.findThemeData(themeId, DEFAULT_THEME_ID).then(data => { - if (!data) { + return this.colorThemeStore.findThemeData(themeId, DEFAULT_THEME_ID).then(themeData => { + if (!themeData) { return null; } - const themeData = data; return themeData.ensureLoaded(this.extensionResourceLoaderService).then(_ => { if (themeId === this.currentColorTheme.id && !this.currentColorTheme.isLoaded && this.currentColorTheme.hasEqualData(themeData)) { this.currentColorTheme.clearCaches(); @@ -641,14 +703,46 @@ registerFileIconThemeSchemas(); // Configuration: Themes const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); +const colorThemeSettingEnum: string[] = []; +const colorThemeSettingEnumDescriptions: string[] = []; + const colorThemeSettingSchema: IConfigurationPropertySchema = { type: 'string', description: nls.localize('colorTheme', "Specifies the color theme used in the workbench."), default: DEFAULT_THEME_SETTING_VALUE, - enum: [], - enumDescriptions: [], + enum: colorThemeSettingEnum, + enumDescriptions: colorThemeSettingEnumDescriptions, errorMessage: nls.localize('colorThemeError', "Theme is unknown or not installed."), }; +const preferredDarkThemeSettingSchema: IConfigurationPropertySchema = { + type: 'string', + description: nls.localize('preferredDarkColorTheme', 'Specifies the preferred color theme for dark OS appearance when \'{0}\' is enabled.', DETECT_COLOR_SCHEME_SETTING), + default: DEFAULT_THEME_DARK_SETTING_VALUE, + enum: colorThemeSettingEnum, + enumDescriptions: colorThemeSettingEnumDescriptions, + errorMessage: nls.localize('colorThemeError', "Theme is unknown or not installed."), +}; +const preferredLightThemeSettingSchema: IConfigurationPropertySchema = { + type: 'string', + description: nls.localize('preferredLightColorTheme', 'Specifies the preferred color theme for light OS appearance when \'{0}\' is enabled.', DETECT_COLOR_SCHEME_SETTING), + default: DEFAULT_THEME_LIGHT_SETTING_VALUE, + enum: colorThemeSettingEnum, + enumDescriptions: colorThemeSettingEnumDescriptions, + errorMessage: nls.localize('colorThemeError', "Theme is unknown or not installed."), +}; +const preferredHCThemeSettingSchema: IConfigurationPropertySchema = { + type: 'string', + description: nls.localize('preferredHCColorTheme', 'Specifies the preferred color theme used in high contrast mode when \'{0}\' is enabled.', DETECT_HC_SETTING), + default: DEFAULT_THEME_HC_SETTING_VALUE, + enum: colorThemeSettingEnum, + enumDescriptions: colorThemeSettingEnumDescriptions, + errorMessage: nls.localize('colorThemeError', "Theme is unknown or not installed."), +}; +const detectColorSchemeSettingSchema: IConfigurationPropertySchema = { + type: 'boolean', + description: nls.localize('detectColorScheme', 'If set, automatically switch to the preferred color theme based on the OS appearance.'), + default: true +}; const iconThemeSettingSchema: IConfigurationPropertySchema = { type: ['string', 'null'], @@ -675,6 +769,10 @@ const themeSettingsConfiguration: IConfigurationNode = { type: 'object', properties: { [COLOR_THEME_SETTING]: colorThemeSettingSchema, + [PREFERRED_DARK_THEME_SETTING]: preferredDarkThemeSettingSchema, + [PREFERRED_LIGHT_THEME_SETTING]: preferredLightThemeSettingSchema, + [PREFERRED_HC_THEME_SETTING]: preferredHCThemeSettingSchema, + [DETECT_COLOR_SCHEME_SETTING]: detectColorSchemeSettingSchema, [ICON_THEME_SETTING]: iconThemeSettingSchema, [CUSTOM_WORKBENCH_COLORS_SETTING]: colorCustomizationsSchema } diff --git a/src/vs/workbench/services/themes/common/workbenchThemeService.ts b/src/vs/workbench/services/themes/common/workbenchThemeService.ts index 9937ca11be9..4756204b595 100644 --- a/src/vs/workbench/services/themes/common/workbenchThemeService.ts +++ b/src/vs/workbench/services/themes/common/workbenchThemeService.ts @@ -18,7 +18,6 @@ export const VS_HC_THEME = 'hc-black'; export const HC_THEME_ID = 'Default High Contrast'; export const COLOR_THEME_SETTING = 'workbench.colorTheme'; -export const DETECT_HC_SETTING = 'window.autoDetectHighContrast'; export const ICON_THEME_SETTING = 'workbench.iconTheme'; export const CUSTOM_WORKBENCH_COLORS_SETTING = 'workbench.colorCustomizations'; export const CUSTOM_EDITOR_COLORS_SETTING = 'editor.tokenColorCustomizations';