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