mirror of
https://github.com/microsoft/vscode.git
synced 2026-05-26 10:16:01 +01:00
Merge pull request #30197 from Microsoft/sandy081/viewsDragAndDrop
Support reordering of views in split view by drag and drop
This commit is contained in:
@@ -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<IView>;
|
||||
private state: IState;
|
||||
private draggedView: IView;
|
||||
private dropBackground: Color;
|
||||
|
||||
private _onFocus: Emitter<IView> = this._register(new Emitter<IView>());
|
||||
readonly onFocus: Event<IView> = this._onFocus.event;
|
||||
|
||||
private _onDidOrderChange: Emitter<void> = this._register(new Emitter<void>());
|
||||
readonly onDidOrderChange: Event<void> = 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<IView>();
|
||||
|
||||
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<IView> {
|
||||
return this._onFocus.event;
|
||||
}
|
||||
|
||||
getViews<T extends IView>(): T[] {
|
||||
return <T[]>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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<IView>();
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user