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