Skip to content

auto switch of color theme based on browser API prefers-color-scheme #87405

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Dec 19, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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