diff --git a/packages/dockview-core/src/__tests__/dockview/dockviewGroupPanelModel.spec.ts b/packages/dockview-core/src/__tests__/dockview/dockviewGroupPanelModel.spec.ts index d9b6d4bfb..6e2401877 100644 --- a/packages/dockview-core/src/__tests__/dockview/dockviewGroupPanelModel.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/dockviewGroupPanelModel.spec.ts @@ -209,6 +209,10 @@ export class TestPanel implements IDockviewPanel { }); } + updateFromStateModel(state: GroupviewPanelState): void { + // + } + init(params: IGroupPanelInitParameters) { this._params = params; } diff --git a/packages/dockview-core/src/api/component.api.ts b/packages/dockview-core/src/api/component.api.ts index fe7d5c2d3..13af839c0 100644 --- a/packages/dockview-core/src/api/component.api.ts +++ b/packages/dockview-core/src/api/component.api.ts @@ -854,8 +854,11 @@ export class DockviewApi implements CommonApi { /** * Create a component from a serialized object. */ - fromJSON(data: SerializedDockview): void { - this.component.fromJSON(data); + fromJSON( + data: SerializedDockview, + options?: { keepExistingPanels: boolean } + ): void { + this.component.fromJSON(data, options); } /** diff --git a/packages/dockview-core/src/dockview/dockviewComponent.ts b/packages/dockview-core/src/dockview/dockviewComponent.ts index 5a20f5b84..cf5e230a2 100644 --- a/packages/dockview-core/src/dockview/dockviewComponent.ts +++ b/packages/dockview-core/src/dockview/dockviewComponent.ts @@ -147,6 +147,7 @@ type MoveGroupOrPanelOptions = { position: Position; index?: number; }; + keepEmptyGroups?: boolean; }; export interface FloatingGroupOptions { @@ -219,6 +220,7 @@ export interface IDockviewComponent extends IBaseGrid { onWillClose?: (event: { id: string; window: Window }) => void; } ): Promise; + fromJSON(data: any, options?: { keepExistingPanels: boolean }): void; } export class DockviewComponent @@ -381,17 +383,17 @@ export class DockviewComponent this.updateWatermark(); }), this.onDidAdd((event) => { - if (!this._moving) { + if (!this._isEventSuppressionEnabled) { this._onDidAddGroup.fire(event); } }), this.onDidRemove((event) => { - if (!this._moving) { + if (!this._isEventSuppressionEnabled) { this._onDidRemoveGroup.fire(event); } }), this.onDidActiveChange((event) => { - if (!this._moving) { + if (!this._isEventSuppressionEnabled) { this._onDidActiveGroupChange.fire(event); } }), @@ -675,13 +677,13 @@ export class DockviewComponent if (!options?.overridePopoutGroup && isGroupAddedToDom) { if (itemToPopout instanceof DockviewPanel) { - this.movingLock(() => { + this.runWithSuppressedEvents(() => { const panel = referenceGroup.model.removePanel(itemToPopout); group.model.openPanel(panel); }); } else { - this.movingLock(() => + this.runWithSuppressedEvents(() => moveGroupWithoutDestroying({ from: referenceGroup, to: group, @@ -773,7 +775,7 @@ export class DockviewComponent isGroupAddedToDom && this.getPanel(referenceGroup.id) ) { - this.movingLock(() => + this.runWithSuppressedEvents(() => moveGroupWithoutDestroying({ from: group, to: referenceGroup, @@ -830,7 +832,7 @@ export class DockviewComponent group = this.createGroup(); this._onDidAddGroup.fire(group); - this.movingLock(() => + this.runWithSuppressedEvents(() => this.removePanel(item, { removeEmptyGroup: true, skipDispose: true, @@ -838,7 +840,7 @@ export class DockviewComponent }) ); - this.movingLock(() => + this.runWithSuppressedEvents(() => group.model.openPanel(item, { skipSetGroupActive: true }) ); } else { @@ -857,7 +859,7 @@ export class DockviewComponent if (!skip) { if (popoutReferenceGroup) { - this.movingLock(() => + this.runWithSuppressedEvents(() => moveGroupWithoutDestroying({ from: item, to: popoutReferenceGroup, @@ -955,7 +957,7 @@ export class DockviewComponent const el = group.element.querySelector('.dv-void-container'); if (!el) { - throw new Error('failed to find drag handle'); + throw new Error('dockview: failed to find drag handle'); } overlay.setupDrag(el, { @@ -1051,7 +1053,7 @@ export class DockviewComponent case 'right': return this.createGroupAtLocation([this.gridview.length]); // insert into last position default: - throw new Error(`unsupported position ${position}`); + throw new Error(`dockview: unsupported position ${position}`); } } @@ -1219,17 +1221,60 @@ export class DockviewComponent return result; } - fromJSON(data: SerializedDockview): void { + fromJSON( + data: SerializedDockview, + options?: { keepExistingPanels: boolean } + ): void { + const existingPanels = new Map(); + + let tempGroup: DockviewGroupPanel | undefined; + + if (options?.keepExistingPanels) { + /** + * What are we doing here? + * + * 1. Create a temporary group to hold any panels that currently exist and that also exist in the new layout + * 2. Remove that temporary group from the group mapping so that it doesn't get cleared when we clear the layout + */ + + tempGroup = this.createGroup(); + this._groups.delete(tempGroup.api.id); + + const newPanels = Object.keys(data.panels); + + for (const panel of this.panels) { + if (newPanels.includes(panel.api.id)) { + existingPanels.set(panel.api.id, panel); + } + } + + this.runWithSuppressedEvents(() => { + Array.from(existingPanels.values()).forEach((panel) => { + this.moveGroupOrPanel({ + from: { + groupId: panel.api.group.api.id, + panelId: panel.api.id, + }, + to: { + group: tempGroup!, + position: 'center', + }, + keepEmptyGroups: true, + }); + }); + }); + } + this.clear(); if (typeof data !== 'object' || data === null) { - throw new Error('serialized layout must be a non-null object'); + throw new Error('dockview: serialized layout must be a non-null object'); } const { grid, panels, activeGroup } = data; if (grid.root.type !== 'branch' || !Array.isArray(grid.root.data)) { - throw new Error('root must be of type branch'); + throw new Error('dockview: root must be of type branch'); } try { @@ -1243,7 +1288,7 @@ export class DockviewComponent const { id, locked, hideHeader, views, activeView } = data; if (typeof id !== 'string') { - throw new Error('group id must be of type string'); + throw new Error('dockview: group id must be of type string'); } const group = this.createGroup({ @@ -1260,11 +1305,23 @@ export class DockviewComponent * In running this section first we avoid firing lots of 'add' events in the event of a failure * due to a corruption of input data. */ - const panel = this._deserializer.fromJSON( - panels[child], - group - ); - createdPanels.push(panel); + + const existingPanel = existingPanels.get(child); + + if (tempGroup && existingPanel) { + this.runWithSuppressedEvents(() => { + tempGroup!.model.removePanel(existingPanel); + }); + + createdPanels.push(existingPanel); + existingPanel.updateFromStateModel(panels[child]); + } else { + const panel = this._deserializer.fromJSON( + panels[child], + group + ); + createdPanels.push(panel); + } } this._onDidAddGroup.fire(group); @@ -1276,10 +1333,21 @@ export class DockviewComponent typeof activeView === 'string' && activeView === panel.id; - group.model.openPanel(panel, { - skipSetActive: !isActive, - skipSetGroupActive: true, - }); + const hasExisting = existingPanels.has(panel.api.id); + + if (hasExisting) { + this.runWithSuppressedEvents(() => { + group.model.openPanel(panel, { + skipSetActive: !isActive, + skipSetGroupActive: true, + }); + }); + } else { + group.model.openPanel(panel, { + skipSetActive: !isActive, + skipSetGroupActive: true, + }); + } } if (!group.activePanel && group.panels.length > 0) { @@ -1426,14 +1494,14 @@ export class DockviewComponent options: AddPanelOptions ): DockviewPanel { if (this.panels.find((_) => _.id === options.id)) { - throw new Error(`panel with id ${options.id} already exists`); + throw new Error(`dockview: panel with id ${options.id} already exists`); } let referenceGroup: DockviewGroupPanel | undefined; if (options.position && options.floating) { throw new Error( - 'you can only provide one of: position, floating as arguments to .addPanel(...)' + 'dockview: you can only provide one of: position, floating as arguments to .addPanel(...)' ); } @@ -1454,7 +1522,7 @@ export class DockviewComponent if (!referencePanel) { throw new Error( - `referencePanel '${options.position.referencePanel}' does not exist` + `dockview: referencePanel '${options.position.referencePanel}' does not exist` ); } @@ -1469,7 +1537,7 @@ export class DockviewComponent if (!referenceGroup) { throw new Error( - `referenceGroup '${options.position.referenceGroup}' does not exist` + `dockview: referenceGroup '${options.position.referenceGroup}' does not exist` ); } } else { @@ -1634,7 +1702,7 @@ export class DockviewComponent if (!group) { throw new Error( - `cannot remove panel ${panel.id}. it's missing a group.` + `dockview: cannot remove panel ${panel.id}. it's missing a group.` ); } @@ -1700,7 +1768,7 @@ export class DockviewComponent if (!referencePanel) { throw new Error( - `reference panel ${options.referencePanel} does not exist` + `dockview: reference panel ${options.referencePanel} does not exist` ); } @@ -1708,7 +1776,7 @@ export class DockviewComponent if (!referenceGroup) { throw new Error( - `reference group for reference panel ${options.referencePanel} does not exist` + `dockview: reference group for reference panel ${options.referencePanel} does not exist` ); } } else if (isGroupOptionsWithGroup(options)) { @@ -1719,7 +1787,7 @@ export class DockviewComponent if (!referenceGroup) { throw new Error( - `reference group ${options.referenceGroup} does not exist` + `dockview: reference group ${options.referenceGroup} does not exist` ); } } else { @@ -1832,7 +1900,7 @@ export class DockviewComponent return floatingGroup.group; } - throw new Error('failed to find floating group'); + throw new Error('dockview: failed to find floating group'); } if (group.api.location.type === 'popout') { @@ -1878,7 +1946,7 @@ export class DockviewComponent return selectedGroup.popoutGroup; } - throw new Error('failed to find popout group'); + throw new Error('dockview: failed to find popout group'); } const re = super.doRemoveGroup(group, options); @@ -1892,16 +1960,21 @@ export class DockviewComponent return re; } - private _moving = false; + private _isEventSuppressionEnabled = false; - movingLock(func: () => T): T { - const isMoving = this._moving; + /** + * Code that runs within the provided function will not cause any events to fire. This is useful if you want + * to move things around as an intermediate step without raises any associated events + */ + runWithSuppressedEvents(func: () => T): T { + const isMoving = this._isEventSuppressionEnabled; try { - this._moving = true; + this._isEventSuppressionEnabled = true; return func(); } finally { - this._moving = isMoving; + // return to the original state which isn't necessarily false since calls may be nested + this._isEventSuppressionEnabled = isMoving; } } @@ -1917,7 +1990,7 @@ export class DockviewComponent : undefined; if (!sourceGroup) { - throw new Error(`Failed to find group id ${sourceGroupId}`); + throw new Error(`dockview: Failed to find group id ${sourceGroupId}`); } if (sourceItemId === undefined) { @@ -1940,24 +2013,24 @@ export class DockviewComponent * Dropping a panel within another group */ - const removedPanel: IDockviewPanel | undefined = this.movingLock( - () => + const removedPanel: IDockviewPanel | undefined = + this.runWithSuppressedEvents(() => sourceGroup.model.removePanel(sourceItemId, { skipSetActive: false, skipSetActiveGroup: true, }) - ); + ); if (!removedPanel) { - throw new Error(`No panel with id ${sourceItemId}`); + throw new Error(`dockview: No panel with id ${sourceItemId}`); } - if (sourceGroup.model.size === 0) { + if (!options.keepEmptyGroups && sourceGroup.model.size === 0) { // remove the group and do not set a new group as active this.doRemoveGroup(sourceGroup, { skipActive: true }); } - this.movingLock(() => + this.runWithSuppressedEvents(() => destinationGroup.model.openPanel(removedPanel, { index: destinationIndex, skipSetGroupActive: true, @@ -2028,7 +2101,7 @@ export class DockviewComponent )!; const removedPanel: IDockviewPanel | undefined = - this.movingLock(() => + this.runWithSuppressedEvents(() => popoutGroup.popoutGroup.model.removePanel( popoutGroup.popoutGroup.panels[0], { @@ -2041,7 +2114,7 @@ export class DockviewComponent this.doRemoveGroup(sourceGroup, { skipActive: true }); const newGroup = this.createGroupAtLocation(targetLocation); - this.movingLock(() => + this.runWithSuppressedEvents(() => newGroup.model.openPanel(removedPanel, { skipSetActive: true, }) @@ -2056,7 +2129,7 @@ export class DockviewComponent } // source group will become empty so delete the group - const targetGroup = this.movingLock(() => + const targetGroup = this.runWithSuppressedEvents(() => this.doRemoveGroup(sourceGroup, { skipActive: true, skipDispose: true, @@ -2073,7 +2146,9 @@ export class DockviewComponent updatedReferenceLocation, destinationTarget ); - this.movingLock(() => this.doAddGroup(targetGroup, location)); + this.runWithSuppressedEvents(() => + this.doAddGroup(targetGroup, location) + ); this.doSetGroupAndPanelActive(targetGroup); this._onDidMovePanel.fire({ @@ -2086,7 +2161,7 @@ export class DockviewComponent * create a new group, add the panels to that new group and add the new group in an appropiate position */ const removedPanel: IDockviewPanel | undefined = - this.movingLock(() => + this.runWithSuppressedEvents(() => sourceGroup.model.removePanel(sourceItemId, { skipSetActive: false, skipSetActiveGroup: true, @@ -2094,7 +2169,7 @@ export class DockviewComponent ); if (!removedPanel) { - throw new Error(`No panel with id ${sourceItemId}`); + throw new Error(`dockview: No panel with id ${sourceItemId}`); } const dropLocation = getRelativeLocation( @@ -2104,7 +2179,7 @@ export class DockviewComponent ); const group = this.createGroupAtLocation(dropLocation); - this.movingLock(() => + this.runWithSuppressedEvents(() => group.model.openPanel(removedPanel, { skipSetGroupActive: true, }) @@ -2127,7 +2202,7 @@ export class DockviewComponent if (target === 'center') { const activePanel = from.activePanel; - const panels = this.movingLock(() => + const panels = this.runWithSuppressedEvents(() => [...from.panels].map((p) => from.model.removePanel(p.id, { skipSetActive: true, @@ -2139,7 +2214,7 @@ export class DockviewComponent this.doRemoveGroup(from, { skipActive: true }); } - this.movingLock(() => { + this.runWithSuppressedEvents(() => { for (const panel of panels) { to.model.openPanel(panel, { skipSetActive: panel !== activePanel, @@ -2159,7 +2234,7 @@ export class DockviewComponent (x) => x.group === from ); if (!selectedFloatingGroup) { - throw new Error('failed to find floating group'); + throw new Error('dockview: failed to find floating group'); } selectedFloatingGroup.dispose(); break; @@ -2169,7 +2244,7 @@ export class DockviewComponent (x) => x.popoutGroup === from ); if (!selectedPopoutGroup) { - throw new Error('failed to find popout group'); + throw new Error('dockview: failed to find popout group'); } selectedPopoutGroup.disposable.dispose(); } @@ -2213,7 +2288,7 @@ export class DockviewComponent const activePanel = this.activePanel; if ( - !this._moving && + !this._isEventSuppressionEnabled && activePanel !== this._onDidActivePanelChange.value ) { this._onDidActivePanelChange.fire(activePanel); @@ -2234,7 +2309,7 @@ export class DockviewComponent } if ( - !this._moving && + !this._isEventSuppressionEnabled && activePanel !== this._onDidActivePanelChange.value ) { this._onDidActivePanelChange.fire(activePanel); @@ -2311,19 +2386,19 @@ export class DockviewComponent this._onUnhandledDragOverEvent.fire(event); }), view.model.onDidAddPanel((event) => { - if (this._moving) { + if (this._isEventSuppressionEnabled) { return; } this._onDidAddPanel.fire(event.panel); }), view.model.onDidRemovePanel((event) => { - if (this._moving) { + if (this._isEventSuppressionEnabled) { return; } this._onDidRemovePanel.fire(event.panel); }), view.model.onDidActivePanelChange((event) => { - if (this._moving) { + if (this._isEventSuppressionEnabled) { return; } if (event.panel !== this.activePanel) { diff --git a/packages/dockview-core/src/dockview/dockviewPanel.ts b/packages/dockview-core/src/dockview/dockviewPanel.ts index bf71ca6c1..c070a9c71 100644 --- a/packages/dockview-core/src/dockview/dockviewPanel.ts +++ b/packages/dockview-core/src/dockview/dockviewPanel.ts @@ -27,6 +27,7 @@ export interface IDockviewPanel extends IDisposable, IPanel { group: DockviewGroupPanel, options?: { skipSetActive?: boolean } ): void; + updateFromStateModel(state: GroupviewPanelState): void; init(params: IGroupPanelInitParameters): void; toJSON(): GroupviewPanelState; setTitle(title: string): void; @@ -45,10 +46,10 @@ export class DockviewPanel private _title: string | undefined; private _renderer: DockviewPanelRenderer | undefined; - private readonly _minimumWidth: number | undefined; - private readonly _minimumHeight: number | undefined; - private readonly _maximumWidth: number | undefined; - private readonly _maximumHeight: number | undefined; + private _minimumWidth: number | undefined; + private _minimumHeight: number | undefined; + private _maximumWidth: number | undefined; + private _maximumHeight: number | undefined; get params(): Parameters | undefined { return this._params; @@ -209,6 +210,20 @@ export class DockviewPanel }); } + updateFromStateModel(state: GroupviewPanelState): void { + this._maximumHeight = state.maximumHeight; + this._minimumHeight = state.minimumHeight; + this._maximumWidth = state.maximumWidth; + this._minimumWidth = state.minimumWidth; + + this.update({ params: state.params ?? {} }); + this.setTitle(state.title ?? this.id); + this.setRenderer(state.renderer ?? this.accessor.renderer); + + // state.contentComponent; + // state.tabComponent; + } + public updateParentGroup( group: DockviewGroupPanel, options?: { skipSetActive?: boolean } 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..fbae859b0 100644 --- a/packages/docs/sandboxes/react/dockview/demo-dockview/src/app.tsx +++ b/packages/docs/sandboxes/react/dockview/demo-dockview/src/app.tsx @@ -35,6 +35,12 @@ const components = { const isDebug = React.useContext(DebugContext); const metadata = usePanelApiMetadata(props.api); + const [firstRender, setFirstRender] = React.useState(''); + + React.useEffect(() => { + setFirstRender(new Date().toISOString()); + }, []); + return (
+
{firstRender}
+ {isDebug && (