AWT minimally working
[fanfix.git] / src / jexer / TApplication.java
CommitLineData
7d4115a5 1/**
7b5261bc 2 * Jexer - Java Text User Interface
7d4115a5
KL
3 *
4 * License: LGPLv3 or later
5 *
7b5261bc
KL
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.
7d4115a5
KL
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
7b5261bc
KL
27 *
28 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
29 * @version 1
7d4115a5
KL
30 */
31package jexer;
32
4328bb42
KL
33import java.io.InputStream;
34import java.io.OutputStream;
35import java.io.UnsupportedEncodingException;
a06459bd 36import java.util.Collections;
d502a0e9 37import java.util.Date;
e826b451 38import java.util.HashMap;
c6940ed9 39import java.util.ArrayList;
4328bb42
KL
40import java.util.LinkedList;
41import java.util.List;
e826b451 42import java.util.Map;
4328bb42
KL
43
44import jexer.bits.CellAttributes;
45import jexer.bits.ColorTheme;
46import jexer.bits.GraphicsChars;
47import jexer.event.TCommandEvent;
48import jexer.event.TInputEvent;
49import jexer.event.TKeypressEvent;
fca67db0 50import jexer.event.TMenuEvent;
4328bb42
KL
51import jexer.event.TMouseEvent;
52import jexer.event.TResizeEvent;
53import jexer.backend.Backend;
1ac2ccb1 54import jexer.backend.AWTBackend;
4328bb42 55import jexer.backend.ECMA48Backend;
48e27807 56import jexer.io.Screen;
928811d8
KL
57import jexer.menu.TMenu;
58import jexer.menu.TMenuItem;
4328bb42
KL
59import static jexer.TCommand.*;
60import static jexer.TKeypress.*;
61
7d4115a5
KL
62/**
63 * TApplication sets up a full Text User Interface application.
64 */
65public class TApplication {
66
c6940ed9
KL
67 /**
68 * WidgetEventHandler is the main event consumer loop. There are at most
69 * two such threads in existence: the primary for normal case and a
70 * secondary that is used for TMessageBox, TInputBox, and similar.
71 */
72 private class WidgetEventHandler implements Runnable {
73 /**
74 * The main application.
75 */
76 private TApplication application;
77
78 /**
79 * Whether or not this WidgetEventHandler is the primary or secondary
80 * thread.
81 */
82 private boolean primary = true;
83
84 /**
85 * Public constructor.
86 *
87 * @param application the main application
88 * @param primary if true, this is the primary event handler thread
89 */
90 public WidgetEventHandler(final TApplication application,
91 final boolean primary) {
92
93 this.application = application;
94 this.primary = primary;
95 }
96
97 /**
98 * The consumer loop.
99 */
100 public void run() {
101
102 // Loop forever
103 while (!application.quit) {
104
105 // Wait until application notifies me
106 while (!application.quit) {
107 try {
108 synchronized (application.drainEventQueue) {
109 if (application.drainEventQueue.size() > 0) {
110 break;
111 }
112 }
113 synchronized (application) {
114 application.wait();
115 if ((!primary)
116 && (application.secondaryEventReceiver == null)
117 ) {
118 // Secondary thread, time to exit
119 return;
120 }
121 break;
122 }
123 } catch (InterruptedException e) {
124 // SQUASH
125 }
126 }
127
128 // Pull all events off the queue
129 for (;;) {
130 TInputEvent event = null;
131 synchronized (application.drainEventQueue) {
132 if (application.drainEventQueue.size() == 0) {
133 break;
134 }
135 event = application.drainEventQueue.remove(0);
136 }
137 if (primary) {
138 primaryHandleEvent(event);
139 } else {
140 secondaryHandleEvent(event);
141 }
142 if ((!primary)
143 && (application.secondaryEventReceiver == null)
144 ) {
145 // Secondary thread, time to exit
146 return;
147 }
148 }
149 } // while (true) (main runnable loop)
150 }
151 }
152
153 /**
154 * The primary event handler thread.
155 */
156 private WidgetEventHandler primaryEventHandler;
157
158 /**
159 * The secondary event handler thread.
160 */
161 private WidgetEventHandler secondaryEventHandler;
162
163 /**
164 * The widget receiving events from the secondary event handler thread.
165 */
166 private TWidget secondaryEventReceiver;
167
7d4115a5 168 /**
4328bb42
KL
169 * Access to the physical screen, keyboard, and mouse.
170 */
7b5261bc 171 private Backend backend;
4328bb42 172
48e27807
KL
173 /**
174 * Get the Screen.
175 *
176 * @return the Screen
177 */
178 public final Screen getScreen() {
179 return backend.getScreen();
180 }
181
4328bb42 182 /**
7b5261bc 183 * Actual mouse coordinate X.
4328bb42
KL
184 */
185 private int mouseX;
186
187 /**
7b5261bc 188 * Actual mouse coordinate Y.
4328bb42
KL
189 */
190 private int mouseY;
191
192 /**
8e688b92 193 * Event queue that is filled by run().
4328bb42 194 */
8e688b92
KL
195 private List<TInputEvent> fillEventQueue;
196
197 /**
198 * Event queue that will be drained by either primary or secondary
199 * Thread.
200 */
201 private List<TInputEvent> drainEventQueue;
4328bb42 202
fca67db0
KL
203 /**
204 * Top-level menus in this application.
205 */
206 private List<TMenu> menus;
207
208 /**
209 * Stack of activated sub-menus in this application.
210 */
211 private List<TMenu> subMenus;
212
213 /**
214 * The currently acive menu.
215 */
216 private TMenu activeMenu = null;
217
e826b451
KL
218 /**
219 * Active keyboard accelerators.
220 */
221 private Map<TKeypress, TMenuItem> accelerators;
222
4328bb42
KL
223 /**
224 * Windows and widgets pull colors from this ColorTheme.
225 */
7b5261bc
KL
226 private ColorTheme theme;
227
228 /**
229 * Get the color theme.
230 *
231 * @return the theme
232 */
233 public final ColorTheme getTheme() {
234 return theme;
235 }
4328bb42 236
a06459bd
KL
237 /**
238 * The top-level windows (but not menus).
239 */
fca67db0 240 private List<TWindow> windows;
a06459bd 241
d502a0e9
KL
242 /**
243 * Timers that are being ticked.
244 */
245 private List<TTimer> timers;
246
4328bb42
KL
247 /**
248 * When true, exit the application.
249 */
48e27807 250 private boolean quit = false;
4328bb42
KL
251
252 /**
253 * When true, repaint the entire screen.
254 */
48e27807
KL
255 private boolean repaint = true;
256
257 /**
258 * Request full repaint on next screen refresh.
259 */
fca67db0 260 public final void setRepaint() {
48e27807
KL
261 repaint = true;
262 }
4328bb42
KL
263
264 /**
265 * When true, just flush updates from the screen.
7d4115a5 266 */
48e27807 267 private boolean flush = false;
4328bb42
KL
268
269 /**
7b5261bc
KL
270 * Y coordinate of the top edge of the desktop. For now this is a
271 * constant. Someday it would be nice to have a multi-line menu or
272 * toolbars.
4328bb42 273 */
48e27807
KL
274 private static final int desktopTop = 1;
275
276 /**
277 * Get Y coordinate of the top edge of the desktop.
278 *
279 * @return Y coordinate of the top edge of the desktop
280 */
281 public final int getDesktopTop() {
282 return desktopTop;
283 }
4328bb42
KL
284
285 /**
286 * Y coordinate of the bottom edge of the desktop.
287 */
48e27807
KL
288 private int desktopBottom;
289
290 /**
291 * Get Y coordinate of the bottom edge of the desktop.
292 *
293 * @return Y coordinate of the bottom edge of the desktop
294 */
295 public final int getDesktopBottom() {
296 return desktopBottom;
297 }
4328bb42
KL
298
299 /**
300 * Public constructor.
301 *
302 * @param input an InputStream connected to the remote user, or null for
303 * System.in. If System.in is used, then on non-Windows systems it will
304 * be put in raw mode; shutdown() will (blindly!) put System.in in cooked
305 * mode. input is always converted to a Reader with UTF-8 encoding.
306 * @param output an OutputStream connected to the remote user, or null
307 * for System.out. output is always converted to a Writer with UTF-8
308 * encoding.
7b5261bc
KL
309 * @throws UnsupportedEncodingException if an exception is thrown when
310 * creating the InputStreamReader
4328bb42 311 */
7b5261bc
KL
312 public TApplication(final InputStream input,
313 final OutputStream output) throws UnsupportedEncodingException {
4328bb42 314
30bd4abd
KL
315 // AWT is the default backend on Windows unless explicitly overridden
316 // by jexer.AWT.
317 boolean useAWT = false;
318 if (System.getProperty("os.name").startsWith("Windows")) {
319 useAWT = true;
320 }
321 if (System.getProperty("jexer.AWT") != null) {
322 if (System.getProperty("jexer.AWT", "false").equals("true")) {
323 useAWT = true;
324 } else {
325 useAWT = false;
326 }
327 }
328
329
330 if (useAWT) {
1ac2ccb1
KL
331 backend = new AWTBackend();
332 } else {
333 backend = new ECMA48Backend(input, output);
334 }
8e688b92
KL
335 theme = new ColorTheme();
336 desktopBottom = getScreen().getHeight() - 1;
c6940ed9
KL
337 fillEventQueue = new ArrayList<TInputEvent>();
338 drainEventQueue = new ArrayList<TInputEvent>();
8e688b92
KL
339 windows = new LinkedList<TWindow>();
340 menus = new LinkedList<TMenu>();
341 subMenus = new LinkedList<TMenu>();
d502a0e9 342 timers = new LinkedList<TTimer>();
e826b451 343 accelerators = new HashMap<TKeypress, TMenuItem>();
c6940ed9
KL
344
345 // Setup the main consumer thread
346 primaryEventHandler = new WidgetEventHandler(this, true);
347 (new Thread(primaryEventHandler)).start();
4328bb42
KL
348 }
349
350 /**
351 * Invert the cell at the mouse pointer position.
352 */
353 private void drawMouse() {
a06459bd 354 CellAttributes attr = getScreen().getAttrXY(mouseX, mouseY);
7b5261bc
KL
355 attr.setForeColor(attr.getForeColor().invert());
356 attr.setBackColor(attr.getBackColor().invert());
a06459bd 357 getScreen().putAttrXY(mouseX, mouseY, attr, false);
7b5261bc
KL
358 flush = true;
359
a06459bd 360 if (windows.size() == 0) {
7b5261bc
KL
361 repaint = true;
362 }
4328bb42
KL
363 }
364
365 /**
366 * Draw everything.
367 */
7b5261bc
KL
368 public final void drawAll() {
369 if ((flush) && (!repaint)) {
370 backend.flushScreen();
371 flush = false;
372 return;
373 }
374
375 if (!repaint) {
376 return;
377 }
378
379 // If true, the cursor is not visible
380 boolean cursor = false;
381
382 // Start with a clean screen
a06459bd 383 getScreen().clear();
7b5261bc
KL
384
385 // Draw the background
386 CellAttributes background = theme.getColor("tapplication.background");
a06459bd 387 getScreen().putAll(GraphicsChars.HATCH, background);
7b5261bc 388
7b5261bc 389 // Draw each window in reverse Z order
a06459bd
KL
390 List<TWindow> sorted = new LinkedList<TWindow>(windows);
391 Collections.sort(sorted);
392 Collections.reverse(sorted);
393 for (TWindow window: sorted) {
394 window.drawChildren();
7b5261bc
KL
395 }
396
397 // Draw the blank menubar line - reset the screen clipping first so
398 // it won't trim it out.
a06459bd
KL
399 getScreen().resetClipping();
400 getScreen().hLineXY(0, 0, getScreen().getWidth(), ' ',
7b5261bc
KL
401 theme.getColor("tmenu"));
402 // Now draw the menus.
403 int x = 1;
fca67db0 404 for (TMenu menu: menus) {
7b5261bc
KL
405 CellAttributes menuColor;
406 CellAttributes menuMnemonicColor;
fca67db0 407 if (menu.getActive()) {
7b5261bc
KL
408 menuColor = theme.getColor("tmenu.highlighted");
409 menuMnemonicColor = theme.getColor("tmenu.mnemonic.highlighted");
410 } else {
411 menuColor = theme.getColor("tmenu");
412 menuMnemonicColor = theme.getColor("tmenu.mnemonic");
413 }
414 // Draw the menu title
fca67db0 415 getScreen().hLineXY(x, 0, menu.getTitle().length() + 2, ' ',
7b5261bc 416 menuColor);
fca67db0 417 getScreen().putStrXY(x + 1, 0, menu.getTitle(), menuColor);
7b5261bc 418 // Draw the highlight character
fca67db0
KL
419 getScreen().putCharXY(x + 1 + menu.getMnemonic().getShortcutIdx(),
420 0, menu.getMnemonic().getShortcut(), menuMnemonicColor);
7b5261bc 421
fca67db0 422 if (menu.getActive()) {
a06459bd 423 menu.drawChildren();
7b5261bc 424 // Reset the screen clipping so we can draw the next title.
a06459bd 425 getScreen().resetClipping();
7b5261bc 426 }
fca67db0 427 x += menu.getTitle().length() + 2;
7b5261bc
KL
428 }
429
a06459bd 430 for (TMenu menu: subMenus) {
7b5261bc 431 // Reset the screen clipping so we can draw the next sub-menu.
a06459bd
KL
432 getScreen().resetClipping();
433 menu.drawChildren();
7b5261bc 434 }
7b5261bc
KL
435
436 // Draw the mouse pointer
437 drawMouse();
438
7b5261bc
KL
439 // Place the cursor if it is visible
440 TWidget activeWidget = null;
a06459bd
KL
441 if (sorted.size() > 0) {
442 activeWidget = sorted.get(sorted.size() - 1).getActiveChild();
443 if (activeWidget.visibleCursor()) {
444 getScreen().putCursor(true, activeWidget.getCursorAbsoluteX(),
7b5261bc
KL
445 activeWidget.getCursorAbsoluteY());
446 cursor = true;
447 }
448 }
449
450 // Kill the cursor
fca67db0 451 if (!cursor) {
a06459bd 452 getScreen().hideCursor();
7b5261bc 453 }
7b5261bc
KL
454
455 // Flush the screen contents
456 backend.flushScreen();
457
458 repaint = false;
459 flush = false;
4328bb42
KL
460 }
461
462 /**
7b5261bc 463 * Run this application until it exits.
4328bb42
KL
464 */
465 public final void run() {
7b5261bc
KL
466 while (!quit) {
467 // Timeout is in milliseconds, so default timeout after 1 second
468 // of inactivity.
469 int timeout = getSleepTime(1000);
470
8e688b92
KL
471 // See if there are any definitely events waiting to be processed
472 // or a screen redraw to do. If so, do not wait if there is no
473 // I/O coming in.
474 synchronized (drainEventQueue) {
475 if (drainEventQueue.size() > 0) {
476 timeout = 0;
477 }
478 }
479 synchronized (fillEventQueue) {
480 if (fillEventQueue.size() > 0) {
481 timeout = 0;
482 }
7b5261bc
KL
483 }
484
8e688b92
KL
485 // Pull any pending I/O events
486 backend.getEvents(fillEventQueue, timeout);
487
488 // Dispatch each event to the appropriate handler, one at a time.
489 for (;;) {
490 TInputEvent event = null;
491 synchronized (fillEventQueue) {
492 if (fillEventQueue.size() == 0) {
493 break;
494 }
495 event = fillEventQueue.remove(0);
496 }
497 metaHandleEvent(event);
498 }
7b5261bc
KL
499
500 // Process timers and call doIdle()'s
501 doIdle();
502
503 // Update the screen
504 drawAll();
505 }
506
c6940ed9
KL
507 // Shutdown the consumer threads
508 synchronized (this) {
509 this.notifyAll();
7b5261bc
KL
510 }
511
7b5261bc 512 backend.shutdown();
4328bb42
KL
513 }
514
515 /**
516 * Peek at certain application-level events, add to eventQueue, and wake
8e688b92 517 * up the consuming Thread.
4328bb42 518 *
8e688b92 519 * @param event the input event to consume
4328bb42 520 */
8e688b92 521 private void metaHandleEvent(final TInputEvent event) {
7b5261bc 522
8e688b92 523 /*
e826b451
KL
524 System.err.printf(String.format("metaHandleEvents event: %s\n",
525 event)); System.err.flush();
8e688b92 526 */
7b5261bc 527
8e688b92
KL
528 if (quit) {
529 // Do no more processing if the application is already trying
530 // to exit.
531 return;
532 }
7b5261bc 533
8e688b92 534 // Special application-wide events -------------------------------
7b5261bc 535
8e688b92
KL
536 // Abort everything
537 if (event instanceof TCommandEvent) {
538 TCommandEvent command = (TCommandEvent) event;
539 if (command.getCmd().equals(cmAbort)) {
540 quit = true;
541 return;
7b5261bc 542 }
8e688b92 543 }
7b5261bc 544
8e688b92
KL
545 // Screen resize
546 if (event instanceof TResizeEvent) {
547 TResizeEvent resize = (TResizeEvent) event;
548 getScreen().setDimensions(resize.getWidth(),
549 resize.getHeight());
550 desktopBottom = getScreen().getHeight() - 1;
551 repaint = true;
552 mouseX = 0;
553 mouseY = 0;
554 return;
555 }
7b5261bc 556
8e688b92
KL
557 // Peek at the mouse position
558 if (event instanceof TMouseEvent) {
559 TMouseEvent mouse = (TMouseEvent) event;
560 if ((mouseX != mouse.getX()) || (mouseY != mouse.getY())) {
561 mouseX = mouse.getX();
562 mouseY = mouse.getY();
563 drawMouse();
7b5261bc 564 }
8e688b92 565 }
7b5261bc 566
8e688b92 567 // Put into the main queue
c6940ed9
KL
568 synchronized (drainEventQueue) {
569 drainEventQueue.add(event);
570 }
4328bb42 571
c6940ed9
KL
572 // Wake all threads: primary thread will either be consuming events
573 // again or waiting in yield(), and secondary thread will either not
574 // exist or consuming events.
575 synchronized (this) {
576 this.notifyAll();
577 }
4328bb42
KL
578 }
579
a06459bd
KL
580 /**
581 * Dispatch one event to the appropriate widget or application-level
fca67db0
KL
582 * event handler. This is the primary event handler, it has the normal
583 * application-wide event handling.
a06459bd
KL
584 *
585 * @param event the input event to consume
fca67db0 586 * @see #secondaryHandleEvent(TInputEvent event)
a06459bd 587 */
fca67db0
KL
588 private void primaryHandleEvent(final TInputEvent event) {
589
590 // System.err.printf("Handle event: %s\n", event);
591
592 // Special application-wide events -----------------------------------
593
594 // Peek at the mouse position
595 if (event instanceof TMouseEvent) {
596 // See if we need to switch focus to another window or the menu
597 checkSwitchFocus((TMouseEvent) event);
598 }
599
600 // Handle menu events
601 if ((activeMenu != null) && !(event instanceof TCommandEvent)) {
602 TMenu menu = activeMenu;
603
604 if (event instanceof TMouseEvent) {
605 TMouseEvent mouse = (TMouseEvent) event;
606
607 while (subMenus.size() > 0) {
608 TMenu subMenu = subMenus.get(subMenus.size() - 1);
609 if (subMenu.mouseWouldHit(mouse)) {
610 break;
611 }
612 if ((mouse.getType() == TMouseEvent.Type.MOUSE_MOTION)
613 && (!mouse.getMouse1())
614 && (!mouse.getMouse2())
615 && (!mouse.getMouse3())
616 && (!mouse.getMouseWheelUp())
617 && (!mouse.getMouseWheelDown())
618 ) {
619 break;
620 }
621 // We navigated away from a sub-menu, so close it
622 closeSubMenu();
623 }
624
625 // Convert the mouse relative x/y to menu coordinates
626 assert (mouse.getX() == mouse.getAbsoluteX());
627 assert (mouse.getY() == mouse.getAbsoluteY());
628 if (subMenus.size() > 0) {
629 menu = subMenus.get(subMenus.size() - 1);
630 }
631 mouse.setX(mouse.getX() - menu.getX());
632 mouse.setY(mouse.getY() - menu.getY());
633 }
634 menu.handleEvent(event);
635 return;
636 }
a06459bd 637
fca67db0
KL
638 if (event instanceof TKeypressEvent) {
639 TKeypressEvent keypress = (TKeypressEvent) event;
e826b451 640
fca67db0
KL
641 // See if this key matches an accelerator, and if so dispatch the
642 // menu event.
643 TKeypress keypressLowercase = keypress.getKey().toLowerCase();
e826b451
KL
644 TMenuItem item = null;
645 synchronized (accelerators) {
646 item = accelerators.get(keypressLowercase);
647 }
fca67db0
KL
648 if (item != null) {
649 // Let the menu item dispatch
650 item.dispatch();
651 return;
652 } else {
653 // Handle the keypress
654 if (onKeypress(keypress)) {
655 return;
656 }
657 }
658 }
a06459bd 659
fca67db0
KL
660 if (event instanceof TCommandEvent) {
661 if (onCommand((TCommandEvent) event)) {
662 return;
663 }
664 }
665
666 if (event instanceof TMenuEvent) {
667 if (onMenu((TMenuEvent) event)) {
668 return;
669 }
670 }
671
672 // Dispatch events to the active window -------------------------------
673 for (TWindow window: windows) {
674 if (window.getActive()) {
a06459bd
KL
675 if (event instanceof TMouseEvent) {
676 TMouseEvent mouse = (TMouseEvent) event;
fca67db0
KL
677 // Convert the mouse relative x/y to window coordinates
678 assert (mouse.getX() == mouse.getAbsoluteX());
679 assert (mouse.getY() == mouse.getAbsoluteY());
680 mouse.setX(mouse.getX() - window.getX());
681 mouse.setY(mouse.getY() - window.getY());
682 }
683 // System.err("TApplication dispatch event: %s\n", event);
684 window.handleEvent(event);
685 break;
686 }
687 }
688 }
689 /**
690 * Dispatch one event to the appropriate widget or application-level
691 * event handler. This is the secondary event handler used by certain
692 * special dialogs (currently TMessageBox and TFileOpenBox).
693 *
694 * @param event the input event to consume
695 * @see #primaryHandleEvent(TInputEvent event)
696 */
697 private void secondaryHandleEvent(final TInputEvent event) {
c6940ed9
KL
698 secondaryEventReceiver.handleEvent(event);
699 }
700
701 /**
702 * Enable a widget to override the primary event thread.
703 *
704 * @param widget widget that will receive events
705 */
706 public final void enableSecondaryEventReceiver(final TWidget widget) {
707 assert (secondaryEventReceiver == null);
708 assert (secondaryEventHandler == null);
709 assert (widget instanceof TMessageBox);
710 secondaryEventReceiver = widget;
711 secondaryEventHandler = new WidgetEventHandler(this, false);
712 (new Thread(secondaryEventHandler)).start();
713
714 // Refresh
715 repaint = true;
716 }
717
718 /**
719 * Yield to the secondary thread.
720 */
721 public final void yield() {
722 assert (secondaryEventReceiver != null);
723 while (secondaryEventReceiver != null) {
724 synchronized (this) {
725 try {
726 this.wait();
727 } catch (InterruptedException e) {
728 // SQUASH
729 }
730 }
731 }
a06459bd
KL
732 }
733
4328bb42
KL
734 /**
735 * Do stuff when there is no user input.
736 */
737 private void doIdle() {
7b5261bc 738 // Now run any timers that have timed out
d502a0e9
KL
739 Date now = new Date();
740 List<TTimer> keepTimers = new LinkedList<TTimer>();
741 for (TTimer timer: timers) {
742 if (timer.getNextTick().getTime() < now.getTime()) {
743 timer.tick();
c6940ed9 744 if (timer.recurring) {
d502a0e9 745 keepTimers.add(timer);
7b5261bc
KL
746 }
747 } else {
d502a0e9 748 keepTimers.add(timer);
7b5261bc
KL
749 }
750 }
751 timers = keepTimers;
752
753 // Call onIdle's
d502a0e9
KL
754 for (TWindow window: windows) {
755 window.onIdle();
7b5261bc 756 }
4328bb42 757 }
7d4115a5 758
4328bb42
KL
759 /**
760 * Get the amount of time I can sleep before missing a Timer tick.
761 *
762 * @param timeout = initial (maximum) timeout
763 * @return number of milliseconds between now and the next timer event
764 */
7b5261bc 765 protected int getSleepTime(final int timeout) {
d502a0e9
KL
766 Date now = new Date();
767 long sleepTime = timeout;
768 for (TTimer timer: timers) {
769 if (timer.getNextTick().getTime() < now.getTime()) {
7b5261bc
KL
770 return 0;
771 }
d502a0e9
KL
772 if ((timer.getNextTick().getTime() > now.getTime())
773 && ((timer.getNextTick().getTime() - now.getTime()) < sleepTime)
7b5261bc 774 ) {
d502a0e9 775 sleepTime = timer.getNextTick().getTime() - now.getTime();
7b5261bc
KL
776 }
777 }
d502a0e9
KL
778 assert (sleepTime >= 0);
779 return (int)sleepTime;
7d4115a5 780 }
4328bb42 781
48e27807
KL
782 /**
783 * Close window. Note that the window's destructor is NOT called by this
784 * method, instead the GC is assumed to do the cleanup.
785 *
786 * @param window the window to remove
787 */
788 public final void closeWindow(final TWindow window) {
fca67db0
KL
789 int z = window.getZ();
790 window.setZ(-1);
791 Collections.sort(windows);
792 windows.remove(0);
48e27807 793 TWindow activeWindow = null;
fca67db0
KL
794 for (TWindow w: windows) {
795 if (w.getZ() > z) {
796 w.setZ(w.getZ() - 1);
797 if (w.getZ() == 0) {
798 w.setActive(true);
799 assert (activeWindow == null);
48e27807
KL
800 activeWindow = w;
801 } else {
fca67db0 802 w.setActive(false);
48e27807
KL
803 }
804 }
805 }
806
807 // Perform window cleanup
808 window.onClose();
809
810 // Refresh screen
811 repaint = true;
812
813 // Check if we are closing a TMessageBox or similar
c6940ed9
KL
814 if (secondaryEventReceiver != null) {
815 assert (secondaryEventHandler != null);
48e27807
KL
816
817 // Do not send events to the secondaryEventReceiver anymore, the
818 // window is closed.
819 secondaryEventReceiver = null;
820
c6940ed9
KL
821 // Wake all threads: primary thread will be consuming events
822 // again, and secondary thread will exit.
823 synchronized (this) {
824 this.notifyAll();
48e27807
KL
825 }
826 }
48e27807
KL
827 }
828
829 /**
830 * Switch to the next window.
831 *
832 * @param forward if true, then switch to the next window in the list,
833 * otherwise switch to the previous window in the list
834 */
835 public final void switchWindow(final boolean forward) {
48e27807 836 // Only switch if there are multiple windows
fca67db0 837 if (windows.size() < 2) {
48e27807
KL
838 return;
839 }
840
fca67db0
KL
841 // Swap z/active between active window and the next in the list
842 int activeWindowI = -1;
843 for (int i = 0; i < windows.size(); i++) {
844 if (windows.get(i).getActive()) {
48e27807
KL
845 activeWindowI = i;
846 break;
847 }
848 }
fca67db0 849 assert (activeWindowI >= 0);
48e27807
KL
850
851 // Do not switch if a window is modal
fca67db0 852 if (windows.get(activeWindowI).isModal()) {
48e27807
KL
853 return;
854 }
855
fca67db0 856 int nextWindowI;
48e27807 857 if (forward) {
fca67db0 858 nextWindowI = (activeWindowI + 1) % windows.size();
48e27807
KL
859 } else {
860 if (activeWindowI == 0) {
fca67db0 861 nextWindowI = windows.size() - 1;
48e27807
KL
862 } else {
863 nextWindowI = activeWindowI - 1;
864 }
865 }
fca67db0
KL
866 windows.get(activeWindowI).setActive(false);
867 windows.get(activeWindowI).setZ(windows.get(nextWindowI).getZ());
868 windows.get(nextWindowI).setZ(0);
869 windows.get(nextWindowI).setActive(true);
48e27807
KL
870
871 // Refresh
872 repaint = true;
48e27807
KL
873 }
874
875 /**
876 * Add a window to my window list and make it active.
877 *
878 * @param window new window to add
879 */
880 public final void addWindow(final TWindow window) {
48e27807 881 // Do not allow a modal window to spawn a non-modal window
a06459bd
KL
882 if ((windows.size() > 0) && (windows.get(0).isModal())) {
883 assert (window.isModal());
48e27807 884 }
a06459bd 885 for (TWindow w: windows) {
fca67db0 886 w.setActive(false);
a06459bd 887 w.setZ(w.getZ() + 1);
48e27807 888 }
a06459bd 889 windows.add(window);
fca67db0 890 window.setActive(true);
a06459bd 891 window.setZ(0);
48e27807
KL
892 }
893
fca67db0
KL
894 /**
895 * Check if there is a system-modal window on top.
896 *
897 * @return true if the active window is modal
898 */
899 private boolean modalWindowActive() {
900 if (windows.size() == 0) {
901 return false;
902 }
903 return windows.get(windows.size() - 1).isModal();
904 }
905
906 /**
907 * Check if a mouse event would hit either the active menu or any open
908 * sub-menus.
909 *
910 * @param mouse mouse event
911 * @return true if the mouse would hit the active menu or an open
912 * sub-menu
913 */
914 private boolean mouseOnMenu(final TMouseEvent mouse) {
915 assert (activeMenu != null);
916 List<TMenu> menus = new LinkedList<TMenu>(subMenus);
917 Collections.reverse(menus);
918 for (TMenu menu: menus) {
919 if (menu.mouseWouldHit(mouse)) {
920 return true;
921 }
922 }
923 return activeMenu.mouseWouldHit(mouse);
924 }
925
926 /**
927 * See if we need to switch window or activate the menu based on
928 * a mouse click.
929 *
930 * @param mouse mouse event
931 */
932 private void checkSwitchFocus(final TMouseEvent mouse) {
933
934 if ((mouse.getType() == TMouseEvent.Type.MOUSE_DOWN)
935 && (activeMenu != null)
936 && (mouse.getAbsoluteY() != 0)
937 && (!mouseOnMenu(mouse))
938 ) {
939 // They clicked outside the active menu, turn it off
940 activeMenu.setActive(false);
941 activeMenu = null;
942 for (TMenu menu: subMenus) {
943 menu.setActive(false);
944 }
945 subMenus.clear();
946 // Continue checks
947 }
948
949 // See if they hit the menu bar
950 if ((mouse.getType() == TMouseEvent.Type.MOUSE_DOWN)
951 && (mouse.getMouse1())
952 && (!modalWindowActive())
953 && (mouse.getAbsoluteY() == 0)
954 ) {
955
956 for (TMenu menu: subMenus) {
957 menu.setActive(false);
958 }
959 subMenus.clear();
960
961 // They selected the menu, go activate it
962 for (TMenu menu: menus) {
963 if ((mouse.getAbsoluteX() >= menu.getX())
964 && (mouse.getAbsoluteX() < menu.getX()
965 + menu.getTitle().length() + 2)
966 ) {
967 menu.setActive(true);
968 activeMenu = menu;
969 } else {
970 menu.setActive(false);
971 }
972 }
973 repaint = true;
974 return;
975 }
976
977 // See if they hit the menu bar
978 if ((mouse.getType() == TMouseEvent.Type.MOUSE_MOTION)
979 && (mouse.getMouse1())
980 && (activeMenu != null)
981 && (mouse.getAbsoluteY() == 0)
982 ) {
983
984 TMenu oldMenu = activeMenu;
985 for (TMenu menu: subMenus) {
986 menu.setActive(false);
987 }
988 subMenus.clear();
989
990 // See if we should switch menus
991 for (TMenu menu: menus) {
992 if ((mouse.getAbsoluteX() >= menu.getX())
993 && (mouse.getAbsoluteX() < menu.getX()
994 + menu.getTitle().length() + 2)
995 ) {
996 menu.setActive(true);
997 activeMenu = menu;
998 }
999 }
1000 if (oldMenu != activeMenu) {
1001 // They switched menus
1002 oldMenu.setActive(false);
1003 }
1004 repaint = true;
1005 return;
1006 }
1007
1008 // Only switch if there are multiple windows
1009 if (windows.size() < 2) {
1010 return;
1011 }
1012
1013 // Switch on the upclick
1014 if (mouse.getType() != TMouseEvent.Type.MOUSE_UP) {
1015 return;
1016 }
1017
1018 Collections.sort(windows);
1019 if (windows.get(0).isModal()) {
1020 // Modal windows don't switch
1021 return;
1022 }
1023
1024 for (TWindow window: windows) {
1025 assert (!window.isModal());
1026 if (window.mouseWouldHit(mouse)) {
1027 if (window == windows.get(0)) {
1028 // Clicked on the same window, nothing to do
1029 return;
1030 }
1031
1032 // We will be switching to another window
1033 assert (windows.get(0).getActive());
1034 assert (!window.getActive());
1035 windows.get(0).setActive(false);
1036 windows.get(0).setZ(window.getZ());
1037 window.setZ(0);
1038 window.setActive(true);
1039 repaint = true;
1040 return;
1041 }
1042 }
1043
1044 // Clicked on the background, nothing to do
1045 return;
1046 }
1047
1048 /**
1049 * Turn off the menu.
1050 */
928811d8 1051 public final void closeMenu() {
fca67db0
KL
1052 if (activeMenu != null) {
1053 activeMenu.setActive(false);
1054 activeMenu = null;
1055 for (TMenu menu: subMenus) {
1056 menu.setActive(false);
1057 }
1058 subMenus.clear();
1059 }
1060 repaint = true;
1061 }
1062
1063 /**
1064 * Turn off a sub-menu.
1065 */
928811d8 1066 public final void closeSubMenu() {
fca67db0
KL
1067 assert (activeMenu != null);
1068 TMenu item = subMenus.get(subMenus.size() - 1);
1069 assert (item != null);
1070 item.setActive(false);
1071 subMenus.remove(subMenus.size() - 1);
1072 repaint = true;
1073 }
1074
1075 /**
1076 * Switch to the next menu.
1077 *
1078 * @param forward if true, then switch to the next menu in the list,
1079 * otherwise switch to the previous menu in the list
1080 */
928811d8 1081 public final void switchMenu(final boolean forward) {
fca67db0
KL
1082 assert (activeMenu != null);
1083
1084 for (TMenu menu: subMenus) {
1085 menu.setActive(false);
1086 }
1087 subMenus.clear();
1088
1089 for (int i = 0; i < menus.size(); i++) {
1090 if (activeMenu == menus.get(i)) {
1091 if (forward) {
1092 if (i < menus.size() - 1) {
1093 i++;
1094 }
1095 } else {
1096 if (i > 0) {
1097 i--;
1098 }
1099 }
1100 activeMenu.setActive(false);
1101 activeMenu = menus.get(i);
1102 activeMenu.setActive(true);
1103 repaint = true;
1104 return;
1105 }
1106 }
1107 }
1108
1109 /**
1110 * Method that TApplication subclasses can override to handle menu or
1111 * posted command events.
1112 *
1113 * @param command command event
1114 * @return if true, this event was consumed
1115 */
1116 protected boolean onCommand(final TCommandEvent command) {
fca67db0
KL
1117 // Default: handle cmExit
1118 if (command.equals(cmExit)) {
1119 if (messageBox("Confirmation", "Exit application?",
c6940ed9 1120 TMessageBox.Type.YESNO).getResult() == TMessageBox.Result.YES) {
fca67db0
KL
1121 quit = true;
1122 }
1123 repaint = true;
1124 return true;
1125 }
1126
c6940ed9
KL
1127 /*
1128 TODO
fca67db0
KL
1129 if (command.equals(cmShell)) {
1130 openTerminal(0, 0, TWindow.Flag.RESIZABLE);
1131 repaint = true;
1132 return true;
1133 }
c6940ed9 1134 */
fca67db0
KL
1135
1136 if (command.equals(cmTile)) {
1137 tileWindows();
1138 repaint = true;
1139 return true;
1140 }
1141 if (command.equals(cmCascade)) {
1142 cascadeWindows();
1143 repaint = true;
1144 return true;
1145 }
1146 if (command.equals(cmCloseAll)) {
1147 closeAllWindows();
1148 repaint = true;
1149 return true;
1150 }
c6940ed9 1151
fca67db0
KL
1152 return false;
1153 }
1154
1155 /**
1156 * Method that TApplication subclasses can override to handle menu
1157 * events.
1158 *
1159 * @param menu menu event
1160 * @return if true, this event was consumed
1161 */
1162 protected boolean onMenu(final TMenuEvent menu) {
1163
fca67db0 1164 // Default: handle MID_EXIT
8e688b92 1165 if (menu.getId() == TMenu.MID_EXIT) {
fca67db0 1166 if (messageBox("Confirmation", "Exit application?",
c6940ed9 1167 TMessageBox.Type.YESNO).getResult() == TMessageBox.Result.YES) {
fca67db0
KL
1168 quit = true;
1169 }
1170 // System.err.printf("onMenu MID_EXIT result: quit = %s\n", quit);
1171 repaint = true;
1172 return true;
1173 }
1174
8e688b92
KL
1175 /*
1176 TODO
fca67db0
KL
1177 if (menu.id == TMenu.MID_SHELL) {
1178 openTerminal(0, 0, TWindow.Flag.RESIZABLE);
1179 repaint = true;
1180 return true;
1181 }
8e688b92 1182 */
fca67db0 1183
8e688b92 1184 if (menu.getId() == TMenu.MID_TILE) {
fca67db0
KL
1185 tileWindows();
1186 repaint = true;
1187 return true;
1188 }
8e688b92 1189 if (menu.getId() == TMenu.MID_CASCADE) {
fca67db0
KL
1190 cascadeWindows();
1191 repaint = true;
1192 return true;
1193 }
8e688b92 1194 if (menu.getId() == TMenu.MID_CLOSE_ALL) {
fca67db0
KL
1195 closeAllWindows();
1196 repaint = true;
1197 return true;
1198 }
fca67db0
KL
1199 return false;
1200 }
1201
1202 /**
1203 * Method that TApplication subclasses can override to handle keystrokes.
1204 *
1205 * @param keypress keystroke event
1206 * @return if true, this event was consumed
1207 */
1208 protected boolean onKeypress(final TKeypressEvent keypress) {
1209 // Default: only menu shortcuts
1210
1211 // Process Alt-F, Alt-E, etc. menu shortcut keys
1212 if (!keypress.getKey().getIsKey()
1213 && keypress.getKey().getAlt()
1214 && !keypress.getKey().getCtrl()
1215 && (activeMenu == null)
1216 ) {
1217
1218 assert (subMenus.size() == 0);
1219
1220 for (TMenu menu: menus) {
1221 if (Character.toLowerCase(menu.getMnemonic().getShortcut())
1222 == Character.toLowerCase(keypress.getKey().getCh())
1223 ) {
1224 activeMenu = menu;
1225 menu.setActive(true);
1226 repaint = true;
1227 return true;
1228 }
1229 }
1230 }
1231
1232 return false;
1233 }
48e27807 1234
928811d8
KL
1235 /**
1236 * Add a keyboard accelerator to the global hash.
1237 *
1238 * @param item menu item this accelerator relates to
1239 * @param keypress keypress that will dispatch a TMenuEvent
1240 */
1241 public final void addAccelerator(final TMenuItem item,
1242 final TKeypress keypress) {
e826b451
KL
1243
1244 // System.err.printf("addAccelerator: key %s item %s\n", keypress, item);
1245
1246 synchronized (accelerators) {
1247 assert (accelerators.get(keypress) == null);
1248 accelerators.put(keypress, item);
1249 }
928811d8
KL
1250 }
1251
1252 /**
1253 * Recompute menu x positions based on their title length.
1254 */
1255 public final void recomputeMenuX() {
1256 int x = 0;
1257 for (TMenu menu: menus) {
1258 menu.setX(x);
1259 x += menu.getTitle().length() + 2;
1260 }
1261 }
1262
1263 /**
1264 * Post an event to process and turn off the menu.
1265 *
1266 * @param event new event to add to the queue
1267 */
1268 public final void addMenuEvent(final TInputEvent event) {
8e688b92
KL
1269 synchronized (fillEventQueue) {
1270 fillEventQueue.add(event);
1271 }
928811d8
KL
1272 closeMenu();
1273 }
1274
1275 /**
1276 * Add a sub-menu to the list of open sub-menus.
1277 *
1278 * @param menu sub-menu
1279 */
1280 public final void addSubMenu(final TMenu menu) {
1281 subMenus.add(menu);
1282 }
1283
8e688b92
KL
1284 /**
1285 * Convenience function to add a top-level menu.
1286 *
1287 * @param title menu title
1288 * @return the new menu
1289 */
1290 public final TMenu addMenu(String title) {
1291 int x = 0;
1292 int y = 0;
1293 TMenu menu = new TMenu(this, x, y, title);
1294 menus.add(menu);
1295 recomputeMenuX();
1296 return menu;
1297 }
1298
1299 /**
1300 * Convenience function to add a default "File" menu.
1301 *
1302 * @return the new menu
1303 */
1304 public final TMenu addFileMenu() {
1305 TMenu fileMenu = addMenu("&File");
1306 fileMenu.addDefaultItem(TMenu.MID_OPEN_FILE);
1307 fileMenu.addSeparator();
1308 fileMenu.addDefaultItem(TMenu.MID_SHELL);
1309 fileMenu.addDefaultItem(TMenu.MID_EXIT);
1310 return fileMenu;
1311 }
1312
1313 /**
1314 * Convenience function to add a default "Edit" menu.
1315 *
1316 * @return the new menu
1317 */
1318 public final TMenu addEditMenu() {
1319 TMenu editMenu = addMenu("&Edit");
1320 editMenu.addDefaultItem(TMenu.MID_CUT);
1321 editMenu.addDefaultItem(TMenu.MID_COPY);
1322 editMenu.addDefaultItem(TMenu.MID_PASTE);
1323 editMenu.addDefaultItem(TMenu.MID_CLEAR);
1324 return editMenu;
1325 }
1326
1327 /**
1328 * Convenience function to add a default "Window" menu.
1329 *
1330 * @return the new menu
1331 */
c6940ed9 1332 public final TMenu addWindowMenu() {
8e688b92
KL
1333 TMenu windowMenu = addMenu("&Window");
1334 windowMenu.addDefaultItem(TMenu.MID_TILE);
1335 windowMenu.addDefaultItem(TMenu.MID_CASCADE);
1336 windowMenu.addDefaultItem(TMenu.MID_CLOSE_ALL);
1337 windowMenu.addSeparator();
1338 windowMenu.addDefaultItem(TMenu.MID_WINDOW_MOVE);
1339 windowMenu.addDefaultItem(TMenu.MID_WINDOW_ZOOM);
1340 windowMenu.addDefaultItem(TMenu.MID_WINDOW_NEXT);
1341 windowMenu.addDefaultItem(TMenu.MID_WINDOW_PREVIOUS);
1342 windowMenu.addDefaultItem(TMenu.MID_WINDOW_CLOSE);
1343 return windowMenu;
1344 }
1345
1346 /**
1347 * Close all open windows.
1348 */
1349 private void closeAllWindows() {
1350 // Don't do anything if we are in the menu
1351 if (activeMenu != null) {
1352 return;
1353 }
1354 for (TWindow window: windows) {
1355 closeWindow(window);
1356 }
1357 }
1358
1359 /**
1360 * Re-layout the open windows as non-overlapping tiles. This produces
1361 * almost the same results as Turbo Pascal 7.0's IDE.
1362 */
1363 private void tileWindows() {
1364 // Don't do anything if we are in the menu
1365 if (activeMenu != null) {
1366 return;
1367 }
1368 int z = windows.size();
1369 if (z == 0) {
1370 return;
1371 }
1372 int a = 0;
1373 int b = 0;
1374 a = (int)(Math.sqrt(z));
1375 int c = 0;
1376 while (c < a) {
1377 b = (z - c) / a;
1378 if (((a * b) + c) == z) {
1379 break;
1380 }
1381 c++;
1382 }
1383 assert (a > 0);
1384 assert (b > 0);
1385 assert (c < a);
1386 int newWidth = (getScreen().getWidth() / a);
1387 int newHeight1 = ((getScreen().getHeight() - 1) / b);
1388 int newHeight2 = ((getScreen().getHeight() - 1) / (b + c));
1389 // System.err.printf("Z %s a %s b %s c %s newWidth %s newHeight1 %s newHeight2 %s",
1390 // z, a, b, c, newWidth, newHeight1, newHeight2);
1391
1392 List<TWindow> sorted = new LinkedList<TWindow>(windows);
1393 Collections.sort(sorted);
1394 Collections.reverse(sorted);
1395 for (int i = 0; i < sorted.size(); i++) {
1396 int logicalX = i / b;
1397 int logicalY = i % b;
1398 if (i >= ((a - 1) * b)) {
1399 logicalX = a - 1;
1400 logicalY = i - ((a - 1) * b);
1401 }
1402
1403 TWindow w = sorted.get(i);
1404 w.setX(logicalX * newWidth);
1405 w.setWidth(newWidth);
1406 if (i >= ((a - 1) * b)) {
1407 w.setY((logicalY * newHeight2) + 1);
1408 w.setHeight(newHeight2);
1409 } else {
1410 w.setY((logicalY * newHeight1) + 1);
1411 w.setHeight(newHeight1);
1412 }
1413 }
1414 }
1415
1416 /**
1417 * Re-layout the open windows as overlapping cascaded windows.
1418 */
1419 private void cascadeWindows() {
1420 // Don't do anything if we are in the menu
1421 if (activeMenu != null) {
1422 return;
1423 }
1424 int x = 0;
1425 int y = 1;
1426 List<TWindow> sorted = new LinkedList<TWindow>(windows);
1427 Collections.sort(sorted);
1428 Collections.reverse(sorted);
d502a0e9
KL
1429 for (TWindow window: sorted) {
1430 window.setX(x);
1431 window.setY(y);
8e688b92
KL
1432 x++;
1433 y++;
1434 if (x > getScreen().getWidth()) {
1435 x = 0;
1436 }
1437 if (y >= getScreen().getHeight()) {
1438 y = 1;
1439 }
1440 }
1441 }
1442
d502a0e9
KL
1443 /**
1444 * Convenience function to add a timer.
1445 *
1446 * @param duration number of milliseconds to wait between ticks
1447 * @param recurring if true, re-schedule this timer after every tick
1448 * @param action function to call when button is pressed
c6940ed9 1449 * @return the timer
d502a0e9
KL
1450 */
1451 public final TTimer addTimer(final long duration, final boolean recurring,
1452 final TAction action) {
1453
1454 TTimer timer = new TTimer(duration, recurring, action);
1455 synchronized (timers) {
1456 timers.add(timer);
1457 }
1458 return timer;
1459 }
1460
1461 /**
1462 * Convenience function to remove a timer.
1463 *
1464 * @param timer timer to remove
1465 */
1466 public final void removeTimer(final TTimer timer) {
1467 synchronized (timers) {
1468 timers.remove(timer);
1469 }
1470 }
1471
c6940ed9
KL
1472 /**
1473 * Convenience function to spawn a message box.
1474 *
1475 * @param title window title, will be centered along the top border
1476 * @param caption message to display. Use embedded newlines to get a
1477 * multi-line box.
1478 * @return the new message box
1479 */
1480 public final TMessageBox messageBox(final String title,
1481 final String caption) {
1482
1483 return new TMessageBox(this, title, caption, TMessageBox.Type.OK);
1484 }
1485
1486 /**
1487 * Convenience function to spawn a message box.
1488 *
1489 * @param title window title, will be centered along the top border
1490 * @param caption message to display. Use embedded newlines to get a
1491 * multi-line box.
1492 * @param type one of the TMessageBox.Type constants. Default is
1493 * Type.OK.
1494 * @return the new message box
1495 */
1496 public final TMessageBox messageBox(final String title,
1497 final String caption, final TMessageBox.Type type) {
1498
1499 return new TMessageBox(this, title, caption, type);
1500 }
1501
1502 /**
1503 * Convenience function to spawn an input box.
1504 *
1505 * @param title window title, will be centered along the top border
1506 * @param caption message to display. Use embedded newlines to get a
1507 * multi-line box.
1508 * @return the new input box
1509 */
1510 public final TInputBox inputBox(final String title, final String caption) {
1511
1512 return new TInputBox(this, title, caption);
1513 }
1514
1515 /**
1516 * Convenience function to spawn an input box.
1517 *
1518 * @param title window title, will be centered along the top border
1519 * @param caption message to display. Use embedded newlines to get a
1520 * multi-line box.
1521 * @param text initial text to seed the field with
1522 * @return the new input box
1523 */
1524 public final TInputBox inputBox(final String title, final String caption,
1525 final String text) {
1526
1527 return new TInputBox(this, title, caption, text);
1528 }
1ac2ccb1 1529
7d4115a5 1530}