1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 | import { Emitter, Event } from '../common/event';
|
18 | import { Disposable } from '../common/disposable';
|
19 | import { FrontendApplicationConfigProvider } from './frontend-application-config-provider';
|
20 | import { ApplicationProps, DefaultTheme } from '@theia/application-package/lib/application-props';
|
21 | import { Theme, ThemeChangeEvent } from '../common/theme';
|
22 | import { inject, injectable, postConstruct } from 'inversify';
|
23 | import { Deferred } from '../common/promise-util';
|
24 | import { PreferenceSchemaProvider, PreferenceService } from './preferences';
|
25 | import debounce = require('lodash.debounce');
|
26 |
|
27 | const COLOR_THEME_PREFERENCE_KEY = 'workbench.colorTheme';
|
28 | const NO_THEME = { id: 'no-theme', label: 'Not a real theme.', type: 'dark' } as const;
|
29 |
|
30 | @injectable()
|
31 | export class ThemeService {
|
32 | static readonly STORAGE_KEY = 'theme';
|
33 |
|
34 | @inject(PreferenceService) protected readonly preferences: PreferenceService;
|
35 | @inject(PreferenceSchemaProvider) protected readonly schemaProvider: PreferenceSchemaProvider;
|
36 |
|
37 | protected themes: { [id: string]: Theme } = {};
|
38 | protected activeTheme: Theme = NO_THEME;
|
39 | protected readonly themeChange = new Emitter<ThemeChangeEvent>();
|
40 | protected readonly deferredInitializer = new Deferred();
|
41 | get initialized(): Promise<void> {
|
42 | return this.deferredInitializer.promise;
|
43 | }
|
44 |
|
45 | readonly onDidColorThemeChange: Event<ThemeChangeEvent> = this.themeChange.event;
|
46 |
|
47 | @postConstruct()
|
48 | protected init(): void {
|
49 | this.register(...BuiltinThemeProvider.themes);
|
50 | this.loadUserTheme();
|
51 | this.preferences.ready.then(() => {
|
52 | this.validateActiveTheme();
|
53 | this.updateColorThemePreference();
|
54 | this.preferences.onPreferencesChanged(changes => {
|
55 | if (COLOR_THEME_PREFERENCE_KEY in changes) {
|
56 | this.validateActiveTheme();
|
57 | }
|
58 | });
|
59 | });
|
60 | }
|
61 |
|
62 | register(...themes: Theme[]): Disposable {
|
63 | for (const theme of themes) {
|
64 | this.themes[theme.id] = theme;
|
65 | }
|
66 | this.validateActiveTheme();
|
67 | this.updateColorThemePreference();
|
68 | return Disposable.create(() => {
|
69 | for (const theme of themes) {
|
70 | delete this.themes[theme.id];
|
71 | if (this.activeTheme === theme) {
|
72 | this.setCurrentTheme(this.defaultTheme.id, false);
|
73 | }
|
74 | }
|
75 | this.updateColorThemePreference();
|
76 | });
|
77 | }
|
78 |
|
79 | protected validateActiveTheme(): void {
|
80 | if (this.preferences.isReady) {
|
81 | const configuredTheme = this.getConfiguredTheme();
|
82 | if (configuredTheme && configuredTheme !== this.activeTheme) {
|
83 | this.setCurrentTheme(configuredTheme.id, false);
|
84 | }
|
85 | }
|
86 | }
|
87 |
|
88 | protected updateColorThemePreference = debounce(() => this.doUpdateColorThemePreference(), 500);
|
89 |
|
90 | protected doUpdateColorThemePreference(): void {
|
91 | const preference = this.schemaProvider.getSchemaProperty(COLOR_THEME_PREFERENCE_KEY);
|
92 | if (preference) {
|
93 | const sortedThemes = this.getThemes().sort((a, b) => a.label.localeCompare(b.label));
|
94 | this.schemaProvider.updateSchemaProperty(COLOR_THEME_PREFERENCE_KEY, {
|
95 | ...preference,
|
96 | enum: sortedThemes.map(e => e.id),
|
97 | enumItemLabels: sortedThemes.map(e => e.label)
|
98 | });
|
99 | }
|
100 | }
|
101 |
|
102 | getThemes(): Theme[] {
|
103 | const result = [];
|
104 | for (const o in this.themes) {
|
105 | if (this.themes.hasOwnProperty(o)) {
|
106 | result.push(this.themes[o]);
|
107 | }
|
108 | }
|
109 | return result;
|
110 | }
|
111 |
|
112 | getTheme(themeId: string): Theme {
|
113 | return this.themes[themeId] || this.defaultTheme;
|
114 | }
|
115 |
|
116 | protected tryGetTheme(themeId: string): Theme | undefined {
|
117 | return this.themes[themeId];
|
118 | }
|
119 |
|
120 |
|
121 | loadUserTheme(): void {
|
122 | const storedThemeId = window.localStorage.getItem(ThemeService.STORAGE_KEY) ?? this.defaultTheme.id;
|
123 | const theme = this.getTheme(storedThemeId);
|
124 | this.setCurrentTheme(theme.id, false);
|
125 | this.deferredInitializer.resolve();
|
126 | }
|
127 |
|
128 | |
129 |
|
130 |
|
131 | setCurrentTheme(themeId: string, persist = true): void {
|
132 | const newTheme = this.tryGetTheme(themeId);
|
133 | const oldTheme = this.activeTheme;
|
134 | if (newTheme && newTheme !== oldTheme) {
|
135 | oldTheme?.deactivate?.();
|
136 | newTheme.activate?.();
|
137 | this.activeTheme = newTheme;
|
138 | this.themeChange.fire({ newTheme, oldTheme });
|
139 | }
|
140 | if (persist) {
|
141 | this.preferences.updateValue(COLOR_THEME_PREFERENCE_KEY, themeId);
|
142 | }
|
143 | }
|
144 |
|
145 | getCurrentTheme(): Theme {
|
146 | return this.activeTheme;
|
147 | }
|
148 |
|
149 | protected getConfiguredTheme(): Theme | undefined {
|
150 | const configuredId = this.preferences.get<string>(COLOR_THEME_PREFERENCE_KEY);
|
151 | return configuredId ? this.themes[configuredId.toString()] : undefined;
|
152 | }
|
153 |
|
154 | |
155 |
|
156 |
|
157 | get defaultTheme(): Theme {
|
158 | return this.tryGetTheme(DefaultTheme.defaultForOSTheme(FrontendApplicationConfigProvider.get().defaultTheme))
|
159 | ?? this.getTheme(DefaultTheme.defaultForOSTheme(ApplicationProps.DEFAULT.frontend.config.defaultTheme));
|
160 | }
|
161 |
|
162 | |
163 |
|
164 |
|
165 | reset(): void {
|
166 | this.setCurrentTheme(this.defaultTheme.id);
|
167 | }
|
168 | }
|
169 |
|
170 | export class BuiltinThemeProvider {
|
171 |
|
172 | static readonly darkTheme: Theme = {
|
173 | id: 'dark',
|
174 | type: 'dark',
|
175 | label: 'Dark (Theia)',
|
176 | editorTheme: 'dark-theia'
|
177 | };
|
178 |
|
179 | static readonly lightTheme: Theme = {
|
180 | id: 'light',
|
181 | type: 'light',
|
182 | label: 'Light (Theia)',
|
183 | editorTheme: 'light-theia'
|
184 | };
|
185 |
|
186 | static readonly hcTheme: Theme = {
|
187 | id: 'hc-theia',
|
188 | type: 'hc',
|
189 | label: 'High Contrast (Theia)',
|
190 | editorTheme: 'hc-theia'
|
191 | };
|
192 |
|
193 | static readonly hcLightTheme: Theme = {
|
194 | id: 'hc-theia-light',
|
195 | type: 'hcLight',
|
196 | label: 'High Contrast Light (Theia)',
|
197 | editorTheme: 'hc-theia-light'
|
198 | };
|
199 |
|
200 | static readonly themes = [
|
201 | BuiltinThemeProvider.darkTheme,
|
202 | BuiltinThemeProvider.lightTheme,
|
203 | BuiltinThemeProvider.hcTheme,
|
204 | BuiltinThemeProvider.hcLightTheme
|
205 | ];
|
206 | }
|