2 * Jexer - Java Text User Interface
4 * License: LGPLv3 or later
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.
10 * Copyright (C) 2015 Kevin Lamonte
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.
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.
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
28 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
33 import java
.util
.List
;
34 import java
.util
.LinkedList
;
36 import jexer
.bits
.ColorTheme
;
37 import jexer
.event
.TCommandEvent
;
38 import jexer
.event
.TInputEvent
;
39 import jexer
.event
.TKeypressEvent
;
40 import jexer
.event
.TMenuEvent
;
41 import jexer
.event
.TMouseEvent
;
42 import jexer
.event
.TResizeEvent
;
43 import jexer
.io
.Screen
;
44 import jexer
.menu
.TMenu
;
45 import static jexer
.TKeypress
.*;
48 * TWidget is the base class of all objects that can be drawn on screen or
49 * handle user input events.
51 public abstract class TWidget
{
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.
58 private TWidget parent
= null;
63 * @return parent widget
65 public final TWidget
getParent() {
70 * Backdoor access for TWindow's constructor. ONLY TWindow USES THIS.
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
78 protected final void setupForTWindow(final TWindow window
,
79 final int x
, final int y
, final int width
, final int height
) {
90 * Request full repaint on next screen refresh.
92 protected final void setRepaint() {
93 window
.getApplication().setRepaint();
97 * Get this TWidget's parent TApplication.
99 * @return the parent TApplication
101 public TApplication
getApplication() {
102 return window
.getApplication();
110 public Screen
getScreen() {
111 return window
.getScreen();
115 * Child widgets that this widget contains.
117 private List
<TWidget
> children
;
120 * Get the list of child widgets that this widget contains.
122 * @return the list of child widgets
124 public List
<TWidget
> getChildren() {
129 * The currently active child widget that will receive keypress events.
131 private TWidget activeChild
= null;
134 * If true, this widget will receive events.
136 private boolean active
= false;
141 * @return if true, this widget will receive events
143 public final boolean getActive() {
150 * @param active if true, this widget will receive events
152 public final void setActive(final boolean active
) {
153 this.active
= active
;
157 * The window that this widget draws to.
159 private TWindow window
= null;
162 * Absolute X position of the top-left corner.
169 * @return absolute X position of the top-left corner
171 public final int getX() {
178 * @param x absolute X position of the top-left corner
180 public final void setX(final int x
) {
185 * Absolute Y position of the top-left corner.
192 * @return absolute Y position of the top-left corner
194 public final int getY() {
201 * @param y absolute Y position of the top-left corner
203 public final void setY(final int y
) {
210 private int width
= 0;
215 * @return widget width
217 public final int getWidth() {
224 * @param width new widget width
226 public final void setWidth(final int width
) {
233 private int height
= 0;
238 * @return widget height
240 public final int getHeight() {
247 * @param height new widget height
249 public final void setHeight(final int height
) {
250 this.height
= height
;
254 * My tab order inside a window or containing widget.
256 private int tabOrder
= 0;
259 * If true, this widget can be tabbed to or receive events.
261 private boolean enabled
= true;
266 * @return if true, this widget can be tabbed to or receive events
268 public final boolean getEnabled() {
275 * @param enabled if true, this widget can be tabbed to or receive events
277 public final void setEnabled(final boolean enabled
) {
278 this.enabled
= enabled
;
281 // See if there are any active siblings to switch to
282 boolean foundSibling
= false;
283 if (parent
!= null) {
284 for (TWidget w
: parent
.children
) {
286 && !(this instanceof THScroller
)
287 && !(this instanceof TVScroller
)
295 parent
.activeChild
= null;
302 * If true, this widget has a cursor.
304 private boolean hasCursor
= false;
307 * See if this widget has a visible cursor.
309 * @return if true, this widget has a visible cursor
311 public final boolean visibleCursor() {
316 * Cursor column position in relative coordinates.
318 private int cursorX
= 0;
321 * Cursor row position in relative coordinates.
323 private int cursorY
= 0;
326 * Comparison operator sorts on tabOrder.
328 * @param that another TWidget instance
329 * @return difference between this.tabOrder and that.tabOrder
331 public final int compare(final TWidget that
) {
332 return (this.tabOrder
- that
.tabOrder
);
336 * See if this widget should render with the active color.
338 * @return true if this widget is active and all of its parents are
341 public final boolean getAbsoluteActive() {
342 if (parent
== this) {
345 return (active
&& parent
.getAbsoluteActive());
349 * Returns the cursor X position.
351 * @return absolute screen column number for the cursor's X position
353 public final int getCursorAbsoluteX() {
355 return getAbsoluteX() + cursorX
;
359 * Returns the cursor Y position.
361 * @return absolute screen row number for the cursor's Y position
363 public final int getCursorAbsoluteY() {
365 return getAbsoluteY() + cursorY
;
369 * Compute my absolute X position as the sum of my X plus all my parent's
372 * @return absolute screen column number for my X position
374 public final int getAbsoluteX() {
375 assert (parent
!= null);
376 if (parent
== this) {
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;
384 return parent
.getAbsoluteX() + x
;
388 * Compute my absolute Y position as the sum of my Y plus all my parent's
391 * @return absolute screen row number for my Y position
393 public final int getAbsoluteY() {
394 assert (parent
!= null);
395 if (parent
== this) {
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;
403 return parent
.getAbsoluteY() + y
;
407 * Get the global color theme.
409 * @return the ColorTheme
411 public final ColorTheme
getTheme() {
412 return window
.getApplication().getTheme();
416 * Draw my specific widget. When called, the screen rectangle I draw
417 * into is already setup (offset and clipping).
420 // Default widget draws nothing.
424 * Called by parent to render to TWindow.
426 public final void drawChildren() {
427 // Set my clipping rectangle
428 assert (window
!= null);
429 assert (window
.getScreen() != null);
430 Screen screen
= window
.getScreen();
432 screen
.setClipRight(width
);
433 screen
.setClipBottom(height
);
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;
440 if (!(this instanceof TWindow
) && !(this instanceof THScroller
)) {
441 absoluteBottomEdge
-= 1;
443 int myRightEdge
= getAbsoluteX() + width
;
444 int myBottomEdge
= getAbsoluteY() + height
;
445 if (getAbsoluteX() > absoluteRightEdge
) {
447 screen
.setClipRight(0);
448 } else if (myRightEdge
> absoluteRightEdge
) {
449 screen
.setClipRight(screen
.getClipRight()
450 - myRightEdge
- absoluteRightEdge
);
452 if (getAbsoluteY() > absoluteBottomEdge
) {
454 screen
.setClipBottom(0);
455 } else if (myBottomEdge
> absoluteBottomEdge
) {
456 screen
.setClipBottom(screen
.getClipBottom()
457 - myBottomEdge
- absoluteBottomEdge
);
461 screen
.setOffsetX(getAbsoluteX());
462 screen
.setOffsetY(getAbsoluteY());
467 // Continue down the chain
468 for (TWidget widget
: children
) {
469 widget
.drawChildren();
474 * Default constructor for subclasses.
476 protected TWidget() {
477 children
= new LinkedList
<TWidget
>();
481 * Protected constructor.
483 * @param parent parent widget
485 protected TWidget(final TWidget parent
) {
486 this.parent
= parent
;
487 this.window
= parent
.window
;
488 children
= new LinkedList
<TWidget
>();
489 parent
.addChild(this);
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.
496 * @param child TWidget to add
498 private void addChild(final TWidget child
) {
502 && !(child
instanceof THScroller
)
503 && !(child
instanceof TVScroller
)
505 for (TWidget widget
: children
) {
506 widget
.active
= false;
511 for (int i
= 0; i
< children
.size(); i
++) {
512 children
.get(i
).tabOrder
= i
;
517 * Switch the active child.
519 * @param child TWidget to activate
521 public final void activate(final TWidget child
) {
522 assert (child
.enabled
);
523 if ((child
instanceof THScroller
)
524 || (child
instanceof TVScroller
)
529 if (child
!= activeChild
) {
530 if (activeChild
!= null) {
531 activeChild
.active
= false;
539 * Switch the active child.
541 * @param tabOrder tabOrder of the child to activate. If that child
542 * isn't enabled, then the next enabled child will be activated.
544 public final void activate(final int tabOrder
) {
545 if (activeChild
== null) {
548 TWidget child
= null;
549 for (TWidget widget
: children
) {
551 && !(widget
instanceof THScroller
)
552 && !(widget
instanceof TVScroller
)
553 && (widget
.tabOrder
>= tabOrder
)
559 if ((child
!= null) && (child
!= activeChild
)) {
560 activeChild
.active
= false;
561 assert (child
.enabled
);
568 * Switch the active widget with the next in the tab order.
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
573 public final void switchWidget(final boolean forward
) {
575 // Only switch if there are multiple enabled widgets
576 if ((children
.size() < 2) || (activeChild
== null)) {
580 int tabOrder
= activeChild
.tabOrder
;
589 // If at the end, pass the switch to my parent.
590 if ((!forward
) && (parent
!= this)) {
591 parent
.switchWidget(forward
);
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
);
605 if (activeChild
.tabOrder
== tabOrder
) {
609 } while ((!children
.get(tabOrder
).enabled
)
610 && !(children
.get(tabOrder
) instanceof THScroller
)
611 && !(children
.get(tabOrder
) instanceof TVScroller
));
613 assert (children
.get(tabOrder
).enabled
);
615 activeChild
.active
= false;
616 children
.get(tabOrder
).active
= true;
617 activeChild
= children
.get(tabOrder
);
620 window
.getApplication().setRepaint();
624 * Returns my active widget.
626 * @return widget that is active, or this if no children
628 public TWidget
getActiveChild() {
629 if ((this instanceof THScroller
)
630 || (this instanceof TVScroller
)
635 for (TWidget widget
: children
) {
637 return widget
.getActiveChild();
640 // No active children, return me
645 * Method that subclasses can override to handle keystrokes.
647 * @param keypress keystroke event
649 public void onKeypress(final TKeypressEvent keypress
) {
651 if ((children
.size() == 0)
653 // || (cast(TTreeView)this)
654 // || (cast(TText)this)
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
))
665 parent
.switchWidget(true);
667 } else if ((keypress
.equals(kbShiftTab
))
668 || (keypress
.equals(kbBackTab
))
669 || (keypress
.equals(kbLeft
))
670 || (keypress
.equals(kbUp
))
672 parent
.switchWidget(false);
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
) {
683 if (TButton button = cast(TButton)w) {
684 if (button.enabled &&
685 !keypress.key.isKey &&
687 !keypress.key.ctrl &&
688 (toLowercase(button.mnemonic.shortcut) == toLowercase(keypress.key.ch))) {
690 w.handleEvent(new TKeypressEvent(kbEnter));
697 // Dispatch the keypress to an active widget
698 for (TWidget widget
: children
) {
700 window
.getApplication().setRepaint();
701 widget
.handleEvent(keypress
);
708 * Method that subclasses can override to handle mouse button presses.
710 * @param mouse mouse button event
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
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
);
729 * Method that subclasses can override to handle mouse button releases.
731 * @param mouse mouse button event
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
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
);
750 * Method that subclasses can override to handle mouse movements.
752 * @param mouse mouse motion event
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
);
766 * Method that subclasses can override to handle window/screen resize
769 * @param resize resize event
771 public void onResize(final TResizeEvent resize
) {
772 // Default: do nothing, pass to children instead
773 for (TWidget widget
: children
) {
774 widget
.onResize(resize
);
779 * Method that subclasses can override to handle posted command events.
781 * @param command command event
783 public void onCommand(final TCommandEvent command
) {
784 // Default: do nothing, pass to children instead
785 for (TWidget widget
: children
) {
786 widget
.onCommand(command
);
791 * Method that subclasses can override to handle menu or posted menu
794 * @param menu menu event
796 public void onMenu(final TMenuEvent menu
) {
797 // Default: do nothing, pass to children instead
798 for (TWidget widget
: children
) {
804 * Method that subclasses can override to do processing when the UI is
807 public void onIdle() {
808 // Default: do nothing, pass to children instead
809 for (TWidget widget
: children
) {
815 * Consume event. Subclasses that want to intercept all events in one go
816 * can override this method.
818 * @param event keyboard, mouse, resize, command, or menu event
820 public void handleEvent(final TInputEvent event
) {
821 // System.err.printf("TWidget (%s) event: %s\n", this.getClass().getName(),
826 // System.err.println(" -- discard --");
830 if (event
instanceof TKeypressEvent
) {
831 onKeypress((TKeypressEvent
) event
);
832 } else if (event
instanceof TMouseEvent
) {
834 TMouseEvent mouse
= (TMouseEvent
) event
;
836 switch (mouse
.getType()) {
847 onMouseMotion(mouse
);
851 throw new IllegalArgumentException("Invalid mouse event type: "
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
);
867 * Check if a mouse press/release event coordinate is contained in this
870 * @param mouse a mouse-based event
871 * @return whether or not a mouse click would be sent to this widget
873 public final boolean mouseWouldHit(final TMouseEvent mouse
) {
879 if ((mouse
.getAbsoluteX() >= getAbsoluteX())
880 && (mouse
.getAbsoluteX() < getAbsoluteX() + width
)
881 && (mouse
.getAbsoluteY() >= getAbsoluteY())
882 && (mouse
.getAbsoluteY() < getAbsoluteY() + height
)