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 e47661931d6..e717934d5ee 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. @@ -674,6 +679,7 @@ export interface ITreeOptions extends ITreeStyles { showTwistie?: boolean; indentPixels?: number; verticalScrollMode?: ScrollbarVisibility; + horizontalScrollMode?: ScrollbarVisibility; 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 8530c00fbc2..03cb48eb818 100644 --- a/src/vs/base/parts/tree/browser/treeImpl.ts +++ b/src/vs/base/parts/tree/browser/treeImpl.ts @@ -160,6 +160,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); } @@ -214,7 +219,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 c5864dc7acd..5798dde88dc 100644 --- a/src/vs/base/parts/tree/browser/treeView.ts +++ b/src/vs/base/parts/tree/browser/treeView.ts @@ -26,6 +26,7 @@ import Event, { Emitter } from 'vs/base/common/event'; import { IDomNodePagePosition } from 'vs/base/browser/dom'; import { DataTransfers } from 'vs/base/browser/dnd'; import { DefaultTreestyler } from './treeDefaults'; +import { Delayer } from 'vs/base/common/async'; export interface IRow { element: HTMLElement; @@ -101,6 +102,7 @@ export class RowCache implements Lifecycle.IDisposable { export interface IViewContext extends _.ITreeContext { cache: RowCache; + horizontalScrolling: boolean; } export class ViewItem implements IViewItem { @@ -113,6 +115,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,10 +254,34 @@ 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 = ''; + } } } + 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); @@ -386,6 +413,9 @@ export class TreeView extends HeightMap { private lastPointerType: string; private lastClickTimeStamp: number = 0; + private horizontalScrolling: boolean = true; + private contentWidthUpdateDelayer = new Delayer(50); + private lastRenderTop: number; private lastRenderHeight: number; @@ -422,6 +452,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, @@ -432,7 +465,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 = []; @@ -471,12 +505,12 @@ export class TreeView extends HeightMap { 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) { @@ -609,9 +643,14 @@ export class TreeView extends HeightMap { } this.viewHeight = height || DOM.getContentHeight(this.wrapper); // render + this.scrollHeight = this.getContentHeight(); + + if (this.horizontalScrolling) { + 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; @@ -645,6 +684,11 @@ export class TreeView extends HeightMap { this.rowsContainer.style.top = (topItem.top - renderTop) + 'px'; } + if (this.horizontalScrolling) { + this.rowsContainer.style.left = -scrollLeft + 'px'; + this.rowsContainer.style.width = `${Math.max(scrollWidth, viewWidth)}px`; + } + this.lastRenderTop = renderTop; this.lastRenderHeight = renderBottom - renderTop; } @@ -685,6 +729,24 @@ export class TreeView extends HeightMap { } this.scrollTop = scrollTop; + this.updateScrollWidth(); + } + + 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 { @@ -742,11 +804,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 { @@ -756,7 +832,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 @@ -764,12 +840,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; } @@ -938,6 +1014,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/base/parts/tree/browser/treeViewModel.ts b/src/vs/base/parts/tree/browser/treeViewModel.ts index 0dcb27d000b..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 { @@ -22,7 +23,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/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 31ab48143ea..3a7eec1acfe 100644 --- a/src/vs/platform/list/browser/listService.ts +++ b/src/vs/platform/list/browser/listService.ts @@ -25,6 +25,7 @@ import { isUndefinedOrNull } from 'vs/base/common/types'; import { IEditorOptions } from 'vs/platform/editor/common/editor'; import Event, { Emitter } from 'vs/base/common/event'; import { createStyleSheet } from 'vs/base/browser/dom'; +import { ScrollbarVisibility } from 'vs/base/common/scrollable'; export type ListWidget = List | PagedList | ITree; @@ -106,6 +107,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'; @@ -330,7 +332,7 @@ export class WorkbenchTree extends Tree { readonly contextKeyService: IContextKeyService; - protected disposables: IDisposable[] = []; + protected disposables: IDisposable[]; private listDoubleSelection: IContextKey; private listMultiSelection: IContextKey; @@ -346,19 +348,20 @@ 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), - // mixin magic: - // - define some custom tree options common for all workbench trees - // - mixin theme colors from default tree styles right on creation - mixin(options, mixin({ - keyboardSupport: false - } as ITreeOptions, computeStyles(themeService.getTheme(), defaultListStyles), false), false) - ); + const config = handleTreeController(configuration, instantiationService); + const horizontalScrollMode = configurationService.getValue(horizontalScrollingKey) ? ScrollbarVisibility.Auto : ScrollbarVisibility.Hidden; + const opts = { + horizontalScrollMode, + keyboardSupport: false, + ...computeStyles(themeService.getTheme(), defaultListStyles), + ...options + }; + super(container, config, opts); + + this.disposables = []; this.contextKeyService = createScopedContextKeyService(contextKeyService, this); this.listDoubleSelection = WorkbenchListDoubleSelection.bindTo(this.contextKeyService); this.listMultiSelection = WorkbenchListMultiSelection.bindTo(this.contextKeyService); @@ -372,23 +375,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); } })); } @@ -573,6 +572,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/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 6dc6d1885ff..fbc8fbd2c96 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); }); 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(