1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 | import { injectable, inject, postConstruct } from 'inversify';
|
18 | import { Emitter } from '../common/event';
|
19 | import { Disposable, DisposableCollection } from '../common/disposable';
|
20 | import { LabelProviderContribution, DidChangeLabelEvent } from './label-provider';
|
21 | import { FrontendApplicationConfigProvider } from './frontend-application-config-provider';
|
22 | import { PreferenceService, PreferenceSchemaProvider } from './preferences';
|
23 | import debounce = require('lodash.debounce');
|
24 |
|
25 | const ICON_THEME_PREFERENCE_KEY = 'workbench.iconTheme';
|
26 |
|
27 | export 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 |
|
36 | export interface IconTheme extends IconThemeDefinition {
|
37 | activate(): Disposable;
|
38 | }
|
39 |
|
40 | @injectable()
|
41 | export 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()
|
80 | export 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 |
|
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 | }
|