@lumino/widgets
Version:
Lumino Widgets
388 lines (330 loc) • 10.7 kB
text/typescript
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
import { ArrayExt } from '@lumino/algorithm';
import { ElementExt } from '@lumino/domutils';
import { Message, MessageLoop } from '@lumino/messaging';
import { Layout, LayoutItem } from './layout';
import { PanelLayout } from './panellayout';
import { Widget } from './widget';
/**
* A layout where visible widgets are stacked atop one another.
*
* #### Notes
* The Z-order of the visible widgets follows their layout order.
*/
export class StackedLayout extends PanelLayout {
constructor(options: StackedLayout.IOptions = {}) {
super(options);
this._hiddenMode =
options.hiddenMode !== undefined
? options.hiddenMode
: Widget.HiddenMode.Display;
}
/**
* The method for hiding widgets.
*
* #### Notes
* If there is only one child widget, `Display` hiding mode will be used
* regardless of this setting.
*/
get hiddenMode(): Widget.HiddenMode {
return this._hiddenMode;
}
/**
* Set the method for hiding widgets.
*
* #### Notes
* If there is only one child widget, `Display` hiding mode will be used
* regardless of this setting.
*/
set hiddenMode(v: Widget.HiddenMode) {
if (this._hiddenMode === v) {
return;
}
this._hiddenMode = v;
if (this.widgets.length > 1) {
this.widgets.forEach(w => {
w.hiddenMode = this._hiddenMode;
});
}
}
/**
* Dispose of the resources held by the layout.
*/
dispose(): void {
// Dispose of the layout items.
for (const item of this._items) {
item.dispose();
}
// Clear the layout state.
this._box = null;
this._items.length = 0;
// Dispose of the rest of the layout.
super.dispose();
}
/**
* Attach a widget to the parent's DOM node.
*
* @param index - The current index of the widget in the layout.
*
* @param widget - The widget to attach to the parent.
*
* #### Notes
* This is a reimplementation of the superclass method.
*/
protected attachWidget(index: number, widget: Widget): void {
// Using transform create an additional layer in the pixel pipeline
// to limit the number of layer, it is set only if there is more than one widget.
if (
this._hiddenMode === Widget.HiddenMode.Scale &&
this._items.length > 0
) {
if (this._items.length === 1) {
this.widgets[0].hiddenMode = Widget.HiddenMode.Scale;
}
widget.hiddenMode = Widget.HiddenMode.Scale;
} else {
widget.hiddenMode = Widget.HiddenMode.Display;
}
// Create and add a new layout item for the widget.
ArrayExt.insert(this._items, index, new LayoutItem(widget));
// Send a `'before-attach'` message if the parent is attached.
if (this.parent!.isAttached) {
MessageLoop.sendMessage(widget, Widget.Msg.BeforeAttach);
}
// Add the widget's node to the parent.
this.parent!.node.appendChild(widget.node);
// Send an `'after-attach'` message if the parent is attached.
if (this.parent!.isAttached) {
MessageLoop.sendMessage(widget, Widget.Msg.AfterAttach);
}
// Post a fit request for the parent widget.
this.parent!.fit();
}
/**
* Move a widget in the parent's DOM node.
*
* @param fromIndex - The previous index of the widget in the layout.
*
* @param toIndex - The current index of the widget in the layout.
*
* @param widget - The widget to move in the parent.
*
* #### Notes
* This is a reimplementation of the superclass method.
*/
protected moveWidget(
fromIndex: number,
toIndex: number,
widget: Widget
): void {
// Move the layout item for the widget.
ArrayExt.move(this._items, fromIndex, toIndex);
// Post an update request for the parent widget.
this.parent!.update();
}
/**
* Detach a widget from the parent's DOM node.
*
* @param index - The previous index of the widget in the layout.
*
* @param widget - The widget to detach from the parent.
*
* #### Notes
* This is a reimplementation of the superclass method.
*/
protected detachWidget(index: number, widget: Widget): void {
// Remove the layout item for the widget.
let item = ArrayExt.removeAt(this._items, index);
// Send a `'before-detach'` message if the parent is attached.
if (this.parent!.isAttached) {
MessageLoop.sendMessage(widget, Widget.Msg.BeforeDetach);
}
// Remove the widget's node from the parent.
this.parent!.node.removeChild(widget.node);
// Send an `'after-detach'` message if the parent is attached.
if (this.parent!.isAttached) {
MessageLoop.sendMessage(widget, Widget.Msg.AfterDetach);
}
// Reset the z-index for the widget.
item!.widget.node.style.zIndex = '';
// Reset the hidden mode for the widget.
if (this._hiddenMode === Widget.HiddenMode.Scale) {
widget.hiddenMode = Widget.HiddenMode.Display;
// Reset the hidden mode for the first widget if necessary.
if (this._items.length === 1) {
this._items[0].widget.hiddenMode = Widget.HiddenMode.Display;
}
}
// Dispose of the layout item.
item!.dispose();
// Post a fit request for the parent widget.
this.parent!.fit();
}
/**
* A message handler invoked on a `'before-show'` message.
*/
protected onBeforeShow(msg: Message): void {
super.onBeforeShow(msg);
this.parent!.update();
}
/**
* A message handler invoked on a `'before-attach'` message.
*/
protected onBeforeAttach(msg: Message): void {
super.onBeforeAttach(msg);
this.parent!.fit();
}
/**
* A message handler invoked on a `'child-shown'` message.
*/
protected onChildShown(msg: Widget.ChildMessage): void {
this.parent!.fit();
}
/**
* A message handler invoked on a `'child-hidden'` message.
*/
protected onChildHidden(msg: Widget.ChildMessage): void {
this.parent!.fit();
}
/**
* A message handler invoked on a `'resize'` message.
*/
protected onResize(msg: Widget.ResizeMessage): void {
if (this.parent!.isVisible) {
this._update(msg.width, msg.height);
}
}
/**
* A message handler invoked on an `'update-request'` message.
*/
protected onUpdateRequest(msg: Message): void {
if (this.parent!.isVisible) {
this._update(-1, -1);
}
}
/**
* A message handler invoked on a `'fit-request'` message.
*/
protected onFitRequest(msg: Message): void {
if (this.parent!.isAttached) {
this._fit();
}
}
/**
* Fit the layout to the total size required by the widgets.
*/
private _fit(): void {
// Set up the computed minimum size.
let minW = 0;
let minH = 0;
// Update the computed minimum size.
for (let i = 0, n = this._items.length; i < n; ++i) {
// Fetch the item.
let item = this._items[i];
// Ignore hidden items.
if (item.isHidden) {
continue;
}
// Update the size limits for the item.
item.fit();
// Update the computed minimum size.
minW = Math.max(minW, item.minWidth);
minH = Math.max(minH, item.minHeight);
}
// Update the box sizing and add it to the computed min size.
let box = (this._box = ElementExt.boxSizing(this.parent!.node));
minW += box.horizontalSum;
minH += box.verticalSum;
// Update the parent's min size constraints.
let style = this.parent!.node.style;
style.minWidth = `${minW}px`;
style.minHeight = `${minH}px`;
// Set the dirty flag to ensure only a single update occurs.
this._dirty = true;
// Notify the ancestor that it should fit immediately. This may
// cause a resize of the parent, fulfilling the required update.
if (this.parent!.parent) {
MessageLoop.sendMessage(this.parent!.parent!, Widget.Msg.FitRequest);
}
// If the dirty flag is still set, the parent was not resized.
// Trigger the required update on the parent widget immediately.
if (this._dirty) {
MessageLoop.sendMessage(this.parent!, Widget.Msg.UpdateRequest);
}
}
/**
* Update the layout position and size of the widgets.
*
* The parent offset dimensions should be `-1` if unknown.
*/
private _update(offsetWidth: number, offsetHeight: number): void {
// Clear the dirty flag to indicate the update occurred.
this._dirty = false;
// Compute the visible item count.
let nVisible = 0;
for (let i = 0, n = this._items.length; i < n; ++i) {
nVisible += +!this._items[i].isHidden;
}
// Bail early if there are no visible items to layout.
if (nVisible === 0) {
return;
}
// Measure the parent if the offset dimensions are unknown.
if (offsetWidth < 0) {
offsetWidth = this.parent!.node.offsetWidth;
}
if (offsetHeight < 0) {
offsetHeight = this.parent!.node.offsetHeight;
}
// Ensure the parent box sizing data is computed.
if (!this._box) {
this._box = ElementExt.boxSizing(this.parent!.node);
}
// Compute the actual layout bounds adjusted for border and padding.
let top = this._box.paddingTop;
let left = this._box.paddingLeft;
let width = offsetWidth - this._box.horizontalSum;
let height = offsetHeight - this._box.verticalSum;
// Update the widget stacking order and layout geometry.
for (let i = 0, n = this._items.length; i < n; ++i) {
// Fetch the item.
let item = this._items[i];
// Ignore hidden items.
if (item.isHidden) {
continue;
}
// Set the z-index for the widget.
item.widget.node.style.zIndex = `${i}`;
// Update the item geometry.
item.update(left, top, width, height);
}
}
private _dirty = false;
private _items: LayoutItem[] = [];
private _box: ElementExt.IBoxSizing | null = null;
private _hiddenMode: Widget.HiddenMode;
}
/**
* The namespace for the `StackedLayout` class statics.
*/
export namespace StackedLayout {
/**
* An options object for initializing a stacked layout.
*/
export interface IOptions extends Layout.IOptions {
/**
* The method for hiding widgets.
*
* The default is `Widget.HiddenMode.Display`.
*/
hiddenMode?: Widget.HiddenMode;
}
}