提交 8d7ef6e8 编写于 作者: M Martin Aeschlimann

adopt product icon theme in extension actions & picker

上级 1ff8393a
......@@ -27,7 +27,7 @@ import { IExtensionsWorkbenchService, IExtensionsViewPaneContainer, VIEWLET_ID,
import { RatingsWidget, InstallCountWidget, RemoteBadgeWidget } from 'vs/workbench/contrib/extensions/browser/extensionsWidgets';
import { EditorOptions } from 'vs/workbench/common/editor';
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
import { CombinedInstallAction, UpdateAction, ExtensionEditorDropDownAction, ReloadAction, MaliciousStatusLabelAction, IgnoreExtensionRecommendationAction, UndoIgnoreExtensionRecommendationAction, EnableDropDownAction, DisableDropDownAction, StatusLabelAction, SetFileIconThemeAction, SetColorThemeAction, RemoteInstallAction, ExtensionToolTipAction, SystemDisabledWarningAction, LocalInstallAction, SyncIgnoredIconAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions';
import { CombinedInstallAction, UpdateAction, ExtensionEditorDropDownAction, ReloadAction, MaliciousStatusLabelAction, IgnoreExtensionRecommendationAction, UndoIgnoreExtensionRecommendationAction, EnableDropDownAction, DisableDropDownAction, StatusLabelAction, SetFileIconThemeAction, SetColorThemeAction, RemoteInstallAction, ExtensionToolTipAction, SystemDisabledWarningAction, LocalInstallAction, SyncIgnoredIconAction, SetProductIconThemeAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
import { IOpenerService, matchesScheme } from 'vs/platform/opener/common/opener';
......@@ -317,8 +317,6 @@ export class ExtensionEditor extends BaseEditor {
private async updateTemplate(input: ExtensionsInput, template: IExtensionEditorTemplate, preserveFocus: boolean): Promise<void> {
const runningExtensions = await this.extensionService.getExtensions();
const colorThemes = await this.workbenchThemeService.getColorThemes();
const fileIconThemes = await this.workbenchThemeService.getFileIconThemes();
this.activeElement = null;
this.editorLoadComplete = false;
......@@ -402,8 +400,10 @@ export class ExtensionEditor extends BaseEditor {
this.instantiationService.createInstance(SyncIgnoredIconAction),
this.instantiationService.createInstance(StatusLabelAction),
this.instantiationService.createInstance(UpdateAction),
this.instantiationService.createInstance(SetColorThemeAction, colorThemes),
this.instantiationService.createInstance(SetFileIconThemeAction, fileIconThemes),
this.instantiationService.createInstance(SetColorThemeAction, await this.workbenchThemeService.getColorThemes()),
this.instantiationService.createInstance(SetFileIconThemeAction, await this.workbenchThemeService.getFileIconThemes()),
this.instantiationService.createInstance(SetProductIconThemeAction, await this.workbenchThemeService.getProductIconThemes()),
this.instantiationService.createInstance(EnableDropDownAction),
this.instantiationService.createInstance(DisableDropDownAction, runningExtensions),
this.instantiationService.createInstance(RemoteInstallAction),
......
......@@ -29,7 +29,7 @@ import { IHostService } from 'vs/workbench/services/host/browser/host';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { URI } from 'vs/base/common/uri';
import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands';
import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { registerThemingParticipant, IColorTheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService';
import { buttonBackground, buttonForeground, buttonHoverBackground, contrastBorder, registerColor, foreground } from 'vs/platform/theme/common/colorRegistry';
import { Color } from 'vs/base/common/color';
......@@ -52,7 +52,7 @@ import { CancellationToken } from 'vs/base/common/cancellation';
import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
import { alert } from 'vs/base/browser/ui/aria/aria';
import { coalesce } from 'vs/base/common/arrays';
import { IWorkbenchThemeService, ThemeSettings, IWorkbenchFileIconTheme, IWorkbenchColorTheme } from 'vs/workbench/services/themes/common/workbenchThemeService';
import { IWorkbenchThemeService, IWorkbenchTheme, IWorkbenchColorTheme, IWorkbenchFileIconTheme, IWorkbenchProductIconTheme } from 'vs/workbench/services/themes/common/workbenchThemeService';
import { ILabelService } from 'vs/platform/label/common/label';
import { prefersExecuteOnUI, prefersExecuteOnWorkspace } from 'vs/workbench/services/extensions/common/extensionsUtil';
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
......@@ -237,17 +237,15 @@ export class InstallAction extends ExtensionAction {
if (extension && extension.local) {
const runningExtension = await this.getRunningExtension(extension.local);
if (runningExtension) {
const colorThemes = await this.workbenchThemeService.getColorThemes();
const fileIconThemes = await this.workbenchThemeService.getFileIconThemes();
if (SetColorThemeAction.getColorThemes(colorThemes, this.extension).length) {
const action = this.instantiationService.createInstance(SetColorThemeAction, colorThemes);
action.extension = extension;
return action.run({ showCurrentTheme: true, ignoreFocusLost: true });
}
if (SetFileIconThemeAction.getFileIconThemes(fileIconThemes, this.extension).length) {
const action = this.instantiationService.createInstance(SetFileIconThemeAction, fileIconThemes);
action.extension = extension;
return action.run({ showCurrentTheme: true, ignoreFocusLost: true });
let action = await SetColorThemeAction.create(this.workbenchThemeService, this.instantiationService, extension)
|| await SetFileIconThemeAction.create(this.workbenchThemeService, this.instantiationService, extension)
|| await SetProductIconThemeAction.create(this.workbenchThemeService, this.instantiationService, extension);
if (action) {
try {
return action.run({ showCurrentTheme: true, ignoreFocusLost: true });
} finally {
action.dispose();
}
}
}
}
......@@ -704,19 +702,22 @@ export class ManageExtensionAction extends ExtensionDropDownAction {
this.update();
}
getActionGroups(runningExtensions: IExtensionDescription[], colorThemes: IWorkbenchColorTheme[], fileIconThemes: IWorkbenchFileIconTheme[]): IAction[][] {
async getActionGroups(runningExtensions: IExtensionDescription[]): Promise<IAction[][]> {
const groups: ExtensionAction[][] = [];
if (this.extension) {
const extensionColorThemes = SetColorThemeAction.getColorThemes(colorThemes, this.extension);
const extensionFileIconThemes = SetFileIconThemeAction.getFileIconThemes(fileIconThemes, this.extension);
if (extensionColorThemes.length || extensionFileIconThemes.length) {
const themesGroup: ExtensionAction[] = [];
if (extensionColorThemes.length) {
themesGroup.push(this.instantiationService.createInstance(SetColorThemeAction, colorThemes));
}
if (extensionFileIconThemes.length) {
themesGroup.push(this.instantiationService.createInstance(SetFileIconThemeAction, fileIconThemes));
const actions = await Promise.all([
SetColorThemeAction.create(this.workbenchThemeService, this.instantiationService, this.extension),
SetFileIconThemeAction.create(this.workbenchThemeService, this.instantiationService, this.extension),
SetProductIconThemeAction.create(this.workbenchThemeService, this.instantiationService, this.extension)
]);
const themesGroup: ExtensionAction[] = [];
for (let action of actions) {
if (action) {
themesGroup.push(action);
}
}
if (themesGroup.length) {
groups.push(themesGroup);
}
}
......@@ -740,9 +741,7 @@ export class ManageExtensionAction extends ExtensionDropDownAction {
async run(): Promise<any> {
const runtimeExtensions = await this.extensionService.getExtensions();
const colorThemes = await this.workbenchThemeService.getColorThemes();
const fileIconThemes = await this.workbenchThemeService.getFileIconThemes();
return super.run({ actionGroups: this.getActionGroups(runtimeExtensions, colorThemes, fileIconThemes), disposeActionsOnHide: true });
return super.run({ actionGroups: await this.getActionGroups(runtimeExtensions), disposeActionsOnHide: true });
}
update(): void {
......@@ -1301,21 +1300,45 @@ export class ReloadAction extends ExtensionAction {
}
}
export class SetColorThemeAction extends ExtensionAction {
function isThemeFromExtension(theme: IWorkbenchTheme, extension: IExtension | undefined | null): boolean {
return !!(extension && theme.extensionData && ExtensionIdentifier.equals(theme.extensionData.extensionId, extension.identifier.id));
}
static getColorThemes(colorThemes: IWorkbenchColorTheme[], extension: IExtension): IWorkbenchColorTheme[] {
return colorThemes.filter(c => c.extensionData && ExtensionIdentifier.equals(c.extensionData.extensionId, extension.identifier.id));
function getQuickPickEntries(themes: IWorkbenchTheme[], currentTheme: IWorkbenchTheme, extension: IExtension | null | undefined, showCurrentTheme: boolean): (IQuickPickItem | IQuickPickSeparator)[] {
const picks: (IQuickPickItem | IQuickPickSeparator)[] = [];
for (const theme of themes) {
if (isThemeFromExtension(theme, extension) && !(showCurrentTheme && theme === currentTheme)) {
picks.push({ label: theme.label, id: theme.id });
}
}
if (showCurrentTheme) {
picks.push(<IQuickPickSeparator>{ type: 'separator', label: localize('current', "Current") });
picks.push(<IQuickPickItem>{ label: currentTheme.label, id: currentTheme.id });
}
return picks;
}
export class SetColorThemeAction extends ExtensionAction {
private static readonly EnabledClass = `${ExtensionAction.LABEL_ACTION_CLASS} theme`;
private static readonly DisabledClass = `${SetColorThemeAction.EnabledClass} disabled`;
static async create(workbenchThemeService: IWorkbenchThemeService, instantiationService: IInstantiationService, extension: IExtension): Promise<SetColorThemeAction | undefined> {
const themes = await workbenchThemeService.getColorThemes();
if (themes.some(th => isThemeFromExtension(th, extension))) {
const action = instantiationService.createInstance(SetColorThemeAction, themes);
action.extension = extension;
return action;
}
return undefined;
}
constructor(
private readonly colorThemes: IWorkbenchColorTheme[],
private colorThemes: IWorkbenchColorTheme[],
@IExtensionService extensionService: IExtensionService,
@IWorkbenchThemeService private readonly workbenchThemeService: IWorkbenchThemeService,
@IQuickInputService private readonly quickInputService: IQuickInputService,
@IConfigurationService private readonly configurationService: IConfigurationService
) {
super(`extensions.colorTheme`, localize('color theme', "Set Color Theme"), SetColorThemeAction.DisabledClass, false);
this._register(Event.any<any>(extensionService.onDidChangeExtensions, workbenchThemeService.onDidColorThemeChange)(() => this.update(), this));
......@@ -1323,36 +1346,21 @@ export class SetColorThemeAction extends ExtensionAction {
}
update(): void {
this.enabled = false;
if (this.extension) {
const isInstalled = this.extension.state === ExtensionState.Installed;
if (isInstalled) {
const extensionThemes = SetColorThemeAction.getColorThemes(this.colorThemes, this.extension);
this.enabled = extensionThemes.length > 0;
}
}
this.enabled = !!this.extension && (this.extension.state === ExtensionState.Installed) && this.colorThemes.some(th => isThemeFromExtension(th, this.extension));
this.class = this.enabled ? SetColorThemeAction.EnabledClass : SetColorThemeAction.DisabledClass;
}
async run({ showCurrentTheme, ignoreFocusLost }: { showCurrentTheme: boolean, ignoreFocusLost: boolean } = { showCurrentTheme: false, ignoreFocusLost: false }): Promise<any> {
this.colorThemes = await this.workbenchThemeService.getColorThemes();
this.update();
if (!this.enabled) {
return;
}
let extensionThemes = SetColorThemeAction.getColorThemes(this.colorThemes, this.extension!);
const currentTheme = this.colorThemes.filter(t => t.settingsId === this.configurationService.getValue(ThemeSettings.COLOR_THEME))[0] || this.workbenchThemeService.getColorTheme();
showCurrentTheme = showCurrentTheme || extensionThemes.some(t => t.id === currentTheme.id);
if (showCurrentTheme) {
extensionThemes = extensionThemes.filter(t => t.id !== currentTheme.id);
}
const currentTheme = this.workbenchThemeService.getColorTheme();
const delayer = new Delayer<any>(100);
const picks: (IQuickPickItem | IQuickPickSeparator)[] = [];
picks.push(...extensionThemes.map(theme => (<IQuickPickItem>{ label: theme.label, id: theme.id })));
if (showCurrentTheme) {
picks.push(<IQuickPickSeparator>{ type: 'separator', label: localize('current', "Current") });
picks.push(<IQuickPickItem>{ label: currentTheme.label, id: currentTheme.id });
}
const picks = getQuickPickEntries(this.colorThemes, currentTheme, this.extension, showCurrentTheme);
const pickedTheme = await this.quickInputService.pick(
picks,
{
......@@ -1360,9 +1368,7 @@ export class SetColorThemeAction extends ExtensionAction {
onDidFocus: item => delayer.trigger(() => this.workbenchThemeService.setColorTheme(item.id, undefined)),
ignoreFocusLost
});
let confValue = this.configurationService.inspect(ThemeSettings.COLOR_THEME);
const target = typeof confValue.workspaceValue !== 'undefined' ? ConfigurationTarget.WORKSPACE : ConfigurationTarget.USER;
return this.workbenchThemeService.setColorTheme(pickedTheme ? pickedTheme.id : currentTheme.id, target);
return this.workbenchThemeService.setColorTheme(pickedTheme ? pickedTheme.id : currentTheme.id, 'auto');
}
}
......@@ -1371,16 +1377,21 @@ export class SetFileIconThemeAction extends ExtensionAction {
private static readonly EnabledClass = `${ExtensionAction.LABEL_ACTION_CLASS} theme`;
private static readonly DisabledClass = `${SetFileIconThemeAction.EnabledClass} disabled`;
static getFileIconThemes(fileIconThemes: IWorkbenchFileIconTheme[], extension: IExtension): IWorkbenchFileIconTheme[] {
return fileIconThemes.filter(c => c.extensionData && ExtensionIdentifier.equals(c.extensionData.extensionId, extension.identifier.id));
static async create(workbenchThemeService: IWorkbenchThemeService, instantiationService: IInstantiationService, extension: IExtension): Promise<SetFileIconThemeAction | undefined> {
const themes = await workbenchThemeService.getFileIconThemes();
if (themes.some(th => isThemeFromExtension(th, extension))) {
const action = instantiationService.createInstance(SetFileIconThemeAction, themes);
action.extension = extension;
return action;
}
return undefined;
}
constructor(
private readonly fileIconThemes: IWorkbenchFileIconTheme[],
private fileIconThemes: IWorkbenchFileIconTheme[],
@IExtensionService extensionService: IExtensionService,
@IWorkbenchThemeService private readonly workbenchThemeService: IWorkbenchThemeService,
@IQuickInputService private readonly quickInputService: IQuickInputService,
@IConfigurationService private readonly configurationService: IConfigurationService
@IQuickInputService private readonly quickInputService: IQuickInputService
) {
super(`extensions.fileIconTheme`, localize('file icon theme', "Set File Icon Theme"), SetFileIconThemeAction.DisabledClass, false);
this._register(Event.any<any>(extensionService.onDidChangeExtensions, workbenchThemeService.onDidFileIconThemeChange)(() => this.update(), this));
......@@ -1388,36 +1399,20 @@ export class SetFileIconThemeAction extends ExtensionAction {
}
update(): void {
this.enabled = false;
if (this.extension) {
const isInstalled = this.extension.state === ExtensionState.Installed;
if (isInstalled) {
const extensionThemes = SetFileIconThemeAction.getFileIconThemes(this.fileIconThemes, this.extension);
this.enabled = extensionThemes.length > 0;
}
}
this.enabled = !!this.extension && (this.extension.state === ExtensionState.Installed) && this.fileIconThemes.some(th => isThemeFromExtension(th, this.extension));
this.class = this.enabled ? SetFileIconThemeAction.EnabledClass : SetFileIconThemeAction.DisabledClass;
}
async run({ showCurrentTheme, ignoreFocusLost }: { showCurrentTheme: boolean, ignoreFocusLost: boolean } = { showCurrentTheme: false, ignoreFocusLost: false }): Promise<any> {
await this.update();
this.fileIconThemes = await this.workbenchThemeService.getFileIconThemes();
this.update();
if (!this.enabled) {
return;
}
let extensionThemes = SetFileIconThemeAction.getFileIconThemes(this.fileIconThemes, this.extension!);
const currentTheme = this.fileIconThemes.filter(t => t.settingsId === this.configurationService.getValue(ThemeSettings.ICON_THEME))[0] || this.workbenchThemeService.getFileIconTheme();
showCurrentTheme = showCurrentTheme || extensionThemes.some(t => t.id === currentTheme.id);
if (showCurrentTheme) {
extensionThemes = extensionThemes.filter(t => t.id !== currentTheme.id);
}
const currentTheme = this.workbenchThemeService.getFileIconTheme();
const delayer = new Delayer<any>(100);
const picks: (IQuickPickItem | IQuickPickSeparator)[] = [];
picks.push(...extensionThemes.map(theme => (<IQuickPickItem>{ label: theme.label, id: theme.id })));
if (showCurrentTheme && currentTheme.label) {
picks.push(<IQuickPickSeparator>{ type: 'separator', label: localize('current', "Current") });
picks.push(<IQuickPickItem>{ label: currentTheme.label, id: currentTheme.id });
}
const picks = getQuickPickEntries(this.fileIconThemes, currentTheme, this.extension, showCurrentTheme);
const pickedTheme = await this.quickInputService.pick(
picks,
{
......@@ -1425,9 +1420,62 @@ export class SetFileIconThemeAction extends ExtensionAction {
onDidFocus: item => delayer.trigger(() => this.workbenchThemeService.setFileIconTheme(item.id, undefined)),
ignoreFocusLost
});
let confValue = this.configurationService.inspect(ThemeSettings.ICON_THEME);
const target = typeof confValue.workspaceValue !== 'undefined' ? ConfigurationTarget.WORKSPACE : ConfigurationTarget.USER;
return this.workbenchThemeService.setFileIconTheme(pickedTheme ? pickedTheme.id : currentTheme.id, target);
return this.workbenchThemeService.setFileIconTheme(pickedTheme ? pickedTheme.id : currentTheme.id, 'auto');
}
}
export class SetProductIconThemeAction extends ExtensionAction {
private static readonly EnabledClass = `${ExtensionAction.LABEL_ACTION_CLASS} theme`;
private static readonly DisabledClass = `${SetProductIconThemeAction.EnabledClass} disabled`;
static async create(workbenchThemeService: IWorkbenchThemeService, instantiationService: IInstantiationService, extension: IExtension): Promise<SetProductIconThemeAction | undefined> {
const themes = await workbenchThemeService.getProductIconThemes();
if (themes.some(th => isThemeFromExtension(th, extension))) {
const action = instantiationService.createInstance(SetProductIconThemeAction, themes);
action.extension = extension;
return action;
}
return undefined;
}
constructor(
private productIconThemes: IWorkbenchProductIconTheme[],
@IExtensionService extensionService: IExtensionService,
@IWorkbenchThemeService private readonly workbenchThemeService: IWorkbenchThemeService,
@IQuickInputService private readonly quickInputService: IQuickInputService
) {
super(`extensions.productIconTheme`, localize('product icon theme', "Set Product Icon Theme"), SetProductIconThemeAction.DisabledClass, false);
this._register(Event.any<any>(extensionService.onDidChangeExtensions, workbenchThemeService.onDidProductIconThemeChange)(() => this.update(), this));
this.enabled = true; // enabled by default
this.class = SetProductIconThemeAction.EnabledClass;
// this.update();
}
update(): void {
this.enabled = !!this.extension && (this.extension.state === ExtensionState.Installed) && this.productIconThemes.some(th => isThemeFromExtension(th, this.extension));
this.class = this.enabled ? SetProductIconThemeAction.EnabledClass : SetProductIconThemeAction.DisabledClass;
}
async run({ showCurrentTheme, ignoreFocusLost }: { showCurrentTheme: boolean, ignoreFocusLost: boolean } = { showCurrentTheme: false, ignoreFocusLost: false }): Promise<any> {
this.productIconThemes = await this.workbenchThemeService.getProductIconThemes();
this.update();
if (!this.enabled) {
return;
}
const currentTheme = this.workbenchThemeService.getProductIconTheme();
const delayer = new Delayer<any>(100);
const picks = getQuickPickEntries(this.productIconThemes, currentTheme, this.extension, showCurrentTheme);
const pickedTheme = await this.quickInputService.pick(
picks,
{
placeHolder: localize('select product icon theme', "Select Product Icon Theme"),
onDidFocus: item => delayer.trigger(() => this.workbenchThemeService.setProductIconTheme(item.id, undefined)),
ignoreFocusLost
});
return this.workbenchThemeService.setProductIconTheme(pickedTheme ? pickedTheme.id : currentTheme.id, 'auto');
}
}
......
......@@ -41,7 +41,6 @@ import { createErrorWithActions } from 'vs/base/common/errorsWithActions';
import { CancellationToken } from 'vs/base/common/cancellation';
import { IAction, Action } from 'vs/base/common/actions';
import { ExtensionType, ExtensionIdentifier, IExtensionDescription, isLanguagePackExtension } from 'vs/platform/extensions/common/extensions';
import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async';
import { IProductService } from 'vs/platform/product/common/productService';
import { SeverityIcon } from 'vs/platform/severityIcon/common/severityIcon';
......@@ -106,7 +105,6 @@ export class ExtensionsListView extends ViewPane {
@IConfigurationService configurationService: IConfigurationService,
@IWorkspaceContextService protected contextService: IWorkspaceContextService,
@IExperimentService private readonly experimentService: IExperimentService,
@IWorkbenchThemeService private readonly workbenchThemeService: IWorkbenchThemeService,
@IExtensionManagementServerService protected readonly extensionManagementServerService: IExtensionManagementServerService,
@IProductService protected readonly productService: IProductService,
@IContextKeyService contextKeyService: IContextKeyService,
......@@ -233,12 +231,10 @@ export class ExtensionsListView extends ViewPane {
private async onContextMenu(e: IListContextMenuEvent<IExtension>): Promise<void> {
if (e.element) {
const runningExtensions = await this.extensionService.getExtensions();
const colorThemes = await this.workbenchThemeService.getColorThemes();
const fileIconThemes = await this.workbenchThemeService.getFileIconThemes();
const manageExtensionAction = this.instantiationService.createInstance(ManageExtensionAction);
manageExtensionAction.extension = e.element;
if (manageExtensionAction.enabled) {
const groups = manageExtensionAction.getActionGroups(runningExtensions, colorThemes, fileIconThemes);
const groups = await manageExtensionAction.getActionGroups(runningExtensions);
let actions: IAction[] = [];
for (const menuActions of groups) {
actions = [...actions, ...menuActions, new Separator()];
......@@ -882,7 +878,6 @@ export class ServerExtensionsView extends ExtensionsListView {
@IConfigurationService configurationService: IConfigurationService,
@IWorkspaceContextService contextService: IWorkspaceContextService,
@IExperimentService experimentService: IExperimentService,
@IWorkbenchThemeService workbenchThemeService: IWorkbenchThemeService,
@IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService,
@IExtensionManagementServerService extensionManagementServerService: IExtensionManagementServerService,
@IProductService productService: IProductService,
......@@ -893,7 +888,7 @@ export class ServerExtensionsView extends ExtensionsListView {
@IPreferencesService preferencesService: IPreferencesService,
) {
options.server = server;
super(options, notificationService, keybindingService, contextMenuService, instantiationService, themeService, extensionService, extensionsWorkbenchService, editorService, tipsService, telemetryService, configurationService, contextService, experimentService, workbenchThemeService, extensionManagementServerService, productService, contextKeyService, viewDescriptorService, menuService, openerService, preferencesService);
super(options, notificationService, keybindingService, contextMenuService, instantiationService, themeService, extensionService, extensionsWorkbenchService, editorService, tipsService, telemetryService, configurationService, contextService, experimentService, extensionManagementServerService, productService, contextKeyService, viewDescriptorService, menuService, openerService, preferencesService);
this._register(onDidChangeTitle(title => this.updateTitle(title)));
}
......
......@@ -10,18 +10,19 @@ import { KeyMod, KeyChord, KeyCode } from 'vs/base/common/keyCodes';
import { SyncActionDescriptor, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions';
import { Registry } from 'vs/platform/registry/common/platform';
import { IWorkbenchActionRegistry, Extensions } from 'vs/workbench/common/actions';
import { IWorkbenchThemeService, ThemeSettings, IWorkbenchColorTheme, IWorkbenchFileIconTheme } from 'vs/workbench/services/themes/common/workbenchThemeService';
import { IWorkbenchThemeService, IWorkbenchTheme } from 'vs/workbench/services/themes/common/workbenchThemeService';
import { VIEWLET_ID, IExtensionsViewPaneContainer } from 'vs/workbench/contrib/extensions/common/extensions';
import { IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement';
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
import { IColorRegistry, Extensions as ColorRegistryExtensions } from 'vs/platform/theme/common/colorRegistry';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { Color } from 'vs/base/common/color';
import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { LIGHT, DARK, HIGH_CONTRAST } from 'vs/platform/theme/common/themeService';
import { colorThemeSchemaId } from 'vs/workbench/services/themes/common/colorThemeSchema';
import { onUnexpectedError } from 'vs/base/common/errors';
import { IQuickInputService, QuickPickInput } from 'vs/platform/quickinput/common/quickInput';
import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
import { DEFAULT_PRODUCT_ICON_THEME_ID } from 'vs/workbench/services/themes/browser/productIconThemeData';
export class SelectColorThemeAction extends Action {
......@@ -34,8 +35,7 @@ export class SelectColorThemeAction extends Action {
@IQuickInputService private readonly quickInputService: IQuickInputService,
@IWorkbenchThemeService private readonly themeService: IWorkbenchThemeService,
@IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService,
@IViewletService private readonly viewletService: IViewletService,
@IConfigurationService private readonly configurationService: IConfigurationService
@IViewletService private readonly viewletService: IViewletService
) {
super(id, label);
}
......@@ -60,13 +60,8 @@ export class SelectColorThemeAction extends Action {
selectThemeTimeout = window.setTimeout(() => {
selectThemeTimeout = undefined;
const themeId = theme && theme.id !== undefined ? theme.id : currentTheme.id;
let target: ConfigurationTarget | undefined = undefined;
if (applyTheme) {
const confValue = this.configurationService.inspect(ThemeSettings.COLOR_THEME);
target = typeof confValue.workspaceValue !== 'undefined' ? ConfigurationTarget.WORKSPACE : ConfigurationTarget.USER;
}
this.themeService.setColorTheme(themeId, target).then(undefined,
this.themeService.setColorTheme(themeId, applyTheme ? 'auto' : undefined).then(undefined,
err => {
onUnexpectedError(err);
this.themeService.setColorTheme(currentTheme.id, undefined);
......@@ -108,7 +103,83 @@ export class SelectColorThemeAction extends Action {
}
}
class SelectIconThemeAction extends Action {
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 {
static readonly ID = 'workbench.action.selectIconTheme';
static readonly LABEL = localize('selectIconTheme.label', "File Icon Theme");
......@@ -116,84 +187,61 @@ class SelectIconThemeAction extends Action {
constructor(
id: string,
label: string,
@IQuickInputService private readonly quickInputService: IQuickInputService,
@IQuickInputService quickInputService: IQuickInputService,
@IWorkbenchThemeService private readonly themeService: IWorkbenchThemeService,
@IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService,
@IViewletService private readonly viewletService: IViewletService,
@IConfigurationService private readonly configurationService: IConfigurationService
@IExtensionGalleryService extensionGalleryService: IExtensionGalleryService,
@IViewletService viewletService: IViewletService
) {
super(id, label);
super(id, label, quickInputService, extensionGalleryService, viewletService);
}
run(): Promise<void> {
return this.themeService.getFileIconThemes().then(themes => {
const currentTheme = this.themeService.getFileIconTheme();
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);
}
let picks: QuickPickInput<ThemeItem>[] = [{ id: '', label: localize('noIconThemeLabel', 'None'), description: localize('noIconThemeDesc', 'Disable file icons') }];
picks = picks.concat(
toEntries(themes),
configurationEntries(this.extensionGalleryService, localize('installIconThemes', "Install Additional File Icon Themes..."))
);
async run(): Promise<void> {
this.pick(await this.themeService.getFileIconThemes(), this.themeService.getFileIconTheme());
}
}
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;
let target: ConfigurationTarget | undefined = undefined;
if (applyTheme) {
const confValue = this.configurationService.inspect(ThemeSettings.ICON_THEME);
target = typeof confValue.workspaceValue !== 'undefined' ? ConfigurationTarget.WORKSPACE : ConfigurationTarget.USER;
}
this.themeService.setFileIconTheme(themeId, target).then(undefined,
err => {
onUnexpectedError(err);
this.themeService.setFileIconTheme(currentTheme.id, undefined);
}
);
}, applyTheme ? 0 : 200);
};
class SelectProductIconThemeAction extends AbstractIconThemeAction {
return new Promise((s, _) => {
let isCompleted = false;
static readonly ID = 'workbench.action.selectProductIconTheme';
static readonly LABEL = localize('selectProductIconTheme.label', "Product Icon Theme");
const autoFocusIndex = firstIndex(picks, p => isItem(p) && p.id === currentTheme.id);
const quickpick = this.quickInputService.createQuickPick<ThemeItem>();
quickpick.items = picks;
quickpick.placeholder = localize('themes.selectIconTheme', "Select File Icon Theme");
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, `tag:icon-theme ${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();
});
});
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());
}
}
function configurationEntries(extensionGalleryService: IExtensionGalleryService, label: string): QuickPickInput<ThemeItem>[] {
if (extensionGalleryService.isEnabled()) {
function configurationEntries(extensionGalleryService: IExtensionGalleryService, label: string | undefined): QuickPickInput<ThemeItem>[] {
if (extensionGalleryService.isEnabled() && label !== undefined) {
return [
{
type: 'separator'
......@@ -227,8 +275,8 @@ function isItem(i: QuickPickInput<ThemeItem>): i is ThemeItem {
return (<any>i)['type'] !== 'separator';
}
function toEntries(themes: Array<IWorkbenchColorTheme | IWorkbenchFileIconTheme>, label?: string): QuickPickInput<ThemeItem>[] {
const toEntry = (theme: IWorkbenchColorTheme | IWorkbenchFileIconTheme): ThemeItem => ({ id: theme.id, label: theme.label, description: theme.description });
function toEntries(themes: Array<IWorkbenchTheme>, label?: string): QuickPickInput<ThemeItem>[] {
const toEntry = (theme: IWorkbenchTheme): ThemeItem => ({ id: theme.id, label: theme.label, description: theme.description });
const sorter = (t1: ThemeItem, t2: ThemeItem) => t1.label.localeCompare(t2.label);
let entries: QuickPickInput<ThemeItem>[] = themes.map(toEntry).sort(sorter);
if (entries.length > 0 && label) {
......@@ -288,8 +336,11 @@ const category = localize('preferences', "Preferences");
const colorThemeDescriptor = SyncActionDescriptor.create(SelectColorThemeAction, SelectColorThemeAction.ID, SelectColorThemeAction.LABEL, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_T) });
Registry.as<IWorkbenchActionRegistry>(Extensions.WorkbenchActions).registerWorkbenchAction(colorThemeDescriptor, 'Preferences: Color Theme', category);
const iconThemeDescriptor = SyncActionDescriptor.create(SelectIconThemeAction, SelectIconThemeAction.ID, SelectIconThemeAction.LABEL);
Registry.as<IWorkbenchActionRegistry>(Extensions.WorkbenchActions).registerWorkbenchAction(iconThemeDescriptor, 'Preferences: File Icon Theme', category);
const fileIconThemeDescriptor = SyncActionDescriptor.create(SelectFileIconThemeAction, SelectFileIconThemeAction.ID, SelectFileIconThemeAction.LABEL);
Registry.as<IWorkbenchActionRegistry>(Extensions.WorkbenchActions).registerWorkbenchAction(fileIconThemeDescriptor, 'Preferences: File Icon Theme', category);
const productIconThemeDescriptor = SyncActionDescriptor.create(SelectProductIconThemeAction, SelectProductIconThemeAction.ID, SelectProductIconThemeAction.LABEL);
Registry.as<IWorkbenchActionRegistry>(Extensions.WorkbenchActions).registerWorkbenchAction(productIconThemeDescriptor, 'Preferences: Product Icon Theme', category);
const developerCategory = localize('developer', "Developer");
......@@ -309,7 +360,7 @@ MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, {
MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, {
group: '4_themes',
command: {
id: SelectIconThemeAction.ID,
id: SelectFileIconThemeAction.ID,
title: localize({ key: 'miSelectIconTheme', comment: ['&& denotes a mnemonic'] }, "File &&Icon Theme")
},
order: 2
......@@ -327,7 +378,7 @@ MenuRegistry.appendMenuItem(MenuId.GlobalActivity, {
MenuRegistry.appendMenuItem(MenuId.GlobalActivity, {
group: '4_themes',
command: {
id: SelectIconThemeAction.ID,
id: SelectFileIconThemeAction.ID,
title: localize('themes.selectIconTheme.label', "File Icon Theme")
},
order: 2
......
......@@ -86,7 +86,7 @@ export class ProductIconThemeData implements IWorkbenchProductIconTheme {
static get defaultTheme(): ProductIconThemeData {
let themeData = ProductIconThemeData._defaultProductIconTheme;
if (!themeData) {
themeData = ProductIconThemeData._defaultProductIconTheme = new ProductIconThemeData(DEFAULT_PRODUCT_ICON_THEME_ID, nls.localize('defaultTheme', 'Default theme'), DEFAULT_PRODUCT_ICON_THEME_SETTING_VALUE);
themeData = ProductIconThemeData._defaultProductIconTheme = new ProductIconThemeData(DEFAULT_PRODUCT_ICON_THEME_ID, nls.localize('defaultTheme', 'Default'), DEFAULT_PRODUCT_ICON_THEME_SETTING_VALUE);
themeData.isLoaded = true;
themeData.extensionData = undefined;
themeData.watch = false;
......
......@@ -263,7 +263,7 @@ export class WorkbenchThemeService implements IWorkbenchThemeService {
if (e.affectsConfiguration(ThemeSettings.PREFERRED_HC_THEME) && this.getPreferredColorScheme() === HIGH_CONTRAST) {
this.applyPreferredColorTheme(HIGH_CONTRAST);
}
if (e.affectsConfiguration(ThemeSettings.ICON_THEME)) {
if (e.affectsConfiguration(ThemeSettings.FILE_ICON_THEME)) {
this.restoreFileIconTheme();
}
if (e.affectsConfiguration(ThemeSettings.PRODUCT_ICON_THEME)) {
......
......@@ -78,11 +78,10 @@ const colorCustomizationsSchema: IConfigurationPropertySchema = {
}
}]
};
const fileIconThemeSettingSchema: IConfigurationPropertySchema = {
type: ['string', 'null'],
default: DEFAULT_FILE_ICON_THEME_SETTING_VALUE,
description: nls.localize('iconTheme', "Specifies the icon theme used in the workbench or 'null' to not show any file icons."),
description: nls.localize('iconTheme', "Specifies the file icon theme used in the workbench or 'null' to not show any file icons."),
enum: [null],
enumDescriptions: [nls.localize('noIconThemeDesc', 'No file icons')],
errorMessage: nls.localize('iconThemeError', "File icon theme is unknown or not installed.")
......@@ -106,7 +105,7 @@ const themeSettingsConfiguration: IConfigurationNode = {
[ThemeSettings.PREFERRED_LIGHT_THEME]: preferredLightThemeSettingSchema,
[ThemeSettings.PREFERRED_HC_THEME]: preferredHCThemeSettingSchema,
[ThemeSettings.DETECT_COLOR_SCHEME]: detectColorSchemeSettingSchema,
[ThemeSettings.ICON_THEME]: fileIconThemeSettingSchema,
[ThemeSettings.FILE_ICON_THEME]: fileIconThemeSettingSchema,
[ThemeSettings.COLOR_CUSTOMIZATIONS]: colorCustomizationsSchema,
[ThemeSettings.PRODUCT_ICON_THEME]: productIconThemeSettingSchema
}
......@@ -212,7 +211,7 @@ export class ThemeConfiguration {
}
public get fileIconTheme(): string | null {
return this.configurationService.getValue<string | null>(ThemeSettings.ICON_THEME);
return this.configurationService.getValue<string | null>(ThemeSettings.FILE_ICON_THEME);
}
public get productIconTheme(): string {
......@@ -237,7 +236,7 @@ export class ThemeConfiguration {
}
public async setFileIconTheme(theme: IWorkbenchFileIconTheme, settingsTarget: ConfigurationTarget | undefined | 'auto'): Promise<IWorkbenchFileIconTheme> {
await this.writeConfiguration(ThemeSettings.ICON_THEME, theme.settingsId, settingsTarget);
await this.writeConfiguration(ThemeSettings.FILE_ICON_THEME, theme.settingsId, settingsTarget);
return theme;
}
......@@ -257,6 +256,8 @@ export class ThemeConfiguration {
settingsTarget = ConfigurationTarget.WORKSPACE_FOLDER;
} else if (!types.isUndefined(settings.workspaceValue)) {
settingsTarget = ConfigurationTarget.WORKSPACE;
} else if (!types.isUndefined(settings.userRemote)) {
settingsTarget = ConfigurationTarget.USER_REMOTE;
} else {
settingsTarget = ConfigurationTarget.USER;
}
......@@ -271,12 +272,11 @@ export class ThemeConfiguration {
}
value = undefined; // remove configuration from user settings
}
} else if (settingsTarget === ConfigurationTarget.WORKSPACE || settingsTarget === ConfigurationTarget.WORKSPACE_FOLDER) {
} else if (settingsTarget === ConfigurationTarget.WORKSPACE || settingsTarget === ConfigurationTarget.WORKSPACE_FOLDER || settingsTarget === ConfigurationTarget.USER_REMOTE) {
if (value === settings.value) {
return Promise.resolve(undefined); // nothing to do
}
}
return this.configurationService.updateValue(key, value, settingsTarget);
}
}
......@@ -20,7 +20,8 @@ export const HC_THEME_ID = 'Default High Contrast';
export enum ThemeSettings {
COLOR_THEME = 'workbench.colorTheme',
ICON_THEME = 'workbench.iconTheme',
FILE_ICON_THEME = 'workbench.iconTheme',
PRODUCT_ICON_THEME = 'workbench.productIconTheme',
COLOR_CUSTOMIZATIONS = 'workbench.colorCustomizations',
TOKEN_COLOR_CUSTOMIZATIONS = 'editor.tokenColorCustomizations',
TOKEN_COLOR_CUSTOMIZATIONS_EXPERIMENTAL = 'editor.tokenColorCustomizationsExperimental',
......@@ -29,18 +30,19 @@ export enum ThemeSettings {
PREFERRED_LIGHT_THEME = 'workbench.preferredLightColorTheme',
PREFERRED_HC_THEME = 'workbench.preferredHighContrastColorTheme',
DETECT_COLOR_SCHEME = 'window.autoDetectColorScheme',
DETECT_HC = 'window.autoDetectHighContrast',
PRODUCT_ICON_THEME = 'workbench.productIconTheme'
DETECT_HC = 'window.autoDetectHighContrast'
}
export interface IWorkbenchColorTheme extends IColorTheme {
export interface IWorkbenchTheme {
readonly id: string;
readonly label: string;
readonly settingsId: string;
readonly extensionData?: ExtensionData;
readonly description?: string;
readonly isLoaded: boolean;
readonly settingsId: string | null;
}
export interface IWorkbenchColorTheme extends IWorkbenchTheme, IColorTheme {
readonly settingsId: string;
readonly tokenColors: ITextMateThemingRule[];
}
......@@ -48,44 +50,28 @@ export interface IColorMap {
[id: string]: Color;
}
export interface IWorkbenchFileIconTheme extends IFileIconTheme {
readonly id: string;
readonly label: string;
readonly settingsId: string | null;
readonly description?: string;
readonly extensionData?: ExtensionData;
readonly isLoaded: boolean;
readonly hasFileIcons: boolean;
readonly hasFolderIcons: boolean;
readonly hidesExplorerArrows: boolean;
export interface IWorkbenchFileIconTheme extends IWorkbenchTheme, IFileIconTheme {
}
export interface IWorkbenchProductIconTheme {
readonly id: string;
readonly label: string;
export interface IWorkbenchProductIconTheme extends IWorkbenchTheme {
readonly settingsId: string;
readonly description?: string;
readonly extensionData?: ExtensionData;
readonly isLoaded: boolean;
}
export interface IWorkbenchThemeService extends IThemeService {
_serviceBrand: undefined;
setColorTheme(themeId: string | undefined, settingsTarget: ConfigurationTarget | undefined): Promise<IWorkbenchColorTheme | null>;
setColorTheme(themeId: string | undefined, settingsTarget: ConfigurationTarget | undefined | 'auto'): Promise<IWorkbenchColorTheme | null>;
getColorTheme(): IWorkbenchColorTheme;
getColorThemes(): Promise<IWorkbenchColorTheme[]>;
onDidColorThemeChange: Event<IWorkbenchColorTheme>;
restoreColorTheme(): void;
setFileIconTheme(iconThemeId: string | undefined, settingsTarget: ConfigurationTarget | undefined): Promise<IWorkbenchFileIconTheme>;
setFileIconTheme(iconThemeId: string | undefined, settingsTarget: ConfigurationTarget | undefined | 'auto'): Promise<IWorkbenchFileIconTheme>;
getFileIconTheme(): IWorkbenchFileIconTheme;
getFileIconThemes(): Promise<IWorkbenchFileIconTheme[]>;
onDidFileIconThemeChange: Event<IWorkbenchFileIconTheme>;
setProductIconTheme(iconThemeId: string | undefined, settingsTarget: ConfigurationTarget | undefined): Promise<IWorkbenchProductIconTheme>;
setProductIconTheme(iconThemeId: string | undefined, settingsTarget: ConfigurationTarget | undefined | 'auto'): Promise<IWorkbenchProductIconTheme>;
getProductIconTheme(): IWorkbenchProductIconTheme;
getProductIconThemes(): Promise<IWorkbenchProductIconTheme[]>;
onDidProductIconThemeChange: Event<IWorkbenchProductIconTheme>;
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册