draggable window
[fanfix.git] / src / jexer / TWidget.java
1 /**
2 * Jexer - Java Text User Interface
3 *
4 * License: LGPLv3 or later
5 *
6 * This module is licensed under the GNU Lesser General Public License
7 * Version 3. Please see the file "COPYING" in this directory for more
8 * information about the GNU Lesser General Public License Version 3.
9 *
10 * Copyright (C) 2015 Kevin Lamonte
11 *
12 * This program is free software; you can redistribute it and/or
13 * modify it under the terms of the GNU Lesser General Public License
14 * as published by the Free Software Foundation; either version 3 of
15 * the License, or (at your option) any later version.
16 *
17 * This program is distributed in the hope that it will be useful, but
18 * WITHOUT ANY WARRANTY; without even the implied warranty of
19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
20 * General Public License for more details.
21 *
22 * You should have received a copy of the GNU Lesser General Public
23 * License along with this program; if not, see
24 * http://www.gnu.org/licenses/, or write to the Free Software
25 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
26 * 02110-1301 USA
27 *
28 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
29 * @version 1
30 */
31 package jexer;
32
33 import java.util.List;
34 import java.util.LinkedList;
35
36 import jexer.event.TCommandEvent;
37 import jexer.event.TInputEvent;
38 import jexer.event.TKeypressEvent;
39 import jexer.event.TMenuEvent;
40 import jexer.event.TMouseEvent;
41 import jexer.event.TResizeEvent;
42 import jexer.io.Screen;
43 import static jexer.TKeypress.*;
44
45 /**
46 * TWidget is the base class of all objects that can be drawn on screen or
47 * handle user input events.
48 */
49 public abstract class TWidget {
50
51 /**
52 * Every widget has a parent widget that it may be "contained" in. For
53 * example, a TWindow might contain several TTextFields, or a TComboBox
54 * may contain a TScrollBar.
55 */
56 protected TWidget parent = null;
57
58 /**
59 * Child widgets that this widget contains.
60 */
61 private List<TWidget> children;
62
63 /**
64 * The currently active child widget that will receive keypress events.
65 */
66 private TWidget activeChild = null;
67
68 /**
69 * If true, this widget will receive events.
70 */
71 protected boolean active = false;
72
73 /**
74 * The window that this widget draws to.
75 */
76 protected TWindow window = null;
77
78 /**
79 * Absolute X position of the top-left corner.
80 */
81 protected int x = 0;
82
83 /**
84 * Absolute Y position of the top-left corner.
85 */
86 protected int y = 0;
87
88 /**
89 * Width.
90 */
91 protected int width = 0;
92
93 /**
94 * Height.
95 */
96 protected int height = 0;
97
98 /**
99 * My tab order inside a window or containing widget.
100 */
101 private int tabOrder = 0;
102
103 /**
104 * If true, this widget can be tabbed to or receive events.
105 */
106 private boolean enabled = true;
107
108 /**
109 * Get enabled flag.
110 *
111 * @return if true, this widget can be tabbed to or receive events
112 */
113 public final boolean getEnabled() {
114 return enabled;
115 }
116
117 /**
118 * Set enabled flag.
119 *
120 * @param enabled if true, this widget can be tabbed to or receive events
121 */
122 public final void setEnabled(final boolean enabled) {
123 this.enabled = enabled;
124 /*
125
126 // TODO: get this working after scrollers are going again
127
128 if (enabled == false) {
129 active = false;
130 // See if there are any active siblings to switch to
131 boolean foundSibling = false;
132 if (parent !is null) {
133 foreach (w; parent.children) {
134 if ((w.enabled) &&
135 (!cast(THScroller)this) &&
136 (!cast(TVScroller)this)
137 ) {
138 parent.activate(w);
139 foundSibling = true;
140 break;
141 }
142 }
143 if (!foundSibling) {
144 parent.activeChild = null;
145 }
146 }
147 }
148 */
149 }
150
151 /**
152 * If true, this widget has a cursor.
153 */
154 private boolean hasCursor = false;
155
156 /**
157 * See if this widget has a visible cursor.
158 *
159 * @return if true, this widget has a visible cursor
160 */
161 public final boolean visibleCursor() {
162 return hasCursor;
163 }
164
165 /**
166 * Cursor column position in relative coordinates.
167 */
168 private int cursorX = 0;
169
170 /**
171 * Cursor row position in relative coordinates.
172 */
173 private int cursorY = 0;
174
175 /**
176 * Comparison operator sorts on tabOrder.
177 *
178 * @param that another TWidget instance
179 * @return difference between this.tabOrder and that.tabOrder
180 */
181 public final int compare(final TWidget that) {
182 return (this.tabOrder - that.tabOrder);
183 }
184
185 /**
186 * See if this widget should render with the active color.
187 *
188 * @return true if this widget is active and all of its parents are
189 * active.
190 */
191 public final boolean getAbsoluteActive() {
192 if (parent == this) {
193 return active;
194 }
195 return (active && parent.getAbsoluteActive());
196 }
197
198 /**
199 * Returns the cursor X position.
200 *
201 * @return absolute screen column number for the cursor's X position
202 */
203 public final int getCursorAbsoluteX() {
204 assert (hasCursor);
205 return getAbsoluteX() + cursorX;
206 }
207
208 /**
209 * Returns the cursor Y position.
210 *
211 * @return absolute screen row number for the cursor's Y position
212 */
213 public final int getCursorAbsoluteY() {
214 assert (hasCursor);
215 return getAbsoluteY() + cursorY;
216 }
217
218 /**
219 * Compute my absolute X position as the sum of my X plus all my parent's
220 * X's.
221 *
222 * @return absolute screen column number for my X position
223 */
224 public final int getAbsoluteX() {
225 assert (parent != null);
226 if (parent == this) {
227 return x;
228 }
229 if ((parent instanceof TWindow) && !(parent instanceof TMenu)) {
230 // Widgets on a TWindow have (0,0) as their top-left, but this is
231 // actually the TWindow's (1,1).
232 return parent.getAbsoluteX() + x + 1;
233 }
234 return parent.getAbsoluteX() + x;
235 }
236
237 /**
238 * Compute my absolute Y position as the sum of my Y plus all my parent's
239 * Y's.
240 *
241 * @return absolute screen row number for my Y position
242 */
243 public final int getAbsoluteY() {
244 assert (parent != null);
245 if (parent == this) {
246 return y;
247 }
248 if ((parent instanceof TWindow) && !(parent instanceof TMenu)) {
249 // Widgets on a TWindow have (0,0) as their top-left, but this is
250 // actually the TWindow's (1,1).
251 return parent.getAbsoluteY() + y + 1;
252 }
253 return parent.getAbsoluteY() + y;
254 }
255
256 /**
257 * Draw my specific widget. When called, the screen rectangle I draw
258 * into is already setup (offset and clipping).
259 */
260 public void draw() {
261 // Default widget draws nothing.
262 }
263
264 /**
265 * Called by parent to render to TWindow.
266 */
267 public final void drawChildren() {
268 // Set my clipping rectangle
269 assert (window != null);
270 assert (window.getScreen() != null);
271 Screen screen = window.getScreen();
272
273 screen.setClipRight(width);
274 screen.setClipBottom(height);
275
276 int absoluteRightEdge = window.getAbsoluteX() + screen.getWidth();
277 int absoluteBottomEdge = window.getAbsoluteY() + screen.getHeight();
278 if (!(this instanceof TWindow) && !(this instanceof TVScroller)) {
279 absoluteRightEdge -= 1;
280 }
281 if (!(this instanceof TWindow) && !(this instanceof THScroller)) {
282 absoluteBottomEdge -= 1;
283 }
284 int myRightEdge = getAbsoluteX() + width;
285 int myBottomEdge = getAbsoluteY() + height;
286 if (getAbsoluteX() > absoluteRightEdge) {
287 // I am offscreen
288 screen.setClipRight(0);
289 } else if (myRightEdge > absoluteRightEdge) {
290 screen.setClipRight(screen.getClipRight()
291 - myRightEdge - absoluteRightEdge);
292 }
293 if (getAbsoluteY() > absoluteBottomEdge) {
294 // I am offscreen
295 screen.setClipBottom(0);
296 } else if (myBottomEdge > absoluteBottomEdge) {
297 screen.setClipBottom(screen.getClipBottom()
298 - myBottomEdge - absoluteBottomEdge);
299 }
300
301 // Set my offset
302 screen.setOffsetX(getAbsoluteX());
303 screen.setOffsetY(getAbsoluteY());
304
305 // Draw me
306 draw();
307
308 // Continue down the chain
309 for (TWidget widget: children) {
310 widget.drawChildren();
311 }
312 }
313
314 /**
315 * Subclasses need this constructor to setup children.
316 */
317 protected TWidget() {
318 children = new LinkedList<TWidget>();
319 }
320
321 /**
322 * Protected constructor.
323 *
324 * @param parent parent widget
325 */
326 protected TWidget(final TWidget parent) {
327 this.parent = parent;
328 this.window = parent.window;
329
330 parent.addChild(this);
331 }
332
333 /**
334 * Add a child widget to my list of children. We set its tabOrder to 0
335 * and increment the tabOrder of all other children.
336 *
337 * @param child TWidget to add
338 */
339 private void addChild(final TWidget child) {
340 children.add(child);
341
342 if ((child.enabled)
343 && !(child instanceof THScroller)
344 && !(child instanceof TVScroller)
345 ) {
346 for (TWidget widget: children) {
347 widget.active = false;
348 }
349 child.active = true;
350 activeChild = child;
351 }
352 for (int i = 0; i < children.size(); i++) {
353 children.get(i).tabOrder = i;
354 }
355 }
356
357 /**
358 * Switch the active child.
359 *
360 * @param child TWidget to activate
361 */
362 public final void activate(final TWidget child) {
363 assert (child.enabled);
364 if ((child instanceof THScroller)
365 || (child instanceof TVScroller)
366 ) {
367 return;
368 }
369
370 if (child != activeChild) {
371 if (activeChild != null) {
372 activeChild.active = false;
373 }
374 child.active = true;
375 activeChild = child;
376 }
377 }
378
379 /**
380 * Switch the active child.
381 *
382 * @param tabOrder tabOrder of the child to activate. If that child
383 * isn't enabled, then the next enabled child will be activated.
384 */
385 public final void activate(final int tabOrder) {
386 if (activeChild == null) {
387 return;
388 }
389 TWidget child = null;
390 for (TWidget widget: children) {
391 if ((widget.enabled)
392 && !(widget instanceof THScroller)
393 && !(widget instanceof TVScroller)
394 && (widget.tabOrder >= tabOrder)
395 ) {
396 child = widget;
397 break;
398 }
399 }
400 if ((child != null) && (child != activeChild)) {
401 activeChild.active = false;
402 assert (child.enabled);
403 child.active = true;
404 activeChild = child;
405 }
406 }
407
408 /**
409 * Switch the active widget with the next in the tab order.
410 *
411 * @param forward if true, then switch to the next enabled widget in the
412 * list, otherwise switch to the previous enabled widget in the list
413 */
414 public final void switchWidget(final boolean forward) {
415
416 // Only switch if there are multiple enabled widgets
417 if ((children.size() < 2) || (activeChild == null)) {
418 return;
419 }
420
421 int tabOrder = activeChild.tabOrder;
422 do {
423 if (forward) {
424 tabOrder++;
425 } else {
426 tabOrder--;
427 }
428 if (tabOrder < 0) {
429
430 // If at the end, pass the switch to my parent.
431 if ((!forward) && (parent != this)) {
432 parent.switchWidget(forward);
433 return;
434 }
435
436 tabOrder = children.size() - 1;
437 } else if (tabOrder == children.size()) {
438 // If at the end, pass the switch to my parent.
439 if ((forward) && (parent != this)) {
440 parent.switchWidget(forward);
441 return;
442 }
443
444 tabOrder = 0;
445 }
446 if (activeChild.tabOrder == tabOrder) {
447 // We wrapped around
448 break;
449 }
450 } while ((!children.get(tabOrder).enabled)
451 && !(children.get(tabOrder) instanceof THScroller)
452 && !(children.get(tabOrder) instanceof TVScroller));
453
454 assert (children.get(tabOrder).enabled);
455
456 activeChild.active = false;
457 children.get(tabOrder).active = true;
458 activeChild = children.get(tabOrder);
459
460 // Refresh
461 window.getApplication().setRepaint();
462 }
463
464 /**
465 * Returns my active widget.
466 *
467 * @return widget that is active, or this if no children
468 */
469 public final TWidget getActiveChild() {
470 if ((this instanceof THScroller)
471 || (this instanceof TVScroller)
472 ) {
473 return parent;
474 }
475
476 for (TWidget widget: children) {
477 if (widget.active) {
478 return widget.getActiveChild();
479 }
480 }
481 // No active children, return me
482 return this;
483 }
484
485 /**
486 * Method that subclasses can override to handle keystrokes.
487 *
488 * @param keypress keystroke event
489 */
490 public void onKeypress(final TKeypressEvent keypress) {
491
492 if ((children.size() == 0)
493 // || (cast(TTreeView)this)
494 // || (cast(TText)this)
495 ) {
496
497 // Defaults:
498 // tab / shift-tab - switch to next/previous widget
499 // right-arrow or down-arrow: same as tab
500 // left-arrow or up-arrow: same as shift-tab
501 if ((keypress.equals(kbTab))
502 || (keypress.equals(kbRight))
503 || (keypress.equals(kbDown))
504 ) {
505 parent.switchWidget(true);
506 return;
507 } else if ((keypress.equals(kbShiftTab))
508 || (keypress.equals(kbBackTab))
509 || (keypress.equals(kbLeft))
510 || (keypress.equals(kbUp))
511 ) {
512 parent.switchWidget(false);
513 return;
514 }
515 }
516
517 // If I have any buttons on me AND this is an Alt-key that matches
518 // its mnemonic, send it an Enter keystroke
519 for (TWidget widget: children) {
520 /*
521 TODO
522
523 if (TButton button = cast(TButton)w) {
524 if (button.enabled &&
525 !keypress.key.isKey &&
526 keypress.key.alt &&
527 !keypress.key.ctrl &&
528 (toLowercase(button.mnemonic.shortcut) == toLowercase(keypress.key.ch))) {
529
530 w.handleEvent(new TKeypressEvent(kbEnter));
531 return;
532 }
533 }
534 */
535 }
536
537 // Dispatch the keypress to an active widget
538 for (TWidget widget: children) {
539 if (widget.active) {
540 window.getApplication().setRepaint();
541 widget.handleEvent(keypress);
542 return;
543 }
544 }
545 }
546
547 /**
548 * Method that subclasses can override to handle mouse button presses.
549 *
550 * @param mouse mouse button event
551 */
552 public void onMouseDown(final TMouseEvent mouse) {
553 // Default: do nothing, pass to children instead
554 for (TWidget widget: children) {
555 if (widget.mouseWouldHit(mouse)) {
556 // Dispatch to this child, also activate it
557 activate(widget);
558
559 // Set x and y relative to the child's coordinates
560 mouse.setX(mouse.getAbsoluteX() - widget.getAbsoluteX());
561 mouse.setY(mouse.getAbsoluteY() - widget.getAbsoluteY());
562 widget.handleEvent(mouse);
563 return;
564 }
565 }
566 }
567
568 /**
569 * Method that subclasses can override to handle mouse button releases.
570 *
571 * @param mouse mouse button event
572 */
573 public void onMouseUp(final TMouseEvent mouse) {
574 // Default: do nothing, pass to children instead
575 for (TWidget widget: children) {
576 if (widget.mouseWouldHit(mouse)) {
577 // Dispatch to this child, also activate it
578 activate(widget);
579
580 // Set x and y relative to the child's coordinates
581 mouse.setX(mouse.getAbsoluteX() - widget.getAbsoluteX());
582 mouse.setY(mouse.getAbsoluteY() - widget.getAbsoluteY());
583 widget.handleEvent(mouse);
584 return;
585 }
586 }
587 }
588
589 /**
590 * Method that subclasses can override to handle mouse movements.
591 *
592 * @param mouse mouse motion event
593 */
594 public void onMouseMotion(final TMouseEvent mouse) {
595 // Default: do nothing, pass it on to ALL of my children. This way
596 // the children can see the mouse "leaving" their area.
597 for (TWidget widget: children) {
598 // Set x and y relative to the child's coordinates
599 mouse.setX(mouse.getAbsoluteX() - widget.getAbsoluteX());
600 mouse.setY(mouse.getAbsoluteY() - widget.getAbsoluteY());
601 widget.handleEvent(mouse);
602 }
603 }
604
605 /**
606 * Method that subclasses can override to handle window/screen resize
607 * events.
608 *
609 * @param resize resize event
610 */
611 public void onResize(final TResizeEvent resize) {
612 // Default: do nothing, pass to children instead
613 for (TWidget widget: children) {
614 widget.onResize(resize);
615 }
616 }
617
618 /**
619 * Method that subclasses can override to handle posted command events.
620 *
621 * @param command command event
622 */
623 public void onCommand(final TCommandEvent command) {
624 // Default: do nothing, pass to children instead
625 for (TWidget widget: children) {
626 widget.onCommand(command);
627 }
628 }
629
630 /**
631 * Method that subclasses can override to handle menu or posted menu
632 * events.
633 *
634 * @param menu menu event
635 */
636 public void onMenu(final TMenuEvent menu) {
637 // Default: do nothing, pass to children instead
638 for (TWidget widget: children) {
639 widget.onMenu(menu);
640 }
641 }
642
643 /**
644 * Method that subclasses can override to do processing when the UI is
645 * idle.
646 */
647 public void onIdle() {
648 // Default: do nothing, pass to children instead
649 for (TWidget widget: children) {
650 widget.onIdle();
651 }
652 }
653
654 /**
655 * Consume event. Subclasses that want to intercept all events in one go
656 * can override this method.
657 *
658 * @param event keyboard, mouse, resize, command, or menu event
659 */
660 public void handleEvent(final TInputEvent event) {
661 // System.err.printf("TWidget (%s) event: %s\n", this.getClass().getName(),
662 // event);
663
664 if (!enabled) {
665 // Discard event
666 // System.err.println(" -- discard --");
667 return;
668 }
669
670 if (event instanceof TKeypressEvent) {
671 onKeypress((TKeypressEvent) event);
672 } else if (event instanceof TMouseEvent) {
673
674 TMouseEvent mouse = (TMouseEvent) event;
675
676 switch (mouse.getType()) {
677
678 case MOUSE_DOWN:
679 onMouseDown(mouse);
680 break;
681
682 case MOUSE_UP:
683 onMouseUp(mouse);
684 break;
685
686 case MOUSE_MOTION:
687 onMouseMotion(mouse);
688 break;
689
690 default:
691 throw new IllegalArgumentException("Invalid mouse event type: "
692 + mouse.getType());
693 }
694 } else if (event instanceof TResizeEvent) {
695 onResize((TResizeEvent) event);
696 } else if (event instanceof TCommandEvent) {
697 onCommand((TCommandEvent) event);
698 } else if (event instanceof TMenuEvent) {
699 onMenu((TMenuEvent) event);
700 }
701
702 // Do nothing else
703 return;
704 }
705
706 /**
707 * Check if a mouse press/release event coordinate is contained in this
708 * widget.
709 *
710 * @param mouse a mouse-based event
711 * @return whether or not a mouse click would be sent to this widget
712 */
713 public final boolean mouseWouldHit(final TMouseEvent mouse) {
714
715 if (!enabled) {
716 return false;
717 }
718
719 if ((mouse.getAbsoluteX() >= getAbsoluteX())
720 && (mouse.getAbsoluteX() < getAbsoluteX() + width)
721 && (mouse.getAbsoluteY() >= getAbsoluteY())
722 && (mouse.getAbsoluteY() < getAbsoluteY() + height)
723 ) {
724 return true;
725 }
726 return false;
727 }
728
729 }