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 protected TWidget parent
= null;
59 * Child widgets that this widget contains.
61 private List
<TWidget
> children
;
64 * The currently active child widget that will receive keypress events.
66 private TWidget activeChild
= null;
69 * If true, this widget will receive events.
71 protected boolean active
= false;
74 * The window that this widget draws to.
76 protected TWindow window
= null;
79 * Absolute X position of the top-left corner.
84 * Absolute Y position of the top-left corner.
91 protected int width
= 0;
96 protected int height
= 0;
99 * My tab order inside a window or containing widget.
101 private int tabOrder
= 0;
104 * If true, this widget can be tabbed to or receive events.
106 private boolean enabled
= true;
111 * @return if true, this widget can be tabbed to or receive events
113 public final boolean getEnabled() {
120 * @param enabled if true, this widget can be tabbed to or receive events
122 public final void setEnabled(final boolean enabled
) {
123 this.enabled
= enabled
;
126 // TODO: get this working after scrollers are going again
128 if (enabled == 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) {
135 (!cast(THScroller)this) &&
136 (!cast(TVScroller)this)
144 parent.activeChild = null;
152 * If true, this widget has a cursor.
154 private boolean hasCursor
= false;
157 * See if this widget has a visible cursor.
159 * @return if true, this widget has a visible cursor
161 public final boolean visibleCursor() {
166 * Cursor column position in relative coordinates.
168 private int cursorX
= 0;
171 * Cursor row position in relative coordinates.
173 private int cursorY
= 0;
176 * Comparison operator sorts on tabOrder.
178 * @param that another TWidget instance
179 * @return difference between this.tabOrder and that.tabOrder
181 public final int compare(final TWidget that
) {
182 return (this.tabOrder
- that
.tabOrder
);
186 * See if this widget should render with the active color.
188 * @return true if this widget is active and all of its parents are
191 public final boolean getAbsoluteActive() {
192 if (parent
== this) {
195 return (active
&& parent
.getAbsoluteActive());
199 * Returns the cursor X position.
201 * @return absolute screen column number for the cursor's X position
203 public final int getCursorAbsoluteX() {
205 return getAbsoluteX() + cursorX
;
209 * Returns the cursor Y position.
211 * @return absolute screen row number for the cursor's Y position
213 public final int getCursorAbsoluteY() {
215 return getAbsoluteY() + cursorY
;
219 * Compute my absolute X position as the sum of my X plus all my parent's
222 * @return absolute screen column number for my X position
224 public final int getAbsoluteX() {
225 assert (parent
!= null);
226 if (parent
== this) {
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;
234 return parent
.getAbsoluteX() + x
;
238 * Compute my absolute Y position as the sum of my Y plus all my parent's
241 * @return absolute screen row number for my Y position
243 public final int getAbsoluteY() {
244 assert (parent
!= null);
245 if (parent
== this) {
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;
253 return parent
.getAbsoluteY() + y
;
257 * Draw my specific widget. When called, the screen rectangle I draw
258 * into is already setup (offset and clipping).
261 // Default widget draws nothing.
265 * Called by parent to render to TWindow.
267 public final void drawChildren() {
268 // Set my clipping rectangle
269 assert (window
!= null);
270 assert (window
.getScreen() != null);
271 Screen screen
= window
.getScreen();
273 screen
.setClipRight(width
);
274 screen
.setClipBottom(height
);
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;
281 if (!(this instanceof TWindow
) && !(this instanceof THScroller
)) {
282 absoluteBottomEdge
-= 1;
284 int myRightEdge
= getAbsoluteX() + width
;
285 int myBottomEdge
= getAbsoluteY() + height
;
286 if (getAbsoluteX() > absoluteRightEdge
) {
288 screen
.setClipRight(0);
289 } else if (myRightEdge
> absoluteRightEdge
) {
290 screen
.setClipRight(screen
.getClipRight()
291 - myRightEdge
- absoluteRightEdge
);
293 if (getAbsoluteY() > absoluteBottomEdge
) {
295 screen
.setClipBottom(0);
296 } else if (myBottomEdge
> absoluteBottomEdge
) {
297 screen
.setClipBottom(screen
.getClipBottom()
298 - myBottomEdge
- absoluteBottomEdge
);
302 screen
.setOffsetX(getAbsoluteX());
303 screen
.setOffsetY(getAbsoluteY());
308 // Continue down the chain
309 for (TWidget widget
: children
) {
310 widget
.drawChildren();
315 * Subclasses need this constructor to setup children.
317 protected TWidget() {
318 children
= new LinkedList
<TWidget
>();
322 * Protected constructor.
324 * @param parent parent widget
326 protected TWidget(final TWidget parent
) {
327 this.parent
= parent
;
328 this.window
= parent
.window
;
330 parent
.addChild(this);
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.
337 * @param child TWidget to add
339 private void addChild(final TWidget child
) {
343 && !(child
instanceof THScroller
)
344 && !(child
instanceof TVScroller
)
346 for (TWidget widget
: children
) {
347 widget
.active
= false;
352 for (int i
= 0; i
< children
.size(); i
++) {
353 children
.get(i
).tabOrder
= i
;
358 * Switch the active child.
360 * @param child TWidget to activate
362 public final void activate(final TWidget child
) {
363 assert (child
.enabled
);
364 if ((child
instanceof THScroller
)
365 || (child
instanceof TVScroller
)
370 if (child
!= activeChild
) {
371 if (activeChild
!= null) {
372 activeChild
.active
= false;
380 * Switch the active child.
382 * @param tabOrder tabOrder of the child to activate. If that child
383 * isn't enabled, then the next enabled child will be activated.
385 public final void activate(final int tabOrder
) {
386 if (activeChild
== null) {
389 TWidget child
= null;
390 for (TWidget widget
: children
) {
392 && !(widget
instanceof THScroller
)
393 && !(widget
instanceof TVScroller
)
394 && (widget
.tabOrder
>= tabOrder
)
400 if ((child
!= null) && (child
!= activeChild
)) {
401 activeChild
.active
= false;
402 assert (child
.enabled
);
409 * Switch the active widget with the next in the tab order.
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
414 public final void switchWidget(final boolean forward
) {
416 // Only switch if there are multiple enabled widgets
417 if ((children
.size() < 2) || (activeChild
== null)) {
421 int tabOrder
= activeChild
.tabOrder
;
430 // If at the end, pass the switch to my parent.
431 if ((!forward
) && (parent
!= this)) {
432 parent
.switchWidget(forward
);
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
);
446 if (activeChild
.tabOrder
== tabOrder
) {
450 } while ((!children
.get(tabOrder
).enabled
)
451 && !(children
.get(tabOrder
) instanceof THScroller
)
452 && !(children
.get(tabOrder
) instanceof TVScroller
));
454 assert (children
.get(tabOrder
).enabled
);
456 activeChild
.active
= false;
457 children
.get(tabOrder
).active
= true;
458 activeChild
= children
.get(tabOrder
);
461 window
.getApplication().setRepaint();
465 * Returns my active widget.
467 * @return widget that is active, or this if no children
469 public final TWidget
getActiveChild() {
470 if ((this instanceof THScroller
)
471 || (this instanceof TVScroller
)
476 for (TWidget widget
: children
) {
478 return widget
.getActiveChild();
481 // No active children, return me
486 * Method that subclasses can override to handle keystrokes.
488 * @param keypress keystroke event
490 public void onKeypress(final TKeypressEvent keypress
) {
492 if ((children
.size() == 0)
493 // || (cast(TTreeView)this)
494 // || (cast(TText)this)
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
))
505 parent
.switchWidget(true);
507 } else if ((keypress
.equals(kbShiftTab
))
508 || (keypress
.equals(kbBackTab
))
509 || (keypress
.equals(kbLeft
))
510 || (keypress
.equals(kbUp
))
512 parent
.switchWidget(false);
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
) {
523 if (TButton button = cast(TButton)w) {
524 if (button.enabled &&
525 !keypress.key.isKey &&
527 !keypress.key.ctrl &&
528 (toLowercase(button.mnemonic.shortcut) == toLowercase(keypress.key.ch))) {
530 w.handleEvent(new TKeypressEvent(kbEnter));
537 // Dispatch the keypress to an active widget
538 for (TWidget widget
: children
) {
540 window
.getApplication().setRepaint();
541 widget
.handleEvent(keypress
);
548 * Method that subclasses can override to handle mouse button presses.
550 * @param mouse mouse button event
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
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
);
569 * Method that subclasses can override to handle mouse button releases.
571 * @param mouse mouse button event
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
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
);
590 * Method that subclasses can override to handle mouse movements.
592 * @param mouse mouse motion event
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
);
606 * Method that subclasses can override to handle window/screen resize
609 * @param resize resize event
611 public void onResize(final TResizeEvent resize
) {
612 // Default: do nothing, pass to children instead
613 for (TWidget widget
: children
) {
614 widget
.onResize(resize
);
619 * Method that subclasses can override to handle posted command events.
621 * @param command command event
623 public void onCommand(final TCommandEvent command
) {
624 // Default: do nothing, pass to children instead
625 for (TWidget widget
: children
) {
626 widget
.onCommand(command
);
631 * Method that subclasses can override to handle menu or posted menu
634 * @param menu menu event
636 public void onMenu(final TMenuEvent menu
) {
637 // Default: do nothing, pass to children instead
638 for (TWidget widget
: children
) {
644 * Method that subclasses can override to do processing when the UI is
647 public void onIdle() {
648 // Default: do nothing, pass to children instead
649 for (TWidget widget
: children
) {
655 * Consume event. Subclasses that want to intercept all events in one go
656 * can override this method.
658 * @param event keyboard, mouse, resize, command, or menu event
660 public void handleEvent(final TInputEvent event
) {
661 // System.err.printf("TWidget (%s) event: %s\n", this.getClass().getName(),
666 // System.err.println(" -- discard --");
670 if (event
instanceof TKeypressEvent
) {
671 onKeypress((TKeypressEvent
) event
);
672 } else if (event
instanceof TMouseEvent
) {
674 TMouseEvent mouse
= (TMouseEvent
) event
;
676 switch (mouse
.getType()) {
687 onMouseMotion(mouse
);
691 throw new IllegalArgumentException("Invalid mouse event type: "
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
);
707 * Check if a mouse press/release event coordinate is contained in this
710 * @param mouse a mouse-based event
711 * @return whether or not a mouse click would be sent to this widget
713 public final boolean mouseWouldHit(final TMouseEvent mouse
) {
719 if ((mouse
.getAbsoluteX() >= getAbsoluteX())
720 && (mouse
.getAbsoluteX() < getAbsoluteX() + width
)
721 && (mouse
.getAbsoluteY() >= getAbsoluteY())
722 && (mouse
.getAbsoluteY() < getAbsoluteY() + height
)