mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-17 15:24:40 +01:00
Implement OS color scheme auto-detection for new users (#302761)
* Implement auto-detect color scheme feature for new users and update related services * Add OS color-scheme auto-detection support for new users Co-authored-by: Copilot <copilot@github.com> * Add assertions to verify color scheme detection for high contrast themes Co-authored-by: Copilot <copilot@github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: mrleemurray <mrleemurray@users.noreply.github.com> Co-authored-by: Copilot <copilot@github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -23,4 +23,12 @@ export interface IThemeMainService {
|
||||
getWindowSplash(workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | undefined): IPartsSplash | undefined;
|
||||
|
||||
getColorScheme(): IColorScheme;
|
||||
|
||||
/**
|
||||
* Whether OS color-scheme auto-detection is active.
|
||||
* Returns `true` when the `window.autoDetectColorScheme` setting is enabled,
|
||||
* or for fresh installs where no theme has been stored yet and the user
|
||||
* has not explicitly configured the setting (e.g. via settings sync).
|
||||
*/
|
||||
isAutoDetectColorScheme(): boolean;
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ export class ThemeMainService extends Disposable implements IThemeMainService {
|
||||
}
|
||||
|
||||
private updateSystemColorTheme(): void {
|
||||
if (isLinux || Setting.DETECT_COLOR_SCHEME.getValue(this.configurationService)) {
|
||||
if (isLinux || this.isAutoDetectColorScheme()) {
|
||||
electron.nativeTheme.themeSource = 'system'; // only with `system` we can detect the system color scheme
|
||||
} else {
|
||||
switch (Setting.SYSTEM_COLOR_THEME.getValue(this.configurationService)) {
|
||||
@@ -174,13 +174,26 @@ export class ThemeMainService extends Disposable implements IThemeMainService {
|
||||
return colorScheme.dark ? ThemeTypeSelector.HC_BLACK : ThemeTypeSelector.HC_LIGHT;
|
||||
}
|
||||
|
||||
if (Setting.DETECT_COLOR_SCHEME.getValue(this.configurationService)) {
|
||||
if (this.isAutoDetectColorScheme()) {
|
||||
return colorScheme.dark ? ThemeTypeSelector.VS_DARK : ThemeTypeSelector.VS;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
isAutoDetectColorScheme(): boolean {
|
||||
if (Setting.DETECT_COLOR_SCHEME.getValue(this.configurationService)) {
|
||||
return true;
|
||||
}
|
||||
// For new installs with no stored theme, auto-detect OS color scheme by default
|
||||
// unless the user has explicitly configured the setting (e.g. via settings sync)
|
||||
if (!this.stateService.getItem(THEME_STORAGE_KEY)) {
|
||||
const { userValue } = this.configurationService.inspect<boolean>(Setting.DETECT_COLOR_SCHEME.key);
|
||||
return userValue === undefined;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
getBackgroundColor(): string {
|
||||
const preferred = this.getPreferredBaseTheme();
|
||||
const stored = this.getStoredBaseTheme();
|
||||
|
||||
@@ -1561,7 +1561,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic
|
||||
os: { release: release(), hostname: hostname(), arch: arch() },
|
||||
|
||||
autoDetectHighContrast: windowConfig?.autoDetectHighContrast ?? true,
|
||||
autoDetectColorScheme: windowConfig?.autoDetectColorScheme ?? false,
|
||||
autoDetectColorScheme: windowConfig?.autoDetectColorScheme ?? this.themeMainService.isAutoDetectColorScheme(),
|
||||
accessibilitySupport: app.accessibilitySupportEnabled,
|
||||
colorScheme: this.themeMainService.getColorScheme(),
|
||||
policiesData: this.policyService.serialize(),
|
||||
|
||||
@@ -118,7 +118,8 @@ export class WorkbenchThemeService extends Disposable implements IWorkbenchTheme
|
||||
) {
|
||||
super();
|
||||
this.container = layoutService.mainContainer;
|
||||
this.settings = new ThemeConfiguration(configurationService, hostColorService);
|
||||
const isNewUser = this.storageService.isNew(StorageScope.APPLICATION);
|
||||
this.settings = new ThemeConfiguration(configurationService, hostColorService, isNewUser);
|
||||
|
||||
this.colorThemeRegistry = this._register(new ThemeRegistry(colorThemesExtPoint, ColorThemeData.fromExtensionTheme));
|
||||
this.colorThemeWatcher = this._register(new ThemeFileWatcher(fileService, environmentService, this.reloadCurrentColorTheme.bind(this)));
|
||||
@@ -245,6 +246,7 @@ export class WorkbenchThemeService extends Disposable implements IWorkbenchTheme
|
||||
|
||||
|
||||
this.migrateColorThemeSettings();
|
||||
await this.migrateAutoDetectColorScheme();
|
||||
const result = await Promise.all([initializeColorTheme(), initializeFileIconTheme(), initializeProductIconTheme()]);
|
||||
this.showNewDefaultThemeNotification();
|
||||
return result;
|
||||
@@ -303,6 +305,29 @@ export class WorkbenchThemeService extends Disposable implements IWorkbenchTheme
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For new users who haven't explicitly configured `window.autoDetectColorScheme`,
|
||||
* persist `true` so that auto-detect becomes the default going forward.
|
||||
*/
|
||||
private async migrateAutoDetectColorScheme(): Promise<void> {
|
||||
if (!this.storageService.isNew(StorageScope.APPLICATION)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure that user data (including synced settings) has finished initializing
|
||||
// so we do not overwrite values that arrive via settings sync.
|
||||
await this.userDataInitializationService.whenInitializationFinished();
|
||||
|
||||
const inspection = this.configurationService.inspect<boolean>(ThemeSettings.DETECT_COLOR_SCHEME);
|
||||
|
||||
// Treat any of userValue, userLocalValue, or userRemoteValue as an explicit configuration.
|
||||
if (inspection.userValue === undefined
|
||||
&& inspection.userLocalValue === undefined
|
||||
&& inspection.userRemoteValue === undefined) {
|
||||
await this.configurationService.updateValue(ThemeSettings.DETECT_COLOR_SCHEME, true, ConfigurationTarget.USER);
|
||||
}
|
||||
}
|
||||
|
||||
private installConfigurationListener() {
|
||||
this._register(this.configurationService.onDidChangeConfiguration(e => {
|
||||
if (e.affectsConfiguration(ThemeSettings.COLOR_THEME)
|
||||
|
||||
@@ -282,7 +282,19 @@ const colorSchemeToPreferred = {
|
||||
};
|
||||
|
||||
export class ThemeConfiguration {
|
||||
constructor(private configurationService: IConfigurationService, private hostColorService: IHostColorSchemeService) {
|
||||
constructor(private configurationService: IConfigurationService, private hostColorService: IHostColorSchemeService, private readonly isNewUser: boolean = false) {
|
||||
}
|
||||
|
||||
private shouldAutoDetectColorScheme(): boolean {
|
||||
const { value, userValue, userLocalValue, userRemoteValue } = this.configurationService.inspect<boolean>(ThemeSettings.DETECT_COLOR_SCHEME);
|
||||
if (value) {
|
||||
return true;
|
||||
}
|
||||
if (this.isNewUser) {
|
||||
const hasUserScopedValue = userValue !== undefined || userLocalValue !== undefined || userRemoteValue !== undefined;
|
||||
return !hasUserScopedValue;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public get colorTheme(): string {
|
||||
@@ -336,14 +348,14 @@ export class ThemeConfiguration {
|
||||
if (this.configurationService.getValue(ThemeSettings.DETECT_HC) && this.hostColorService.highContrast) {
|
||||
return this.hostColorService.dark ? ColorScheme.HIGH_CONTRAST_DARK : ColorScheme.HIGH_CONTRAST_LIGHT;
|
||||
}
|
||||
if (this.configurationService.getValue(ThemeSettings.DETECT_COLOR_SCHEME)) {
|
||||
if (this.shouldAutoDetectColorScheme()) {
|
||||
return this.hostColorService.dark ? ColorScheme.DARK : ColorScheme.LIGHT;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public isDetectingColorScheme(): boolean {
|
||||
return this.configurationService.getValue(ThemeSettings.DETECT_COLOR_SCHEME);
|
||||
return this.shouldAutoDetectColorScheme();
|
||||
}
|
||||
|
||||
public getColorThemeSettingId(): ThemeSettings {
|
||||
|
||||
@@ -6,6 +6,12 @@
|
||||
import assert from 'assert';
|
||||
import { migrateThemeSettingsId } from '../../common/workbenchThemeService.js';
|
||||
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
|
||||
import { ThemeConfiguration } from '../../common/themeConfiguration.js';
|
||||
import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js';
|
||||
import { IHostColorSchemeService } from '../../common/hostColorSchemeService.js';
|
||||
import { ColorScheme } from '../../../../../platform/theme/common/theme.js';
|
||||
import { Event } from '../../../../../base/common/event.js';
|
||||
import { IConfigurationOverrides, IConfigurationValue } from '../../../../../platform/configuration/common/configuration.js';
|
||||
|
||||
suite('WorkbenchThemeService', () => {
|
||||
|
||||
@@ -34,4 +40,124 @@ suite('WorkbenchThemeService', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
suite('ThemeConfiguration', () => {
|
||||
|
||||
function createHostColorSchemeService(dark: boolean, highContrast: boolean = false): IHostColorSchemeService {
|
||||
return {
|
||||
_serviceBrand: undefined,
|
||||
dark,
|
||||
highContrast,
|
||||
onDidChangeColorScheme: Event.None,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a config service that separates the resolved value from the user value,
|
||||
* matching production behaviour where getValue() returns the schema default
|
||||
* while inspect().userValue is undefined when no explicit user value is set.
|
||||
*/
|
||||
function createConfigServiceWithDefaults(
|
||||
userConfig: Record<string, unknown>,
|
||||
defaults: Record<string, unknown>
|
||||
): TestConfigurationService {
|
||||
const configService = new TestConfigurationService(userConfig);
|
||||
const originalInspect = configService.inspect.bind(configService);
|
||||
configService.inspect = <T>(key: string, overrides?: IConfigurationOverrides): IConfigurationValue<T> => {
|
||||
if (Object.prototype.hasOwnProperty.call(userConfig, key)) {
|
||||
return originalInspect(key, overrides);
|
||||
}
|
||||
// No explicit user value: return the default as the resolved value
|
||||
const defaultVal = defaults[key] as T;
|
||||
return {
|
||||
value: defaultVal,
|
||||
defaultValue: defaultVal,
|
||||
userValue: undefined,
|
||||
userLocalValue: undefined,
|
||||
};
|
||||
};
|
||||
const originalGetValue = configService.getValue.bind(configService);
|
||||
configService.getValue = <T>(arg1?: string | IConfigurationOverrides, arg2?: IConfigurationOverrides): T => {
|
||||
const result = originalGetValue(arg1, arg2);
|
||||
if (result === undefined && typeof arg1 === 'string' && Object.prototype.hasOwnProperty.call(defaults, arg1)) {
|
||||
return defaults[arg1] as T;
|
||||
}
|
||||
return result as T;
|
||||
};
|
||||
return configService;
|
||||
}
|
||||
|
||||
test('new user with no explicit setting gets auto-detect on light OS', () => {
|
||||
const configService = new TestConfigurationService();
|
||||
const hostColor = createHostColorSchemeService(false);
|
||||
const config = new ThemeConfiguration(configService, hostColor, true);
|
||||
|
||||
assert.deepStrictEqual(config.getPreferredColorScheme(), ColorScheme.LIGHT);
|
||||
assert.deepStrictEqual(config.isDetectingColorScheme(), true);
|
||||
});
|
||||
|
||||
test('new user with no explicit setting gets auto-detect on dark OS', () => {
|
||||
const configService = new TestConfigurationService();
|
||||
const hostColor = createHostColorSchemeService(true);
|
||||
const config = new ThemeConfiguration(configService, hostColor, true);
|
||||
|
||||
assert.deepStrictEqual(config.getPreferredColorScheme(), ColorScheme.DARK);
|
||||
assert.deepStrictEqual(config.isDetectingColorScheme(), true);
|
||||
});
|
||||
|
||||
test('new user with no explicit setting and schema default false still gets auto-detect', () => {
|
||||
// Simulates production: getValue() returns false (schema default) but userValue is undefined
|
||||
const configService = createConfigServiceWithDefaults({}, { 'window.autoDetectColorScheme': false });
|
||||
const hostColor = createHostColorSchemeService(false);
|
||||
const config = new ThemeConfiguration(configService, hostColor, true);
|
||||
|
||||
assert.deepStrictEqual(config.getPreferredColorScheme(), ColorScheme.LIGHT);
|
||||
assert.deepStrictEqual(config.isDetectingColorScheme(), true);
|
||||
});
|
||||
|
||||
test('new user with explicit false does not get auto-detect', () => {
|
||||
const configService = new TestConfigurationService({ 'window.autoDetectColorScheme': false });
|
||||
const hostColor = createHostColorSchemeService(false);
|
||||
const config = new ThemeConfiguration(configService, hostColor, true);
|
||||
|
||||
assert.deepStrictEqual(config.getPreferredColorScheme(), undefined);
|
||||
assert.deepStrictEqual(config.isDetectingColorScheme(), false);
|
||||
});
|
||||
|
||||
test('existing user without explicit setting does not get auto-detect', () => {
|
||||
const configService = new TestConfigurationService();
|
||||
const hostColor = createHostColorSchemeService(false);
|
||||
const config = new ThemeConfiguration(configService, hostColor, false);
|
||||
|
||||
assert.deepStrictEqual(config.getPreferredColorScheme(), undefined);
|
||||
assert.deepStrictEqual(config.isDetectingColorScheme(), false);
|
||||
});
|
||||
|
||||
test('existing user with explicit true gets auto-detect', () => {
|
||||
const configService = new TestConfigurationService({ 'window.autoDetectColorScheme': true });
|
||||
const hostColor = createHostColorSchemeService(false);
|
||||
const config = new ThemeConfiguration(configService, hostColor, false);
|
||||
|
||||
assert.deepStrictEqual(config.getPreferredColorScheme(), ColorScheme.LIGHT);
|
||||
assert.deepStrictEqual(config.isDetectingColorScheme(), true);
|
||||
});
|
||||
|
||||
test('high contrast OS takes priority over auto-detect for new user', () => {
|
||||
const configService = new TestConfigurationService({ 'window.autoDetectHighContrast': true });
|
||||
const hostColor = createHostColorSchemeService(true, true);
|
||||
const config = new ThemeConfiguration(configService, hostColor, true);
|
||||
|
||||
assert.deepStrictEqual(config.getPreferredColorScheme(), ColorScheme.HIGH_CONTRAST_DARK);
|
||||
assert.deepStrictEqual(config.isDetectingColorScheme(), true);
|
||||
});
|
||||
|
||||
test('high contrast light OS takes priority over auto-detect for new user', () => {
|
||||
const configService = new TestConfigurationService({ 'window.autoDetectHighContrast': true });
|
||||
const hostColor = createHostColorSchemeService(false, true);
|
||||
const config = new ThemeConfiguration(configService, hostColor, true);
|
||||
|
||||
assert.deepStrictEqual(config.getPreferredColorScheme(), ColorScheme.HIGH_CONTRAST_LIGHT);
|
||||
assert.deepStrictEqual(config.isDetectingColorScheme(), true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user