Skip to content

Commit

Permalink
Splits with merge option for dock panels (#582)
Browse files Browse the repository at this point in the history
* dock insert modes allowing reuse of existing splits

* splits and merges in dockpanel example

* revise description of merge modes

* yarn ran api

* rudimentary tests for addWidget()

* ¯\_(ツ)_/¯
  • Loading branch information
tavin authored Jun 13, 2023
1 parent 507f027 commit 3f0be5c
Show file tree
Hide file tree
Showing 4 changed files with 210 additions and 8 deletions.
80 changes: 80 additions & 0 deletions examples/example-dockpanel/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ function createMenu(): Menu {
}

class ContentWidget extends Widget {
static menuFocus: ContentWidget | null;

static createNode(): HTMLElement {
let node = document.createElement('div');
let content = document.createElement('div');
Expand All @@ -82,6 +84,10 @@ class ContentWidget extends Widget {
this.title.label = name;
this.title.closable = true;
this.title.caption = `Long description for: ${name}`;
let widget = this;
this.node.addEventListener('contextmenu', (event: MouseEvent) => {
ContentWidget.menuFocus = widget;
});
}

get inputNode(): HTMLInputElement {
Expand All @@ -93,6 +99,12 @@ class ContentWidget extends Widget {
this.inputNode.focus();
}
}

protected onBeforeDetach(msg: Message): void {
if (ContentWidget.menuFocus === this) {
ContentWidget.menuFocus = null;
}
}
}

function main(): void {
Expand Down Expand Up @@ -322,6 +334,7 @@ function main(): void {
let contextMenu = new ContextMenu({ commands });

document.addEventListener('contextmenu', (event: MouseEvent) => {
if (event.shiftKey) return;
if (contextMenu.open(event)) {
event.preventDefault();
}
Expand Down Expand Up @@ -397,6 +410,56 @@ function main(): void {
sender.addWidget(w, { ref: arg.titles[0].owner });
});

let doSplit = (mode: DockPanel.InsertMode) => {
let ref = ContentWidget.menuFocus;
if (ref) {
let name = ref.title.label;
let widget = new ContentWidget(name);
widget.inputNode.value = `${name} ${mode}`;
dock.addWidget(widget, { mode: mode, ref: ref });
}
};

commands.addCommand('example:split-left', {
label: 'Split left',
execute: () => doSplit('split-left')
});

commands.addCommand('example:split-right', {
label: 'Split right',
execute: () => doSplit('split-right')
});

commands.addCommand('example:split-top', {
label: 'Split top',
execute: () => doSplit('split-top')
});

commands.addCommand('example:split-bottom', {
label: 'Split bottom',
execute: () => doSplit('split-bottom')
});

commands.addCommand('example:merge-left', {
label: 'Merge left',
execute: () => doSplit('merge-left')
});

commands.addCommand('example:merge-right', {
label: 'Merge right',
execute: () => doSplit('merge-right')
});

commands.addCommand('example:merge-top', {
label: 'Merge top',
execute: () => doSplit('merge-top')
});

commands.addCommand('example:merge-bottom', {
label: 'Merge bottom',
execute: () => doSplit('merge-bottom')
});

let savedLayouts: DockPanel.ILayoutConfig[] = [];

commands.addCommand('example:add-button', {
Expand All @@ -408,7 +471,24 @@ function main(): void {
console.log('Toggle add button');
}
});

contextMenu.addItem({ command: 'example:add-button', selector: '.content' });
let contextSub1 = new Menu({ commands });
contextSub1.title.label = 'Splitting';
contextSub1.addItem({ command: 'example:split-left' });
contextSub1.addItem({ command: 'example:split-right' });
contextSub1.addItem({ command: 'example:split-top' });
contextSub1.addItem({ command: 'example:split-bottom' });
contextSub1.addItem({ type: 'separator' });
contextSub1.addItem({ command: 'example:merge-left' });
contextSub1.addItem({ command: 'example:merge-right' });
contextSub1.addItem({ command: 'example:merge-top' });
contextSub1.addItem({ command: 'example:merge-bottom' });
contextMenu.addItem({
type: 'submenu',
submenu: contextSub1,
selector: '.content'
});

commands.addCommand('save-dock-layout', {
label: 'Save Layout',
Expand Down
70 changes: 63 additions & 7 deletions packages/widgets/src/docklayout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,18 @@ export class DockLayout extends Layout {
case 'split-bottom':
this._insertSplit(widget, ref, refNode, 'vertical', true);
break;
case 'merge-top':
this._insertSplit(widget, ref, refNode, 'vertical', false, true);
break;
case 'merge-left':
this._insertSplit(widget, ref, refNode, 'horizontal', false, true);
break;
case 'merge-right':
this._insertSplit(widget, ref, refNode, 'horizontal', true, true);
break;
case 'merge-bottom':
this._insertSplit(widget, ref, refNode, 'vertical', true, true);
break;
}

// Do nothing else if there is no parent widget.
Expand Down Expand Up @@ -791,6 +803,16 @@ export class DockLayout extends Layout {
parentNode.syncHandles();
}

/**
* Create the tab layout node to hold the widget.
*/
private _createTabNode(widget: Widget): Private.TabLayoutNode {
let tabNode = new Private.TabLayoutNode(this._createTabBar());
tabNode.tabBar.addTab(widget.title);
Private.addAria(widget, tabNode.tabBar);
return tabNode;
}

/**
* Insert a widget next to an existing tab.
*
Expand Down Expand Up @@ -872,7 +894,8 @@ export class DockLayout extends Layout {
ref: Widget | null,
refNode: Private.TabLayoutNode | null,
orientation: Private.Orientation,
after: boolean
after: boolean,
merge: boolean = false
): void {
// Do nothing if there is no effective split.
if (widget === ref && refNode && refNode.tabBar.titles.length === 1) {
Expand All @@ -882,14 +905,9 @@ export class DockLayout extends Layout {
// Ensure the widget is removed from the current layout.
this._removeWidget(widget);

// Create the tab layout node to hold the widget.
let tabNode = new Private.TabLayoutNode(this._createTabBar());
tabNode.tabBar.addTab(widget.title);
Private.addAria(widget, tabNode.tabBar);

// Set the root if it does not exist.
if (!this._root) {
this._root = tabNode;
this._root = this._createTabNode(widget);
return;
}

Expand All @@ -908,6 +926,7 @@ export class DockLayout extends Layout {
let sizer = Private.createSizer(refNode ? 1 : Private.GOLDEN_RATIO);

// Insert the tab node sized to the golden ratio.
let tabNode = this._createTabNode(widget);
ArrayExt.insert(root.children, i, tabNode);
ArrayExt.insert(root.sizers, i, sizer);
ArrayExt.insert(root.handles, i, this._createHandle());
Expand All @@ -930,6 +949,17 @@ export class DockLayout extends Layout {
// Find the index of the ref node.
let i = splitNode.children.indexOf(refNode);

// Conditionally reuse a tab layout found in the wanted position.
if (merge) {
let j = i + (after ? 1 : -1);
let sibling = splitNode.children[j];
if (sibling instanceof Private.TabLayoutNode) {
this._insertTab(widget, null, sibling, true);
++sibling.tabBar.currentIndex;
return;
}
}

// Normalize the split node.
splitNode.normalizeSizes();

Expand All @@ -938,6 +968,7 @@ export class DockLayout extends Layout {

// Insert the tab node sized to the other half.
let j = i + (after ? 1 : 0);
let tabNode = this._createTabNode(widget);
ArrayExt.insert(splitNode.children, j, tabNode);
ArrayExt.insert(splitNode.sizers, j, Private.createSizer(s));
ArrayExt.insert(splitNode.handles, j, this._createHandle());
Expand All @@ -963,6 +994,7 @@ export class DockLayout extends Layout {

// Add the tab node sized to the other half.
let j = after ? 1 : 0;
let tabNode = this._createTabNode(widget);
ArrayExt.insert(childNode.children, j, tabNode);
ArrayExt.insert(childNode.sizers, j, Private.createSizer(0.5));
ArrayExt.insert(childNode.handles, j, this._createHandle());
Expand Down Expand Up @@ -1244,6 +1276,30 @@ export namespace DockLayout {
*/
| 'split-bottom'

/**
* Like `split-top` but if a tab layout exists above the reference widget,
* it behaves like `tab-after` with reference to that instead.
*/
| 'merge-top'

/**
* Like `split-left` but if a tab layout exists left of the reference widget,
* it behaves like `tab-after` with reference to that instead.
*/
| 'merge-left'

/**
* Like `split-right` but if a tab layout exists right of the reference widget,
* it behaves like `tab-after` with reference to that instead.
*/
| 'merge-right'

/**
* Like `split-bottom` but if a tab layout exists below the reference widget,
* it behaves like `tab-after` with reference to that instead.
*/
| 'merge-bottom'

/**
* The tab position before the reference widget.
*
Expand Down
48 changes: 47 additions & 1 deletion packages/widgets/tests/src/docklayout.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,53 @@ describe('@lumino/widgets', () => {
it.skip('should have some tests');
});
describe('#addWidget()', () => {
it.skip('should have some tests');
type Mode = DockLayout.InsertMode;
it('should add widgets', () => {
const layout = new DockLayout({ renderer });
const widget = new Widget();
layout.addWidget(widget);
expect(layout['_root'].tabBar).to.exist;
expect(layout['_root'].tabBar.titles[0].owner).to.equal(widget);
});
it('should add splits', () => {
for (let i = 0, d = ['right', 'left', 'bottom', 'top']; i < 4; ++i) {
const layout = new DockLayout({ renderer });
const w1 = new Widget();
const w2 = new Widget();
layout.addWidget(w1);
layout.addWidget(w2, { ref: w1, mode: <Mode>`split-${d[i]}` });
expect(layout['_root'].children).to.exist;
expect(layout['_root'].children.length).to.equal(2);
expect(
layout['_root'].children[0 + (i % 2)].tabBar.titles[0].owner
).to.equal(w1);
expect(
layout['_root'].children[1 - (i % 2)].tabBar.titles[0].owner
).to.equal(w2);
}
});
it('should merge splits', () => {
for (let i = 0, d = ['right', 'left', 'bottom', 'top']; i < 4; ++i) {
const layout = new DockLayout({ renderer });
const w1 = new Widget();
const w2 = new Widget();
const w3 = new Widget();
layout.addWidget(w1);
layout.addWidget(w2, { ref: w1, mode: <Mode>`merge-${d[i]}` });
layout.addWidget(w3, { ref: w1, mode: <Mode>`merge-${d[i]}` });
expect(layout['_root'].children).to.exist;
expect(layout['_root'].children.length).to.equal(2);
expect(
layout['_root'].children[0 + (i % 2)].tabBar.titles[0].owner
).to.equal(w1);
expect(
layout['_root'].children[1 - (i % 2)].tabBar.titles[0].owner
).to.equal(w2);
expect(
layout['_root'].children[1 - (i % 2)].tabBar.titles[1].owner
).to.equal(w3);
}
});
});
describe('#removeWidget()', () => {
it.skip('should have some tests');
Expand Down
20 changes: 20 additions & 0 deletions review/api/widgets.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,26 @@ export namespace DockLayout {
*/
| 'split-bottom'
/**
* Like `split-top` but if a tab layout exists above the reference widget,
* it behaves like `tab-after` with reference to that instead.
*/
| 'merge-top'
/**
* Like `split-left` but if a tab layout exists left of the reference widget,
* it behaves like `tab-after` with reference to that instead.
*/
| 'merge-left'
/**
* Like `split-right` but if a tab layout exists right of the reference widget,
* it behaves like `tab-after` with reference to that instead.
*/
| 'merge-right'
/**
* Like `split-bottom` but if a tab layout exists below the reference widget,
* it behaves like `tab-after` with reference to that instead.
*/
| 'merge-bottom'
/**
* The tab position before the reference widget.
*
* The widget will be added as a tab before the reference widget.
Expand Down

0 comments on commit 3f0be5c

Please sign in to comment.