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