/* 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 { each, IIterable, IIterator } from '@lumino/algorithm'; 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 IIterable, 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 iter(): IIterator; /** * 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 { each(this, widget => { 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 { each(this, widget => { 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 { each(this, widget => { 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 { each(this, widget => { 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 { each(this, widget => { 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 { each(this, widget => { 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 { each(this, widget => { 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 { each(this, widget => { 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 { each(this, widget => { 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 { each(this, widget => { 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 { each(this, widget => { 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. */ constructor(widget: Widget) { this.widget = widget; this.widget.node.style.position = 'absolute'; } /** * 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 = ''; } /** * 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(); } } }