Merge pull request #798 from mathuo/797-popout-group-flow-error

bug: fixup popout group flows
This commit is contained in:
mathuo 2024-12-21 19:24:50 +00:00 committed by GitHub
commit 6fbe2b4233
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 349 additions and 104 deletions

View File

@ -140,109 +140,114 @@ describe('dockviewComponent', () => {
expect(dockview.element.className).toBe('test-b test-c');
});
// describe('memory leakage', () => {
// beforeEach(() => {
// window.open = () => fromPartial<Window>({
// 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();

View File

@ -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.
*/

View File

@ -506,7 +506,9 @@ export class DockviewGroupPanelModel
this._onDidAddPanel,
this._onDidRemovePanel,
this._onDidActivePanelChange,
this._onUnhandledDragOverEvent
this._onUnhandledDragOverEvent,
this._onDidPanelTitleChange,
this._onDidPanelParametersChange
);
}

View File

@ -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<string>();
const groups = new Set<string>();
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);
}
})
);
}
}

View File

@ -205,6 +205,8 @@ export abstract class BaseGrid<T extends IGridPanelView>
)(() => {
this._bufferOnDidLayoutChange.fire();
}),
this._onDidMaximizedChange,
this._onDidViewVisibilityChangeMicroTaskQueue,
this._bufferOnDidLayoutChange
);
}