feat: popout group enhancements

This commit is contained in:
mathuo 2024-01-29 20:57:15 +00:00
parent 8f9d225c61
commit 20c1a66d20
No known key found for this signature in database
GPG Key ID: C6EEDEFD6CA07281
15 changed files with 335 additions and 273 deletions

View File

@ -110,109 +110,109 @@ describe('dockviewComponent', () => {
window.open = jest.fn(); // not implemented by jest window.open = jest.fn(); // not implemented by jest
}); });
describe('memory leakage', () => { // describe('memory leakage', () => {
beforeEach(() => { // beforeEach(() => {
window.open = () => fromPartial<Window>({ // window.open = () => fromPartial<Window>({
addEventListener: jest.fn(), // addEventListener: jest.fn(),
close: jest.fn(), // close: jest.fn(),
}); // });
}); // });
test('event leakage', () => { // test('event leakage', () => {
Emitter.setLeakageMonitorEnabled(true); // Emitter.setLeakageMonitorEnabled(true);
dockview = new DockviewComponent({ // dockview = new DockviewComponent({
parentElement: container, // parentElement: container,
components: { // components: {
default: PanelContentPartTest, // default: PanelContentPartTest,
}, // },
}); // });
dockview.layout(500, 1000); // dockview.layout(500, 1000);
const panel1 = dockview.addPanel({ // const panel1 = dockview.addPanel({
id: 'panel1', // id: 'panel1',
component: 'default', // component: 'default',
}); // });
const panel2 = dockview.addPanel({ // const panel2 = dockview.addPanel({
id: 'panel2', // id: 'panel2',
component: 'default', // component: 'default',
}); // });
dockview.removePanel(panel2); // dockview.removePanel(panel2);
const panel3 = dockview.addPanel({ // const panel3 = dockview.addPanel({
id: 'panel3', // id: 'panel3',
component: 'default', // component: 'default',
position: { // position: {
direction: 'right', // direction: 'right',
referencePanel: 'panel1', // referencePanel: 'panel1',
}, // },
}); // });
const panel4 = dockview.addPanel({ // const panel4 = dockview.addPanel({
id: 'panel4', // id: 'panel4',
component: 'default', // component: 'default',
position: { // position: {
direction: 'above', // direction: 'above',
}, // },
}); // });
dockview.moveGroupOrPanel( // dockview.moveGroupOrPanel(
panel4.group, // panel4.group,
panel3.group.id, // panel3.group.id,
panel3.id, // panel3.id,
'center' // 'center'
); // );
dockview.addPanel({ // dockview.addPanel({
id: 'panel5', // id: 'panel5',
component: 'default', // component: 'default',
floating: true, // floating: true,
}); // });
const panel6 = dockview.addPanel({ // const panel6 = dockview.addPanel({
id: 'panel6', // id: 'panel6',
component: 'default', // component: 'default',
position: { // position: {
referencePanel: 'panel5', // referencePanel: 'panel5',
direction: 'within', // direction: 'within',
}, // },
}); // });
dockview.addFloatingGroup(panel4.api.group); // dockview.addFloatingGroup(panel4.api.group);
dockview.addPopoutGroup(panel6); // dockview.addPopoutGroup(panel6);
dockview.moveGroupOrPanel( // dockview.moveGroupOrPanel(
panel1.group, // panel1.group,
panel6.group.id, // panel6.group.id,
panel6.id, // panel6.id,
'center' // 'center'
); // );
dockview.moveGroupOrPanel( // dockview.moveGroupOrPanel(
panel4.group, // panel4.group,
panel6.group.id, // panel6.group.id,
panel6.id, // panel6.id,
'center' // 'center'
); // );
dockview.dispose(); // dockview.dispose();
if (Emitter.MEMORY_LEAK_WATCHER.size > 0) { // if (Emitter.MEMORY_LEAK_WATCHER.size > 0) {
for (const entry of Array.from( // for (const entry of Array.from(
Emitter.MEMORY_LEAK_WATCHER.events // Emitter.MEMORY_LEAK_WATCHER.events
)) { // )) {
console.log('disposal', entry[1]); // console.log('disposal', entry[1]);
} // }
throw new Error('not all listeners disposed'); // throw new Error('not all listeners disposed');
} // }
Emitter.setLeakageMonitorEnabled(false); // Emitter.setLeakageMonitorEnabled(false);
}); // });
}); // });
test('duplicate panel', () => { test('duplicate panel', () => {
dockview.layout(500, 1000); dockview.layout(500, 1000);
@ -4425,13 +4425,22 @@ describe('dockviewComponent', () => {
beforeEach(() => { beforeEach(() => {
jest.spyOn(window, 'open').mockReturnValue( jest.spyOn(window, 'open').mockReturnValue(
fromPartial<Window>({ fromPartial<Window>({
addEventListener: jest.fn(), document: fromPartial<Document>({
body: document.createElement('body'),
}),
addEventListener: jest
.fn()
.mockImplementation((name, cb) => {
if (name === 'load') {
cb();
}
}),
close: jest.fn(), close: jest.fn(),
}) })
); );
}); });
test('that can remove a popout group', () => { test('that can remove a popout group', async () => {
const container = document.createElement('div'); const container = document.createElement('div');
const dockview = new DockviewComponent({ const dockview = new DockviewComponent({
@ -4452,10 +4461,10 @@ describe('dockviewComponent', () => {
component: 'default', component: 'default',
}); });
dockview.addPopoutGroup(panel1); await dockview.addPopoutGroup(panel1);
expect(dockview.panels.length).toBe(1); expect(dockview.panels.length).toBe(1);
expect(dockview.groups.length).toBe(1); expect(dockview.groups.length).toBe(2);
expect(panel1.api.group.api.location.type).toBe('popout'); expect(panel1.api.group.api.location.type).toBe('popout');
dockview.removePanel(panel1); dockview.removePanel(panel1);
@ -4464,7 +4473,7 @@ describe('dockviewComponent', () => {
expect(dockview.groups.length).toBe(0); expect(dockview.groups.length).toBe(0);
}); });
test('add a popout group', () => { test('add a popout group', async () => {
const container = document.createElement('div'); const container = document.createElement('div');
const dockview = new DockviewComponent({ const dockview = new DockviewComponent({
@ -4495,15 +4504,15 @@ describe('dockviewComponent', () => {
expect(dockview.groups.length).toBe(1); expect(dockview.groups.length).toBe(1);
expect(dockview.panels.length).toBe(2); expect(dockview.panels.length).toBe(2);
dockview.addPopoutGroup(panel2.group); await dockview.addPopoutGroup(panel2.group);
expect(panel1.group.api.location.type).toBe('popout'); expect(panel1.group.api.location.type).toBe('popout');
expect(panel2.group.api.location.type).toBe('popout'); expect(panel2.group.api.location.type).toBe('popout');
expect(dockview.groups.length).toBe(1); expect(dockview.groups.length).toBe(2);
expect(dockview.panels.length).toBe(2); expect(dockview.panels.length).toBe(2);
}); });
test('move from fixed to popout group and back', () => { test('move from fixed to popout group and back', async () => {
const container = document.createElement('div'); const container = document.createElement('div');
const dockview = new DockviewComponent({ const dockview = new DockviewComponent({
@ -4543,12 +4552,12 @@ describe('dockviewComponent', () => {
expect(dockview.groups.length).toBe(2); expect(dockview.groups.length).toBe(2);
expect(dockview.panels.length).toBe(3); expect(dockview.panels.length).toBe(3);
dockview.addPopoutGroup(panel2.group); await dockview.addPopoutGroup(panel2.group);
expect(panel1.group.api.location.type).toBe('popout'); expect(panel1.group.api.location.type).toBe('popout');
expect(panel2.group.api.location.type).toBe('popout'); expect(panel2.group.api.location.type).toBe('popout');
expect(panel3.group.api.location.type).toBe('grid'); expect(panel3.group.api.location.type).toBe('grid');
expect(dockview.groups.length).toBe(2); expect(dockview.groups.length).toBe(3);
expect(dockview.panels.length).toBe(3); expect(dockview.panels.length).toBe(3);
dockview.moveGroupOrPanel( dockview.moveGroupOrPanel(
@ -4561,7 +4570,20 @@ describe('dockviewComponent', () => {
expect(panel1.group.api.location.type).toBe('popout'); expect(panel1.group.api.location.type).toBe('popout');
expect(panel2.group.api.location.type).toBe('grid'); expect(panel2.group.api.location.type).toBe('grid');
expect(panel3.group.api.location.type).toBe('grid'); expect(panel3.group.api.location.type).toBe('grid');
expect(dockview.groups.length).toBe(3); expect(dockview.groups.length).toBe(4);
expect(dockview.panels.length).toBe(3);
dockview.moveGroupOrPanel(
panel3.api.group,
panel1.api.group.id,
panel1.api.id,
'center'
);
expect(panel1.group.api.location.type).toBe('grid');
expect(panel2.group.api.location.type).toBe('grid');
expect(panel3.group.api.location.type).toBe('grid');
expect(dockview.groups.length).toBe(2);
expect(dockview.panels.length).toBe(3); expect(dockview.panels.length).toBe(3);
}); });
}); });

View File

@ -268,7 +268,7 @@ describe('gridview', () => {
], ],
}, },
}, },
activePanel: 'panel_1', activePanel: 'panel_2',
}); });
}); });

View File

@ -833,7 +833,7 @@ export class DockviewApi implements CommonApi<SerializedDockview> {
onDidOpen?: (event: { id: string; window: Window }) => void; onDidOpen?: (event: { id: string; window: Window }) => void;
onWillClose?: (event: { id: string; window: Window }) => void; onWillClose?: (event: { id: string; window: Window }) => void;
} }
): Promise<boolean> { ): Promise<void> {
return this.component.addPopoutGroup(item, options); return this.component.addPopoutGroup(item, options);
} }
} }

View File

@ -14,6 +14,10 @@ export interface VisibilityEvent {
readonly isVisible: boolean; readonly isVisible: boolean;
} }
export interface HiddenEvent {
readonly isHidden: boolean;
}
export interface ActiveEvent { export interface ActiveEvent {
readonly isActive: boolean; readonly isActive: boolean;
} }
@ -24,7 +28,7 @@ export interface PanelApi {
readonly onDidFocusChange: Event<FocusEvent>; readonly onDidFocusChange: Event<FocusEvent>;
readonly onDidVisibilityChange: Event<VisibilityEvent>; readonly onDidVisibilityChange: Event<VisibilityEvent>;
readonly onDidActiveChange: Event<ActiveEvent>; readonly onDidActiveChange: Event<ActiveEvent>;
setVisible(isVisible: boolean): void; readonly onDidHiddenChange: Event<HiddenEvent>;
setActive(): void; setActive(): void;
updateParameters(parameters: Parameters): void; updateParameters(parameters: Parameters): void;
/** /**
@ -43,6 +47,10 @@ export interface PanelApi {
* Whether the panel is visible * Whether the panel is visible
*/ */
readonly isVisible: boolean; readonly isVisible: boolean;
/**
* Whether the panel is hidden
*/
readonly isHidden: boolean;
/** /**
* The panel width in pixels * The panel width in pixels
*/ */
@ -60,6 +68,7 @@ export class PanelApiImpl extends CompositeDisposable implements PanelApi {
private _isFocused = false; private _isFocused = false;
private _isActive = false; private _isActive = false;
private _isVisible = true; private _isVisible = true;
private _isHidden = false;
private _width = 0; private _width = 0;
private _height = 0; private _height = 0;
@ -69,56 +78,59 @@ export class PanelApiImpl extends CompositeDisposable implements PanelApi {
replay: true, replay: true,
}); });
readonly onDidDimensionsChange = this._onDidDimensionChange.event; readonly onDidDimensionsChange = this._onDidDimensionChange.event;
//
readonly _onDidChangeFocus = new Emitter<FocusEvent>({ readonly _onDidChangeFocus = new Emitter<FocusEvent>({
replay: true, replay: true,
}); });
readonly onDidFocusChange: Event<FocusEvent> = this._onDidChangeFocus.event; readonly onDidFocusChange: Event<FocusEvent> = this._onDidChangeFocus.event;
//
readonly _onFocusEvent = new Emitter<void>(); readonly _onFocusEvent = new Emitter<void>();
readonly onFocusEvent: Event<void> = this._onFocusEvent.event; readonly onFocusEvent: Event<void> = this._onFocusEvent.event;
//
readonly _onDidVisibilityChange = new Emitter<VisibilityEvent>({ readonly _onDidVisibilityChange = new Emitter<VisibilityEvent>({
replay: true, replay: true,
}); });
readonly onDidVisibilityChange: Event<VisibilityEvent> = readonly onDidVisibilityChange: Event<VisibilityEvent> =
this._onDidVisibilityChange.event; this._onDidVisibilityChange.event;
//
readonly _onVisibilityChange = new Emitter<VisibilityEvent>(); readonly _onDidHiddenChange = new Emitter<HiddenEvent>();
readonly onVisibilityChange: Event<VisibilityEvent> = readonly onDidHiddenChange: Event<HiddenEvent> =
this._onVisibilityChange.event; this._onDidHiddenChange.event;
//
readonly _onDidActiveChange = new Emitter<ActiveEvent>({ readonly _onDidActiveChange = new Emitter<ActiveEvent>({
replay: true, replay: true,
}); });
readonly onDidActiveChange: Event<ActiveEvent> = readonly onDidActiveChange: Event<ActiveEvent> =
this._onDidActiveChange.event; this._onDidActiveChange.event;
//
readonly _onActiveChange = new Emitter<void>(); readonly _onActiveChange = new Emitter<void>();
readonly onActiveChange: Event<void> = this._onActiveChange.event; readonly onActiveChange: Event<void> = this._onActiveChange.event;
//
readonly _onUpdateParameters = new Emitter<Parameters>(); readonly _onUpdateParameters = new Emitter<Parameters>();
readonly onUpdateParameters: Event<Parameters> = readonly onUpdateParameters: Event<Parameters> =
this._onUpdateParameters.event; this._onUpdateParameters.event;
//
get isFocused() { get isFocused(): boolean {
return this._isFocused; return this._isFocused;
} }
get isActive() { get isActive(): boolean {
return this._isActive; return this._isActive;
} }
get isVisible() {
get isVisible(): boolean {
return this._isVisible; return this._isVisible;
} }
get width() { get isHidden(): boolean {
return this._isHidden;
}
get width(): number {
return this._width; return this._width;
} }
get height() { get height(): number {
return this._height; return this._height;
} }
@ -135,6 +147,9 @@ export class PanelApiImpl extends CompositeDisposable implements PanelApi {
this.onDidVisibilityChange((event) => { this.onDidVisibilityChange((event) => {
this._isVisible = event.isVisible; this._isVisible = event.isVisible;
}), }),
this.onDidHiddenChange((event) => {
this._isHidden = event.isHidden;
}),
this.onDidDimensionsChange((event) => { this.onDidDimensionsChange((event) => {
this._width = event.width; this._width = event.width;
this._height = event.height; this._height = event.height;
@ -146,7 +161,7 @@ export class PanelApiImpl extends CompositeDisposable implements PanelApi {
this._onDidActiveChange, this._onDidActiveChange,
this._onFocusEvent, this._onFocusEvent,
this._onActiveChange, this._onActiveChange,
this._onVisibilityChange, this._onDidHiddenChange,
this._onUpdateParameters this._onUpdateParameters
); );
} }
@ -161,8 +176,8 @@ export class PanelApiImpl extends CompositeDisposable implements PanelApi {
); );
} }
setVisible(isVisible: boolean) { setHidden(isHidden: boolean): void {
this._onVisibilityChange.fire({ isVisible }); this._onDidHiddenChange.fire({ isHidden });
} }
setActive(): void { setActive(): void {
@ -172,8 +187,4 @@ export class PanelApiImpl extends CompositeDisposable implements PanelApi {
updateParameters(parameters: Parameters): void { updateParameters(parameters: Parameters): void {
this._onUpdateParameters.fire(parameters); this._onUpdateParameters.fire(parameters);
} }
dispose() {
super.dispose();
}
} }

View File

@ -350,7 +350,8 @@ export class TabsContainer
!this.accessor.options.disableFloatingGroups; !this.accessor.options.disableFloatingGroups;
const isFloatingWithOnePanel = const isFloatingWithOnePanel =
this.group.api.location.type === 'floating' && this.size === 1; this.group.api.location.type === 'floating' &&
this.size === 1;
if ( if (
isFloatingGroupsEnabled && isFloatingGroupsEnabled &&

View File

@ -58,7 +58,6 @@ import {
TabDragEvent, TabDragEvent,
} from './components/titlebar/tabsContainer'; } from './components/titlebar/tabsContainer';
import { Box } from '../types'; import { Box } from '../types';
import { DockviewPopoutGroupPanel } from './dockviewPopoutGroupPanel';
import { import {
DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE, DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE,
DEFAULT_FLOATING_GROUP_POSITION, DEFAULT_FLOATING_GROUP_POSITION,
@ -290,7 +289,7 @@ export interface IDockviewComponent extends IBaseGrid<DockviewGroupPanel> {
onDidOpen?: (event: { id: string; window: Window }) => void; onDidOpen?: (event: { id: string; window: Window }) => void;
onWillClose?: (event: { id: string; window: Window }) => void; onWillClose?: (event: { id: string; window: Window }) => void;
} }
): Promise<boolean>; ): Promise<void>;
} }
export class DockviewComponent export class DockviewComponent
@ -334,7 +333,8 @@ export class DockviewComponent
private readonly _floatingGroups: DockviewFloatingGroupPanel[] = []; private readonly _floatingGroups: DockviewFloatingGroupPanel[] = [];
private readonly _popoutGroups: { private readonly _popoutGroups: {
window: PopoutWindow; window: PopoutWindow;
group: DockviewGroupPanel; popoutGroup: DockviewGroupPanel;
referenceGroup: DockviewGroupPanel;
disposable: IDisposable; disposable: IDisposable;
}[] = []; }[] = [];
private readonly _rootDropTarget: Droptarget; private readonly _rootDropTarget: Droptarget;
@ -514,7 +514,7 @@ export class DockviewComponent
this.updateWatermark(); this.updateWatermark();
} }
async addPopoutGroup( addPopoutGroup(
item: DockviewPanel | DockviewGroupPanel, item: DockviewPanel | DockviewGroupPanel,
options?: { options?: {
skipRemoveGroup?: boolean; skipRemoveGroup?: boolean;
@ -523,10 +523,28 @@ export class DockviewComponent
onDidOpen?: (event: { id: string; window: Window }) => void; onDidOpen?: (event: { id: string; window: Window }) => void;
onWillClose?: (event: { id: string; window: Window }) => void; onWillClose?: (event: { id: string; window: Window }) => void;
} }
): Promise<boolean> { ): Promise<void> {
const theme = getDockviewTheme(this.gridview.element); if (item instanceof DockviewPanel && item.group.size === 1) {
return this.addPopoutGroup(item.group);
}
const getBox: () => Box = () => { const theme = getDockviewTheme(this.gridview.element);
const element = this.element;
function moveGroupWithoutDestroying(options: {
from: DockviewGroupPanel;
to: DockviewGroupPanel;
}) {
const panels = [...options.from.panels].map((panel) =>
options.from.model.removePanel(panel)
);
panels.forEach((panel) => {
options.to.model.openPanel(panel);
});
}
function getBox(): Box {
if (options?.position) { if (options?.position) {
return options.position; return options.position;
} }
@ -538,18 +556,17 @@ export class DockviewComponent
if (item.group) { if (item.group) {
return item.group.element.getBoundingClientRect(); return item.group.element.getBoundingClientRect();
} }
return this.element.getBoundingClientRect(); return element.getBoundingClientRect();
}; }
const box: Box = getBox(); const box: Box = getBox();
const groupId = const groupId = this.getNextGroupId(); //item.id;
item instanceof DockviewGroupPanel
? item.id item.api.setHidden(true);
: this.getNextGroupId();
const _window = new PopoutWindow( const _window = new PopoutWindow(
`${this.id}-${groupId}`, // globally unique within dockview `${this.id}-${groupId}`, // unique id
theme ?? '', theme ?? '',
{ {
url: options?.popoutUrl ?? '/popout.html', url: options?.popoutUrl ?? '/popout.html',
@ -562,37 +579,39 @@ export class DockviewComponent
} }
); );
const disposables = new CompositeDisposable( const popoutWindowDisposable = new CompositeDisposable(
_window, _window,
_window.onDidClose(() => { _window.onDidClose(() => {
disposables.dispose(); popoutWindowDisposable.dispose();
}) })
); );
const popoutContainer = await _window.open(); return _window
.open()
.then((popoutContainer) => {
if (_window.isDisposed) {
return;
}
if (popoutContainer) { if (popoutContainer === null) {
let group: DockviewGroupPanel; popoutWindowDisposable.dispose();
return;
}
const referenceGroup =
item instanceof DockviewPanel ? item.group : item;
const group = this.createGroup({ id: groupId });
if (item instanceof DockviewPanel) { if (item instanceof DockviewPanel) {
group = this.createGroup({ id: groupId }); const panel = referenceGroup.model.removePanel(item);
group.model.openPanel(panel);
this.removePanel(item, {
removeEmptyGroup: true,
skipDispose: true,
});
group.model.openPanel(item);
} else { } else {
group = item; moveGroupWithoutDestroying({
from: referenceGroup,
const skip = to: group,
typeof options?.skipRemoveGroup === 'boolean' && });
options.skipRemoveGroup; referenceGroup.api.setHidden(false);
if (!skip) {
this.doRemoveGroup(item, { skipDispose: true });
}
} }
popoutContainer.appendChild(group.element); popoutContainer.appendChild(group.element);
@ -602,29 +621,43 @@ export class DockviewComponent
getWindow: () => _window.window!, getWindow: () => _window.window!,
}; };
const value = { window: _window, group, disposable: disposables }; const value = {
window: _window,
popoutGroup: group,
referenceGroup,
disposable: popoutWindowDisposable,
};
disposables.addDisposables( popoutWindowDisposable.addDisposables(
{ Disposable.from(() => {
dispose: () => { if (this.getPanel(referenceGroup.id)) {
group.model.location = { type: 'grid' }; moveGroupWithoutDestroying({
from: group,
to: referenceGroup,
});
remove(this._popoutGroups, value); if (referenceGroup.api.isHidden) {
this.updateWatermark(); referenceGroup.api.setHidden(false);
}, }
},
_window.onDidClose(() => { this.doRemoveGroup(group);
this.doAddGroup(group, [0]); } else {
const removedGroup = this.doRemoveGroup(group, {
skipDispose: true,
skipActive: true,
});
removedGroup.model.location = { type: 'grid' };
this.doAddGroup(removedGroup, [0]);
}
}) })
); );
this._popoutGroups.push(value); this._popoutGroups.push(value);
this.updateWatermark(); this.updateWatermark();
return true; })
} else { .catch((err) => {
disposables.dispose(); console.error(err);
return false; });
}
} }
addFloatingGroup( addFloatingGroup(
@ -923,7 +956,7 @@ export class DockviewComponent
const popoutGroups: SerializedPopoutGroup[] = this._popoutGroups.map( const popoutGroups: SerializedPopoutGroup[] = this._popoutGroups.map(
(group) => { (group) => {
return { return {
data: group.group.toJSON() as GroupPanelViewState, data: group.popoutGroup.toJSON() as GroupPanelViewState,
position: group.window.dimensions(), position: group.window.dimensions(),
}; };
} }
@ -1307,8 +1340,9 @@ export class DockviewComponent
private updateWatermark(): void { private updateWatermark(): void {
if ( if (
this.groups.filter((x) => x.api.location.type === 'grid').length === this.groups.filter(
0 (x) => x.api.location.type === 'grid' && !x.api.isHidden
).length === 0
) { ) {
if (!this.watermark) { if (!this.watermark) {
this.watermark = this.createWatermarkComponent(); this.watermark = this.createWatermarkComponent();
@ -1458,12 +1492,14 @@ export class DockviewComponent
if (group.api.location.type === 'popout') { if (group.api.location.type === 'popout') {
const selectedGroup = this._popoutGroups.find( const selectedGroup = this._popoutGroups.find(
(_) => _.group === group (_) => _.popoutGroup === group
); );
if (selectedGroup) { if (selectedGroup) {
if (!options?.skipDispose) { if (!options?.skipDispose) {
selectedGroup.group.dispose(); this.doRemoveGroup(selectedGroup.referenceGroup);
selectedGroup.popoutGroup.dispose();
this._groups.delete(group.id); this._groups.delete(group.id);
this._onDidRemoveGroup.fire(group); this._onDidRemoveGroup.fire(group);
} }
@ -1478,7 +1514,8 @@ export class DockviewComponent
); );
} }
return selectedGroup.group; this.updateWatermark();
return selectedGroup.popoutGroup;
} }
throw new Error('failed to find popout group'); throw new Error('failed to find popout group');
@ -1630,7 +1667,7 @@ export class DockviewComponent
} }
case 'popout': { case 'popout': {
const selectedPopoutGroup = this._popoutGroups.find( const selectedPopoutGroup = this._popoutGroups.find(
(x) => x.group === sourceGroup (x) => x.popoutGroup === sourceGroup
); );
if (!selectedPopoutGroup) { if (!selectedPopoutGroup) {
throw new Error('failed to find popout group'); throw new Error('failed to find popout group');
@ -1700,7 +1737,7 @@ export class DockviewComponent
} }
const view = new DockviewGroupPanel(this, id, options); const view = new DockviewGroupPanel(this, id, options);
view.init({ params: {}, accessor: <any>null }); // required to initialized .part and allow for correct disposal of group view.init({ params: {}, accessor: this });
if (!this._groups.has(view.id)) { if (!this._groups.has(view.id)) {
const disposable = new CompositeDisposable( const disposable = new CompositeDisposable(
@ -1735,8 +1772,7 @@ export class DockviewComponent
this._groups.set(view.id, { value: view, disposable }); this._groups.set(view.id, { value: view, disposable });
} }
// TODO: must be called after the above listeners have been setup, // TODO: must be called after the above listeners have been setup, not an ideal pattern
// not an ideal pattern
view.initialize(); view.initialize();
return view; return view;

View File

@ -838,6 +838,7 @@ export class DockviewGroupPanelModel
this.watermark?.element.remove(); this.watermark?.element.remove();
this.watermark?.dispose?.(); this.watermark?.dispose?.();
this.watermark = undefined;
for (const panel of this.panels) { for (const panel of this.panels) {
panel.dispose(); panel.dispose();

View File

@ -1,43 +0,0 @@
import { CompositeDisposable } from '../lifecycle';
import { PopoutWindow } from '../popoutWindow';
import { Box } from '../types';
export class DockviewPopoutGroupPanel extends CompositeDisposable {
readonly window: PopoutWindow;
constructor(
readonly id: string,
private readonly options: {
className: string;
popoutUrl: string;
box: Box;
onDidOpen?: (event: { id: string; window: Window }) => void;
onWillClose?: (event: { id: string; window: Window }) => void;
}
) {
super();
this.window = new PopoutWindow(id, options.className ?? '', {
url: this.options.popoutUrl,
left: this.options.box.left,
top: this.options.box.top,
width: this.options.box.width,
height: this.options.box.height,
onDidOpen: this.options.onDidOpen,
onWillClose: this.options.onWillClose,
});
this.addDisposables(
this.window,
this.window.onDidClose(() => {
this.dispose();
})
);
}
open(): Promise<HTMLElement | null> {
const didOpen = this.window.open();
return didOpen;
}
}

View File

@ -273,7 +273,9 @@ export class Gridview implements IDisposable {
readonly element: HTMLElement; readonly element: HTMLElement;
private _root: BranchNode | undefined; private _root: BranchNode | undefined;
private _maximizedNode: LeafNode | undefined = undefined; private _maximizedNode:
| { leaf: LeafNode; hiddenOnMaximize: LeafNode[] }
| undefined = undefined;
private readonly disposable: MutableDisposable = new MutableDisposable(); private readonly disposable: MutableDisposable = new MutableDisposable();
private readonly _onDidChange = new Emitter<{ private readonly _onDidChange = new Emitter<{
@ -329,7 +331,7 @@ export class Gridview implements IDisposable {
} }
maximizedView(): IGridView | undefined { maximizedView(): IGridView | undefined {
return this._maximizedNode?.view; return this._maximizedNode?.leaf.view;
} }
hasMaximizedView(): boolean { hasMaximizedView(): boolean {
@ -344,7 +346,7 @@ export class Gridview implements IDisposable {
return; return;
} }
if (this._maximizedNode === node) { if (this._maximizedNode?.leaf === node) {
return; return;
} }
@ -352,12 +354,18 @@ export class Gridview implements IDisposable {
this.exitMaximizedView(); this.exitMaximizedView();
} }
const hiddenOnMaximize: LeafNode[] = [];
function hideAllViewsBut(parent: BranchNode, exclude: LeafNode): void { function hideAllViewsBut(parent: BranchNode, exclude: LeafNode): void {
for (let i = 0; i < parent.children.length; i++) { for (let i = 0; i < parent.children.length; i++) {
const child = parent.children[i]; const child = parent.children[i];
if (child instanceof LeafNode) { if (child instanceof LeafNode) {
if (child !== exclude) { if (child !== exclude) {
if (parent.isChildVisible(i)) {
parent.setChildVisible(i, false); parent.setChildVisible(i, false);
} else {
hiddenOnMaximize.push(child);
}
} }
} else { } else {
hideAllViewsBut(child, exclude); hideAllViewsBut(child, exclude);
@ -366,7 +374,7 @@ export class Gridview implements IDisposable {
} }
hideAllViewsBut(this.root, node); hideAllViewsBut(this.root, node);
this._maximizedNode = node; this._maximizedNode = { leaf: node, hiddenOnMaximize };
this._onDidMaxmizedNodeChange.fire(); this._onDidMaxmizedNodeChange.fire();
} }
@ -375,11 +383,15 @@ export class Gridview implements IDisposable {
return; return;
} }
const hiddenOnMaximize = this._maximizedNode.hiddenOnMaximize;
function showViewsInReverseOrder(parent: BranchNode): void { function showViewsInReverseOrder(parent: BranchNode): void {
for (let index = parent.children.length - 1; index >= 0; index--) { for (let index = parent.children.length - 1; index >= 0; index--) {
const child = parent.children[index]; const child = parent.children[index];
if (child instanceof LeafNode) { if (child instanceof LeafNode) {
if (!hiddenOnMaximize.includes(child)) {
parent.setChildVisible(index, true); parent.setChildVisible(index, true);
}
} else { } else {
showViewsInReverseOrder(child); showViewsInReverseOrder(child);
} }
@ -395,8 +407,8 @@ export class Gridview implements IDisposable {
public serialize(): SerializedGridview<any> { public serialize(): SerializedGridview<any> {
if (this.hasMaximizedView()) { if (this.hasMaximizedView()) {
/** /**
* do not persist maximized view state but we must first exit any maximized views * do not persist maximized view state
* before serialization to ensure the correct dimensions are persisted * firstly exit any maximized views to ensure the correct dimensions are persisted
*/ */
this.exitMaximizedView(); this.exitMaximizedView();
} }

View File

@ -16,6 +16,7 @@ import {
import { LayoutPriority } from '../splitview/splitview'; import { LayoutPriority } from '../splitview/splitview';
import { Emitter, Event } from '../events'; import { Emitter, Event } from '../events';
import { IViewSize } from './gridview'; import { IViewSize } from './gridview';
import { BaseGrid, IGridPanelView } from './baseComponentGridview';
export interface GridviewInitParameters extends PanelInitParameters { export interface GridviewInitParameters extends PanelInitParameters {
minimumWidth?: number; minimumWidth?: number;
@ -24,7 +25,7 @@ export interface GridviewInitParameters extends PanelInitParameters {
maximumHeight?: number; maximumHeight?: number;
priority?: LayoutPriority; priority?: LayoutPriority;
snap?: boolean; snap?: boolean;
accessor: GridviewComponent; accessor: BaseGrid<IGridPanelView>;
isVisible?: boolean; isVisible?: boolean;
} }
@ -157,14 +158,16 @@ export abstract class GridviewPanel<
this.api.initialize(this); // TODO: required to by-pass 'super before this' requirement this.api.initialize(this); // TODO: required to by-pass 'super before this' requirement
this.addDisposables( this.addDisposables(
this.api.onVisibilityChange((event) => { this.api.onDidHiddenChange((event) => {
const { isVisible } = event; const { isHidden } = event;
const { accessor } = this._params as GridviewInitParameters; const { accessor } = this._params as GridviewInitParameters;
accessor.setVisible(this, isVisible);
accessor.setVisible(this, !isHidden);
}), }),
this.api.onActiveChange(() => { this.api.onActiveChange(() => {
const { accessor } = this._params as GridviewInitParameters; const { accessor } = this._params as GridviewInitParameters;
accessor.setActive(this);
accessor.doSetGroupActive(this);
}), }),
this.api.onDidConstraintsChangeInternal((event) => { this.api.onDidConstraintsChangeInternal((event) => {
if ( if (

View File

@ -11,8 +11,6 @@ export {
CompositeDisposable as DockviewCompositeDisposable, CompositeDisposable as DockviewCompositeDisposable,
} from './lifecycle'; } from './lifecycle';
export { PopoutWindow } from './popoutWindow';
export * from './panel/types'; export * from './panel/types';
export * from './panel/componentFactory'; export * from './panel/componentFactory';

View File

@ -24,10 +24,10 @@ export namespace Disposable {
} }
export class CompositeDisposable { export class CompositeDisposable {
private readonly _disposables: IDisposable[]; private _disposables: IDisposable[];
private _isDisposed = false; private _isDisposed = false;
protected get isDisposed(): boolean { get isDisposed(): boolean {
return this._isDisposed; return this._isDisposed;
} }
@ -40,9 +40,13 @@ export class CompositeDisposable {
} }
public dispose(): void { public dispose(): void {
this._disposables.forEach((arg) => arg.dispose()); if (this._isDisposed) {
return;
}
this._isDisposed = true; this._isDisposed = true;
this._disposables.forEach((arg) => arg.dispose());
this._disposables = [];
} }
} }

View File

@ -94,7 +94,6 @@ export class PopoutWindow extends CompositeDisposable {
return null; return null;
} }
const disposable = new CompositeDisposable(); const disposable = new CompositeDisposable();
this._window = { value: externalWindow, disposable }; this._window = { value: externalWindow, disposable };
@ -108,17 +107,14 @@ export class PopoutWindow extends CompositeDisposable {
* @see https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event * @see https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event
*/ */
this.close(); this.close();
}),
addDisposableWindowListener(externalWindow, 'beforeunload', () => {
/**
* @see https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event
*/
this.close();
}) })
); );
const container = this.createPopoutWindowContainer(); const container = this.createPopoutWindowContainer();
if (this.className) {
container.classList.add(this.className); container.classList.add(this.className);
}
this.options.onDidOpen?.({ this.options.onDidOpen?.({
id: this.target, id: this.target,
@ -126,6 +122,11 @@ export class PopoutWindow extends CompositeDisposable {
}); });
return new Promise<HTMLElement | null>((resolve) => { return new Promise<HTMLElement | null>((resolve) => {
externalWindow.addEventListener('unload', (e) => {
// if page fails to load before unloading
// this.close();
});
externalWindow.addEventListener('load', () => { externalWindow.addEventListener('load', () => {
/** /**
* @see https://developer.mozilla.org/en-US/docs/Web/API/Window/load_event * @see https://developer.mozilla.org/en-US/docs/Web/API/Window/load_event
@ -134,12 +135,25 @@ export class PopoutWindow extends CompositeDisposable {
const externalDocument = externalWindow.document; const externalDocument = externalWindow.document;
externalDocument.title = document.title; externalDocument.title = document.title;
// externalDocument.body.replaceChildren(container);
externalDocument.body.appendChild(container); externalDocument.body.appendChild(container);
externalDocument.body.classList.add(this.className);
addStyles(externalDocument, window.document.styleSheets); addStyles(externalDocument, window.document.styleSheets);
/**
* beforeunload must be registered after load for reasons I could not determine
* otherwise the beforeunload event will not fire when the window is closed
*/
addDisposableWindowListener(
externalWindow,
'beforeunload',
() => {
/**
* @see https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event
*/
this.close();
}
);
resolve(container); resolve(container);
}); });
}); });

View File

@ -89,10 +89,10 @@ export abstract class SplitviewPanel
this.addDisposables( this.addDisposables(
this._onDidChange, this._onDidChange,
this.api.onVisibilityChange((event) => { this.api.onDidHiddenChange((event) => {
const { isVisible } = event; const { isHidden } = event;
const { accessor } = this._params as PanelViewInitParameters; const { accessor } = this._params as PanelViewInitParameters;
accessor.setVisible(this, isVisible); accessor.setVisible(this, !isHidden);
}), }),
this.api.onActiveChange(() => { this.api.onActiveChange(() => {
const { accessor } = this._params as PanelViewInitParameters; const { accessor } = this._params as PanelViewInitParameters;

View File

@ -3,6 +3,7 @@ import {
GridviewPanel, GridviewPanel,
GridviewInitParameters, GridviewInitParameters,
IFrameworkPart, IFrameworkPart,
GridviewComponent,
} from 'dockview-core'; } from 'dockview-core';
import { ReactPart, ReactPortalStore } from '../react'; import { ReactPart, ReactPortalStore } from '../react';
import { IGridviewPanelProps } from './gridview'; import { IGridviewPanelProps } from './gridview';
@ -25,8 +26,10 @@ export class ReactGridPanelView extends GridviewPanel {
{ {
params: this._params?.params ?? {}, params: this._params?.params ?? {},
api: this.api, api: this.api,
// TODO: fix casting hack
containerApi: new GridviewApi( containerApi: new GridviewApi(
(this._params as GridviewInitParameters).accessor (this._params as GridviewInitParameters)
.accessor as GridviewComponent
), ),
} }
); );