UNPKG

23.4 kBPlain TextView Raw
1/* eslint-disable @typescript-eslint/no-empty-function */
2// Copyright (c) Jupyter Development Team.
3// Distributed under the terms of the Modified BSD License.
4/*-----------------------------------------------------------------------------
5| Copyright (c) 2014-2017, PhosphorJS Contributors
6|
7| Distributed under the terms of the BSD 3-Clause License.
8|
9| The full license is in the file LICENSE, distributed with this software.
10|----------------------------------------------------------------------------*/
11import { each, IIterable, IIterator } from '@lumino/algorithm';
12
13import { IDisposable } from '@lumino/disposable';
14
15import { ElementExt } from '@lumino/domutils';
16
17import { Message, MessageLoop } from '@lumino/messaging';
18
19import { AttachedProperty } from '@lumino/properties';
20
21import { Signal } from '@lumino/signaling';
22
23import { Widget } from './widget';
24
25/**
26 * An abstract base class for creating lumino layouts.
27 *
28 * #### Notes
29 * A layout is used to add widgets to a parent and to arrange those
30 * widgets within the parent's DOM node.
31 *
32 * This class implements the base functionality which is required of
33 * nearly all layouts. It must be subclassed in order to be useful.
34 *
35 * Notably, this class does not define a uniform interface for adding
36 * widgets to the layout. A subclass should define that API in a way
37 * which is meaningful for its intended use.
38 */
39export abstract class Layout implements IIterable<Widget>, IDisposable {
40 /**
41 * Construct a new layout.
42 *
43 * @param options - The options for initializing the layout.
44 */
45 constructor(options: Layout.IOptions = {}) {
46 this._fitPolicy = options.fitPolicy || 'set-min-size';
47 }
48
49 /**
50 * Dispose of the resources held by the layout.
51 *
52 * #### Notes
53 * This should be reimplemented to clear and dispose of the widgets.
54 *
55 * All reimplementations should call the superclass method.
56 *
57 * This method is called automatically when the parent is disposed.
58 */
59 dispose(): void {
60 this._parent = null;
61 this._disposed = true;
62 Signal.clearData(this);
63 AttachedProperty.clearData(this);
64 }
65
66 /**
67 * Test whether the layout is disposed.
68 */
69 get isDisposed(): boolean {
70 return this._disposed;
71 }
72
73 /**
74 * Get the parent widget of the layout.
75 */
76 get parent(): Widget | null {
77 return this._parent;
78 }
79
80 /**
81 * Set the parent widget of the layout.
82 *
83 * #### Notes
84 * This is set automatically when installing the layout on the parent
85 * widget. The parent widget should not be set directly by user code.
86 */
87 set parent(value: Widget | null) {
88 if (this._parent === value) {
89 return;
90 }
91 if (this._parent) {
92 throw new Error('Cannot change parent widget.');
93 }
94 if (value!.layout !== this) {
95 throw new Error('Invalid parent widget.');
96 }
97 this._parent = value;
98 this.init();
99 }
100
101 /**
102 * Get the fit policy for the layout.
103 *
104 * #### Notes
105 * The fit policy controls the computed size constraints which are
106 * applied to the parent widget by the layout.
107 *
108 * Some layout implementations may ignore the fit policy.
109 */
110 get fitPolicy(): Layout.FitPolicy {
111 return this._fitPolicy;
112 }
113
114 /**
115 * Set the fit policy for the layout.
116 *
117 * #### Notes
118 * The fit policy controls the computed size constraints which are
119 * applied to the parent widget by the layout.
120 *
121 * Some layout implementations may ignore the fit policy.
122 *
123 * Changing the fit policy will clear the current size constraint
124 * for the parent widget and then re-fit the parent.
125 */
126 set fitPolicy(value: Layout.FitPolicy) {
127 // Bail if the policy does not change
128 if (this._fitPolicy === value) {
129 return;
130 }
131
132 // Update the internal policy.
133 this._fitPolicy = value;
134
135 // Clear the size constraints and schedule a fit of the parent.
136 if (this._parent) {
137 let style = this._parent.node.style;
138 style.minWidth = '';
139 style.minHeight = '';
140 style.maxWidth = '';
141 style.maxHeight = '';
142 this._parent.fit();
143 }
144 }
145
146 /**
147 * Create an iterator over the widgets in the layout.
148 *
149 * @returns A new iterator over the widgets in the layout.
150 *
151 * #### Notes
152 * This abstract method must be implemented by a subclass.
153 */
154 abstract iter(): IIterator<Widget>;
155
156 /**
157 * Remove a widget from the layout.
158 *
159 * @param widget - The widget to remove from the layout.
160 *
161 * #### Notes
162 * A widget is automatically removed from the layout when its `parent`
163 * is set to `null`. This method should only be invoked directly when
164 * removing a widget from a layout which has yet to be installed on a
165 * parent widget.
166 *
167 * This method should *not* modify the widget's `parent`.
168 */
169 abstract removeWidget(widget: Widget): void;
170
171 /**
172 * Process a message sent to the parent widget.
173 *
174 * @param msg - The message sent to the parent widget.
175 *
176 * #### Notes
177 * This method is called by the parent widget to process a message.
178 *
179 * Subclasses may reimplement this method as needed.
180 */
181 processParentMessage(msg: Message): void {
182 switch (msg.type) {
183 case 'resize':
184 this.onResize(msg as Widget.ResizeMessage);
185 break;
186 case 'update-request':
187 this.onUpdateRequest(msg);
188 break;
189 case 'fit-request':
190 this.onFitRequest(msg);
191 break;
192 case 'before-show':
193 this.onBeforeShow(msg);
194 break;
195 case 'after-show':
196 this.onAfterShow(msg);
197 break;
198 case 'before-hide':
199 this.onBeforeHide(msg);
200 break;
201 case 'after-hide':
202 this.onAfterHide(msg);
203 break;
204 case 'before-attach':
205 this.onBeforeAttach(msg);
206 break;
207 case 'after-attach':
208 this.onAfterAttach(msg);
209 break;
210 case 'before-detach':
211 this.onBeforeDetach(msg);
212 break;
213 case 'after-detach':
214 this.onAfterDetach(msg);
215 break;
216 case 'child-removed':
217 this.onChildRemoved(msg as Widget.ChildMessage);
218 break;
219 case 'child-shown':
220 this.onChildShown(msg as Widget.ChildMessage);
221 break;
222 case 'child-hidden':
223 this.onChildHidden(msg as Widget.ChildMessage);
224 break;
225 }
226 }
227
228 /**
229 * Perform layout initialization which requires the parent widget.
230 *
231 * #### Notes
232 * This method is invoked immediately after the layout is installed
233 * on the parent widget.
234 *
235 * The default implementation reparents all of the widgets to the
236 * layout parent widget.
237 *
238 * Subclasses should reimplement this method and attach the child
239 * widget nodes to the parent widget's node.
240 */
241 protected init(): void {
242 each(this, widget => {
243 widget.parent = this.parent;
244 });
245 }
246
247 /**
248 * A message handler invoked on a `'resize'` message.
249 *
250 * #### Notes
251 * The layout should ensure that its widgets are resized according
252 * to the specified layout space, and that they are sent a `'resize'`
253 * message if appropriate.
254 *
255 * The default implementation of this method sends an `UnknownSize`
256 * resize message to all widgets.
257 *
258 * This may be reimplemented by subclasses as needed.
259 */
260 protected onResize(msg: Widget.ResizeMessage): void {
261 each(this, widget => {
262 MessageLoop.sendMessage(widget, Widget.ResizeMessage.UnknownSize);
263 });
264 }
265
266 /**
267 * A message handler invoked on an `'update-request'` message.
268 *
269 * #### Notes
270 * The layout should ensure that its widgets are resized according
271 * to the available layout space, and that they are sent a `'resize'`
272 * message if appropriate.
273 *
274 * The default implementation of this method sends an `UnknownSize`
275 * resize message to all widgets.
276 *
277 * This may be reimplemented by subclasses as needed.
278 */
279 protected onUpdateRequest(msg: Message): void {
280 each(this, widget => {
281 MessageLoop.sendMessage(widget, Widget.ResizeMessage.UnknownSize);
282 });
283 }
284
285 /**
286 * A message handler invoked on a `'before-attach'` message.
287 *
288 * #### Notes
289 * The default implementation of this method forwards the message
290 * to all widgets. It assumes all widget nodes are attached to the
291 * parent widget node.
292 *
293 * This may be reimplemented by subclasses as needed.
294 */
295 protected onBeforeAttach(msg: Message): void {
296 each(this, widget => {
297 MessageLoop.sendMessage(widget, msg);
298 });
299 }
300
301 /**
302 * A message handler invoked on an `'after-attach'` message.
303 *
304 * #### Notes
305 * The default implementation of this method forwards the message
306 * to all widgets. It assumes all widget nodes are attached to the
307 * parent widget node.
308 *
309 * This may be reimplemented by subclasses as needed.
310 */
311 protected onAfterAttach(msg: Message): void {
312 each(this, widget => {
313 MessageLoop.sendMessage(widget, msg);
314 });
315 }
316
317 /**
318 * A message handler invoked on a `'before-detach'` message.
319 *
320 * #### Notes
321 * The default implementation of this method forwards the message
322 * to all widgets. It assumes all widget nodes are attached to the
323 * parent widget node.
324 *
325 * This may be reimplemented by subclasses as needed.
326 */
327 protected onBeforeDetach(msg: Message): void {
328 each(this, widget => {
329 MessageLoop.sendMessage(widget, msg);
330 });
331 }
332
333 /**
334 * A message handler invoked on an `'after-detach'` message.
335 *
336 * #### Notes
337 * The default implementation of this method forwards the message
338 * to all widgets. It assumes all widget nodes are attached to the
339 * parent widget node.
340 *
341 * This may be reimplemented by subclasses as needed.
342 */
343 protected onAfterDetach(msg: Message): void {
344 each(this, widget => {
345 MessageLoop.sendMessage(widget, msg);
346 });
347 }
348
349 /**
350 * A message handler invoked on a `'before-show'` message.
351 *
352 * #### Notes
353 * The default implementation of this method forwards the message to
354 * all non-hidden widgets. It assumes all widget nodes are attached
355 * to the parent widget node.
356 *
357 * This may be reimplemented by subclasses as needed.
358 */
359 protected onBeforeShow(msg: Message): void {
360 each(this, widget => {
361 if (!widget.isHidden) {
362 MessageLoop.sendMessage(widget, msg);
363 }
364 });
365 }
366
367 /**
368 * A message handler invoked on an `'after-show'` message.
369 *
370 * #### Notes
371 * The default implementation of this method forwards the message to
372 * all non-hidden widgets. It assumes all widget nodes are attached
373 * to the parent widget node.
374 *
375 * This may be reimplemented by subclasses as needed.
376 */
377 protected onAfterShow(msg: Message): void {
378 each(this, widget => {
379 if (!widget.isHidden) {
380 MessageLoop.sendMessage(widget, msg);
381 }
382 });
383 }
384
385 /**
386 * A message handler invoked on a `'before-hide'` message.
387 *
388 * #### Notes
389 * The default implementation of this method forwards the message to
390 * all non-hidden widgets. It assumes all widget nodes are attached
391 * to the parent widget node.
392 *
393 * This may be reimplemented by subclasses as needed.
394 */
395 protected onBeforeHide(msg: Message): void {
396 each(this, widget => {
397 if (!widget.isHidden) {
398 MessageLoop.sendMessage(widget, msg);
399 }
400 });
401 }
402
403 /**
404 * A message handler invoked on an `'after-hide'` message.
405 *
406 * #### Notes
407 * The default implementation of this method forwards the message to
408 * all non-hidden widgets. It assumes all widget nodes are attached
409 * to the parent widget node.
410 *
411 * This may be reimplemented by subclasses as needed.
412 */
413 protected onAfterHide(msg: Message): void {
414 each(this, widget => {
415 if (!widget.isHidden) {
416 MessageLoop.sendMessage(widget, msg);
417 }
418 });
419 }
420
421 /**
422 * A message handler invoked on a `'child-removed'` message.
423 *
424 * #### Notes
425 * This will remove the child widget from the layout.
426 *
427 * Subclasses should **not** typically reimplement this method.
428 */
429 protected onChildRemoved(msg: Widget.ChildMessage): void {
430 this.removeWidget(msg.child);
431 }
432
433 /**
434 * A message handler invoked on a `'fit-request'` message.
435 *
436 * #### Notes
437 * The default implementation of this handler is a no-op.
438 */
439 protected onFitRequest(msg: Message): void {}
440
441 /**
442 * A message handler invoked on a `'child-shown'` message.
443 *
444 * #### Notes
445 * The default implementation of this handler is a no-op.
446 */
447 protected onChildShown(msg: Widget.ChildMessage): void {}
448
449 /**
450 * A message handler invoked on a `'child-hidden'` message.
451 *
452 * #### Notes
453 * The default implementation of this handler is a no-op.
454 */
455 protected onChildHidden(msg: Widget.ChildMessage): void {}
456
457 private _disposed = false;
458 private _fitPolicy: Layout.FitPolicy;
459 private _parent: Widget | null = null;
460}
461
462/**
463 * The namespace for the `Layout` class statics.
464 */
465export namespace Layout {
466 /**
467 * A type alias for the layout fit policy.
468 *
469 * #### Notes
470 * The fit policy controls the computed size constraints which are
471 * applied to the parent widget by the layout.
472 *
473 * Some layout implementations may ignore the fit policy.
474 */
475 export type FitPolicy =
476 | /**
477 * No size constraint will be applied to the parent widget.
478 */
479 'set-no-constraint'
480
481 /**
482 * The computed min size will be applied to the parent widget.
483 */
484 | 'set-min-size';
485
486 /**
487 * An options object for initializing a layout.
488 */
489 export interface IOptions {
490 /**
491 * The fit policy for the layout.
492 *
493 * The default is `'set-min-size'`.
494 */
495 fitPolicy?: FitPolicy;
496 }
497
498 /**
499 * A type alias for the horizontal alignment of a widget.
500 */
501 export type HorizontalAlignment = 'left' | 'center' | 'right';
502
503 /**
504 * A type alias for the vertical alignment of a widget.
505 */
506 export type VerticalAlignment = 'top' | 'center' | 'bottom';
507
508 /**
509 * Get the horizontal alignment for a widget.
510 *
511 * @param widget - The widget of interest.
512 *
513 * @returns The horizontal alignment for the widget.
514 *
515 * #### Notes
516 * If the layout width allocated to a widget is larger than its max
517 * width, the horizontal alignment controls how the widget is placed
518 * within the extra horizontal space.
519 *
520 * If the allocated width is less than the widget's max width, the
521 * horizontal alignment has no effect.
522 *
523 * Some layout implementations may ignore horizontal alignment.
524 */
525 export function getHorizontalAlignment(widget: Widget): HorizontalAlignment {
526 return Private.horizontalAlignmentProperty.get(widget);
527 }
528
529 /**
530 * Set the horizontal alignment for a widget.
531 *
532 * @param widget - The widget of interest.
533 *
534 * @param value - The value for the horizontal alignment.
535 *
536 * #### Notes
537 * If the layout width allocated to a widget is larger than its max
538 * width, the horizontal alignment controls how the widget is placed
539 * within the extra horizontal space.
540 *
541 * If the allocated width is less than the widget's max width, the
542 * horizontal alignment has no effect.
543 *
544 * Some layout implementations may ignore horizontal alignment.
545 *
546 * Changing the horizontal alignment will post an `update-request`
547 * message to widget's parent, provided the parent has a layout
548 * installed.
549 */
550 export function setHorizontalAlignment(
551 widget: Widget,
552 value: HorizontalAlignment
553 ): void {
554 Private.horizontalAlignmentProperty.set(widget, value);
555 }
556
557 /**
558 * Get the vertical alignment for a widget.
559 *
560 * @param widget - The widget of interest.
561 *
562 * @returns The vertical alignment for the widget.
563 *
564 * #### Notes
565 * If the layout height allocated to a widget is larger than its max
566 * height, the vertical alignment controls how the widget is placed
567 * within the extra vertical space.
568 *
569 * If the allocated height is less than the widget's max height, the
570 * vertical alignment has no effect.
571 *
572 * Some layout implementations may ignore vertical alignment.
573 */
574 export function getVerticalAlignment(widget: Widget): VerticalAlignment {
575 return Private.verticalAlignmentProperty.get(widget);
576 }
577
578 /**
579 * Set the vertical alignment for a widget.
580 *
581 * @param widget - The widget of interest.
582 *
583 * @param value - The value for the vertical alignment.
584 *
585 * #### Notes
586 * If the layout height allocated to a widget is larger than its max
587 * height, the vertical alignment controls how the widget is placed
588 * within the extra vertical space.
589 *
590 * If the allocated height is less than the widget's max height, the
591 * vertical alignment has no effect.
592 *
593 * Some layout implementations may ignore vertical alignment.
594 *
595 * Changing the horizontal alignment will post an `update-request`
596 * message to widget's parent, provided the parent has a layout
597 * installed.
598 */
599 export function setVerticalAlignment(
600 widget: Widget,
601 value: VerticalAlignment
602 ): void {
603 Private.verticalAlignmentProperty.set(widget, value);
604 }
605}
606
607/**
608 * An object which assists in the absolute layout of widgets.
609 *
610 * #### Notes
611 * This class is useful when implementing a layout which arranges its
612 * widgets using absolute positioning.
613 *
614 * This class is used by nearly all of the built-in lumino layouts.
615 */
616export class LayoutItem implements IDisposable {
617 /**
618 * Construct a new layout item.
619 *
620 * @param widget - The widget to be managed by the item.
621 *
622 * #### Notes
623 * The widget will be set to absolute positioning.
624 */
625 constructor(widget: Widget) {
626 this.widget = widget;
627 this.widget.node.style.position = 'absolute';
628 }
629
630 /**
631 * Dispose of the the layout item.
632 *
633 * #### Notes
634 * This will reset the positioning of the widget.
635 */
636 dispose(): void {
637 // Do nothing if the item is already disposed.
638 if (this._disposed) {
639 return;
640 }
641
642 // Mark the item as disposed.
643 this._disposed = true;
644
645 // Reset the widget style.
646 let style = this.widget.node.style;
647 style.position = '';
648 style.top = '';
649 style.left = '';
650 style.width = '';
651 style.height = '';
652 }
653
654 /**
655 * The widget managed by the layout item.
656 */
657 readonly widget: Widget;
658
659 /**
660 * The computed minimum width of the widget.
661 *
662 * #### Notes
663 * This value can be updated by calling the `fit` method.
664 */
665 get minWidth(): number {
666 return this._minWidth;
667 }
668
669 /**
670 * The computed minimum height of the widget.
671 *
672 * #### Notes
673 * This value can be updated by calling the `fit` method.
674 */
675 get minHeight(): number {
676 return this._minHeight;
677 }
678
679 /**
680 * The computed maximum width of the widget.
681 *
682 * #### Notes
683 * This value can be updated by calling the `fit` method.
684 */
685 get maxWidth(): number {
686 return this._maxWidth;
687 }
688
689 /**
690 * The computed maximum height of the widget.
691 *
692 * #### Notes
693 * This value can be updated by calling the `fit` method.
694 */
695 get maxHeight(): number {
696 return this._maxHeight;
697 }
698
699 /**
700 * Whether the layout item is disposed.
701 */
702 get isDisposed(): boolean {
703 return this._disposed;
704 }
705
706 /**
707 * Whether the managed widget is hidden.
708 */
709 get isHidden(): boolean {
710 return this.widget.isHidden;
711 }
712
713 /**
714 * Whether the managed widget is visible.
715 */
716 get isVisible(): boolean {
717 return this.widget.isVisible;
718 }
719
720 /**
721 * Whether the managed widget is attached.
722 */
723 get isAttached(): boolean {
724 return this.widget.isAttached;
725 }
726
727 /**
728 * Update the computed size limits of the managed widget.
729 */
730 fit(): void {
731 let limits = ElementExt.sizeLimits(this.widget.node);
732 this._minWidth = limits.minWidth;
733 this._minHeight = limits.minHeight;
734 this._maxWidth = limits.maxWidth;
735 this._maxHeight = limits.maxHeight;
736 }
737
738 /**
739 * Update the position and size of the managed widget.
740 *
741 * @param left - The left edge position of the layout box.
742 *
743 * @param top - The top edge position of the layout box.
744 *
745 * @param width - The width of the layout box.
746 *
747 * @param height - The height of the layout box.
748 */
749 update(left: number, top: number, width: number, height: number): void {
750 // Clamp the size to the computed size limits.
751 let clampW = Math.max(this._minWidth, Math.min(width, this._maxWidth));
752 let clampH = Math.max(this._minHeight, Math.min(height, this._maxHeight));
753
754 // Adjust the left edge for the horizontal alignment, if needed.
755 if (clampW < width) {
756 switch (Layout.getHorizontalAlignment(this.widget)) {
757 case 'left':
758 break;
759 case 'center':
760 left += (width - clampW) / 2;
761 break;
762 case 'right':
763 left += width - clampW;
764 break;
765 default:
766 throw 'unreachable';
767 }
768 }
769
770 // Adjust the top edge for the vertical alignment, if needed.
771 if (clampH < height) {
772 switch (Layout.getVerticalAlignment(this.widget)) {
773 case 'top':
774 break;
775 case 'center':
776 top += (height - clampH) / 2;
777 break;
778 case 'bottom':
779 top += height - clampH;
780 break;
781 default:
782 throw 'unreachable';
783 }
784 }
785
786 // Set up the resize variables.
787 let resized = false;
788 let style = this.widget.node.style;
789
790 // Update the top edge of the widget if needed.
791 if (this._top !== top) {
792 this._top = top;
793 style.top = `${top}px`;
794 }
795
796 // Update the left edge of the widget if needed.
797 if (this._left !== left) {
798 this._left = left;
799 style.left = `${left}px`;
800 }
801
802 // Update the width of the widget if needed.
803 if (this._width !== clampW) {
804 resized = true;
805 this._width = clampW;
806 style.width = `${clampW}px`;
807 }
808
809 // Update the height of the widget if needed.
810 if (this._height !== clampH) {
811 resized = true;
812 this._height = clampH;
813 style.height = `${clampH}px`;
814 }
815
816 // Send a resize message to the widget if needed.
817 if (resized) {
818 let msg = new Widget.ResizeMessage(clampW, clampH);
819 MessageLoop.sendMessage(this.widget, msg);
820 }
821 }
822
823 private _top = NaN;
824 private _left = NaN;
825 private _width = NaN;
826 private _height = NaN;
827 private _minWidth = 0;
828 private _minHeight = 0;
829 private _maxWidth = Infinity;
830 private _maxHeight = Infinity;
831 private _disposed = false;
832}
833
834/**
835 * The namespace for the module implementation details.
836 */
837namespace Private {
838 /**
839 * The attached property for a widget horizontal alignment.
840 */
841 export const horizontalAlignmentProperty = new AttachedProperty<
842 Widget,
843 Layout.HorizontalAlignment
844 >({
845 name: 'horizontalAlignment',
846 create: () => 'center',
847 changed: onAlignmentChanged
848 });
849
850 /**
851 * The attached property for a widget vertical alignment.
852 */
853 export const verticalAlignmentProperty = new AttachedProperty<
854 Widget,
855 Layout.VerticalAlignment
856 >({
857 name: 'verticalAlignment',
858 create: () => 'top',
859 changed: onAlignmentChanged
860 });
861
862 /**
863 * The change handler for the attached alignment properties.
864 */
865 function onAlignmentChanged(child: Widget): void {
866 if (child.parent && child.parent.layout) {
867 child.parent.update();
868 }
869 }
870}