draw mouse arrows on pane split
[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 remove(this.left);
279 this.left = null;
280 return;
281 }
282 this.left = left;
283 left.setParent(this, false);
284 onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(),
285 getHeight()));
286 }
287
288 /**
289 * Get the widget on the right side.
290 *
291 * @return the widget on the right, or null if not set
292 */
293 public TWidget getRight() {
294 return right;
295 }
296
297 /**
298 * Set the widget on the right side.
299 *
300 * @param right the widget to set, or null to remove
301 */
302 public void setRight(final TWidget right) {
303 if (!vertical) {
304 throw new IllegalArgumentException("cannot set right on " +
305 "horizontal split pane");
306 }
307 if (right == null) {
308 remove(this.right);
309 this.right = null;
310 return;
311 }
312 this.right = right;
313 right.setParent(this, false);
314 onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(),
315 getHeight()));
316 }
317
318 /**
319 * Get the widget on the top side.
320 *
321 * @return the widget on the top, or null if not set
322 */
323 public TWidget getTop() {
324 return top;
325 }
326
327 /**
328 * Set the widget on the top side.
329 *
330 * @param top the widget to set, or null to remove
331 */
332 public void setTop(final TWidget top) {
333 if (vertical) {
334 throw new IllegalArgumentException("cannot set top on vertical " +
335 "split pane");
336 }
337 if (top == null) {
338 remove(this.top);
339 this.top = null;
340 return;
341 }
342 this.top = top;
343 top.setParent(this, false);
344 onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(),
345 getHeight()));
346 }
347
348 /**
349 * Get the widget on the bottom side.
350 *
351 * @return the widget on the bottom, or null if not set
352 */
353 public TWidget getBottom() {
354 return bottom;
355 }
356
357 /**
358 * Set the widget on the bottom side.
359 *
360 * @param bottom the widget to set, or null to remove
361 */
362 public void setBottom(final TWidget bottom) {
363 if (vertical) {
364 throw new IllegalArgumentException("cannot set bottom on " +
365 "vertical split pane");
366 }
367 if (bottom == null) {
368 remove(this.bottom);
369 this.bottom = null;
370 return;
371 }
372 this.bottom = bottom;
373 bottom.setParent(this, false);
374 onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(),
375 getHeight()));
376 }
377
378 /**
379 * Layout the two child widgets.
380 */
381 private void layoutChildren() {
382 if (vertical) {
383 if (left != null) {
384 left.setDimensions(0, 0, split, getHeight());
385 left.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET,
386 left.getWidth(), left.getHeight()));
387 }
388 if (right != null) {
389 right.setDimensions(split + 1, 0, getWidth() - split - 1,
390 getHeight());
391 right.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET,
392 right.getWidth(), right.getHeight()));
393 }
394 } else {
395 if (top != null) {
396 top.setDimensions(0, 0, getWidth(), split);
397 top.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET,
398 top.getWidth(), top.getHeight()));
399 }
400 if (bottom != null) {
401 bottom.setDimensions(0, split + 1, getWidth(),
402 getHeight() - split - 1);
403 bottom.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET,
404 bottom.getWidth(), bottom.getHeight()));
405 }
406 }
407 }
408
409 /**
410 * Recenter the split to the middle of this split pane.
411 */
412 public void center() {
413 if (vertical) {
414 split = getWidth() / 2;
415 } else {
416 split = getHeight() / 2;
417 }
418 layoutChildren();
419 }
420
421 }