Merge pull request #36232 from Microsoft/isidorn/compositeBar

Composite Bar
This commit is contained in:
Isidor Nikolic 2017-10-16 11:08:42 +02:00 committed by GitHub
commit ff87265b2c
4 changed files with 981 additions and 888 deletions

View file

@ -9,72 +9,20 @@ import 'vs/css!./media/activityaction';
import nls = require('vs/nls');
import DOM = require('vs/base/browser/dom');
import { TPromise } from 'vs/base/common/winjs.base';
import { Builder, $ } from 'vs/base/browser/builder';
import { DelayedDragHandler } from 'vs/base/browser/dnd';
import { Action } from 'vs/base/common/actions';
import { BaseActionItem, Separator, IBaseActionItemOptions } from 'vs/base/browser/ui/actionbar/actionbar';
import { IActivityBarService, ProgressBadge, TextBadge, NumberBadge, IconBadge, IBadge } from 'vs/workbench/services/activity/common/activityBarService';
import Event, { Emitter } from 'vs/base/common/event';
import { IActivityBarService } from 'vs/workbench/services/activity/common/activityBarService';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { ViewletDescriptor } from 'vs/workbench/browser/viewlet';
import { IActivity, IGlobalActivity } from 'vs/workbench/common/activity';
import { dispose } from 'vs/base/common/lifecycle';
import { IViewletService, } from 'vs/workbench/services/viewlet/browser/viewlet';
import { IPartService, Parts } from 'vs/workbench/services/part/common/partService';
import { IThemeService, ITheme, registerThemingParticipant, ICssStyleCollector } from 'vs/platform/theme/common/themeService';
import { ACTIVITY_BAR_BADGE_FOREGROUND, ACTIVITY_BAR_BADGE_BACKGROUND, ACTIVITY_BAR_DRAG_AND_DROP_BACKGROUND, ACTIVITY_BAR_FOREGROUND } from 'vs/workbench/common/theme';
import { contrastBorder, activeContrastBorder, focusBorder } from 'vs/platform/theme/common/colorRegistry';
import { activeContrastBorder, focusBorder } from 'vs/platform/theme/common/colorRegistry';
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
import { KeyCode } from 'vs/base/common/keyCodes';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
export interface IViewletActivity {
badge: IBadge;
clazz: string;
}
export class ActivityAction extends Action {
private badge: IBadge;
private _onDidChangeBadge = new Emitter<this>();
constructor(private _activity: IActivity) {
super(_activity.id, _activity.name, _activity.cssClass);
this.badge = null;
}
public get activity(): IActivity {
return this._activity;
}
public get onDidChangeBadge(): Event<this> {
return this._onDidChangeBadge.event;
}
public activate(): void {
if (!this.checked) {
this._setChecked(true);
}
}
public deactivate(): void {
if (this.checked) {
this._setChecked(false);
}
}
public getBadge(): IBadge {
return this.badge;
}
public setBadge(badge: IBadge): void {
this.badge = badge;
this._onDidChangeBadge.fire(this);
}
}
import { ActivityAction, ActivityActionItem } from 'vs/workbench/browser/parts/compositebar/compositeBarActions';
export class ViewletActivityAction extends ActivityAction {
@ -83,15 +31,11 @@ export class ViewletActivityAction extends ActivityAction {
private lastRun: number = 0;
constructor(
private viewlet: ViewletDescriptor,
activity: IActivity,
@IViewletService private viewletService: IViewletService,
@IPartService private partService: IPartService
) {
super(viewlet);
}
public get descriptor(): ViewletDescriptor {
return this.viewlet;
super(activity);
}
public run(event: any): TPromise<any> {
@ -110,495 +54,15 @@ export class ViewletActivityAction extends ActivityAction {
const activeViewlet = this.viewletService.getActiveViewlet();
// Hide sidebar if selected viewlet already visible
if (sideBarVisible && activeViewlet && activeViewlet.getId() === this.viewlet.id) {
if (sideBarVisible && activeViewlet && activeViewlet.getId() === this.activity.id) {
return this.partService.setSideBarHidden(true);
}
return this.viewletService.openViewlet(this.viewlet.id, true).then(() => this.activate());
return this.viewletService.openViewlet(this.activity.id, true).then(() => this.activate());
}
}
export class ActivityActionItem extends BaseActionItem {
protected $container: Builder;
protected $label: Builder;
protected $badge: Builder;
private $badgeContent: Builder;
private mouseUpTimeout: number;
constructor(
action: ActivityAction,
options: IBaseActionItemOptions,
@IThemeService protected themeService: IThemeService
) {
super(null, action, options);
this.themeService.onThemeChange(this.onThemeChange, this, this._callOnDispose);
action.onDidChangeBadge(this.handleBadgeChangeEvenet, this, this._callOnDispose);
}
protected get activity(): IActivity {
return (this._action as ActivityAction).activity;
}
protected updateStyles(): void {
const theme = this.themeService.getTheme();
// Label
if (this.$label) {
const background = theme.getColor(ACTIVITY_BAR_FOREGROUND);
this.$label.style('background-color', background ? background.toString() : null);
}
// Badge
if (this.$badgeContent) {
const badgeForeground = theme.getColor(ACTIVITY_BAR_BADGE_FOREGROUND);
const badgeBackground = theme.getColor(ACTIVITY_BAR_BADGE_BACKGROUND);
const contrastBorderColor = theme.getColor(contrastBorder);
this.$badgeContent.style('color', badgeForeground ? badgeForeground.toString() : null);
this.$badgeContent.style('background-color', badgeBackground ? badgeBackground.toString() : null);
this.$badgeContent.style('border-style', contrastBorderColor ? 'solid' : null);
this.$badgeContent.style('border-width', contrastBorderColor ? '1px' : null);
this.$badgeContent.style('border-color', contrastBorderColor ? contrastBorderColor.toString() : null);
}
}
public render(container: HTMLElement): void {
super.render(container);
// Make the container tab-able for keyboard navigation
this.$container = $(container).attr({
tabIndex: '0',
role: 'button',
title: this.activity.name
});
// Try hard to prevent keyboard only focus feedback when using mouse
this.$container.on(DOM.EventType.MOUSE_DOWN, () => {
this.$container.addClass('clicked');
});
this.$container.on(DOM.EventType.MOUSE_UP, () => {
if (this.mouseUpTimeout) {
clearTimeout(this.mouseUpTimeout);
}
this.mouseUpTimeout = setTimeout(() => {
this.$container.removeClass('clicked');
}, 800); // delayed to prevent focus feedback from showing on mouse up
});
// Label
this.$label = $('a.action-label').appendTo(this.builder);
if (this.activity.cssClass) {
this.$label.addClass(this.activity.cssClass);
}
this.$badge = this.builder.clone().div({ 'class': 'badge' }, (badge: Builder) => {
this.$badgeContent = badge.div({ 'class': 'badge-content' });
});
this.$badge.hide();
this.updateStyles();
}
private onThemeChange(theme: ITheme): void {
this.updateStyles();
}
public setBadge(badge: IBadge): void {
this.updateBadge(badge);
}
protected updateBadge(badge: IBadge): void {
this.$badgeContent.empty();
this.$badge.hide();
if (badge) {
// Number
if (badge instanceof NumberBadge) {
if (badge.number) {
this.$badgeContent.text(badge.number > 99 ? '99+' : badge.number.toString());
this.$badge.show();
}
}
// Text
else if (badge instanceof TextBadge) {
this.$badgeContent.text(badge.text);
this.$badge.show();
}
// Text
else if (badge instanceof IconBadge) {
this.$badge.show();
}
// Progress
else if (badge instanceof ProgressBadge) {
this.$badge.show();
}
}
// Title
let title: string;
if (badge && badge.getDescription()) {
if (this.activity.name) {
title = nls.localize('badgeTitle', "{0} - {1}", this.activity.name, badge.getDescription());
} else {
title = badge.getDescription();
}
} else {
title = this.activity.name;
}
[this.$label, this.$badge, this.$container].forEach(b => {
if (b) {
b.attr('aria-label', title);
b.title(title);
}
});
}
private handleBadgeChangeEvenet(): void {
const action = this.getAction();
if (action instanceof ActivityAction) {
this.updateBadge(action.getBadge());
}
}
public dispose(): void {
super.dispose();
if (this.mouseUpTimeout) {
clearTimeout(this.mouseUpTimeout);
}
this.$badge.destroy();
}
}
export class ViewletActionItem extends ActivityActionItem {
private static manageExtensionAction: ManageExtensionAction;
private static toggleViewletPinnedAction: ToggleViewletPinnedAction;
private static draggedViewlet: ViewletDescriptor;
private viewletActivity: IActivity;
private cssClass: string;
constructor(
private action: ViewletActivityAction,
@IContextMenuService private contextMenuService: IContextMenuService,
@IActivityBarService private activityBarService: IActivityBarService,
@IKeybindingService private keybindingService: IKeybindingService,
@IInstantiationService instantiationService: IInstantiationService,
@IThemeService themeService: IThemeService
) {
super(action, { draggable: true }, themeService);
this.cssClass = action.class;
if (!ViewletActionItem.manageExtensionAction) {
ViewletActionItem.manageExtensionAction = instantiationService.createInstance(ManageExtensionAction);
}
if (!ViewletActionItem.toggleViewletPinnedAction) {
ViewletActionItem.toggleViewletPinnedAction = instantiationService.createInstance(ToggleViewletPinnedAction, void 0);
}
}
protected get activity(): IActivity {
if (!this.viewletActivity) {
let activityName: string;
const keybinding = this.getKeybindingLabel(this.viewlet.id);
if (keybinding) {
activityName = nls.localize('titleKeybinding', "{0} ({1})", this.viewlet.name, keybinding);
} else {
activityName = this.viewlet.name;
}
this.viewletActivity = {
id: this.viewlet.id,
cssClass: this.cssClass,
name: activityName
};
}
return this.viewletActivity;
}
private get viewlet(): ViewletDescriptor {
return this.action.descriptor;
}
private getKeybindingLabel(id: string): string {
const kb = this.keybindingService.lookupKeybinding(id);
if (kb) {
return kb.getLabel();
}
return null;
}
public render(container: HTMLElement): void {
super.render(container);
this.$container.on('contextmenu', e => {
DOM.EventHelper.stop(e, true);
this.showContextMenu(container);
});
// Allow to drag
this.$container.on(DOM.EventType.DRAG_START, (e: DragEvent) => {
e.dataTransfer.effectAllowed = 'move';
this.setDraggedViewlet(this.viewlet);
// Trigger the action even on drag start to prevent clicks from failing that started a drag
if (!this.getAction().checked) {
this.getAction().run();
}
});
// Drag enter
let counter = 0; // see https://github.com/Microsoft/vscode/issues/14470
this.$container.on(DOM.EventType.DRAG_ENTER, (e: DragEvent) => {
const draggedViewlet = ViewletActionItem.getDraggedViewlet();
if (draggedViewlet && draggedViewlet.id !== this.viewlet.id) {
counter++;
this.updateFromDragging(container, true);
}
});
// Drag leave
this.$container.on(DOM.EventType.DRAG_LEAVE, (e: DragEvent) => {
const draggedViewlet = ViewletActionItem.getDraggedViewlet();
if (draggedViewlet) {
counter--;
if (counter === 0) {
this.updateFromDragging(container, false);
}
}
});
// Drag end
this.$container.on(DOM.EventType.DRAG_END, (e: DragEvent) => {
const draggedViewlet = ViewletActionItem.getDraggedViewlet();
if (draggedViewlet) {
counter = 0;
this.updateFromDragging(container, false);
ViewletActionItem.clearDraggedViewlet();
}
});
// Drop
this.$container.on(DOM.EventType.DROP, (e: DragEvent) => {
DOM.EventHelper.stop(e, true);
const draggedViewlet = ViewletActionItem.getDraggedViewlet();
if (draggedViewlet && draggedViewlet.id !== this.viewlet.id) {
this.updateFromDragging(container, false);
ViewletActionItem.clearDraggedViewlet();
this.activityBarService.move(draggedViewlet.id, this.viewlet.id);
}
});
// Activate on drag over to reveal targets
[this.$badge, this.$label].forEach(b => new DelayedDragHandler(b.getHTMLElement(), () => {
if (!ViewletActionItem.getDraggedViewlet() && !this.getAction().checked) {
this.getAction().run();
}
}));
this.updateStyles();
}
private updateFromDragging(element: HTMLElement, isDragging: boolean): void {
const theme = this.themeService.getTheme();
const dragBackground = theme.getColor(ACTIVITY_BAR_DRAG_AND_DROP_BACKGROUND);
element.style.backgroundColor = isDragging && dragBackground ? dragBackground.toString() : null;
}
public static getDraggedViewlet(): ViewletDescriptor {
return ViewletActionItem.draggedViewlet;
}
private setDraggedViewlet(viewlet: ViewletDescriptor): void {
ViewletActionItem.draggedViewlet = viewlet;
}
public static clearDraggedViewlet(): void {
ViewletActionItem.draggedViewlet = void 0;
}
private showContextMenu(container: HTMLElement): void {
const actions: Action[] = [ViewletActionItem.toggleViewletPinnedAction];
if (this.viewlet.extensionId) {
actions.push(new Separator());
actions.push(ViewletActionItem.manageExtensionAction);
}
const isPinned = this.activityBarService.isPinned(this.viewlet.id);
if (isPinned) {
ViewletActionItem.toggleViewletPinnedAction.label = nls.localize('removeFromActivityBar', "Hide from Activity Bar");
} else {
ViewletActionItem.toggleViewletPinnedAction.label = nls.localize('keepInActivityBar', "Keep in Activity Bar");
}
this.contextMenuService.showContextMenu({
getAnchor: () => container,
getActionsContext: () => this.viewlet,
getActions: () => TPromise.as(actions)
});
}
public focus(): void {
this.$container.domFocus();
}
protected _updateClass(): void {
if (this.cssClass) {
this.$badge.removeClass(this.cssClass);
}
this.cssClass = this.getAction().class;
this.$badge.addClass(this.cssClass);
}
protected _updateChecked(): void {
if (this.getAction().checked) {
this.$container.addClass('checked');
} else {
this.$container.removeClass('checked');
}
}
protected _updateEnabled(): void {
if (this.getAction().enabled) {
this.builder.removeClass('disabled');
} else {
this.builder.addClass('disabled');
}
}
public dispose(): void {
super.dispose();
ViewletActionItem.clearDraggedViewlet();
this.$label.destroy();
}
}
export class ViewletOverflowActivityAction extends ActivityAction {
constructor(
private showMenu: () => void
) {
super({
id: 'activitybar.additionalViewlets.action',
name: nls.localize('additionalViews', "Additional Views"),
cssClass: 'toggle-more'
});
}
public run(event: any): TPromise<any> {
this.showMenu();
return TPromise.as(true);
}
}
export class ViewletOverflowActivityActionItem extends ActivityActionItem {
private name: string;
private cssClass: string;
private actions: OpenViewletAction[];
constructor(
action: ActivityAction,
private getOverflowingViewlets: () => ViewletDescriptor[],
private getBadge: (viewlet: ViewletDescriptor) => IBadge,
@IInstantiationService private instantiationService: IInstantiationService,
@IViewletService private viewletService: IViewletService,
@IContextMenuService private contextMenuService: IContextMenuService,
@IThemeService themeService: IThemeService
) {
super(action, null, themeService);
this.cssClass = action.class;
this.name = action.label;
}
public showMenu(): void {
if (this.actions) {
dispose(this.actions);
}
this.actions = this.getActions();
this.contextMenuService.showContextMenu({
getAnchor: () => this.builder.getHTMLElement(),
getActions: () => TPromise.as(this.actions),
onHide: () => dispose(this.actions)
});
}
private getActions(): OpenViewletAction[] {
const activeViewlet = this.viewletService.getActiveViewlet();
return this.getOverflowingViewlets().map(viewlet => {
const action = this.instantiationService.createInstance(OpenViewletAction, viewlet);
action.radio = activeViewlet && activeViewlet.getId() === action.id;
const badge = this.getBadge(action.viewlet);
let suffix: string | number;
if (badge instanceof NumberBadge) {
suffix = badge.number;
} else if (badge instanceof TextBadge) {
suffix = badge.text;
}
if (suffix) {
action.label = nls.localize('numberBadge', "{0} ({1})", action.viewlet.name, suffix);
} else {
action.label = action.viewlet.name;
}
return action;
});
}
public dispose(): void {
super.dispose();
this.actions = dispose(this.actions);
}
}
class ManageExtensionAction extends Action {
constructor(
@ICommandService private commandService: ICommandService
) {
super('activitybar.manage.extension', nls.localize('manageExtension', "Manage Extension"));
}
public run(viewlet: ViewletDescriptor): TPromise<any> {
return this.commandService.executeCommand('_extensions.manage', viewlet.extensionId);
}
}
class OpenViewletAction extends Action {
export class OpenViewletAction extends Action {
constructor(
private _viewlet: ViewletDescriptor,
@ -608,41 +72,37 @@ class OpenViewletAction extends Action {
super(_viewlet.id, _viewlet.name);
}
public get viewlet(): ViewletDescriptor {
return this._viewlet;
}
public run(): TPromise<any> {
const sideBarVisible = this.partService.isVisible(Parts.SIDEBAR_PART);
const activeViewlet = this.viewletService.getActiveViewlet();
// Hide sidebar if selected viewlet already visible
if (sideBarVisible && activeViewlet && activeViewlet.getId() === this.viewlet.id) {
if (sideBarVisible && activeViewlet && activeViewlet.getId() === this._viewlet.id) {
return this.partService.setSideBarHidden(true);
}
return this.viewletService.openViewlet(this.viewlet.id, true);
return this.viewletService.openViewlet(this._viewlet.id, true);
}
}
export class ToggleViewletPinnedAction extends Action {
constructor(
private viewlet: ViewletDescriptor,
private activity: IActivity,
@IActivityBarService private activityBarService: IActivityBarService
) {
super('activitybar.show.toggleViewletPinned', viewlet ? viewlet.name : nls.localize('toggle', "Toggle View Pinned"));
super('activitybar.show.toggleViewletPinned', activity ? activity.name : nls.localize('toggle', "Toggle View Pinned"));
this.checked = this.viewlet && this.activityBarService.isPinned(this.viewlet.id);
this.checked = this.activity && this.activityBarService.isPinned(this.activity.id);
}
public run(context?: ViewletDescriptor): TPromise<any> {
const viewlet = this.viewlet || context;
public run(context: string): TPromise<any> {
const id = this.activity ? this.activity.id : context;
if (this.activityBarService.isPinned(viewlet.id)) {
this.activityBarService.unpin(viewlet.id);
if (this.activityBarService.isPinned(id)) {
this.activityBarService.unpin(id);
} else {
this.activityBarService.pin(viewlet.id);
this.activityBarService.pin(id);
}
return TPromise.as(true);

View file

@ -8,25 +8,20 @@
import 'vs/css!./media/activitybarpart';
import nls = require('vs/nls');
import { TPromise } from 'vs/base/common/winjs.base';
import DOM = require('vs/base/browser/dom');
import * as arrays from 'vs/base/common/arrays';
import { illegalArgument } from 'vs/base/common/errors';
import { Builder, $, Dimension } from 'vs/base/browser/builder';
import { Action } from 'vs/base/common/actions';
import { ActionsOrientation, ActionBar, IActionItem, Separator } from 'vs/base/browser/ui/actionbar/actionbar';
import { ViewletDescriptor } from 'vs/workbench/browser/viewlet';
import { ActionsOrientation, ActionBar, Separator } from 'vs/base/browser/ui/actionbar/actionbar';
import { GlobalActivityExtensions, IGlobalActivityRegistry } from 'vs/workbench/common/activity';
import { Registry } from 'vs/platform/registry/common/platform';
import { Part } from 'vs/workbench/browser/part';
import { IViewlet } from 'vs/workbench/common/viewlet';
import { ToggleViewletPinnedAction, ViewletActivityAction, ActivityAction, GlobalActivityActionItem, ViewletActionItem, ViewletOverflowActivityAction, ViewletOverflowActivityActionItem, GlobalActivityAction, IViewletActivity } from 'vs/workbench/browser/parts/activitybar/activitybarActions';
import { ToggleViewletPinnedAction, GlobalActivityActionItem, GlobalActivityAction, ViewletActivityAction, OpenViewletAction } from 'vs/workbench/browser/parts/activitybar/activitybarActions';
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
import { IActivityBarService, IBadge } from 'vs/workbench/services/activity/common/activityBarService';
import { IPartService, Position as SideBarPosition } from 'vs/workbench/services/part/common/partService';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IExtensionService } from 'vs/platform/extensions/common/extensions';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { Scope as MementoScope } from 'vs/workbench/common/memento';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
import { dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
@ -34,6 +29,7 @@ import { ToggleActivityBarVisibilityAction } from 'vs/workbench/browser/actions/
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { ACTIVITY_BAR_BACKGROUND, ACTIVITY_BAR_BORDER } from 'vs/workbench/common/theme';
import { contrastBorder } from 'vs/platform/theme/common/colorRegistry';
import { CompositeBar } from 'vs/workbench/browser/parts/compositebar/compositeBar';
export class ActivitybarPart extends Part implements IActivityBarService {
@ -47,17 +43,7 @@ export class ActivitybarPart extends Part implements IActivityBarService {
private globalActionBar: ActionBar;
private globalActivityIdToActions: { [globalActivityId: string]: GlobalActivityAction; };
private viewletSwitcherBar: ActionBar;
private viewletOverflowAction: ViewletOverflowActivityAction;
private viewletOverflowActionItem: ViewletOverflowActivityActionItem;
private viewletIdToActions: { [viewletId: string]: ActivityAction; };
private viewletIdToActionItems: { [viewletId: string]: IActionItem; };
private viewletIdToActivityStack: { [viewletId: string]: IViewletActivity[]; };
private memento: object;
private pinnedViewlets: string[];
private activeUnpinnedViewlet: ViewletDescriptor;
private compositeBar: CompositeBar;
constructor(
id: string,
@ -72,58 +58,33 @@ export class ActivitybarPart extends Part implements IActivityBarService {
super(id, { hasTitle: false }, themeService);
this.globalActivityIdToActions = Object.create(null);
this.viewletIdToActionItems = Object.create(null);
this.viewletIdToActions = Object.create(null);
this.viewletIdToActivityStack = Object.create(null);
this.memento = this.getMemento(this.storageService, MementoScope.GLOBAL);
const pinnedViewlets = this.memento[ActivitybarPart.PINNED_VIEWLETS] as string[];
if (pinnedViewlets) {
this.pinnedViewlets = pinnedViewlets;
} else {
this.pinnedViewlets = this.viewletService.getViewlets().map(v => v.id);
}
this.compositeBar = this.instantiationService.createInstance(CompositeBar, {
label: 'icon',
storageId: ActivitybarPart.PINNED_VIEWLETS,
orientation: ActionsOrientation.VERTICAL,
composites: this.viewletService.getViewlets(),
getCompositeSize: (compositeId: string) => ActivitybarPart.ACTIVITY_ACTION_HEIGHT,
getActivityAction: (compositeId: string) => this.instantiationService.createInstance(ViewletActivityAction, this.viewletService.getViewlet(compositeId)),
getCompositePinnedAction: (compositeId: string) => this.instantiationService.createInstance(ToggleViewletPinnedAction, this.viewletService.getViewlet(compositeId)),
getOpenCompositeAction: (compositeId: string) => this.instantiationService.createInstance(OpenViewletAction, this.viewletService.getViewlet(compositeId))
});
this.registerListeners();
}
private registerListeners(): void {
// Activate viewlet action on opening of a viewlet
this.toUnbind.push(this.viewletService.onDidViewletOpen(viewlet => this.onDidViewletOpen(viewlet)));
this.toUnbind.push(this.viewletService.onDidViewletOpen(viewlet => this.compositeBar.activateComposite(viewlet.getId())));
// Deactivate viewlet action on close
this.toUnbind.push(this.viewletService.onDidViewletClose(viewlet => this.onDidViewletClose(viewlet)));
}
private onDidViewletOpen(viewlet: IViewlet): void {
const id = viewlet.getId();
if (this.viewletIdToActions[id]) {
this.viewletIdToActions[id].activate();
}
const activeUnpinnedViewletShouldClose = this.activeUnpinnedViewlet && this.activeUnpinnedViewlet.id !== viewlet.getId();
const activeUnpinnedViewletShouldShow = !this.getPinnedViewlets().some(v => v.id === viewlet.getId());
if (activeUnpinnedViewletShouldShow || activeUnpinnedViewletShouldClose) {
this.updateViewletSwitcher();
}
}
private onDidViewletClose(viewlet: IViewlet): void {
const id = viewlet.getId();
if (this.viewletIdToActions[id]) {
this.viewletIdToActions[id].deactivate();
}
this.toUnbind.push(this.viewletService.onDidViewletClose(viewlet => this.compositeBar.deactivateComposite(viewlet.getId())));
this.toUnbind.push(this.compositeBar.onDidDropComposite(data => this.move(data.compositeId, data.toCompositeId)));
this.toUnbind.push(this.compositeBar.onDidContextMenu(e => this.showContextMenu(e)));
}
public showActivity(viewletOrActionId: string, badge: IBadge, clazz?: string): IDisposable {
if (this.viewletService.getViewlet(viewletOrActionId)) {
return this.showViewletActivity(viewletOrActionId, badge, clazz);
return this.compositeBar.showActivity(viewletOrActionId, badge, clazz);
}
return this.showGlobalActivity(viewletOrActionId, badge);
@ -144,94 +105,16 @@ export class ActivitybarPart extends Part implements IActivityBarService {
return toDisposable(() => action.setBadge(undefined));
}
private showViewletActivity(viewletId: string, badge: IBadge, clazz?: string): IDisposable {
if (!badge) {
throw illegalArgument('badge');
}
const activity = <IViewletActivity>{ badge, clazz };
const stack = this.viewletIdToActivityStack[viewletId] || (this.viewletIdToActivityStack[viewletId] = []);
stack.unshift(activity);
this.updateViewletActivity(viewletId);
return {
dispose: () => {
const stack = this.viewletIdToActivityStack[viewletId];
if (!stack) {
return;
}
const idx = stack.indexOf(activity);
if (idx < 0) {
return;
}
stack.splice(idx, 1);
if (stack.length === 0) {
delete this.viewletIdToActivityStack[viewletId];
}
this.updateViewletActivity(viewletId);
}
};
}
private updateViewletActivity(viewletId: string) {
const action = this.viewletIdToActions[viewletId];
if (!action) {
return;
}
const stack = this.viewletIdToActivityStack[viewletId];
// reset
if (!stack || !stack.length) {
action.setBadge(undefined);
}
// update
else {
const [{ badge, clazz }] = stack;
action.setBadge(badge);
if (clazz) {
action.class = clazz;
}
}
}
public createContentArea(parent: Builder): Builder {
const $el = $(parent);
const $result = $('.content').appendTo($el);
// Top Actionbar with action items for each viewlet action
this.createViewletSwitcher($result.clone());
this.compositeBar.create($result.clone().getHTMLElement());
// Top Actionbar with action items for each viewlet action
this.createGlobalActivityActionBar($result.getHTMLElement());
// Contextmenu for viewlets
$(parent).on('contextmenu', (e: MouseEvent) => {
DOM.EventHelper.stop(e, true);
this.showContextMenu(e);
}, this.toUnbind);
// Allow to drop at the end to move viewlet to the end
$(parent).on(DOM.EventType.DROP, (e: DragEvent) => {
const draggedViewlet = ViewletActionItem.getDraggedViewlet();
if (draggedViewlet) {
DOM.EventHelper.stop(e, true);
ViewletActionItem.clearDraggedViewlet();
const targetId = this.pinnedViewlets[this.pinnedViewlets.length - 1];
if (targetId !== draggedViewlet.id) {
this.move(draggedViewlet.id, this.pinnedViewlets[this.pinnedViewlets.length - 1]);
}
}
});
return $result;
}
@ -268,20 +151,6 @@ export class ActivitybarPart extends Part implements IActivityBarService {
});
}
private createViewletSwitcher(div: Builder): void {
this.viewletSwitcherBar = new ActionBar(div, {
actionItemProvider: (action: Action) => action instanceof ViewletOverflowActivityAction ? this.viewletOverflowActionItem : this.viewletIdToActionItems[action.id],
orientation: ActionsOrientation.VERTICAL,
ariaLabel: nls.localize('activityBarAriaLabel', "Active View Switcher"),
animated: false
});
this.updateViewletSwitcher();
// Update viewlet switcher when external viewlets become ready
this.extensionService.onReady().then(() => this.updateViewletSwitcher());
}
private createGlobalActivityActionBar(container: HTMLElement): void {
const activityRegistry = Registry.as<IGlobalActivityRegistry>(GlobalActivityExtensions);
const descriptors = activityRegistry.getActivities();
@ -302,152 +171,18 @@ export class ActivitybarPart extends Part implements IActivityBarService {
});
}
private updateViewletSwitcher() {
if (!this.viewletSwitcherBar) {
return; // We have not been rendered yet so there is nothing to update.
}
let viewletsToShow = this.getPinnedViewlets();
// Always show the active viewlet even if it is marked to be hidden
const activeViewlet = this.viewletService.getActiveViewlet();
if (activeViewlet && !viewletsToShow.some(viewlet => viewlet.id === activeViewlet.getId())) {
this.activeUnpinnedViewlet = this.viewletService.getViewlet(activeViewlet.getId());
viewletsToShow.push(this.activeUnpinnedViewlet);
} else {
this.activeUnpinnedViewlet = void 0;
}
// Ensure we are not showing more viewlets than we have height for
let overflows = false;
if (this.dimension) {
let availableHeight = this.dimension.height;
if (this.globalActionBar) {
availableHeight -= (this.globalActionBar.items.length * ActivitybarPart.ACTIVITY_ACTION_HEIGHT); // adjust for global actions showing
}
const maxVisible = Math.floor(availableHeight / ActivitybarPart.ACTIVITY_ACTION_HEIGHT);
overflows = viewletsToShow.length > maxVisible;
if (overflows) {
viewletsToShow = viewletsToShow.slice(0, maxVisible - 1 /* make room for overflow action */);
}
}
const visibleViewlets = Object.keys(this.viewletIdToActions);
const visibleViewletsChange = !arrays.equals(viewletsToShow.map(viewlet => viewlet.id), visibleViewlets);
// Pull out overflow action if there is a viewlet change so that we can add it to the end later
if (this.viewletOverflowAction && visibleViewletsChange) {
this.viewletSwitcherBar.pull(this.viewletSwitcherBar.length() - 1);
this.viewletOverflowAction.dispose();
this.viewletOverflowAction = null;
this.viewletOverflowActionItem.dispose();
this.viewletOverflowActionItem = null;
}
// Pull out viewlets that overflow or got hidden
const viewletIdsToShow = viewletsToShow.map(v => v.id);
visibleViewlets.forEach(viewletId => {
if (viewletIdsToShow.indexOf(viewletId) === -1) {
this.pullViewlet(viewletId);
}
});
// Built actions for viewlets to show
const newViewletsToShow = viewletsToShow
.filter(viewlet => !this.viewletIdToActions[viewlet.id])
.map(viewlet => this.toAction(viewlet));
// Update when we have new viewlets to show
if (newViewletsToShow.length) {
// Add to viewlet switcher
this.viewletSwitcherBar.push(newViewletsToShow, { label: true, icon: true });
// Make sure to activate the active one
const activeViewlet = this.viewletService.getActiveViewlet();
if (activeViewlet) {
const activeViewletEntry = this.viewletIdToActions[activeViewlet.getId()];
if (activeViewletEntry) {
activeViewletEntry.activate();
}
}
// Make sure to restore activity
Object.keys(this.viewletIdToActions).forEach(viewletId => {
this.updateViewletActivity(viewletId);
});
}
// Add overflow action as needed
if (visibleViewletsChange && overflows) {
this.viewletOverflowAction = this.instantiationService.createInstance(ViewletOverflowActivityAction, () => this.viewletOverflowActionItem.showMenu());
this.viewletOverflowActionItem = this.instantiationService.createInstance(ViewletOverflowActivityActionItem, this.viewletOverflowAction, () => this.getOverflowingViewlets(), (viewlet: ViewletDescriptor) => this.viewletIdToActivityStack[viewlet.id] && this.viewletIdToActivityStack[viewlet.id][0].badge);
this.viewletSwitcherBar.push(this.viewletOverflowAction, { label: true, icon: true });
}
}
private getOverflowingViewlets(): ViewletDescriptor[] {
const viewlets = this.getPinnedViewlets();
if (this.activeUnpinnedViewlet) {
viewlets.push(this.activeUnpinnedViewlet);
}
const visibleViewlets = Object.keys(this.viewletIdToActions);
return viewlets.filter(viewlet => visibleViewlets.indexOf(viewlet.id) === -1);
}
private getVisibleViewlets(): ViewletDescriptor[] {
const viewlets = this.viewletService.getViewlets();
const visibleViewlets = Object.keys(this.viewletIdToActions);
return viewlets.filter(viewlet => visibleViewlets.indexOf(viewlet.id) >= 0);
}
private getPinnedViewlets(): ViewletDescriptor[] {
return this.pinnedViewlets.map(viewletId => this.viewletService.getViewlet(viewletId)).filter(v => !!v); // ensure to remove those that might no longer exist
}
private pullViewlet(viewletId: string): void {
const index = Object.keys(this.viewletIdToActions).indexOf(viewletId);
if (index >= 0) {
this.viewletSwitcherBar.pull(index);
const action = this.viewletIdToActions[viewletId];
action.dispose();
delete this.viewletIdToActions[viewletId];
const actionItem = this.viewletIdToActionItems[action.id];
actionItem.dispose();
delete this.viewletIdToActionItems[action.id];
}
}
private toAction(viewlet: ViewletDescriptor): ActivityAction {
const action = this.instantiationService.createInstance(ViewletActivityAction, viewlet);
this.viewletIdToActionItems[action.id] = this.instantiationService.createInstance(ViewletActionItem, action);
this.viewletIdToActions[viewlet.id] = action;
return action;
}
public getPinned(): string[] {
return this.pinnedViewlets;
return this.viewletService.getViewlets().map(v => v.id).filter(id => this.compositeBar.isPinned(id));;
}
public unpin(viewletId: string): void {
if (!this.isPinned(viewletId)) {
if (!this.compositeBar.isPinned(viewletId)) {
return;
}
const activeViewlet = this.viewletService.getActiveViewlet();
const defaultViewletId = this.viewletService.getDefaultViewletId();
const visibleViewlets = this.getVisibleViewlets();
const visibleViewlets = this.compositeBar.getVisibleComposites();
let unpinPromise: TPromise<any>;
@ -459,7 +194,7 @@ export class ActivitybarPart extends Part implements IActivityBarService {
// Case: viewlet is not the default viewlet and default viewlet is still showing
// Solv: we open the default viewlet
else if (defaultViewletId !== viewletId && this.isPinned(defaultViewletId)) {
else if (defaultViewletId !== viewletId && this.compositeBar.isPinned(defaultViewletId)) {
unpinPromise = this.viewletService.openViewlet(defaultViewletId, true);
}
@ -472,21 +207,17 @@ export class ActivitybarPart extends Part implements IActivityBarService {
// Case: we closed the default viewlet
// Solv: we open the next visible viewlet from top
else {
unpinPromise = this.viewletService.openViewlet(visibleViewlets.filter(viewlet => viewlet.id !== viewletId)[0].id, true);
unpinPromise = this.viewletService.openViewlet(visibleViewlets.filter(viewletId => viewletId !== viewletId)[0], true);
}
unpinPromise.then(() => {
// then remove from pinned and update switcher
const index = this.pinnedViewlets.indexOf(viewletId);
this.pinnedViewlets.splice(index, 1);
this.updateViewletSwitcher();
this.compositeBar.unpin(viewletId);
});
}
public isPinned(viewletId: string): boolean {
return this.pinnedViewlets.indexOf(viewletId) >= 0;
return this.compositeBar.isPinned(viewletId);
}
public pin(viewletId: string, update = true): void {
@ -495,41 +226,17 @@ export class ActivitybarPart extends Part implements IActivityBarService {
}
// first open that viewlet
this.viewletService.openViewlet(viewletId, true).then(() => {
// then update
this.pinnedViewlets.push(viewletId);
this.pinnedViewlets = arrays.distinct(this.pinnedViewlets);
if (update) {
this.updateViewletSwitcher();
}
});
this.viewletService.openViewlet(viewletId, true)
.then(() => this.compositeBar.pin(viewletId, update));
}
public move(viewletId: string, toViewletId: string): void {
// Make sure a moved viewlet gets pinned
if (!this.isPinned(viewletId)) {
this.pin(viewletId, false /* defer update, we take care of it */);
}
const fromIndex = this.pinnedViewlets.indexOf(viewletId);
const toIndex = this.pinnedViewlets.indexOf(toViewletId);
this.pinnedViewlets.splice(fromIndex, 1);
this.pinnedViewlets.splice(toIndex, 0, viewletId);
// Clear viewlets that are impacted by the move
const visibleViewlets = Object.keys(this.viewletIdToActions);
for (let i = Math.min(fromIndex, toIndex); i < visibleViewlets.length; i++) {
this.pullViewlet(visibleViewlets[i]);
}
// timeout helps to prevent artifacts from showing up
setTimeout(() => {
this.updateViewletSwitcher();
}, 0);
this.compositeBar.move(viewletId, toViewletId);
}
/**
@ -542,16 +249,20 @@ export class ActivitybarPart extends Part implements IActivityBarService {
this.dimension = sizes[1];
// Update switcher to handle overflow issues
this.updateViewletSwitcher();
let availableHeight = this.dimension.height;
if (this.globalActionBar) {
// adjust height for global actions showing
availableHeight -= (this.globalActionBar.items.length * ActivitybarPart.ACTIVITY_ACTION_HEIGHT);
}
this.compositeBar.layout(new Dimension(dimension.width, availableHeight));
return sizes;
}
public dispose(): void {
if (this.viewletSwitcherBar) {
this.viewletSwitcherBar.dispose();
this.viewletSwitcherBar = null;
if (this.compositeBar) {
this.compositeBar.dispose();
this.compositeBar = null;
}
if (this.globalActionBar) {
@ -565,9 +276,9 @@ export class ActivitybarPart extends Part implements IActivityBarService {
public shutdown(): void {
// Persist Hidden State
this.memento[ActivitybarPart.PINNED_VIEWLETS] = this.pinnedViewlets;
this.compositeBar.store();
// Pass to super
super.shutdown();
}
}
}

View file

@ -0,0 +1,380 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import nls = require('vs/nls');
import { Action } from 'vs/base/common/actions';
import { illegalArgument } from 'vs/base/common/errors';
import * as dom from 'vs/base/browser/dom';
import * as arrays from 'vs/base/common/arrays';
import { Dimension } from 'vs/base/browser/builder';
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IBadge } from 'vs/workbench/services/activity/common/activityBarService';
import { IPartService } from 'vs/workbench/services/part/common/partService';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ActionBar, IActionItem, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar';
import Event, { Emitter } from 'vs/base/common/event';
import { CompositeActionItem, CompositeOverflowActivityAction, ICompositeActivity, CompositeOverflowActivityActionItem, ActivityAction } from 'vs/workbench/browser/parts/compositebar/compositeBarActions';
export interface ICompositeBarOptions {
label: 'icon' | 'name';
storageId: string;
orientation: ActionsOrientation;
composites: { id: string, name: string }[];
getActivityAction: (compositeId: string) => ActivityAction;
getCompositePinnedAction: (compositeId: string) => Action;
getOpenCompositeAction: (compositeId: string) => Action;
getCompositeSize: (compositeId: string) => number;
}
export class CompositeBar {
private _onDidContextMenu: Emitter<MouseEvent>;
private _onDidDropComposite: Emitter<{ compositeId: string, toCompositeId: string }>;
private dimension: Dimension;
private toDispose: IDisposable[];
private compositeSwitcherBar: ActionBar;
private compositeOverflowAction: CompositeOverflowActivityAction;
private compositeOverflowActionItem: CompositeOverflowActivityActionItem;
private compositeIdToActions: { [compositeId: string]: ActivityAction; };
private compositeIdToActionItems: { [compositeId: string]: IActionItem; };
private compositeIdToActivityStack: { [compositeId: string]: ICompositeActivity[]; };
private pinnedComposites: string[];
private activeCompositeId: string;
private activeUnpinnedCompositeId: string;
constructor(
private options: ICompositeBarOptions,
@IContextMenuService private contextMenuService: IContextMenuService,
@IInstantiationService private instantiationService: IInstantiationService,
@IStorageService private storageService: IStorageService,
@IPartService private partService: IPartService,
@IThemeService themeService: IThemeService,
) {
this.toDispose = [];
this.compositeIdToActionItems = Object.create(null);
this.compositeIdToActions = Object.create(null);
this.compositeIdToActivityStack = Object.create(null);
this._onDidContextMenu = new Emitter<MouseEvent>();
this._onDidDropComposite = new Emitter<{ compositeId: string, toCompositeId: string }>();
const pinnedComposites = JSON.parse(this.storageService.get(this.options.storageId, StorageScope.GLOBAL, null)) as string[];
if (pinnedComposites) {
this.pinnedComposites = pinnedComposites;
} else {
this.pinnedComposites = this.options.composites.map(c => c.id);
}
}
public get onDidContextMenu(): Event<MouseEvent> {
return this._onDidContextMenu.event;
}
public get onDidDropComposite(): Event<{ compositeId: string, toCompositeId: string }> {
return this._onDidDropComposite.event;
}
public activateComposite(id: string): void {
if (this.compositeIdToActions[id]) {
this.compositeIdToActions[id].activate();
}
this.activeCompositeId = id;
const activeUnpinnedCompositeShouldClose = this.activeUnpinnedCompositeId && this.activeUnpinnedCompositeId !== id;
const activeUnpinnedCompositeShouldShow = !this.pinnedComposites.some(pid => pid === id);
if (activeUnpinnedCompositeShouldShow || activeUnpinnedCompositeShouldClose) {
this.updateCompositeSwitcher();
}
}
public deactivateComposite(id: string): void {
if (this.compositeIdToActions[id]) {
this.compositeIdToActions[id].deactivate();
}
}
public showActivity(compositeId: string, badge: IBadge, clazz?: string): IDisposable {
if (!badge) {
throw illegalArgument('badge');
}
const activity = <ICompositeActivity>{ badge, clazz };
const stack = this.compositeIdToActivityStack[compositeId] || (this.compositeIdToActivityStack[compositeId] = []);
stack.unshift(activity);
this.updateActivity(compositeId);
return {
dispose: () => {
const stack = this.compositeIdToActivityStack[compositeId];
if (!stack) {
return;
}
const idx = stack.indexOf(activity);
if (idx < 0) {
return;
}
stack.splice(idx, 1);
if (stack.length === 0) {
delete this.compositeIdToActivityStack[compositeId];
}
this.updateActivity(compositeId);
}
};
}
private updateActivity(compositeId: string) {
const action = this.compositeIdToActions[compositeId];
if (!action) {
return;
}
const stack = this.compositeIdToActivityStack[compositeId];
// reset
if (!stack || !stack.length) {
action.setBadge(undefined);
}
// update
else {
const [{ badge, clazz }] = stack;
action.setBadge(badge);
if (clazz) {
action.class = clazz;
}
}
}
public create(container: HTMLElement): void {
this.compositeSwitcherBar = new ActionBar(container, {
actionItemProvider: (action: Action) => action instanceof CompositeOverflowActivityAction ? this.compositeOverflowActionItem : this.compositeIdToActionItems[action.id],
orientation: this.options.orientation,
ariaLabel: nls.localize('activityBarAriaLabel', "Active View Switcher"),
animated: false
});
this.updateCompositeSwitcher();
// Contextmenu for composites
this.toDispose.push(dom.addDisposableListener(container, dom.EventType.CONTEXT_MENU, (e: MouseEvent) => {
dom.EventHelper.stop(e, true);
this._onDidContextMenu.fire(e);
}));
// Allow to drop at the end to move composites to the end
this.toDispose.push(dom.addDisposableListener(container, dom.EventType.DROP, (e: DragEvent) => {
const draggedCompositeId = CompositeActionItem.getDraggedCompositeId();
if (draggedCompositeId) {
dom.EventHelper.stop(e, true);
CompositeActionItem.clearDraggedComposite();
const targetId = this.pinnedComposites[this.pinnedComposites.length - 1];
if (targetId !== draggedCompositeId) {
this._onDidDropComposite.fire({ compositeId: draggedCompositeId, toCompositeId: this.pinnedComposites[this.pinnedComposites.length - 1] });
}
}
}));
}
private updateCompositeSwitcher(): void {
if (!this.compositeSwitcherBar) {
return; // We have not been rendered yet so there is nothing to update.
}
let compositesToShow = this.pinnedComposites;
// Always show the active composite even if it is marked to be hidden
if (this.activeCompositeId && !compositesToShow.some(id => id === this.activeCompositeId)) {
this.activeUnpinnedCompositeId = this.activeCompositeId;
compositesToShow = compositesToShow.concat(this.activeUnpinnedCompositeId);
} else {
this.activeUnpinnedCompositeId = void 0;
}
// Ensure we are not showing more composites than we have height for
let overflows = false;
if (this.dimension) {
let maxVisible = compositesToShow.length;
let size = 0;
const limit = this.options.orientation === ActionsOrientation.VERTICAL ? this.dimension.height : this.dimension.width;
for (let i = 0; i < compositesToShow.length && size <= limit; i++) {
size += this.options.getCompositeSize(compositesToShow[i]);
if (size > limit) {
maxVisible = i;
}
}
overflows = compositesToShow.length > maxVisible;
if (overflows) {
compositesToShow = compositesToShow.slice(0, maxVisible - 1 /* make room for overflow action */);
}
}
const visibleComposites = Object.keys(this.compositeIdToActions);
const visibleCompositesChange = !arrays.equals(compositesToShow, visibleComposites);
// Pull out overflow action if there is a composite change so that we can add it to the end later
if (this.compositeOverflowAction && visibleCompositesChange) {
this.compositeSwitcherBar.pull(this.compositeSwitcherBar.length() - 1);
this.compositeOverflowAction.dispose();
this.compositeOverflowAction = null;
this.compositeOverflowActionItem.dispose();
this.compositeOverflowActionItem = null;
}
// Pull out composites that overflow or got hidden
visibleComposites.forEach(compositeId => {
if (compositesToShow.indexOf(compositeId) === -1) {
this.pullComposite(compositeId);
}
});
// Built actions for composites to show
const newCompositesToShow = compositesToShow
.filter(compositeId => !this.compositeIdToActions[compositeId])
.map(compositeId => this.toAction(compositeId));
// Update when we have new composites to show
if (newCompositesToShow.length) {
// Add to composite switcher
this.compositeSwitcherBar.push(newCompositesToShow, { label: true, icon: true });
// Make sure to activate the active one
if (this.activeCompositeId) {
const activeCompositeEntry = this.compositeIdToActions[this.activeCompositeId];
if (activeCompositeEntry) {
activeCompositeEntry.activate();
}
}
// Make sure to restore activity
Object.keys(this.compositeIdToActions).forEach(compositeId => {
this.updateActivity(compositeId);
});
}
// Add overflow action as needed
if (visibleCompositesChange && overflows) {
this.compositeOverflowAction = this.instantiationService.createInstance(CompositeOverflowActivityAction, () => this.compositeOverflowActionItem.showMenu());
this.compositeOverflowActionItem = this.instantiationService.createInstance(
CompositeOverflowActivityActionItem,
this.compositeOverflowAction,
() => this.getOverflowingComposites(),
() => this.activeCompositeId,
(compositeId: string) => this.compositeIdToActivityStack[compositeId] && this.compositeIdToActivityStack[compositeId][0].badge,
this.options.getOpenCompositeAction
);
this.compositeSwitcherBar.push(this.compositeOverflowAction, { label: true, icon: true });
}
}
private getOverflowingComposites(): { id: string, name: string }[] {
let overflowingIds = this.pinnedComposites;
if (this.activeUnpinnedCompositeId) {
overflowingIds = overflowingIds.concat(this.activeUnpinnedCompositeId);
}
const visibleComposites = Object.keys(this.compositeIdToActions);
overflowingIds = overflowingIds.filter(compositeId => visibleComposites.indexOf(compositeId) === -1);
return this.options.composites.filter(c => overflowingIds.indexOf(c.id) !== -1);
}
public getVisibleComposites(): string[] {
return Object.keys(this.compositeIdToActions);
}
private pullComposite(compositeId: string): void {
const index = Object.keys(this.compositeIdToActions).indexOf(compositeId);
if (index >= 0) {
this.compositeSwitcherBar.pull(index);
const action = this.compositeIdToActions[compositeId];
action.dispose();
delete this.compositeIdToActions[compositeId];
const actionItem = this.compositeIdToActionItems[action.id];
actionItem.dispose();
delete this.compositeIdToActionItems[action.id];
}
}
private toAction(compositeId: string): ActivityAction {
const compositeActivityAction = this.options.getActivityAction(compositeId);
const pinnedAction = this.options.getCompositePinnedAction(compositeId);
this.compositeIdToActionItems[compositeId] = this.instantiationService.createInstance(CompositeActionItem, compositeActivityAction, pinnedAction);
this.compositeIdToActions[compositeId] = compositeActivityAction;
return compositeActivityAction;
}
public unpin(compositeId: string): void {
const index = this.pinnedComposites.indexOf(compositeId);
this.pinnedComposites.splice(index, 1);
this.updateCompositeSwitcher();
}
public isPinned(compositeId: string): boolean {
return this.pinnedComposites.indexOf(compositeId) >= 0;
}
public pin(compositeId: string, update = true): void {
this.pinnedComposites.push(compositeId);
this.pinnedComposites = arrays.distinct(this.pinnedComposites);
if (update) {
this.updateCompositeSwitcher();
}
}
public move(compositeId: string, toCompositeId: string): void {
const fromIndex = this.pinnedComposites.indexOf(compositeId);
const toIndex = this.pinnedComposites.indexOf(toCompositeId);
this.pinnedComposites.splice(fromIndex, 1);
this.pinnedComposites.splice(toIndex, 0, compositeId);
// Clear composites that are impacted by the move
const visibleComposites = Object.keys(this.compositeIdToActions);
for (let i = Math.min(fromIndex, toIndex); i < visibleComposites.length; i++) {
this.pullComposite(visibleComposites[i]);
}
// timeout helps to prevent artifacts from showing up
setTimeout(() => {
this.updateCompositeSwitcher();
}, 0);
}
public layout(dimension: Dimension): void {
this.dimension = dimension;
this.updateCompositeSwitcher();
}
public store(): void {
this.storageService.store(this.options.storageId, JSON.stringify(this.pinnedComposites), StorageScope.GLOBAL);
}
public dispose(): void {
this.toDispose = dispose(this.toDispose);
}
}

View file

@ -0,0 +1,542 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import nls = require('vs/nls');
import { Action } from 'vs/base/common/actions';
import { TPromise } from 'vs/base/common/winjs.base';
import * as dom from 'vs/base/browser/dom';
import { Builder, $ } from 'vs/base/browser/builder';
import { BaseActionItem, IBaseActionItemOptions, Separator } from 'vs/base/browser/ui/actionbar/actionbar';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { dispose } from 'vs/base/common/lifecycle';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IThemeService, ITheme } from 'vs/platform/theme/common/themeService';
import { IActivityBarService, TextBadge, NumberBadge, IBadge, IconBadge, ProgressBadge } from 'vs/workbench/services/activity/common/activityBarService';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { contrastBorder } from 'vs/platform/theme/common/colorRegistry';
import { ACTIVITY_BAR_BADGE_FOREGROUND, ACTIVITY_BAR_DRAG_AND_DROP_BACKGROUND, ACTIVITY_BAR_BADGE_BACKGROUND, ACTIVITY_BAR_FOREGROUND } from 'vs/workbench/common/theme';
import { DelayedDragHandler } from 'vs/base/browser/dnd';
import { IActivity } from 'vs/workbench/common/activity';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import Event, { Emitter } from 'vs/base/common/event';
export interface ICompositeActivity {
badge: IBadge;
clazz: string;
}
export class ActivityAction extends Action {
private badge: IBadge;
private _onDidChangeBadge = new Emitter<this>();
constructor(private _activity: IActivity) {
super(_activity.id, _activity.name, _activity.cssClass);
this.badge = null;
}
public get activity(): IActivity {
return this._activity;
}
public get onDidChangeBadge(): Event<this> {
return this._onDidChangeBadge.event;
}
public activate(): void {
if (!this.checked) {
this._setChecked(true);
}
}
public deactivate(): void {
if (this.checked) {
this._setChecked(false);
}
}
public getBadge(): IBadge {
return this.badge;
}
public setBadge(badge: IBadge): void {
this.badge = badge;
this._onDidChangeBadge.fire(this);
}
}
export class ActivityActionItem extends BaseActionItem {
protected $container: Builder;
protected $label: Builder;
protected $badge: Builder;
private $badgeContent: Builder;
private mouseUpTimeout: number;
constructor(
action: ActivityAction,
options: IBaseActionItemOptions,
@IThemeService protected themeService: IThemeService
) {
super(null, action, options);
this.themeService.onThemeChange(this.onThemeChange, this, this._callOnDispose);
action.onDidChangeBadge(this.handleBadgeChangeEvenet, this, this._callOnDispose);
}
protected get activity(): IActivity {
return (this._action as ActivityAction).activity;
}
protected updateStyles(): void {
const theme = this.themeService.getTheme();
// Label
if (this.$label) {
const background = theme.getColor(ACTIVITY_BAR_FOREGROUND);
this.$label.style('background-color', background ? background.toString() : null);
}
// Badge
if (this.$badgeContent) {
const badgeForeground = theme.getColor(ACTIVITY_BAR_BADGE_FOREGROUND);
const badgeBackground = theme.getColor(ACTIVITY_BAR_BADGE_BACKGROUND);
const contrastBorderColor = theme.getColor(contrastBorder);
this.$badgeContent.style('color', badgeForeground ? badgeForeground.toString() : null);
this.$badgeContent.style('background-color', badgeBackground ? badgeBackground.toString() : null);
this.$badgeContent.style('border-style', contrastBorderColor ? 'solid' : null);
this.$badgeContent.style('border-width', contrastBorderColor ? '1px' : null);
this.$badgeContent.style('border-color', contrastBorderColor ? contrastBorderColor.toString() : null);
}
}
public render(container: HTMLElement): void {
super.render(container);
// Make the container tab-able for keyboard navigation
this.$container = $(container).attr({
tabIndex: '0',
role: 'button',
title: this.activity.name
});
// Try hard to prevent keyboard only focus feedback when using mouse
this.$container.on(dom.EventType.MOUSE_DOWN, () => {
this.$container.addClass('clicked');
});
this.$container.on(dom.EventType.MOUSE_UP, () => {
if (this.mouseUpTimeout) {
clearTimeout(this.mouseUpTimeout);
}
this.mouseUpTimeout = setTimeout(() => {
this.$container.removeClass('clicked');
}, 800); // delayed to prevent focus feedback from showing on mouse up
});
// Label
this.$label = $('a.action-label').appendTo(this.builder);
if (this.activity.cssClass) {
this.$label.addClass(this.activity.cssClass);
}
this.$badge = this.builder.clone().div({ 'class': 'badge' }, (badge: Builder) => {
this.$badgeContent = badge.div({ 'class': 'badge-content' });
});
this.$badge.hide();
this.updateStyles();
}
private onThemeChange(theme: ITheme): void {
this.updateStyles();
}
public setBadge(badge: IBadge): void {
this.updateBadge(badge);
}
protected updateBadge(badge: IBadge): void {
this.$badgeContent.empty();
this.$badge.hide();
if (badge) {
// Number
if (badge instanceof NumberBadge) {
if (badge.number) {
this.$badgeContent.text(badge.number > 99 ? '99+' : badge.number.toString());
this.$badge.show();
}
}
// Text
else if (badge instanceof TextBadge) {
this.$badgeContent.text(badge.text);
this.$badge.show();
}
// Text
else if (badge instanceof IconBadge) {
this.$badge.show();
}
// Progress
else if (badge instanceof ProgressBadge) {
this.$badge.show();
}
}
// Title
let title: string;
if (badge && badge.getDescription()) {
if (this.activity.name) {
title = nls.localize('badgeTitle', "{0} - {1}", this.activity.name, badge.getDescription());
} else {
title = badge.getDescription();
}
} else {
title = this.activity.name;
}
[this.$label, this.$badge, this.$container].forEach(b => {
if (b) {
b.attr('aria-label', title);
b.title(title);
}
});
}
private handleBadgeChangeEvenet(): void {
const action = this.getAction();
if (action instanceof ActivityAction) {
this.updateBadge(action.getBadge());
}
}
public dispose(): void {
super.dispose();
if (this.mouseUpTimeout) {
clearTimeout(this.mouseUpTimeout);
}
this.$badge.destroy();
}
}
export class CompositeOverflowActivityAction extends ActivityAction {
constructor(
private showMenu: () => void
) {
super({
id: 'activitybar.additionalComposites.action',
name: nls.localize('additionalViews', "Additional Views"),
cssClass: 'toggle-more'
});
}
public run(event: any): TPromise<any> {
this.showMenu();
return TPromise.as(true);
}
}
export class CompositeOverflowActivityActionItem extends ActivityActionItem {
private name: string;
private cssClass: string;
private actions: Action[];
constructor(
action: ActivityAction,
private getOverflowingComposites: () => { id: string, name: string }[],
private getActiveCompositeId: () => string,
private getBadge: (compositeId: string) => IBadge,
private getCompositeOpenAction: (compositeId: string) => Action,
@IInstantiationService private instantiationService: IInstantiationService,
@IContextMenuService private contextMenuService: IContextMenuService,
@IThemeService themeService: IThemeService
) {
super(action, null, themeService);
this.cssClass = action.class;
this.name = action.label;
}
public showMenu(): void {
if (this.actions) {
dispose(this.actions);
}
this.actions = this.getActions();
this.contextMenuService.showContextMenu({
getAnchor: () => this.builder.getHTMLElement(),
getActions: () => TPromise.as(this.actions),
onHide: () => dispose(this.actions)
});
}
private getActions(): Action[] {
return this.getOverflowingComposites().map(composite => {
const action = this.getCompositeOpenAction(composite.id);
action.radio = this.getActiveCompositeId() === action.id;
const badge = this.getBadge(composite.id);
let suffix: string | number;
if (badge instanceof NumberBadge) {
suffix = badge.number;
} else if (badge instanceof TextBadge) {
suffix = badge.text;
}
if (suffix) {
action.label = nls.localize('numberBadge', "{0} ({1})", composite.name, suffix);
} else {
action.label = composite.name;
}
return action;
});
}
public dispose(): void {
super.dispose();
this.actions = dispose(this.actions);
}
}
class ManageExtensionAction extends Action {
constructor(
@ICommandService private commandService: ICommandService
) {
super('activitybar.manage.extension', nls.localize('manageExtension', "Manage Extension"));
}
public run(id: string): TPromise<any> {
return this.commandService.executeCommand('_extensions.manage', id);
}
}
export class CompositeActionItem extends ActivityActionItem {
private static manageExtensionAction: ManageExtensionAction;
private static draggedCompositeId: string;
private compositeActivity: IActivity;
private cssClass: string;
constructor(
private compositeActivityAction: ActivityAction,
private toggleCompositePinnedAction: Action,
@IContextMenuService private contextMenuService: IContextMenuService,
@IActivityBarService private activityBarService: IActivityBarService,
@IKeybindingService private keybindingService: IKeybindingService,
@IInstantiationService instantiationService: IInstantiationService,
@IThemeService themeService: IThemeService
) {
super(compositeActivityAction, { draggable: true }, themeService);
this.cssClass = compositeActivityAction.class;
if (!CompositeActionItem.manageExtensionAction) {
CompositeActionItem.manageExtensionAction = instantiationService.createInstance(ManageExtensionAction);
}
}
protected get activity(): IActivity {
if (!this.compositeActivity) {
let activityName: string;
const keybinding = this.getKeybindingLabel(this.compositeActivityAction.activity.id);
if (keybinding) {
activityName = nls.localize('titleKeybinding', "{0} ({1})", this.compositeActivityAction.activity.name, keybinding);
} else {
activityName = this.compositeActivityAction.activity.name;
}
this.compositeActivity = {
id: this.compositeActivityAction.activity.id,
cssClass: this.cssClass,
name: activityName
};
}
return this.compositeActivity;
}
private getKeybindingLabel(id: string): string {
const kb = this.keybindingService.lookupKeybinding(id);
if (kb) {
return kb.getLabel();
}
return null;
}
public render(container: HTMLElement): void {
super.render(container);
this.$container.on('contextmenu', e => {
dom.EventHelper.stop(e, true);
this.showContextMenu(container);
});
// Allow to drag
this.$container.on(dom.EventType.DRAG_START, (e: DragEvent) => {
e.dataTransfer.effectAllowed = 'move';
this.setDraggedComposite(this.activity.id);
// Trigger the action even on drag start to prevent clicks from failing that started a drag
if (!this.getAction().checked) {
this.getAction().run();
}
});
// Drag enter
let counter = 0; // see https://github.com/Microsoft/vscode/issues/14470
this.$container.on(dom.EventType.DRAG_ENTER, (e: DragEvent) => {
const draggedCompositeId = CompositeActionItem.getDraggedCompositeId();
if (draggedCompositeId && draggedCompositeId !== this.activity.id) {
counter++;
this.updateFromDragging(container, true);
}
});
// Drag leave
this.$container.on(dom.EventType.DRAG_LEAVE, (e: DragEvent) => {
const draggedCompositeId = CompositeActionItem.getDraggedCompositeId();
if (draggedCompositeId) {
counter--;
if (counter === 0) {
this.updateFromDragging(container, false);
}
}
});
// Drag end
this.$container.on(dom.EventType.DRAG_END, (e: DragEvent) => {
const draggedCompositeId = CompositeActionItem.getDraggedCompositeId();
if (draggedCompositeId) {
counter = 0;
this.updateFromDragging(container, false);
CompositeActionItem.clearDraggedComposite();
}
});
// Drop
this.$container.on(dom.EventType.DROP, (e: DragEvent) => {
dom.EventHelper.stop(e, true);
const draggedCompositeId = CompositeActionItem.getDraggedCompositeId();
if (draggedCompositeId && draggedCompositeId !== this.activity.id) {
this.updateFromDragging(container, false);
CompositeActionItem.clearDraggedComposite();
this.activityBarService.move(draggedCompositeId, this.activity.id);
}
});
// Activate on drag over to reveal targets
[this.$badge, this.$label].forEach(b => new DelayedDragHandler(b.getHTMLElement(), () => {
if (!CompositeActionItem.getDraggedCompositeId() && !this.getAction().checked) {
this.getAction().run();
}
}));
this.updateStyles();
}
private updateFromDragging(element: HTMLElement, isDragging: boolean): void {
const theme = this.themeService.getTheme();
const dragBackground = theme.getColor(ACTIVITY_BAR_DRAG_AND_DROP_BACKGROUND);
element.style.backgroundColor = isDragging && dragBackground ? dragBackground.toString() : null;
}
public static getDraggedCompositeId(): string {
return CompositeActionItem.draggedCompositeId;
}
private setDraggedComposite(compositeId: string): void {
CompositeActionItem.draggedCompositeId = compositeId;
}
public static clearDraggedComposite(): void {
CompositeActionItem.draggedCompositeId = void 0;
}
private showContextMenu(container: HTMLElement): void {
const actions: Action[] = [this.toggleCompositePinnedAction];
if ((<any>this.compositeActivityAction.activity).extensionId) {
actions.push(new Separator());
actions.push(CompositeActionItem.manageExtensionAction);
}
const isPinned = this.activityBarService.isPinned(this.activity.id);
if (isPinned) {
this.toggleCompositePinnedAction.label = nls.localize('removeFromActivityBar', "Hide from Activity Bar");
this.toggleCompositePinnedAction.checked = false;
} else {
this.toggleCompositePinnedAction.label = nls.localize('keepInActivityBar', "Keep in Activity Bar");
}
this.contextMenuService.showContextMenu({
getAnchor: () => container,
getActionsContext: () => this.activity.id,
getActions: () => TPromise.as(actions)
});
}
public focus(): void {
this.$container.domFocus();
}
protected _updateClass(): void {
if (this.cssClass) {
this.$badge.removeClass(this.cssClass);
}
this.cssClass = this.getAction().class;
this.$badge.addClass(this.cssClass);
}
protected _updateChecked(): void {
if (this.getAction().checked) {
this.$container.addClass('checked');
} else {
this.$container.removeClass('checked');
}
}
protected _updateEnabled(): void {
if (this.getAction().enabled) {
this.builder.removeClass('disabled');
} else {
this.builder.addClass('disabled');
}
}
public dispose(): void {
super.dispose();
CompositeActionItem.clearDraggedComposite();
this.$label.destroy();
}
}