UNPKG

7.65 kBPlain TextView Raw
1// *****************************************************************************
2// Copyright (C) 2019 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 { injectable, inject, postConstruct } from 'inversify';
18import { Emitter } from '../common/event';
19import { Disposable, DisposableCollection } from '../common/disposable';
20import { LabelProviderContribution, DidChangeLabelEvent } from './label-provider';
21import { FrontendApplicationConfigProvider } from './frontend-application-config-provider';
22import { PreferenceService, PreferenceSchemaProvider } from './preferences';
23import debounce = require('lodash.debounce');
24
25const ICON_THEME_PREFERENCE_KEY = 'workbench.iconTheme';
26
27export interface IconThemeDefinition {
28 readonly id: string
29 readonly label: string
30 readonly description?: string
31 readonly hasFileIcons?: boolean;
32 readonly hasFolderIcons?: boolean;
33 readonly hidesExplorerArrows?: boolean;
34}
35
36export interface IconTheme extends IconThemeDefinition {
37 activate(): Disposable;
38}
39
40@injectable()
41export class NoneIconTheme implements IconTheme, LabelProviderContribution {
42
43 readonly id = 'none';
44 readonly label = 'None';
45 readonly description = 'Disable file icons';
46 readonly hasFileIcons = true;
47 readonly hasFolderIcons = true;
48
49 protected readonly onDidChangeEmitter = new Emitter<DidChangeLabelEvent>();
50 readonly onDidChange = this.onDidChangeEmitter.event;
51
52 protected readonly toDeactivate = new DisposableCollection();
53
54 activate(): Disposable {
55 if (this.toDeactivate.disposed) {
56 this.toDeactivate.push(Disposable.create(() => this.fireDidChange()));
57 this.fireDidChange();
58 }
59 return this.toDeactivate;
60 }
61
62 protected fireDidChange(): void {
63 this.onDidChangeEmitter.fire({ affects: () => true });
64 }
65
66 canHandle(): number {
67 if (this.toDeactivate.disposed) {
68 return 0;
69 }
70 return Number.MAX_SAFE_INTEGER - 1024;
71 }
72
73 getIcon(): string {
74 return '';
75 }
76
77}
78
79@injectable()
80export class IconThemeService {
81 static readonly STORAGE_KEY = 'iconTheme';
82
83 protected readonly onDidChangeEmitter = new Emitter<void>();
84 readonly onDidChange = this.onDidChangeEmitter.event;
85
86 protected readonly _iconThemes = new Map<string, IconTheme>();
87 get ids(): IterableIterator<string> {
88 return this._iconThemes.keys();
89 }
90 get definitions(): IterableIterator<IconThemeDefinition> {
91 return this._iconThemes.values();
92 }
93 getDefinition(id: string): IconThemeDefinition | undefined {
94 return this._iconThemes.get(id);
95 }
96
97 @inject(NoneIconTheme) protected readonly noneIconTheme: NoneIconTheme;
98 @inject(PreferenceService) protected readonly preferences: PreferenceService;
99 @inject(PreferenceSchemaProvider) protected readonly schemaProvider: PreferenceSchemaProvider;
100
101 protected readonly onDidChangeCurrentEmitter = new Emitter<string>();
102 readonly onDidChangeCurrent = this.onDidChangeCurrentEmitter.event;
103
104 protected readonly toDeactivate = new DisposableCollection();
105
106 protected activeTheme: IconTheme;
107
108 @postConstruct()
109 protected init(): void {
110 this.register(this.fallback);
111 this.setCurrent(this.fallback, false);
112 this.preferences.ready.then(() => {
113 this.validateActiveTheme();
114 this.updateIconThemePreference();
115 this.preferences.onPreferencesChanged(changes => {
116 if (ICON_THEME_PREFERENCE_KEY in changes) {
117 this.validateActiveTheme();
118 }
119 });
120 });
121 }
122
123 register(iconTheme: IconTheme): Disposable {
124 if (this._iconThemes.has(iconTheme.id)) {
125 console.warn(new Error(`Icon theme '${iconTheme.id}' has already been registered, skipping.`));
126 return Disposable.NULL;
127 }
128 this._iconThemes.set(iconTheme.id, iconTheme);
129 this.onDidChangeEmitter.fire(undefined);
130 this.validateActiveTheme();
131 this.updateIconThemePreference();
132 return Disposable.create(() => {
133 this.unregister(iconTheme.id);
134 this.updateIconThemePreference();
135 });
136 }
137
138 unregister(id: string): IconTheme | undefined {
139 const iconTheme = this._iconThemes.get(id);
140 if (!iconTheme) {
141 return undefined;
142 }
143 this._iconThemes.delete(id);
144 this.onDidChangeEmitter.fire(undefined);
145 if (id === this.getCurrent().id) {
146 this.setCurrent(this.default, false);
147 }
148 return iconTheme;
149 }
150
151 get current(): string {
152 return this.getCurrent().id;
153 }
154
155 set current(id: string) {
156 const newCurrent = this._iconThemes.get(id);
157 if (newCurrent && this.getCurrent().id !== newCurrent.id) {
158 this.setCurrent(newCurrent);
159 }
160 }
161
162 getCurrent(): IconTheme {
163 return this.activeTheme;
164 }
165
166 /**
167 * @param persistSetting If `true`, the theme's id will be set as the value of the `workbench.iconTheme` preference. (default: `true`)
168 */
169 setCurrent(newCurrent: IconTheme, persistSetting = true): void {
170 if (newCurrent !== this.getCurrent()) {
171 this.activeTheme = newCurrent;
172 this.toDeactivate.dispose();
173 this.toDeactivate.push(newCurrent.activate());
174 this.onDidChangeCurrentEmitter.fire(newCurrent.id);
175 }
176 if (persistSetting) {
177 this.preferences.updateValue(ICON_THEME_PREFERENCE_KEY, newCurrent.id);
178 }
179 }
180
181 protected getConfiguredTheme(): IconTheme | undefined {
182 const configuredId = this.preferences.get<string>(ICON_THEME_PREFERENCE_KEY);
183 return configuredId ? this._iconThemes.get(configuredId) : undefined;
184 }
185
186 protected validateActiveTheme(): void {
187 if (this.preferences.isReady) {
188 const configured = this.getConfiguredTheme();
189 if (configured && configured !== this.getCurrent()) {
190 this.setCurrent(configured, false);
191 }
192 }
193 }
194
195 protected updateIconThemePreference = debounce(() => this.doUpdateIconThemePreference(), 500);
196
197 protected doUpdateIconThemePreference(): void {
198 const preference = this.schemaProvider.getSchemaProperty(ICON_THEME_PREFERENCE_KEY);
199 if (preference) {
200 const sortedThemes = Array.from(this.definitions).sort((a, b) => a.label.localeCompare(b.label));
201 this.schemaProvider.updateSchemaProperty(ICON_THEME_PREFERENCE_KEY, {
202 ...preference,
203 enum: sortedThemes.map(e => e.id),
204 enumItemLabels: sortedThemes.map(e => e.label)
205 });
206 }
207 }
208
209 get default(): IconTheme {
210 return this._iconThemes.get(FrontendApplicationConfigProvider.get().defaultIconTheme) || this.fallback;
211 }
212
213 get fallback(): IconTheme {
214 return this.noneIconTheme;
215 }
216}