#51 wip
[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 // System.err.println("onResize(): " + toString());
128
129 if (vertical && (split >= getWidth() - 2)) {
130 center();
131 } else if (!vertical && (split >= getHeight() - 2)) {
132 center();
133 } else {
134 layoutChildren();
135 }
136 }
137 }
138
139 /**
140 * Handle mouse button presses.
141 *
142 * @param mouse mouse button event
143 */
144 @Override
145 public void onMouseDown(final TMouseEvent mouse) {
146 this.mouse = mouse;
147
148 inSplitMove = false;
149
150 if (mouse.isMouse1()) {
151 if (vertical) {
152 inSplitMove = (mouse.getAbsoluteX() - getAbsoluteX() == split);
153 } else {
154 inSplitMove = (mouse.getAbsoluteY() - getAbsoluteY() == split);
155 }
156 if (inSplitMove) {
157 return;
158 }
159 }
160
161 // I didn't take it, pass it on to my children
162 super.onMouseDown(mouse);
163 }
164
165 /**
166 * Handle mouse button releases.
167 *
168 * @param mouse mouse button release event
169 */
170 @Override
171 public void onMouseUp(final TMouseEvent mouse) {
172 this.mouse = mouse;
173
174 if (inSplitMove && mouse.isMouse1()) {
175 // DEBUG
176 // System.err.println(toPrettyString());
177
178 // Stop moving split
179 inSplitMove = false;
180 return;
181 }
182
183 // I didn't take it, pass it on to my children
184 super.onMouseUp(mouse);
185 }
186
187 /**
188 * Handle mouse movements.
189 *
190 * @param mouse mouse motion event
191 */
192 @Override
193 public void onMouseMotion(final TMouseEvent mouse) {
194 this.mouse = mouse;
195
196 if ((mouse.getAbsoluteX() - getAbsoluteX() < 0)
197 || (mouse.getAbsoluteX() - getAbsoluteX() >= getWidth())
198 || (mouse.getAbsoluteY() - getAbsoluteY() < 0)
199 || (mouse.getAbsoluteY() - getAbsoluteY() >= getHeight())
200 ) {
201 // Mouse has travelled out of my window.
202 inSplitMove = false;
203 }
204
205 if (inSplitMove) {
206 if (vertical) {
207 split = mouse.getAbsoluteX() - getAbsoluteX();
208 split = Math.min(Math.max(1, split), getWidth() - 2);
209 } else {
210 split = mouse.getAbsoluteY() - getAbsoluteY();
211 split = Math.min(Math.max(1, split), getHeight() - 2);
212 }
213 layoutChildren();
214 return;
215 }
216
217 // I didn't take it, pass it on to my children
218 super.onMouseMotion(mouse);
219 }
220
221 // ------------------------------------------------------------------------
222 // TWidget ----------------------------------------------------------------
223 // ------------------------------------------------------------------------
224
225 /**
226 * Draw me on screen.
227 */
228 @Override
229 public void draw() {
230 CellAttributes attr = getTheme().getColor("tsplitpane");
231 if (vertical) {
232 vLineXY(split, 0, getHeight(), GraphicsChars.WINDOW_SIDE, attr);
233 // TODO: draw intersections of children
234
235 if ((mouse != null)
236 && (mouse.getAbsoluteX() == getAbsoluteX() + split)
237 && (mouse.getAbsoluteY() >= getAbsoluteY()) &&
238 (mouse.getAbsoluteY() < getAbsoluteY() + getHeight())
239 ) {
240 putCharXY(split, mouse.getAbsoluteY() - getAbsoluteY(),
241 '\u2194', attr);
242 }
243 } else {
244 hLineXY(0, split, getWidth(), GraphicsChars.SINGLE_BAR, attr);
245 // TODO: draw intersections of children
246
247 if ((mouse != null)
248 && (mouse.getAbsoluteY() == getAbsoluteY() + split)
249 && (mouse.getAbsoluteX() >= getAbsoluteX()) &&
250 (mouse.getAbsoluteX() < getAbsoluteX() + getWidth())
251 ) {
252 putCharXY(mouse.getAbsoluteX() - getAbsoluteX(), split,
253 '\u2195', attr);
254 }
255 }
256
257 }
258
259 /**
260 * Generate a human-readable string for this widget.
261 *
262 * @return a human-readable string
263 */
264 @Override
265 public String toString() {
266 return String.format("%s(%8x) %s position (%d, %d) geometry %dx%d " +
267 "split %d left %s(%8x) right %s(%8x) top %s(%8x) bottom %s(%8x) " +
268 "active %s enabled %s visible %s", getClass().getName(),
269 hashCode(), (vertical ? "VERTICAL" : "HORIZONTAL"),
270 getX(), getY(), getWidth(), getHeight(), split,
271 (left == null ? "null" : left.getClass().getName()),
272 (left == null ? 0 : left.hashCode()),
273 (right == null ? "null" : right.getClass().getName()),
274 (right == null ? 0 : right.hashCode()),
275 (top == null ? "null" : top.getClass().getName()),
276 (top == null ? 0 : top.hashCode()),
277 (bottom == null ? "null" : bottom.getClass().getName()),
278 (bottom == null ? 0 : bottom.hashCode()),
279 isActive(), isEnabled(), isVisible());
280 }
281
282 // ------------------------------------------------------------------------
283 // TSplitPane -------------------------------------------------------------
284 // ------------------------------------------------------------------------
285
286 /**
287 * Get the widget on the left side.
288 *
289 * @return the widget on the left, or null if not set
290 */
291 public TWidget getLeft() {
292 return left;
293 }
294
295 /**
296 * Set the widget on the left side.
297 *
298 * @param left the widget to set, or null to remove
299 */
300 public void setLeft(final TWidget left) {
301 if (!vertical) {
302 throw new IllegalArgumentException("cannot set left on " +
303 "horizontal split pane");
304 }
305 if (left == null) {
306 if (this.left != null) {
307 remove(this.left);
308 }
309 this.left = null;
310 return;
311 }
312 this.left = left;
313 left.setParent(this, false);
314 onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(),
315 getHeight()));
316 }
317
318 /**
319 * Get the widget on the right side.
320 *
321 * @return the widget on the right, or null if not set
322 */
323 public TWidget getRight() {
324 return right;
325 }
326
327 /**
328 * Set the widget on the right side.
329 *
330 * @param right the widget to set, or null to remove
331 */
332 public void setRight(final TWidget right) {
333 if (!vertical) {
334 throw new IllegalArgumentException("cannot set right on " +
335 "horizontal split pane");
336 }
337 if (right == null) {
338 if (this.right != null) {
339 remove(this.right);
340 }
341 this.right = null;
342 return;
343 }
344 this.right = right;
345 right.setParent(this, false);
346 onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(),
347 getHeight()));
348 }
349
350 /**
351 * Get the widget on the top side.
352 *
353 * @return the widget on the top, or null if not set
354 */
355 public TWidget getTop() {
356 return top;
357 }
358
359 /**
360 * Set the widget on the top side.
361 *
362 * @param top the widget to set, or null to remove
363 */
364 public void setTop(final TWidget top) {
365 if (vertical) {
366 throw new IllegalArgumentException("cannot set top on vertical " +
367 "split pane");
368 }
369 if (top == null) {
370 if (this.top != null) {
371 remove(this.top);
372 }
373 this.top = null;
374 return;
375 }
376 this.top = top;
377 top.setParent(this, false);
378 onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(),
379 getHeight()));
380 }
381
382 /**
383 * Get the widget on the bottom side.
384 *
385 * @return the widget on the bottom, or null if not set
386 */
387 public TWidget getBottom() {
388 return bottom;
389 }
390
391 /**
392 * Set the widget on the bottom side.
393 *
394 * @param bottom the widget to set, or null to remove
395 */
396 public void setBottom(final TWidget bottom) {
397 if (vertical) {
398 throw new IllegalArgumentException("cannot set bottom on " +
399 "vertical split pane");
400 }
401 if (bottom == null) {
402 if (this.bottom != null) {
403 remove(this.bottom);
404 }
405 this.bottom = null;
406 return;
407 }
408 this.bottom = bottom;
409 bottom.setParent(this, false);
410 onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(),
411 getHeight()));
412 }
413
414 /**
415 * Remove a widget, regardless of what pane it is on.
416 *
417 * @param widget the widget to remove
418 */
419 public void removeWidget(final TWidget widget) {
420 if (widget == null) {
421 throw new IllegalArgumentException("cannot remove null widget");
422 }
423 if (left == widget) {
424 left = null;
425 assert(right != widget);
426 assert(top != widget);
427 assert(bottom != widget);
428 return;
429 }
430 if (right == widget) {
431 right = null;
432 assert(left != widget);
433 assert(top != widget);
434 assert(bottom != widget);
435 return;
436 }
437 if (top == widget) {
438 top = null;
439 assert(left != widget);
440 assert(right != widget);
441 assert(bottom != widget);
442 return;
443 }
444 if (bottom == widget) {
445 bottom = null;
446 assert(left != widget);
447 assert(right != widget);
448 assert(top != widget);
449 return;
450 }
451 throw new IllegalArgumentException("widget " + widget +
452 " not in this split");
453 }
454
455 /**
456 * Replace a widget, regardless of what pane it is on, with another
457 * widget.
458 *
459 * @param oldWidget the widget to remove
460 * @param newWidget the widget to replace it with
461 */
462 public void replaceWidget(final TWidget oldWidget,
463 final TWidget newWidget) {
464
465 if (oldWidget == null) {
466 throw new IllegalArgumentException("cannot remove null oldWidget");
467 }
468 if (left == oldWidget) {
469 setLeft(newWidget);
470 assert(right != newWidget);
471 assert(top != newWidget);
472 assert(bottom != newWidget);
473 return;
474 }
475 if (right == oldWidget) {
476 setRight(newWidget);
477 assert(left != newWidget);
478 assert(top != newWidget);
479 assert(bottom != newWidget);
480 return;
481 }
482 if (top == oldWidget) {
483 setTop(newWidget);
484 assert(left != newWidget);
485 assert(right != newWidget);
486 assert(bottom != newWidget);
487 return;
488 }
489 if (bottom == oldWidget) {
490 setBottom(newWidget);
491 assert(left != newWidget);
492 assert(right != newWidget);
493 assert(top != newWidget);
494 return;
495 }
496 throw new IllegalArgumentException("oldWidget " + oldWidget +
497 " not in this split");
498 }
499
500 /**
501 * Layout the two child widgets.
502 */
503 private void layoutChildren() {
504
505 // System.err.println("layoutChildren(): " + toString());
506
507 if (vertical) {
508 if (left != null) {
509 left.setDimensions(0, 0, split, getHeight());
510 left.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET,
511 left.getWidth(), left.getHeight()));
512 // System.err.println(" move/size left: " + left.toString());
513 }
514 if (right != null) {
515 right.setDimensions(split + 1, 0, getWidth() - split - 1,
516 getHeight());
517 right.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET,
518 right.getWidth(), right.getHeight()));
519 // System.err.println(" move/size right: " + right.toString());
520 }
521 } else {
522 if (top != null) {
523 top.setDimensions(0, 0, getWidth(), split);
524 top.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET,
525 top.getWidth(), top.getHeight()));
526 // System.err.println(" move/size top: " + top.toString());
527 }
528 if (bottom != null) {
529 bottom.setDimensions(0, split + 1, getWidth(),
530 getHeight() - split - 1);
531 bottom.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET,
532 bottom.getWidth(), bottom.getHeight()));
533 // System.err.println(" move/size bottom: " + bottom.toString());
534 }
535 }
536 }
537
538 /**
539 * Recenter the split to the middle of this split pane.
540 */
541 public void center() {
542 if (vertical) {
543 split = getWidth() / 2;
544 } else {
545 split = getHeight() / 2;
546 }
547 layoutChildren();
548 }
549
550 /**
551 * Remove this split, removing the widget specified.
552 *
553 * @param widgetToRemove the widget to remove
554 * @param doClose if true, call the close() method before removing the
555 * child
556 * @return the pane that remains, or null if nothing is retained
557 */
558 public TWidget removeSplit(final TWidget widgetToRemove,
559 final boolean doClose) {
560
561 TWidget keep = null;
562 if (vertical) {
563 if ((widgetToRemove != left) && (widgetToRemove != right)) {
564 throw new IllegalArgumentException("widget to remove is not " +
565 "either of the panes in this splitpane");
566 }
567 if (widgetToRemove == left) {
568 keep = right;
569 } else {
570 keep = left;
571 }
572
573 } else {
574 if ((widgetToRemove != top) && (widgetToRemove != bottom)) {
575 throw new IllegalArgumentException("widget to remove is not " +
576 "either of the panes in this splitpane");
577 }
578 if (widgetToRemove == top) {
579 keep = bottom;
580 } else {
581 keep = top;
582 }
583 }
584
585 // Remove me from my parent widget.
586 TWidget myParent = getParent();
587 remove(false);
588
589 if (keep == null) {
590 // Nothing is left of either pane. Remove me and bail out.
591 return null;
592 }
593
594 keep.setParent(myParent, false);
595 keep.setDimensions(getX(), getY(), getWidth(), getHeight());
596 keep.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(),
597 getHeight()));
598
599 return keep;
600 }
601
602 }