From 239edbf05fd2181878273f0c2f8e2365da04945d Mon Sep 17 00:00:00 2001 From: Maxime Quandalle Date: Fri, 15 Apr 2016 11:30:52 +0200 Subject: [PATCH] Sash double clicks (#4702) * Support double click on sashes We had to implement a slight change in the sash drag "hack", by removing the full cover transparent overlay (that was preventing clicks events from being fired) and replacing it by a CSS `cursor` rule on the document.body. The ideal solution for a clean dragging events implementation would be to use the `Element.setCapture` API that is unfortunately not available in Chrome. * Center the split editor frontier by double-clicking on it * Reset the bottom panel size by double-clicking on it * Implement sash reset on diff editors * Fix a bug with DOM measurement The calculus was plain wrong as showed in https://github.com/winjs/winjs/issues/1621 This was probably unotified since this utility function wasn't used in the code base. * Implement sidebar sash optimal resizing Fixes #4660 * Abstract `getLargestChildWidth` into a DOM method This commit also moves the `getOptimalWidth` method from the `Composite` to the `Viewlet` component. This partially addresses https://github.com/Microsoft/vscode/pull/4702#issuecomment-207813241 * Calculate the sidebar optimal width correctly in case no folder is open --- src/vs/base/browser/dom.ts | 38 +++++++------------ src/vs/base/browser/ui/sash/sash.css | 8 ++++ src/vs/base/browser/ui/sash/sash.ts | 24 +++++------- .../editor/browser/widget/diffEditorWidget.ts | 19 +++++++--- src/vs/workbench/browser/layout.ts | 21 +++++++++- .../browser/parts/editor/binaryDiffEditor.ts | 7 ++++ .../parts/editor/sideBySideEditorControl.ts | 20 ++++++++++ .../browser/parts/sidebar/sidebarPart.ts | 2 +- src/vs/workbench/browser/viewlet.ts | 6 ++- src/vs/workbench/common/viewlet.ts | 7 +++- .../parts/files/browser/explorerViewlet.ts | 9 +++++ .../parts/files/browser/views/explorerView.ts | 8 +++- .../files/browser/views/workingFilesView.ts | 10 ++++- 13 files changed, 127 insertions(+), 52 deletions(-) diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index 4964d28c457b9..1dad1e4d9929b 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -637,46 +637,34 @@ export function getTotalHeight(element: HTMLElement): number { return element.offsetHeight + margin; } -// Adapted from WinJS // Gets the left coordinate of the specified element relative to the specified parent. export function getRelativeLeft(element: HTMLElement, parent: HTMLElement): number { if (element === null) { return 0; } - let left = element.offsetLeft; - let e = element.parentNode; - while (e !== null) { - left -= e.offsetLeft; - - if (e === parent) { - break; - } - e = e.parentNode; - } - - return left; + let elementPosition = getTopLeftOffset(element); + let parentPosition = getTopLeftOffset(parent); + return elementPosition.left - parentPosition.left; } -// Adapted from WinJS // Gets the top coordinate of the element relative to the specified parent. export function getRelativeTop(element: HTMLElement, parent: HTMLElement): number { if (element === null) { return 0; } - let top = element.offsetTop; - let e = element.parentNode; - while (e !== null) { - top -= e.offsetTop; - - if (e === parent) { - break; - } - e = e.parentNode; - } + let elementPosition = getTopLeftOffset(element); + let parentPosition = getTopLeftOffset(parent); + return parentPosition.top - elementPosition.top; +} - return top; +export function getLargestChildWidth(parent: HTMLElement, children: HTMLElement[]): number { + let childWidths = children.map((child) => { + return getTotalWidth(child) + getRelativeLeft(child, parent) || 0; + }); + let maxWidth = Math.max(...childWidths); + return maxWidth; } // ---------------------------------------------------------------------------------------- diff --git a/src/vs/base/browser/ui/sash/sash.css b/src/vs/base/browser/ui/sash/sash.css index fc4d62526a5c5..cf8d86709f393 100644 --- a/src/vs/base/browser/ui/sash/sash.css +++ b/src/vs/base/browser/ui/sash/sash.css @@ -23,4 +23,12 @@ .monaco-sash.disabled { cursor: default; +} + +.vertical-cursor-container * { + cursor: ew-resize !important; +} + +.horizontal-cursor-container * { + cursor: ns-resize !important; } \ No newline at end of file diff --git a/src/vs/base/browser/ui/sash/sash.ts b/src/vs/base/browser/ui/sash/sash.ts index 8f6bdfaa57609..b9317e62c46e8 100644 --- a/src/vs/base/browser/ui/sash/sash.ts +++ b/src/vs/base/browser/ui/sash/sash.ts @@ -63,11 +63,12 @@ export class Sash extends EventEmitter { this.gesture = new Gesture(this.$e.getHTMLElement()); - this.$e.on('mousedown', (e: MouseEvent) => { this.onMouseDown(e); }); + this.$e.on(DOM.EventType.MOUSE_DOWN, (e: MouseEvent) => { this.onMouseDown(e); }); + this.$e.on(DOM.EventType.DBLCLICK, (e: MouseEvent) => { this.emit('reset', e); }); this.$e.on(EventType.Start, (e: GestureEvent) => { this.onTouchStart(e); }); this.orientation = options.orientation || Orientation.VERTICAL; - this.$e.addClass(this.orientation === Orientation.HORIZONTAL ? 'horizontal' : 'vertical'); + this.$e.addClass(this.getOrientation()); this.size = options.baseSize || 5; @@ -91,6 +92,10 @@ export class Sash extends EventEmitter { return this.$e.getHTMLElement(); } + private getOrientation(): 'horizontal' | 'vertical' { + return this.orientation === Orientation.HORIZONTAL ? 'horizontal' : 'vertical'; + } + private onMouseDown(e: MouseEvent): void { DOM.EventHelper.stop(e, false); @@ -112,17 +117,8 @@ export class Sash extends EventEmitter { this.$e.addClass('active'); this.emit('start', startEvent); - let overlayDiv = $('div').style({ - position: 'absolute', - top: 0, - left: 0, - width: '100%', - height: '100%', - zIndex: 1000000, - cursor: this.orientation === Orientation.VERTICAL ? 'ew-resize' : 'ns-resize' - }); - let $window = $(window); + let containerCssClass = `${this.getOrientation()}-cursor-container`; let lastCurrentX = startX; let lastCurrentY = startY; @@ -148,10 +144,10 @@ export class Sash extends EventEmitter { this.emit('end'); $window.off('mousemove'); - overlayDiv.destroy(); + document.body.classList.remove(containerCssClass); }); - overlayDiv.appendTo(document.body); + document.body.classList.add(containerCssClass); } private onTouchStart(event: GestureEvent): void { diff --git a/src/vs/editor/browser/widget/diffEditorWidget.ts b/src/vs/editor/browser/widget/diffEditorWidget.ts index 390139d00d96a..8af52c3c38057 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget.ts @@ -1327,9 +1327,10 @@ class DiffEdtorWidgetSideBySide extends DiffEditorWidgetStyle implements IDiffEd this._sash.disable(); } - this._sash.on('start', () => this._onSashDragStart()); - this._sash.on('change', (e: ISashEvent) => this._onSashDrag(e)); - this._sash.on('end', () => this._onSashDragEnd()); + this._sash.on('start', () => this.onSashDragStart()); + this._sash.on('change', (e: ISashEvent) => this.onSashDrag(e)); + this._sash.on('end', () => this.onSashDragEnd()); + this._sash.on('reset', () => this.onSashReset()); } public dispose(): void { @@ -1378,11 +1379,11 @@ class DiffEdtorWidgetSideBySide extends DiffEditorWidgetStyle implements IDiffEd return this._sashPosition; } - private _onSashDragStart(): void { + private onSashDragStart(): void { this._startSashPosition = this._sashPosition; } - private _onSashDrag(e:ISashEvent): void { + private onSashDrag(e:ISashEvent): void { var w = this._dataSource.getWidth(); var contentWidth = w - DiffEditorWidget.ENTIRE_DIFF_OVERVIEW_WIDTH; var sashPosition = this.layout((this._startSashPosition + (e.currentX - e.startX)) / contentWidth); @@ -1392,7 +1393,13 @@ class DiffEdtorWidgetSideBySide extends DiffEditorWidgetStyle implements IDiffEd this._dataSource.relayoutEditors(); } - private _onSashDragEnd(): void { + private onSashDragEnd(): void { + this._sash.layout(); + } + + private onSashReset(): void { + this._sashRatio = 0.5; + this._dataSource.relayoutEditors(); this._sash.layout(); } diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index 52ecb35d19412..c336d8e85356b 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -12,6 +12,7 @@ import {Sash, ISashEvent, IVerticalSashLayoutProvider, IHorizontalSashLayoutProv import {IWorkbenchEditorService} from 'vs/workbench/services/editor/common/editorService'; import {IPartService, Position} from 'vs/workbench/services/part/common/partService'; import {IWorkspaceContextService} from 'vs/workbench/services/workspace/common/contextService'; +import {IViewletService} from 'vs/workbench/services/viewlet/common/viewletService'; import {IStorageService, StorageScope} from 'vs/platform/storage/common/storage'; import {IContextViewService} from 'vs/platform/contextview/browser/contextView'; import {IEventService} from 'vs/platform/event/common/event'; @@ -94,6 +95,7 @@ export class WorkbenchLayout implements IVerticalSashLayoutProvider, IHorizontal @IWorkbenchEditorService private editorService: IWorkbenchEditorService, @IWorkspaceContextService private contextService: IWorkspaceContextService, @IPartService private partService: IPartService, + @IViewletService private viewletService: IViewletService, @IThemeService themeService: IThemeService ) { this.parent = parent; @@ -190,7 +192,7 @@ export class WorkbenchLayout implements IVerticalSashLayoutProvider, IHorizontal let dragCompensation = DEFAULT_MIN_PANEL_PART_HEIGHT - HIDE_PANEL_HEIGHT_THRESHOLD; this.partService.setPanelHidden(true); startY = Math.min(this.sidebarHeight - this.computedStyles.statusbar.height, e.currentY + dragCompensation); - this.panelHeight = this.startPanelHeight; // when restoring panel, restore to the panel width we started from + this.panelHeight = this.startPanelHeight; // when restoring panel, restore to the panel height we started from } // Otherwise size the panel accordingly @@ -217,9 +219,26 @@ export class WorkbenchLayout implements IVerticalSashLayoutProvider, IHorizontal this.sashX.addListener('end', () => { this.storageService.store(WorkbenchLayout.sashXWidthSettingsKey, this.sidebarWidth, StorageScope.GLOBAL); }); + this.sashY.addListener('end', () => { this.storageService.store(WorkbenchLayout.sashYHeightSettingsKey, this.panelHeight, StorageScope.GLOBAL); }); + + this.sashY.addListener('reset', () => { + this.panelHeight = DEFAULT_MIN_PANEL_PART_HEIGHT; + this.storageService.store(WorkbenchLayout.sashYHeightSettingsKey, this.panelHeight, StorageScope.GLOBAL); + this.partService.setPanelHidden(false); + this.layout(); + }); + + this.sashX.addListener('reset', () => { + let activeViewlet = this.viewletService.getActiveViewlet(); + let optimalWidth = activeViewlet && activeViewlet.getOptimalWidth(); + this.sidebarWidth = Math.max(DEFAULT_MIN_PART_WIDTH, optimalWidth || 0); + this.storageService.store(WorkbenchLayout.sashXWidthSettingsKey, this.sidebarWidth, StorageScope.GLOBAL); + this.partService.setSideBarHidden(false); + this.layout(); + }); } private onEditorInputChanging(e: EditorEvent): void { diff --git a/src/vs/workbench/browser/parts/editor/binaryDiffEditor.ts b/src/vs/workbench/browser/parts/editor/binaryDiffEditor.ts index 4d4c2a98a5af2..124d90574d36a 100644 --- a/src/vs/workbench/browser/parts/editor/binaryDiffEditor.ts +++ b/src/vs/workbench/browser/parts/editor/binaryDiffEditor.ts @@ -72,6 +72,7 @@ export class BinaryResourceDiffEditor extends BaseEditor implements IVerticalSas this.sash.addListener('start', () => this.onSashDragStart()); this.sash.addListener('change', (e: ISashEvent) => this.onSashDrag(e)); this.sash.addListener('end', () => this.onSashDragEnd()); + this.sash.addListener('reset', () => this.onSashReset()); // Right Container for Binary let rightBinaryContainerElement = document.createElement('div'); @@ -199,6 +200,12 @@ export class BinaryResourceDiffEditor extends BaseEditor implements IVerticalSas this.sash.layout(); } + private onSashReset(): void { + this.leftContainerWidth = this.dimension.width / 2; + this.layoutContainers(); + this.sash.layout(); + } + public getVerticalSashTop(sash: Sash): number { return 0; } diff --git a/src/vs/workbench/browser/parts/editor/sideBySideEditorControl.ts b/src/vs/workbench/browser/parts/editor/sideBySideEditorControl.ts index f463514cdf508..536e43becf026 100644 --- a/src/vs/workbench/browser/parts/editor/sideBySideEditorControl.ts +++ b/src/vs/workbench/browser/parts/editor/sideBySideEditorControl.ts @@ -728,6 +728,7 @@ export class SideBySideEditorControl extends EventEmitter implements IVerticalSa this.leftSash.addListener('start', () => this.onLeftSashDragStart()); this.leftSash.addListener('change', (e: ISashEvent) => this.onLeftSashDrag(e)); this.leftSash.addListener('end', () => this.onLeftSashDragEnd()); + this.leftSash.addListener('reset', () => this.onLeftSashReset()); this.leftSash.hide(); // Center Container @@ -738,6 +739,7 @@ export class SideBySideEditorControl extends EventEmitter implements IVerticalSa this.rightSash.addListener('start', () => this.onRightSashDragStart()); this.rightSash.addListener('change', (e: ISashEvent) => this.onRightSashDrag(e)); this.rightSash.addListener('end', () => this.onRightSashDragEnd()); + this.rightSash.addListener('reset', () => this.onRightSashReset()); this.rightSash.hide(); // Right Container @@ -1212,6 +1214,14 @@ export class SideBySideEditorControl extends EventEmitter implements IVerticalSa this.editorActionsToolbar[position].setActions([], [])(); } + private centerSash(a: Position, b: Position): void { + let sumWidth = this.containerWidth[a] + this.containerWidth[b]; + let meanWidth = sumWidth / 2; + this.containerWidth[a] = meanWidth; + this.containerWidth[b] = sumWidth - meanWidth; + this.layoutContainers(); + } + private onLeftSashDragStart(): void { this.startLeftContainerWidth = this.containerWidth[Position.LEFT]; } @@ -1301,6 +1311,11 @@ export class SideBySideEditorControl extends EventEmitter implements IVerticalSa this.focusNextNonMinimized(); } + private onLeftSashReset(): void { + this.centerSash(Position.LEFT, Position.CENTER); + this.leftSash.layout(); + } + private onRightSashDragStart(): void { this.startRightContainerWidth = this.containerWidth[Position.RIGHT]; } @@ -1358,6 +1373,11 @@ export class SideBySideEditorControl extends EventEmitter implements IVerticalSa this.focusNextNonMinimized(); } + private onRightSashReset(): void { + this.centerSash(Position.CENTER, Position.RIGHT); + this.rightSash.layout(); + } + public getVerticalSashTop(sash: Sash): number { return 0; } diff --git a/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts b/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts index a51bae8311eb1..3475e15ab0502 100644 --- a/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts +++ b/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts @@ -78,7 +78,7 @@ export class SidebarPart extends CompositePart implements IViewletServi } public getActiveViewlet(): IViewlet { - return this.getActiveComposite(); + return this.getActiveComposite(); } public getLastActiveViewletId(): string { diff --git a/src/vs/workbench/browser/viewlet.ts b/src/vs/workbench/browser/viewlet.ts index 2e37bc68ce281..1b7ff3b84c9fb 100644 --- a/src/vs/workbench/browser/viewlet.ts +++ b/src/vs/workbench/browser/viewlet.ts @@ -25,7 +25,11 @@ import {IContextMenuService} from 'vs/platform/contextview/browser/contextView'; import {IMessageService} from 'vs/platform/message/common/message'; import {StructuredSelection} from 'vs/platform/selection/common/selection'; -export abstract class Viewlet extends Composite implements IViewlet { } +export abstract class Viewlet extends Composite implements IViewlet { + public getOptimalWidth(): number { + return null; + } +} /** * Helper subtype of viewlet for those that use a tree inside. diff --git a/src/vs/workbench/common/viewlet.ts b/src/vs/workbench/common/viewlet.ts index 49b9e866a8959..045876ee41eca 100644 --- a/src/vs/workbench/common/viewlet.ts +++ b/src/vs/workbench/common/viewlet.ts @@ -5,4 +5,9 @@ import {IComposite} from 'vs/workbench/common/composite'; -export interface IViewlet extends IComposite { } +export interface IViewlet extends IComposite { + /** + * Returns the minimal width needed to avoid any content horizontal truncation + */ + getOptimalWidth(): number; +} diff --git a/src/vs/workbench/parts/files/browser/explorerViewlet.ts b/src/vs/workbench/parts/files/browser/explorerViewlet.ts index 4ba5f27ee3adb..dd79614f41944 100644 --- a/src/vs/workbench/parts/files/browser/explorerViewlet.ts +++ b/src/vs/workbench/parts/files/browser/explorerViewlet.ts @@ -175,6 +175,15 @@ export class ExplorerViewlet extends Viewlet { return this.actionRunner; } + public getOptimalWidth(): number { + let additionalMargin = 16; + let workingFilesViewWidth = this.getWorkingFilesView().getOptimalWidth(); + let explorerView = this.getExplorerView(); + let explorerViewWidth = explorerView ? explorerView.getOptimalWidth() : 0; + let optimalWidth = Math.max(workingFilesViewWidth, explorerViewWidth); + return optimalWidth + additionalMargin; + } + public shutdown(): void { this.views.forEach((view) => view.shutdown()); diff --git a/src/vs/workbench/parts/files/browser/views/explorerView.ts b/src/vs/workbench/parts/files/browser/views/explorerView.ts index e55737fe568a0..6d21444b3e757 100644 --- a/src/vs/workbench/parts/files/browser/views/explorerView.ts +++ b/src/vs/workbench/parts/files/browser/views/explorerView.ts @@ -24,7 +24,7 @@ import {FileEditorInput} from 'vs/workbench/parts/files/browser/editors/fileEdit import {FileDragAndDrop, FileFilter, FileSorter, FileController, FileRenderer, FileDataSource, FileViewletState, FileAccessibilityProvider} from 'vs/workbench/parts/files/browser/views/explorerViewer'; import lifecycle = require('vs/base/common/lifecycle'); import {IEditor} from 'vs/platform/editor/common/editor'; -import DOM = require('vs/base/browser/dom'); +import * as DOM from 'vs/base/browser/dom'; import {CollapseAction, CollapsibleViewletView} from 'vs/workbench/browser/viewlet'; import {FileStat} from 'vs/workbench/parts/files/common/explorerViewModel'; import {IWorkbenchEditorService} from 'vs/workbench/services/editor/common/editorService'; @@ -348,6 +348,12 @@ export class ExplorerView extends CollapsibleViewletView { return this.explorerViewer; } + public getOptimalWidth(): number { + let parentNode = this.explorerViewer.getHTMLElement(); + let childNodes = [].slice.call(parentNode.querySelectorAll('.explorer-item-label > a')); + return DOM.getLargestChildWidth(parentNode, childNodes); + } + private onLocalFileChange(e: LocalFileChangeEvent): void { let modelElement: FileStat; let parent: FileStat; diff --git a/src/vs/workbench/parts/files/browser/views/workingFilesView.ts b/src/vs/workbench/parts/files/browser/views/workingFilesView.ts index 180c2224346b1..501a81eb063e4 100644 --- a/src/vs/workbench/parts/files/browser/views/workingFilesView.ts +++ b/src/vs/workbench/parts/files/browser/views/workingFilesView.ts @@ -13,7 +13,7 @@ import {IAction, IActionRunner} from 'vs/base/common/actions'; import workbenchEditorCommon = require('vs/workbench/common/editor'); import {CollapsibleState} from 'vs/base/browser/ui/splitview/splitview'; import {IWorkingFileEntry, IWorkingFilesModel, IWorkingFileModelChangeEvent, LocalFileChangeEvent, EventType as FileEventType, IFilesConfiguration, ITextFileService, AutoSaveMode} from 'vs/workbench/parts/files/common/files'; -import dom = require('vs/base/browser/dom'); +import * as DOM from 'vs/base/browser/dom'; import {IDisposable} from 'vs/base/common/lifecycle'; import errors = require('vs/base/common/errors'); import {EventType as WorkbenchEventType, UntitledEditorEvent, EditorEvent} from 'vs/workbench/common/events'; @@ -82,7 +82,7 @@ export class WorkingFilesView extends AdaptiveCollapsibleViewletView { public renderBody(container: HTMLElement): void { this.treeContainer = super.renderViewTree(container); - dom.addClass(this.treeContainer, 'explorer-working-files'); + DOM.addClass(this.treeContainer, 'explorer-working-files'); this.createViewer($(this.treeContainer)); } @@ -303,6 +303,12 @@ export class WorkingFilesView extends AdaptiveCollapsibleViewletView { return this.tree; } + public getOptimalWidth():number { + let parentNode = this.tree.getHTMLElement(); + let childNodes = [].slice.call(parentNode.querySelectorAll('.monaco-file-label > .file-name')); + return DOM.getLargestChildWidth(parentNode, childNodes); + } + public shutdown(): void { this.settings[WorkingFilesView.MEMENTO_COLLAPSED] = (this.state === CollapsibleState.COLLAPSED);