cutting v0.0.1
[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
87a17f3c
KL
504 synchronized (getScreen()) {
505 drawAll();
506 }
7b5261bc
KL
507 }
508
c6940ed9
KL
509 // Shutdown the consumer threads
510 synchronized (this) {
511 this.notifyAll();
7b5261bc
KL
512 }
513
7b5261bc 514 backend.shutdown();
4328bb42
KL
515 }
516
517 /**
518 * Peek at certain application-level events, add to eventQueue, and wake
8e688b92 519 * up the consuming Thread.
4328bb42 520 *
8e688b92 521 * @param event the input event to consume
4328bb42 522 */
8e688b92 523 private void metaHandleEvent(final TInputEvent event) {
7b5261bc 524
8e688b92 525 /*
e826b451
KL
526 System.err.printf(String.format("metaHandleEvents event: %s\n",
527 event)); System.err.flush();
8e688b92 528 */
7b5261bc 529
8e688b92
KL
530 if (quit) {
531 // Do no more processing if the application is already trying
532 // to exit.
533 return;
534 }
7b5261bc 535
8e688b92 536 // Special application-wide events -------------------------------
7b5261bc 537
8e688b92
KL
538 // Abort everything
539 if (event instanceof TCommandEvent) {
540 TCommandEvent command = (TCommandEvent) event;
541 if (command.getCmd().equals(cmAbort)) {
542 quit = true;
543 return;
7b5261bc 544 }
8e688b92 545 }
7b5261bc 546
8e688b92
KL
547 // Screen resize
548 if (event instanceof TResizeEvent) {
549 TResizeEvent resize = (TResizeEvent) event;
550 getScreen().setDimensions(resize.getWidth(),
551 resize.getHeight());
552 desktopBottom = getScreen().getHeight() - 1;
553 repaint = true;
554 mouseX = 0;
555 mouseY = 0;
556 return;
557 }
7b5261bc 558
8e688b92
KL
559 // Peek at the mouse position
560 if (event instanceof TMouseEvent) {
561 TMouseEvent mouse = (TMouseEvent) event;
562 if ((mouseX != mouse.getX()) || (mouseY != mouse.getY())) {
563 mouseX = mouse.getX();
564 mouseY = mouse.getY();
565 drawMouse();
7b5261bc 566 }
8e688b92 567 }
7b5261bc 568
8e688b92 569 // Put into the main queue
c6940ed9
KL
570 synchronized (drainEventQueue) {
571 drainEventQueue.add(event);
572 }
4328bb42 573
c6940ed9
KL
574 // Wake all threads: primary thread will either be consuming events
575 // again or waiting in yield(), and secondary thread will either not
576 // exist or consuming events.
577 synchronized (this) {
578 this.notifyAll();
579 }
4328bb42
KL
580 }
581
a06459bd
KL
582 /**
583 * Dispatch one event to the appropriate widget or application-level
fca67db0
KL
584 * event handler. This is the primary event handler, it has the normal
585 * application-wide event handling.
a06459bd
KL
586 *
587 * @param event the input event to consume
fca67db0 588 * @see #secondaryHandleEvent(TInputEvent event)
a06459bd 589 */
fca67db0
KL
590 private void primaryHandleEvent(final TInputEvent event) {
591
592 // System.err.printf("Handle event: %s\n", event);
593
594 // Special application-wide events -----------------------------------
595
596 // Peek at the mouse position
597 if (event instanceof TMouseEvent) {
598 // See if we need to switch focus to another window or the menu
599 checkSwitchFocus((TMouseEvent) event);
600 }
601
602 // Handle menu events
603 if ((activeMenu != null) && !(event instanceof TCommandEvent)) {
604 TMenu menu = activeMenu;
605
606 if (event instanceof TMouseEvent) {
607 TMouseEvent mouse = (TMouseEvent) event;
608
609 while (subMenus.size() > 0) {
610 TMenu subMenu = subMenus.get(subMenus.size() - 1);
611 if (subMenu.mouseWouldHit(mouse)) {
612 break;
613 }
614 if ((mouse.getType() == TMouseEvent.Type.MOUSE_MOTION)
615 && (!mouse.getMouse1())
616 && (!mouse.getMouse2())
617 && (!mouse.getMouse3())
618 && (!mouse.getMouseWheelUp())
619 && (!mouse.getMouseWheelDown())
620 ) {
621 break;
622 }
623 // We navigated away from a sub-menu, so close it
624 closeSubMenu();
625 }
626
627 // Convert the mouse relative x/y to menu coordinates
628 assert (mouse.getX() == mouse.getAbsoluteX());
629 assert (mouse.getY() == mouse.getAbsoluteY());
630 if (subMenus.size() > 0) {
631 menu = subMenus.get(subMenus.size() - 1);
632 }
633 mouse.setX(mouse.getX() - menu.getX());
634 mouse.setY(mouse.getY() - menu.getY());
635 }
636 menu.handleEvent(event);
637 return;
638 }
a06459bd 639
fca67db0
KL
640 if (event instanceof TKeypressEvent) {
641 TKeypressEvent keypress = (TKeypressEvent) event;
e826b451 642
fca67db0
KL
643 // See if this key matches an accelerator, and if so dispatch the
644 // menu event.
645 TKeypress keypressLowercase = keypress.getKey().toLowerCase();
e826b451
KL
646 TMenuItem item = null;
647 synchronized (accelerators) {
648 item = accelerators.get(keypressLowercase);
649 }
fca67db0
KL
650 if (item != null) {
651 // Let the menu item dispatch
652 item.dispatch();
653 return;
654 } else {
655 // Handle the keypress
656 if (onKeypress(keypress)) {
657 return;
658 }
659 }
660 }
a06459bd 661
fca67db0
KL
662 if (event instanceof TCommandEvent) {
663 if (onCommand((TCommandEvent) event)) {
664 return;
665 }
666 }
667
668 if (event instanceof TMenuEvent) {
669 if (onMenu((TMenuEvent) event)) {
670 return;
671 }
672 }
673
674 // Dispatch events to the active window -------------------------------
675 for (TWindow window: windows) {
676 if (window.getActive()) {
a06459bd
KL
677 if (event instanceof TMouseEvent) {
678 TMouseEvent mouse = (TMouseEvent) event;
fca67db0
KL
679 // Convert the mouse relative x/y to window coordinates
680 assert (mouse.getX() == mouse.getAbsoluteX());
681 assert (mouse.getY() == mouse.getAbsoluteY());
682 mouse.setX(mouse.getX() - window.getX());
683 mouse.setY(mouse.getY() - window.getY());
684 }
685 // System.err("TApplication dispatch event: %s\n", event);
686 window.handleEvent(event);
687 break;
688 }
689 }
690 }
691 /**
692 * Dispatch one event to the appropriate widget or application-level
693 * event handler. This is the secondary event handler used by certain
694 * special dialogs (currently TMessageBox and TFileOpenBox).
695 *
696 * @param event the input event to consume
697 * @see #primaryHandleEvent(TInputEvent event)
698 */
699 private void secondaryHandleEvent(final TInputEvent event) {
c6940ed9
KL
700 secondaryEventReceiver.handleEvent(event);
701 }
702
703 /**
704 * Enable a widget to override the primary event thread.
705 *
706 * @param widget widget that will receive events
707 */
708 public final void enableSecondaryEventReceiver(final TWidget widget) {
709 assert (secondaryEventReceiver == null);
710 assert (secondaryEventHandler == null);
711 assert (widget instanceof TMessageBox);
712 secondaryEventReceiver = widget;
713 secondaryEventHandler = new WidgetEventHandler(this, false);
714 (new Thread(secondaryEventHandler)).start();
715
716 // Refresh
717 repaint = true;
718 }
719
720 /**
721 * Yield to the secondary thread.
722 */
723 public final void yield() {
724 assert (secondaryEventReceiver != null);
725 while (secondaryEventReceiver != null) {
726 synchronized (this) {
727 try {
728 this.wait();
729 } catch (InterruptedException e) {
730 // SQUASH
731 }
732 }
733 }
a06459bd
KL
734 }
735
4328bb42
KL
736 /**
737 * Do stuff when there is no user input.
738 */
739 private void doIdle() {
7b5261bc 740 // Now run any timers that have timed out
d502a0e9
KL
741 Date now = new Date();
742 List<TTimer> keepTimers = new LinkedList<TTimer>();
743 for (TTimer timer: timers) {
744 if (timer.getNextTick().getTime() < now.getTime()) {
745 timer.tick();
c6940ed9 746 if (timer.recurring) {
d502a0e9 747 keepTimers.add(timer);
7b5261bc
KL
748 }
749 } else {
d502a0e9 750 keepTimers.add(timer);
7b5261bc
KL
751 }
752 }
753 timers = keepTimers;
754
755 // Call onIdle's
d502a0e9
KL
756 for (TWindow window: windows) {
757 window.onIdle();
7b5261bc 758 }
4328bb42 759 }
7d4115a5 760
4328bb42
KL
761 /**
762 * Get the amount of time I can sleep before missing a Timer tick.
763 *
764 * @param timeout = initial (maximum) timeout
765 * @return number of milliseconds between now and the next timer event
766 */
7b5261bc 767 protected int getSleepTime(final int timeout) {
d502a0e9
KL
768 Date now = new Date();
769 long sleepTime = timeout;
770 for (TTimer timer: timers) {
771 if (timer.getNextTick().getTime() < now.getTime()) {
7b5261bc
KL
772 return 0;
773 }
d502a0e9
KL
774 if ((timer.getNextTick().getTime() > now.getTime())
775 && ((timer.getNextTick().getTime() - now.getTime()) < sleepTime)
7b5261bc 776 ) {
d502a0e9 777 sleepTime = timer.getNextTick().getTime() - now.getTime();
7b5261bc
KL
778 }
779 }
d502a0e9
KL
780 assert (sleepTime >= 0);
781 return (int)sleepTime;
7d4115a5 782 }
4328bb42 783
48e27807
KL
784 /**
785 * Close window. Note that the window's destructor is NOT called by this
786 * method, instead the GC is assumed to do the cleanup.
787 *
788 * @param window the window to remove
789 */
790 public final void closeWindow(final TWindow window) {
fca67db0
KL
791 int z = window.getZ();
792 window.setZ(-1);
793 Collections.sort(windows);
794 windows.remove(0);
48e27807 795 TWindow activeWindow = null;
fca67db0
KL
796 for (TWindow w: windows) {
797 if (w.getZ() > z) {
798 w.setZ(w.getZ() - 1);
799 if (w.getZ() == 0) {
800 w.setActive(true);
801 assert (activeWindow == null);
48e27807
KL
802 activeWindow = w;
803 } else {
fca67db0 804 w.setActive(false);
48e27807
KL
805 }
806 }
807 }
808
809 // Perform window cleanup
810 window.onClose();
811
812 // Refresh screen
813 repaint = true;
814
815 // Check if we are closing a TMessageBox or similar
c6940ed9
KL
816 if (secondaryEventReceiver != null) {
817 assert (secondaryEventHandler != null);
48e27807
KL
818
819 // Do not send events to the secondaryEventReceiver anymore, the
820 // window is closed.
821 secondaryEventReceiver = null;
822
c6940ed9
KL
823 // Wake all threads: primary thread will be consuming events
824 // again, and secondary thread will exit.
825 synchronized (this) {
826 this.notifyAll();
48e27807
KL
827 }
828 }
48e27807
KL
829 }
830
831 /**
832 * Switch to the next window.
833 *
834 * @param forward if true, then switch to the next window in the list,
835 * otherwise switch to the previous window in the list
836 */
837 public final void switchWindow(final boolean forward) {
48e27807 838 // Only switch if there are multiple windows
fca67db0 839 if (windows.size() < 2) {
48e27807
KL
840 return;
841 }
842
fca67db0
KL
843 // Swap z/active between active window and the next in the list
844 int activeWindowI = -1;
845 for (int i = 0; i < windows.size(); i++) {
846 if (windows.get(i).getActive()) {
48e27807
KL
847 activeWindowI = i;
848 break;
849 }
850 }
fca67db0 851 assert (activeWindowI >= 0);
48e27807
KL
852
853 // Do not switch if a window is modal
fca67db0 854 if (windows.get(activeWindowI).isModal()) {
48e27807
KL
855 return;
856 }
857
fca67db0 858 int nextWindowI;
48e27807 859 if (forward) {
fca67db0 860 nextWindowI = (activeWindowI + 1) % windows.size();
48e27807
KL
861 } else {
862 if (activeWindowI == 0) {
fca67db0 863 nextWindowI = windows.size() - 1;
48e27807
KL
864 } else {
865 nextWindowI = activeWindowI - 1;
866 }
867 }
fca67db0
KL
868 windows.get(activeWindowI).setActive(false);
869 windows.get(activeWindowI).setZ(windows.get(nextWindowI).getZ());
870 windows.get(nextWindowI).setZ(0);
871 windows.get(nextWindowI).setActive(true);
48e27807
KL
872
873 // Refresh
874 repaint = true;
48e27807
KL
875 }
876
877 /**
878 * Add a window to my window list and make it active.
879 *
880 * @param window new window to add
881 */
882 public final void addWindow(final TWindow window) {
48e27807 883 // Do not allow a modal window to spawn a non-modal window
a06459bd
KL
884 if ((windows.size() > 0) && (windows.get(0).isModal())) {
885 assert (window.isModal());
48e27807 886 }
a06459bd 887 for (TWindow w: windows) {
fca67db0 888 w.setActive(false);
a06459bd 889 w.setZ(w.getZ() + 1);
48e27807 890 }
a06459bd 891 windows.add(window);
fca67db0 892 window.setActive(true);
a06459bd 893 window.setZ(0);
48e27807
KL
894 }
895
fca67db0
KL
896 /**
897 * Check if there is a system-modal window on top.
898 *
899 * @return true if the active window is modal
900 */
901 private boolean modalWindowActive() {
902 if (windows.size() == 0) {
903 return false;
904 }
905 return windows.get(windows.size() - 1).isModal();
906 }
907
908 /**
909 * Check if a mouse event would hit either the active menu or any open
910 * sub-menus.
911 *
912 * @param mouse mouse event
913 * @return true if the mouse would hit the active menu or an open
914 * sub-menu
915 */
916 private boolean mouseOnMenu(final TMouseEvent mouse) {
917 assert (activeMenu != null);
918 List<TMenu> menus = new LinkedList<TMenu>(subMenus);
919 Collections.reverse(menus);
920 for (TMenu menu: menus) {
921 if (menu.mouseWouldHit(mouse)) {
922 return true;
923 }
924 }
925 return activeMenu.mouseWouldHit(mouse);
926 }
927
928 /**
929 * See if we need to switch window or activate the menu based on
930 * a mouse click.
931 *
932 * @param mouse mouse event
933 */
934 private void checkSwitchFocus(final TMouseEvent mouse) {
935
936 if ((mouse.getType() == TMouseEvent.Type.MOUSE_DOWN)
937 && (activeMenu != null)
938 && (mouse.getAbsoluteY() != 0)
939 && (!mouseOnMenu(mouse))
940 ) {
941 // They clicked outside the active menu, turn it off
942 activeMenu.setActive(false);
943 activeMenu = null;
944 for (TMenu menu: subMenus) {
945 menu.setActive(false);
946 }
947 subMenus.clear();
948 // Continue checks
949 }
950
951 // See if they hit the menu bar
952 if ((mouse.getType() == TMouseEvent.Type.MOUSE_DOWN)
953 && (mouse.getMouse1())
954 && (!modalWindowActive())
955 && (mouse.getAbsoluteY() == 0)
956 ) {
957
958 for (TMenu menu: subMenus) {
959 menu.setActive(false);
960 }
961 subMenus.clear();
962
963 // They selected the menu, go activate it
964 for (TMenu menu: menus) {
965 if ((mouse.getAbsoluteX() >= menu.getX())
966 && (mouse.getAbsoluteX() < menu.getX()
967 + menu.getTitle().length() + 2)
968 ) {
969 menu.setActive(true);
970 activeMenu = menu;
971 } else {
972 menu.setActive(false);
973 }
974 }
975 repaint = true;
976 return;
977 }
978
979 // See if they hit the menu bar
980 if ((mouse.getType() == TMouseEvent.Type.MOUSE_MOTION)
981 && (mouse.getMouse1())
982 && (activeMenu != null)
983 && (mouse.getAbsoluteY() == 0)
984 ) {
985
986 TMenu oldMenu = activeMenu;
987 for (TMenu menu: subMenus) {
988 menu.setActive(false);
989 }
990 subMenus.clear();
991
992 // See if we should switch menus
993 for (TMenu menu: menus) {
994 if ((mouse.getAbsoluteX() >= menu.getX())
995 && (mouse.getAbsoluteX() < menu.getX()
996 + menu.getTitle().length() + 2)
997 ) {
998 menu.setActive(true);
999 activeMenu = menu;
1000 }
1001 }
1002 if (oldMenu != activeMenu) {
1003 // They switched menus
1004 oldMenu.setActive(false);
1005 }
1006 repaint = true;
1007 return;
1008 }
1009
1010 // Only switch if there are multiple windows
1011 if (windows.size() < 2) {
1012 return;
1013 }
1014
1015 // Switch on the upclick
1016 if (mouse.getType() != TMouseEvent.Type.MOUSE_UP) {
1017 return;
1018 }
1019
1020 Collections.sort(windows);
1021 if (windows.get(0).isModal()) {
1022 // Modal windows don't switch
1023 return;
1024 }
1025
1026 for (TWindow window: windows) {
1027 assert (!window.isModal());
1028 if (window.mouseWouldHit(mouse)) {
1029 if (window == windows.get(0)) {
1030 // Clicked on the same window, nothing to do
1031 return;
1032 }
1033
1034 // We will be switching to another window
1035 assert (windows.get(0).getActive());
1036 assert (!window.getActive());
1037 windows.get(0).setActive(false);
1038 windows.get(0).setZ(window.getZ());
1039 window.setZ(0);
1040 window.setActive(true);
1041 repaint = true;
1042 return;
1043 }
1044 }
1045
1046 // Clicked on the background, nothing to do
1047 return;
1048 }
1049
1050 /**
1051 * Turn off the menu.
1052 */
928811d8 1053 public final void closeMenu() {
fca67db0
KL
1054 if (activeMenu != null) {
1055 activeMenu.setActive(false);
1056 activeMenu = null;
1057 for (TMenu menu: subMenus) {
1058 menu.setActive(false);
1059 }
1060 subMenus.clear();
1061 }
1062 repaint = true;
1063 }
1064
1065 /**
1066 * Turn off a sub-menu.
1067 */
928811d8 1068 public final void closeSubMenu() {
fca67db0
KL
1069 assert (activeMenu != null);
1070 TMenu item = subMenus.get(subMenus.size() - 1);
1071 assert (item != null);
1072 item.setActive(false);
1073 subMenus.remove(subMenus.size() - 1);
1074 repaint = true;
1075 }
1076
1077 /**
1078 * Switch to the next menu.
1079 *
1080 * @param forward if true, then switch to the next menu in the list,
1081 * otherwise switch to the previous menu in the list
1082 */
928811d8 1083 public final void switchMenu(final boolean forward) {
fca67db0
KL
1084 assert (activeMenu != null);
1085
1086 for (TMenu menu: subMenus) {
1087 menu.setActive(false);
1088 }
1089 subMenus.clear();
1090
1091 for (int i = 0; i < menus.size(); i++) {
1092 if (activeMenu == menus.get(i)) {
1093 if (forward) {
1094 if (i < menus.size() - 1) {
1095 i++;
1096 }
1097 } else {
1098 if (i > 0) {
1099 i--;
1100 }
1101 }
1102 activeMenu.setActive(false);
1103 activeMenu = menus.get(i);
1104 activeMenu.setActive(true);
1105 repaint = true;
1106 return;
1107 }
1108 }
1109 }
1110
1111 /**
1112 * Method that TApplication subclasses can override to handle menu or
1113 * posted command events.
1114 *
1115 * @param command command event
1116 * @return if true, this event was consumed
1117 */
1118 protected boolean onCommand(final TCommandEvent command) {
fca67db0
KL
1119 // Default: handle cmExit
1120 if (command.equals(cmExit)) {
1121 if (messageBox("Confirmation", "Exit application?",
c6940ed9 1122 TMessageBox.Type.YESNO).getResult() == TMessageBox.Result.YES) {
fca67db0
KL
1123 quit = true;
1124 }
1125 repaint = true;
1126 return true;
1127 }
1128
c6940ed9
KL
1129 /*
1130 TODO
fca67db0
KL
1131 if (command.equals(cmShell)) {
1132 openTerminal(0, 0, TWindow.Flag.RESIZABLE);
1133 repaint = true;
1134 return true;
1135 }
c6940ed9 1136 */
fca67db0
KL
1137
1138 if (command.equals(cmTile)) {
1139 tileWindows();
1140 repaint = true;
1141 return true;
1142 }
1143 if (command.equals(cmCascade)) {
1144 cascadeWindows();
1145 repaint = true;
1146 return true;
1147 }
1148 if (command.equals(cmCloseAll)) {
1149 closeAllWindows();
1150 repaint = true;
1151 return true;
1152 }
c6940ed9 1153
fca67db0
KL
1154 return false;
1155 }
1156
1157 /**
1158 * Method that TApplication subclasses can override to handle menu
1159 * events.
1160 *
1161 * @param menu menu event
1162 * @return if true, this event was consumed
1163 */
1164 protected boolean onMenu(final TMenuEvent menu) {
1165
fca67db0 1166 // Default: handle MID_EXIT
8e688b92 1167 if (menu.getId() == TMenu.MID_EXIT) {
fca67db0 1168 if (messageBox("Confirmation", "Exit application?",
c6940ed9 1169 TMessageBox.Type.YESNO).getResult() == TMessageBox.Result.YES) {
fca67db0
KL
1170 quit = true;
1171 }
1172 // System.err.printf("onMenu MID_EXIT result: quit = %s\n", quit);
1173 repaint = true;
1174 return true;
1175 }
1176
8e688b92
KL
1177 /*
1178 TODO
fca67db0
KL
1179 if (menu.id == TMenu.MID_SHELL) {
1180 openTerminal(0, 0, TWindow.Flag.RESIZABLE);
1181 repaint = true;
1182 return true;
1183 }
8e688b92 1184 */
fca67db0 1185
8e688b92 1186 if (menu.getId() == TMenu.MID_TILE) {
fca67db0
KL
1187 tileWindows();
1188 repaint = true;
1189 return true;
1190 }
8e688b92 1191 if (menu.getId() == TMenu.MID_CASCADE) {
fca67db0
KL
1192 cascadeWindows();
1193 repaint = true;
1194 return true;
1195 }
8e688b92 1196 if (menu.getId() == TMenu.MID_CLOSE_ALL) {
fca67db0
KL
1197 closeAllWindows();
1198 repaint = true;
1199 return true;
1200 }
fca67db0
KL
1201 return false;
1202 }
1203
1204 /**
1205 * Method that TApplication subclasses can override to handle keystrokes.
1206 *
1207 * @param keypress keystroke event
1208 * @return if true, this event was consumed
1209 */
1210 protected boolean onKeypress(final TKeypressEvent keypress) {
1211 // Default: only menu shortcuts
1212
1213 // Process Alt-F, Alt-E, etc. menu shortcut keys
1214 if (!keypress.getKey().getIsKey()
1215 && keypress.getKey().getAlt()
1216 && !keypress.getKey().getCtrl()
1217 && (activeMenu == null)
1218 ) {
1219
1220 assert (subMenus.size() == 0);
1221
1222 for (TMenu menu: menus) {
1223 if (Character.toLowerCase(menu.getMnemonic().getShortcut())
1224 == Character.toLowerCase(keypress.getKey().getCh())
1225 ) {
1226 activeMenu = menu;
1227 menu.setActive(true);
1228 repaint = true;
1229 return true;
1230 }
1231 }
1232 }
1233
1234 return false;
1235 }
48e27807 1236
928811d8
KL
1237 /**
1238 * Add a keyboard accelerator to the global hash.
1239 *
1240 * @param item menu item this accelerator relates to
1241 * @param keypress keypress that will dispatch a TMenuEvent
1242 */
1243 public final void addAccelerator(final TMenuItem item,
1244 final TKeypress keypress) {
e826b451
KL
1245
1246 // System.err.printf("addAccelerator: key %s item %s\n", keypress, item);
1247
1248 synchronized (accelerators) {
1249 assert (accelerators.get(keypress) == null);
1250 accelerators.put(keypress, item);
1251 }
928811d8
KL
1252 }
1253
1254 /**
1255 * Recompute menu x positions based on their title length.
1256 */
1257 public final void recomputeMenuX() {
1258 int x = 0;
1259 for (TMenu menu: menus) {
1260 menu.setX(x);
1261 x += menu.getTitle().length() + 2;
1262 }
1263 }
1264
1265 /**
1266 * Post an event to process and turn off the menu.
1267 *
1268 * @param event new event to add to the queue
1269 */
1270 public final void addMenuEvent(final TInputEvent event) {
8e688b92
KL
1271 synchronized (fillEventQueue) {
1272 fillEventQueue.add(event);
1273 }
928811d8
KL
1274 closeMenu();
1275 }
1276
1277 /**
1278 * Add a sub-menu to the list of open sub-menus.
1279 *
1280 * @param menu sub-menu
1281 */
1282 public final void addSubMenu(final TMenu menu) {
1283 subMenus.add(menu);
1284 }
1285
8e688b92
KL
1286 /**
1287 * Convenience function to add a top-level menu.
1288 *
1289 * @param title menu title
1290 * @return the new menu
1291 */
87a17f3c 1292 public final TMenu addMenu(final String title) {
8e688b92
KL
1293 int x = 0;
1294 int y = 0;
1295 TMenu menu = new TMenu(this, x, y, title);
1296 menus.add(menu);
1297 recomputeMenuX();
1298 return menu;
1299 }
1300
1301 /**
1302 * Convenience function to add a default "File" menu.
1303 *
1304 * @return the new menu
1305 */
1306 public final TMenu addFileMenu() {
1307 TMenu fileMenu = addMenu("&File");
1308 fileMenu.addDefaultItem(TMenu.MID_OPEN_FILE);
1309 fileMenu.addSeparator();
1310 fileMenu.addDefaultItem(TMenu.MID_SHELL);
1311 fileMenu.addDefaultItem(TMenu.MID_EXIT);
1312 return fileMenu;
1313 }
1314
1315 /**
1316 * Convenience function to add a default "Edit" menu.
1317 *
1318 * @return the new menu
1319 */
1320 public final TMenu addEditMenu() {
1321 TMenu editMenu = addMenu("&Edit");
1322 editMenu.addDefaultItem(TMenu.MID_CUT);
1323 editMenu.addDefaultItem(TMenu.MID_COPY);
1324 editMenu.addDefaultItem(TMenu.MID_PASTE);
1325 editMenu.addDefaultItem(TMenu.MID_CLEAR);
1326 return editMenu;
1327 }
1328
1329 /**
1330 * Convenience function to add a default "Window" menu.
1331 *
1332 * @return the new menu
1333 */
c6940ed9 1334 public final TMenu addWindowMenu() {
8e688b92
KL
1335 TMenu windowMenu = addMenu("&Window");
1336 windowMenu.addDefaultItem(TMenu.MID_TILE);
1337 windowMenu.addDefaultItem(TMenu.MID_CASCADE);
1338 windowMenu.addDefaultItem(TMenu.MID_CLOSE_ALL);
1339 windowMenu.addSeparator();
1340 windowMenu.addDefaultItem(TMenu.MID_WINDOW_MOVE);
1341 windowMenu.addDefaultItem(TMenu.MID_WINDOW_ZOOM);
1342 windowMenu.addDefaultItem(TMenu.MID_WINDOW_NEXT);
1343 windowMenu.addDefaultItem(TMenu.MID_WINDOW_PREVIOUS);
1344 windowMenu.addDefaultItem(TMenu.MID_WINDOW_CLOSE);
1345 return windowMenu;
1346 }
1347
1348 /**
1349 * Close all open windows.
1350 */
1351 private void closeAllWindows() {
1352 // Don't do anything if we are in the menu
1353 if (activeMenu != null) {
1354 return;
1355 }
1356 for (TWindow window: windows) {
1357 closeWindow(window);
1358 }
1359 }
1360
1361 /**
1362 * Re-layout the open windows as non-overlapping tiles. This produces
1363 * almost the same results as Turbo Pascal 7.0's IDE.
1364 */
1365 private void tileWindows() {
1366 // Don't do anything if we are in the menu
1367 if (activeMenu != null) {
1368 return;
1369 }
1370 int z = windows.size();
1371 if (z == 0) {
1372 return;
1373 }
1374 int a = 0;
1375 int b = 0;
1376 a = (int)(Math.sqrt(z));
1377 int c = 0;
1378 while (c < a) {
1379 b = (z - c) / a;
1380 if (((a * b) + c) == z) {
1381 break;
1382 }
1383 c++;
1384 }
1385 assert (a > 0);
1386 assert (b > 0);
1387 assert (c < a);
1388 int newWidth = (getScreen().getWidth() / a);
1389 int newHeight1 = ((getScreen().getHeight() - 1) / b);
1390 int newHeight2 = ((getScreen().getHeight() - 1) / (b + c));
1391 // System.err.printf("Z %s a %s b %s c %s newWidth %s newHeight1 %s newHeight2 %s",
1392 // z, a, b, c, newWidth, newHeight1, newHeight2);
1393
1394 List<TWindow> sorted = new LinkedList<TWindow>(windows);
1395 Collections.sort(sorted);
1396 Collections.reverse(sorted);
1397 for (int i = 0; i < sorted.size(); i++) {
1398 int logicalX = i / b;
1399 int logicalY = i % b;
1400 if (i >= ((a - 1) * b)) {
1401 logicalX = a - 1;
1402 logicalY = i - ((a - 1) * b);
1403 }
1404
1405 TWindow w = sorted.get(i);
1406 w.setX(logicalX * newWidth);
1407 w.setWidth(newWidth);
1408 if (i >= ((a - 1) * b)) {
1409 w.setY((logicalY * newHeight2) + 1);
1410 w.setHeight(newHeight2);
1411 } else {
1412 w.setY((logicalY * newHeight1) + 1);
1413 w.setHeight(newHeight1);
1414 }
1415 }
1416 }
1417
1418 /**
1419 * Re-layout the open windows as overlapping cascaded windows.
1420 */
1421 private void cascadeWindows() {
1422 // Don't do anything if we are in the menu
1423 if (activeMenu != null) {
1424 return;
1425 }
1426 int x = 0;
1427 int y = 1;
1428 List<TWindow> sorted = new LinkedList<TWindow>(windows);
1429 Collections.sort(sorted);
1430 Collections.reverse(sorted);
d502a0e9
KL
1431 for (TWindow window: sorted) {
1432 window.setX(x);
1433 window.setY(y);
8e688b92
KL
1434 x++;
1435 y++;
1436 if (x > getScreen().getWidth()) {
1437 x = 0;
1438 }
1439 if (y >= getScreen().getHeight()) {
1440 y = 1;
1441 }
1442 }
1443 }
1444
d502a0e9
KL
1445 /**
1446 * Convenience function to add a timer.
1447 *
1448 * @param duration number of milliseconds to wait between ticks
1449 * @param recurring if true, re-schedule this timer after every tick
1450 * @param action function to call when button is pressed
c6940ed9 1451 * @return the timer
d502a0e9
KL
1452 */
1453 public final TTimer addTimer(final long duration, final boolean recurring,
1454 final TAction action) {
1455
1456 TTimer timer = new TTimer(duration, recurring, action);
1457 synchronized (timers) {
1458 timers.add(timer);
1459 }
1460 return timer;
1461 }
1462
1463 /**
1464 * Convenience function to remove a timer.
1465 *
1466 * @param timer timer to remove
1467 */
1468 public final void removeTimer(final TTimer timer) {
1469 synchronized (timers) {
1470 timers.remove(timer);
1471 }
1472 }
1473
c6940ed9
KL
1474 /**
1475 * Convenience function to spawn a message box.
1476 *
1477 * @param title window title, will be centered along the top border
1478 * @param caption message to display. Use embedded newlines to get a
1479 * multi-line box.
1480 * @return the new message box
1481 */
1482 public final TMessageBox messageBox(final String title,
1483 final String caption) {
1484
1485 return new TMessageBox(this, title, caption, TMessageBox.Type.OK);
1486 }
1487
1488 /**
1489 * Convenience function to spawn a message box.
1490 *
1491 * @param title window title, will be centered along the top border
1492 * @param caption message to display. Use embedded newlines to get a
1493 * multi-line box.
1494 * @param type one of the TMessageBox.Type constants. Default is
1495 * Type.OK.
1496 * @return the new message box
1497 */
1498 public final TMessageBox messageBox(final String title,
1499 final String caption, final TMessageBox.Type type) {
1500
1501 return new TMessageBox(this, title, caption, type);
1502 }
1503
1504 /**
1505 * Convenience function to spawn an input box.
1506 *
1507 * @param title window title, will be centered along the top border
1508 * @param caption message to display. Use embedded newlines to get a
1509 * multi-line box.
1510 * @return the new input box
1511 */
1512 public final TInputBox inputBox(final String title, final String caption) {
1513
1514 return new TInputBox(this, title, caption);
1515 }
1516
1517 /**
1518 * Convenience function to spawn an input box.
1519 *
1520 * @param title window title, will be centered along the top border
1521 * @param caption message to display. Use embedded newlines to get a
1522 * multi-line box.
1523 * @param text initial text to seed the field with
1524 * @return the new input box
1525 */
1526 public final TInputBox inputBox(final String title, final String caption,
1527 final String text) {
1528
1529 return new TInputBox(this, title, caption, text);
1530 }
1ac2ccb1 1531
7d4115a5 1532}