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