diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index f3fe115dd45da692f47011711d8bcd8760ef6325..b197ecacbe7742ae4fe1c08eb8633183ec3efa46 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -522,6 +522,7 @@ export class WorkbenchLayout implements IVerticalSashLayoutProvider, IHorizontal this.editor.layout(new Dimension(editorSize.width, editorSize.height)); this.sidebar.layout(sidebarSize); this.panel.layout(panelDimension); + this.activitybar.layout(activityBarSize); // Propagate to Context View this.contextViewService.layout(); diff --git a/src/vs/workbench/browser/parts/activitybar/activityAction.ts b/src/vs/workbench/browser/parts/activitybar/activityAction.ts index f828641da676739e962c205aa67671eafe6d3ce1..7d35646c990d32ab137446f5ddde253ce242aa98 100644 --- a/src/vs/workbench/browser/parts/activitybar/activityAction.ts +++ b/src/vs/workbench/browser/parts/activitybar/activityAction.ts @@ -68,12 +68,11 @@ export class ViewletActivityAction extends ActivityAction { private lastRun: number = 0; constructor( - id: string, private viewlet: ViewletDescriptor, @IViewletService private viewletService: IViewletService, @IPartService private partService: IPartService ) { - super(id, viewlet.name, viewlet.cssClass); + super(viewlet.id, viewlet.name, viewlet.cssClass); } public run(event): TPromise { @@ -103,9 +102,94 @@ export class ViewletActivityAction extends ActivityAction { } } +export class ViewletOverflowActivityAction extends ActivityAction { + + constructor( + private viewlets: ViewletDescriptor[], + private showMenu: () => void + ) { + super('activitybar.additionalViewlets.action', nls.localize('additionalViewlets', "Additional Viewlets"), 'toggle-more'); + } + + public run(event): TPromise { + this.showMenu(); + + return TPromise.as(true); + } +} + +export class ViewletOverflowActivityActionItem extends BaseActionItem { + private $e: Builder; + private name: string; + private cssClass: string; + private actions: OpenViewletAction[]; + + constructor( + action: ActivityAction, + private viewlets: ViewletDescriptor[], + private getBadge: (viewlet: ViewletDescriptor) => IBadge, + @IInstantiationService private instantiationService: IInstantiationService, + @IViewletService private viewletService: IViewletService, + @IContextMenuService private contextMenuService: IContextMenuService, + ) { + super(null, action); + + this.cssClass = action.class; + this.name = action.label; + this.actions = viewlets.map(viewlet => this.instantiationService.createInstance(OpenViewletAction, viewlet)); + } + + public render(container: HTMLElement): void { + super.render(container); + + this.$e = $('a.action-label').attr({ + tabIndex: '0', + role: 'button', + title: this.name, + class: this.cssClass + }).appendTo(this.builder); + } + + public showMenu(): void { + this.updateActions(); + + this.contextMenuService.showContextMenu({ + getAnchor: () => this.builder.getHTMLElement(), + getActions: () => TPromise.as(this.actions) + }); + } + + private updateActions(): void { + const activeViewlet = this.viewletService.getActiveViewlet(); + + this.actions.forEach(action => { + action.checked = 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; + } + }); + } + + public dispose(): void { + super.dispose(); + + this.actions = dispose(this.actions); + } +} + let manageExtensionAction: ManageExtensionAction; export class ActivityActionItem extends BaseActionItem { - private $e: Builder; private name: string; private _keybinding: string; @@ -289,10 +373,35 @@ class ManageExtensionAction extends Action { constructor( @ICommandService private commandService: ICommandService ) { - super('statusbar.manage.extension', nls.localize('manageExtension', "Manage Extension")); + super('activitybar.manage.extension', nls.localize('manageExtension', "Manage Extension")); } public run(extensionId: string): TPromise { return this.commandService.executeCommand('_extensions.manage', extensionId); } +} + +class OpenViewletAction extends Action { + + constructor( + public viewlet: ViewletDescriptor, + @IPartService private partService: IPartService, + @IViewletService private viewletService: IViewletService + ) { + super(viewlet.id, viewlet.name); + } + + public run(): TPromise { + 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) { + this.partService.setSideBarHidden(true); + } else { + this.viewletService.openViewlet(this.viewlet.id, true).done(null, errors.onUnexpectedError); + } + + return TPromise.as(true); + } } \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts index 2495758d82822a0db0ba3e1492b00148f583000c..529565db35f0cfc1da96e81b7fc8f0de16b5db98 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts @@ -7,13 +7,13 @@ import 'vs/css!./media/activitybarpart'; import nls = require('vs/nls'); -import { Builder, $ } from 'vs/base/browser/builder'; +import { Builder, $, Dimension } from 'vs/base/browser/builder'; import { Action } from 'vs/base/common/actions'; import { ActionsOrientation, ActionBar, IActionItem } from 'vs/base/browser/ui/actionbar/actionbar'; import { IComposite } from 'vs/workbench/common/composite'; import { ViewletDescriptor } from 'vs/workbench/browser/viewlet'; import { Part } from 'vs/workbench/browser/part'; -import { ViewletActivityAction, ActivityAction, ActivityActionItem } from 'vs/workbench/browser/parts/activitybar/activityAction'; +import { ViewletActivityAction, ActivityAction, ActivityActionItem, ViewletOverflowActivityAction, ViewletOverflowActivityActionItem } from 'vs/workbench/browser/parts/activitybar/activityAction'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IActivityService, IBadge } from 'vs/workbench/services/activity/common/activityService'; import { IPartService } from 'vs/workbench/services/part/common/partService'; @@ -21,12 +21,26 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IExtensionService } from 'vs/platform/extensions/common/extensions'; +interface IViewletActivity { + badge: IBadge; + clazz: string; +} + export class ActivitybarPart extends Part implements IActivityService { + + private static ACTIVITY_ACTION_HEIGHT = 50; + public _serviceBrand: any; + private dimension: Dimension; + private viewletSwitcherBar: ActionBar; + private viewletOverflowAction: ViewletOverflowActivityAction; + private viewletOverflowActionItem: ViewletOverflowActivityActionItem; + private activityActionItems: { [actionId: string]: IActionItem; }; private viewletIdToActions: { [viewletId: string]: ActivityAction; }; + private viewletIdToActivity: { [viewletId: string]: IViewletActivity; }; constructor( id: string, @@ -38,8 +52,9 @@ export class ActivitybarPart extends Part implements IActivityService { ) { super(id); - this.activityActionItems = {}; - this.viewletIdToActions = {}; + this.activityActionItems = Object.create(null); + this.viewletIdToActions = Object.create(null); + this.viewletIdToActivity = Object.create(null); // Update viewlet switcher when external viewlets become ready this.extensionService.onReady().then(() => this.updateViewletSwitcher()); @@ -68,7 +83,9 @@ export class ActivitybarPart extends Part implements IActivityService { } } - public showActivity(viewletId: string, badge: IBadge, clazz?: string): void { + public showActivity(viewletId: string, badge?: IBadge, clazz?: string): void { + + // Update Action with activity const action = this.viewletIdToActions[viewletId]; if (action) { action.setBadge(badge); @@ -76,6 +93,13 @@ export class ActivitybarPart extends Part implements IActivityService { action.class = clazz; } } + + // Keep for future use + if (badge) { + this.viewletIdToActivity[viewletId] = { badge, clazz }; + } else { + delete this.viewletIdToActivity[viewletId]; + } } public clearActivity(viewletId: string): void { @@ -94,46 +118,97 @@ export class ActivitybarPart extends Part implements IActivityService { private createViewletSwitcher(div: Builder): void { this.viewletSwitcherBar = new ActionBar(div, { - actionItemProvider: (action: Action) => this.activityActionItems[action.id], + actionItemProvider: (action: Action) => action instanceof ViewletOverflowActivityAction ? this.viewletOverflowActionItem : this.activityActionItems[action.id], orientation: ActionsOrientation.VERTICAL, - ariaLabel: nls.localize('activityBarAriaLabel', "Active View Switcher") + ariaLabel: nls.localize('activityBarAriaLabel', "Active View Switcher"), + animated: false }); this.updateViewletSwitcher(); } private updateViewletSwitcher() { - const viewlets = this.viewletService.getViewlets(); + let viewlets = this.viewletService.getViewlets(); + let viewletsToShow = viewlets; + + // Ensure we are not showing more viewlets than we have height for + let overflows = false; + if (this.dimension) { + const maxVisible = Math.floor(this.dimension.height / ActivitybarPart.ACTIVITY_ACTION_HEIGHT); + overflows = viewlets.length > maxVisible; + + if (overflows) { + viewletsToShow = viewlets.slice(0, maxVisible - 1 /* make room for overflow action */); + } + } + + const visibleViewlets = Object.keys(this.viewletIdToActions); + const visibleViewletsChange = (viewletsToShow.length !== visibleViewlets.length); + + // 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 no longer needed - const newViewletIds = viewlets.map(v => v.id); - const existingViewletIds = Object.keys(this.viewletIdToActions); - existingViewletIds.forEach(viewletId => { - if (newViewletIds.indexOf(viewletId) === -1) { + // Pull out viewlets that overflow + 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 actionsToPush = viewlets + const newViewletsToShow = viewletsToShow .filter(viewlet => !this.viewletIdToActions[viewlet.id]) .map(viewlet => this.toAction(viewlet)); - // Add to viewlet switcher - this.viewletSwitcherBar.push(actionsToPush, { label: true, icon: true }); + // Update when we have new viewlets to show + if (newViewletsToShow.length) { - // Make sure to activate the active one - const activeViewlet = this.viewletService.getActiveViewlet(); - if (activeViewlet) { - const activeViewletEntry = this.viewletIdToActions[activeViewlet.getId()]; - if (activeViewletEntry) { - activeViewletEntry.activate(); + // 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 => { + const activity = this.viewletIdToActivity[viewletId]; + if (activity) { + this.showActivity(viewletId, activity.badge, activity.clazz); + } else { + this.showActivity(viewletId); + } + }); + } + + // Add overflow action as needed + if (visibleViewletsChange && overflows) { + const viewletsOverflowing = viewlets.slice(viewletsToShow.length); + + this.viewletOverflowAction = this.instantiationService.createInstance(ViewletOverflowActivityAction, viewletsOverflowing, () => this.viewletOverflowActionItem.showMenu()); + this.viewletOverflowActionItem = this.instantiationService.createInstance(ViewletOverflowActivityActionItem, this.viewletOverflowAction, viewletsOverflowing, viewlet => this.viewletIdToActivity[viewlet.id] && this.viewletIdToActivity[viewlet.id].badge); + + this.viewletSwitcherBar.push(this.viewletOverflowAction, { label: true, icon: true }); } } private pullViewlet(viewletId: string): void { const index = Object.keys(this.viewletIdToActions).indexOf(viewletId); + this.viewletSwitcherBar.pull(index); const action = this.viewletIdToActions[viewletId]; action.dispose(); @@ -142,18 +217,32 @@ export class ActivitybarPart extends Part implements IActivityService { const actionItem = this.activityActionItems[action.id]; actionItem.dispose(); delete this.activityActionItems[action.id]; - - this.viewletSwitcherBar.pull(index); } private toAction(viewlet: ViewletDescriptor): ActivityAction { - const action = this.instantiationService.createInstance(ViewletActivityAction, `${viewlet.id}.activity-bar-action`, viewlet); + const action = this.instantiationService.createInstance(ViewletActivityAction, viewlet); this.activityActionItems[action.id] = this.instantiationService.createInstance(ActivityActionItem, action, viewlet); this.viewletIdToActions[viewlet.id] = action; return action; - }; + } + + /** + * Layout title, content and status area in the given dimension. + */ + public layout(dimension: Dimension): Dimension[] { + + // Pass to super + const sizes = super.layout(dimension); + + this.dimension = sizes[1]; + + // Update switcher to handle overflow issues + this.updateViewletSwitcher(); + + return sizes; + } public dispose(): void { if (this.viewletSwitcherBar) { diff --git a/src/vs/workbench/browser/parts/activitybar/media/activityaction.css b/src/vs/workbench/browser/parts/activitybar/media/activityaction.css index 7fd426208ac7441f58c75c28857fffed63807731..a4d20c2462ffcbfaa45099e177c5828ac570521e 100644 --- a/src/vs/workbench/browser/parts/activitybar/media/activityaction.css +++ b/src/vs/workbench/browser/parts/activitybar/media/activityaction.css @@ -34,6 +34,10 @@ border-left: 2px solid; } +.monaco-workbench > .activitybar > .content .monaco-action-bar .action-label.toggle-more { + background-image: url('ellipsis-global.svg'); +} + .vs .monaco-workbench > .activitybar > .content .monaco-action-bar .action-item .action-label:focus:before, .vs-dark .monaco-workbench > .activitybar > .content .monaco-action-bar .action-item .action-label:focus:before { border-left-color: #007ACC; diff --git a/src/vs/workbench/browser/parts/activitybar/media/ellipsis-global.svg b/src/vs/workbench/browser/parts/activitybar/media/ellipsis-global.svg index e98af0d881a87a2819af5e3703d6d420f343a78b..5df227c5829561697cacfa56369daad8512f6f5f 100644 --- a/src/vs/workbench/browser/parts/activitybar/media/ellipsis-global.svg +++ b/src/vs/workbench/browser/parts/activitybar/media/ellipsis-global.svg @@ -1 +1 @@ - \ No newline at end of file +Ellipsis_32x \ No newline at end of file diff --git a/src/vs/workbench/services/contextview/electron-browser/contextmenuService.ts b/src/vs/workbench/services/contextview/electron-browser/contextmenuService.ts index 4f982c393c65287ef439546e14d53ed1a7d9fd3e..3242cf16db80ce4a6a037820467385fc80f573b0 100644 --- a/src/vs/workbench/services/contextview/electron-browser/contextmenuService.ts +++ b/src/vs/workbench/services/contextview/electron-browser/contextmenuService.ts @@ -82,6 +82,7 @@ export class ContextMenuService implements IContextMenuService { const item = new remote.MenuItem({ label: e.label, checked: !!e.checked, + type: !!e.checked ? 'radio' : void 0, accelerator, enabled: !!e.enabled, click: (menuItem, win, event) => {