From d522fb67cf7e3a3b28a9b657a46db738c260864e Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Sun, 17 Sep 2017 16:59:11 +0200 Subject: [PATCH] panelview: dnd --- src/vs/base/browser/ui/splitview/panelview.ts | 120 +++++++++++++++++- 1 file changed, 116 insertions(+), 4 deletions(-) diff --git a/src/vs/base/browser/ui/splitview/panelview.ts b/src/vs/base/browser/ui/splitview/panelview.ts index 157be8261dc..97c8e20274c 100644 --- a/src/vs/base/browser/ui/splitview/panelview.ts +++ b/src/vs/base/browser/ui/splitview/panelview.ts @@ -6,13 +6,14 @@ 'use strict'; import 'vs/css!./splitview'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { IDisposable, dispose, combinedDisposable } from 'vs/base/common/lifecycle'; import Event, { Emitter, chain } from 'vs/base/common/event'; import { domEvent } from 'vs/base/browser/event'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeyCode } from 'vs/base/common/keyCodes'; import { $, append, addClass, removeClass, toggleClass } from 'vs/base/browser/dom'; import { firstIndex } from 'vs/base/common/arrays'; +import { Color } from 'vs/base/common/color'; import { SplitView, IView } from './splitview2'; enum PanelState { @@ -36,7 +37,7 @@ export abstract class Panel implements IView { private _minimumBodySize: number; private _maximumBodySize: number; private ariaHeaderLabel: string; - private header: HTMLElement; + readonly header: HTMLElement; private disposables: IDisposable[] = []; get minimumBodySize(): number { @@ -155,6 +156,95 @@ export abstract class Panel implements IView { } } +interface IDndContext { + draggable: PanelDraggable | null; +} + +class PanelDraggable implements IDisposable { + + // see https://github.com/Microsoft/vscode/issues/14470 + private dragOverCounter = 0; + private dropBackground: Color | undefined; + private disposables: IDisposable[] = []; + + private _onDidDrop = new Emitter<{ from: Panel, to: Panel }>(); + readonly onDidDrop = this._onDidDrop.event; + + constructor(private panel: Panel, private context: IDndContext) { + domEvent(panel.header, 'dragstart')(this.onDragStart, this, this.disposables); + domEvent(panel.header, 'dragenter')(this.onDragEnter, this, this.disposables); + domEvent(panel.header, 'dragleave')(this.onDragLeave, this, this.disposables); + domEvent(panel.header, 'dragend')(this.onDragEnd, this, this.disposables); + domEvent(panel.header, 'drop')(this.onDrop, this, this.disposables); + } + + private onDragStart(e: DragEvent): void { + e.dataTransfer.effectAllowed = 'move'; + + const dragImage = append(document.body, $('.monaco-panel-drag-image', {}, this.panel.header.textContent)); + e.dataTransfer.setDragImage(dragImage, -10, -10); + setTimeout(() => document.body.removeChild(dragImage), 0); + + this.context.draggable = this; + } + + private onDragEnter(e: DragEvent): void { + if (!this.context.draggable || this.context.draggable === this) { + return; + } + + this.dragOverCounter++; + this.renderHeader(); + } + + private onDragLeave(e: DragEvent): void { + if (!this.context.draggable || this.context.draggable === this) { + return; + } + + this.dragOverCounter--; + + if (this.dragOverCounter === 0) { + this.renderHeader(); + } + } + + private onDragEnd(e: DragEvent): void { + if (!this.context.draggable) { + return; + } + + this.dragOverCounter = 0; + this.renderHeader(); + this.context.draggable = null; + } + + private onDrop(e: DragEvent): void { + if (!this.context.draggable) { + return; + } + + this.dragOverCounter = 0; + this.renderHeader(); + + if (this.context.draggable !== this) { + this._onDidDrop.fire({ from: this.context.draggable.panel, to: this.panel }); + } + + this.context.draggable = null; + } + + private renderHeader(): void { + this.panel.header.style.backgroundColor = this.dragOverCounter === 0 && this.dropBackground + ? this.dropBackground.toString() + : null; + } + + dispose(): void { + this.disposables = dispose(this.disposables); + } +} + export class IPanelViewOptions { dnd?: boolean; } @@ -166,19 +256,30 @@ interface IPanelItem { export class PanelView implements IDisposable { + private dnd: boolean; + private dndContext: IDndContext = { draggable: null }; private el: HTMLElement; private panelItems: IPanelItem[] = []; private splitview: SplitView; private animationTimer: number | null = null; constructor(private container: HTMLElement, options?: IPanelViewOptions) { + this.dnd = !!options.dnd; this.el = append(container, $('.monaco-panel-view')); this.splitview = new SplitView(container); } addPanel(panel: Panel, size: number, index = this.splitview.length): void { - const disposable = panel.onDidChange(this.setupAnimation, this); - const panelItem = { panel, disposable }; + const disposables: IDisposable[] = []; + panel.onDidChange(this.setupAnimation, this, disposables); + + if (this.dnd) { + const draggable = new PanelDraggable(panel, this.dndContext); + disposables.push(draggable); + draggable.onDidDrop(({ from, to }) => this.movePanel(from, to), null, disposables); + } + + const panelItem = { panel, disposable: combinedDisposable(disposables) }; this.panelItems.splice(index, 0, panelItem); this.splitview.addView(panel, size, index); @@ -196,6 +297,17 @@ export class PanelView implements IDisposable { panelItem.disposable.dispose(); } + movePanel(from: Panel, to: Panel): void { + const fromIndex = firstIndex(this.panelItems, item => item.panel === from); + const toIndex = firstIndex(this.panelItems, item => item.panel === to); + + if (fromIndex === -1 || toIndex === -1) { + return; + } + + this.splitview.moveView(fromIndex, toIndex); + } + layout(size: number): void { this.splitview.layout(size); }