feat: serialization of maximized views

This commit is contained in:
mathuo 2024-11-10 15:06:48 +00:00
parent 24cc974a68
commit 2f4150013b
No known key found for this signature in database
GPG Key ID: C6EEDEFD6CA07281
7 changed files with 350 additions and 184 deletions

View File

@ -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',
},
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',
},
{
type: 'leaf',
data: { views: ['panel4'], id: 'group-3' },
size: 500,
size: 500,
},
{
type: 'branch',
data: [
{
type: 'leaf',
data: {
views: ['panel2', 'panel3'],
id: 'group-2',
activeView: 'panel3',
},
size: 500,
},
{
type: 'leaf',
data: {
views: ['panel4'],
id: 'group-3',
activeView: 'panel4',
},
size: 500,
},
],
size: 250,
},
{
type: 'leaf',
data: {
views: ['panel5'],
id: 'group-4',
activeView: 'panel5',
},
],
size: 250,
},
{
type: 'leaf',
data: { views: ['panel5'], id: 'group-4' },
size: 250,
},
],
size: 1000,
size: 250,
},
],
size: 1000,
},
height: 1000,
width: 1000,
orientation: Orientation.VERTICAL,
},
height: 1000,
width: 1000,
orientation: Orientation.VERTICAL,
},
panels: {
panel1: {
id: 'panel1',
contentComponent: 'default',
tabComponent: 'tab-default',
title: 'panel1',
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,
},
},
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',
},
size: 500,
},
{
type: 'branch',
data: [
{
type: 'leaf',
data: {
views: ['panel2', 'panel3'],
id: 'group-2',
activeView: 'panel3',
},
size: 500,
},
{
type: 'leaf',
data: {
views: ['panel4'],
id: 'group-3',
activeView: 'panel4',
},
size: 500,
},
],
size: 250,
},
{
type: 'leaf',
data: {
views: ['panel5'],
id: 'group-4',
activeView: 'panel5',
},
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',
},
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());
});
});

View File

@ -1,4 +1,5 @@
import {
DockviewMaximizedGroupChanged,
FloatingGroupOptions,
IDockviewComponent,
MovePanelEvent,
@ -898,7 +899,7 @@ export class DockviewApi implements CommonApi<SerializedDockview> {
this.component.exitMaximizedGroup();
}
get onDidMaximizedGroupChange(): Event<void> {
get onDidMaximizedGroupChange(): Event<DockviewMaximizedGroupChanged> {
return this.component.onDidMaximizedGroupChange;
}

View File

@ -163,6 +163,11 @@ export interface FloatingGroupOptionsInternal extends FloatingGroupOptions {
skipActiveGroup?: boolean;
}
export interface DockviewMaximizedGroupChanged {
group: DockviewGroupPanel;
isMaximized: boolean;
}
export interface IDockviewComponent extends IBaseGrid<DockviewGroupPanel> {
readonly activePanel: IDockviewPanel | undefined;
readonly totalPanels: number;
@ -183,6 +188,7 @@ export interface IDockviewComponent extends IBaseGrid<DockviewGroupPanel> {
readonly onDidActiveGroupChange: Event<DockviewGroupPanel | undefined>;
readonly onUnhandledDragOverEvent: Event<DockviewDndOverlayEvent>;
readonly onDidMovePanel: Event<MovePanelEvent>;
readonly onDidMaximizedGroupChange: Event<DockviewMaximizedGroupChanged>;
readonly options: DockviewComponentOptions;
updateOptions(options: DockviewOptions): void;
moveGroupOrPanel(options: MoveGroupOrPanelOptions): void;
@ -275,6 +281,10 @@ export class DockviewComponent
private readonly _onDidMovePanel = new Emitter<MovePanelEvent>();
readonly onDidMovePanel = this._onDidMovePanel.event;
private readonly _onDidMaximizedGroupChange =
new Emitter<DockviewMaximizedGroupChanged>();
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

View File

@ -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<T extends IGridPanelView> {
panel: T;
isMaximized: boolean;
}
export interface BaseGridOptions {
readonly proportionalLayout: boolean;
readonly orientation: Orientation;
@ -56,6 +67,8 @@ export interface IBaseGrid<T extends IGridPanelView> extends IDisposable {
readonly activeGroup: T | undefined;
readonly size: number;
readonly groups: T[];
readonly onDidMaximizedChange: Event<MaximizedChanged<T>>;
readonly onDidLayoutChange: Event<void>;
getPanel(id: string): T | undefined;
toJSON(): object;
fromJSON(data: any): void;
@ -67,8 +80,6 @@ export interface IBaseGrid<T extends IGridPanelView> extends IDisposable {
isMaximizedGroup(panel: T): boolean;
exitMaximizedGroup(): void;
hasMaximizedGroup(): boolean;
readonly onDidMaximizedGroupChange: Event<void>;
readonly onDidLayoutChange: Event<void>;
}
export abstract class BaseGrid<T extends IGridPanelView>
@ -87,6 +98,10 @@ export abstract class BaseGrid<T extends IGridPanelView>
private readonly _onDidAdd = new Emitter<T>();
readonly onDidAdd: Event<T> = this._onDidAdd.event;
private readonly _onDidMaximizedChange = new Emitter<MaximizedChanged<T>>();
readonly onDidMaximizedChange: Event<MaximizedChanged<T>> =
this._onDidMaximizedChange.event;
private readonly _onDidActiveChange = new Emitter<T | undefined>();
readonly onDidActiveChange: Event<T | undefined> =
this._onDidActiveChange.event;
@ -171,6 +186,12 @@ export abstract class BaseGrid<T extends IGridPanelView>
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<T extends IGridPanelView>
return this.gridview.hasMaximizedView();
}
get onDidMaximizedGroupChange(): Event<void> {
return this.gridview.onDidMaximizedNodeChange;
}
protected doAddGroup(
group: T,
location: number[] = [0],

View File

@ -265,11 +265,21 @@ export interface IViewDeserializer {
fromJSON: (data: ISerializedLeafNode) => IGridView;
}
export interface SerializedNodeDescriptor {
location: number[];
}
export interface SerializedGridview<T> {
root: SerializedGridObject<T>;
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<void>();
readonly onDidViewVisibilityChange = this._onDidViewVisibilityChange.event;
private readonly _onDidMaximizedNodeChange = new Emitter<void>();
private readonly _onDidMaximizedNodeChange =
new Emitter<MaximizedViewChanged>();
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<any> {
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<any> = {
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(

View File

@ -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
</span>
</button>
{showLogs && (
<button
onClick={() => {
setLogLines([]);
}}
>
<span className="material-symbols-outlined">undo</span>
</button>
)}
<button
onClick={() => {
setShowLogs(!showLogs);

View File

@ -112,11 +112,10 @@ export const GridActions = (props: {
const onSave = () => {
if (props.api) {
console.log(props.api.toJSON());
localStorage.setItem(
'dv-demo-state',
JSON.stringify(props.api.toJSON())
);
const state = props.api.toJSON();
console.log(state);
localStorage.setItem('dv-demo-state', JSON.stringify(state));
}
};