Switchable windows
[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 private TWidget parent = null;
57
58 /**
59 * Backdoor access for TWindow's constructor. ONLY TWindow USES THIS.
60 *
61 * @param window the top-level window
62 * @param x column relative to parent
63 * @param y row relative to parent
64 * @param width width of window
65 * @param height height of window
66 */
67 protected final void setupForTWindow(final TWindow window,
68 final int x, final int y, final int width, final int height) {
69
70 this.parent = window;
71 this.window = window;
72 this.x = x;
73 this.y = y;
74 this.width = width;
75 this.height = height;
76 }
77
78 /**
79 * Child widgets that this widget contains.
80 */
81 private List<TWidget> children;
82
83 /**
84 * The currently active child widget that will receive keypress events.
85 */
86 private TWidget activeChild = null;
87
88 /**
89 * If true, this widget will receive events.
90 */
91 private boolean active = false;
92
93 /**
94 * Get active flag.
95 *
96 * @return if true, this widget will receive events
97 */
98 public final boolean getActive() {
99 return active;
100 }
101
102 /**
103 * Set active flag.
104 *
105 * @param active if true, this widget will receive events
106 */
107 public final void setActive(boolean active) {
108 this.active = active;
109 }
110
111 /**
112 * The window that this widget draws to.
113 */
114 private TWindow window = null;
115
116 /**
117 * Absolute X position of the top-left corner.
118 */
119 private int x = 0;
120
121 /**
122 * Get X position.
123 *
124 * @return absolute X position of the top-left corner
125 */
126 public final int getX() {
127 return x;
128 }
129
130 /**
131 * Set X position.
132 *
133 * @param x absolute X position of the top-left corner
134 */
135 public final void setX(final int x) {
136 this.x = x;
137 }
138
139 /**
140 * Absolute Y position of the top-left corner.
141 */
142 private int y = 0;
143
144 /**
145 * Get Y position.
146 *
147 * @return absolute Y position of the top-left corner
148 */
149 public final int getY() {
150 return y;
151 }
152
153 /**
154 * Set Y position.
155 *
156 * @param y absolute Y position of the top-left corner
157 */
158 public final void setY(final int y) {
159 this.y = y;
160 }
161
162 /**
163 * Width.
164 */
165 private int width = 0;
166
167 /**
168 * Get the width.
169 *
170 * @return widget width
171 */
172 public final int getWidth() {
173 return this.width;
174 }
175
176 /**
177 * Change the width.
178 *
179 * @param width new widget width
180 */
181 public final void setWidth(final int width) {
182 this.width = width;
183 }
184
185 /**
186 * Height.
187 */
188 private int height = 0;
189
190 /**
191 * Get the height.
192 *
193 * @return widget height
194 */
195 public final int getHeight() {
196 return this.height;
197 }
198
199 /**
200 * Change the height.
201 *
202 * @param height new widget height
203 */
204 public final void setHeight(final int height) {
205 this.height = height;
206 }
207
208 /**
209 * My tab order inside a window or containing widget.
210 */
211 private int tabOrder = 0;
212
213 /**
214 * If true, this widget can be tabbed to or receive events.
215 */
216 private boolean enabled = true;
217
218 /**
219 * Get enabled flag.
220 *
221 * @return if true, this widget can be tabbed to or receive events
222 */
223 public final boolean getEnabled() {
224 return enabled;
225 }
226
227 /**
228 * Set enabled flag.
229 *
230 * @param enabled if true, this widget can be tabbed to or receive events
231 */
232 public final void setEnabled(final boolean enabled) {
233 this.enabled = enabled;
234 if (!enabled) {
235 active = false;
236 // See if there are any active siblings to switch to
237 boolean foundSibling = false;
238 if (parent != null) {
239 for (TWidget w: parent.children) {
240 if ((w.enabled)
241 && !(this instanceof THScroller)
242 && !(this instanceof TVScroller)
243 ) {
244 parent.activate(w);
245 foundSibling = true;
246 break;
247 }
248 }
249 if (!foundSibling) {
250 parent.activeChild = null;
251 }
252 }
253 }
254 }
255
256 /**
257 * If true, this widget has a cursor.
258 */
259 private boolean hasCursor = false;
260
261 /**
262 * See if this widget has a visible cursor.
263 *
264 * @return if true, this widget has a visible cursor
265 */
266 public final boolean visibleCursor() {
267 return hasCursor;
268 }
269
270 /**
271 * Cursor column position in relative coordinates.
272 */
273 private int cursorX = 0;
274
275 /**
276 * Cursor row position in relative coordinates.
277 */
278 private int cursorY = 0;
279
280 /**
281 * Comparison operator sorts on tabOrder.
282 *
283 * @param that another TWidget instance
284 * @return difference between this.tabOrder and that.tabOrder
285 */
286 public final int compare(final TWidget that) {
287 return (this.tabOrder - that.tabOrder);
288 }
289
290 /**
291 * See if this widget should render with the active color.
292 *
293 * @return true if this widget is active and all of its parents are
294 * active.
295 */
296 public final boolean getAbsoluteActive() {
297 if (parent == this) {
298 return active;
299 }
300 return (active && parent.getAbsoluteActive());
301 }
302
303 /**
304 * Returns the cursor X position.
305 *
306 * @return absolute screen column number for the cursor's X position
307 */
308 public final int getCursorAbsoluteX() {
309 assert (hasCursor);
310 return getAbsoluteX() + cursorX;
311 }
312
313 /**
314 * Returns the cursor Y position.
315 *
316 * @return absolute screen row number for the cursor's Y position
317 */
318 public final int getCursorAbsoluteY() {
319 assert (hasCursor);
320 return getAbsoluteY() + cursorY;
321 }
322
323 /**
324 * Compute my absolute X position as the sum of my X plus all my parent's
325 * X's.
326 *
327 * @return absolute screen column number for my X position
328 */
329 public final int getAbsoluteX() {
330 assert (parent != null);
331 if (parent == this) {
332 return x;
333 }
334 if ((parent instanceof TWindow) && !(parent instanceof TMenu)) {
335 // Widgets on a TWindow have (0,0) as their top-left, but this is
336 // actually the TWindow's (1,1).
337 return parent.getAbsoluteX() + x + 1;
338 }
339 return parent.getAbsoluteX() + x;
340 }
341
342 /**
343 * Compute my absolute Y position as the sum of my Y plus all my parent's
344 * Y's.
345 *
346 * @return absolute screen row number for my Y position
347 */
348 public final int getAbsoluteY() {
349 assert (parent != null);
350 if (parent == this) {
351 return y;
352 }
353 if ((parent instanceof TWindow) && !(parent instanceof TMenu)) {
354 // Widgets on a TWindow have (0,0) as their top-left, but this is
355 // actually the TWindow's (1,1).
356 return parent.getAbsoluteY() + y + 1;
357 }
358 return parent.getAbsoluteY() + y;
359 }
360
361 /**
362 * Draw my specific widget. When called, the screen rectangle I draw
363 * into is already setup (offset and clipping).
364 */
365 public void draw() {
366 // Default widget draws nothing.
367 }
368
369 /**
370 * Called by parent to render to TWindow.
371 */
372 public final void drawChildren() {
373 // Set my clipping rectangle
374 assert (window != null);
375 assert (window.getScreen() != null);
376 Screen screen = window.getScreen();
377
378 screen.setClipRight(width);
379 screen.setClipBottom(height);
380
381 int absoluteRightEdge = window.getAbsoluteX() + screen.getWidth();
382 int absoluteBottomEdge = window.getAbsoluteY() + screen.getHeight();
383 if (!(this instanceof TWindow) && !(this instanceof TVScroller)) {
384 absoluteRightEdge -= 1;
385 }
386 if (!(this instanceof TWindow) && !(this instanceof THScroller)) {
387 absoluteBottomEdge -= 1;
388 }
389 int myRightEdge = getAbsoluteX() + width;
390 int myBottomEdge = getAbsoluteY() + height;
391 if (getAbsoluteX() > absoluteRightEdge) {
392 // I am offscreen
393 screen.setClipRight(0);
394 } else if (myRightEdge > absoluteRightEdge) {
395 screen.setClipRight(screen.getClipRight()
396 - myRightEdge - absoluteRightEdge);
397 }
398 if (getAbsoluteY() > absoluteBottomEdge) {
399 // I am offscreen
400 screen.setClipBottom(0);
401 } else if (myBottomEdge > absoluteBottomEdge) {
402 screen.setClipBottom(screen.getClipBottom()
403 - myBottomEdge - absoluteBottomEdge);
404 }
405
406 // Set my offset
407 screen.setOffsetX(getAbsoluteX());
408 screen.setOffsetY(getAbsoluteY());
409
410 // Draw me
411 draw();
412
413 // Continue down the chain
414 for (TWidget widget: children) {
415 widget.drawChildren();
416 }
417 }
418
419 /**
420 * Default constructor for subclasses.
421 */
422 protected TWidget() {
423 children = new LinkedList<TWidget>();
424 }
425
426 /**
427 * Protected constructor.
428 *
429 * @param parent parent widget
430 */
431 protected TWidget(final TWidget parent) {
432 this.parent = parent;
433 this.window = parent.window;
434 children = new LinkedList<TWidget>();
435 parent.addChild(this);
436 }
437
438 /**
439 * Add a child widget to my list of children. We set its tabOrder to 0
440 * and increment the tabOrder of all other children.
441 *
442 * @param child TWidget to add
443 */
444 private void addChild(final TWidget child) {
445 children.add(child);
446
447 if ((child.enabled)
448 && !(child instanceof THScroller)
449 && !(child instanceof TVScroller)
450 ) {
451 for (TWidget widget: children) {
452 widget.active = false;
453 }
454 child.active = true;
455 activeChild = child;
456 }
457 for (int i = 0; i < children.size(); i++) {
458 children.get(i).tabOrder = i;
459 }
460 }
461
462 /**
463 * Switch the active child.
464 *
465 * @param child TWidget to activate
466 */
467 public final void activate(final TWidget child) {
468 assert (child.enabled);
469 if ((child instanceof THScroller)
470 || (child instanceof TVScroller)
471 ) {
472 return;
473 }
474
475 if (child != activeChild) {
476 if (activeChild != null) {
477 activeChild.active = false;
478 }
479 child.active = true;
480 activeChild = child;
481 }
482 }
483
484 /**
485 * Switch the active child.
486 *
487 * @param tabOrder tabOrder of the child to activate. If that child
488 * isn't enabled, then the next enabled child will be activated.
489 */
490 public final void activate(final int tabOrder) {
491 if (activeChild == null) {
492 return;
493 }
494 TWidget child = null;
495 for (TWidget widget: children) {
496 if ((widget.enabled)
497 && !(widget instanceof THScroller)
498 && !(widget instanceof TVScroller)
499 && (widget.tabOrder >= tabOrder)
500 ) {
501 child = widget;
502 break;
503 }
504 }
505 if ((child != null) && (child != activeChild)) {
506 activeChild.active = false;
507 assert (child.enabled);
508 child.active = true;
509 activeChild = child;
510 }
511 }
512
513 /**
514 * Switch the active widget with the next in the tab order.
515 *
516 * @param forward if true, then switch to the next enabled widget in the
517 * list, otherwise switch to the previous enabled widget in the list
518 */
519 public final void switchWidget(final boolean forward) {
520
521 // Only switch if there are multiple enabled widgets
522 if ((children.size() < 2) || (activeChild == null)) {
523 return;
524 }
525
526 int tabOrder = activeChild.tabOrder;
527 do {
528 if (forward) {
529 tabOrder++;
530 } else {
531 tabOrder--;
532 }
533 if (tabOrder < 0) {
534
535 // If at the end, pass the switch to my parent.
536 if ((!forward) && (parent != this)) {
537 parent.switchWidget(forward);
538 return;
539 }
540
541 tabOrder = children.size() - 1;
542 } else if (tabOrder == children.size()) {
543 // If at the end, pass the switch to my parent.
544 if ((forward) && (parent != this)) {
545 parent.switchWidget(forward);
546 return;
547 }
548
549 tabOrder = 0;
550 }
551 if (activeChild.tabOrder == tabOrder) {
552 // We wrapped around
553 break;
554 }
555 } while ((!children.get(tabOrder).enabled)
556 && !(children.get(tabOrder) instanceof THScroller)
557 && !(children.get(tabOrder) instanceof TVScroller));
558
559 assert (children.get(tabOrder).enabled);
560
561 activeChild.active = false;
562 children.get(tabOrder).active = true;
563 activeChild = children.get(tabOrder);
564
565 // Refresh
566 window.getApplication().setRepaint();
567 }
568
569 /**
570 * Returns my active widget.
571 *
572 * @return widget that is active, or this if no children
573 */
574 public final TWidget getActiveChild() {
575 if ((this instanceof THScroller)
576 || (this instanceof TVScroller)
577 ) {
578 return parent;
579 }
580
581 for (TWidget widget: children) {
582 if (widget.active) {
583 return widget.getActiveChild();
584 }
585 }
586 // No active children, return me
587 return this;
588 }
589
590 /**
591 * Method that subclasses can override to handle keystrokes.
592 *
593 * @param keypress keystroke event
594 */
595 public void onKeypress(final TKeypressEvent keypress) {
596
597 if ((children.size() == 0)
598 // TODO
599 // || (cast(TTreeView)this)
600 // || (cast(TText)this)
601 ) {
602
603 // Defaults:
604 // tab / shift-tab - switch to next/previous widget
605 // right-arrow or down-arrow: same as tab
606 // left-arrow or up-arrow: same as shift-tab
607 if ((keypress.equals(kbTab))
608 || (keypress.equals(kbRight))
609 || (keypress.equals(kbDown))
610 ) {
611 parent.switchWidget(true);
612 return;
613 } else if ((keypress.equals(kbShiftTab))
614 || (keypress.equals(kbBackTab))
615 || (keypress.equals(kbLeft))
616 || (keypress.equals(kbUp))
617 ) {
618 parent.switchWidget(false);
619 return;
620 }
621 }
622
623 // If I have any buttons on me AND this is an Alt-key that matches
624 // its mnemonic, send it an Enter keystroke
625 for (TWidget widget: children) {
626 /*
627 TODO
628
629 if (TButton button = cast(TButton)w) {
630 if (button.enabled &&
631 !keypress.key.isKey &&
632 keypress.key.alt &&
633 !keypress.key.ctrl &&
634 (toLowercase(button.mnemonic.shortcut) == toLowercase(keypress.key.ch))) {
635
636 w.handleEvent(new TKeypressEvent(kbEnter));
637 return;
638 }
639 }
640 */
641 }
642
643 // Dispatch the keypress to an active widget
644 for (TWidget widget: children) {
645 if (widget.active) {
646 window.getApplication().setRepaint();
647 widget.handleEvent(keypress);
648 return;
649 }
650 }
651 }
652
653 /**
654 * Method that subclasses can override to handle mouse button presses.
655 *
656 * @param mouse mouse button event
657 */
658 public void onMouseDown(final TMouseEvent mouse) {
659 // Default: do nothing, pass to children instead
660 for (TWidget widget: children) {
661 if (widget.mouseWouldHit(mouse)) {
662 // Dispatch to this child, also activate it
663 activate(widget);
664
665 // Set x and y relative to the child's coordinates
666 mouse.setX(mouse.getAbsoluteX() - widget.getAbsoluteX());
667 mouse.setY(mouse.getAbsoluteY() - widget.getAbsoluteY());
668 widget.handleEvent(mouse);
669 return;
670 }
671 }
672 }
673
674 /**
675 * Method that subclasses can override to handle mouse button releases.
676 *
677 * @param mouse mouse button event
678 */
679 public void onMouseUp(final TMouseEvent mouse) {
680 // Default: do nothing, pass to children instead
681 for (TWidget widget: children) {
682 if (widget.mouseWouldHit(mouse)) {
683 // Dispatch to this child, also activate it
684 activate(widget);
685
686 // Set x and y relative to the child's coordinates
687 mouse.setX(mouse.getAbsoluteX() - widget.getAbsoluteX());
688 mouse.setY(mouse.getAbsoluteY() - widget.getAbsoluteY());
689 widget.handleEvent(mouse);
690 return;
691 }
692 }
693 }
694
695 /**
696 * Method that subclasses can override to handle mouse movements.
697 *
698 * @param mouse mouse motion event
699 */
700 public void onMouseMotion(final TMouseEvent mouse) {
701 // Default: do nothing, pass it on to ALL of my children. This way
702 // the children can see the mouse "leaving" their area.
703 for (TWidget widget: children) {
704 // Set x and y relative to the child's coordinates
705 mouse.setX(mouse.getAbsoluteX() - widget.getAbsoluteX());
706 mouse.setY(mouse.getAbsoluteY() - widget.getAbsoluteY());
707 widget.handleEvent(mouse);
708 }
709 }
710
711 /**
712 * Method that subclasses can override to handle window/screen resize
713 * events.
714 *
715 * @param resize resize event
716 */
717 public void onResize(final TResizeEvent resize) {
718 // Default: do nothing, pass to children instead
719 for (TWidget widget: children) {
720 widget.onResize(resize);
721 }
722 }
723
724 /**
725 * Method that subclasses can override to handle posted command events.
726 *
727 * @param command command event
728 */
729 public void onCommand(final TCommandEvent command) {
730 // Default: do nothing, pass to children instead
731 for (TWidget widget: children) {
732 widget.onCommand(command);
733 }
734 }
735
736 /**
737 * Method that subclasses can override to handle menu or posted menu
738 * events.
739 *
740 * @param menu menu event
741 */
742 public void onMenu(final TMenuEvent menu) {
743 // Default: do nothing, pass to children instead
744 for (TWidget widget: children) {
745 widget.onMenu(menu);
746 }
747 }
748
749 /**
750 * Method that subclasses can override to do processing when the UI is
751 * idle.
752 */
753 public void onIdle() {
754 // Default: do nothing, pass to children instead
755 for (TWidget widget: children) {
756 widget.onIdle();
757 }
758 }
759
760 /**
761 * Consume event. Subclasses that want to intercept all events in one go
762 * can override this method.
763 *
764 * @param event keyboard, mouse, resize, command, or menu event
765 */
766 public void handleEvent(final TInputEvent event) {
767 // System.err.printf("TWidget (%s) event: %s\n", this.getClass().getName(),
768 // event);
769
770 if (!enabled) {
771 // Discard event
772 // System.err.println(" -- discard --");
773 return;
774 }
775
776 if (event instanceof TKeypressEvent) {
777 onKeypress((TKeypressEvent) event);
778 } else if (event instanceof TMouseEvent) {
779
780 TMouseEvent mouse = (TMouseEvent) event;
781
782 switch (mouse.getType()) {
783
784 case MOUSE_DOWN:
785 onMouseDown(mouse);
786 break;
787
788 case MOUSE_UP:
789 onMouseUp(mouse);
790 break;
791
792 case MOUSE_MOTION:
793 onMouseMotion(mouse);
794 break;
795
796 default:
797 throw new IllegalArgumentException("Invalid mouse event type: "
798 + mouse.getType());
799 }
800 } else if (event instanceof TResizeEvent) {
801 onResize((TResizeEvent) event);
802 } else if (event instanceof TCommandEvent) {
803 onCommand((TCommandEvent) event);
804 } else if (event instanceof TMenuEvent) {
805 onMenu((TMenuEvent) event);
806 }
807
808 // Do nothing else
809 return;
810 }
811
812 /**
813 * Check if a mouse press/release event coordinate is contained in this
814 * widget.
815 *
816 * @param mouse a mouse-based event
817 * @return whether or not a mouse click would be sent to this widget
818 */
819 public final boolean mouseWouldHit(final TMouseEvent mouse) {
820
821 if (!enabled) {
822 return false;
823 }
824
825 if ((mouse.getAbsoluteX() >= getAbsoluteX())
826 && (mouse.getAbsoluteX() < getAbsoluteX() + width)
827 && (mouse.getAbsoluteY() >= getAbsoluteY())
828 && (mouse.getAbsoluteY() < getAbsoluteY() + height)
829 ) {
830 return true;
831 }
832 return false;
833 }
834
835 }