UNPKG

7.79 kBPlain TextView Raw
1// *****************************************************************************
2// Copyright (C) 2017 TypeFox and others.
3//
4// This program and the accompanying materials are made available under the
5// terms of the Eclipse Public License v. 2.0 which is available at
6// http://www.eclipse.org/legal/epl-2.0.
7//
8// This Source Code may also be made available under the following Secondary
9// Licenses when the conditions for such availability set forth in the Eclipse
10// Public License v. 2.0 are satisfied: GNU General Public License, version 2
11// with the GNU Classpath Exception which is available at
12// https://www.gnu.org/software/classpath/license.html.
13//
14// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15// *****************************************************************************
16
17import { Emitter, Event } from '../common/event';
18import { Disposable } from '../common/disposable';
19import { FrontendApplicationConfigProvider } from './frontend-application-config-provider';
20import { ApplicationProps, DefaultTheme } from '@theia/application-package/lib/application-props';
21import { Theme, ThemeChangeEvent } from '../common/theme';
22import { inject, injectable, postConstruct } from 'inversify';
23import { Deferred } from '../common/promise-util';
24import { PreferenceSchemaProvider, PreferenceService } from './preferences';
25import debounce = require('lodash.debounce');
26
27const COLOR_THEME_PREFERENCE_KEY = 'workbench.colorTheme';
28const NO_THEME = { id: 'no-theme', label: 'Not a real theme.', type: 'dark' } as const;
29
30@injectable()
31export 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 /** Should only be called at startup. */
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 * @param persist If `true`, the value of the `workbench.colorTheme` preference will be set to the provided ID.
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 * The default theme. If that is not applicable, returns with the fallback theme.
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 * Resets the state to the user's default, or to the fallback theme. Also discards any persisted state in the local storage.
164 */
165 reset(): void {
166 this.setCurrentTheme(this.defaultTheme.id);
167 }
168}
169
170export class BuiltinThemeProvider {
171
172 static readonly darkTheme: Theme = {
173 id: 'dark',
174 type: 'dark',
175 label: 'Dark (Theia)',
176 editorTheme: 'dark-theia' // loaded in /packages/monaco/src/browser/textmate/monaco-theme-registry.ts
177 };
178
179 static readonly lightTheme: Theme = {
180 id: 'light',
181 type: 'light',
182 label: 'Light (Theia)',
183 editorTheme: 'light-theia' // loaded in /packages/monaco/src/browser/textmate/monaco-theme-registry.ts
184 };
185
186 static readonly hcTheme: Theme = {
187 id: 'hc-theia',
188 type: 'hc',
189 label: 'High Contrast (Theia)',
190 editorTheme: 'hc-theia' // loaded in /packages/monaco/src/browser/textmate/monaco-theme-registry.ts
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' // loaded in /packages/monaco/src/browser/textmate/monaco-theme-registry.ts
198 };
199
200 static readonly themes = [
201 BuiltinThemeProvider.darkTheme,
202 BuiltinThemeProvider.lightTheme,
203 BuiltinThemeProvider.hcTheme,
204 BuiltinThemeProvider.hcLightTheme
205 ];
206}