Add 'src/jexer/' from commit 'cf01c92f5809a0732409e280fb0f32f27393618d'
[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 // TODO: draw intersections of children
229
230 if ((mouse != null)
231 && (mouse.getAbsoluteX() == getAbsoluteX() + split)
232 && (mouse.getAbsoluteY() >= getAbsoluteY()) &&
233 (mouse.getAbsoluteY() < getAbsoluteY() + getHeight())
234 ) {
235 putCharXY(split, mouse.getAbsoluteY() - getAbsoluteY(),
236 '\u2194', attr);
237 }
238 } else {
239 hLineXY(0, split, getWidth(), GraphicsChars.SINGLE_BAR, attr);
240 // TODO: draw intersections of children
241
242 if ((mouse != null)
243 && (mouse.getAbsoluteY() == getAbsoluteY() + split)
244 && (mouse.getAbsoluteX() >= getAbsoluteX()) &&
245 (mouse.getAbsoluteX() < getAbsoluteX() + getWidth())
246 ) {
247 putCharXY(mouse.getAbsoluteX() - getAbsoluteX(), split,
248 '\u2195', attr);
249 }
250 }
251
252 }
253
254 /**
255 * Generate a human-readable string for this widget.
256 *
257 * @return a human-readable string
258 */
259 @Override
260 public String toString() {
261 return String.format("%s(%8x) %s position (%d, %d) geometry %dx%d " +
262 "split %d left %s(%8x) right %s(%8x) top %s(%8x) bottom %s(%8x) " +
263 "active %s enabled %s visible %s", getClass().getName(),
264 hashCode(), (vertical ? "VERTICAL" : "HORIZONTAL"),
265 getX(), getY(), getWidth(), getHeight(), split,
266 (left == null ? "null" : left.getClass().getName()),
267 (left == null ? 0 : left.hashCode()),
268 (right == null ? "null" : right.getClass().getName()),
269 (right == null ? 0 : right.hashCode()),
270 (top == null ? "null" : top.getClass().getName()),
271 (top == null ? 0 : top.hashCode()),
272 (bottom == null ? "null" : bottom.getClass().getName()),
273 (bottom == null ? 0 : bottom.hashCode()),
274 isActive(), isEnabled(), isVisible());
275 }
276
277 // ------------------------------------------------------------------------
278 // TSplitPane -------------------------------------------------------------
279 // ------------------------------------------------------------------------
280
281 /**
282 * Get the widget on the left side.
283 *
284 * @return the widget on the left, or null if not set
285 */
286 public TWidget getLeft() {
287 return left;
288 }
289
290 /**
291 * Set the widget on the left side.
292 *
293 * @param left the widget to set, or null to remove
294 */
295 public void setLeft(final TWidget left) {
296 if (!vertical) {
297 throw new IllegalArgumentException("cannot set left on " +
298 "horizontal split pane");
299 }
300 if (left == null) {
301 if (this.left != null) {
302 remove(this.left);
303 }
304 this.left = null;
305 return;
306 }
307 this.left = left;
308 left.setParent(this, false);
309 onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(),
310 getHeight()));
311 }
312
313 /**
314 * Get the widget on the right side.
315 *
316 * @return the widget on the right, or null if not set
317 */
318 public TWidget getRight() {
319 return right;
320 }
321
322 /**
323 * Set the widget on the right side.
324 *
325 * @param right the widget to set, or null to remove
326 */
327 public void setRight(final TWidget right) {
328 if (!vertical) {
329 throw new IllegalArgumentException("cannot set right on " +
330 "horizontal split pane");
331 }
332 if (right == null) {
333 if (this.right != null) {
334 remove(this.right);
335 }
336 this.right = null;
337 return;
338 }
339 this.right = right;
340 right.setParent(this, false);
341 onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(),
342 getHeight()));
343 }
344
345 /**
346 * Get the widget on the top side.
347 *
348 * @return the widget on the top, or null if not set
349 */
350 public TWidget getTop() {
351 return top;
352 }
353
354 /**
355 * Set the widget on the top side.
356 *
357 * @param top the widget to set, or null to remove
358 */
359 public void setTop(final TWidget top) {
360 if (vertical) {
361 throw new IllegalArgumentException("cannot set top on vertical " +
362 "split pane");
363 }
364 if (top == null) {
365 if (this.top != null) {
366 remove(this.top);
367 }
368 this.top = null;
369 return;
370 }
371 this.top = top;
372 top.setParent(this, false);
373 onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(),
374 getHeight()));
375 }
376
377 /**
378 * Get the widget on the bottom side.
379 *
380 * @return the widget on the bottom, or null if not set
381 */
382 public TWidget getBottom() {
383 return bottom;
384 }
385
386 /**
387 * Set the widget on the bottom side.
388 *
389 * @param bottom the widget to set, or null to remove
390 */
391 public void setBottom(final TWidget bottom) {
392 if (vertical) {
393 throw new IllegalArgumentException("cannot set bottom on " +
394 "vertical split pane");
395 }
396 if (bottom == null) {
397 if (this.bottom != null) {
398 remove(this.bottom);
399 }
400 this.bottom = null;
401 return;
402 }
403 this.bottom = bottom;
404 bottom.setParent(this, false);
405 onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(),
406 getHeight()));
407 }
408
409 /**
410 * Remove a widget, regardless of what pane it is on.
411 *
412 * @param widget the widget to remove
413 */
414 public void removeWidget(final TWidget widget) {
415 if (widget == null) {
416 throw new IllegalArgumentException("cannot remove null widget");
417 }
418 if (left == widget) {
419 left = null;
420 assert(right != widget);
421 assert(top != widget);
422 assert(bottom != widget);
423 return;
424 }
425 if (right == widget) {
426 right = null;
427 assert(left != widget);
428 assert(top != widget);
429 assert(bottom != widget);
430 return;
431 }
432 if (top == widget) {
433 top = null;
434 assert(left != widget);
435 assert(right != widget);
436 assert(bottom != widget);
437 return;
438 }
439 if (bottom == widget) {
440 bottom = null;
441 assert(left != widget);
442 assert(right != widget);
443 assert(top != widget);
444 return;
445 }
446 throw new IllegalArgumentException("widget " + widget +
447 " not in this split");
448 }
449
450 /**
451 * Replace a widget, regardless of what pane it is on, with another
452 * widget.
453 *
454 * @param oldWidget the widget to remove
455 * @param newWidget the widget to replace it with
456 */
457 public void replaceWidget(final TWidget oldWidget,
458 final TWidget newWidget) {
459
460 if (oldWidget == null) {
461 throw new IllegalArgumentException("cannot remove null oldWidget");
462 }
463 if (left == oldWidget) {
464 setLeft(newWidget);
465 assert(right != newWidget);
466 assert(top != newWidget);
467 assert(bottom != newWidget);
468 return;
469 }
470 if (right == oldWidget) {
471 setRight(newWidget);
472 assert(left != newWidget);
473 assert(top != newWidget);
474 assert(bottom != newWidget);
475 return;
476 }
477 if (top == oldWidget) {
478 setTop(newWidget);
479 assert(left != newWidget);
480 assert(right != newWidget);
481 assert(bottom != newWidget);
482 return;
483 }
484 if (bottom == oldWidget) {
485 setBottom(newWidget);
486 assert(left != newWidget);
487 assert(right != newWidget);
488 assert(top != newWidget);
489 return;
490 }
491 throw new IllegalArgumentException("oldWidget " + oldWidget +
492 " not in this split");
493 }
494
495 /**
496 * Layout the two child widgets.
497 */
498 private void layoutChildren() {
499 if (vertical) {
500 if (left != null) {
501 left.setDimensions(0, 0, split, getHeight());
502 left.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET,
503 left.getWidth(), left.getHeight()));
504 }
505 if (right != null) {
506 right.setDimensions(split + 1, 0, getWidth() - split - 1,
507 getHeight());
508 right.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET,
509 right.getWidth(), right.getHeight()));
510 }
511 } else {
512 if (top != null) {
513 top.setDimensions(0, 0, getWidth(), split);
514 top.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET,
515 top.getWidth(), top.getHeight()));
516 }
517 if (bottom != null) {
518 bottom.setDimensions(0, split + 1, getWidth(),
519 getHeight() - split - 1);
520 bottom.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET,
521 bottom.getWidth(), bottom.getHeight()));
522 }
523 }
524 }
525
526 /**
527 * Recenter the split to the middle of this split pane.
528 */
529 public void center() {
530 if (vertical) {
531 split = getWidth() / 2;
532 } else {
533 split = getHeight() / 2;
534 }
535 layoutChildren();
536 }
537
538 /**
539 * Remove this split, removing the widget specified.
540 *
541 * @param widgetToRemove the widget to remove
542 * @param doClose if true, call the close() method before removing the
543 * child
544 * @return the pane that remains, or null if nothing is retained
545 */
546 public TWidget removeSplit(final TWidget widgetToRemove,
547 final boolean doClose) {
548
549 TWidget keep = null;
550 if (vertical) {
551 if ((widgetToRemove != left) && (widgetToRemove != right)) {
552 throw new IllegalArgumentException("widget to remove is not " +
553 "either of the panes in this splitpane");
554 }
555 if (widgetToRemove == left) {
556 keep = right;
557 } else {
558 keep = left;
559 }
560
561 } else {
562 if ((widgetToRemove != top) && (widgetToRemove != bottom)) {
563 throw new IllegalArgumentException("widget to remove is not " +
564 "either of the panes in this splitpane");
565 }
566 if (widgetToRemove == top) {
567 keep = bottom;
568 } else {
569 keep = top;
570 }
571 }
572
573 // Remove me from my parent widget.
574 TWidget myParent = getParent();
575 remove(false);
576
577 if (keep == null) {
578 if (myParent instanceof TSplitPane) {
579 // TSplitPane has a left/right/top/bottom link to me
580 // somewhere, remove it.
581 ((TSplitPane) myParent).removeWidget(this);
582 }
583
584 // Nothing is left of either pane. Remove me and bail out.
585 return null;
586 }
587
588 if (myParent instanceof TSplitPane) {
589 // TSplitPane has a left/right/top/bottom link to me
590 // somewhere, replace me with keep.
591 ((TSplitPane) myParent).replaceWidget(this, keep);
592 } else {
593 keep.setParent(myParent, false);
594 keep.setDimensions(getX(), getY(), getWidth(), getHeight());
595 keep.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(),
596 getHeight()));
597 }
598
599 return keep;
600 }
601
602 }