Skip to content
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

[theme] avoid flashing when starting up with a new OS color scheme #235913

Merged
merged 2 commits into from
Dec 12, 2024
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
8 changes: 8 additions & 0 deletions src/vs/platform/theme/common/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ export enum ColorScheme {
HIGH_CONTRAST_LIGHT = 'hcLight'
}

export enum ThemeTypeSelector {
VS = 'vs',
VS_DARK = 'vs-dark',
HC_BLACK = 'hc-black',
HC_LIGHT = 'hc-light'
}


export function isHighContrast(scheme: ColorScheme): boolean {
return scheme === ColorScheme.HIGH_CONTRAST_DARK || scheme === ColorScheme.HIGH_CONTRAST_LIGHT;
}
Expand Down
14 changes: 7 additions & 7 deletions src/vs/platform/theme/common/themeService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { createDecorator } from '../../instantiation/common/instantiation.js';
import * as platform from '../../registry/common/platform.js';
import { ColorIdentifier } from './colorRegistry.js';
import { IconContribution, IconDefinition } from './iconRegistry.js';
import { ColorScheme } from './theme.js';
import { ColorScheme, ThemeTypeSelector } from './theme.js';

export const IThemeService = createDecorator<IThemeService>('themeService');

Expand All @@ -23,12 +23,12 @@ export function themeColorFromId(id: ColorIdentifier) {
export const FileThemeIcon = Codicon.file;
export const FolderThemeIcon = Codicon.folder;

export function getThemeTypeSelector(type: ColorScheme): string {
export function getThemeTypeSelector(type: ColorScheme): ThemeTypeSelector {
switch (type) {
case ColorScheme.DARK: return 'vs-dark';
case ColorScheme.HIGH_CONTRAST_DARK: return 'hc-black';
case ColorScheme.HIGH_CONTRAST_LIGHT: return 'hc-light';
default: return 'vs';
case ColorScheme.DARK: return ThemeTypeSelector.VS_DARK;
case ColorScheme.HIGH_CONTRAST_DARK: return ThemeTypeSelector.HC_BLACK;
case ColorScheme.HIGH_CONTRAST_LIGHT: return ThemeTypeSelector.HC_LIGHT;
default: return ThemeTypeSelector.VS;
}
}

Expand Down Expand Up @@ -208,7 +208,7 @@ export class Themable extends Disposable {

export interface IPartsSplash {
zoomLevel: number | undefined;
baseTheme: string;
baseTheme: ThemeTypeSelector;
colorInfo: {
background: string;
foreground: string | undefined;
Expand Down
58 changes: 36 additions & 22 deletions src/vs/platform/theme/electron-main/themeMainService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { createDecorator } from '../../instantiation/common/instantiation.js';
import { IStateService } from '../../state/node/state.js';
import { IPartsSplash } from '../common/themeService.js';
import { IColorScheme } from '../../window/common/window.js';
import { ThemeTypeSelector } from '../common/theme.js';

// These default colors match our default themes
// editor background color ("Dark Modern", etc...)
Expand All @@ -26,6 +27,7 @@ const THEME_WINDOW_SPLASH = 'windowSplash';

namespace ThemeSettings {
export const DETECT_COLOR_SCHEME = 'window.autoDetectColorScheme';
export const DETECT_HC = 'window.autoDetectHighContrast';
export const SYSTEM_COLOR_THEME = 'window.systemColorTheme';
}

Expand Down Expand Up @@ -82,9 +84,9 @@ export class ThemeMainService extends Disposable implements IThemeMainService {
electron.nativeTheme.themeSource = 'light';
break;
case 'auto':
switch (this.getBaseTheme()) {
case 'vs': electron.nativeTheme.themeSource = 'light'; break;
case 'vs-dark': electron.nativeTheme.themeSource = 'dark'; break;
switch (this.getPreferredBaseTheme() ?? this.getStoredBaseTheme()) {
case ThemeTypeSelector.VS: electron.nativeTheme.themeSource = 'light'; break;
case ThemeTypeSelector.VS_DARK: electron.nativeTheme.themeSource = 'dark'; break;
default: electron.nativeTheme.themeSource = 'system';
}
break;
Expand All @@ -98,7 +100,7 @@ export class ThemeMainService extends Disposable implements IThemeMainService {

getColorScheme(): IColorScheme {
if (isWindows) {
// high contrast is refelected by the shouldUseInvertedColorScheme property
// high contrast is reflected by the shouldUseInvertedColorScheme property
if (electron.nativeTheme.shouldUseHighContrastColors) {
// shouldUseInvertedColorScheme is dark, !shouldUseInvertedColorScheme is light
return { dark: electron.nativeTheme.shouldUseInvertedColorScheme, highContrast: true };
Expand All @@ -120,32 +122,44 @@ export class ThemeMainService extends Disposable implements IThemeMainService {
};
}

getBackgroundColor(): string {
getPreferredBaseTheme(): ThemeTypeSelector | undefined {
const colorScheme = this.getColorScheme();
if (colorScheme.highContrast && this.configurationService.getValue('window.autoDetectHighContrast')) {
return colorScheme.dark ? DEFAULT_BG_HC_BLACK : DEFAULT_BG_HC_LIGHT;
if (this.configurationService.getValue(ThemeSettings.DETECT_HC) && colorScheme.highContrast) {
return colorScheme.dark ? ThemeTypeSelector.HC_BLACK : ThemeTypeSelector.HC_LIGHT;
}
if (this.configurationService.getValue(ThemeSettings.DETECT_COLOR_SCHEME)) {
return colorScheme.dark ? ThemeTypeSelector.VS_DARK : ThemeTypeSelector.VS;
}
return undefined;
}

let background = this.stateService.getItem<string | null>(THEME_BG_STORAGE_KEY, null);
if (!background) {
switch (this.getBaseTheme()) {
case 'vs': background = DEFAULT_BG_LIGHT; break;
case 'hc-black': background = DEFAULT_BG_HC_BLACK; break;
case 'hc-light': background = DEFAULT_BG_HC_LIGHT; break;
default: background = DEFAULT_BG_DARK;
getBackgroundColor(): string {
const preferred = this.getPreferredBaseTheme();
const stored = this.getStoredBaseTheme();

// If the stored theme has the same base as the preferred, we can return the stored background
if (preferred === undefined || preferred === stored) {
const storedBackground = this.stateService.getItem<string | null>(THEME_BG_STORAGE_KEY, null);
if (storedBackground) {
return storedBackground;
}
}

return background;
// Otherwise we return the default background for the preferred base theme. If there's no preferred, use the stored one.
switch (preferred ?? stored) {
case ThemeTypeSelector.VS: return DEFAULT_BG_LIGHT;
case ThemeTypeSelector.HC_BLACK: return DEFAULT_BG_HC_BLACK;
case ThemeTypeSelector.HC_LIGHT: return DEFAULT_BG_HC_LIGHT;
default: return DEFAULT_BG_DARK;
}
}

private getBaseTheme(): 'vs' | 'vs-dark' | 'hc-black' | 'hc-light' {
const baseTheme = this.stateService.getItem<string>(THEME_STORAGE_KEY, 'vs-dark').split(' ')[0];
private getStoredBaseTheme(): ThemeTypeSelector {
const baseTheme = this.stateService.getItem<ThemeTypeSelector>(THEME_STORAGE_KEY, ThemeTypeSelector.VS_DARK).split(' ')[0];
switch (baseTheme) {
case 'vs': return 'vs';
case 'hc-black': return 'hc-black';
case 'hc-light': return 'hc-light';
default: return 'vs-dark';
case ThemeTypeSelector.VS: return ThemeTypeSelector.VS;
case ThemeTypeSelector.HC_BLACK: return ThemeTypeSelector.HC_BLACK;
case ThemeTypeSelector.HC_LIGHT: return ThemeTypeSelector.HC_LIGHT;
default: return ThemeTypeSelector.VS_DARK;
}
}

Expand Down
137 changes: 4 additions & 133 deletions src/vs/workbench/contrib/themes/browser/themes.contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { MenuRegistry, MenuId, Action2, registerAction2, ISubmenuItem } from '..
import { equalsIgnoreCase } from '../../../../base/common/strings.js';
import { Registry } from '../../../../platform/registry/common/platform.js';
import { Categories } from '../../../../platform/action/common/actionCommonCategories.js';
import { IWorkbenchThemeService, IWorkbenchTheme, ThemeSettingTarget, IWorkbenchColorTheme, IWorkbenchFileIconTheme, IWorkbenchProductIconTheme, ThemeSettings, ThemeSettingDefaults } from '../../../services/themes/common/workbenchThemeService.js';
import { IWorkbenchThemeService, IWorkbenchTheme, ThemeSettingTarget, IWorkbenchColorTheme, IWorkbenchFileIconTheme, IWorkbenchProductIconTheme, ThemeSettings } from '../../../services/themes/common/workbenchThemeService.js';
import { IExtensionsWorkbenchService } from '../../extensions/common/extensions.js';
import { IExtensionGalleryService, IExtensionManagementService, IGalleryExtension } from '../../../../platform/extensionManagement/common/extensionManagement.js';
import { IColorRegistry, Extensions as ColorRegistryExtensions } from '../../../../platform/theme/common/colorRegistry.js';
Expand All @@ -31,17 +31,12 @@ import { Emitter } from '../../../../base/common/event.js';
import { IExtensionResourceLoaderService } from '../../../../platform/extensionResourceLoader/common/extensionResourceLoader.js';
import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
import { CommandsRegistry, ICommandService } from '../../../../platform/commands/common/commands.js';
import { CommandsRegistry } from '../../../../platform/commands/common/commands.js';
import { FileIconThemeData } from '../../../services/themes/browser/fileIconThemeData.js';
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';
import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions } from '../../../common/contributions.js';
import { LifecyclePhase } from '../../../services/lifecycle/common/lifecycle.js';
import { INotificationService, IPromptChoice, Severity } from '../../../../platform/notification/common/notification.js';
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
import { isWeb } from '../../../../base/common/platform.js';
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
import { IHostService } from '../../../services/host/browser/host.js';
import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js';

import { mainWindow } from '../../../../base/browser/window.js';
import { IPreferencesService } from '../../../services/preferences/common/preferences.js';
import { Toggle } from '../../../../base/browser/ui/toggle/toggle.js';
Expand Down Expand Up @@ -843,127 +838,3 @@ MenuRegistry.appendMenuItem(ThemesSubMenu, {
},
order: 3
});

type DefaultThemeUpdatedNotificationReaction = 'keepNew' | 'keepOld' | 'tryNew' | 'cancel' | 'browse';

class DefaultThemeUpdatedNotificationContribution implements IWorkbenchContribution {

static STORAGE_KEY = 'themeUpdatedNotificationShown';

constructor(
@INotificationService private readonly _notificationService: INotificationService,
@IWorkbenchThemeService private readonly _workbenchThemeService: IWorkbenchThemeService,
@IStorageService private readonly _storageService: IStorageService,
@ICommandService private readonly _commandService: ICommandService,
@ITelemetryService private readonly _telemetryService: ITelemetryService,
@IHostService private readonly _hostService: IHostService,
) {
if (_storageService.getBoolean(DefaultThemeUpdatedNotificationContribution.STORAGE_KEY, StorageScope.APPLICATION)) {
return;
}
setTimeout(async () => {
if (_storageService.getBoolean(DefaultThemeUpdatedNotificationContribution.STORAGE_KEY, StorageScope.APPLICATION)) {
return;
}
if (await this._hostService.hadLastFocus()) {
this._storageService.store(DefaultThemeUpdatedNotificationContribution.STORAGE_KEY, true, StorageScope.APPLICATION, StorageTarget.USER);
if (this._workbenchThemeService.hasUpdatedDefaultThemes()) {
this._showYouGotMigratedNotification();
} else {
const currentTheme = this._workbenchThemeService.getColorTheme().settingsId;
if (currentTheme === ThemeSettingDefaults.COLOR_THEME_LIGHT_OLD || currentTheme === ThemeSettingDefaults.COLOR_THEME_DARK_OLD) {
this._tryNewThemeNotification();
}
}
}
}, 3000);
}

private async _showYouGotMigratedNotification(): Promise<void> {
const usingLight = this._workbenchThemeService.getColorTheme().type === ColorScheme.LIGHT;
const newThemeSettingsId = usingLight ? ThemeSettingDefaults.COLOR_THEME_LIGHT : ThemeSettingDefaults.COLOR_THEME_DARK;
const newTheme = (await this._workbenchThemeService.getColorThemes()).find(theme => theme.settingsId === newThemeSettingsId);
if (newTheme) {
const choices = [
{
label: localize('button.keep', "Keep New Theme"),
run: () => {
this._writeTelemetry('keepNew');
}
},
{
label: localize('button.browse', "Browse Themes"),
run: () => {
this._writeTelemetry('browse');
this._commandService.executeCommand(SelectColorThemeCommandId);
}
},
{
label: localize('button.revert', "Revert"),
run: async () => {
this._writeTelemetry('keepOld');
const oldSettingsId = usingLight ? ThemeSettingDefaults.COLOR_THEME_LIGHT_OLD : ThemeSettingDefaults.COLOR_THEME_DARK_OLD;
const oldTheme = (await this._workbenchThemeService.getColorThemes()).find(theme => theme.settingsId === oldSettingsId);
if (oldTheme) {
this._workbenchThemeService.setColorTheme(oldTheme, 'auto');
}
}
}
];
await this._notificationService.prompt(
Severity.Info,
localize({ key: 'themeUpdatedNotification', comment: ['{0} is the name of the new default theme'] }, "Visual Studio Code now ships with a new default theme '{0}'. If you prefer, you can switch back to the old theme or try one of the many other color themes available.", newTheme.label),
choices,
{
onCancel: () => this._writeTelemetry('cancel')
}
);
}
}

private async _tryNewThemeNotification(): Promise<void> {
const newThemeSettingsId = this._workbenchThemeService.getColorTheme().type === ColorScheme.LIGHT ? ThemeSettingDefaults.COLOR_THEME_LIGHT : ThemeSettingDefaults.COLOR_THEME_DARK;
const theme = (await this._workbenchThemeService.getColorThemes()).find(theme => theme.settingsId === newThemeSettingsId);
if (theme) {
const choices: IPromptChoice[] = [{
label: localize('button.tryTheme', "Try New Theme"),
run: () => {
this._writeTelemetry('tryNew');
this._workbenchThemeService.setColorTheme(theme, 'auto');
}
},
{
label: localize('button.cancel', "Cancel"),
run: () => {
this._writeTelemetry('cancel');
}
}];
await this._notificationService.prompt(
Severity.Info,
localize({ key: 'newThemeNotification', comment: ['{0} is the name of the new default theme'] }, "Visual Studio Code now ships with a new default theme '{0}'. Do you want to give it a try?", theme.label),
choices,
{ onCancel: () => this._writeTelemetry('cancel') }
);
}
}

private _writeTelemetry(outcome: DefaultThemeUpdatedNotificationReaction): void {
type ThemeUpdatedNoticationClassification = {
owner: 'aeschli';
comment: 'Reaction to the notification that theme has updated to a new default theme';
web: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Whether this is running on web' };
reaction: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Outcome of the notification' };
};
type ThemeUpdatedNoticationEvent = {
web: boolean;
reaction: DefaultThemeUpdatedNotificationReaction;
};

this._telemetryService.publicLog2<ThemeUpdatedNoticationEvent, ThemeUpdatedNoticationClassification>('themeUpdatedNotication', {
web: isWeb,
reaction: outcome
});
}
}
const workbenchRegistry = Registry.as<IWorkbenchContributionsRegistry>(Extensions.Workbench);
workbenchRegistry.registerWorkbenchContribution(DefaultThemeUpdatedNotificationContribution, LifecyclePhase.Eventually);
Loading
Loading