themes.contribution.ts 14.7 KB
Newer Older
E
Erich Gamma 已提交
1 2 3 4
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/
J
Joao Moreno 已提交
5

J
Johannes Rieken 已提交
6 7
import { localize } from 'vs/nls';
import { Action } from 'vs/base/common/actions';
C
Christof Marti 已提交
8
import { KeyMod, KeyChord, KeyCode } from 'vs/base/common/keyCodes';
I
isidor 已提交
9
import { SyncActionDescriptor, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions';
10
import { Registry } from 'vs/platform/registry/common/platform';
B
Benjamin Pasero 已提交
11
import { IWorkbenchActionRegistry, Extensions } from 'vs/workbench/common/actions';
12
import { IWorkbenchThemeService, IWorkbenchTheme } from 'vs/workbench/services/themes/common/workbenchThemeService';
S
SteVen Batten 已提交
13
import { VIEWLET_ID, IExtensionsViewPaneContainer } from 'vs/workbench/contrib/extensions/common/extensions';
J
Johannes Rieken 已提交
14
import { IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement';
B
Benjamin Pasero 已提交
15
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
16
import { IColorRegistry, Extensions as ColorRegistryExtensions } from 'vs/platform/theme/common/colorRegistry';
17
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
J
Joao Moreno 已提交
18
import { Color } from 'vs/base/common/color';
19
import { ColorScheme } from 'vs/platform/theme/common/theme';
20
import { colorThemeSchemaId } from 'vs/workbench/services/themes/common/colorThemeSchema';
21
import { onUnexpectedError } from 'vs/base/common/errors';
22
import { IQuickInputService, QuickPickInput } from 'vs/platform/quickinput/common/quickInput';
23 24
import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
import { DEFAULT_PRODUCT_ICON_THEME_ID } from 'vs/workbench/services/themes/browser/productIconThemeData';
E
Erich Gamma 已提交
25

C
Christof Marti 已提交
26
export class SelectColorThemeAction extends Action {
E
Erich Gamma 已提交
27

28
	static readonly ID = 'workbench.action.selectTheme';
29
	static readonly LABEL = localize('selectTheme.label', "Color Theme");
E
Erich Gamma 已提交
30 31 32 33

	constructor(
		id: string,
		label: string,
34 35 36
		@IQuickInputService private readonly quickInputService: IQuickInputService,
		@IWorkbenchThemeService private readonly themeService: IWorkbenchThemeService,
		@IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService,
37
		@IViewletService private readonly viewletService: IViewletService
E
Erich Gamma 已提交
38 39 40 41
	) {
		super(id, label);
	}

J
Johannes Rieken 已提交
42
	run(): Promise<void> {
M
Martin Aeschlimann 已提交
43
		return this.themeService.getColorThemes().then(themes => {
M
Martin Aeschlimann 已提交
44
			const currentTheme = this.themeService.getColorTheme();
E
Erich Gamma 已提交
45

46
			const picks: QuickPickInput<ThemeItem>[] = [
47 48 49
				...toEntries(themes.filter(t => t.type === ColorScheme.LIGHT), localize('themes.category.light', "light themes")),
				...toEntries(themes.filter(t => t.type === ColorScheme.DARK), localize('themes.category.dark', "dark themes")),
				...toEntries(themes.filter(t => t.type === ColorScheme.HIGH_CONTRAST), localize('themes.category.hc', "high contrast themes")),
50 51
				...configurationEntries(this.extensionGalleryService, localize('installColorThemes', "Install Additional Color Themes..."))
			];
E
Erich Gamma 已提交
52

M
Martin Aeschlimann 已提交
53
			let selectThemeTimeout: number | undefined;
54

55
			const selectTheme = (theme: ThemeItem, applyTheme: boolean) => {
56 57 58
				if (selectThemeTimeout) {
					clearTimeout(selectThemeTimeout);
				}
M
Martin Aeschlimann 已提交
59
				selectThemeTimeout = window.setTimeout(() => {
60
					selectThemeTimeout = undefined;
61
					const themeId = theme && theme.id !== undefined ? theme.id : currentTheme.id;
M
Martin Aeschlimann 已提交
62

63
					this.themeService.setColorTheme(themeId, applyTheme ? 'auto' : undefined).then(undefined,
64 65 66 67 68 69
						err => {
							onUnexpectedError(err);
							this.themeService.setColorTheme(currentTheme.id, undefined);
						}
					);
				}, applyTheme ? 0 : 200);
J
Joao Moreno 已提交
70
			};
E
Erich Gamma 已提交
71

72 73 74
			return new Promise((s, _) => {
				let isCompleted = false;

M
Matt Bierner 已提交
75
				const autoFocusIndex = picks.findIndex(p => isItem(p) && p.id === currentTheme.id);
76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
				const quickpick = this.quickInputService.createQuickPick<ThemeItem>();
				quickpick.items = picks;
				quickpick.placeholder = localize('themes.selectTheme', "Select Color Theme (Up/Down Keys to Preview)");
				quickpick.activeItems = [picks[autoFocusIndex] as ThemeItem];
				quickpick.canSelectMany = false;
				quickpick.onDidAccept(_ => {
					const theme = quickpick.activeItems[0];
					if (!theme || typeof theme.id === 'undefined') { // 'pick in marketplace' entry
						openExtensionViewlet(this.viewletService, `category:themes ${quickpick.value}`);
					} else {
						selectTheme(theme, true);
					}
					isCompleted = true;
					quickpick.hide();
					s();
				});
				quickpick.onDidChangeActive(themes => selectTheme(themes[0], false));
				quickpick.onDidHide(() => {
					if (!isCompleted) {
						selectTheme(currentTheme, true);
						s();
					}
				});
				quickpick.show();
			});
E
Erich Gamma 已提交
101 102 103 104
		});
	}
}

105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148
abstract class AbstractIconThemeAction extends Action {
	constructor(
		id: string,
		label: string,
		private readonly quickInputService: IQuickInputService,
		private readonly extensionGalleryService: IExtensionGalleryService,
		private readonly viewletService: IViewletService

	) {
		super(id, label);
	}

	protected abstract get builtInEntry(): QuickPickInput<ThemeItem>;
	protected abstract get installMessage(): string | undefined;
	protected abstract get placeholderMessage(): string;
	protected abstract get marketplaceTag(): string;

	protected abstract setTheme(id: string, settingsTarget: ConfigurationTarget | undefined | 'auto'): Promise<any>;

	protected pick(themes: IWorkbenchTheme[], currentTheme: IWorkbenchTheme) {
		let picks: QuickPickInput<ThemeItem>[] = [this.builtInEntry];
		picks = picks.concat(
			toEntries(themes),
			configurationEntries(this.extensionGalleryService, this.installMessage)
		);

		let selectThemeTimeout: number | undefined;

		const selectTheme = (theme: ThemeItem, applyTheme: boolean) => {
			if (selectThemeTimeout) {
				clearTimeout(selectThemeTimeout);
			}
			selectThemeTimeout = window.setTimeout(() => {
				selectThemeTimeout = undefined;
				const themeId = theme && theme.id !== undefined ? theme.id : currentTheme.id;
				this.setTheme(themeId, applyTheme ? 'auto' : undefined).then(undefined,
					err => {
						onUnexpectedError(err);
						this.setTheme(currentTheme.id, undefined);
					}
				);
			}, applyTheme ? 0 : 200);
		};

149
		return new Promise<void>((s, _) => {
150 151
			let isCompleted = false;

M
Matt Bierner 已提交
152
			const autoFocusIndex = picks.findIndex(p => isItem(p) && p.id === currentTheme.id);
153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181
			const quickpick = this.quickInputService.createQuickPick<ThemeItem>();
			quickpick.items = picks;
			quickpick.placeholder = this.placeholderMessage;
			quickpick.activeItems = [picks[autoFocusIndex] as ThemeItem];
			quickpick.canSelectMany = false;
			quickpick.onDidAccept(_ => {
				const theme = quickpick.activeItems[0];
				if (!theme || typeof theme.id === 'undefined') { // 'pick in marketplace' entry
					openExtensionViewlet(this.viewletService, `${this.marketplaceTag} ${quickpick.value}`);
				} else {
					selectTheme(theme, true);
				}
				isCompleted = true;
				quickpick.hide();
				s();
			});
			quickpick.onDidChangeActive(themes => selectTheme(themes[0], false));
			quickpick.onDidHide(() => {
				if (!isCompleted) {
					selectTheme(currentTheme, true);
					s();
				}
			});
			quickpick.show();
		});
	}
}

class SelectFileIconThemeAction extends AbstractIconThemeAction {
182

183
	static readonly ID = 'workbench.action.selectIconTheme';
184
	static readonly LABEL = localize('selectIconTheme.label', "File Icon Theme");
185 186 187 188

	constructor(
		id: string,
		label: string,
189
		@IQuickInputService quickInputService: IQuickInputService,
190
		@IWorkbenchThemeService private readonly themeService: IWorkbenchThemeService,
191 192
		@IExtensionGalleryService extensionGalleryService: IExtensionGalleryService,
		@IViewletService viewletService: IViewletService
M
Martin Aeschlimann 已提交
193

194
	) {
195
		super(id, label, quickInputService, extensionGalleryService, viewletService);
196 197
	}

198 199 200 201 202 203 204
	protected builtInEntry: QuickPickInput<ThemeItem> = { id: '', label: localize('noIconThemeLabel', 'None'), description: localize('noIconThemeDesc', 'Disable file icons') };
	protected installMessage = localize('installIconThemes', "Install Additional File Icon Themes...");
	protected placeholderMessage = localize('themes.selectIconTheme', "Select File Icon Theme");
	protected marketplaceTag = 'tag:icon-theme';
	protected setTheme(id: string, settingsTarget: ConfigurationTarget | undefined | 'auto') {
		return this.themeService.setFileIconTheme(id, settingsTarget);
	}
205

206 207 208 209
	async run(): Promise<void> {
		this.pick(await this.themeService.getFileIconThemes(), this.themeService.getFileIconTheme());
	}
}
210

211

212
class SelectProductIconThemeAction extends AbstractIconThemeAction {
213

214 215
	static readonly ID = 'workbench.action.selectProductIconTheme';
	static readonly LABEL = localize('selectProductIconTheme.label', "Product Icon Theme");
216

217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238
	constructor(
		id: string,
		label: string,
		@IQuickInputService quickInputService: IQuickInputService,
		@IWorkbenchThemeService private readonly themeService: IWorkbenchThemeService,
		@IExtensionGalleryService extensionGalleryService: IExtensionGalleryService,
		@IViewletService viewletService: IViewletService

	) {
		super(id, label, quickInputService, extensionGalleryService, viewletService);
	}

	protected builtInEntry: QuickPickInput<ThemeItem> = { id: DEFAULT_PRODUCT_ICON_THEME_ID, label: localize('defaultProductIconThemeLabel', 'Default') };
	protected installMessage = undefined; //localize('installProductIconThemes', "Install Additional Product Icon Themes...");
	protected placeholderMessage = localize('themes.selectProductIconTheme', "Select Product Icon Theme");
	protected marketplaceTag = 'tag:product-icon-theme';
	protected setTheme(id: string, settingsTarget: ConfigurationTarget | undefined | 'auto') {
		return this.themeService.setProductIconTheme(id, settingsTarget);
	}

	async run(): Promise<void> {
		this.pick(await this.themeService.getProductIconThemes(), this.themeService.getProductIconTheme());
239 240 241
	}
}

242 243
function configurationEntries(extensionGalleryService: IExtensionGalleryService, label: string | undefined): QuickPickInput<ThemeItem>[] {
	if (extensionGalleryService.isEnabled() && label !== undefined) {
C
Christof Marti 已提交
244 245
		return [
			{
C
Christof Marti 已提交
246
				type: 'separator'
C
Christof Marti 已提交
247 248
			},
			{
R
Rob Lourens 已提交
249
				id: undefined,
C
Christof Marti 已提交
250
				label: label,
251
				alwaysShow: true
C
Christof Marti 已提交
252 253
			}
		];
254 255 256 257
	}
	return [];
}

C
Christof Marti 已提交
258 259
function openExtensionViewlet(viewletService: IViewletService, query: string) {
	return viewletService.openViewlet(VIEWLET_ID, true).then(viewlet => {
260
		if (viewlet) {
S
SteVen Batten 已提交
261
			(viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer).search(query);
262 263
			viewlet.focus();
		}
C
Christof Marti 已提交
264 265
	});
}
266 267 268 269 270 271 272 273
interface ThemeItem {
	id: string | undefined;
	label: string;
	description?: string;
	alwaysShow?: boolean;
}

function isItem(i: QuickPickInput<ThemeItem>): i is ThemeItem {
274
	return (<any>i)['type'] !== 'separator';
275
}
C
Christof Marti 已提交
276

277 278
function toEntries(themes: Array<IWorkbenchTheme>, label?: string): QuickPickInput<ThemeItem>[] {
	const toEntry = (theme: IWorkbenchTheme): ThemeItem => ({ id: theme.id, label: theme.label, description: theme.description });
279 280
	const sorter = (t1: ThemeItem, t2: ThemeItem) => t1.label.localeCompare(t2.label);
	let entries: QuickPickInput<ThemeItem>[] = themes.map(toEntry).sort(sorter);
C
Christof Marti 已提交
281 282
	if (entries.length > 0 && label) {
		entries.unshift({ type: 'separator', label });
283 284
	}
	return entries;
285 286
}

287 288
class GenerateColorThemeAction extends Action {

289
	static readonly ID = 'workbench.action.generateColorTheme';
290
	static readonly LABEL = localize('generateColorTheme.label', "Generate Color Theme From Current Settings");
291 292 293 294

	constructor(
		id: string,
		label: string,
295 296
		@IWorkbenchThemeService private readonly themeService: IWorkbenchThemeService,
		@IEditorService private readonly editorService: IEditorService,
297 298 299 300
	) {
		super(id, label);
	}

J
Johannes Rieken 已提交
301
	run(): Promise<any> {
302
		let theme = this.themeService.getColorTheme();
303 304
		let colors = Registry.as<IColorRegistry>(ColorRegistryExtensions.ColorContribution).getColors();
		let colorIds = colors.map(c => c.id).sort();
305
		let resultingColors: { [key: string]: string } = {};
M
Matt Bierner 已提交
306
		let inherited: string[] = [];
307
		for (let colorId of colorIds) {
M
Matt Bierner 已提交
308
			const color = theme.getColor(colorId, false);
309
			if (color) {
310 311 312
				resultingColors[colorId] = Color.Format.CSS.formatHexA(color, true);
			} else {
				inherited.push(colorId);
313
			}
314 315
		}
		for (let id of inherited) {
M
Matt Bierner 已提交
316
			const color = theme.getColor(id);
317 318 319 320
			if (color) {
				resultingColors['__' + id] = Color.Format.CSS.formatHexA(color, true);
			}
		}
321
		let contents = JSON.stringify({
322
			'$schema': colorThemeSchemaId,
323 324
			type: theme.type,
			colors: resultingColors,
325
			tokenColors: theme.tokenColors.filter(t => !!t.scope)
326
		}, null, '\t');
327 328
		contents = contents.replace(/\"__/g, '//"');

329
		return this.editorService.openEditor({ contents, mode: 'jsonc' });
330 331 332
	}
}

J
Joao Moreno 已提交
333
const category = localize('preferences', "Preferences");
334

335
const colorThemeDescriptor = SyncActionDescriptor.from(SelectColorThemeAction, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_T) });
336 337
Registry.as<IWorkbenchActionRegistry>(Extensions.WorkbenchActions).registerWorkbenchAction(colorThemeDescriptor, 'Preferences: Color Theme', category);

338
const fileIconThemeDescriptor = SyncActionDescriptor.from(SelectFileIconThemeAction);
339 340
Registry.as<IWorkbenchActionRegistry>(Extensions.WorkbenchActions).registerWorkbenchAction(fileIconThemeDescriptor, 'Preferences: File Icon Theme', category);

341
const productIconThemeDescriptor = SyncActionDescriptor.from(SelectProductIconThemeAction);
342
Registry.as<IWorkbenchActionRegistry>(Extensions.WorkbenchActions).registerWorkbenchAction(productIconThemeDescriptor, 'Preferences: Product Icon Theme', category);
343 344


B
Benjamin Pasero 已提交
345
const developerCategory = localize({ key: 'developer', comment: ['A developer on Code itself or someone diagnosing issues in Code'] }, "Developer");
346

347
const generateColorThemeDescriptor = SyncActionDescriptor.from(GenerateColorThemeAction);
348
Registry.as<IWorkbenchActionRegistry>(Extensions.WorkbenchActions).registerWorkbenchAction(generateColorThemeDescriptor, 'Developer: Generate Color Theme From Current Settings', developerCategory);
I
isidor 已提交
349 350 351 352 353 354 355 356 357 358 359 360 361

MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, {
	group: '4_themes',
	command: {
		id: SelectColorThemeAction.ID,
		title: localize({ key: 'miSelectColorTheme', comment: ['&& denotes a mnemonic'] }, "&&Color Theme")
	},
	order: 1
});

MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, {
	group: '4_themes',
	command: {
362
		id: SelectFileIconThemeAction.ID,
I
isidor 已提交
363 364 365 366
		title: localize({ key: 'miSelectIconTheme', comment: ['&& denotes a mnemonic'] }, "File &&Icon Theme")
	},
	order: 2
});
367 368 369 370 371 372 373 374 375 376 377 378 379

MenuRegistry.appendMenuItem(MenuId.GlobalActivity, {
	group: '4_themes',
	command: {
		id: SelectColorThemeAction.ID,
		title: localize('selectTheme.label', "Color Theme")
	},
	order: 1
});

MenuRegistry.appendMenuItem(MenuId.GlobalActivity, {
	group: '4_themes',
	command: {
380
		id: SelectFileIconThemeAction.ID,
381 382 383
		title: localize('themes.selectIconTheme.label', "File Icon Theme")
	},
	order: 2
384
});