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