Skip to content

Commit 5121f5b

Browse files
committed
[icons] icon theme selection picker and menu action
1 parent 9301205 commit 5121f5b

6 files changed

Lines changed: 142 additions & 48 deletions

File tree

src/vs/code/electron-main/menus.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -384,7 +384,8 @@ export class VSCodeMenu {
384384
let workspaceSettings = this.createMenuItem(nls.localize({ key: 'miOpenWorkspaceSettings', comment: ['&& denotes a mnemonic'] }, "&&Workspace Settings"), 'workbench.action.openWorkspaceSettings');
385385
let kebindingSettings = this.createMenuItem(nls.localize({ key: 'miOpenKeymap', comment: ['&& denotes a mnemonic'] }, "&&Keyboard Shortcuts"), 'workbench.action.openGlobalKeybindings');
386386
let snippetsSettings = this.createMenuItem(nls.localize({ key: 'miOpenSnippets', comment: ['&& denotes a mnemonic'] }, "User &&Snippets"), 'workbench.action.openSnippets');
387-
let themeSelection = this.createMenuItem(nls.localize({ key: 'miSelectTheme', comment: ['&& denotes a mnemonic'] }, "&&Color Theme"), 'workbench.action.selectTheme');
387+
let colorThemeSelection = this.createMenuItem(nls.localize({ key: 'miSelectColorTheme', comment: ['&& denotes a mnemonic'] }, "&&Color Theme"), 'workbench.action.selectTheme');
388+
let iconThemeSelection = this.createMenuItem(nls.localize({ key: 'miSelectIconTheme', comment: ['&& denotes a mnemonic'] }, "File &&Icon Theme"), 'workbench.action.selectIconTheme');
388389

389390
let preferencesMenu = new Menu();
390391
preferencesMenu.append(userSettings);
@@ -394,7 +395,8 @@ export class VSCodeMenu {
394395
preferencesMenu.append(__separator__());
395396
preferencesMenu.append(snippetsSettings);
396397
preferencesMenu.append(__separator__());
397-
preferencesMenu.append(themeSelection);
398+
preferencesMenu.append(colorThemeSelection);
399+
preferencesMenu.append(iconThemeSelection);
398400

399401
return new MenuItem({ label: mnemonicLabel(nls.localize({ key: 'miPreferences', comment: ['&& denotes a mnemonic'] }, "&&Preferences")), submenu: preferencesMenu });
400402
}

src/vs/code/electron-main/window.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ export interface IWindowSettings {
116116
export class VSCodeWindow {
117117

118118
public static menuBarHiddenKey = 'menuBarHidden';
119-
public static themeStorageKey = 'theme';
119+
public static colorThemeStorageKey = 'theme';
120120

121121
private static MIN_WIDTH = 200;
122122
private static MIN_HEIGHT = 120;
@@ -153,7 +153,7 @@ export class VSCodeWindow {
153153
this.restoreWindowState(config.state);
154154

155155
// For VS theme we can show directly because background is white
156-
const usesLightTheme = /vs($| )/.test(this.storageService.getItem<string>(VSCodeWindow.themeStorageKey));
156+
const usesLightTheme = /vs($| )/.test(this.storageService.getItem<string>(VSCodeWindow.colorThemeStorageKey));
157157
if (!global.windowShow) {
158158
global.windowShow = Date.now();
159159
}

src/vs/code/electron-main/windows.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -486,8 +486,8 @@ export class WindowsManager implements IWindowsService {
486486
private onBroadcast(event: string, payload: any): void {
487487

488488
// Theme changes
489-
if (event === 'vscode:changeTheme' && typeof payload === 'string') {
490-
this.storageService.setItem(VSCodeWindow.themeStorageKey, payload);
489+
if (event === 'vscode:changeColorTheme' && typeof payload === 'string') {
490+
this.storageService.setItem(VSCodeWindow.colorThemeStorageKey, payload);
491491
}
492492
}
493493

src/vs/workbench/parts/themes/electron-browser/themes.contribution.ts

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {IExtensionGalleryService} from 'vs/platform/extensionManagement/common/e
2020
import {IViewletService} from 'vs/workbench/services/viewlet/common/viewletService';
2121
import {Delayer} from 'vs/base/common/async';
2222

23-
class SelectThemeAction extends Action {
23+
class SelectColorThemeAction extends Action {
2424

2525
static ID = 'workbench.action.selectTheme';
2626
static LABEL = localize('selectTheme.label', "Color Theme");
@@ -84,6 +84,76 @@ class SelectThemeAction extends Action {
8484
}
8585
}
8686

87+
class SelectIconThemeAction extends Action {
88+
89+
static ID = 'workbench.action.selectIconTheme';
90+
static LABEL = localize('selectIconTheme.label', "File Icon Theme");
91+
92+
constructor(
93+
id: string,
94+
label: string,
95+
@IQuickOpenService private quickOpenService: IQuickOpenService,
96+
@IMessageService private messageService: IMessageService,
97+
@IThemeService private themeService: IThemeService,
98+
@IExtensionGalleryService private extensionGalleryService: IExtensionGalleryService,
99+
@IViewletService private viewletService: IViewletService
100+
) {
101+
super(id, label);
102+
}
103+
104+
run(): TPromise<void> {
105+
return this.themeService.getFileIconThemes().then(themes => {
106+
const currentThemeId = this.themeService.getFileIconTheme();
107+
const currentTheme = themes.filter(theme => theme.id === currentThemeId)[0];
108+
109+
const picks: IPickOpenEntry[] = themes
110+
.map(theme => ({ id: theme.id, label: theme.label, description: theme.description }))
111+
.sort((t1, t2) => t1.label.localeCompare(t2.label));
112+
113+
picks.splice(0, 0, { id: '', label: localize('noIconThemeLabel', 'None'), description: localize('noIconThemeDesc', 'Disable file icons') });
114+
115+
const selectTheme = (theme, broadcast) => {
116+
this.themeService.setFileIconTheme(theme && theme.id, broadcast)
117+
.done(null, err => this.messageService.show(Severity.Info, localize('problemChangingIconTheme', "Problem loading icon theme: {0}", err.message)));
118+
};
119+
120+
const placeHolder = localize('themes.selectIconTheme', "Select File Icon Theme");
121+
const autoFocusIndex = firstIndex(picks, p => p.id === currentThemeId);
122+
const delayer = new Delayer<void>(100);
123+
124+
/* if (this.extensionGalleryService.isEnabled()) {
125+
const run = () => {
126+
return this.viewletService.openViewlet(VIEWLET_ID, true)
127+
.then(viewlet => viewlet as IExtensionsViewlet)
128+
.then(viewlet => {
129+
viewlet.search('category:themes', true); // define our own category
130+
viewlet.focus();
131+
});
132+
};
133+
134+
picks.push({
135+
id: 'themes.findmoreiconthemes',
136+
label: localize('findMoreIconThemes', "Find more in the Marketplace..."),
137+
separator: { border: true },
138+
alwaysShow: true,
139+
run
140+
});
141+
}*/
142+
143+
return this.quickOpenService.pick(picks, { placeHolder, autoFocus: { autoFocusIndex }})
144+
.then(
145+
theme => delayer.trigger(() => selectTheme(theme || currentTheme, true), 0),
146+
null,
147+
theme => delayer.trigger(() => selectTheme(theme, false))
148+
);
149+
});
150+
}
151+
}
152+
87153
const category = localize('preferences', "Preferences");
88-
const descriptor = new SyncActionDescriptor(SelectThemeAction, SelectThemeAction.ID, SelectThemeAction.LABEL);
89-
Registry.as<IWorkbenchActionRegistry>(Extensions.WorkbenchActions).registerWorkbenchAction(descriptor, 'Preferences: Color Theme', category);
154+
155+
const colorThemeDescriptor = new SyncActionDescriptor(SelectColorThemeAction, SelectColorThemeAction.ID, SelectColorThemeAction.LABEL);
156+
Registry.as<IWorkbenchActionRegistry>(Extensions.WorkbenchActions).registerWorkbenchAction(colorThemeDescriptor, 'Preferences: Color Theme', category);
157+
158+
const iconThemeDescriptor = new SyncActionDescriptor(SelectIconThemeAction, SelectIconThemeAction.ID, SelectIconThemeAction.LABEL);
159+
Registry.as<IWorkbenchActionRegistry>(Extensions.WorkbenchActions).registerWorkbenchAction(iconThemeDescriptor, 'Preferences: File Icon Theme', category);

src/vs/workbench/services/themes/common/themeService.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ export interface IThemeService {
1717
getColorThemes(): TPromise<IThemeData[]>;
1818
onDidColorThemeChange: Event<string>;
1919

20-
getFileIcons(): TPromise<IThemeData[]>;
20+
setFileIconTheme(iconThemeId: string, broadcastToAllWindows: boolean): TPromise<boolean>;
21+
getFileIconTheme(): string;
22+
getFileIconThemes(): TPromise<IThemeData[]>;
2123
}
2224

2325
export interface IThemeData {

src/vs/workbench/services/themes/electron-browser/themeService.ts

Lines changed: 58 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*--------------------------------------------------------------------------------------------*/
55
'use strict';
66

7-
import {TPromise} from 'vs/base/common/winjs.base';
7+
import {TPromise, Promise} from 'vs/base/common/winjs.base';
88
import nls = require('vs/nls');
99
import Paths = require('vs/base/common/paths');
1010
import Json = require('vs/base/common/json');
@@ -16,10 +16,8 @@ import {getBaseThemeId, getSyntaxThemeId} from 'vs/platform/theme/common/themes'
1616
import {IWindowService} from 'vs/workbench/services/window/electron-browser/windowService';
1717
import {IStorageService, StorageScope} from 'vs/platform/storage/common/storage';
1818
import {ITelemetryService} from 'vs/platform/telemetry/common/telemetry';
19-
import {IConfigurationService} from 'vs/platform/configuration/common/configuration';
2019
import {Registry} from 'vs/platform/platform';
2120
import {IConfigurationRegistry, Extensions} from 'vs/platform/configuration/common/configurationRegistry';
22-
import {IFilesConfiguration} from 'vs/platform/files/common/files';
2321
import {Extensions as JSONExtensions, IJSONContributionRegistry} from 'vs/platform/jsonschemas/common/jsonContributionRegistry';
2422
import {IJSONSchema} from 'vs/base/common/jsonSchema';
2523

@@ -34,8 +32,10 @@ import pfs = require('vs/base/node/pfs');
3432
const DEFAULT_THEME_ID = 'vs-dark vscode-theme-defaults-themes-dark_plus-json';
3533
const DEFAULT_FILE_ICONS = 'vs-standard';
3634

37-
const THEME_CHANNEL = 'vscode:changeTheme';
38-
const THEME_PREF = 'workbench.theme';
35+
const COLOR_THEME_CHANNEL = 'vscode:changeColorTheme';
36+
const ICON_THEME_CHANNEL = 'vscode:changeIconTheme';
37+
const COLOR_THEME_PREF = 'workbench.theme';
38+
const ICON_THEME_PREF = 'workbench.iconTheme';
3939

4040
let defaultBaseTheme = getBaseThemeId(DEFAULT_THEME_ID);
4141

@@ -172,19 +172,18 @@ export class ThemeService implements IThemeService {
172172
private onColorThemeChange: Emitter<string>;
173173

174174
private knownFileIconContributions: IInternalThemeData[];
175-
private currentFileIcons: string;
175+
private currentIconTheme: string;
176176

177177
constructor(
178178
@IExtensionService private extensionService: IExtensionService,
179179
@IWindowService private windowService: IWindowService,
180180
@IStorageService private storageService: IStorageService,
181-
@IConfigurationService private configurationService: IConfigurationService,
182181
@ITelemetryService private telemetryService: ITelemetryService) {
183182

184183
this.knownThemes = [];
185184
this.onColorThemeChange = new Emitter<string>();
186185
this.knownFileIconContributions = [];
187-
this.currentFileIcons = null;
186+
this.currentIconTheme = '';
188187

189188
themesExtPoint.setHandler((extensions) => {
190189
for (let ext of extensions) {
@@ -193,7 +192,7 @@ export class ThemeService implements IThemeService {
193192
});
194193

195194
windowService.onBroadcast(e => {
196-
if (e.channel === THEME_CHANNEL && typeof e.payload === 'string') {
195+
if (e.channel === COLOR_THEME_CHANNEL && typeof e.payload === 'string') {
197196
this.setColorTheme(e.payload, false);
198197
}
199198
});
@@ -204,32 +203,31 @@ export class ThemeService implements IThemeService {
204203
}
205204
});
206205

207-
const settings = configurationService.getConfiguration<IFilesConfiguration>();
208-
let iconTheme = settings && settings.files && settings.files.iconTheme;
209-
if (iconTheme) {
210-
this.setFileIcons(iconTheme);
211-
}
212-
213-
configurationService.onDidUpdateConfiguration(e => {
214-
let filesConfig = e.config.files;
215-
let iconTheme = filesConfig && filesConfig.iconTheme;
216-
this.setFileIcons(iconTheme);
206+
windowService.onBroadcast(e => {
207+
if (e.channel === ICON_THEME_CHANNEL && typeof e.payload === 'string') {
208+
this.setFileIconTheme(e.payload, false);
209+
}
217210
});
218211
}
219212

220213
public get onDidColorThemeChange(): Event<string> {
221214
return this.onColorThemeChange.event;
222215
}
223216

224-
public initialize(container: HTMLElement): TPromise<boolean> {
217+
public initialize(container: HTMLElement): TPromise<void> {
225218
this.container = container;
226219

227-
let themeId = this.storageService.get(THEME_PREF, StorageScope.GLOBAL, null);
220+
let themeId = this.storageService.get(COLOR_THEME_PREF, StorageScope.GLOBAL, null);
228221
if (!themeId) {
229222
themeId = DEFAULT_THEME_ID;
230-
this.storageService.store(THEME_PREF, themeId, StorageScope.GLOBAL);
223+
this.storageService.store(COLOR_THEME_PREF, themeId, StorageScope.GLOBAL);
231224
}
232-
return this.setColorTheme(themeId, false);
225+
let iconThemeId = this.storageService.get(ICON_THEME_PREF, StorageScope.GLOBAL, null);
226+
return Promise.join([
227+
this.setColorTheme(themeId, false),
228+
this.setFileIconTheme(iconThemeId, false)
229+
]);
230+
233231
}
234232

235233
public setColorTheme(themeId: string, broadcastToAllWindows: boolean) : TPromise<boolean> {
@@ -238,7 +236,7 @@ export class ThemeService implements IThemeService {
238236
}
239237
if (themeId === this.currentTheme) {
240238
if (broadcastToAllWindows) {
241-
this.windowService.broadcast({ channel: THEME_CHANNEL, payload: themeId });
239+
this.windowService.broadcast({ channel: COLOR_THEME_CHANNEL, payload: themeId });
242240
}
243241
return TPromise.as(true);
244242
}
@@ -255,9 +253,9 @@ export class ThemeService implements IThemeService {
255253
$(this.container).addClass(newThemeId);
256254
}
257255

258-
this.storageService.store(THEME_PREF, newThemeId, StorageScope.GLOBAL);
256+
this.storageService.store(COLOR_THEME_PREF, newThemeId, StorageScope.GLOBAL);
259257
if (broadcastToAllWindows) {
260-
this.windowService.broadcast({ channel: THEME_CHANNEL, payload: newThemeId });
258+
this.windowService.broadcast({ channel: COLOR_THEME_CHANNEL, payload: newThemeId });
261259
} else {
262260
this.sendTelemetry(newTheme);
263261
}
@@ -268,7 +266,7 @@ export class ThemeService implements IThemeService {
268266
}
269267

270268
public getColorTheme() {
271-
return this.currentTheme || this.storageService.get(THEME_PREF, StorageScope.GLOBAL, DEFAULT_THEME_ID);
269+
return this.currentTheme || this.storageService.get(COLOR_THEME_PREF, StorageScope.GLOBAL, DEFAULT_THEME_ID);
272270
}
273271

274272
private findThemeData(themeId: string, defaultId?: string): TPromise<IInternalThemeData> {
@@ -399,48 +397,70 @@ export class ThemeService implements IThemeService {
399397
}
400398
}
401399

402-
public getFileIcons(): TPromise<IThemeData[]> {
400+
public getFileIconThemes(): TPromise<IThemeData[]> {
403401
return this.extensionService.onReady().then(isReady => {
404402
return this.knownFileIconContributions;
405403
});
406404
}
407405

408-
private setFileIcons(fileIcons: string) : TPromise<boolean> {
409-
if (fileIcons !== this.currentFileIcons) {
410-
this.currentFileIcons = fileIcons;
411-
return this._updateFileIcons();
406+
public getFileIconTheme() {
407+
return this.currentIconTheme || this.storageService.get(ICON_THEME_PREF, StorageScope.GLOBAL, '');
408+
}
409+
410+
public setFileIconTheme(fileIcons: string, broadcastToAllWindows: boolean) : TPromise<boolean> {
411+
fileIcons = fileIcons || '';
412+
if (fileIcons === this.currentIconTheme) {
413+
if (broadcastToAllWindows) {
414+
this.windowService.broadcast({ channel: ICON_THEME_CHANNEL, payload: fileIcons });
415+
}
416+
return TPromise.as(true);
412417
}
413-
return TPromise.as(true);
418+
let onApply = (newIconTheme: IInternalThemeData) => {
419+
let newIconThemeId = newIconTheme ? newIconTheme.id : '';
420+
421+
this.storageService.store(ICON_THEME_PREF, newIconThemeId, StorageScope.GLOBAL);
422+
if (broadcastToAllWindows) {
423+
this.windowService.broadcast({ channel: ICON_THEME_CHANNEL, payload: newIconThemeId });
424+
} else if (newIconTheme) {
425+
this.sendTelemetry(newIconTheme);
426+
}
427+
};
428+
429+
this.currentIconTheme = fileIcons;
430+
return this._updateFileIcons(onApply);
414431
}
415432

416-
private _updateFileIcons() : TPromise<boolean> {
417-
return this.getFileIcons().then(allIconSets => {
433+
private _updateFileIcons(onApply: (theme:IInternalThemeData) => void) : TPromise<boolean> {
434+
return this.getFileIconThemes().then(allIconSets => {
418435
let iconSetData;
419436
for (let iconSet of allIconSets) {
420-
if (iconSet.id === this.currentFileIcons) {
437+
if (iconSet.id === this.currentIconTheme) {
421438
iconSetData = <IInternalThemeData> iconSet;
422439
break;
423440
}
424441
}
425-
return _applyFileIcons(iconSetData);
442+
return _applyFileIcons(iconSetData, onApply);
426443
});
427444
}
428445
}
429446

430-
function _applyFileIcons(data: IInternalThemeData): TPromise<boolean> {
447+
function _applyFileIcons(data: IInternalThemeData, onApply: (theme:IInternalThemeData) => void): TPromise<boolean> {
431448
if (!data) {
432449
_applyRules('', fileIconRulesClassName);
450+
onApply(data);
433451
return TPromise.as(true);
434452
}
435453

436454
if (data.styleSheetContent) {
437455
_applyRules(data.styleSheetContent, fileIconRulesClassName);
456+
onApply(data);
438457
return TPromise.as(true);
439458
}
440459
return _loadFileIconsDocument(data.path).then(fileIconsDocument => {
441460
let styleSheetContent = _processFileIconsObject(data.id, data.path, fileIconsDocument);
442461
data.styleSheetContent = styleSheetContent;
443462
_applyRules(styleSheetContent, fileIconRulesClassName);
463+
onApply(data);
444464
return true;
445465
}, error => {
446466
return TPromise.wrapError(nls.localize('error.cannotloadfileicons', "Unable to load {0}", data.path));

0 commit comments

Comments
 (0)