From 0ece60040fa2fd41375d77ce5f9d988113e397c8 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Fri, 9 Mar 2018 16:48:17 +0100 Subject: [PATCH 1/4] wip: explorer horizontal scrolling --- src/vs/base/browser/dom.ts | 15 +++++ src/vs/base/parts/tree/browser/tree.ts | 5 ++ src/vs/base/parts/tree/browser/treeImpl.ts | 2 +- src/vs/base/parts/tree/browser/treeView.ts | 58 +++++++++++++++---- .../base/parts/tree/browser/treeViewModel.ts | 2 +- .../electron-browser/views/explorerView.ts | 31 ++++++++-- 6 files changed, 96 insertions(+), 17 deletions(-) diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index 13951c0ec24..636af54a1f0 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -459,6 +459,9 @@ const sizeUtils = { getBorderLeftWidth: function (element: HTMLElement): number { return getDimension(element, 'border-left-width', 'borderLeftWidth'); }, + getBorderRightWidth: function (element: HTMLElement): number { + return getDimension(element, 'border-right-width', 'borderRightWidth'); + }, getBorderTopWidth: function (element: HTMLElement): number { return getDimension(element, 'border-top-width', 'borderTopWidth'); }, @@ -466,6 +469,12 @@ const sizeUtils = { return getDimension(element, 'border-bottom-width', 'borderBottomWidth'); }, + getPaddingLeft: function (element: HTMLElement): number { + return getDimension(element, 'padding-left', 'paddingLeft'); + }, + getPaddingRight: function (element: HTMLElement): number { + return getDimension(element, 'padding-right', 'paddingRight'); + }, getPaddingTop: function (element: HTMLElement): number { return getDimension(element, 'padding-top', 'paddingTop'); }, @@ -571,6 +580,12 @@ export function getTotalWidth(element: HTMLElement): number { return element.offsetWidth + margin; } +export function getContentWidth(element: HTMLElement): number { + let border = sizeUtils.getBorderLeftWidth(element) + sizeUtils.getBorderRightWidth(element); + let padding = sizeUtils.getPaddingLeft(element) + sizeUtils.getPaddingRight(element); + return element.offsetWidth - border - padding; +} + export function getTotalScrollWidth(element: HTMLElement): number { let margin = sizeUtils.getMarginLeft(element) + sizeUtils.getMarginRight(element); return element.scrollWidth + margin; diff --git a/src/vs/base/parts/tree/browser/tree.ts b/src/vs/base/parts/tree/browser/tree.ts index 0d66edf1667..e9b6eb2fb50 100644 --- a/src/vs/base/parts/tree/browser/tree.ts +++ b/src/vs/base/parts/tree/browser/tree.ts @@ -668,11 +668,16 @@ export interface ITreeConfiguration { accessibilityProvider?: IAccessibilityProvider; } +export interface IContentWidthProvider { + getContentWidth(): number; +} + export interface ITreeOptions extends ITreeStyles { twistiePixels?: number; showTwistie?: boolean; indentPixels?: number; verticalScrollMode?: ScrollbarVisibility; + contentWidthProvider?: IContentWidthProvider; alwaysFocused?: boolean; autoExpandSingleChildren?: boolean; useShadows?: boolean; diff --git a/src/vs/base/parts/tree/browser/treeImpl.ts b/src/vs/base/parts/tree/browser/treeImpl.ts index f7c9173df5a..527ff39518c 100644 --- a/src/vs/base/parts/tree/browser/treeImpl.ts +++ b/src/vs/base/parts/tree/browser/treeImpl.ts @@ -212,7 +212,7 @@ export class Tree implements _.ITree { } getContentHeight(): number { - return this.view.getTotalHeight(); + return this.view.getContentHeight(); } public setHighlight(element?: any, eventPayload?: any): void { diff --git a/src/vs/base/parts/tree/browser/treeView.ts b/src/vs/base/parts/tree/browser/treeView.ts index e717487827d..bd750717af2 100644 --- a/src/vs/base/parts/tree/browser/treeView.ts +++ b/src/vs/base/parts/tree/browser/treeView.ts @@ -25,6 +25,7 @@ import { KeyCode } from 'vs/base/common/keyCodes'; import Event, { Emitter } from 'vs/base/common/event'; import { IDomNodePagePosition } from 'vs/base/browser/dom'; import { DataTransfers } from 'vs/base/browser/dnd'; +import { Delayer } from 'vs/base/common/async'; export interface IRow { element: HTMLElement; @@ -383,6 +384,8 @@ export class TreeView extends HeightMap { private msGesture: MSGesture; private lastPointerType: string; private lastClickTimeStamp: number = 0; + private contentWidthProvider?: _.IContentWidthProvider; + private contentWidthUpdateDelayer = new Delayer(50); private lastRenderTop: number; private lastRenderHeight: number; @@ -460,16 +463,19 @@ export class TreeView extends HeightMap { DOM.addClass(this.domNode, 'no-row-padding'); } + const horizontalScrollMode = typeof context.options.contentWidthProvider === 'undefined' ? ScrollbarVisibility.Hidden : ScrollbarVisibility.Auto; + this.contentWidthProvider = context.options.contentWidthProvider; + this.wrapper = document.createElement('div'); this.wrapper.className = 'monaco-tree-wrapper'; this.scrollableElement = new ScrollableElement(this.wrapper, { alwaysConsumeMouseWheel: true, - horizontal: ScrollbarVisibility.Hidden, + horizontal: horizontalScrollMode, vertical: (typeof context.options.verticalScrollMode !== 'undefined' ? context.options.verticalScrollMode : ScrollbarVisibility.Auto), useShadows: context.options.useShadows }); this.scrollableElement.onScroll((e) => { - this.render(e.scrollTop, e.height); + this.render(e.scrollTop, e.height, e.scrollLeft, e.width, e.scrollWidth); }); if (Browser.isIE) { @@ -670,9 +676,14 @@ export class TreeView extends HeightMap { } this.viewHeight = height || DOM.getContentHeight(this.wrapper); // render + this.scrollHeight = this.getContentHeight(); + + if (this.contentWidthProvider) { + this.viewWidth = DOM.getContentWidth(this.wrapper); + } } - private render(scrollTop: number, viewHeight: number): void { + private render(scrollTop: number, viewHeight: number, scrollLeft: number, viewWidth: number, scrollWidth: number): void { var i: number; var stop: number; @@ -706,6 +717,11 @@ export class TreeView extends HeightMap { this.rowsContainer.style.top = (topItem.top - renderTop) + 'px'; } + if (this.contentWidthProvider) { + this.rowsContainer.style.left = -scrollLeft + 'px'; + this.rowsContainer.style.width = `${Math.max(scrollWidth, viewWidth)}px`; + } + this.lastRenderTop = renderTop; this.lastRenderHeight = renderBottom - renderTop; } @@ -746,6 +762,12 @@ export class TreeView extends HeightMap { } this.scrollTop = scrollTop; + + if (this.contentWidthProvider) { + this.contentWidthUpdateDelayer.trigger(() => { + this.scrollWidth = this.contentWidthProvider.getContentWidth(); + }); + } } public focusNextPage(eventPayload?: any): void { @@ -803,11 +825,25 @@ export class TreeView extends HeightMap { return scrollDimensions.height; } - public set viewHeight(viewHeight: number) { - this.scrollableElement.setScrollDimensions({ - height: viewHeight, - scrollHeight: this.getTotalHeight() - }); + public set viewHeight(height: number) { + this.scrollableElement.setScrollDimensions({ height }); + } + + private set scrollHeight(scrollHeight: number) { + this.scrollableElement.setScrollDimensions({ scrollHeight }); + } + + public get viewWidth(): number { + const scrollDimensions = this.scrollableElement.getScrollDimensions(); + return scrollDimensions.width; + } + + public set viewWidth(viewWidth: number) { + this.scrollableElement.setScrollDimensions({ width: viewWidth }); + } + + private set scrollWidth(scrollWidth: number) { + this.scrollableElement.setScrollDimensions({ scrollWidth }); } public get scrollTop(): number { @@ -817,7 +853,7 @@ export class TreeView extends HeightMap { public set scrollTop(scrollTop: number) { this.scrollableElement.setScrollDimensions({ - scrollHeight: this.getTotalHeight() + scrollHeight: this.getContentHeight() }); this.scrollableElement.setScrollPosition({ scrollTop: scrollTop @@ -825,12 +861,12 @@ export class TreeView extends HeightMap { } public getScrollPosition(): number { - const height = this.getTotalHeight() - this.viewHeight; + const height = this.getContentHeight() - this.viewHeight; return height <= 0 ? 1 : this.scrollTop / height; } public setScrollPosition(pos: number): void { - const height = this.getTotalHeight() - this.viewHeight; + const height = this.getContentHeight() - this.viewHeight; this.scrollTop = height * pos; } diff --git a/src/vs/base/parts/tree/browser/treeViewModel.ts b/src/vs/base/parts/tree/browser/treeViewModel.ts index 0dcb27d000b..016d3ce2093 100644 --- a/src/vs/base/parts/tree/browser/treeViewModel.ts +++ b/src/vs/base/parts/tree/browser/treeViewModel.ts @@ -22,7 +22,7 @@ export class HeightMap { this.indexes = {}; } - public getTotalHeight(): number { + public getContentHeight(): number { var last = this.heightMap[this.heightMap.length - 1]; return !last ? 0 : last.top + last.height; } diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts index acac6760d6a..bd2ceea814c 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts @@ -416,7 +416,8 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView accessibilityProvider }, { autoExpandSingleChildren: true, - ariaLabel: nls.localize('treeAriaLabel', "Files Explorer") + ariaLabel: nls.localize('treeAriaLabel', "Files Explorer"), + contentWidthProvider: { getContentWidth: () => this.getOptimalWidth() } }); // Bind context keys @@ -459,10 +460,32 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView } public getOptimalWidth(): number { - const parentNode = this.explorerViewer.getHTMLElement(); - const childNodes = [].slice.call(parentNode.querySelectorAll('.explorer-item .label-name')); // select all file labels + if (!this.explorerViewer) { + return 0; + } - return DOM.getLargestChildWidth(parentNode, childNodes); + let result = 0; + const parentNode = this.explorerViewer.getHTMLElement(); + const rows = [].slice.call(parentNode.querySelectorAll('.monaco-tree-row')) as HTMLElement[]; + + for (const row of rows) { + const rowStyle = window.getComputedStyle(row); + const content = row.querySelector('.content') as HTMLElement; + const twistieStyle = window.getComputedStyle(content, ':before'); + const iconLabel = content.querySelector('.monaco-icon-label') as HTMLElement; + const iconStyle = window.getComputedStyle(iconLabel, ':before'); // width + padding + const decorationsStyle = window.getComputedStyle(iconLabel, ':after'); // width + padding + const label = iconLabel.querySelector('.monaco-icon-label-description-container > .label-name') as HTMLElement; + + const twistieWidth = (twistieStyle.display !== 'none' && twistieStyle.content) ? parseFloat(twistieStyle.width) + parseFloat(twistieStyle.paddingLeft) + parseFloat(twistieStyle.paddingRight) : 0; + const iconWidth = iconStyle.content ? parseFloat(iconStyle.width) + parseFloat(iconStyle.paddingLeft) + parseFloat(iconStyle.paddingRight) : 0; + const decorationsWidth = decorationsStyle.content ? parseFloat(decorationsStyle.width) + parseFloat(decorationsStyle.paddingLeft) + parseFloat(decorationsStyle.paddingRight) : 0; + const width = parseFloat(rowStyle.paddingLeft) + twistieWidth + iconWidth + decorationsWidth + label.offsetWidth; + + result = Math.max(result, width); + } + + return result + 12; // 12 for right padding } private onFileOperation(e: FileOperationEvent): void { From 86e5a0b4c370027b1919e2986b6cfec02d7204c5 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Mon, 12 Mar 2018 12:30:20 +0100 Subject: [PATCH 2/4] wip: tree user-independent horizontal scrolling --- src/vs/base/parts/tree/browser/tree.ts | 2 +- src/vs/base/parts/tree/browser/treeView.ts | 41 +++++++++++++++---- .../base/parts/tree/browser/treeViewModel.ts | 1 + .../tree/test/browser/treeViewModel.test.ts | 3 +- src/vs/platform/list/browser/listService.ts | 3 +- .../electron-browser/views/explorerView.ts | 29 ++----------- 6 files changed, 41 insertions(+), 38 deletions(-) diff --git a/src/vs/base/parts/tree/browser/tree.ts b/src/vs/base/parts/tree/browser/tree.ts index e9b6eb2fb50..f1b6f8a60ad 100644 --- a/src/vs/base/parts/tree/browser/tree.ts +++ b/src/vs/base/parts/tree/browser/tree.ts @@ -677,7 +677,7 @@ export interface ITreeOptions extends ITreeStyles { showTwistie?: boolean; indentPixels?: number; verticalScrollMode?: ScrollbarVisibility; - contentWidthProvider?: IContentWidthProvider; + horizontalScrollMode?: ScrollbarVisibility; alwaysFocused?: boolean; autoExpandSingleChildren?: boolean; useShadows?: boolean; diff --git a/src/vs/base/parts/tree/browser/treeView.ts b/src/vs/base/parts/tree/browser/treeView.ts index bd750717af2..23436abafbf 100644 --- a/src/vs/base/parts/tree/browser/treeView.ts +++ b/src/vs/base/parts/tree/browser/treeView.ts @@ -101,6 +101,7 @@ export class RowCache implements Lifecycle.IDisposable { export interface IViewContext extends _.ITreeContext { cache: RowCache; + horizontalScrolling: boolean; } export class ViewItem implements IViewItem { @@ -113,6 +114,7 @@ export class ViewItem implements IViewItem { public top: number; public height: number; + public width: number = 0; public onDragStart: (e: DragEvent) => void; public needsRender: boolean; @@ -251,7 +253,19 @@ export class ViewItem implements IViewItem { } if (!skipUserRender) { + const style = window.getComputedStyle(this.element); + const paddingLeft = parseFloat(style.paddingLeft); + + if (this.context.horizontalScrolling) { + this.element.style.width = 'fit-content'; + } + this.context.renderer.renderElement(this.context.tree, this.model.getElement(), this.templateId, this.row.templateData); + + if (this.context.horizontalScrolling) { + this.width = DOM.getContentWidth(this.element) + paddingLeft; + this.element.style.width = ''; + } } } @@ -384,7 +398,8 @@ export class TreeView extends HeightMap { private msGesture: MSGesture; private lastPointerType: string; private lastClickTimeStamp: number = 0; - private contentWidthProvider?: _.IContentWidthProvider; + + private horizontalScrolling: boolean = true; private contentWidthUpdateDelayer = new Delayer(50); private lastRenderTop: number; @@ -423,6 +438,9 @@ export class TreeView extends HeightMap { TreeView.counter++; this.instance = TreeView.counter; + const horizontalScrollMode = typeof context.options.horizontalScrollMode === 'undefined' ? ScrollbarVisibility.Hidden : context.options.horizontalScrollMode; + const horizontalScrolling = horizontalScrollMode !== ScrollbarVisibility.Hidden; + this.context = { dataSource: context.dataSource, renderer: context.renderer, @@ -433,7 +451,8 @@ export class TreeView extends HeightMap { tree: context.tree, accessibilityProvider: context.accessibilityProvider, options: context.options, - cache: new RowCache(context) + cache: new RowCache(context), + horizontalScrolling }; this.modelListeners = []; @@ -463,9 +482,6 @@ export class TreeView extends HeightMap { DOM.addClass(this.domNode, 'no-row-padding'); } - const horizontalScrollMode = typeof context.options.contentWidthProvider === 'undefined' ? ScrollbarVisibility.Hidden : ScrollbarVisibility.Auto; - this.contentWidthProvider = context.options.contentWidthProvider; - this.wrapper = document.createElement('div'); this.wrapper.className = 'monaco-tree-wrapper'; this.scrollableElement = new ScrollableElement(this.wrapper, { @@ -678,7 +694,7 @@ export class TreeView extends HeightMap { this.viewHeight = height || DOM.getContentHeight(this.wrapper); // render this.scrollHeight = this.getContentHeight(); - if (this.contentWidthProvider) { + if (this.horizontalScrolling) { this.viewWidth = DOM.getContentWidth(this.wrapper); } } @@ -717,7 +733,7 @@ export class TreeView extends HeightMap { this.rowsContainer.style.top = (topItem.top - renderTop) + 'px'; } - if (this.contentWidthProvider) { + if (this.horizontalScrolling) { this.rowsContainer.style.left = -scrollLeft + 'px'; this.rowsContainer.style.width = `${Math.max(scrollWidth, viewWidth)}px`; } @@ -763,9 +779,16 @@ export class TreeView extends HeightMap { this.scrollTop = scrollTop; - if (this.contentWidthProvider) { + if (this.horizontalScrolling) { this.contentWidthUpdateDelayer.trigger(() => { - this.scrollWidth = this.contentWidthProvider.getContentWidth(); + const keys = Object.keys(this.items); + let scrollWidth = 0; + + for (const key of keys) { + scrollWidth = Math.max(scrollWidth, this.items[key].width); + } + + this.scrollWidth = scrollWidth + 10 /* scrollbar */; }); } } diff --git a/src/vs/base/parts/tree/browser/treeViewModel.ts b/src/vs/base/parts/tree/browser/treeViewModel.ts index 016d3ce2093..0238c16102f 100644 --- a/src/vs/base/parts/tree/browser/treeViewModel.ts +++ b/src/vs/base/parts/tree/browser/treeViewModel.ts @@ -10,6 +10,7 @@ export interface IViewItem { model: Item; top: number; height: number; + width: number; } export class HeightMap { diff --git a/src/vs/base/parts/tree/test/browser/treeViewModel.test.ts b/src/vs/base/parts/tree/test/browser/treeViewModel.test.ts index 7906b72e993..9bc0a347dcf 100644 --- a/src/vs/base/parts/tree/test/browser/treeViewModel.test.ts +++ b/src/vs/base/parts/tree/test/browser/treeViewModel.test.ts @@ -45,7 +45,8 @@ class TestHeightMap extends HeightMap { return { model: item, top: 0, - height: item.getHeight() + height: item.getHeight(), + width: 0 }; } } diff --git a/src/vs/platform/list/browser/listService.ts b/src/vs/platform/list/browser/listService.ts index 955b1b34b56..eb3980531c4 100644 --- a/src/vs/platform/list/browser/listService.ts +++ b/src/vs/platform/list/browser/listService.ts @@ -24,6 +24,7 @@ import { DefaultController, IControllerOptions, OpenMode, ClickBehavior } from ' import { isUndefinedOrNull } from 'vs/base/common/types'; import { IEditorOptions } from 'vs/platform/editor/common/editor'; import Event, { Emitter } from 'vs/base/common/event'; +import { ScrollbarVisibility } from '../../../base/common/scrollable'; export type ListWidget = List | PagedList | ITree; @@ -299,7 +300,7 @@ export class WorkbenchTree extends Tree { @IInstantiationService instantiationService: IInstantiationService, @IConfigurationService private configurationService: IConfigurationService ) { - super(container, handleTreeController(configuration, instantiationService), mixin(options, { keyboardSupport: false } as ITreeOptions, false)); + super(container, handleTreeController(configuration, instantiationService), { horizontalScrollMode: ScrollbarVisibility.Auto, keyboardSupport: false, ...options }); this.contextKeyService = createScopedContextKeyService(contextKeyService, this); this.listDoubleSelection = WorkbenchListDoubleSelection.bindTo(this.contextKeyService); diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts index bd2ceea814c..acac6760d6a 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts @@ -416,8 +416,7 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView accessibilityProvider }, { autoExpandSingleChildren: true, - ariaLabel: nls.localize('treeAriaLabel', "Files Explorer"), - contentWidthProvider: { getContentWidth: () => this.getOptimalWidth() } + ariaLabel: nls.localize('treeAriaLabel', "Files Explorer") }); // Bind context keys @@ -460,32 +459,10 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView } public getOptimalWidth(): number { - if (!this.explorerViewer) { - return 0; - } - - let result = 0; const parentNode = this.explorerViewer.getHTMLElement(); - const rows = [].slice.call(parentNode.querySelectorAll('.monaco-tree-row')) as HTMLElement[]; + const childNodes = [].slice.call(parentNode.querySelectorAll('.explorer-item .label-name')); // select all file labels - for (const row of rows) { - const rowStyle = window.getComputedStyle(row); - const content = row.querySelector('.content') as HTMLElement; - const twistieStyle = window.getComputedStyle(content, ':before'); - const iconLabel = content.querySelector('.monaco-icon-label') as HTMLElement; - const iconStyle = window.getComputedStyle(iconLabel, ':before'); // width + padding - const decorationsStyle = window.getComputedStyle(iconLabel, ':after'); // width + padding - const label = iconLabel.querySelector('.monaco-icon-label-description-container > .label-name') as HTMLElement; - - const twistieWidth = (twistieStyle.display !== 'none' && twistieStyle.content) ? parseFloat(twistieStyle.width) + parseFloat(twistieStyle.paddingLeft) + parseFloat(twistieStyle.paddingRight) : 0; - const iconWidth = iconStyle.content ? parseFloat(iconStyle.width) + parseFloat(iconStyle.paddingLeft) + parseFloat(iconStyle.paddingRight) : 0; - const decorationsWidth = decorationsStyle.content ? parseFloat(decorationsStyle.width) + parseFloat(decorationsStyle.paddingLeft) + parseFloat(decorationsStyle.paddingRight) : 0; - const width = parseFloat(rowStyle.paddingLeft) + twistieWidth + iconWidth + decorationsWidth + label.offsetWidth; - - result = Math.max(result, width); - } - - return result + 12; // 12 for right padding + return DOM.getLargestChildWidth(parentNode, childNodes); } private onFileOperation(e: FileOperationEvent): void { From 83558baef98f2035e4ac155828f8d146f9e542d8 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Mon, 12 Mar 2018 15:43:31 +0100 Subject: [PATCH 3/4] explorer decorations should update element's width --- src/vs/base/parts/tree/browser/tree.ts | 5 ++ src/vs/base/parts/tree/browser/treeImpl.ts | 5 ++ src/vs/base/parts/tree/browser/treeView.ts | 54 +++++++++++++++---- src/vs/workbench/browser/labels.ts | 6 +++ .../electron-browser/views/explorerViewer.ts | 16 ++++-- 5 files changed, 71 insertions(+), 15 deletions(-) diff --git a/src/vs/base/parts/tree/browser/tree.ts b/src/vs/base/parts/tree/browser/tree.ts index f1b6f8a60ad..7640cf64656 100644 --- a/src/vs/base/parts/tree/browser/tree.ts +++ b/src/vs/base/parts/tree/browser/tree.ts @@ -78,6 +78,11 @@ export interface ITree { */ refresh(element?: any, recursive?: boolean): WinJS.Promise; + /** + * Updates an element's width. + */ + updateWidth(element: any): void; + /** * Expands an element. * The returned promise returns a boolean for whether the element was expanded or not. diff --git a/src/vs/base/parts/tree/browser/treeImpl.ts b/src/vs/base/parts/tree/browser/treeImpl.ts index 527ff39518c..268cb993a9a 100644 --- a/src/vs/base/parts/tree/browser/treeImpl.ts +++ b/src/vs/base/parts/tree/browser/treeImpl.ts @@ -158,6 +158,11 @@ export class Tree implements _.ITree { return this.model.refresh(element, recursive); } + public updateWidth(element: any): void { + let item = this.model.getItem(element); + return this.view.updateWidth(item); + } + public expand(element: any): WinJS.Promise { return this.model.expand(element); } diff --git a/src/vs/base/parts/tree/browser/treeView.ts b/src/vs/base/parts/tree/browser/treeView.ts index 23436abafbf..64fb53b6bc8 100644 --- a/src/vs/base/parts/tree/browser/treeView.ts +++ b/src/vs/base/parts/tree/browser/treeView.ts @@ -269,6 +269,18 @@ export class ViewItem implements IViewItem { } } + updateWidth(): any { + if (!this.context.horizontalScrolling) { + return; + } + + const style = window.getComputedStyle(this.element); + const paddingLeft = parseFloat(style.paddingLeft); + this.element.style.width = 'fit-content'; + this.width = DOM.getContentWidth(this.element) + paddingLeft; + this.element.style.width = ''; + } + public insertInDOM(container: HTMLElement, afterElement: HTMLElement): void { if (!this.row) { this.row = this.context.cache.alloc(this.templateId); @@ -778,19 +790,24 @@ export class TreeView extends HeightMap { } this.scrollTop = scrollTop; + this.updateScrollWidth(); + } - if (this.horizontalScrolling) { - this.contentWidthUpdateDelayer.trigger(() => { - const keys = Object.keys(this.items); - let scrollWidth = 0; - - for (const key of keys) { - scrollWidth = Math.max(scrollWidth, this.items[key].width); - } - - this.scrollWidth = scrollWidth + 10 /* scrollbar */; - }); + private updateScrollWidth(): void { + if (!this.horizontalScrolling) { + return; } + + this.contentWidthUpdateDelayer.trigger(() => { + const keys = Object.keys(this.items); + let scrollWidth = 0; + + for (const key of keys) { + scrollWidth = Math.max(scrollWidth, this.items[key].width); + } + + this.scrollWidth = scrollWidth + 10 /* scrollbar */; + }); } public focusNextPage(eventPayload?: any): void { @@ -1058,6 +1075,21 @@ export class TreeView extends HeightMap { } } + public updateWidth(item: Model.Item): void { + if (!item || !item.isVisible()) { + return; + } + + const viewItem = this.items[item.id]; + + if (!viewItem) { + return; + } + + viewItem.updateWidth(); + this.updateScrollWidth(); + } + public getRelativeTop(item: Model.Item): number { if (item && item.isVisible()) { var viewItem = this.items[item.id]; diff --git a/src/vs/workbench/browser/labels.ts b/src/vs/workbench/browser/labels.ts index 02522e3f8e0..9d898f2cdba 100644 --- a/src/vs/workbench/browser/labels.ts +++ b/src/vs/workbench/browser/labels.ts @@ -25,6 +25,7 @@ import { Schemas } from 'vs/base/common/network'; import { FileKind, FILES_ASSOCIATIONS_CONFIG } from 'vs/platform/files/common/files'; import { ITextModel } from 'vs/editor/common/model'; import { IThemeService } from 'vs/platform/theme/common/themeService'; +import Event, { Emitter } from 'vs/base/common/event'; export interface IResourceLabel { name: string; @@ -38,12 +39,16 @@ export interface IResourceLabelOptions extends IIconLabelValueOptions { } export class ResourceLabel extends IconLabel { + private toDispose: IDisposable[]; private label: IResourceLabel; private options: IResourceLabelOptions; private computedIconClasses: string[]; private lastKnownConfiguredLangId: string; + private _onDidRender = new Emitter(); + readonly onDidRender: Event = this._onDidRender.event; + constructor( container: HTMLElement, options: IIconLabelCreationOptions, @@ -217,6 +222,7 @@ export class ResourceLabel extends IconLabel { } this.setValue(label, this.label.description, iconLabelOptions); + this._onDidRender.fire(); } public dispose(): void { diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts index 75e7120f0a0..a26168c831f 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts @@ -6,7 +6,6 @@ import { TPromise } from 'vs/base/common/winjs.base'; import nls = require('vs/nls'); -import lifecycle = require('vs/base/common/lifecycle'); import objects = require('vs/base/common/objects'); import DOM = require('vs/base/browser/dom'); import URI from 'vs/base/common/uri'; @@ -20,7 +19,7 @@ import { InputBox, MessageType } from 'vs/base/browser/ui/inputbox/inputBox'; import { isMacintosh, isLinux } from 'vs/base/common/platform'; import glob = require('vs/base/common/glob'); import { FileLabel, IFileLabelOptions } from 'vs/workbench/browser/labels'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { IDisposable, dispose, empty as EmptyDisposable } from 'vs/base/common/lifecycle'; import { IFilesConfiguration, SortOrder } from 'vs/workbench/parts/files/common/files'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { FileOperationError, FileOperationResult, IFileService, FileKind } from 'vs/platform/files/common/files'; @@ -179,6 +178,7 @@ export class ActionRunner extends BaseActionRunner implements IActionRunner { } export interface IFileTemplateData { + elementDisposable: IDisposable; label: FileLabel; container: HTMLElement; } @@ -228,12 +228,15 @@ export class FileRenderer implements IRenderer { } public renderTemplate(tree: ITree, templateId: string, container: HTMLElement): IFileTemplateData { + const elementDisposable = EmptyDisposable; const label = this.instantiationService.createInstance(FileLabel, container, void 0); - return { label, container }; + return { elementDisposable, label, container }; } public renderElement(tree: ITree, stat: FileStat, templateId: string, templateData: IFileTemplateData): void { + templateData.elementDisposable.dispose(); + const editableData: IEditableData = this.state.getEditableData(stat); // File Label @@ -246,12 +249,17 @@ export class FileRenderer implements IRenderer { extraClasses, fileDecorations: this.config.explorer.decorations }); + + templateData.elementDisposable = templateData.label.onDidRender(() => { + tree.updateWidth(stat); + }); } // Input Box else { templateData.label.element.style.display = 'none'; this.renderInputBox(templateData.container, tree, stat, editableData); + templateData.elementDisposable = EmptyDisposable; } } @@ -296,7 +304,7 @@ export class FileRenderer implements IRenderer { if (!blur) { // https://github.com/Microsoft/vscode/issues/20269 tree.domFocus(); } - lifecycle.dispose(toDispose); + dispose(toDispose); container.removeChild(label.element); }, 0); }); From e2bc0e116f8637b4edfa8cb28ebb346995df55a7 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Mon, 12 Mar 2018 16:05:31 +0100 Subject: [PATCH 4/4] workbench.tree.horizontalScrolling setting --- src/vs/platform/list/browser/listService.ts | 27 ++++++++++++------- .../relauncher.contribution.ts | 8 ++++++ 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/vs/platform/list/browser/listService.ts b/src/vs/platform/list/browser/listService.ts index eb3980531c4..24a177ae1bc 100644 --- a/src/vs/platform/list/browser/listService.ts +++ b/src/vs/platform/list/browser/listService.ts @@ -106,6 +106,7 @@ function createScopedContextKeyService(contextKeyService: IContextKeyService, wi export const multiSelectModifierSettingKey = 'workbench.list.multiSelectModifier'; export const openModeSettingKey = 'workbench.list.openMode'; +export const horizontalScrollingKey = 'workbench.tree.horizontalScrolling'; function useAltAsMultipleSelectionModifier(configurationService: IConfigurationService): boolean { return configurationService.getValue(multiSelectModifierSettingKey) === 'alt'; @@ -282,7 +283,7 @@ export class WorkbenchTree extends Tree { readonly contextKeyService: IContextKeyService; - protected disposables: IDisposable[] = []; + protected disposables: IDisposable[]; private listDoubleSelection: IContextKey; private listMultiSelection: IContextKey; @@ -298,10 +299,15 @@ export class WorkbenchTree extends Tree { @IListService listService: IListService, @IThemeService themeService: IThemeService, @IInstantiationService instantiationService: IInstantiationService, - @IConfigurationService private configurationService: IConfigurationService + @IConfigurationService configurationService: IConfigurationService ) { - super(container, handleTreeController(configuration, instantiationService), { horizontalScrollMode: ScrollbarVisibility.Auto, keyboardSupport: false, ...options }); + const config = handleTreeController(configuration, instantiationService); + const horizontalScrollMode = configurationService.getValue(horizontalScrollingKey) ? ScrollbarVisibility.Auto : ScrollbarVisibility.Hidden; + const opts = { horizontalScrollMode, keyboardSupport: false, ...options }; + super(container, config, opts); + + this.disposables = []; this.contextKeyService = createScopedContextKeyService(contextKeyService, this); this.listDoubleSelection = WorkbenchListDoubleSelection.bindTo(this.contextKeyService); this.listMultiSelection = WorkbenchListMultiSelection.bindTo(this.contextKeyService); @@ -315,23 +321,19 @@ export class WorkbenchTree extends Tree { attachListStyler(this, themeService) ); - this.registerListeners(); - } - - private registerListeners(): void { this.disposables.push(this.onDidChangeSelection(() => { const selection = this.getSelection(); this.listDoubleSelection.set(selection && selection.length === 2); this.listMultiSelection.set(selection && selection.length > 1); })); - this.disposables.push(this.configurationService.onDidChangeConfiguration(e => { + this.disposables.push(configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(openModeSettingKey)) { - this._openOnSingleClick = useSingleClickToOpen(this.configurationService); + this._openOnSingleClick = useSingleClickToOpen(configurationService); } if (e.affectsConfiguration(multiSelectModifierSettingKey)) { - this._useAltAsMultipleSelectionModifier = useAltAsMultipleSelectionModifier(this.configurationService); + this._useAltAsMultipleSelectionModifier = useAltAsMultipleSelectionModifier(configurationService); } })); } @@ -516,6 +518,11 @@ configurationRegistry.registerConfiguration({ key: 'openModeModifier', comment: ['`singleClick` and `doubleClick` refers to a value the setting can take and should not be localized.'] }, "Controls how to open items in trees and lists using the mouse (if supported). Set to `singleClick` to open items with a single mouse click and `doubleClick` to only open via mouse double click. For parents with children in trees, this setting will control if a single click expands the parent or a double click. Note that some trees and lists might choose to ignore this setting if it is not applicable. ") + }, + [horizontalScrollingKey]: { + 'type': 'boolean', + 'default': false, + 'description': localize('horizontalScrolling setting', "Controls whether trees support horizontal scrolling in the workbench.") } } }); diff --git a/src/vs/workbench/parts/relauncher/electron-browser/relauncher.contribution.ts b/src/vs/workbench/parts/relauncher/electron-browser/relauncher.contribution.ts index 9f5b18c4111..382ae5cb8fc 100644 --- a/src/vs/workbench/parts/relauncher/electron-browser/relauncher.contribution.ts +++ b/src/vs/workbench/parts/relauncher/electron-browser/relauncher.contribution.ts @@ -25,6 +25,7 @@ interface IConfiguration extends IWindowsConfiguration { update: { channel: string; }; telemetry: { enableCrashReporter: boolean }; keyboard: { touchbar: { enabled: boolean } }; + workbench: { tree: { horizontalScrolling: boolean } }; } export class SettingsChangeRelauncher implements IWorkbenchContribution { @@ -36,6 +37,7 @@ export class SettingsChangeRelauncher implements IWorkbenchContribution { private updateChannel: string; private enableCrashReporter: boolean; private touchbarEnabled: boolean; + private treeHorizontalScrolling: boolean; private firstFolderResource: URI; private extensionHostRestarter: RunOnceScheduler; @@ -99,6 +101,12 @@ export class SettingsChangeRelauncher implements IWorkbenchContribution { changed = true; } + // Tree horizontal scrolling support + if (config.workbench && config.workbench.tree && typeof config.workbench.tree.horizontalScrolling === 'boolean' && config.workbench.tree.horizontalScrolling !== this.treeHorizontalScrolling) { + this.treeHorizontalScrolling = config.workbench.tree.horizontalScrolling; + changed = true; + } + // Notify only when changed and we are the focused window (avoids notification spam across windows) if (notify && changed) { this.doConfirm(