mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-11-09 10:28:32 +00:00
feat: removed the non official themes from the UI
Closes #1283 Ref eclipse-theia/theia#11151 Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
This commit is contained in:
@@ -1,15 +1,19 @@
|
||||
import type { Theme } from '@theia/core/lib/common/theme';
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { ThemeServiceWithDB as TheiaThemeServiceWithDB } from '@theia/monaco/lib/browser/monaco-indexed-db';
|
||||
import {
|
||||
BuiltinThemeProvider,
|
||||
ThemeService,
|
||||
} from '@theia/core/lib/browser/theming';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import type { Theme, ThemeType } from '@theia/core/lib/common/theme';
|
||||
import { assertUnreachable } from '../../../common/utils';
|
||||
|
||||
export namespace ArduinoThemes {
|
||||
export const Light: Theme = {
|
||||
export const light: Theme = {
|
||||
id: 'arduino-theme',
|
||||
type: 'light',
|
||||
label: 'Light (Arduino)',
|
||||
editorTheme: 'arduino-theme',
|
||||
};
|
||||
export const Dark: Theme = {
|
||||
export const dark: Theme = {
|
||||
id: 'arduino-theme-dark',
|
||||
type: 'dark',
|
||||
label: 'Dark (Arduino)',
|
||||
@@ -17,10 +21,166 @@ export namespace ArduinoThemes {
|
||||
};
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class ThemeServiceWithDB extends TheiaThemeServiceWithDB {
|
||||
protected override init(): void {
|
||||
this.register(ArduinoThemes.Light, ArduinoThemes.Dark);
|
||||
super.init();
|
||||
const builtInThemeIds = new Set(
|
||||
[
|
||||
ArduinoThemes.light,
|
||||
ArduinoThemes.dark,
|
||||
BuiltinThemeProvider.hcTheme,
|
||||
// TODO: add the HC light theme after Theia 1.36
|
||||
].map(({ id }) => id)
|
||||
);
|
||||
const deprecatedThemeIds = new Set(
|
||||
[BuiltinThemeProvider.lightTheme, BuiltinThemeProvider.darkTheme].map(
|
||||
({ id }) => id
|
||||
)
|
||||
);
|
||||
|
||||
export const lightThemeLabel = nls.localize('arduino/theme/light', 'Light');
|
||||
export const darkThemeLabel = nls.localize('arduino/theme/dark', 'Dark');
|
||||
export const hcThemeLabel = nls.localize('arduino/theme/hc', 'High Contrast');
|
||||
export function userThemeLabel(theme: Theme): string {
|
||||
return nls.localize('arduino/theme/user', '{0} (user)', theme.label);
|
||||
}
|
||||
export function deprecatedThemeLabel(theme: Theme): string {
|
||||
return nls.localize(
|
||||
'arduino/theme/deprecated',
|
||||
'{0} (deprecated)',
|
||||
theme.label
|
||||
);
|
||||
}
|
||||
|
||||
export function themeLabelForSettings(theme: Theme): string {
|
||||
switch (theme.id) {
|
||||
case ArduinoThemes.light.id:
|
||||
return lightThemeLabel;
|
||||
case ArduinoThemes.dark.id:
|
||||
return darkThemeLabel;
|
||||
case BuiltinThemeProvider.hcTheme.id:
|
||||
return hcThemeLabel;
|
||||
case BuiltinThemeProvider.lightTheme.id: // fall-through
|
||||
case BuiltinThemeProvider.darkTheme.id:
|
||||
return deprecatedThemeLabel(theme);
|
||||
default:
|
||||
return userThemeLabel(theme);
|
||||
}
|
||||
}
|
||||
|
||||
export function compatibleBuiltInTheme(theme: Theme): Theme {
|
||||
switch (theme.type) {
|
||||
case 'light':
|
||||
return ArduinoThemes.light;
|
||||
case 'dark':
|
||||
return ArduinoThemes.dark;
|
||||
case 'hc':
|
||||
return BuiltinThemeProvider.hcTheme;
|
||||
default: {
|
||||
console.warn(
|
||||
`Unhandled theme type: ${theme.type}. Theme ID: ${theme.id}, label: ${theme.label}`
|
||||
);
|
||||
return ArduinoThemes.light;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For tests without DI
|
||||
interface ThemeProvider {
|
||||
themes(): Theme[];
|
||||
currentTheme(): Theme;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns with a list of built-in themes officially supported by IDE2 (https://github.com/arduino/arduino-ide/issues/1283).
|
||||
* The themes in the array follow the following order:
|
||||
* - built-in themes first (in `Light`, `Dark`, `High Contrast`), // TODO -> High Contrast will be split up to HC Dark and HC Light after the Theia version uplift
|
||||
* - followed by user installed (VSIX) themes grouped by theme type, then alphabetical order,
|
||||
* - if the `currentTheme` is either Light (Theia) or Dark (Theia), the last item of the array will be the selected theme with `(deprecated)` suffix.
|
||||
*/
|
||||
export function userConfigurableThemes(service: ThemeService): Theme[][];
|
||||
export function userConfigurableThemes(provider: ThemeProvider): Theme[][];
|
||||
export function userConfigurableThemes(
|
||||
serviceOrProvider: ThemeService | ThemeProvider
|
||||
): Theme[][] {
|
||||
const provider =
|
||||
serviceOrProvider instanceof ThemeService
|
||||
? {
|
||||
currentTheme: () => serviceOrProvider.getCurrentTheme(),
|
||||
themes: () => serviceOrProvider.getThemes(),
|
||||
}
|
||||
: serviceOrProvider;
|
||||
const currentTheme = provider.currentTheme();
|
||||
const allThemes = provider
|
||||
.themes()
|
||||
.map((theme) => ({ ...theme, arduinoThemeType: arduinoThemeTypeOf(theme) }))
|
||||
.filter(
|
||||
(theme) =>
|
||||
theme.arduinoThemeType !== 'deprecated' || currentTheme.id === theme.id
|
||||
)
|
||||
.sort((left, right) => {
|
||||
const leftArduinoThemeType = left.arduinoThemeType;
|
||||
const rightArduinoThemeType = right.arduinoThemeType;
|
||||
if (leftArduinoThemeType === rightArduinoThemeType) {
|
||||
const result = themeTypeOrder[left.type] - themeTypeOrder[right.type];
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
return left.label.localeCompare(right.label); // alphabetical order
|
||||
}
|
||||
return (
|
||||
arduinoThemeTypeOrder[leftArduinoThemeType] -
|
||||
arduinoThemeTypeOrder[rightArduinoThemeType]
|
||||
);
|
||||
});
|
||||
const builtInThemes: Theme[] = [];
|
||||
const userThemes: Theme[] = [];
|
||||
const deprecatedThemes: Theme[] = [];
|
||||
allThemes.forEach((theme) => {
|
||||
const { arduinoThemeType } = theme;
|
||||
switch (arduinoThemeType) {
|
||||
case 'built-in':
|
||||
builtInThemes.push(theme);
|
||||
break;
|
||||
case 'user':
|
||||
userThemes.push(theme);
|
||||
break;
|
||||
case 'deprecated':
|
||||
deprecatedThemes.push(theme);
|
||||
break;
|
||||
default:
|
||||
assertUnreachable(arduinoThemeType);
|
||||
}
|
||||
});
|
||||
const groupedThemes: Theme[][] = [];
|
||||
if (builtInThemes.length) {
|
||||
groupedThemes.push(builtInThemes);
|
||||
}
|
||||
if (userThemes.length) {
|
||||
groupedThemes.push(userThemes);
|
||||
}
|
||||
if (deprecatedThemes.length) {
|
||||
groupedThemes.push(deprecatedThemes);
|
||||
}
|
||||
return groupedThemes;
|
||||
}
|
||||
|
||||
export type ArduinoThemeType = 'built-in' | 'user' | 'deprecated';
|
||||
const arduinoThemeTypeOrder: Record<ArduinoThemeType, number> = {
|
||||
'built-in': 0,
|
||||
user: 1,
|
||||
deprecated: 2,
|
||||
};
|
||||
const themeTypeOrder: Record<ThemeType, number> = {
|
||||
light: 0,
|
||||
dark: 1,
|
||||
hc: 2,
|
||||
};
|
||||
|
||||
export function arduinoThemeTypeOf(theme: Theme | string): ArduinoThemeType {
|
||||
const themeId = typeof theme === 'string' ? theme : theme.id;
|
||||
if (builtInThemeIds.has(themeId)) {
|
||||
return 'built-in';
|
||||
}
|
||||
if (deprecatedThemeIds.has(themeId)) {
|
||||
return 'deprecated';
|
||||
}
|
||||
return 'user';
|
||||
}
|
||||
|
||||
@@ -1,23 +1,231 @@
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { MonacoThemingService as TheiaMonacoThemingService } from '@theia/monaco/lib/browser/monaco-theming-service';
|
||||
import { ArduinoThemes } from '../core/theming';
|
||||
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
|
||||
import { ThemeService } from '@theia/core/lib/browser/theming';
|
||||
import {
|
||||
Disposable,
|
||||
DisposableCollection,
|
||||
} from '@theia/core/lib/common/disposable';
|
||||
import { MessageService } from '@theia/core/lib/common/message-service';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { deepClone } from '@theia/core/lib/common/objects';
|
||||
import { wait } from '@theia/core/lib/common/promise-util';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import {
|
||||
MonacoThemeState,
|
||||
deleteTheme as deleteThemeFromIndexedDB,
|
||||
getThemes as getThemesFromIndexedDB,
|
||||
} from '@theia/monaco/lib/browser/monaco-indexed-db';
|
||||
import {
|
||||
MonacoTheme,
|
||||
MonacoThemingService as TheiaMonacoThemingService,
|
||||
} from '@theia/monaco/lib/browser/monaco-theming-service';
|
||||
import { MonacoThemeRegistry as TheiaMonacoThemeRegistry } from '@theia/monaco/lib/browser/textmate/monaco-theme-registry';
|
||||
import type { ThemeMix } from '@theia/monaco/lib/browser/textmate/monaco-theme-types';
|
||||
import { HostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin';
|
||||
import { ArduinoThemes, compatibleBuiltInTheme } from '../core/theming';
|
||||
import { WindowServiceExt } from '../core/window-service-ext';
|
||||
|
||||
type MonacoThemeRegistrationSource =
|
||||
/**
|
||||
* When reading JS/TS contributed theme from a JSON file. Such as the Arduino themes and the ones contributed by Theia.
|
||||
*/
|
||||
| 'compiled'
|
||||
/**
|
||||
* When reading and registering previous monaco themes from the `indexedDB`.
|
||||
*/
|
||||
| 'indexedDB'
|
||||
/**
|
||||
* Contributed by VS Code extensions when starting the app and loading the plugins.
|
||||
*/
|
||||
| 'vsix';
|
||||
|
||||
@injectable()
|
||||
export class ThemesRegistrationSummary {
|
||||
private readonly _summary: Record<MonacoThemeRegistrationSource, string[]> = {
|
||||
compiled: [],
|
||||
indexedDB: [],
|
||||
vsix: [],
|
||||
};
|
||||
|
||||
add(source: MonacoThemeRegistrationSource, themeId: string): void {
|
||||
const themeIds = this._summary[source];
|
||||
if (!themeIds.includes(themeId)) {
|
||||
themeIds.push(themeId);
|
||||
}
|
||||
}
|
||||
|
||||
get summary(): Record<MonacoThemeRegistrationSource, string[]> {
|
||||
return deepClone(this._summary);
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class MonacoThemeRegistry extends TheiaMonacoThemeRegistry {
|
||||
@inject(ThemesRegistrationSummary)
|
||||
private readonly summary: ThemesRegistrationSummary;
|
||||
|
||||
private initializing = false;
|
||||
|
||||
override initializeDefaultThemes(): void {
|
||||
this.initializing = true;
|
||||
try {
|
||||
super.initializeDefaultThemes();
|
||||
} finally {
|
||||
this.initializing = false;
|
||||
}
|
||||
}
|
||||
|
||||
override setTheme(name: string, data: ThemeMix): void {
|
||||
super.setTheme(name, data);
|
||||
if (this.initializing) {
|
||||
this.summary.add('compiled', name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class MonacoThemingService extends TheiaMonacoThemingService {
|
||||
override initialize(): void {
|
||||
super.initialize();
|
||||
const { Light, Dark } = ArduinoThemes;
|
||||
@inject(ThemesRegistrationSummary)
|
||||
private readonly summary: ThemesRegistrationSummary;
|
||||
|
||||
private themeRegistrationSource: MonacoThemeRegistrationSource | undefined;
|
||||
|
||||
protected override async restore(): Promise<void> {
|
||||
// The custom theme registration must happen before restoring the themes.
|
||||
// Otherwise, theme changes are not picked up.
|
||||
// https://github.com/arduino/arduino-ide/issues/1251#issuecomment-1436737702
|
||||
this.registerArduinoThemes();
|
||||
this.themeRegistrationSource = 'indexedDB';
|
||||
try {
|
||||
await super.restore();
|
||||
} finally {
|
||||
this.themeRegistrationSource = 'indexedDB';
|
||||
}
|
||||
}
|
||||
|
||||
private registerArduinoThemes(): void {
|
||||
const { light, dark } = ArduinoThemes;
|
||||
this.registerParsedTheme({
|
||||
id: Light.id,
|
||||
label: Light.label,
|
||||
id: light.id,
|
||||
label: light.label,
|
||||
uiTheme: 'vs',
|
||||
json: require('../../../../src/browser/data/default.color-theme.json'),
|
||||
});
|
||||
this.registerParsedTheme({
|
||||
id: Dark.id,
|
||||
label: Dark.label,
|
||||
id: dark.id,
|
||||
label: dark.label,
|
||||
uiTheme: 'vs-dark',
|
||||
json: require('../../../../src/browser/data/dark.color-theme.json'),
|
||||
});
|
||||
}
|
||||
|
||||
protected override doRegisterParsedTheme(
|
||||
state: MonacoThemeState
|
||||
): Disposable {
|
||||
const themeId = state.id;
|
||||
const source = this.themeRegistrationSource ?? 'compiled';
|
||||
const disposable = super.doRegisterParsedTheme(state);
|
||||
this.summary.add(source, themeId);
|
||||
return disposable;
|
||||
}
|
||||
|
||||
protected override async doRegister(
|
||||
theme: MonacoTheme,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
pending: { [uri: string]: Promise<any> },
|
||||
toDispose: DisposableCollection
|
||||
): Promise<void> {
|
||||
try {
|
||||
this.themeRegistrationSource = 'vsix';
|
||||
await super.doRegister(theme, pending, toDispose);
|
||||
} finally {
|
||||
this.themeRegistrationSource = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Workaround for removing VSIX themes from the indexedDB if they were not loaded during the app startup.
|
||||
*/
|
||||
@injectable()
|
||||
export class CleanupObsoleteThemes implements FrontendApplicationContribution {
|
||||
@inject(HostedPluginSupport)
|
||||
private readonly hostedPlugin: HostedPluginSupport;
|
||||
@inject(ThemesRegistrationSummary)
|
||||
private readonly summary: ThemesRegistrationSummary;
|
||||
@inject(ThemeService)
|
||||
private readonly themeService: ThemeService;
|
||||
@inject(MessageService)
|
||||
private readonly messageService: MessageService;
|
||||
@inject(WindowServiceExt)
|
||||
private readonly windowService: WindowServiceExt;
|
||||
|
||||
onStart(): void {
|
||||
this.hostedPlugin.didStart.then(() => this.cleanupObsoleteThemes());
|
||||
}
|
||||
|
||||
private async cleanupObsoleteThemes(): Promise<void> {
|
||||
const persistedThemes = await getThemesFromIndexedDB();
|
||||
const obsoleteThemeIds = collectObsoleteThemeIds(
|
||||
persistedThemes,
|
||||
this.summary.summary
|
||||
);
|
||||
if (!obsoleteThemeIds.length) {
|
||||
return;
|
||||
}
|
||||
const firstWindow = await this.windowService.isFirstWindow();
|
||||
if (firstWindow) {
|
||||
await this.removeObsoleteThemesFromIndexedDB(obsoleteThemeIds);
|
||||
this.unregisterObsoleteThemes(obsoleteThemeIds);
|
||||
}
|
||||
}
|
||||
|
||||
private removeObsoleteThemesFromIndexedDB(themeIds: string[]): Promise<void> {
|
||||
return themeIds.reduce(async (previousTask, themeId) => {
|
||||
await previousTask;
|
||||
return deleteThemeFromIndexedDB(themeId);
|
||||
}, Promise.resolve());
|
||||
}
|
||||
|
||||
private unregisterObsoleteThemes(themeIds: string[]): void {
|
||||
const currentTheme = this.themeService.getCurrentTheme();
|
||||
const switchToCompatibleTheme = themeIds.includes(currentTheme.id);
|
||||
for (const themeId of themeIds) {
|
||||
delete this.themeService['themes'][themeId];
|
||||
}
|
||||
this.themeService['doUpdateColorThemePreference']();
|
||||
if (switchToCompatibleTheme) {
|
||||
this.themeService.setCurrentTheme(
|
||||
compatibleBuiltInTheme(currentTheme).id,
|
||||
true
|
||||
);
|
||||
wait(250).then(() =>
|
||||
requestAnimationFrame(() =>
|
||||
this.messageService.info(
|
||||
nls.localize(
|
||||
'arduino/theme/currentThemeNotFound',
|
||||
'Could not find the currently selected theme: {0}. Arduino IDE has picked a built-in theme compatible with the missing one.',
|
||||
currentTheme.label
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An indexedDB registered theme is obsolete if it is in the indexedDB but was registered
|
||||
* from neither a `vsix` nor `compiled` source during the app startup.
|
||||
*/
|
||||
export function collectObsoleteThemeIds(
|
||||
indexedDBThemes: MonacoThemeState[],
|
||||
summary: Record<MonacoThemeRegistrationSource, string[]>
|
||||
): string[] {
|
||||
const vsixThemeIds = summary['vsix'];
|
||||
const compiledThemeIds = summary['compiled'];
|
||||
return indexedDBThemes
|
||||
.map(({ id }) => id)
|
||||
.filter(
|
||||
(id) => !vsixThemeIds.includes(id) && !compiledThemeIds.includes(id)
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user