@lumino/widgets
Version:
Lumino Widgets
872 lines (802 loc) • 23.5 kB
text/typescript
/* eslint-disable @typescript-eslint/no-empty-function */
// 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 { IDisposable } from '@lumino/disposable';
import { ElementExt } from '@lumino/domutils';
import { Message, MessageLoop } from '@lumino/messaging';
import { AttachedProperty } from '@lumino/properties';
import { Signal } from '@lumino/signaling';
import { Widget } from './widget';
/**
* An abstract base class for creating lumino layouts.
*
* #### Notes
* A layout is used to add widgets to a parent and to arrange those
* widgets within the parent's DOM node.
*
* This class implements the base functionality which is required of
* nearly all layouts. It must be subclassed in order to be useful.
*
* Notably, this class does not define a uniform interface for adding
* widgets to the layout. A subclass should define that API in a way
* which is meaningful for its intended use.
*/
export abstract class Layout implements Iterable<Widget>, IDisposable {
/**
* Construct a new layout.
*
* @param options - The options for initializing the layout.
*/
constructor(options: Layout.IOptions = {}) {
this._fitPolicy = options.fitPolicy || 'set-min-size';
}
/**
* Dispose of the resources held by the layout.
*
* #### Notes
* This should be reimplemented to clear and dispose of the widgets.
*
* All reimplementations should call the superclass method.
*
* This method is called automatically when the parent is disposed.
*/
dispose(): void {
this._parent = null;
this._disposed = true;
Signal.clearData(this);
AttachedProperty.clearData(this);
}
/**
* Test whether the layout is disposed.
*/
get isDisposed(): boolean {
return this._disposed;
}
/**
* Get the parent widget of the layout.
*/
get parent(): Widget | null {
return this._parent;
}
/**
* Set the parent widget of the layout.
*
* #### Notes
* This is set automatically when installing the layout on the parent
* widget. The parent widget should not be set directly by user code.
*/
set parent(value: Widget | null) {
if (this._parent === value) {
return;
}
if (this._parent) {
throw new Error('Cannot change parent widget.');
}
if (value!.layout !== this) {
throw new Error('Invalid parent widget.');
}
this._parent = value;
this.init();
}
/**
* Get the fit policy for the layout.
*
* #### Notes
* The fit policy controls the computed size constraints which are
* applied to the parent widget by the layout.
*
* Some layout implementations may ignore the fit policy.
*/
get fitPolicy(): Layout.FitPolicy {
return this._fitPolicy;
}
/**
* Set the fit policy for the layout.
*
* #### Notes
* The fit policy controls the computed size constraints which are
* applied to the parent widget by the layout.
*
* Some layout implementations may ignore the fit policy.
*
* Changing the fit policy will clear the current size constraint
* for the parent widget and then re-fit the parent.
*/
set fitPolicy(value: Layout.FitPolicy) {
// Bail if the policy does not change
if (this._fitPolicy === value) {
return;
}
// Update the internal policy.
this._fitPolicy = value;
// Clear the size constraints and schedule a fit of the parent.
if (this._parent) {
let style = this._parent.node.style;
style.minWidth = '';
style.minHeight = '';
style.maxWidth = '';
style.maxHeight = '';
this._parent.fit();
}
}
/**
* Create an iterator over the widgets in the layout.
*
* @returns A new iterator over the widgets in the layout.
*
* #### Notes
* This abstract method must be implemented by a subclass.
*/
abstract [Symbol.iterator](): IterableIterator<Widget>;
/**
* Remove a widget from the layout.
*
* @param widget - The widget to remove from the layout.
*
* #### Notes
* A widget is automatically removed from the layout when its `parent`
* is set to `null`. This method should only be invoked directly when
* removing a widget from a layout which has yet to be installed on a
* parent widget.
*
* This method should *not* modify the widget's `parent`.
*/
abstract removeWidget(widget: Widget): void;
/**
* Process a message sent to the parent widget.
*
* @param msg - The message sent to the parent widget.
*
* #### Notes
* This method is called by the parent widget to process a message.
*
* Subclasses may reimplement this method as needed.
*/
processParentMessage(msg: Message): void {
switch (msg.type) {
case 'resize':
this.onResize(msg as Widget.ResizeMessage);
break;
case 'update-request':
this.onUpdateRequest(msg);
break;
case 'fit-request':
this.onFitRequest(msg);
break;
case 'before-show':
this.onBeforeShow(msg);
break;
case 'after-show':
this.onAfterShow(msg);
break;
case 'before-hide':
this.onBeforeHide(msg);
break;
case 'after-hide':
this.onAfterHide(msg);
break;
case 'before-attach':
this.onBeforeAttach(msg);
break;
case 'after-attach':
this.onAfterAttach(msg);
break;
case 'before-detach':
this.onBeforeDetach(msg);
break;
case 'after-detach':
this.onAfterDetach(msg);
break;
case 'child-removed':
this.onChildRemoved(msg as Widget.ChildMessage);
break;
case 'child-shown':
this.onChildShown(msg as Widget.ChildMessage);
break;
case 'child-hidden':
this.onChildHidden(msg as Widget.ChildMessage);
break;
}
}
/**
* Perform layout initialization which requires the parent widget.
*
* #### Notes
* This method is invoked immediately after the layout is installed
* on the parent widget.
*
* The default implementation reparents all of the widgets to the
* layout parent widget.
*
* Subclasses should reimplement this method and attach the child
* widget nodes to the parent widget's node.
*/
protected init(): void {
for (const widget of this) {
widget.parent = this.parent;
}
}
/**
* A message handler invoked on a `'resize'` message.
*
* #### Notes
* The layout should ensure that its widgets are resized according
* to the specified layout space, and that they are sent a `'resize'`
* message if appropriate.
*
* The default implementation of this method sends an `UnknownSize`
* resize message to all widgets.
*
* This may be reimplemented by subclasses as needed.
*/
protected onResize(msg: Widget.ResizeMessage): void {
for (const widget of this) {
MessageLoop.sendMessage(widget, Widget.ResizeMessage.UnknownSize);
}
}
/**
* A message handler invoked on an `'update-request'` message.
*
* #### Notes
* The layout should ensure that its widgets are resized according
* to the available layout space, and that they are sent a `'resize'`
* message if appropriate.
*
* The default implementation of this method sends an `UnknownSize`
* resize message to all widgets.
*
* This may be reimplemented by subclasses as needed.
*/
protected onUpdateRequest(msg: Message): void {
for (const widget of this) {
MessageLoop.sendMessage(widget, Widget.ResizeMessage.UnknownSize);
}
}
/**
* A message handler invoked on a `'before-attach'` message.
*
* #### Notes
* The default implementation of this method forwards the message
* to all widgets. It assumes all widget nodes are attached to the
* parent widget node.
*
* This may be reimplemented by subclasses as needed.
*/
protected onBeforeAttach(msg: Message): void {
for (const widget of this) {
MessageLoop.sendMessage(widget, msg);
}
}
/**
* A message handler invoked on an `'after-attach'` message.
*
* #### Notes
* The default implementation of this method forwards the message
* to all widgets. It assumes all widget nodes are attached to the
* parent widget node.
*
* This may be reimplemented by subclasses as needed.
*/
protected onAfterAttach(msg: Message): void {
for (const widget of this) {
MessageLoop.sendMessage(widget, msg);
}
}
/**
* A message handler invoked on a `'before-detach'` message.
*
* #### Notes
* The default implementation of this method forwards the message
* to all widgets. It assumes all widget nodes are attached to the
* parent widget node.
*
* This may be reimplemented by subclasses as needed.
*/
protected onBeforeDetach(msg: Message): void {
for (const widget of this) {
MessageLoop.sendMessage(widget, msg);
}
}
/**
* A message handler invoked on an `'after-detach'` message.
*
* #### Notes
* The default implementation of this method forwards the message
* to all widgets. It assumes all widget nodes are attached to the
* parent widget node.
*
* This may be reimplemented by subclasses as needed.
*/
protected onAfterDetach(msg: Message): void {
for (const widget of this) {
MessageLoop.sendMessage(widget, msg);
}
}
/**
* A message handler invoked on a `'before-show'` message.
*
* #### Notes
* The default implementation of this method forwards the message to
* all non-hidden widgets. It assumes all widget nodes are attached
* to the parent widget node.
*
* This may be reimplemented by subclasses as needed.
*/
protected onBeforeShow(msg: Message): void {
for (const widget of this) {
if (!widget.isHidden) {
MessageLoop.sendMessage(widget, msg);
}
}
}
/**
* A message handler invoked on an `'after-show'` message.
*
* #### Notes
* The default implementation of this method forwards the message to
* all non-hidden widgets. It assumes all widget nodes are attached
* to the parent widget node.
*
* This may be reimplemented by subclasses as needed.
*/
protected onAfterShow(msg: Message): void {
for (const widget of this) {
if (!widget.isHidden) {
MessageLoop.sendMessage(widget, msg);
}
}
}
/**
* A message handler invoked on a `'before-hide'` message.
*
* #### Notes
* The default implementation of this method forwards the message to
* all non-hidden widgets. It assumes all widget nodes are attached
* to the parent widget node.
*
* This may be reimplemented by subclasses as needed.
*/
protected onBeforeHide(msg: Message): void {
for (const widget of this) {
if (!widget.isHidden) {
MessageLoop.sendMessage(widget, msg);
}
}
}
/**
* A message handler invoked on an `'after-hide'` message.
*
* #### Notes
* The default implementation of this method forwards the message to
* all non-hidden widgets. It assumes all widget nodes are attached
* to the parent widget node.
*
* This may be reimplemented by subclasses as needed.
*/
protected onAfterHide(msg: Message): void {
for (const widget of this) {
if (!widget.isHidden) {
MessageLoop.sendMessage(widget, msg);
}
}
}
/**
* A message handler invoked on a `'child-removed'` message.
*
* #### Notes
* This will remove the child widget from the layout.
*
* Subclasses should **not** typically reimplement this method.
*/
protected onChildRemoved(msg: Widget.ChildMessage): void {
this.removeWidget(msg.child);
}
/**
* A message handler invoked on a `'fit-request'` message.
*
* #### Notes
* The default implementation of this handler is a no-op.
*/
protected onFitRequest(msg: Message): void {}
/**
* A message handler invoked on a `'child-shown'` message.
*
* #### Notes
* The default implementation of this handler is a no-op.
*/
protected onChildShown(msg: Widget.ChildMessage): void {}
/**
* A message handler invoked on a `'child-hidden'` message.
*
* #### Notes
* The default implementation of this handler is a no-op.
*/
protected onChildHidden(msg: Widget.ChildMessage): void {}
private _disposed = false;
private _fitPolicy: Layout.FitPolicy;
private _parent: Widget | null = null;
}
/**
* The namespace for the `Layout` class statics.
*/
export namespace Layout {
/**
* A type alias for the layout fit policy.
*
* #### Notes
* The fit policy controls the computed size constraints which are
* applied to the parent widget by the layout.
*
* Some layout implementations may ignore the fit policy.
*/
export type FitPolicy =
| /**
* No size constraint will be applied to the parent widget.
*/
'set-no-constraint'
/**
* The computed min size will be applied to the parent widget.
*/
| 'set-min-size';
/**
* An options object for initializing a layout.
*/
export interface IOptions {
/**
* The fit policy for the layout.
*
* The default is `'set-min-size'`.
*/
fitPolicy?: FitPolicy;
}
/**
* A type alias for the horizontal alignment of a widget.
*/
export type HorizontalAlignment = 'left' | 'center' | 'right';
/**
* A type alias for the vertical alignment of a widget.
*/
export type VerticalAlignment = 'top' | 'center' | 'bottom';
/**
* Get the horizontal alignment for a widget.
*
* @param widget - The widget of interest.
*
* @returns The horizontal alignment for the widget.
*
* #### Notes
* If the layout width allocated to a widget is larger than its max
* width, the horizontal alignment controls how the widget is placed
* within the extra horizontal space.
*
* If the allocated width is less than the widget's max width, the
* horizontal alignment has no effect.
*
* Some layout implementations may ignore horizontal alignment.
*/
export function getHorizontalAlignment(widget: Widget): HorizontalAlignment {
return Private.horizontalAlignmentProperty.get(widget);
}
/**
* Set the horizontal alignment for a widget.
*
* @param widget - The widget of interest.
*
* @param value - The value for the horizontal alignment.
*
* #### Notes
* If the layout width allocated to a widget is larger than its max
* width, the horizontal alignment controls how the widget is placed
* within the extra horizontal space.
*
* If the allocated width is less than the widget's max width, the
* horizontal alignment has no effect.
*
* Some layout implementations may ignore horizontal alignment.
*
* Changing the horizontal alignment will post an `update-request`
* message to widget's parent, provided the parent has a layout
* installed.
*/
export function setHorizontalAlignment(
widget: Widget,
value: HorizontalAlignment
): void {
Private.horizontalAlignmentProperty.set(widget, value);
}
/**
* Get the vertical alignment for a widget.
*
* @param widget - The widget of interest.
*
* @returns The vertical alignment for the widget.
*
* #### Notes
* If the layout height allocated to a widget is larger than its max
* height, the vertical alignment controls how the widget is placed
* within the extra vertical space.
*
* If the allocated height is less than the widget's max height, the
* vertical alignment has no effect.
*
* Some layout implementations may ignore vertical alignment.
*/
export function getVerticalAlignment(widget: Widget): VerticalAlignment {
return Private.verticalAlignmentProperty.get(widget);
}
/**
* Set the vertical alignment for a widget.
*
* @param widget - The widget of interest.
*
* @param value - The value for the vertical alignment.
*
* #### Notes
* If the layout height allocated to a widget is larger than its max
* height, the vertical alignment controls how the widget is placed
* within the extra vertical space.
*
* If the allocated height is less than the widget's max height, the
* vertical alignment has no effect.
*
* Some layout implementations may ignore vertical alignment.
*
* Changing the horizontal alignment will post an `update-request`
* message to widget's parent, provided the parent has a layout
* installed.
*/
export function setVerticalAlignment(
widget: Widget,
value: VerticalAlignment
): void {
Private.verticalAlignmentProperty.set(widget, value);
}
}
/**
* An object which assists in the absolute layout of widgets.
*
* #### Notes
* This class is useful when implementing a layout which arranges its
* widgets using absolute positioning.
*
* This class is used by nearly all of the built-in lumino layouts.
*/
export class LayoutItem implements IDisposable {
/**
* Construct a new layout item.
*
* @param widget - The widget to be managed by the item.
*
* #### Notes
* The widget will be set to absolute positioning.
* The widget will use strict CSS containment.
*/
constructor(widget: Widget) {
this.widget = widget;
this.widget.node.style.position = 'absolute';
this.widget.node.style.contain = 'strict';
}
/**
* Dispose of the the layout item.
*
* #### Notes
* This will reset the positioning of the widget.
*/
dispose(): void {
// Do nothing if the item is already disposed.
if (this._disposed) {
return;
}
// Mark the item as disposed.
this._disposed = true;
// Reset the widget style.
let style = this.widget.node.style;
style.position = '';
style.top = '';
style.left = '';
style.width = '';
style.height = '';
style.contain = '';
}
/**
* The widget managed by the layout item.
*/
readonly widget: Widget;
/**
* The computed minimum width of the widget.
*
* #### Notes
* This value can be updated by calling the `fit` method.
*/
get minWidth(): number {
return this._minWidth;
}
/**
* The computed minimum height of the widget.
*
* #### Notes
* This value can be updated by calling the `fit` method.
*/
get minHeight(): number {
return this._minHeight;
}
/**
* The computed maximum width of the widget.
*
* #### Notes
* This value can be updated by calling the `fit` method.
*/
get maxWidth(): number {
return this._maxWidth;
}
/**
* The computed maximum height of the widget.
*
* #### Notes
* This value can be updated by calling the `fit` method.
*/
get maxHeight(): number {
return this._maxHeight;
}
/**
* Whether the layout item is disposed.
*/
get isDisposed(): boolean {
return this._disposed;
}
/**
* Whether the managed widget is hidden.
*/
get isHidden(): boolean {
return this.widget.isHidden;
}
/**
* Whether the managed widget is visible.
*/
get isVisible(): boolean {
return this.widget.isVisible;
}
/**
* Whether the managed widget is attached.
*/
get isAttached(): boolean {
return this.widget.isAttached;
}
/**
* Update the computed size limits of the managed widget.
*/
fit(): void {
let limits = ElementExt.sizeLimits(this.widget.node);
this._minWidth = limits.minWidth;
this._minHeight = limits.minHeight;
this._maxWidth = limits.maxWidth;
this._maxHeight = limits.maxHeight;
}
/**
* Update the position and size of the managed widget.
*
* @param left - The left edge position of the layout box.
*
* @param top - The top edge position of the layout box.
*
* @param width - The width of the layout box.
*
* @param height - The height of the layout box.
*/
update(left: number, top: number, width: number, height: number): void {
// Clamp the size to the computed size limits.
let clampW = Math.max(this._minWidth, Math.min(width, this._maxWidth));
let clampH = Math.max(this._minHeight, Math.min(height, this._maxHeight));
// Adjust the left edge for the horizontal alignment, if needed.
if (clampW < width) {
switch (Layout.getHorizontalAlignment(this.widget)) {
case 'left':
break;
case 'center':
left += (width - clampW) / 2;
break;
case 'right':
left += width - clampW;
break;
default:
throw 'unreachable';
}
}
// Adjust the top edge for the vertical alignment, if needed.
if (clampH < height) {
switch (Layout.getVerticalAlignment(this.widget)) {
case 'top':
break;
case 'center':
top += (height - clampH) / 2;
break;
case 'bottom':
top += height - clampH;
break;
default:
throw 'unreachable';
}
}
// Set up the resize variables.
let resized = false;
let style = this.widget.node.style;
// Update the top edge of the widget if needed.
if (this._top !== top) {
this._top = top;
style.top = `${top}px`;
}
// Update the left edge of the widget if needed.
if (this._left !== left) {
this._left = left;
style.left = `${left}px`;
}
// Update the width of the widget if needed.
if (this._width !== clampW) {
resized = true;
this._width = clampW;
style.width = `${clampW}px`;
}
// Update the height of the widget if needed.
if (this._height !== clampH) {
resized = true;
this._height = clampH;
style.height = `${clampH}px`;
}
// Send a resize message to the widget if needed.
if (resized) {
let msg = new Widget.ResizeMessage(clampW, clampH);
MessageLoop.sendMessage(this.widget, msg);
}
}
private _top = NaN;
private _left = NaN;
private _width = NaN;
private _height = NaN;
private _minWidth = 0;
private _minHeight = 0;
private _maxWidth = Infinity;
private _maxHeight = Infinity;
private _disposed = false;
}
/**
* The namespace for the module implementation details.
*/
namespace Private {
/**
* The attached property for a widget horizontal alignment.
*/
export const horizontalAlignmentProperty = new AttachedProperty<
Widget,
Layout.HorizontalAlignment
>({
name: 'horizontalAlignment',
create: () => 'center',
changed: onAlignmentChanged
});
/**
* The attached property for a widget vertical alignment.
*/
export const verticalAlignmentProperty = new AttachedProperty<
Widget,
Layout.VerticalAlignment
>({
name: 'verticalAlignment',
create: () => 'top',
changed: onAlignmentChanged
});
/**
* The change handler for the attached alignment properties.
*/
function onAlignmentChanged(child: Widget): void {
if (child.parent && child.parent.layout) {
child.parent.update();
}
}
}