Skip to content

Commit

Permalink
Merge pull request #87405 from microsoft/aeschli/detectOSColorSchemeC…
Browse files Browse the repository at this point in the history
…hanges

auto switch of color theme based on browser API `prefers-color-scheme`
  • Loading branch information
aeschli committed Dec 19, 2019
2 parents c1c46d0 + 10091e7 commit 139e59f
Show file tree
Hide file tree
Showing 2 changed files with 141 additions and 44 deletions.
184 changes: 141 additions & 43 deletions src/vs/workbench/services/themes/browser/workbenchThemeService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@
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';
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';
Expand All @@ -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';
Expand Down Expand Up @@ -148,15 +160,16 @@ export class WorkbenchThemeService implements IWorkbenchThemeService {

this.initialize().then(undefined, errors.onUnexpectedError).then(_ => {
this.installConfigurationListener();
this.installPreferredSchemeListener();
});

let prevColorId: string | undefined = undefined;

// 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: {} };
Expand Down Expand Up @@ -248,44 +261,40 @@ export class WorkbenchThemeService implements IWorkbenchThemeService {
}

private initialize(): Promise<[IColorTheme | null, IFileIconTheme | null]> {
let detectHCThemeSetting = this.configurationService.getValue<boolean>(DETECT_HC_SETTING);
const colorThemeSetting = this.configurationService.getValue<string>(COLOR_THEME_SETTING);
const iconThemeSetting = this.configurationService.getValue<string | null>(ICON_THEME_SETTING);

let colorThemeSetting: string;
if (this.environmentService.configuration.highContrast && detectHCThemeSetting) {
colorThemeSetting = HC_THEME_ID;
} else {
colorThemeSetting = this.configurationService.getValue<string>(COLOR_THEME_SETTING);
}
const extDevLocs = this.environmentService.extensionDevelopmentLocationURI;

let iconThemeSetting = this.configurationService.getValue<string | null>(ICON_THEME_SETTING);
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 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];
}
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);
};

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 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() {
Expand All @@ -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<string | null>(ICON_THEME_SETTING);
if (iconThemeSetting !== this.currentIconTheme.settingsId) {
Expand Down Expand Up @@ -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<boolean>(DETECT_HC_SETTING);
if (this.environmentService.configuration.highContrast && detectHCThemeSetting) {
return HIGH_CONTRAST;
}
if (this.configurationService.getValue<boolean>(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<IColorTheme | null> {
const settingId = type === DARK ? PREFERRED_DARK_THEME_SETTING : type === LIGHT ? PREFERRED_LIGHT_THEME_SETTING : PREFERRED_HC_THEME_SETTING;
const themeSettingId = this.configurationService.getValue<string>(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;
}
Expand All @@ -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();
Expand Down Expand Up @@ -641,14 +703,46 @@ registerFileIconThemeSchemas();
// Configuration: Themes
const configurationRegistry = Registry.as<IConfigurationRegistry>(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'],
Expand All @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down

0 comments on commit 139e59f

Please sign in to comment.