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