#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 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.getX() == split);
151 } else {
152 inSplitMove = (mouse.getY() == 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.getX();
203 split = Math.min(Math.max(1, split), getWidth() - 2);
204 } else {
205 split = mouse.getY();
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 // TSplitPane -------------------------------------------------------------
256 // ------------------------------------------------------------------------
257
258 /**
259 * Get the widget on the left side.
260 *
261 * @return the widget on the left, or null if not set
262 */
263 public TWidget getLeft() {
264 return left;
265 }
266
267 /**
268 * Set the widget on the left side.
269 *
270 * @param left the widget to set, or null to remove
271 */
272 public void setLeft(final TWidget left) {
273 if (!vertical) {
274 throw new IllegalArgumentException("cannot set left on " +
275 "horizontal split pane");
276 }
277 if (left == null) {
278 if (this.left != null) {
279 remove(this.left);
280 }
281 this.left = null;
282 return;
283 }
284 this.left = left;
285 left.setParent(this, false);
286 onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(),
287 getHeight()));
288 }
289
290 /**
291 * Get the widget on the right side.
292 *
293 * @return the widget on the right, or null if not set
294 */
295 public TWidget getRight() {
296 return right;
297 }
298
299 /**
300 * Set the widget on the right side.
301 *
302 * @param right the widget to set, or null to remove
303 */
304 public void setRight(final TWidget right) {
305 if (!vertical) {
306 throw new IllegalArgumentException("cannot set right on " +
307 "horizontal split pane");
308 }
309 if (right == null) {
310 if (this.right != null) {
311 remove(this.right);
312 }
313 this.right = null;
314 return;
315 }
316 this.right = right;
317 right.setParent(this, false);
318 onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(),
319 getHeight()));
320 }
321
322 /**
323 * Get the widget on the top side.
324 *
325 * @return the widget on the top, or null if not set
326 */
327 public TWidget getTop() {
328 return top;
329 }
330
331 /**
332 * Set the widget on the top side.
333 *
334 * @param top the widget to set, or null to remove
335 */
336 public void setTop(final TWidget top) {
337 if (vertical) {
338 throw new IllegalArgumentException("cannot set top on vertical " +
339 "split pane");
340 }
341 if (top == null) {
342 if (this.top != null) {
343 remove(this.top);
344 }
345 this.top = null;
346 return;
347 }
348 this.top = top;
349 top.setParent(this, false);
350 onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(),
351 getHeight()));
352 }
353
354 /**
355 * Get the widget on the bottom side.
356 *
357 * @return the widget on the bottom, or null if not set
358 */
359 public TWidget getBottom() {
360 return bottom;
361 }
362
363 /**
364 * Set the widget on the bottom side.
365 *
366 * @param bottom the widget to set, or null to remove
367 */
368 public void setBottom(final TWidget bottom) {
369 if (vertical) {
370 throw new IllegalArgumentException("cannot set bottom on " +
371 "vertical split pane");
372 }
373 if (bottom == null) {
374 if (this.bottom != null) {
375 remove(this.bottom);
376 }
377 this.bottom = null;
378 return;
379 }
380 this.bottom = bottom;
381 bottom.setParent(this, false);
382 onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(),
383 getHeight()));
384 }
385
386 /**
387 * Layout the two child widgets.
388 */
389 private void layoutChildren() {
390 if (vertical) {
391 if (left != null) {
392 left.setDimensions(0, 0, split, getHeight());
393 left.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET,
394 left.getWidth(), left.getHeight()));
395 }
396 if (right != null) {
397 right.setDimensions(split + 1, 0, getWidth() - split - 1,
398 getHeight());
399 right.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET,
400 right.getWidth(), right.getHeight()));
401 }
402 } else {
403 if (top != null) {
404 top.setDimensions(0, 0, getWidth(), split);
405 top.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET,
406 top.getWidth(), top.getHeight()));
407 }
408 if (bottom != null) {
409 bottom.setDimensions(0, split + 1, getWidth(),
410 getHeight() - split - 1);
411 bottom.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET,
412 bottom.getWidth(), bottom.getHeight()));
413 }
414 }
415 }
416
417 /**
418 * Recenter the split to the middle of this split pane.
419 */
420 public void center() {
421 if (vertical) {
422 split = getWidth() / 2;
423 } else {
424 split = getHeight() / 2;
425 }
426 layoutChildren();
427 }
428
429 /**
430 * Remove this split, removing the widget specified.
431 *
432 * @param widgetToRemove the widget to remove
433 * @param doClose if true, call the close() method before removing the
434 * child
435 * @return the pane that remains, or null if nothing is retained
436 */
437 public TWidget removeSplit(final TWidget widgetToRemove,
438 final boolean doClose) {
439
440 TWidget keep = null;
441 if (vertical) {
442 if ((widgetToRemove != left) && (widgetToRemove != right)) {
443 throw new IllegalArgumentException("widget to remove is not " +
444 "either of the panes in this splitpane");
445 }
446 if (widgetToRemove == left) {
447 keep = right;
448 } else {
449 keep = left;
450 }
451
452 } else {
453 if ((widgetToRemove != top) && (widgetToRemove != bottom)) {
454 throw new IllegalArgumentException("widget to remove is not " +
455 "either of the panes in this splitpane");
456 }
457 if (widgetToRemove == top) {
458 keep = bottom;
459 } else {
460 keep = top;
461 }
462 }
463
464 // Remove me from my parent widget.
465 TWidget newParent = getParent();
466 remove(false);
467
468 if (keep == null) {
469 // Nothing is left of either pane. Remove me and bail out.
470 return null;
471 }
472
473 keep.setParent(newParent, false);
474 keep.setDimensions(getX(), getY(), getWidth(), getHeight());
475 keep.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(),
476 getHeight()));
477
478 return keep;
479 }
480
481 }