From 788f2602c9c086d107ef0f26094241cb8a83ae38 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 6 Jul 2017 18:01:14 +0200 Subject: [PATCH] Support reordering of views in split view by drag and drop --- src/vs/base/browser/ui/splitview/splitview.ts | 140 +++++++++++++++++- src/vs/workbench/parts/views/browser/views.ts | 14 +- 2 files changed, 143 insertions(+), 11 deletions(-) diff --git a/src/vs/base/browser/ui/splitview/splitview.ts b/src/vs/base/browser/ui/splitview/splitview.ts index 06d5b9bd50c..661abc0a5ef 100644 --- a/src/vs/base/browser/ui/splitview/splitview.ts +++ b/src/vs/base/browser/ui/splitview/splitview.ts @@ -29,6 +29,7 @@ export enum ViewSizing { export interface IOptions { orientation?: Orientation; // default Orientation.VERTICAL + canChangeOrderByDragAndDrop?: boolean; } export interface ISashEvent { @@ -48,6 +49,7 @@ export interface IView extends ee.IEventEmitter { fixedSize: number; minimumSize: number; maximumSize: number; + draggableElement: HTMLElement; render(container: HTMLElement, orientation: Orientation): void; layout(size: number, orientation: Orientation): void; focus(): void; @@ -70,6 +72,7 @@ export abstract class View extends ee.EventEmitter implements IView { protected _sizing: ViewSizing; protected _fixedSize: number; protected _minimumSize: number; + protected _draggableElement: HTMLElement = null; constructor(opts: IViewOptions) { super(); @@ -84,6 +87,7 @@ export abstract class View extends ee.EventEmitter implements IView { get fixedSize(): number { return this._fixedSize; } get minimumSize(): number { return this.sizing === ViewSizing.Fixed ? this.fixedSize : this._minimumSize; } get maximumSize(): number { return this.sizing === ViewSizing.Fixed ? this.fixedSize : Number.POSITIVE_INFINITY; } + get draggableElement(): HTMLElement { return this._draggableElement; } protected setFlexible(size?: number): void { this._sizing = ViewSizing.Flexible; @@ -165,6 +169,7 @@ export abstract class HeaderView extends View { render(container: HTMLElement, orientation: Orientation): void { this.header = document.createElement('div'); this.header.className = 'header'; + this._draggableElement = this.header; let headerSize = this.headerSize + 'px'; @@ -476,10 +481,15 @@ function sum(arr: number[]): number { return arr.reduce((a, b) => a + b); } -export class SplitView implements +export interface SplitViewStyles { + dropBackground?: Color; +} + +export class SplitView extends lifecycle.Disposable implements sash.IHorizontalSashLayoutProvider, sash.IVerticalSashLayoutProvider { private orientation: Orientation; + private canDragAndDrop: boolean; private el: HTMLElement; private size: number; private viewElements: HTMLElement[]; @@ -488,6 +498,7 @@ export class SplitView implements private viewFocusPreviousListeners: lifecycle.IDisposable[]; private viewFocusNextListeners: lifecycle.IDisposable[]; private viewFocusListeners: lifecycle.IDisposable[]; + private viewDnDListeners: lifecycle.IDisposable[][]; private initialWeights: number[]; private sashOrientation: sash.Orientation; private sashes: sash.Sash[]; @@ -496,13 +507,22 @@ export class SplitView implements private layoutViewElement: (viewElement: HTMLElement, size: number) => void; private eventWrapper: (event: sash.ISashEvent) => ISashEvent; private animationTimeout: number; - private _onFocus: Emitter; private state: IState; + private draggedView: IView; + private dropBackground: Color; + + private _onFocus: Emitter = this._register(new Emitter()); + readonly onFocus: Event = this._onFocus.event; + + private _onDidOrderChange: Emitter = this._register(new Emitter()); + readonly onDidOrderChange: Event = this._onDidOrderChange.event; constructor(container: HTMLElement, options?: IOptions) { + super(); options = options || {}; this.orientation = types.isUndefined(options.orientation) ? Orientation.VERTICAL : options.orientation; + this.canDragAndDrop = !!options.canChangeOrderByDragAndDrop; this.el = document.createElement('div'); dom.addClass(this.el, 'monaco-split-view'); @@ -516,11 +536,11 @@ export class SplitView implements this.viewFocusPreviousListeners = []; this.viewFocusNextListeners = []; this.viewFocusListeners = []; + this.viewDnDListeners = []; this.initialWeights = []; this.sashes = []; this.sashesListeners = []; this.animationTimeout = null; - this._onFocus = new Emitter(); this.sashOrientation = this.orientation === Orientation.VERTICAL ? sash.Orientation.HORIZONTAL @@ -540,10 +560,6 @@ export class SplitView implements this.addView(new VoidView(), 1, 0); } - get onFocus(): Event { - return this._onFocus.event; - } - getViews(): T[] { return this.views.slice(0, this.views.length - 1); } @@ -579,6 +595,9 @@ export class SplitView implements this.el.insertBefore(viewElement, this.el.children.item(index)); } + // Listen to Drag and Drop + this.viewDnDListeners[index] = this.listenToDragAndDrop(view, viewElement); + // Add sash if (this.views.length > 2) { let s = new sash.Sash(this.el, this, { orientation: this.sashOrientation }); @@ -636,6 +655,9 @@ export class SplitView implements this.viewFocusNextListeners[index].dispose(); this.viewFocusNextListeners.splice(index, 1); + lifecycle.dispose(this.viewDnDListeners[index]); + this.viewDnDListeners.splice(index, 1); + this.views.splice(index, 1); this.initialWeights.splice(index, 1); this.el.removeChild(this.viewElements[index]); @@ -672,6 +694,108 @@ export class SplitView implements this.layoutViews(); } + style(styles: SplitViewStyles): void { + this.dropBackground = styles.dropBackground; + } + + private listenToDragAndDrop(view: IView, viewElement: HTMLElement): lifecycle.IDisposable[] { + if (!this.canDragAndDrop || view instanceof VoidView) { + return []; + } + + const disposables: lifecycle.IDisposable[] = []; + + // Allow to drag + if (view.draggableElement) { + view.draggableElement.draggable = true; + disposables.push(dom.addDisposableListener(view.draggableElement, dom.EventType.DRAG_START, (e: DragEvent) => { + e.dataTransfer.effectAllowed = 'move'; + this.draggedView = view; + })); + } + + // Drag enter + let counter = 0; // see https://github.com/Microsoft/vscode/issues/14470 + disposables.push(dom.addDisposableListener(viewElement, dom.EventType.DRAG_ENTER, (e: DragEvent) => { + if (this.draggedView && this.draggedView !== view) { + counter++; + this.updateFromDragging(view, viewElement, true); + } + })); + + // Drag leave + disposables.push(dom.addDisposableListener(viewElement, dom.EventType.DRAG_LEAVE, (e: DragEvent) => { + if (this.draggedView && this.draggedView !== view) { + counter--; + if (counter === 0) { + this.updateFromDragging(view, viewElement, false); + } + } + })); + + // Drag end + disposables.push(dom.addDisposableListener(viewElement, dom.EventType.DRAG_END, (e: DragEvent) => { + if (this.draggedView) { + counter = 0; + this.updateFromDragging(view, viewElement, false); + this.draggedView = null; + } + })); + + // Drop + disposables.push(dom.addDisposableListener(viewElement, dom.EventType.DROP, (e: DragEvent) => { + dom.EventHelper.stop(e, true); + counter = 0; + this.updateFromDragging(view, viewElement, false); + if (this.draggedView && this.draggedView !== view) { + this.move(this.views.indexOf(this.draggedView), this.views.indexOf(view)); + } + this.draggedView = null; + })); + + return disposables; + } + + private updateFromDragging(view: IView, viewElement: HTMLElement, isDragging: boolean): void { + viewElement.style.backgroundColor = isDragging && this.dropBackground ? this.dropBackground.toString() : null; + } + + private move(fromIndex: number, toIndex: number): void { + if (fromIndex < 0 || toIndex > this.views.length - 2) { + return; + } + + const [viewChangeListener] = this.viewChangeListeners.splice(fromIndex, 1); + this.viewChangeListeners.splice(toIndex, 0, viewChangeListener); + + const [viewFocusPreviousListener] = this.viewFocusPreviousListeners.splice(fromIndex, 1); + this.viewFocusPreviousListeners.splice(toIndex, 0, viewFocusPreviousListener); + + const [viewFocusListener] = this.viewFocusListeners.splice(fromIndex, 1); + this.viewFocusListeners.splice(toIndex, 0, viewFocusListener); + + const [viewFocusNextListener] = this.viewFocusNextListeners.splice(fromIndex, 1); + this.viewFocusNextListeners.splice(toIndex, 0, viewFocusNextListener); + + const [viewDnDListeners] = this.viewDnDListeners.splice(fromIndex, 1); + this.viewDnDListeners.splice(toIndex, 0, viewDnDListeners); + + const [view] = this.views.splice(fromIndex, 1); + this.views.splice(toIndex, 0, view); + + const [weight] = this.initialWeights.splice(fromIndex, 1); + this.initialWeights.splice(toIndex, 0, weight); + + this.el.removeChild(this.viewElements[fromIndex]); + this.el.insertBefore(this.viewElements[fromIndex], this.viewElements[toIndex < fromIndex ? toIndex : toIndex + 1]); + const [viewElement] = this.viewElements.splice(fromIndex, 1); + this.viewElements.splice(toIndex, 0, viewElement); + + this.layout(); + + this._onDidOrderChange.fire(); + } + private onSashStart(sash: sash.Sash, event: ISashEvent): void { let i = this.sashes.indexOf(sash); let collapses = this.views.map(v => v.size - v.minimumSize); @@ -913,5 +1037,7 @@ export class SplitView implements this.layoutViewElement = null; this.eventWrapper = null; this.state = null; + + super.dispose(); } } diff --git a/src/vs/workbench/parts/views/browser/views.ts b/src/vs/workbench/parts/views/browser/views.ts index 086b1cb31bd..9bbc16d2b70 100644 --- a/src/vs/workbench/parts/views/browser/views.ts +++ b/src/vs/workbench/parts/views/browser/views.ts @@ -354,9 +354,15 @@ export class ComposedViewsViewlet extends Viewlet { super.create(parent); this.viewletContainer = DOM.append(parent.getHTMLElement(), DOM.$('')); - this.splitView = this._register(new SplitView(this.viewletContainer/* , { canChangeOrderByDragAndDrop: true } */)); - // this.attachSplitViewStyler(this.splitView); + this.splitView = this._register(new SplitView(this.viewletContainer, { canChangeOrderByDragAndDrop: true })); + this.attachSplitViewStyler(this.splitView); this._register(this.splitView.onFocus((view: IView) => this.lastFocusedView = view)); + this._register(this.splitView.onDidOrderChange(() => { + const views = this.splitView.getViews(); + for (let order = 0; order < views.length; order++) { + this.viewsStates.get(views[order].id).order = order; + } + })); return this.onViewDescriptorsChanged() .then(() => { @@ -561,7 +567,7 @@ export class ComposedViewsViewlet extends Viewlet { }, widget); } - protected attachSplitViewStyler(widget: IThemable): IDisposable { + private attachSplitViewStyler(widget: IThemable): IDisposable { return attachStyler(this.themeService, { dropBackground: SIDE_BAR_DRAG_AND_DROP_BACKGROUND }, widget); @@ -651,7 +657,7 @@ export class ComposedViewsViewlet extends Viewlet { return true; } - private getViewDescriptorsFromRegistry(defaultOrder: boolean = true): IViewDescriptor[] { + private getViewDescriptorsFromRegistry(defaultOrder: boolean = false): IViewDescriptor[] { return ViewsRegistry.getViews(this.location) .sort((a, b) => { const viewStateA = this.viewsStates.get(a.id);