diff --git a/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts b/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts index c646ca0a5..e1ea35cbd 100644 --- a/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts @@ -140,109 +140,114 @@ describe('dockviewComponent', () => { expect(dockview.element.className).toBe('test-b test-c'); }); - // describe('memory leakage', () => { - // beforeEach(() => { - // window.open = () => fromPartial({ - // addEventListener: jest.fn(), - // close: jest.fn(), - // }); - // }); + describe('memory leakage', () => { + beforeEach(() => { + window.open = () => setupMockWindow(); + }); - // test('event leakage', () => { - // Emitter.setLeakageMonitorEnabled(true); + test('event leakage', async () => { + Emitter.setLeakageMonitorEnabled(true); - // dockview = new DockviewComponent({ - // parentElement: container, - // components: { - // default: PanelContentPartTest, - // }, - // }); + dockview = new DockviewComponent(container, { + createComponent(options) { + switch (options.name) { + case 'default': + return new PanelContentPartTest( + options.id, + options.name + ); + default: + throw new Error(`unsupported`); + } + }, + className: 'test-a test-b', + }); - // dockview.layout(500, 1000); + dockview.layout(500, 1000); - // const panel1 = dockview.addPanel({ - // id: 'panel1', - // component: 'default', - // }); + const panel1 = dockview.addPanel({ + id: 'panel1', + component: 'default', + }); - // const panel2 = dockview.addPanel({ - // id: 'panel2', - // component: 'default', - // }); + const panel2 = dockview.addPanel({ + id: 'panel2', + component: 'default', + }); - // dockview.removePanel(panel2); + dockview.removePanel(panel2); - // const panel3 = dockview.addPanel({ - // id: 'panel3', - // component: 'default', - // position: { - // direction: 'right', - // referencePanel: 'panel1', - // }, - // }); + const panel3 = dockview.addPanel({ + id: 'panel3', + component: 'default', + position: { + direction: 'right', + referencePanel: 'panel1', + }, + }); - // const panel4 = dockview.addPanel({ - // id: 'panel4', - // component: 'default', - // position: { - // direction: 'above', - // }, - // }); + const panel4 = dockview.addPanel({ + id: 'panel4', + component: 'default', + position: { + direction: 'above', + }, + }); - // dockview.moveGroupOrPanel( - // panel4.group, - // panel3.group.id, - // panel3.id, - // 'center' - // ); + panel4.api.group.api.moveTo({ + group: panel3.api.group, + position: 'center', + }); - // dockview.addPanel({ - // id: 'panel5', - // component: 'default', - // floating: true, - // }); + dockview.addPanel({ + id: 'panel5', + component: 'default', + floating: true, + }); - // const panel6 = dockview.addPanel({ - // id: 'panel6', - // component: 'default', - // position: { - // referencePanel: 'panel5', - // direction: 'within', - // }, - // }); + const panel6 = dockview.addPanel({ + id: 'panel6', + component: 'default', + position: { + referencePanel: 'panel5', + direction: 'within', + }, + }); - // dockview.addFloatingGroup(panel4.api.group); + dockview.addFloatingGroup(panel4.api.group); - // dockview.addPopoutGroup(panel6); + await dockview.addPopoutGroup(panel2); - // dockview.moveGroupOrPanel( - // panel1.group, - // panel6.group.id, - // panel6.id, - // 'center' - // ); + panel1.api.group.api.moveTo({ + group: panel6.api.group, + position: 'center', + }); - // dockview.moveGroupOrPanel( - // panel4.group, - // panel6.group.id, - // panel6.id, - // 'center' - // ); + panel4.api.group.api.moveTo({ + group: panel6.api.group, + position: 'center', + }); - // dockview.dispose(); + dockview.dispose(); - // if (Emitter.MEMORY_LEAK_WATCHER.size > 0) { - // for (const entry of Array.from( - // Emitter.MEMORY_LEAK_WATCHER.events - // )) { - // console.log('disposal', entry[1]); - // } - // throw new Error('not all listeners disposed'); - // } + if (Emitter.MEMORY_LEAK_WATCHER.size > 0) { + console.warn( + `${Emitter.MEMORY_LEAK_WATCHER.size} undisposed resources` + ); - // Emitter.setLeakageMonitorEnabled(false); - // }); - // }); + for (const entry of Array.from( + Emitter.MEMORY_LEAK_WATCHER.events + )) { + console.log('disposal', entry[1]); + } + throw new Error( + `${Emitter.MEMORY_LEAK_WATCHER.size} undisposed resources` + ); + } + + Emitter.setLeakageMonitorEnabled(false); + }); + }); test('duplicate panel', () => { dockview.layout(500, 1000); @@ -3707,16 +3712,16 @@ describe('dockviewComponent', () => { floatingGroups: [ { data: { - views: ['panelB'], - activeView: 'panelB', + views: ['panelC'], + activeView: 'panelC', id: '3', }, position: { left: 0, top: 0, height: 100, width: 100 }, }, { data: { - views: ['panelC'], - activeView: 'panelC', + views: ['panelD'], + activeView: 'panelD', id: '4', }, position: { left: 0, top: 0, height: 100, width: 100 }, @@ -4867,6 +4872,155 @@ describe('dockviewComponent', () => { ); }); + test('basic', async () => { + jest.useRealTimers(); + + const container = document.createElement('div'); + + window.open = () => setupMockWindow(); + + const dockview = new DockviewComponent(container, { + createComponent(options) { + switch (options.name) { + case 'default': + return new PanelContentPartTest( + options.id, + options.name + ); + default: + throw new Error(`unsupported`); + } + }, + }); + + dockview.layout(1000, 1000); + + dockview.fromJSON({ + activeGroup: 'group-1', + grid: { + root: { + type: 'branch', + data: [ + { + type: 'leaf', + data: { + views: ['panel1'], + id: 'group-1', + activeView: 'panel1', + }, + size: 1000, + }, + ], + size: 1000, + }, + height: 1000, + width: 1000, + orientation: Orientation.VERTICAL, + }, + popoutGroups: [ + { + data: { + views: ['panel2'], + id: 'group-2', + activeView: 'panel2', + }, + position: null, + }, + ], + panels: { + panel1: { + id: 'panel1', + contentComponent: 'default', + title: 'panel1', + }, + panel2: { + id: 'panel2', + contentComponent: 'default', + title: 'panel2', + }, + }, + }); + + await new Promise((resolve) => setTimeout(resolve, 0)); + + const panel2 = dockview.api.getPanel('panel2'); + + const windowObject = + panel2?.api.location.type === 'popout' + ? panel2?.api.location.getWindow() + : undefined; + + expect(windowObject).toBeTruthy(); + + windowObject!.close(); + }); + + test('grid -> floating -> popout -> popout closed', async () => { + const container = document.createElement('div'); + + window.open = () => setupMockWindow(); + + const dockview = new DockviewComponent(container, { + createComponent(options) { + switch (options.name) { + case 'default': + return new PanelContentPartTest( + options.id, + options.name + ); + default: + throw new Error(`unsupported`); + } + }, + }); + + dockview.layout(1000, 500); + + const panel1 = dockview.addPanel({ + id: 'panel_1', + component: 'default', + }); + + const panel2 = dockview.addPanel({ + id: 'panel_2', + component: 'default', + }); + + const panel3 = dockview.addPanel({ + id: 'panel_3', + component: 'default', + position: { direction: 'right' }, + }); + + expect(panel1.api.location.type).toBe('grid'); + expect(panel2.api.location.type).toBe('grid'); + expect(panel3.api.location.type).toBe('grid'); + + dockview.addFloatingGroup(panel2); + + expect(panel1.api.location.type).toBe('grid'); + expect(panel2.api.location.type).toBe('floating'); + expect(panel3.api.location.type).toBe('grid'); + + await dockview.addPopoutGroup(panel2); + + expect(panel1.api.location.type).toBe('grid'); + expect(panel2.api.location.type).toBe('popout'); + expect(panel3.api.location.type).toBe('grid'); + + const windowObject = + panel2.api.location.type === 'popout' + ? panel2.api.location.getWindow() + : undefined; + expect(windowObject).toBeTruthy(); + + windowObject!.close(); + + expect(panel1.api.location.type).toBe('grid'); + expect(panel2.api.location.type).toBe('floating'); + expect(panel3.api.location.type).toBe('grid'); + }); + test('move popout group of 1 panel inside grid', async () => { const container = document.createElement('div'); @@ -5116,7 +5270,7 @@ describe('dockviewComponent', () => { mockWindow.close(); expect(panel1.group.api.location.type).toBe('grid'); - expect(panel2.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('floating'); expect(panel3.group.api.location.type).toBe('grid'); dockview.clear(); diff --git a/packages/dockview-core/src/dockview/dockviewComponent.ts b/packages/dockview-core/src/dockview/dockviewComponent.ts index cf7823d9f..d0d1c39aa 100644 --- a/packages/dockview-core/src/dockview/dockviewComponent.ts +++ b/packages/dockview-core/src/dockview/dockviewComponent.ts @@ -73,6 +73,7 @@ import { OverlayRenderContainer, } from '../overlay/overlayRenderContainer'; import { PopoutWindow } from '../popoutWindow'; +import { StrictEventsSequencing } from './strictEventsSequencing'; const DEFAULT_ROOT_OVERLAY_MODEL: DroptargetOverlayModel = { activationSize: { type: 'pixels', value: 10 }, @@ -388,6 +389,10 @@ export class DockviewComponent toggleClass(this.gridview.element, 'dv-dockview', true); toggleClass(this.element, 'dv-debug', !!options.debug); + if (options.debug) { + this.addDisposables(new StrictEventsSequencing(this)); + } + this.addDisposables( this.overlayRenderContainer, this._onWillDragPanel, @@ -404,6 +409,7 @@ export class DockviewComponent this._onDidRemoveGroup, this._onDidActiveGroupChange, this._onUnhandledDragOverEvent, + this._onDidMaximizedGroupChange, this.onDidViewVisibilityChangeMicroTaskQueue(() => { this.updateWatermark(); }), @@ -571,6 +577,11 @@ export class DockviewComponent this.updateWatermark(); } + override dispose(): void { + this.clear(); // explicitly clear the layout before cleaning up + super.dispose(); + } + override setVisible(panel: DockviewGroupPanel, visible: boolean): void { switch (panel.api.location.type) { case 'grid': @@ -706,6 +717,8 @@ export class DockviewComponent _window.window!.innerHeight ); + let floatingBox: AnchoredBox | undefined; + if (!options?.overridePopoutGroup && isGroupAddedToDom) { if (itemToPopout instanceof DockviewPanel) { this.movingLock(() => { @@ -727,7 +740,16 @@ export class DockviewComponent break; case 'floating': case 'popout': + floatingBox = this._floatingGroups + .find( + (value) => + value.group.api.id === + itemToPopout.api.id + ) + ?.overlay.toJSON(); + this.removeGroup(referenceGroup); + break; } } @@ -825,20 +847,31 @@ export class DockviewComponent }); } } else if (this.getPanel(group.id)) { - this.doRemoveGroup(group, { - skipDispose: true, - skipActive: true, - skipPopoutReturn: true, - }); - const removedGroup = group; - removedGroup.model.renderContainer = - this.overlayRenderContainer; - removedGroup.model.location = { type: 'grid' }; - returnedGroup = removedGroup; + if (floatingBox) { + this.addFloatingGroup(removedGroup, { + height: floatingBox.height, + width: floatingBox.width, + position: floatingBox, + }); + } else { + this.doRemoveGroup(removedGroup, { + skipDispose: true, + skipActive: true, + skipPopoutReturn: true, + }); - this.doAddGroup(removedGroup, [0]); + removedGroup.model.renderContainer = + this.overlayRenderContainer; + removedGroup.model.location = { type: 'grid' }; + returnedGroup = removedGroup; + + this.movingLock(() => { + // suppress group add events since the group already exists + this.doAddGroup(removedGroup, [0]); + }); + } this.doSetGroupAndPanelActive(removedGroup); } }) @@ -1290,6 +1323,7 @@ export class DockviewComponent locked: !!locked, hideHeader: !!hideHeader, }); + this._onDidAddGroup.fire(group); const createdPanels: IDockviewPanel[] = []; @@ -1306,8 +1340,6 @@ export class DockviewComponent createdPanels.push(panel); } - this._onDidAddGroup.fire(group); - for (let i = 0; i < views.length; i++) { const panel = createdPanels[i]; @@ -1394,6 +1426,7 @@ export class DockviewComponent 'dockview: failed to deserialize layout. Reverting changes', err ); + /** * Takes all the successfully created groups and remove all of their panels. */ diff --git a/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts b/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts index 17af6e573..a34d5ef10 100644 --- a/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts +++ b/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts @@ -506,7 +506,9 @@ export class DockviewGroupPanelModel this._onDidAddPanel, this._onDidRemovePanel, this._onDidActivePanelChange, - this._onUnhandledDragOverEvent + this._onUnhandledDragOverEvent, + this._onDidPanelTitleChange, + this._onDidPanelParametersChange ); } diff --git a/packages/dockview-core/src/dockview/strictEventsSequencing.ts b/packages/dockview-core/src/dockview/strictEventsSequencing.ts new file mode 100644 index 000000000..60f91e613 --- /dev/null +++ b/packages/dockview-core/src/dockview/strictEventsSequencing.ts @@ -0,0 +1,54 @@ +import { CompositeDisposable } from '../lifecycle'; +import { DockviewComponent } from './dockviewComponent'; + +export class StrictEventsSequencing extends CompositeDisposable { + constructor(private readonly accessor: DockviewComponent) { + super(); + + this.init(); + } + + private init(): void { + const panels = new Set(); + const groups = new Set(); + + this.addDisposables( + this.accessor.onDidAddPanel((panel) => { + if (panels.has(panel.api.id)) { + throw new Error( + `dockview: Invalid event sequence. [onDidAddPanel] called for panel ${panel.api.id} but panel already exists` + ); + } else { + panels.add(panel.api.id); + } + }), + this.accessor.onDidRemovePanel((panel) => { + if (!panels.has(panel.api.id)) { + throw new Error( + `dockview: Invalid event sequence. [onDidRemovePanel] called for panel ${panel.api.id} but panel does not exists` + ); + } else { + panels.delete(panel.api.id); + } + }), + this.accessor.onDidAddGroup((group) => { + if (groups.has(group.api.id)) { + throw new Error( + `dockview: Invalid event sequence. [onDidAddGroup] called for group ${group.api.id} but group already exists` + ); + } else { + groups.add(group.api.id); + } + }), + this.accessor.onDidRemoveGroup((group) => { + if (!groups.has(group.api.id)) { + throw new Error( + `dockview: Invalid event sequence. [onDidRemoveGroup] called for group ${group.api.id} but group does not exists` + ); + } else { + groups.delete(group.api.id); + } + }) + ); + } +} diff --git a/packages/dockview-core/src/gridview/baseComponentGridview.ts b/packages/dockview-core/src/gridview/baseComponentGridview.ts index 44e810a50..9d02993f3 100644 --- a/packages/dockview-core/src/gridview/baseComponentGridview.ts +++ b/packages/dockview-core/src/gridview/baseComponentGridview.ts @@ -205,6 +205,8 @@ export abstract class BaseGrid )(() => { this._bufferOnDidLayoutChange.fire(); }), + this._onDidMaximizedChange, + this._onDidViewVisibilityChangeMicroTaskQueue, this._bufferOnDidLayoutChange ); }