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

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

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

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

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

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

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

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

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

73 74 75 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 101
			return new Promise((s, _) => {
				let isCompleted = false;

				const autoFocusIndex = firstIndex(picks, p => isItem(p) && p.id === currentTheme.id);
				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 已提交
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 149 150 151 152 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 182
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);
		};

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

			const autoFocusIndex = firstIndex(picks, p => isItem(p) && p.id === currentTheme.id);
			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 {
183

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

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

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

199 200 201 202 203 204 205
	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);
	}
206

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

212

213
class SelectProductIconThemeAction extends AbstractIconThemeAction {
214

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

218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239
	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());
240 241 242
	}
}

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

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

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

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

288 289
class GenerateColorThemeAction extends Action {

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

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

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

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

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

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

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

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


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

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

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: {
363
		id: SelectFileIconThemeAction.ID,
I
isidor 已提交
364 365 366 367
		title: localize({ key: 'miSelectIconTheme', comment: ['&& denotes a mnemonic'] }, "File &&Icon Theme")
	},
	order: 2
});
368 369 370 371 372 373 374 375 376 377 378 379 380

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: {
381
		id: SelectFileIconThemeAction.ID,
382 383 384
		title: localize('themes.selectIconTheme.label', "File Icon Theme")
	},
	order: 2
385
});