1 | // Copyright (c) Jupyter Development Team.
|
2 | // Distributed under the terms of the Modified BSD License.
|
3 | /*-----------------------------------------------------------------------------
|
4 | | Copyright (c) 2014-2017, PhosphorJS Contributors
|
5 | |
|
6 | | Distributed under the terms of the BSD 3-Clause License.
|
7 | |
|
8 | | The full license is in the file LICENSE, distributed with this software.
|
9 | |----------------------------------------------------------------------------*/
|
10 | import { ArrayExt, each } from '@lumino/algorithm';
|
11 |
|
12 | import { ElementExt } from '@lumino/domutils';
|
13 |
|
14 | import { Message, MessageLoop } from '@lumino/messaging';
|
15 |
|
16 | import { Layout, LayoutItem } from './layout';
|
17 |
|
18 | import { PanelLayout } from './panellayout';
|
19 |
|
20 | import { Widget } from './widget';
|
21 |
|
22 | /**
|
23 | * A layout where visible widgets are stacked atop one another.
|
24 | *
|
25 | * #### Notes
|
26 | * The Z-order of the visible widgets follows their layout order.
|
27 | */
|
28 | export class StackedLayout extends PanelLayout {
|
29 | constructor(options: StackedLayout.IOptions = {}) {
|
30 | super(options);
|
31 | this._hiddenMode =
|
32 | options.hiddenMode !== undefined
|
33 | ? options.hiddenMode
|
34 | : Widget.HiddenMode.Display;
|
35 | }
|
36 |
|
37 | /**
|
38 | * The method for hiding widgets.
|
39 | *
|
40 | * #### Notes
|
41 | * If there is only one child widget, `Display` hiding mode will be used
|
42 | * regardless of this setting.
|
43 | */
|
44 | get hiddenMode(): Widget.HiddenMode {
|
45 | return this._hiddenMode;
|
46 | }
|
47 |
|
48 | /**
|
49 | * Set the method for hiding widgets.
|
50 | *
|
51 | * #### Notes
|
52 | * If there is only one child widget, `Display` hiding mode will be used
|
53 | * regardless of this setting.
|
54 | */
|
55 | set hiddenMode(v: Widget.HiddenMode) {
|
56 | if (this._hiddenMode === v) {
|
57 | return;
|
58 | }
|
59 | this._hiddenMode = v;
|
60 | if (this.widgets.length > 1) {
|
61 | this.widgets.forEach(w => {
|
62 | w.hiddenMode = this._hiddenMode;
|
63 | });
|
64 | }
|
65 | }
|
66 |
|
67 | /**
|
68 | * Dispose of the resources held by the layout.
|
69 | */
|
70 | dispose(): void {
|
71 | // Dispose of the layout items.
|
72 | each(this._items, item => {
|
73 | item.dispose();
|
74 | });
|
75 |
|
76 | // Clear the layout state.
|
77 | this._box = null;
|
78 | this._items.length = 0;
|
79 |
|
80 | // Dispose of the rest of the layout.
|
81 | super.dispose();
|
82 | }
|
83 |
|
84 | /**
|
85 | * Attach a widget to the parent's DOM node.
|
86 | *
|
87 | * @param index - The current index of the widget in the layout.
|
88 | *
|
89 | * @param widget - The widget to attach to the parent.
|
90 | *
|
91 | * #### Notes
|
92 | * This is a reimplementation of the superclass method.
|
93 | */
|
94 | protected attachWidget(index: number, widget: Widget): void {
|
95 | // Using transform create an additional layer in the pixel pipeline
|
96 | // to limit the number of layer, it is set only if there is more than one widget.
|
97 | if (
|
98 | this._hiddenMode === Widget.HiddenMode.Scale &&
|
99 | this._items.length > 0
|
100 | ) {
|
101 | if (this._items.length === 1) {
|
102 | this.widgets[0].hiddenMode = Widget.HiddenMode.Scale;
|
103 | }
|
104 | widget.hiddenMode = Widget.HiddenMode.Scale;
|
105 | } else {
|
106 | widget.hiddenMode = Widget.HiddenMode.Display;
|
107 | }
|
108 |
|
109 | // Create and add a new layout item for the widget.
|
110 | ArrayExt.insert(this._items, index, new LayoutItem(widget));
|
111 |
|
112 | // Send a `'before-attach'` message if the parent is attached.
|
113 | if (this.parent!.isAttached) {
|
114 | MessageLoop.sendMessage(widget, Widget.Msg.BeforeAttach);
|
115 | }
|
116 |
|
117 | // Add the widget's node to the parent.
|
118 | this.parent!.node.appendChild(widget.node);
|
119 |
|
120 | // Send an `'after-attach'` message if the parent is attached.
|
121 | if (this.parent!.isAttached) {
|
122 | MessageLoop.sendMessage(widget, Widget.Msg.AfterAttach);
|
123 | }
|
124 |
|
125 | // Post a fit request for the parent widget.
|
126 | this.parent!.fit();
|
127 | }
|
128 |
|
129 | /**
|
130 | * Move a widget in the parent's DOM node.
|
131 | *
|
132 | * @param fromIndex - The previous index of the widget in the layout.
|
133 | *
|
134 | * @param toIndex - The current index of the widget in the layout.
|
135 | *
|
136 | * @param widget - The widget to move in the parent.
|
137 | *
|
138 | * #### Notes
|
139 | * This is a reimplementation of the superclass method.
|
140 | */
|
141 | protected moveWidget(
|
142 | fromIndex: number,
|
143 | toIndex: number,
|
144 | widget: Widget
|
145 | ): void {
|
146 | // Move the layout item for the widget.
|
147 | ArrayExt.move(this._items, fromIndex, toIndex);
|
148 |
|
149 | // Post an update request for the parent widget.
|
150 | this.parent!.update();
|
151 | }
|
152 |
|
153 | /**
|
154 | * Detach a widget from the parent's DOM node.
|
155 | *
|
156 | * @param index - The previous index of the widget in the layout.
|
157 | *
|
158 | * @param widget - The widget to detach from the parent.
|
159 | *
|
160 | * #### Notes
|
161 | * This is a reimplementation of the superclass method.
|
162 | */
|
163 | protected detachWidget(index: number, widget: Widget): void {
|
164 | // Remove the layout item for the widget.
|
165 | let item = ArrayExt.removeAt(this._items, index);
|
166 |
|
167 | // Send a `'before-detach'` message if the parent is attached.
|
168 | if (this.parent!.isAttached) {
|
169 | MessageLoop.sendMessage(widget, Widget.Msg.BeforeDetach);
|
170 | }
|
171 |
|
172 | // Remove the widget's node from the parent.
|
173 | this.parent!.node.removeChild(widget.node);
|
174 |
|
175 | // Send an `'after-detach'` message if the parent is attached.
|
176 | if (this.parent!.isAttached) {
|
177 | MessageLoop.sendMessage(widget, Widget.Msg.AfterDetach);
|
178 | }
|
179 |
|
180 | // Reset the z-index for the widget.
|
181 | item!.widget.node.style.zIndex = '';
|
182 |
|
183 | // Reset the hidden mode for the widget.
|
184 | if (this._hiddenMode === Widget.HiddenMode.Scale) {
|
185 | widget.hiddenMode = Widget.HiddenMode.Display;
|
186 |
|
187 | // Reset the hidden mode for the first widget if necessary.
|
188 | if (this._items.length === 1) {
|
189 | this._items[0].widget.hiddenMode = Widget.HiddenMode.Display;
|
190 | }
|
191 | }
|
192 |
|
193 | // Dispose of the layout item.
|
194 | item!.dispose();
|
195 |
|
196 | // Post a fit request for the parent widget.
|
197 | this.parent!.fit();
|
198 | }
|
199 |
|
200 | /**
|
201 | * A message handler invoked on a `'before-show'` message.
|
202 | */
|
203 | protected onBeforeShow(msg: Message): void {
|
204 | super.onBeforeShow(msg);
|
205 | this.parent!.update();
|
206 | }
|
207 |
|
208 | /**
|
209 | * A message handler invoked on a `'before-attach'` message.
|
210 | */
|
211 | protected onBeforeAttach(msg: Message): void {
|
212 | super.onBeforeAttach(msg);
|
213 | this.parent!.fit();
|
214 | }
|
215 |
|
216 | /**
|
217 | * A message handler invoked on a `'child-shown'` message.
|
218 | */
|
219 | protected onChildShown(msg: Widget.ChildMessage): void {
|
220 | this.parent!.fit();
|
221 | }
|
222 |
|
223 | /**
|
224 | * A message handler invoked on a `'child-hidden'` message.
|
225 | */
|
226 | protected onChildHidden(msg: Widget.ChildMessage): void {
|
227 | this.parent!.fit();
|
228 | }
|
229 |
|
230 | /**
|
231 | * A message handler invoked on a `'resize'` message.
|
232 | */
|
233 | protected onResize(msg: Widget.ResizeMessage): void {
|
234 | if (this.parent!.isVisible) {
|
235 | this._update(msg.width, msg.height);
|
236 | }
|
237 | }
|
238 |
|
239 | /**
|
240 | * A message handler invoked on an `'update-request'` message.
|
241 | */
|
242 | protected onUpdateRequest(msg: Message): void {
|
243 | if (this.parent!.isVisible) {
|
244 | this._update(-1, -1);
|
245 | }
|
246 | }
|
247 |
|
248 | /**
|
249 | * A message handler invoked on a `'fit-request'` message.
|
250 | */
|
251 | protected onFitRequest(msg: Message): void {
|
252 | if (this.parent!.isAttached) {
|
253 | this._fit();
|
254 | }
|
255 | }
|
256 |
|
257 | /**
|
258 | * Fit the layout to the total size required by the widgets.
|
259 | */
|
260 | private _fit(): void {
|
261 | // Set up the computed minimum size.
|
262 | let minW = 0;
|
263 | let minH = 0;
|
264 |
|
265 | // Update the computed minimum size.
|
266 | for (let i = 0, n = this._items.length; i < n; ++i) {
|
267 | // Fetch the item.
|
268 | let item = this._items[i];
|
269 |
|
270 | // Ignore hidden items.
|
271 | if (item.isHidden) {
|
272 | continue;
|
273 | }
|
274 |
|
275 | // Update the size limits for the item.
|
276 | item.fit();
|
277 |
|
278 | // Update the computed minimum size.
|
279 | minW = Math.max(minW, item.minWidth);
|
280 | minH = Math.max(minH, item.minHeight);
|
281 | }
|
282 |
|
283 | // Update the box sizing and add it to the computed min size.
|
284 | let box = (this._box = ElementExt.boxSizing(this.parent!.node));
|
285 | minW += box.horizontalSum;
|
286 | minH += box.verticalSum;
|
287 |
|
288 | // Update the parent's min size constraints.
|
289 | let style = this.parent!.node.style;
|
290 | style.minWidth = `${minW}px`;
|
291 | style.minHeight = `${minH}px`;
|
292 |
|
293 | // Set the dirty flag to ensure only a single update occurs.
|
294 | this._dirty = true;
|
295 |
|
296 | // Notify the ancestor that it should fit immediately. This may
|
297 | // cause a resize of the parent, fulfilling the required update.
|
298 | if (this.parent!.parent) {
|
299 | MessageLoop.sendMessage(this.parent!.parent!, Widget.Msg.FitRequest);
|
300 | }
|
301 |
|
302 | // If the dirty flag is still set, the parent was not resized.
|
303 | // Trigger the required update on the parent widget immediately.
|
304 | if (this._dirty) {
|
305 | MessageLoop.sendMessage(this.parent!, Widget.Msg.UpdateRequest);
|
306 | }
|
307 | }
|
308 |
|
309 | /**
|
310 | * Update the layout position and size of the widgets.
|
311 | *
|
312 | * The parent offset dimensions should be `-1` if unknown.
|
313 | */
|
314 | private _update(offsetWidth: number, offsetHeight: number): void {
|
315 | // Clear the dirty flag to indicate the update occurred.
|
316 | this._dirty = false;
|
317 |
|
318 | // Compute the visible item count.
|
319 | let nVisible = 0;
|
320 | for (let i = 0, n = this._items.length; i < n; ++i) {
|
321 | nVisible += +!this._items[i].isHidden;
|
322 | }
|
323 |
|
324 | // Bail early if there are no visible items to layout.
|
325 | if (nVisible === 0) {
|
326 | return;
|
327 | }
|
328 |
|
329 | // Measure the parent if the offset dimensions are unknown.
|
330 | if (offsetWidth < 0) {
|
331 | offsetWidth = this.parent!.node.offsetWidth;
|
332 | }
|
333 | if (offsetHeight < 0) {
|
334 | offsetHeight = this.parent!.node.offsetHeight;
|
335 | }
|
336 |
|
337 | // Ensure the parent box sizing data is computed.
|
338 | if (!this._box) {
|
339 | this._box = ElementExt.boxSizing(this.parent!.node);
|
340 | }
|
341 |
|
342 | // Compute the actual layout bounds adjusted for border and padding.
|
343 | let top = this._box.paddingTop;
|
344 | let left = this._box.paddingLeft;
|
345 | let width = offsetWidth - this._box.horizontalSum;
|
346 | let height = offsetHeight - this._box.verticalSum;
|
347 |
|
348 | // Update the widget stacking order and layout geometry.
|
349 | for (let i = 0, n = this._items.length; i < n; ++i) {
|
350 | // Fetch the item.
|
351 | let item = this._items[i];
|
352 |
|
353 | // Ignore hidden items.
|
354 | if (item.isHidden) {
|
355 | continue;
|
356 | }
|
357 |
|
358 | // Set the z-index for the widget.
|
359 | item.widget.node.style.zIndex = `${i}`;
|
360 |
|
361 | // Update the item geometry.
|
362 | item.update(left, top, width, height);
|
363 | }
|
364 | }
|
365 |
|
366 | private _dirty = false;
|
367 | private _items: LayoutItem[] = [];
|
368 | private _box: ElementExt.IBoxSizing | null = null;
|
369 | private _hiddenMode: Widget.HiddenMode;
|
370 | }
|
371 |
|
372 | /**
|
373 | * The namespace for the `StackedLayout` class statics.
|
374 | */
|
375 | export namespace StackedLayout {
|
376 | /**
|
377 | * An options object for initializing a stacked layout.
|
378 | */
|
379 | export interface IOptions extends Layout.IOptions {
|
380 | /**
|
381 | * The method for hiding widgets.
|
382 | *
|
383 | * The default is `Widget.HiddenMode.Display`.
|
384 | */
|
385 | hiddenMode?: Widget.HiddenMode;
|
386 | }
|
387 | }
|