From 2f4150013bdc37a086a0e02cbe6e1f174bc8a425 Mon Sep 17 00:00:00 2001 From: mathuo <6710312+mathuo@users.noreply.github.com> Date: Sun, 10 Nov 2024 15:06:48 +0000 Subject: [PATCH] feat: serialization of maximized views --- .../dockview/dockviewComponent.spec.ts | 373 ++++++++++-------- .../dockview-core/src/api/component.api.ts | 3 +- .../src/dockview/dockviewComponent.ts | 16 + .../src/gridview/baseComponentGridview.ts | 31 +- .../dockview-core/src/gridview/gridview.ts | 79 +++- .../react/dockview/demo-dockview/src/app.tsx | 15 + .../demo-dockview/src/gridActions.tsx | 9 +- 7 files changed, 346 insertions(+), 180 deletions(-) diff --git a/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts b/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts index 66f4f023e..3d31d0d98 100644 --- a/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts @@ -679,180 +679,231 @@ describe('dockviewComponent', () => { expect(viewQuery.length).toBe(1); }); - test('serialization', () => { - dockview.layout(1000, 1000); + describe('serialization', () => { + test('basic', () => { + dockview.layout(1000, 1000); - dockview.fromJSON({ - activeGroup: 'group-1', - grid: { - root: { - type: 'branch', - data: [ - { - type: 'leaf', - data: { - views: ['panel1'], - id: 'group-1', - activeView: 'panel1', + dockview.fromJSON({ + activeGroup: 'group-1', + grid: { + root: { + type: 'branch', + data: [ + { + type: 'leaf', + data: { + views: ['panel1'], + id: 'group-1', + activeView: 'panel1', + }, + size: 500, }, - size: 500, - }, - { - type: 'branch', - data: [ - { - type: 'leaf', - data: { - views: ['panel2', 'panel3'], - id: 'group-2', + { + type: 'branch', + data: [ + { + type: 'leaf', + data: { + views: ['panel2', 'panel3'], + id: 'group-2', + }, + size: 500, }, - size: 500, - }, - { - type: 'leaf', - data: { views: ['panel4'], id: 'group-3' }, - size: 500, - }, - ], - size: 250, - }, - { - type: 'leaf', - data: { views: ['panel5'], id: 'group-4' }, - size: 250, - }, - ], - size: 1000, - }, - height: 1000, - width: 1000, - orientation: Orientation.VERTICAL, - }, - panels: { - panel1: { - id: 'panel1', - contentComponent: 'default', - tabComponent: 'tab-default', - title: 'panel1', - }, - panel2: { - id: 'panel2', - contentComponent: 'default', - title: 'panel2', - }, - panel3: { - id: 'panel3', - contentComponent: 'default', - title: 'panel3', - renderer: 'onlyWhenVisible', - }, - panel4: { - id: 'panel4', - contentComponent: 'default', - title: 'panel4', - renderer: 'always', + { + type: 'leaf', + data: { + views: ['panel4'], + id: 'group-3', + }, + size: 500, + }, + ], + size: 250, + }, + { + type: 'leaf', + data: { views: ['panel5'], id: 'group-4' }, + size: 250, + }, + ], + size: 1000, + }, + height: 1000, + width: 1000, + orientation: Orientation.VERTICAL, }, - panel5: { - id: 'panel5', - contentComponent: 'default', - title: 'panel5', - minimumHeight: 100, - maximumHeight: 1000, - minimumWidth: 200, - maximumWidth: 2000, + panels: { + panel1: { + id: 'panel1', + contentComponent: 'default', + tabComponent: 'tab-default', + title: 'panel1', + }, + panel2: { + id: 'panel2', + contentComponent: 'default', + title: 'panel2', + }, + panel3: { + id: 'panel3', + contentComponent: 'default', + title: 'panel3', + renderer: 'onlyWhenVisible', + }, + panel4: { + id: 'panel4', + contentComponent: 'default', + title: 'panel4', + renderer: 'always', + }, + panel5: { + id: 'panel5', + contentComponent: 'default', + title: 'panel5', + minimumHeight: 100, + maximumHeight: 1000, + minimumWidth: 200, + maximumWidth: 2000, + }, }, - }, - }); + }); - expect(JSON.parse(JSON.stringify(dockview.toJSON()))).toEqual({ - activeGroup: 'group-1', - grid: { - root: { - type: 'branch', - data: [ - { - type: 'leaf', - data: { - views: ['panel1'], - id: 'group-1', - activeView: 'panel1', + expect(JSON.parse(JSON.stringify(dockview.toJSON()))).toEqual({ + activeGroup: 'group-1', + grid: { + root: { + type: 'branch', + data: [ + { + type: 'leaf', + data: { + views: ['panel1'], + id: 'group-1', + activeView: 'panel1', + }, + size: 500, }, - size: 500, - }, - { - type: 'branch', - data: [ - { - type: 'leaf', - data: { - views: ['panel2', 'panel3'], - id: 'group-2', - activeView: 'panel3', + { + type: 'branch', + data: [ + { + type: 'leaf', + data: { + views: ['panel2', 'panel3'], + id: 'group-2', + activeView: 'panel3', + }, + size: 500, }, - size: 500, - }, - { - type: 'leaf', - data: { - views: ['panel4'], - id: 'group-3', - activeView: 'panel4', + { + type: 'leaf', + data: { + views: ['panel4'], + id: 'group-3', + activeView: 'panel4', + }, + size: 500, }, - size: 500, + ], + size: 250, + }, + { + type: 'leaf', + data: { + views: ['panel5'], + id: 'group-4', + activeView: 'panel5', }, - ], - size: 250, - }, - { - type: 'leaf', - data: { - views: ['panel5'], - id: 'group-4', - activeView: 'panel5', + size: 250, }, - size: 250, - }, - ], - size: 1000, - }, - height: 1000, - width: 1000, - orientation: Orientation.VERTICAL, - }, - panels: { - panel1: { - id: 'panel1', - contentComponent: 'default', - tabComponent: 'tab-default', - title: 'panel1', - }, - panel2: { - id: 'panel2', - contentComponent: 'default', - title: 'panel2', - }, - panel3: { - id: 'panel3', - contentComponent: 'default', - title: 'panel3', - renderer: 'onlyWhenVisible', - }, - panel4: { - id: 'panel4', - contentComponent: 'default', - title: 'panel4', - renderer: 'always', + ], + size: 1000, + }, + height: 1000, + width: 1000, + orientation: Orientation.VERTICAL, }, - panel5: { - id: 'panel5', - contentComponent: 'default', - title: 'panel5', - minimumHeight: 100, - maximumHeight: 1000, - minimumWidth: 200, - maximumWidth: 2000, + panels: { + panel1: { + id: 'panel1', + contentComponent: 'default', + tabComponent: 'tab-default', + title: 'panel1', + }, + panel2: { + id: 'panel2', + contentComponent: 'default', + title: 'panel2', + }, + panel3: { + id: 'panel3', + contentComponent: 'default', + title: 'panel3', + renderer: 'onlyWhenVisible', + }, + panel4: { + id: 'panel4', + contentComponent: 'default', + title: 'panel4', + renderer: 'always', + }, + panel5: { + id: 'panel5', + contentComponent: 'default', + title: 'panel5', + minimumHeight: 100, + maximumHeight: 1000, + minimumWidth: 200, + maximumWidth: 2000, + }, }, - }, + }); + }); + + test('serialized layout with maximized node', () => { + const api = new DockviewApi(dockview); + + api.layout(500, 1000); + + api.addPanel({ + id: 'panel1', + component: 'default', + }); + + api.addPanel({ + id: 'panel2', + component: 'default', + position: { direction: 'right' }, + }); + + api.addPanel({ + id: 'panel3', + component: 'default', + position: { direction: 'below' }, + }); + + const panel4 = api.addPanel({ + id: 'panel4', + component: 'default', + }); + + panel4.api.maximize(); + expect(panel4.api.isMaximized()).toBeTruthy(); + + const state = api.toJSON(); + expect(api.hasMaximizedGroup()).toBeTruthy(); + expect(panel4.api.isMaximized()).toBeTruthy(); + + api.clear(); + expect(api.groups.length).toBe(0); + expect(api.panels.length).toBe(0); + + api.fromJSON(state); + const newPanel4 = api.getPanel('panel4')!; + expect(api.hasMaximizedGroup()).toBeTruthy(); + expect(newPanel4.api.isMaximized()).toBeTruthy(); + + expect(state).toEqual(api.toJSON()); }); }); diff --git a/packages/dockview-core/src/api/component.api.ts b/packages/dockview-core/src/api/component.api.ts index fe7d5c2d3..01bff2a8c 100644 --- a/packages/dockview-core/src/api/component.api.ts +++ b/packages/dockview-core/src/api/component.api.ts @@ -1,4 +1,5 @@ import { + DockviewMaximizedGroupChanged, FloatingGroupOptions, IDockviewComponent, MovePanelEvent, @@ -898,7 +899,7 @@ export class DockviewApi implements CommonApi { this.component.exitMaximizedGroup(); } - get onDidMaximizedGroupChange(): Event { + get onDidMaximizedGroupChange(): Event { return this.component.onDidMaximizedGroupChange; } diff --git a/packages/dockview-core/src/dockview/dockviewComponent.ts b/packages/dockview-core/src/dockview/dockviewComponent.ts index 5a20f5b84..e18cd9d03 100644 --- a/packages/dockview-core/src/dockview/dockviewComponent.ts +++ b/packages/dockview-core/src/dockview/dockviewComponent.ts @@ -163,6 +163,11 @@ export interface FloatingGroupOptionsInternal extends FloatingGroupOptions { skipActiveGroup?: boolean; } +export interface DockviewMaximizedGroupChanged { + group: DockviewGroupPanel; + isMaximized: boolean; +} + export interface IDockviewComponent extends IBaseGrid { readonly activePanel: IDockviewPanel | undefined; readonly totalPanels: number; @@ -183,6 +188,7 @@ export interface IDockviewComponent extends IBaseGrid { readonly onDidActiveGroupChange: Event; readonly onUnhandledDragOverEvent: Event; readonly onDidMovePanel: Event; + readonly onDidMaximizedGroupChange: Event; readonly options: DockviewComponentOptions; updateOptions(options: DockviewOptions): void; moveGroupOrPanel(options: MoveGroupOrPanelOptions): void; @@ -275,6 +281,10 @@ export class DockviewComponent private readonly _onDidMovePanel = new Emitter(); readonly onDidMovePanel = this._onDidMovePanel.event; + private readonly _onDidMaximizedGroupChange = + new Emitter(); + readonly onDidMaximizedGroupChange = this._onDidMaximizedGroupChange.event; + private readonly _floatingGroups: DockviewFloatingGroupPanel[] = []; private readonly _popoutGroups: { window: PopoutWindow; @@ -395,6 +405,12 @@ export class DockviewComponent this._onDidActiveGroupChange.fire(event); } }), + this.onDidMaximizedChange((event) => { + this._onDidMaximizedGroupChange.fire({ + group: event.panel, + isMaximized: event.isMaximized, + }); + }), Event.any( this.onDidAdd, this.onDidRemove diff --git a/packages/dockview-core/src/gridview/baseComponentGridview.ts b/packages/dockview-core/src/gridview/baseComponentGridview.ts index e86dee387..610afefd4 100644 --- a/packages/dockview-core/src/gridview/baseComponentGridview.ts +++ b/packages/dockview-core/src/gridview/baseComponentGridview.ts @@ -1,5 +1,10 @@ import { Emitter, Event, AsapEvent } from '../events'; -import { getGridLocation, Gridview, IGridView } from './gridview'; +import { + getGridLocation, + Gridview, + IGridView, + MaximizedViewChanged, +} from './gridview'; import { Position } from '../dnd/droptarget'; import { Disposable, IDisposable, IValueDisposable } from '../lifecycle'; import { sequentialNumberGenerator } from '../math'; @@ -8,6 +13,7 @@ import { IPanel } from '../panel/types'; import { MovementOptions2 } from '../dockview/options'; import { Resizable } from '../resizable'; import { Classnames } from '../dom'; +import { IGridviewComponent } from './gridviewComponent'; const nextLayoutId = sequentialNumberGenerator(); @@ -29,6 +35,11 @@ export function toTarget(direction: Direction): Position { } } +export interface MaximizedChanged { + panel: T; + isMaximized: boolean; +} + export interface BaseGridOptions { readonly proportionalLayout: boolean; readonly orientation: Orientation; @@ -56,6 +67,8 @@ export interface IBaseGrid extends IDisposable { readonly activeGroup: T | undefined; readonly size: number; readonly groups: T[]; + readonly onDidMaximizedChange: Event>; + readonly onDidLayoutChange: Event; getPanel(id: string): T | undefined; toJSON(): object; fromJSON(data: any): void; @@ -67,8 +80,6 @@ export interface IBaseGrid extends IDisposable { isMaximizedGroup(panel: T): boolean; exitMaximizedGroup(): void; hasMaximizedGroup(): boolean; - readonly onDidMaximizedGroupChange: Event; - readonly onDidLayoutChange: Event; } export abstract class BaseGrid @@ -87,6 +98,10 @@ export abstract class BaseGrid private readonly _onDidAdd = new Emitter(); readonly onDidAdd: Event = this._onDidAdd.event; + private readonly _onDidMaximizedChange = new Emitter>(); + readonly onDidMaximizedChange: Event> = + this._onDidMaximizedChange.event; + private readonly _onDidActiveChange = new Emitter(); readonly onDidActiveChange: Event = this._onDidActiveChange.event; @@ -171,6 +186,12 @@ export abstract class BaseGrid this.layout(0, 0, true); // set some elements height/widths this.addDisposables( + this.gridview.onDidMaximizedNodeChange((event) => { + this._onDidMaximizedChange.fire({ + panel: event.view as T, + isMaximized: event.isMaximized, + }); + }), this.gridview.onDidViewVisibilityChange(() => this._onDidViewVisibilityChangeMicroTaskQueue.fire() ), @@ -250,10 +271,6 @@ export abstract class BaseGrid return this.gridview.hasMaximizedView(); } - get onDidMaximizedGroupChange(): Event { - return this.gridview.onDidMaximizedNodeChange; - } - protected doAddGroup( group: T, location: number[] = [0], diff --git a/packages/dockview-core/src/gridview/gridview.ts b/packages/dockview-core/src/gridview/gridview.ts index 7f76f4978..bda8c0638 100644 --- a/packages/dockview-core/src/gridview/gridview.ts +++ b/packages/dockview-core/src/gridview/gridview.ts @@ -265,11 +265,21 @@ export interface IViewDeserializer { fromJSON: (data: ISerializedLeafNode) => IGridView; } +export interface SerializedNodeDescriptor { + location: number[]; +} + export interface SerializedGridview { root: SerializedGridObject; width: number; height: number; orientation: Orientation; + maximizedNode?: SerializedNodeDescriptor; +} + +export interface MaximizedViewChanged { + view: IGridView; + isMaximized: boolean; } export class Gridview implements IDisposable { @@ -293,7 +303,8 @@ export class Gridview implements IDisposable { private readonly _onDidViewVisibilityChange = new Emitter(); readonly onDidViewVisibilityChange = this._onDidViewVisibilityChange.event; - private readonly _onDidMaximizedNodeChange = new Emitter(); + private readonly _onDidMaximizedNodeChange = + new Emitter(); readonly onDidMaximizedNodeChange = this._onDidMaximizedNodeChange.event; public get length(): number { @@ -395,6 +406,8 @@ export class Gridview implements IDisposable { this.exitMaximizedView(); } + serializeBranchNode(this.getView(), this.orientation); + const hiddenOnMaximize: LeafNode[] = []; function hideAllViewsBut(parent: BranchNode, exclude: LeafNode): void { @@ -416,7 +429,10 @@ export class Gridview implements IDisposable { hideAllViewsBut(this.root, node); this._maximizedNode = { leaf: node, hiddenOnMaximize }; - this._onDidMaximizedNodeChange.fire(); + this._onDidMaximizedNodeChange.fire({ + view: node.view, + isMaximized: true, + }); } exitMaximizedView(): void { @@ -441,27 +457,60 @@ export class Gridview implements IDisposable { showViewsInReverseOrder(this.root); + const tmp = this._maximizedNode.leaf; this._maximizedNode = undefined; - this._onDidMaximizedNodeChange.fire(); + this._onDidMaximizedNodeChange.fire({ + view: tmp.view, + isMaximized: false, + }); } public serialize(): SerializedGridview { + const maximizedView = this.maximizedView(); + + let maxmizedViewLocation: number[] | undefined; + + if (maximizedView) { + /** + * The minimum information we can get away with in order to serialize a maxmized view is it's location within the grid + * which is represented as a branch of indices + */ + maxmizedViewLocation = getGridLocation(maximizedView.element); + } + if (this.hasMaximizedView()) { /** - * do not persist maximized view state - * firstly exit any maximized views to ensure the correct dimensions are persisted + * the saved layout cannot be in its maxmized state otherwise all of the underlying + * view dimensions will be wrong + * + * To counteract this we temporaily remove the maximized view to compute the serialized output + * of the grid before adding back the maxmized view as to not alter the layout from the users + * perspective when `.toJSON()` is called */ this.exitMaximizedView(); } const root = serializeBranchNode(this.getView(), this.orientation); - return { + const resullt: SerializedGridview = { root, width: this.width, height: this.height, orientation: this.orientation, }; + + if (maxmizedViewLocation) { + resullt.maximizedNode = { + location: maxmizedViewLocation, + }; + } + + if (maximizedView) { + // replace any maximzied view that was removed for serialization purposes + this.maximizeView(maximizedView); + } + + return resullt; } public dispose(): void { @@ -502,6 +551,24 @@ export class Gridview implements IDisposable { deserializer, height ); + + /** + * The deserialied layout must be positioned through this.layout(...) + * before any maximizedNode can be positioned + */ + this.layout(json.width, json.height); + + if (json.maximizedNode) { + const location = json.maximizedNode.location; + + const [_, node] = this.getNode(location); + + if (!(node instanceof LeafNode)) { + return; + } + + this.maximizeView(node.view); + } } private _deserialize( diff --git a/packages/docs/sandboxes/react/dockview/demo-dockview/src/app.tsx b/packages/docs/sandboxes/react/dockview/demo-dockview/src/app.tsx index e2449a4ab..6595a3fce 100644 --- a/packages/docs/sandboxes/react/dockview/demo-dockview/src/app.tsx +++ b/packages/docs/sandboxes/react/dockview/demo-dockview/src/app.tsx @@ -206,6 +206,12 @@ const DockviewDemo = (props: { theme?: string }) => { addLogLine(`Panel Moved ${event.panel.id}`); }); + event.api.onDidMaximizedGroupChange((event) => { + addLogLine( + `Group Maximized Changed ${event.view.id} [${event.isMaximized}]` + ); + }); + event.api.onDidRemoveGroup((event) => { setGroups((_) => { const next = [..._]; @@ -318,6 +324,15 @@ const DockviewDemo = (props: { theme?: string }) => { engineering + {showLogs && ( + + )}