提交 b3e25d3c 编写于 作者: B Benjamin Pasero

allow to DND viewlets in activity bar and keep order (for #15155)

上级 5926ba02
......@@ -31,6 +31,10 @@ export interface IActionItem extends IEventEmitter {
dispose(): void;
}
export interface IBaseActionItemOptions {
draggable?: boolean;
}
export class BaseActionItem extends EventEmitter implements IActionItem {
public builder: Builder;
......@@ -41,7 +45,7 @@ export class BaseActionItem extends EventEmitter implements IActionItem {
private gesture: Gesture;
private _actionRunner: IActionRunner;
constructor(context: any, action: IAction) {
constructor(context: any, action: IAction, protected options?: IBaseActionItemOptions) {
super();
this._callOnDispose = [];
......@@ -107,6 +111,11 @@ export class BaseActionItem extends EventEmitter implements IActionItem {
this.builder = $(container);
this.gesture = new Gesture(container);
const enableDragging = this.options && this.options.draggable;
if (enableDragging) {
container.draggable = true;
}
this.builder.on(EventType.Tap, e => this.onClick(e));
if (platform.isMacintosh) {
......@@ -114,11 +123,15 @@ export class BaseActionItem extends EventEmitter implements IActionItem {
}
this.builder.on(DOM.EventType.MOUSE_DOWN, (e: MouseEvent) => {
DOM.EventHelper.stop(e);
if (!enableDragging) {
DOM.EventHelper.stop(e); // do not run when dragging is on because that would disable it
}
if (this._action.enabled) {
this.builder.addClass('active');
}
});
this.builder.on(DOM.EventType.CLICK, (e: MouseEvent) => {
DOM.EventHelper.stop(e, true);
setTimeout(() => this.onClick(e), 50);
......@@ -206,7 +219,7 @@ export class Separator extends Action {
}
}
export interface IActionItemOptions {
export interface IActionItemOptions extends IBaseActionItemOptions {
icon?: boolean;
label?: boolean;
keybinding?: string;
......@@ -219,7 +232,7 @@ export class ActionItem extends BaseActionItem {
private cssClass: string;
constructor(context: any, action: IAction, options: IActionItemOptions = {}) {
super(context, action);
super(context, action, options);
this.options = options;
this.options.icon = options.icon !== undefined ? options.icon : false;
......
......@@ -21,7 +21,7 @@ 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 { dispose, IDisposable } from 'vs/base/common/lifecycle';
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';
......@@ -102,102 +102,11 @@ export class ViewletActivityAction extends ActivityAction {
}
}
export class ViewletOverflowActivityAction extends ActivityAction {
constructor(
private showMenu: () => void
) {
super('activitybar.additionalViewlets.action', nls.localize('additionalViewlets', "Additional Viewlets"), 'toggle-more');
}
public run(event): TPromise<any> {
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 getOverflowingViewlets: () => 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;
}
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 {
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);
}
}
export class ActivityActionItem extends BaseActionItem {
private static manageExtensionAction: ManageExtensionAction;
private static toggleViewletPinnedAction: ToggleViewletPinnedAction;
private static draggedViewlet: ViewletDescriptor;
private $e: Builder;
private name: string;
......@@ -205,7 +114,7 @@ export class ActivityActionItem extends BaseActionItem {
private cssClass: string;
private $badge: Builder;
private $badgeContent: Builder;
private toDispose: IDisposable[];
private mouseUpTimeout: number;
constructor(
action: ActivityAction,
......@@ -216,11 +125,12 @@ export class ActivityActionItem extends BaseActionItem {
@IKeybindingService private keybindingService: IKeybindingService,
@IInstantiationService instantiationService: IInstantiationService
) {
super(null, action);
super(null, action, { draggable: true });
this.cssClass = action.class;
this.name = viewlet.name;
this._keybinding = this.getKeybindingLabel(viewlet.id);
action.onDidChangeBadge(this.handleBadgeChangeEvenet, this, this._callOnDispose);
if (!ActivityActionItem.manageExtensionAction) {
......@@ -249,11 +159,26 @@ export class ActivityActionItem extends BaseActionItem {
role: 'button'
}).appendTo(this.builder);
// Try hard to prevent keyboard only focus feedback when using mouse
this.$e.on(DOM.EventType.MOUSE_DOWN, () => {
this.$e.addClass('clicked');
});
this.$e.on(DOM.EventType.MOUSE_UP, () => {
if (this.mouseUpTimeout) {
clearTimeout(this.mouseUpTimeout);
}
this.mouseUpTimeout = setTimeout(() => {
this.$e.removeClass('clicked');
}, 800); // delayed to prevent focus feedback from showing on mouse up
});
$(container).on('contextmenu', e => {
DOM.EventHelper.stop(e, true);
this.showContextMenu(container);
}, this.toDispose);
});
if (this.cssClass) {
this.$e.addClass(this.cssClass);
......@@ -269,10 +194,78 @@ export class ActivityActionItem extends BaseActionItem {
// Activate on drag over to reveal targets
[this.$badge, this.$e].forEach(b => new DelayedDragHandler(b.getHTMLElement(), () => {
if (!this.getAction().checked) {
if (!this.getDraggedViewlet() && !this.getAction().checked) {
this.getAction().run();
}
}));
// Allow to drag
$(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
$(container).on(DOM.EventType.DRAG_ENTER, (e: DragEvent) => {
const draggedViewlet = this.getDraggedViewlet();
if (draggedViewlet && draggedViewlet.id !== this.viewlet.id) {
counter++;
DOM.addClass(container, 'dropfeedback');
}
});
// Drag leave
$(container).on(DOM.EventType.DRAG_LEAVE, (e: DragEvent) => {
const draggedViewlet = this.getDraggedViewlet();
if (draggedViewlet) {
counter--;
if (counter === 0) {
DOM.removeClass(container, 'dropfeedback');
}
}
});
// Drag end
$(container).on(DOM.EventType.DRAG_END, (e: DragEvent) => {
const draggedViewlet = this.getDraggedViewlet();
if (draggedViewlet) {
counter = 0;
DOM.removeClass(container, 'dropfeedback');
this.clearDraggedViewlet();
}
});
// Drop
$(container).on(DOM.EventType.DROP, (e: DragEvent) => {
const draggedViewlet = this.getDraggedViewlet();
if (draggedViewlet && draggedViewlet.id !== this.viewlet.id) {
DOM.EventHelper.stop(e, true);
DOM.removeClass(container, 'dropfeedback');
this.clearDraggedViewlet();
this.activityBarService.move(draggedViewlet.id, this.viewlet.id);
}
});
}
private getDraggedViewlet(): ViewletDescriptor {
return ActivityActionItem.draggedViewlet;
}
private setDraggedViewlet(viewlet: ViewletDescriptor): void {
ActivityActionItem.draggedViewlet = viewlet;
}
private clearDraggedViewlet(): void {
ActivityActionItem.draggedViewlet = void 0;
}
private showContextMenu(container: HTMLElement): void {
......@@ -391,13 +384,109 @@ export class ActivityActionItem extends BaseActionItem {
public dispose(): void {
super.dispose();
dispose(this.toDispose);
this.clearDraggedViewlet();
if (this.mouseUpTimeout) {
clearTimeout(this.mouseUpTimeout);
}
this.$badge.destroy();
this.$e.destroy();
}
}
export class ViewletOverflowActivityAction extends ActivityAction {
constructor(
private showMenu: () => void
) {
super('activitybar.additionalViewlets.action', nls.localize('additionalViewlets', "Additional Viewlets"), 'toggle-more');
}
public run(event): TPromise<any> {
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 getOverflowingViewlets: () => 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;
}
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 {
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(
......
......@@ -38,7 +38,7 @@ interface IViewletActivity {
export class ActivitybarPart extends Part implements IActivityBarService {
private static readonly ACTIVITY_ACTION_HEIGHT = 50;
private static readonly UNPINNED_VIEWLETS = 'workbench.activity.unpinnedViewlets';
private static readonly PINNED_VIEWLETS = 'workbench.activity.pinnedViewlets';
public _serviceBrand: any;
......@@ -53,7 +53,7 @@ export class ActivitybarPart extends Part implements IActivityBarService {
private viewletIdToActivity: { [viewletId: string]: IViewletActivity; };
private memento: any;
private unpinnedViewlets: string[];
private pinnedViewlets: string[];
private activeUnpinnedViewlet: ViewletDescriptor;
constructor(
......@@ -73,7 +73,7 @@ export class ActivitybarPart extends Part implements IActivityBarService {
this.viewletIdToActivity = Object.create(null);
this.memento = this.getMemento(this.storageService, MementoScope.GLOBAL);
this.unpinnedViewlets = this.memento[ActivitybarPart.UNPINNED_VIEWLETS] || [];
this.pinnedViewlets = this.memento[ActivitybarPart.PINNED_VIEWLETS] || this.viewletService.getViewlets().map(v => v.id);
// Update viewlet switcher when external viewlets become ready
this.extensionService.onReady().then(() => this.updateViewletSwitcher());
......@@ -280,7 +280,7 @@ export class ActivitybarPart extends Part implements IActivityBarService {
}
private getPinnedViewlets(): ViewletDescriptor[] {
return this.viewletService.getViewlets().filter(viewlet => this.isPinned(viewlet.id));
return this.pinnedViewlets.map(viewletId => this.viewletService.getViewlet(viewletId));
}
private pullViewlet(viewletId: string): void {
......@@ -342,16 +342,16 @@ export class ActivitybarPart extends Part implements IActivityBarService {
unpinPromise.then(() => {
// then add to unpinned and update switcher
this.unpinnedViewlets.push(viewletId);
this.unpinnedViewlets = arrays.distinct(this.unpinnedViewlets);
// then remove from pinned and update switcher
const index = this.pinnedViewlets.indexOf(viewletId);
this.pinnedViewlets.splice(index, 1);
this.updateViewletSwitcher();
});
}
public isPinned(viewletId: string): boolean {
return this.unpinnedViewlets.indexOf(viewletId) === -1;
return this.pinnedViewlets.indexOf(viewletId) >= 0;
}
public pin(viewletId: string): void {
......@@ -363,13 +363,32 @@ export class ActivitybarPart extends Part implements IActivityBarService {
this.viewletService.openViewlet(viewletId, true).then(() => {
// then update
const index = this.unpinnedViewlets.indexOf(viewletId);
this.unpinnedViewlets.splice(index, 1);
this.pinnedViewlets.push(viewletId);
this.pinnedViewlets = arrays.distinct(this.pinnedViewlets);
this.updateViewletSwitcher();
});
}
public move(viewletId: string, toViewletId: string): void {
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);
}
/**
* Layout title, content and status area in the given dimension.
*/
......@@ -398,7 +417,7 @@ export class ActivitybarPart extends Part implements IActivityBarService {
public shutdown(): void {
// Persist Hidden State
this.memento[ActivitybarPart.UNPINNED_VIEWLETS] = this.unpinnedViewlets;
this.memento[ActivitybarPart.PINNED_VIEWLETS] = this.pinnedViewlets;
// Pass to super
super.shutdown();
......
......@@ -9,6 +9,10 @@
margin: 10px 0;
}
.monaco-workbench > .activitybar > .content .monaco-action-bar .action-item.dropfeedback {
background-color: #403F3F;
}
.monaco-workbench > .activitybar > .content .monaco-action-bar .action-label {
display: flex;
overflow: hidden;
......@@ -34,8 +38,8 @@
border-left: 2px solid;
}
.monaco-workbench > .activitybar > .content .monaco-action-bar .action-label.toggle-more {
background-image: url('ellipsis-global.svg');
.monaco-workbench > .activitybar > .content .monaco-action-bar .action-item .action-label.clicked:focus:before {
border-left: none !important; /* no focus feedback when using mouse */
}
.vs .monaco-workbench > .activitybar > .content .monaco-action-bar .action-item .action-label:focus:before,
......@@ -55,6 +59,10 @@
right: 1px;
}
.monaco-workbench > .activitybar > .content .monaco-action-bar .action-label.toggle-more {
background-image: url('ellipsis-global.svg');
}
.monaco-workbench > .activitybar > .content .monaco-action-bar .action-label > .label {
flex: 1 1 auto;
flex: 1 1 auto;
......
......@@ -85,4 +85,9 @@ export interface IActivityBarService {
* Find out if a viewlet is pinned in the activity bar.
*/
isPinned(viewletId: string): boolean;
/**
* Reorder viewlet ordering by moving a viewlet to the location of another viewlet.
*/
move(viewletId: string, toViewletId: string): void;
}
\ No newline at end of file
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册