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
.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
.*;
46 * TWidget is the base class of all objects that can be drawn on screen or
47 * handle user input events.
49 public abstract class TWidget
{
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.
56 private TWidget parent
= null;
59 * Backdoor access for TWindow's constructor. ONLY TWindow USES THIS.
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
67 protected final void setupForTWindow(final TWindow window
,
68 final int x
, final int y
, final int width
, final int height
) {
79 * Child widgets that this widget contains.
81 private List
<TWidget
> children
;
84 * The currently active child widget that will receive keypress events.
86 private TWidget activeChild
= null;
89 * If true, this widget will receive events.
91 private boolean active
= false;
96 * @return if true, this widget will receive events
98 public final boolean getActive() {
105 * @param active if true, this widget will receive events
107 public final void setActive(boolean active
) {
108 this.active
= active
;
112 * The window that this widget draws to.
114 private TWindow window
= null;
117 * Absolute X position of the top-left corner.
124 * @return absolute X position of the top-left corner
126 public final int getX() {
133 * @param x absolute X position of the top-left corner
135 public final void setX(final int x
) {
140 * Absolute Y position of the top-left corner.
147 * @return absolute Y position of the top-left corner
149 public final int getY() {
156 * @param y absolute Y position of the top-left corner
158 public final void setY(final int y
) {
165 private int width
= 0;
170 * @return widget width
172 public final int getWidth() {
179 * @param width new widget width
181 public final void setWidth(final int width
) {
188 private int height
= 0;
193 * @return widget height
195 public final int getHeight() {
202 * @param height new widget height
204 public final void setHeight(final int height
) {
205 this.height
= height
;
209 * My tab order inside a window or containing widget.
211 private int tabOrder
= 0;
214 * If true, this widget can be tabbed to or receive events.
216 private boolean enabled
= true;
221 * @return if true, this widget can be tabbed to or receive events
223 public final boolean getEnabled() {
230 * @param enabled if true, this widget can be tabbed to or receive events
232 public final void setEnabled(final boolean enabled
) {
233 this.enabled
= enabled
;
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
) {
241 && !(this instanceof THScroller
)
242 && !(this instanceof TVScroller
)
250 parent
.activeChild
= null;
257 * If true, this widget has a cursor.
259 private boolean hasCursor
= false;
262 * See if this widget has a visible cursor.
264 * @return if true, this widget has a visible cursor
266 public final boolean visibleCursor() {
271 * Cursor column position in relative coordinates.
273 private int cursorX
= 0;
276 * Cursor row position in relative coordinates.
278 private int cursorY
= 0;
281 * Comparison operator sorts on tabOrder.
283 * @param that another TWidget instance
284 * @return difference between this.tabOrder and that.tabOrder
286 public final int compare(final TWidget that
) {
287 return (this.tabOrder
- that
.tabOrder
);
291 * See if this widget should render with the active color.
293 * @return true if this widget is active and all of its parents are
296 public final boolean getAbsoluteActive() {
297 if (parent
== this) {
300 return (active
&& parent
.getAbsoluteActive());
304 * Returns the cursor X position.
306 * @return absolute screen column number for the cursor's X position
308 public final int getCursorAbsoluteX() {
310 return getAbsoluteX() + cursorX
;
314 * Returns the cursor Y position.
316 * @return absolute screen row number for the cursor's Y position
318 public final int getCursorAbsoluteY() {
320 return getAbsoluteY() + cursorY
;
324 * Compute my absolute X position as the sum of my X plus all my parent's
327 * @return absolute screen column number for my X position
329 public final int getAbsoluteX() {
330 assert (parent
!= null);
331 if (parent
== this) {
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;
339 return parent
.getAbsoluteX() + x
;
343 * Compute my absolute Y position as the sum of my Y plus all my parent's
346 * @return absolute screen row number for my Y position
348 public final int getAbsoluteY() {
349 assert (parent
!= null);
350 if (parent
== this) {
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;
358 return parent
.getAbsoluteY() + y
;
362 * Draw my specific widget. When called, the screen rectangle I draw
363 * into is already setup (offset and clipping).
366 // Default widget draws nothing.
370 * Called by parent to render to TWindow.
372 public final void drawChildren() {
373 // Set my clipping rectangle
374 assert (window
!= null);
375 assert (window
.getScreen() != null);
376 Screen screen
= window
.getScreen();
378 screen
.setClipRight(width
);
379 screen
.setClipBottom(height
);
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;
386 if (!(this instanceof TWindow
) && !(this instanceof THScroller
)) {
387 absoluteBottomEdge
-= 1;
389 int myRightEdge
= getAbsoluteX() + width
;
390 int myBottomEdge
= getAbsoluteY() + height
;
391 if (getAbsoluteX() > absoluteRightEdge
) {
393 screen
.setClipRight(0);
394 } else if (myRightEdge
> absoluteRightEdge
) {
395 screen
.setClipRight(screen
.getClipRight()
396 - myRightEdge
- absoluteRightEdge
);
398 if (getAbsoluteY() > absoluteBottomEdge
) {
400 screen
.setClipBottom(0);
401 } else if (myBottomEdge
> absoluteBottomEdge
) {
402 screen
.setClipBottom(screen
.getClipBottom()
403 - myBottomEdge
- absoluteBottomEdge
);
407 screen
.setOffsetX(getAbsoluteX());
408 screen
.setOffsetY(getAbsoluteY());
413 // Continue down the chain
414 for (TWidget widget
: children
) {
415 widget
.drawChildren();
420 * Default constructor for subclasses.
422 protected TWidget() {
423 children
= new LinkedList
<TWidget
>();
427 * Protected constructor.
429 * @param parent parent widget
431 protected TWidget(final TWidget parent
) {
432 this.parent
= parent
;
433 this.window
= parent
.window
;
434 children
= new LinkedList
<TWidget
>();
435 parent
.addChild(this);
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.
442 * @param child TWidget to add
444 private void addChild(final TWidget child
) {
448 && !(child
instanceof THScroller
)
449 && !(child
instanceof TVScroller
)
451 for (TWidget widget
: children
) {
452 widget
.active
= false;
457 for (int i
= 0; i
< children
.size(); i
++) {
458 children
.get(i
).tabOrder
= i
;
463 * Switch the active child.
465 * @param child TWidget to activate
467 public final void activate(final TWidget child
) {
468 assert (child
.enabled
);
469 if ((child
instanceof THScroller
)
470 || (child
instanceof TVScroller
)
475 if (child
!= activeChild
) {
476 if (activeChild
!= null) {
477 activeChild
.active
= false;
485 * Switch the active child.
487 * @param tabOrder tabOrder of the child to activate. If that child
488 * isn't enabled, then the next enabled child will be activated.
490 public final void activate(final int tabOrder
) {
491 if (activeChild
== null) {
494 TWidget child
= null;
495 for (TWidget widget
: children
) {
497 && !(widget
instanceof THScroller
)
498 && !(widget
instanceof TVScroller
)
499 && (widget
.tabOrder
>= tabOrder
)
505 if ((child
!= null) && (child
!= activeChild
)) {
506 activeChild
.active
= false;
507 assert (child
.enabled
);
514 * Switch the active widget with the next in the tab order.
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
519 public final void switchWidget(final boolean forward
) {
521 // Only switch if there are multiple enabled widgets
522 if ((children
.size() < 2) || (activeChild
== null)) {
526 int tabOrder
= activeChild
.tabOrder
;
535 // If at the end, pass the switch to my parent.
536 if ((!forward
) && (parent
!= this)) {
537 parent
.switchWidget(forward
);
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
);
551 if (activeChild
.tabOrder
== tabOrder
) {
555 } while ((!children
.get(tabOrder
).enabled
)
556 && !(children
.get(tabOrder
) instanceof THScroller
)
557 && !(children
.get(tabOrder
) instanceof TVScroller
));
559 assert (children
.get(tabOrder
).enabled
);
561 activeChild
.active
= false;
562 children
.get(tabOrder
).active
= true;
563 activeChild
= children
.get(tabOrder
);
566 window
.getApplication().setRepaint();
570 * Returns my active widget.
572 * @return widget that is active, or this if no children
574 public final TWidget
getActiveChild() {
575 if ((this instanceof THScroller
)
576 || (this instanceof TVScroller
)
581 for (TWidget widget
: children
) {
583 return widget
.getActiveChild();
586 // No active children, return me
591 * Method that subclasses can override to handle keystrokes.
593 * @param keypress keystroke event
595 public void onKeypress(final TKeypressEvent keypress
) {
597 if ((children
.size() == 0)
599 // || (cast(TTreeView)this)
600 // || (cast(TText)this)
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
))
611 parent
.switchWidget(true);
613 } else if ((keypress
.equals(kbShiftTab
))
614 || (keypress
.equals(kbBackTab
))
615 || (keypress
.equals(kbLeft
))
616 || (keypress
.equals(kbUp
))
618 parent
.switchWidget(false);
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
) {
629 if (TButton button = cast(TButton)w) {
630 if (button.enabled &&
631 !keypress.key.isKey &&
633 !keypress.key.ctrl &&
634 (toLowercase(button.mnemonic.shortcut) == toLowercase(keypress.key.ch))) {
636 w.handleEvent(new TKeypressEvent(kbEnter));
643 // Dispatch the keypress to an active widget
644 for (TWidget widget
: children
) {
646 window
.getApplication().setRepaint();
647 widget
.handleEvent(keypress
);
654 * Method that subclasses can override to handle mouse button presses.
656 * @param mouse mouse button event
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
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
);
675 * Method that subclasses can override to handle mouse button releases.
677 * @param mouse mouse button event
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
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
);
696 * Method that subclasses can override to handle mouse movements.
698 * @param mouse mouse motion event
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
);
712 * Method that subclasses can override to handle window/screen resize
715 * @param resize resize event
717 public void onResize(final TResizeEvent resize
) {
718 // Default: do nothing, pass to children instead
719 for (TWidget widget
: children
) {
720 widget
.onResize(resize
);
725 * Method that subclasses can override to handle posted command events.
727 * @param command command event
729 public void onCommand(final TCommandEvent command
) {
730 // Default: do nothing, pass to children instead
731 for (TWidget widget
: children
) {
732 widget
.onCommand(command
);
737 * Method that subclasses can override to handle menu or posted menu
740 * @param menu menu event
742 public void onMenu(final TMenuEvent menu
) {
743 // Default: do nothing, pass to children instead
744 for (TWidget widget
: children
) {
750 * Method that subclasses can override to do processing when the UI is
753 public void onIdle() {
754 // Default: do nothing, pass to children instead
755 for (TWidget widget
: children
) {
761 * Consume event. Subclasses that want to intercept all events in one go
762 * can override this method.
764 * @param event keyboard, mouse, resize, command, or menu event
766 public void handleEvent(final TInputEvent event
) {
767 // System.err.printf("TWidget (%s) event: %s\n", this.getClass().getName(),
772 // System.err.println(" -- discard --");
776 if (event
instanceof TKeypressEvent
) {
777 onKeypress((TKeypressEvent
) event
);
778 } else if (event
instanceof TMouseEvent
) {
780 TMouseEvent mouse
= (TMouseEvent
) event
;
782 switch (mouse
.getType()) {
793 onMouseMotion(mouse
);
797 throw new IllegalArgumentException("Invalid mouse event type: "
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
);
813 * Check if a mouse press/release event coordinate is contained in this
816 * @param mouse a mouse-based event
817 * @return whether or not a mouse click would be sent to this widget
819 public final boolean mouseWouldHit(final TMouseEvent mouse
) {
825 if ((mouse
.getAbsoluteX() >= getAbsoluteX())
826 && (mouse
.getAbsoluteX() < getAbsoluteX() + width
)
827 && (mouse
.getAbsoluteY() >= getAbsoluteY())
828 && (mouse
.getAbsoluteY() < getAbsoluteY() + height
)