diff --git a/src/vs/base/browser/ui/grid/grid.ts b/src/vs/base/browser/ui/grid/grid.ts index e2b977a6cc8411b46669782750cc8290da78f0d1..48a8222bbe25629c40b2cd11472d02b18ebed286 100644 --- a/src/vs/base/browser/ui/grid/grid.ts +++ b/src/vs/base/browser/ui/grid/grid.ts @@ -362,12 +362,20 @@ interface InitialLayoutContext { root: GridBranchNode; } -export interface ISerializedNode { - type: 'branch' | 'leaf'; - data: ISerializedNode[] | object; +export interface ISerializedLeafNode { + type: 'leaf'; + data: object; size: number; } +export interface ISerializedBranchNode { + type: 'branch'; + data: ISerializedNode[]; + size: number; +} + +export type ISerializedNode = ISerializedLeafNode | ISerializedBranchNode; + export interface ISerializedGrid { root: ISerializedNode; orientation: Orientation; @@ -535,4 +543,81 @@ export class SerializableGrid extends Grid { this.restoreViewsSize(childLocation, child, orthogonal(orientation), widthScale, heightScale); } } +} + +export type GridNodeDescriptor = { size?: number, groups?: GridNodeDescriptor[] }; +export type GridDescriptor = { orientation: Orientation, groups?: GridNodeDescriptor[] }; + +export function sanitizeGridNodeDescriptor(nodeDescriptor: GridNodeDescriptor): void { + if (nodeDescriptor.groups && nodeDescriptor.groups.length === 0) { + nodeDescriptor.groups = undefined; + } + + if (!nodeDescriptor.groups) { + return; + } + + let totalDefinedSize = 0; + let totalDefinedSizeCount = 0; + + for (const child of nodeDescriptor.groups) { + sanitizeGridNodeDescriptor(child); + + if (child.size) { + totalDefinedSize += child.size; + totalDefinedSizeCount++; + } + } + + const totalUndefinedSize = totalDefinedSizeCount > 0 ? totalDefinedSize : 1; + const totalUndefinedSizeCount = nodeDescriptor.groups.length - totalDefinedSizeCount; + const eachUndefinedSize = totalUndefinedSize / totalUndefinedSizeCount; + + for (const child of nodeDescriptor.groups) { + if (!child.size) { + child.size = eachUndefinedSize; + } + } +} + +function createSerializedNode(nodeDescriptor: GridNodeDescriptor): ISerializedNode { + if (nodeDescriptor.groups) { + return { type: 'branch', data: nodeDescriptor.groups.map(c => createSerializedNode(c)), size: nodeDescriptor.size! }; + } else { + return { type: 'leaf', data: null, size: nodeDescriptor.size! }; + } +} + +function getDimensions(node: ISerializedNode, orientation: Orientation): { width?: number, height?: number } { + if (node.type === 'branch') { + const childrenDimensions = node.data.map(c => getDimensions(c, orthogonal(orientation))); + + if (orientation === Orientation.VERTICAL) { + const width = node.size || (childrenDimensions.length === 0 ? undefined : Math.max(...childrenDimensions.map(d => d.width || 0))); + const height = childrenDimensions.length === 0 ? undefined : childrenDimensions.reduce((r, d) => r + d.height, 0); + return { width, height }; + } else { + const width = childrenDimensions.length === 0 ? undefined : childrenDimensions.reduce((r, d) => r + d.width, 0); + const height = node.size || (childrenDimensions.length === 0 ? undefined : Math.max(...childrenDimensions.map(d => d.height || 0))); + return { width, height }; + } + } else { + const width = orientation === Orientation.VERTICAL ? node.size : undefined; + const height = orientation === Orientation.VERTICAL ? undefined : node.size; + return { width, height }; + } +} + +export function createSerializedGrid(gridDescriptor: GridDescriptor): ISerializedGrid { + sanitizeGridNodeDescriptor(gridDescriptor); + + const root = createSerializedNode(gridDescriptor); + const { width, height } = getDimensions(root, gridDescriptor.orientation); + + return { + root, + orientation: gridDescriptor.orientation, + width: width || 1, + height: height || 1 + }; } \ No newline at end of file diff --git a/src/vs/base/browser/ui/grid/gridview.ts b/src/vs/base/browser/ui/grid/gridview.ts index 1eca8960ca1eb989ddf621a94372d7f64c285e72..42c90d6d15cf96d72e2d5a982069e4d15a3bf0b7 100644 --- a/src/vs/base/browser/ui/grid/gridview.ts +++ b/src/vs/base/browser/ui/grid/gridview.ts @@ -459,8 +459,8 @@ export class GridView implements IDisposable { constructor(container: HTMLElement, options: IGridViewOptions = {}) { this.element = append(container, $('.monaco-grid-view')); - this.root = new BranchNode(Orientation.VERTICAL, this.styles); this.styles = options.styles || defaultStyles; + this.root = new BranchNode(Orientation.VERTICAL, this.styles); } style(styles: IGridViewStyles): void { @@ -687,5 +687,11 @@ export class GridView implements IDisposable { dispose(): void { this.onDidSashResetRelay.dispose(); this.root.dispose(); + + if (this.element && this.element.parentElement) { + this.element.parentElement.removeChild(this.element); + } + + this.element = null; } } \ No newline at end of file diff --git a/src/vs/base/test/browser/ui/grid/grid.test.ts b/src/vs/base/test/browser/ui/grid/grid.test.ts index 75a3ed4c8657e181ac7060366da89813a6332abf..04f49e8338932ad560bf35622a85d76de4495903 100644 --- a/src/vs/base/test/browser/ui/grid/grid.test.ts +++ b/src/vs/base/test/browser/ui/grid/grid.test.ts @@ -4,8 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { Direction, Grid, getRelativeLocation, Orientation, SerializableGrid, ISerializableView, IViewDeserializer, GridNode, Sizing, isGridBranchNode } from 'vs/base/browser/ui/grid/grid'; +import { Direction, Grid, getRelativeLocation, Orientation, SerializableGrid, ISerializableView, IViewDeserializer, GridNode, Sizing, isGridBranchNode, sanitizeGridNodeDescriptor, GridNodeDescriptor, createSerializedGrid } from 'vs/base/browser/ui/grid/grid'; import { TestView, nodesToArrays } from './util'; +import { deepClone } from 'vs/base/common/objects'; suite('Grid', function () { let container: HTMLElement; @@ -737,4 +738,35 @@ suite('SerializableGrid', function () { assert.deepEqual(view2Copy.size, [800, 300]); assert.deepEqual(view3Copy.size, [800, 300]); }); + + test('sanitizeGridNodeDescriptor', function () { + const nodeDescriptor = { groups: [{ size: 0.2 }, { size: 0.2 }, { size: 0.6, groups: [{}, {}] }] }; + const nodeDescriptorCopy = deepClone(nodeDescriptor); + sanitizeGridNodeDescriptor(nodeDescriptorCopy); + assert.deepEqual(nodeDescriptorCopy, { groups: [{ size: 0.2 }, { size: 0.2 }, { size: 0.6, groups: [{ size: 0.5 }, { size: 0.5 }] }] }); + }); + + test('createSerializedGrid', function () { + const gridDescriptor = { orientation: Orientation.VERTICAL, groups: [{ size: 0.2 }, { size: 0.2 }, { size: 0.6, groups: [{}, {}] }] }; + const serializedGrid = createSerializedGrid(gridDescriptor); + assert.deepEqual(serializedGrid, { + root: { + type: 'branch', + size: undefined, + data: [ + { type: 'leaf', size: 0.2, data: null }, + { type: 'leaf', size: 0.2, data: null }, + { + type: 'branch', size: 0.6, data: [ + { type: 'leaf', size: 0.5, data: null }, + { type: 'leaf', size: 0.5, data: null } + ] + } + ] + }, + orientation: Orientation.VERTICAL, + width: 1, + height: 1 + }); + }); }); \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/editor/editorPart.ts b/src/vs/workbench/browser/parts/editor/editorPart.ts index 339dcc7d78dae48e39aa24d7b041011434d2c673..e78c364f7d589725318b5c6cee22a25c067cddb9 100644 --- a/src/vs/workbench/browser/parts/editor/editorPart.ts +++ b/src/vs/workbench/browser/parts/editor/editorPart.ts @@ -13,7 +13,7 @@ import { Event, Emitter, once } from 'vs/base/common/event'; import { contrastBorder, editorBackground } from 'vs/platform/theme/common/colorRegistry'; import { GroupDirection, IAddGroupOptions, GroupsArrangement, GroupOrientation, IMergeGroupOptions, MergeGroupMode, ICopyEditorOptions, GroupsOrder, GroupChangeKind, GroupLocation, IFindGroupScope, EditorGroupLayout, GroupLayoutArgument } from 'vs/workbench/services/group/common/editorGroupsService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { Direction, SerializableGrid, Sizing, ISerializedGrid, Orientation, ISerializedNode, GridBranchNode, isGridBranchNode, GridNode } from 'vs/base/browser/ui/grid/grid'; +import { Direction, SerializableGrid, Sizing, ISerializedGrid, Orientation, ISerializedNode, GridBranchNode, isGridBranchNode, GridNode, createSerializedGrid } from 'vs/base/browser/ui/grid/grid'; import { GroupIdentifier, IWorkbenchEditorConfiguration } from 'vs/workbench/common/editor'; import { values } from 'vs/base/common/map'; import { EDITOR_GROUP_BORDER } from 'vs/workbench/common/theme'; @@ -332,80 +332,61 @@ export class EditorPart extends Part implements EditorGroupsServiceImpl, IEditor applyLayout(layout: EditorGroupLayout): void { const gridHasFocus = isAncestor(document.activeElement, this.container); - const groupsInGridOrder = this.getGroups(GroupsOrder.GRID_APPEARANCE); // Determine how many groups we need overall - let groupsInLayout = 0; + let layoutGroupsCount = 0; function countGroups(groups: GroupLayoutArgument[]): void { groups.forEach(group => { if (Array.isArray(group.groups)) { countGroups(group.groups); } else { - groupsInLayout++; + layoutGroupsCount++; } }); } countGroups(layout.groups); // If we currently have too many groups, merge them into the last one - if (groupsInLayout < groupsInGridOrder.length) { - const lastGroupInLayout = groupsInGridOrder[groupsInLayout - 1]; - groupsInGridOrder.forEach((group, index) => { - if (index >= groupsInLayout) { + let currentGroupViews = this.getGroups(GroupsOrder.GRID_APPEARANCE); + if (layoutGroupsCount < currentGroupViews.length) { + const lastGroupInLayout = currentGroupViews[layoutGroupsCount - 1]; + currentGroupViews.forEach((group, index) => { + if (index >= layoutGroupsCount) { this.mergeGroup(group, lastGroupInLayout); } }); - } - // Apply orientation - if (typeof layout.orientation === 'number') { - this.setGroupOrientation(layout.orientation); + currentGroupViews = this.getGroups(GroupsOrder.GRID_APPEARANCE); } - // Build layout - let currentGroupIndex = 0; - const buildLayout = (groups: IEditorGroupView[], descriptions: GroupLayoutArgument[], direction: GroupDirection) => { - if (descriptions.length === 0) { - return; // we need at least one group to layout - } + const activeGroup = this.activeGroup; - // Either move existing or add a new group for each item in the description - let totalProportions = 0; - descriptions.forEach((description, index) => { - if (index > 0) { - currentGroupIndex++; - const existingGroup = groupsInGridOrder[currentGroupIndex]; - if (existingGroup) { - groups.push(this.moveGroup(existingGroup, groups[index - 1], direction)); - } else { - groups.push(this.addGroup(groups[index - 1], direction)); - } - } + // Prepare grid descriptor to create new grid from + const gridDescriptor = createSerializedGrid({ + orientation: this.toGridViewOrientation(layout.orientation, this.gridWidget.orientation), + groups: layout.groups + }); - if (typeof description.size === 'number') { - totalProportions += description.size; - } - }); + // Recreate gridwidget with descriptor + this.doCreateGridControlWithState(this.container, gridDescriptor, activeGroup.id, currentGroupViews); - // Apply proportions if they are valid (sum() === 1) - if (totalProportions === 1) { - const totalSize = groups.map(group => this.getSize(group)).reduce(((prev, cur) => prev + cur)); - descriptions.forEach((description, index) => { - this.setSize(groups[index], totalSize * description.size); - }); - } + // Layout + this.doLayout(this.dimension); - // Continue building layout if description.groups is array-type - descriptions.forEach((description, index) => { - if (Array.isArray(description.groups)) { - buildLayout([groups[index]], description.groups, direction === GroupDirection.RIGHT ? GroupDirection.DOWN : GroupDirection.RIGHT); - } - }); - }; + // Update container + this.updateContainer(); + + // Mark preferred size as changed + this.resetPreferredSize(); - buildLayout([groupsInGridOrder[0]], layout.groups, this.orientation === GroupOrientation.HORIZONTAL ? GroupDirection.RIGHT : GroupDirection.DOWN); + // Events for groupd that got added + this.getGroups(GroupsOrder.GRID_APPEARANCE).forEach(groupView => { + if (currentGroupViews.indexOf(groupView) === -1) { + this._onDidAddGroup.fire(groupView); + } + }); - // Restore Focus + // Restore focus as needed if (gridHasFocus) { this._activeGroup.focus(); } @@ -533,6 +514,14 @@ export class EditorPart extends Part implements EditorGroupsServiceImpl, IEditor } } + private toGridViewOrientation(orientation: GroupOrientation, fallback?: Orientation): Orientation { + if (typeof orientation === 'number') { + return orientation === GroupOrientation.HORIZONTAL ? Orientation.HORIZONTAL : Orientation.VERTICAL; + } + + return fallback; + } + removeGroup(group: IEditorGroupView | GroupIdentifier): void { const groupView = this.assertGroupView(group); if (this.groupViews.size === 1) { @@ -707,14 +696,14 @@ export class EditorPart extends Part implements EditorGroupsServiceImpl, IEditor this._onDidPreferredSizeChange.fire(); } - private getGridSeparatorBorder(): Color { + private get gridSeparatorBorder(): Color { return this.theme.getColor(EDITOR_GROUP_BORDER) || this.theme.getColor(contrastBorder) || Color.transparent; } protected updateStyles(): void { this.container.style.backgroundColor = this.getColor(editorBackground); - this.gridWidget.style({ separatorBorder: this.getGridSeparatorBorder() }); + this.gridWidget.style({ separatorBorder: this.gridSeparatorBorder }); } createContentArea(parent: HTMLElement): HTMLElement { @@ -743,7 +732,7 @@ export class EditorPart extends Part implements EditorGroupsServiceImpl, IEditor // Grid Widget (no previous UI state or failed to restore) if (!this.gridWidget) { const initialGroup = this.doCreateGroupView(); - this.gridWidget = this._register(new SerializableGrid(container, initialGroup)); + this.gridWidget = new SerializableGrid(container, initialGroup); // Ensure a group is active this.doSetGroupActive(initialGroup); @@ -766,16 +755,7 @@ export class EditorPart extends Part implements EditorGroupsServiceImpl, IEditor this.mostRecentActiveGroups = uiState.mostRecentActiveGroups; // Grid Widget - this.gridWidget = this._register(SerializableGrid.deserialize(container, uiState.serializedGrid, { - fromJSON: (serializedEditorGroup: ISerializedEditorGroup) => { - const groupView = this.doCreateGroupView(serializedEditorGroup); - if (groupView.id === uiState.activeGroup) { - this.doSetGroupActive(groupView); - } - - return groupView; - } - }, { styles: { separatorBorder: this.getGridSeparatorBorder() } })); + this.doCreateGridControlWithState(container, uiState.serializedGrid, uiState.activeGroup); // Ensure last active group has focus this._activeGroup.focus(); @@ -796,6 +776,40 @@ export class EditorPart extends Part implements EditorGroupsServiceImpl, IEditor } } + private doCreateGridControlWithState(container: HTMLElement, serializedGrid: ISerializedGrid, activeGroupId: GroupIdentifier, editorGroupViewsToReuse?: IEditorGroupView[]): void { + + // Dispose old + if (this.gridWidget) { + this.gridWidget.dispose(); + } + + // Determine group views to reuse if any + let reuseGroupViews: IEditorGroupView[]; + if (editorGroupViewsToReuse) { + reuseGroupViews = editorGroupViewsToReuse.slice(0); // do not modify original array + } else { + reuseGroupViews = []; + } + + // Create new + this.gridWidget = SerializableGrid.deserialize(container, serializedGrid, { + fromJSON: (serializedEditorGroup: ISerializedEditorGroup) => { + let groupView: IEditorGroupView; + if (reuseGroupViews.length > 0) { + groupView = reuseGroupViews.shift(); + } else { + groupView = this.doCreateGroupView(serializedEditorGroup); + } + + if (groupView.id === activeGroupId) { + this.doSetGroupActive(groupView); + } + + return groupView; + } + }, { styles: { separatorBorder: this.gridSeparatorBorder } }); + } + private doGetPreviousState(): IEditorPartUIState { const legacyState = this.doGetPreviousLegacyState(); if (legacyState) { @@ -937,7 +951,13 @@ export class EditorPart extends Part implements EditorGroupsServiceImpl, IEditor layout(dimension: Dimension): Dimension[] { const sizes = super.layout(dimension); - this.dimension = sizes[1]; + this.doLayout(sizes[1]); + + return sizes; + } + + private doLayout(dimension: Dimension): void { + this.dimension = dimension; // Layout Grid try { @@ -948,8 +968,6 @@ export class EditorPart extends Part implements EditorGroupsServiceImpl, IEditor // Event this._onDidLayout.fire(dimension); - - return sizes; } shutdown(): void { @@ -981,8 +999,13 @@ export class EditorPart extends Part implements EditorGroupsServiceImpl, IEditor this.groupViews.forEach(group => group.dispose()); this.groupViews.clear(); + // Grid widget + if (this.gridWidget) { + this.gridWidget = dispose(this.gridWidget); + } + super.dispose(); } //#endregion -} \ No newline at end of file +}