Merge branch 'subtree'
[fanfix.git] / src / jexer / TSplitPane.java
1 /*
2 * Jexer - Java Text User Interface
3 *
4 * The MIT License (MIT)
5 *
6 * Copyright (C) 2019 Kevin Lamonte
7 *
8 * Permission is hereby granted, free of charge, to any person obtaining a
9 * copy of this software and associated documentation files (the "Software"),
10 * to deal in the Software without restriction, including without limitation
11 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
12 * and/or sell copies of the Software, and to permit persons to whom the
13 * Software is furnished to do so, subject to the following conditions:
14 *
15 * The above copyright notice and this permission notice shall be included in
16 * all copies or substantial portions of the Software.
17 *
18 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
21 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
24 * DEALINGS IN THE SOFTWARE.
25 *
26 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
27 * @version 1
28 */
29 package jexer;
30
31 import jexer.bits.CellAttributes;
32 import jexer.bits.GraphicsChars;
33 import jexer.event.TMouseEvent;
34 import jexer.event.TResizeEvent;
35
36 /**
37 * TSplitPane contains two widgets with a draggable horizontal or vertical
38 * bar between them.
39 */
40 public class TSplitPane extends TWidget {
41
42 // ------------------------------------------------------------------------
43 // Variables --------------------------------------------------------------
44 // ------------------------------------------------------------------------
45
46 /**
47 * If true, split vertically. If false, split horizontally.
48 */
49 private boolean vertical = true;
50
51 /**
52 * The location of the split bar, either as a column number for vertical
53 * split or a row number for horizontal split.
54 */
55 private int split = 0;
56
57 /**
58 * The widget on the left side.
59 */
60 private TWidget left;
61
62 /**
63 * The widget on the right side.
64 */
65 private TWidget right;
66
67 /**
68 * The widget on the top side.
69 */
70 private TWidget top;
71
72 /**
73 * The widget on the bottom side.
74 */
75 private TWidget bottom;
76
77 /**
78 * If true, we are in the middle of a split move.
79 */
80 private boolean inSplitMove = false;
81
82 /**
83 * The last seen mouse position.
84 */
85 private TMouseEvent mouse;
86
87 // ------------------------------------------------------------------------
88 // Constructors -----------------------------------------------------------
89 // ------------------------------------------------------------------------
90
91 /**
92 * Public constructor.
93 *
94 * @param parent parent widget
95 * @param x column relative to parent
96 * @param y row relative to parent
97 * @param width width of widget
98 * @param height height of widget
99 * @param vertical if true, split vertically
100 */
101 public TSplitPane(final TWidget parent, final int x, final int y,
102 final int width, final int height, final boolean vertical) {
103
104 super(parent, x, y, width, height);
105
106 this.vertical = vertical;
107 center();
108 }
109
110 // ------------------------------------------------------------------------
111 // Event handlers ---------------------------------------------------------
112 // ------------------------------------------------------------------------
113
114 /**
115 * Handle window/screen resize events.
116 *
117 * @param event resize event
118 */
119 @Override
120 public void onResize(final TResizeEvent event) {
121 if (event.getType() == TResizeEvent.Type.WIDGET) {
122 // Resize me
123 super.onResize(event);
124
125 if (vertical && (split >= getWidth() - 2)) {
126 center();
127 } else if (!vertical && (split >= getHeight() - 2)) {
128 center();
129 } else {
130 layoutChildren();
131 }
132 }
133 }
134
135 /**
136 * Handle mouse button presses.
137 *
138 * @param mouse mouse button event
139 */
140 @Override
141 public void onMouseDown(final TMouseEvent mouse) {
142 this.mouse = mouse;
143
144 inSplitMove = false;
145
146 if (mouse.isMouse1()) {
147 if (vertical) {
148 inSplitMove = (mouse.getAbsoluteX() - getAbsoluteX() == split);
149 } else {
150 inSplitMove = (mouse.getAbsoluteY() - getAbsoluteY() == split);
151 }
152 if (inSplitMove) {
153 return;
154 }
155 }
156
157 // I didn't take it, pass it on to my children
158 super.onMouseDown(mouse);
159 }
160
161 /**
162 * Handle mouse button releases.
163 *
164 * @param mouse mouse button release event
165 */
166 @Override
167 public void onMouseUp(final TMouseEvent mouse) {
168 this.mouse = mouse;
169
170 if (inSplitMove && mouse.isMouse1()) {
171 // Stop moving split
172 inSplitMove = false;
173 return;
174 }
175
176 // I didn't take it, pass it on to my children
177 super.onMouseUp(mouse);
178 }
179
180 /**
181 * Handle mouse movements.
182 *
183 * @param mouse mouse motion event
184 */
185 @Override
186 public void onMouseMotion(final TMouseEvent mouse) {
187 this.mouse = mouse;
188
189 if ((mouse.getAbsoluteX() - getAbsoluteX() < 0)
190 || (mouse.getAbsoluteX() - getAbsoluteX() >= getWidth())
191 || (mouse.getAbsoluteY() - getAbsoluteY() < 0)
192 || (mouse.getAbsoluteY() - getAbsoluteY() >= getHeight())
193 ) {
194 // Mouse has travelled out of my window.
195 inSplitMove = false;
196 }
197
198 if (inSplitMove) {
199 if (vertical) {
200 split = mouse.getAbsoluteX() - getAbsoluteX();
201 split = Math.min(Math.max(1, split), getWidth() - 2);
202 } else {
203 split = mouse.getAbsoluteY() - getAbsoluteY();
204 split = Math.min(Math.max(1, split), getHeight() - 2);
205 }
206 layoutChildren();
207 return;
208 }
209
210 // I didn't take it, pass it on to my children
211 super.onMouseMotion(mouse);
212 }
213
214 // ------------------------------------------------------------------------
215 // TWidget ----------------------------------------------------------------
216 // ------------------------------------------------------------------------
217
218 /**
219 * Draw me on screen.
220 */
221 @Override
222 public void draw() {
223 CellAttributes attr = getTheme().getColor("tsplitpane");
224 if (vertical) {
225 vLineXY(split, 0, getHeight(), GraphicsChars.WINDOW_SIDE, attr);
226
227 // Draw intersections of children
228 if ((left instanceof TSplitPane)
229 && (((TSplitPane) left).vertical == false)
230 && (right instanceof TSplitPane)
231 && (((TSplitPane) right).vertical == false)
232 && (((TSplitPane) left).split == ((TSplitPane) right).split)
233 ) {
234 putCharXY(split, ((TSplitPane) left).split, '\u253C', attr);
235 } else {
236 if ((left instanceof TSplitPane)
237 && (((TSplitPane) left).vertical == false)
238 ) {
239 putCharXY(split, ((TSplitPane) left).split, '\u2524', attr);
240 }
241 if ((right instanceof TSplitPane)
242 && (((TSplitPane) right).vertical == false)
243 ) {
244 putCharXY(split, ((TSplitPane) right).split, '\u251C',
245 attr);
246 }
247 }
248
249 if ((mouse != null)
250 && (mouse.getAbsoluteX() == getAbsoluteX() + split)
251 && (mouse.getAbsoluteY() >= getAbsoluteY()) &&
252 (mouse.getAbsoluteY() < getAbsoluteY() + getHeight())
253 ) {
254 putCharXY(split, mouse.getAbsoluteY() - getAbsoluteY(),
255 '\u2194', attr);
256 }
257 } else {
258 hLineXY(0, split, getWidth(), GraphicsChars.SINGLE_BAR, attr);
259
260 // Draw intersections of children
261 if ((top instanceof TSplitPane)
262 && (((TSplitPane) top).vertical == true)
263 && (bottom instanceof TSplitPane)
264 && (((TSplitPane) bottom).vertical == true)
265 && (((TSplitPane) top).split == ((TSplitPane) bottom).split)
266 ) {
267 putCharXY(((TSplitPane) top).split, split, '\u253C', attr);
268 } else {
269 if ((top instanceof TSplitPane)
270 && (((TSplitPane) top).vertical == true)
271 ) {
272 putCharXY(((TSplitPane) top).split, split, '\u2534', attr);
273 }
274 if ((bottom instanceof TSplitPane)
275 && (((TSplitPane) bottom).vertical == true)
276 ) {
277 putCharXY(((TSplitPane) bottom).split, split, '\u252C',
278 attr);
279 }
280 }
281
282 if ((mouse != null)
283 && (mouse.getAbsoluteY() == getAbsoluteY() + split)
284 && (mouse.getAbsoluteX() >= getAbsoluteX()) &&
285 (mouse.getAbsoluteX() < getAbsoluteX() + getWidth())
286 ) {
287 putCharXY(mouse.getAbsoluteX() - getAbsoluteX(), split,
288 '\u2195', attr);
289 }
290 }
291
292 }
293
294 /**
295 * Generate a human-readable string for this widget.
296 *
297 * @return a human-readable string
298 */
299 @Override
300 public String toString() {
301 return String.format("%s(%8x) %s position (%d, %d) geometry %dx%d " +
302 "split %d left %s(%8x) right %s(%8x) top %s(%8x) bottom %s(%8x) " +
303 "active %s enabled %s visible %s", getClass().getName(),
304 hashCode(), (vertical ? "VERTICAL" : "HORIZONTAL"),
305 getX(), getY(), getWidth(), getHeight(), split,
306 (left == null ? "null" : left.getClass().getName()),
307 (left == null ? 0 : left.hashCode()),
308 (right == null ? "null" : right.getClass().getName()),
309 (right == null ? 0 : right.hashCode()),
310 (top == null ? "null" : top.getClass().getName()),
311 (top == null ? 0 : top.hashCode()),
312 (bottom == null ? "null" : bottom.getClass().getName()),
313 (bottom == null ? 0 : bottom.hashCode()),
314 isActive(), isEnabled(), isVisible());
315 }
316
317 // ------------------------------------------------------------------------
318 // TSplitPane -------------------------------------------------------------
319 // ------------------------------------------------------------------------
320
321 /**
322 * Get the widget on the left side.
323 *
324 * @return the widget on the left, or null if not set
325 */
326 public TWidget getLeft() {
327 return left;
328 }
329
330 /**
331 * Set the widget on the left side.
332 *
333 * @param left the widget to set, or null to remove
334 */
335 public void setLeft(final TWidget left) {
336 if (!vertical) {
337 throw new IllegalArgumentException("cannot set left on " +
338 "horizontal split pane");
339 }
340 if (left == null) {
341 if (this.left != null) {
342 remove(this.left);
343 }
344 this.left = null;
345 return;
346 }
347 this.left = left;
348 left.setParent(this, false);
349 onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(),
350 getHeight()));
351 }
352
353 /**
354 * Get the widget on the right side.
355 *
356 * @return the widget on the right, or null if not set
357 */
358 public TWidget getRight() {
359 return right;
360 }
361
362 /**
363 * Set the widget on the right side.
364 *
365 * @param right the widget to set, or null to remove
366 */
367 public void setRight(final TWidget right) {
368 if (!vertical) {
369 throw new IllegalArgumentException("cannot set right on " +
370 "horizontal split pane");
371 }
372 if (right == null) {
373 if (this.right != null) {
374 remove(this.right);
375 }
376 this.right = null;
377 return;
378 }
379 this.right = right;
380 right.setParent(this, false);
381 onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(),
382 getHeight()));
383 }
384
385 /**
386 * Get the widget on the top side.
387 *
388 * @return the widget on the top, or null if not set
389 */
390 public TWidget getTop() {
391 return top;
392 }
393
394 /**
395 * Set the widget on the top side.
396 *
397 * @param top the widget to set, or null to remove
398 */
399 public void setTop(final TWidget top) {
400 if (vertical) {
401 throw new IllegalArgumentException("cannot set top on vertical " +
402 "split pane");
403 }
404 if (top == null) {
405 if (this.top != null) {
406 remove(this.top);
407 }
408 this.top = null;
409 return;
410 }
411 this.top = top;
412 top.setParent(this, false);
413 onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(),
414 getHeight()));
415 }
416
417 /**
418 * Get the widget on the bottom side.
419 *
420 * @return the widget on the bottom, or null if not set
421 */
422 public TWidget getBottom() {
423 return bottom;
424 }
425
426 /**
427 * Set the widget on the bottom side.
428 *
429 * @param bottom the widget to set, or null to remove
430 */
431 public void setBottom(final TWidget bottom) {
432 if (vertical) {
433 throw new IllegalArgumentException("cannot set bottom on " +
434 "vertical split pane");
435 }
436 if (bottom == null) {
437 if (this.bottom != null) {
438 remove(this.bottom);
439 }
440 this.bottom = null;
441 return;
442 }
443 this.bottom = bottom;
444 bottom.setParent(this, false);
445 onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(),
446 getHeight()));
447 }
448
449 /**
450 * Remove a widget, regardless of what pane it is on.
451 *
452 * @param widget the widget to remove
453 */
454 public void removeWidget(final TWidget widget) {
455 if (widget == null) {
456 throw new IllegalArgumentException("cannot remove null widget");
457 }
458 if (left == widget) {
459 left = null;
460 assert(right != widget);
461 assert(top != widget);
462 assert(bottom != widget);
463 return;
464 }
465 if (right == widget) {
466 right = null;
467 assert(left != widget);
468 assert(top != widget);
469 assert(bottom != widget);
470 return;
471 }
472 if (top == widget) {
473 top = null;
474 assert(left != widget);
475 assert(right != widget);
476 assert(bottom != widget);
477 return;
478 }
479 if (bottom == widget) {
480 bottom = null;
481 assert(left != widget);
482 assert(right != widget);
483 assert(top != widget);
484 return;
485 }
486 throw new IllegalArgumentException("widget " + widget +
487 " not in this split");
488 }
489
490 /**
491 * Replace a widget, regardless of what pane it is on, with another
492 * widget.
493 *
494 * @param oldWidget the widget to remove
495 * @param newWidget the widget to replace it with
496 */
497 public void replaceWidget(final TWidget oldWidget,
498 final TWidget newWidget) {
499
500 if (oldWidget == null) {
501 throw new IllegalArgumentException("cannot remove null oldWidget");
502 }
503 if (left == oldWidget) {
504 setLeft(newWidget);
505 assert(right != newWidget);
506 assert(top != newWidget);
507 assert(bottom != newWidget);
508 return;
509 }
510 if (right == oldWidget) {
511 setRight(newWidget);
512 assert(left != newWidget);
513 assert(top != newWidget);
514 assert(bottom != newWidget);
515 return;
516 }
517 if (top == oldWidget) {
518 setTop(newWidget);
519 assert(left != newWidget);
520 assert(right != newWidget);
521 assert(bottom != newWidget);
522 return;
523 }
524 if (bottom == oldWidget) {
525 setBottom(newWidget);
526 assert(left != newWidget);
527 assert(right != newWidget);
528 assert(top != newWidget);
529 return;
530 }
531 throw new IllegalArgumentException("oldWidget " + oldWidget +
532 " not in this split");
533 }
534
535 /**
536 * Layout the two child widgets.
537 */
538 private void layoutChildren() {
539 if (vertical) {
540 if (left != null) {
541 left.setDimensions(0, 0, split, getHeight());
542 left.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET,
543 left.getWidth(), left.getHeight()));
544 }
545 if (right != null) {
546 right.setDimensions(split + 1, 0, getWidth() - split - 1,
547 getHeight());
548 right.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET,
549 right.getWidth(), right.getHeight()));
550 }
551 } else {
552 if (top != null) {
553 top.setDimensions(0, 0, getWidth(), split);
554 top.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET,
555 top.getWidth(), top.getHeight()));
556 }
557 if (bottom != null) {
558 bottom.setDimensions(0, split + 1, getWidth(),
559 getHeight() - split - 1);
560 bottom.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET,
561 bottom.getWidth(), bottom.getHeight()));
562 }
563 }
564 }
565
566 /**
567 * Recenter the split to the middle of this split pane.
568 */
569 public void center() {
570 if (vertical) {
571 split = getWidth() / 2;
572 } else {
573 split = getHeight() / 2;
574 }
575 layoutChildren();
576 }
577
578 /**
579 * Remove this split, removing the widget specified.
580 *
581 * @param widgetToRemove the widget to remove
582 * @param doClose if true, call the close() method before removing the
583 * child
584 * @return the pane that remains, or null if nothing is retained
585 */
586 public TWidget removeSplit(final TWidget widgetToRemove,
587 final boolean doClose) {
588
589 TWidget keep = null;
590 if (vertical) {
591 if ((widgetToRemove != left) && (widgetToRemove != right)) {
592 throw new IllegalArgumentException("widget to remove is not " +
593 "either of the panes in this splitpane");
594 }
595 if (widgetToRemove == left) {
596 keep = right;
597 } else {
598 keep = left;
599 }
600
601 } else {
602 if ((widgetToRemove != top) && (widgetToRemove != bottom)) {
603 throw new IllegalArgumentException("widget to remove is not " +
604 "either of the panes in this splitpane");
605 }
606 if (widgetToRemove == top) {
607 keep = bottom;
608 } else {
609 keep = top;
610 }
611 }
612
613 // Remove me from my parent widget.
614 TWidget myParent = getParent();
615 remove(false);
616
617 if (keep == null) {
618 if (myParent instanceof TSplitPane) {
619 // TSplitPane has a left/right/top/bottom link to me
620 // somewhere, remove it.
621 ((TSplitPane) myParent).removeWidget(this);
622 }
623
624 // Nothing is left of either pane. Remove me and bail out.
625 return null;
626 }
627
628 if (myParent instanceof TSplitPane) {
629 // TSplitPane has a left/right/top/bottom link to me
630 // somewhere, replace me with keep.
631 ((TSplitPane) myParent).replaceWidget(this, keep);
632 } else {
633 keep.setParent(myParent, false);
634 keep.setDimensions(getX(), getY(), getWidth(), getHeight());
635 keep.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(),
636 getHeight()));
637 }
638
639 return keep;
640 }
641
642 }