#18 move to event-driven main loop
[nikiroo-utils.git] / src / jexer / TApplication.java
1 /*
2 * Jexer - Java Text User Interface
3 *
4 * The MIT License (MIT)
5 *
6 * Copyright (C) 2017 Kevin Lamonte
7 *
8 * Permission is hereby granted, free of charge, to any person obtaining a
9 * copy of this software and associated documentation files (the "Software"),
10 * to deal in the Software without restriction, including without limitation
11 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
12 * and/or sell copies of the Software, and to permit persons to whom the
13 * Software is furnished to do so, subject to the following conditions:
14 *
15 * The above copyright notice and this permission notice shall be included in
16 * all copies or substantial portions of the Software.
17 *
18 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
21 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
24 * DEALINGS IN THE SOFTWARE.
25 *
26 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
27 * @version 1
28 */
29 package jexer;
30
31 import java.io.InputStream;
32 import java.io.IOException;
33 import java.io.OutputStream;
34 import java.io.PrintWriter;
35 import java.io.Reader;
36 import java.io.UnsupportedEncodingException;
37 import java.util.Collections;
38 import java.util.Date;
39 import java.util.HashMap;
40 import java.util.ArrayList;
41 import java.util.LinkedList;
42 import java.util.List;
43 import java.util.Map;
44
45 import jexer.bits.CellAttributes;
46 import jexer.bits.ColorTheme;
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.Screen;
55 import jexer.backend.MultiBackend;
56 import jexer.backend.SwingBackend;
57 import jexer.backend.ECMA48Backend;
58 import jexer.backend.TWindowBackend;
59 import jexer.menu.TMenu;
60 import jexer.menu.TMenuItem;
61 import static jexer.TCommand.*;
62 import static jexer.TKeypress.*;
63
64 /**
65 * TApplication is the main driver class for a full Text User Interface
66 * application. It manages windows, provides a menu bar and status bar, and
67 * processes events received from the user.
68 */
69 public class TApplication implements Runnable {
70
71 // ------------------------------------------------------------------------
72 // Public constants -------------------------------------------------------
73 // ------------------------------------------------------------------------
74
75 /**
76 * If true, emit thread stuff to System.err.
77 */
78 private static final boolean debugThreads = false;
79
80 /**
81 * If true, emit events being processed to System.err.
82 */
83 private static final boolean debugEvents = false;
84
85 /**
86 * If true, do "smart placement" on new windows that are not specified to
87 * be centered.
88 */
89 private static final boolean smartWindowPlacement = true;
90
91 /**
92 * Two backend types are available.
93 */
94 public static enum BackendType {
95 /**
96 * A Swing JFrame.
97 */
98 SWING,
99
100 /**
101 * An ECMA48 / ANSI X3.64 / XTERM style terminal.
102 */
103 ECMA48,
104
105 /**
106 * Synonym for ECMA48.
107 */
108 XTERM
109 }
110
111 // ------------------------------------------------------------------------
112 // Primary/secondary event handlers ---------------------------------------
113 // ------------------------------------------------------------------------
114
115 /**
116 * WidgetEventHandler is the main event consumer loop. There are at most
117 * two such threads in existence: the primary for normal case and a
118 * secondary that is used for TMessageBox, TInputBox, and similar.
119 */
120 private class WidgetEventHandler implements Runnable {
121 /**
122 * The main application.
123 */
124 private TApplication application;
125
126 /**
127 * Whether or not this WidgetEventHandler is the primary or secondary
128 * thread.
129 */
130 private boolean primary = true;
131
132 /**
133 * Public constructor.
134 *
135 * @param application the main application
136 * @param primary if true, this is the primary event handler thread
137 */
138 public WidgetEventHandler(final TApplication application,
139 final boolean primary) {
140
141 this.application = application;
142 this.primary = primary;
143 }
144
145 /**
146 * The consumer loop.
147 */
148 public void run() {
149 boolean first = true;
150
151 // Loop forever
152 while (!application.quit) {
153
154 // Wait until application notifies me
155 while (!application.quit) {
156 try {
157 synchronized (application.drainEventQueue) {
158 if (application.drainEventQueue.size() > 0) {
159 break;
160 }
161 }
162
163 long timeout = 0;
164 if (first) {
165 first = false;
166 } else {
167 timeout = application.getSleepTime(1000);
168 }
169
170 if (timeout == 0) {
171 // A timer needs to fire, break out.
172 break;
173 }
174
175 if (debugThreads) {
176 System.err.printf("%d %s %s sleep %d millis\n",
177 System.currentTimeMillis(), this,
178 primary ? "primary" : "secondary", timeout);
179 }
180
181 synchronized (this) {
182 this.wait(timeout);
183 }
184
185 if (debugThreads) {
186 System.err.printf("%d %s %s AWAKE\n",
187 System.currentTimeMillis(), this,
188 primary ? "primary" : "secondary");
189 }
190
191 if ((!primary)
192 && (application.secondaryEventReceiver == null)
193 ) {
194 // Secondary thread, emergency exit. If we got
195 // here then something went wrong with the
196 // handoff between yield() and closeWindow().
197 synchronized (application.primaryEventHandler) {
198 application.primaryEventHandler.notify();
199 }
200 application.secondaryEventHandler = null;
201 throw new RuntimeException("secondary exited " +
202 "at wrong time");
203 }
204 break;
205 } catch (InterruptedException e) {
206 // SQUASH
207 }
208 } // while (!application.quit)
209
210 // Pull all events off the queue
211 for (;;) {
212 TInputEvent event = null;
213 synchronized (application.drainEventQueue) {
214 if (application.drainEventQueue.size() == 0) {
215 break;
216 }
217 event = application.drainEventQueue.remove(0);
218 }
219
220 // We will have an event to process, so repaint the
221 // screen at the end.
222 application.repaint = true;
223
224 if (primary) {
225 primaryHandleEvent(event);
226 } else {
227 secondaryHandleEvent(event);
228 }
229 if ((!primary)
230 && (application.secondaryEventReceiver == null)
231 ) {
232 // Secondary thread, time to exit.
233
234 // DO NOT UNLOCK. Primary thread just came back from
235 // primaryHandleEvent() and will unlock in the else
236 // block below. Just wake it up.
237 synchronized (application.primaryEventHandler) {
238 application.primaryEventHandler.notify();
239 }
240 // Now eliminate my reference so that
241 // wakeEventHandler() resumes working on the primary.
242 application.secondaryEventHandler = null;
243
244 // All done!
245 return;
246 }
247
248 } // for (;;)
249
250 // Fire timers, update screen.
251 if (!quit) {
252 application.finishEventProcessing();
253 }
254
255 } // while (true) (main runnable loop)
256 }
257 }
258
259 /**
260 * The primary event handler thread.
261 */
262 private volatile WidgetEventHandler primaryEventHandler;
263
264 /**
265 * The secondary event handler thread.
266 */
267 private volatile WidgetEventHandler secondaryEventHandler;
268
269 /**
270 * The widget receiving events from the secondary event handler thread.
271 */
272 private volatile TWidget secondaryEventReceiver;
273
274 /**
275 * Wake the sleeping active event handler.
276 */
277 private void wakeEventHandler() {
278 if (secondaryEventHandler != null) {
279 synchronized (secondaryEventHandler) {
280 secondaryEventHandler.notify();
281 }
282 } else {
283 assert (primaryEventHandler != null);
284 synchronized (primaryEventHandler) {
285 primaryEventHandler.notify();
286 }
287 }
288 }
289
290 // ------------------------------------------------------------------------
291 // TApplication attributes ------------------------------------------------
292 // ------------------------------------------------------------------------
293
294 /**
295 * Access to the physical screen, keyboard, and mouse.
296 */
297 private Backend backend;
298
299 /**
300 * Get the Backend.
301 *
302 * @return the Backend
303 */
304 public final Backend getBackend() {
305 return backend;
306 }
307
308 /**
309 * Get the Screen.
310 *
311 * @return the Screen
312 */
313 public final Screen getScreen() {
314 if (backend instanceof TWindowBackend) {
315 // We are being rendered to a TWindow. We can't use its
316 // getScreen() method because that is how it is rendering to a
317 // hardware backend somewhere. Instead use its getOtherScreen()
318 // method.
319 return ((TWindowBackend) backend).getOtherScreen();
320 } else {
321 return backend.getScreen();
322 }
323 }
324
325 /**
326 * Actual mouse coordinate X.
327 */
328 private int mouseX;
329
330 /**
331 * Actual mouse coordinate Y.
332 */
333 private int mouseY;
334
335 /**
336 * Old version of mouse coordinate X.
337 */
338 private int oldMouseX;
339
340 /**
341 * Old version mouse coordinate Y.
342 */
343 private int oldMouseY;
344
345 /**
346 * Event queue that is filled by run().
347 */
348 private List<TInputEvent> fillEventQueue;
349
350 /**
351 * Event queue that will be drained by either primary or secondary
352 * Thread.
353 */
354 private List<TInputEvent> drainEventQueue;
355
356 /**
357 * Top-level menus in this application.
358 */
359 private List<TMenu> menus;
360
361 /**
362 * Stack of activated sub-menus in this application.
363 */
364 private List<TMenu> subMenus;
365
366 /**
367 * The currently active menu.
368 */
369 private TMenu activeMenu = null;
370
371 /**
372 * Active keyboard accelerators.
373 */
374 private Map<TKeypress, TMenuItem> accelerators;
375
376 /**
377 * All menu items.
378 */
379 private List<TMenuItem> menuItems;
380
381 /**
382 * Windows and widgets pull colors from this ColorTheme.
383 */
384 private ColorTheme theme;
385
386 /**
387 * Get the color theme.
388 *
389 * @return the theme
390 */
391 public final ColorTheme getTheme() {
392 return theme;
393 }
394
395 /**
396 * The top-level windows (but not menus).
397 */
398 private List<TWindow> windows;
399
400 /**
401 * The currently acive window.
402 */
403 private TWindow activeWindow = null;
404
405 /**
406 * Timers that are being ticked.
407 */
408 private List<TTimer> timers;
409
410 /**
411 * When true, exit the application.
412 */
413 private volatile boolean quit = false;
414
415 /**
416 * When true, repaint the entire screen.
417 */
418 private volatile boolean repaint = true;
419
420 /**
421 * Repaint the screen on the next update.
422 */
423 public void doRepaint() {
424 repaint = true;
425 wakeEventHandler();
426 }
427
428 /**
429 * Y coordinate of the top edge of the desktop. For now this is a
430 * constant. Someday it would be nice to have a multi-line menu or
431 * toolbars.
432 */
433 private static final int desktopTop = 1;
434
435 /**
436 * Get Y coordinate of the top edge of the desktop.
437 *
438 * @return Y coordinate of the top edge of the desktop
439 */
440 public final int getDesktopTop() {
441 return desktopTop;
442 }
443
444 /**
445 * Y coordinate of the bottom edge of the desktop.
446 */
447 private int desktopBottom;
448
449 /**
450 * Get Y coordinate of the bottom edge of the desktop.
451 *
452 * @return Y coordinate of the bottom edge of the desktop
453 */
454 public final int getDesktopBottom() {
455 return desktopBottom;
456 }
457
458 /**
459 * An optional TDesktop background window that is drawn underneath
460 * everything else.
461 */
462 private TDesktop desktop;
463
464 /**
465 * Set the TDesktop instance.
466 *
467 * @param desktop a TDesktop instance, or null to remove the one that is
468 * set
469 */
470 public final void setDesktop(final TDesktop desktop) {
471 if (this.desktop != null) {
472 this.desktop.onClose();
473 }
474 this.desktop = desktop;
475 }
476
477 /**
478 * Get the TDesktop instance.
479 *
480 * @return the desktop, or null if it is not set
481 */
482 public final TDesktop getDesktop() {
483 return desktop;
484 }
485
486 /**
487 * Get the current active window.
488 *
489 * @return the active window, or null if it is not set
490 */
491 public final TWindow getActiveWindow() {
492 return activeWindow;
493 }
494
495 /**
496 * Get a (shallow) copy of the window list.
497 *
498 * @return a copy of the list of windows for this application
499 */
500 public final List<TWindow> getAllWindows() {
501 List<TWindow> result = new LinkedList<TWindow>();
502 result.addAll(windows);
503 return result;
504 }
505
506 /**
507 * If true, focus follows mouse: windows automatically raised if the
508 * mouse passes over them.
509 */
510 private boolean focusFollowsMouse = false;
511
512 /**
513 * Get focusFollowsMouse flag.
514 *
515 * @return true if focus follows mouse: windows automatically raised if
516 * the mouse passes over them
517 */
518 public boolean getFocusFollowsMouse() {
519 return focusFollowsMouse;
520 }
521
522 /**
523 * Set focusFollowsMouse flag.
524 *
525 * @param focusFollowsMouse if true, focus follows mouse: windows
526 * automatically raised if the mouse passes over them
527 */
528 public void setFocusFollowsMouse(final boolean focusFollowsMouse) {
529 this.focusFollowsMouse = focusFollowsMouse;
530 }
531
532 // ------------------------------------------------------------------------
533 // General behavior -------------------------------------------------------
534 // ------------------------------------------------------------------------
535
536 /**
537 * Display the about dialog.
538 */
539 protected void showAboutDialog() {
540 messageBox("About", "Jexer Version " +
541 this.getClass().getPackage().getImplementationVersion(),
542 TMessageBox.Type.OK);
543 }
544
545 // ------------------------------------------------------------------------
546 // Constructors -----------------------------------------------------------
547 // ------------------------------------------------------------------------
548
549 /**
550 * Public constructor.
551 *
552 * @param backendType BackendType.XTERM, BackendType.ECMA48 or
553 * BackendType.SWING
554 * @throws UnsupportedEncodingException if an exception is thrown when
555 * creating the InputStreamReader
556 */
557 public TApplication(final BackendType backendType)
558 throws UnsupportedEncodingException {
559
560 switch (backendType) {
561 case SWING:
562 // The default SwingBackend is 80x25, 20 pt font. If you want to
563 // change that, you can pass the extra arguments to the
564 // SwingBackend constructor here. For example, if you wanted
565 // 90x30, 16 pt font:
566 //
567 // backend = new SwingBackend(this, 90, 30, 16);
568 backend = new SwingBackend(this);
569 break;
570 case XTERM:
571 // Fall through...
572 case ECMA48:
573 backend = new ECMA48Backend(this, null, null);
574 break;
575 default:
576 throw new IllegalArgumentException("Invalid backend type: "
577 + backendType);
578 }
579 TApplicationImpl();
580 }
581
582 /**
583 * Public constructor. The backend type will be BackendType.ECMA48.
584 *
585 * @param input an InputStream connected to the remote user, or null for
586 * System.in. If System.in is used, then on non-Windows systems it will
587 * be put in raw mode; shutdown() will (blindly!) put System.in in cooked
588 * mode. input is always converted to a Reader with UTF-8 encoding.
589 * @param output an OutputStream connected to the remote user, or null
590 * for System.out. output is always converted to a Writer with UTF-8
591 * encoding.
592 * @throws UnsupportedEncodingException if an exception is thrown when
593 * creating the InputStreamReader
594 */
595 public TApplication(final InputStream input,
596 final OutputStream output) throws UnsupportedEncodingException {
597
598 backend = new ECMA48Backend(this, input, output);
599 TApplicationImpl();
600 }
601
602 /**
603 * Public constructor. The backend type will be BackendType.ECMA48.
604 *
605 * @param input the InputStream underlying 'reader'. Its available()
606 * method is used to determine if reader.read() will block or not.
607 * @param reader a Reader connected to the remote user.
608 * @param writer a PrintWriter connected to the remote user.
609 * @param setRawMode if true, set System.in into raw mode with stty.
610 * This should in general not be used. It is here solely for Demo3,
611 * which uses System.in.
612 * @throws IllegalArgumentException if input, reader, or writer are null.
613 */
614 public TApplication(final InputStream input, final Reader reader,
615 final PrintWriter writer, final boolean setRawMode) {
616
617 backend = new ECMA48Backend(this, input, reader, writer, setRawMode);
618 TApplicationImpl();
619 }
620
621 /**
622 * Public constructor. The backend type will be BackendType.ECMA48.
623 *
624 * @param input the InputStream underlying 'reader'. Its available()
625 * method is used to determine if reader.read() will block or not.
626 * @param reader a Reader connected to the remote user.
627 * @param writer a PrintWriter connected to the remote user.
628 * @throws IllegalArgumentException if input, reader, or writer are null.
629 */
630 public TApplication(final InputStream input, final Reader reader,
631 final PrintWriter writer) {
632
633 this(input, reader, writer, false);
634 }
635
636 /**
637 * Public constructor. This hook enables use with new non-Jexer
638 * backends.
639 *
640 * @param backend a Backend that is already ready to go.
641 */
642 public TApplication(final Backend backend) {
643 this.backend = backend;
644 backend.setListener(this);
645 TApplicationImpl();
646 }
647
648 /**
649 * Finish construction once the backend is set.
650 */
651 private void TApplicationImpl() {
652 theme = new ColorTheme();
653 desktopBottom = getScreen().getHeight() - 1;
654 fillEventQueue = new ArrayList<TInputEvent>();
655 drainEventQueue = new ArrayList<TInputEvent>();
656 windows = new LinkedList<TWindow>();
657 menus = new LinkedList<TMenu>();
658 subMenus = new LinkedList<TMenu>();
659 timers = new LinkedList<TTimer>();
660 accelerators = new HashMap<TKeypress, TMenuItem>();
661 menuItems = new ArrayList<TMenuItem>();
662 desktop = new TDesktop(this);
663
664 // Special case: the Swing backend needs to have a timer to drive its
665 // blink state.
666 if ((backend instanceof SwingBackend)
667 || (backend instanceof MultiBackend)
668 ) {
669 // Default to 500 millis, unless a SwingBackend has its own
670 // value.
671 long millis = 500;
672 if (backend instanceof SwingBackend) {
673 millis = ((SwingBackend) backend).getBlinkMillis();
674 }
675 if (millis > 0) {
676 addTimer(millis, true,
677 new TAction() {
678 public void DO() {
679 TApplication.this.doRepaint();
680 }
681 }
682 );
683 }
684 }
685 }
686
687 // ------------------------------------------------------------------------
688 // Screen refresh loop ----------------------------------------------------
689 // ------------------------------------------------------------------------
690
691 /**
692 * Process background events, and update the screen.
693 */
694 private void finishEventProcessing() {
695 if (debugThreads) {
696 System.err.printf(System.currentTimeMillis() + " " +
697 Thread.currentThread() + " finishEventProcessing()\n");
698 }
699
700 // Process timers and call doIdle()'s
701 doIdle();
702
703 // Update the screen
704 synchronized (getScreen()) {
705 drawAll();
706 }
707
708 if (debugThreads) {
709 System.err.printf(System.currentTimeMillis() + " " +
710 Thread.currentThread() + " finishEventProcessing() END\n");
711 }
712 }
713
714 /**
715 * Invert the cell color at a position. This is used to track the mouse.
716 *
717 * @param x column position
718 * @param y row position
719 */
720 private void invertCell(final int x, final int y) {
721 if (debugThreads) {
722 System.err.printf("%d %s invertCell() %d %d\n",
723 System.currentTimeMillis(), Thread.currentThread(), x, y);
724 }
725 CellAttributes attr = getScreen().getAttrXY(x, y);
726 attr.setForeColor(attr.getForeColor().invert());
727 attr.setBackColor(attr.getBackColor().invert());
728 getScreen().putAttrXY(x, y, attr, false);
729 }
730
731 /**
732 * Draw everything.
733 */
734 private void drawAll() {
735 if (debugThreads) {
736 System.err.printf("%d %s drawAll() enter\n",
737 System.currentTimeMillis(), Thread.currentThread());
738 }
739
740 if (!repaint) {
741 if (debugThreads) {
742 System.err.printf("%d %s drawAll() !repaint\n",
743 System.currentTimeMillis(), Thread.currentThread());
744 }
745 synchronized (getScreen()) {
746 if ((oldMouseX != mouseX) || (oldMouseY != mouseY)) {
747 // The only thing that has happened is the mouse moved.
748 // Clear the old position and draw the new position.
749 invertCell(oldMouseX, oldMouseY);
750 invertCell(mouseX, mouseY);
751 oldMouseX = mouseX;
752 oldMouseY = mouseY;
753 }
754 if (getScreen().isDirty()) {
755 backend.flushScreen();
756 }
757 return;
758 }
759 }
760
761 if (debugThreads) {
762 System.err.printf("%d %s drawAll() REDRAW\n",
763 System.currentTimeMillis(), Thread.currentThread());
764 }
765
766 // If true, the cursor is not visible
767 boolean cursor = false;
768
769 // Start with a clean screen
770 getScreen().clear();
771
772 // Draw the desktop
773 if (desktop != null) {
774 desktop.drawChildren();
775 }
776
777 // Draw each window in reverse Z order
778 List<TWindow> sorted = new LinkedList<TWindow>(windows);
779 Collections.sort(sorted);
780 TWindow topLevel = null;
781 if (sorted.size() > 0) {
782 topLevel = sorted.get(0);
783 }
784 Collections.reverse(sorted);
785 for (TWindow window: sorted) {
786 if (window.isShown()) {
787 window.drawChildren();
788 }
789 }
790
791 // Draw the blank menubar line - reset the screen clipping first so
792 // it won't trim it out.
793 getScreen().resetClipping();
794 getScreen().hLineXY(0, 0, getScreen().getWidth(), ' ',
795 theme.getColor("tmenu"));
796 // Now draw the menus.
797 int x = 1;
798 for (TMenu menu: menus) {
799 CellAttributes menuColor;
800 CellAttributes menuMnemonicColor;
801 if (menu.isActive()) {
802 menuColor = theme.getColor("tmenu.highlighted");
803 menuMnemonicColor = theme.getColor("tmenu.mnemonic.highlighted");
804 topLevel = menu;
805 } else {
806 menuColor = theme.getColor("tmenu");
807 menuMnemonicColor = theme.getColor("tmenu.mnemonic");
808 }
809 // Draw the menu title
810 getScreen().hLineXY(x, 0, menu.getTitle().length() + 2, ' ',
811 menuColor);
812 getScreen().putStringXY(x + 1, 0, menu.getTitle(), menuColor);
813 // Draw the highlight character
814 getScreen().putCharXY(x + 1 + menu.getMnemonic().getShortcutIdx(),
815 0, menu.getMnemonic().getShortcut(), menuMnemonicColor);
816
817 if (menu.isActive()) {
818 menu.drawChildren();
819 // Reset the screen clipping so we can draw the next title.
820 getScreen().resetClipping();
821 }
822 x += menu.getTitle().length() + 2;
823 }
824
825 for (TMenu menu: subMenus) {
826 // Reset the screen clipping so we can draw the next sub-menu.
827 getScreen().resetClipping();
828 menu.drawChildren();
829 }
830
831 // Draw the status bar of the top-level window
832 TStatusBar statusBar = null;
833 if (topLevel != null) {
834 statusBar = topLevel.getStatusBar();
835 }
836 if (statusBar != null) {
837 getScreen().resetClipping();
838 statusBar.setWidth(getScreen().getWidth());
839 statusBar.setY(getScreen().getHeight() - topLevel.getY());
840 statusBar.draw();
841 } else {
842 CellAttributes barColor = new CellAttributes();
843 barColor.setTo(getTheme().getColor("tstatusbar.text"));
844 getScreen().hLineXY(0, desktopBottom, getScreen().getWidth(), ' ',
845 barColor);
846 }
847
848 // Draw the mouse pointer
849 invertCell(mouseX, mouseY);
850 oldMouseX = mouseX;
851 oldMouseY = mouseY;
852
853 // Place the cursor if it is visible
854 TWidget activeWidget = null;
855 if (sorted.size() > 0) {
856 activeWidget = sorted.get(sorted.size() - 1).getActiveChild();
857 if (activeWidget.isCursorVisible()) {
858 if ((activeWidget.getCursorAbsoluteY() < desktopBottom)
859 && (activeWidget.getCursorAbsoluteY() > desktopTop)
860 ) {
861 getScreen().putCursor(true,
862 activeWidget.getCursorAbsoluteX(),
863 activeWidget.getCursorAbsoluteY());
864 cursor = true;
865 } else {
866 getScreen().putCursor(false,
867 activeWidget.getCursorAbsoluteX(),
868 activeWidget.getCursorAbsoluteY());
869 cursor = false;
870 }
871 }
872 }
873
874 // Kill the cursor
875 if (!cursor) {
876 getScreen().hideCursor();
877 }
878
879 // Flush the screen contents
880 if (getScreen().isDirty()) {
881 backend.flushScreen();
882 }
883
884 repaint = false;
885 }
886
887 // ------------------------------------------------------------------------
888 // Main loop --------------------------------------------------------------
889 // ------------------------------------------------------------------------
890
891 /**
892 * Force this application to exit.
893 */
894 public void exit() {
895 quit = true;
896 synchronized (this) {
897 this.notify();
898 }
899 }
900
901 /**
902 * Run this application until it exits.
903 */
904 public void run() {
905 // Start the main consumer thread
906 primaryEventHandler = new WidgetEventHandler(this, true);
907 (new Thread(primaryEventHandler)).start();
908
909 while (!quit) {
910 synchronized (this) {
911 boolean doWait = false;
912
913 synchronized (fillEventQueue) {
914 if (fillEventQueue.size() == 0) {
915 doWait = true;
916 }
917 }
918
919 if (doWait) {
920 // No I/O to dispatch, so wait until the backend
921 // provides new I/O.
922 try {
923 if (debugThreads) {
924 System.err.println(System.currentTimeMillis() +
925 " MAIN sleep");
926 }
927
928 this.wait();
929
930 if (debugThreads) {
931 System.err.println(System.currentTimeMillis() +
932 " MAIN AWAKE");
933 }
934 } catch (InterruptedException e) {
935 // I'm awake and don't care why, let's see what's
936 // going on out there.
937 }
938 }
939
940 } // synchronized (this)
941
942 synchronized (fillEventQueue) {
943 // Pull any pending I/O events
944 backend.getEvents(fillEventQueue);
945
946 // Dispatch each event to the appropriate handler, one at a
947 // time.
948 for (;;) {
949 TInputEvent event = null;
950 if (fillEventQueue.size() == 0) {
951 break;
952 }
953 event = fillEventQueue.remove(0);
954 metaHandleEvent(event);
955 }
956 }
957
958 // Wake a consumer thread if we have any pending events.
959 if (drainEventQueue.size() > 0) {
960 wakeEventHandler();
961 }
962
963 } // while (!quit)
964
965 // Shutdown the event consumer threads
966 if (secondaryEventHandler != null) {
967 synchronized (secondaryEventHandler) {
968 secondaryEventHandler.notify();
969 }
970 }
971 if (primaryEventHandler != null) {
972 synchronized (primaryEventHandler) {
973 primaryEventHandler.notify();
974 }
975 }
976
977 // Shutdown the user I/O thread(s)
978 backend.shutdown();
979
980 // Close all the windows. This gives them an opportunity to release
981 // resources.
982 closeAllWindows();
983
984 }
985
986 /**
987 * Peek at certain application-level events, add to eventQueue, and wake
988 * up the consuming Thread.
989 *
990 * @param event the input event to consume
991 */
992 private void metaHandleEvent(final TInputEvent event) {
993
994 if (debugEvents) {
995 System.err.printf(String.format("metaHandleEvents event: %s\n",
996 event)); System.err.flush();
997 }
998
999 if (quit) {
1000 // Do no more processing if the application is already trying
1001 // to exit.
1002 return;
1003 }
1004
1005 // Special application-wide events -------------------------------
1006
1007 // Abort everything
1008 if (event instanceof TCommandEvent) {
1009 TCommandEvent command = (TCommandEvent) event;
1010 if (command.getCmd().equals(cmAbort)) {
1011 exit();
1012 return;
1013 }
1014 }
1015
1016 synchronized (drainEventQueue) {
1017 // Screen resize
1018 if (event instanceof TResizeEvent) {
1019 TResizeEvent resize = (TResizeEvent) event;
1020 synchronized (getScreen()) {
1021 getScreen().setDimensions(resize.getWidth(),
1022 resize.getHeight());
1023 desktopBottom = getScreen().getHeight() - 1;
1024 mouseX = 0;
1025 mouseY = 0;
1026 oldMouseX = 0;
1027 oldMouseY = 0;
1028 }
1029 if (desktop != null) {
1030 desktop.setDimensions(0, 0, resize.getWidth(),
1031 resize.getHeight() - 1);
1032 }
1033 return;
1034 }
1035
1036 // Put into the main queue
1037 drainEventQueue.add(event);
1038 }
1039 }
1040
1041 /**
1042 * Dispatch one event to the appropriate widget or application-level
1043 * event handler. This is the primary event handler, it has the normal
1044 * application-wide event handling.
1045 *
1046 * @param event the input event to consume
1047 * @see #secondaryHandleEvent(TInputEvent event)
1048 */
1049 private void primaryHandleEvent(final TInputEvent event) {
1050
1051 if (debugEvents) {
1052 System.err.printf("Handle event: %s\n", event);
1053 }
1054
1055 // Special application-wide events -----------------------------------
1056
1057 // Peek at the mouse position
1058 if (event instanceof TMouseEvent) {
1059 TMouseEvent mouse = (TMouseEvent) event;
1060 if ((mouseX != mouse.getX()) || (mouseY != mouse.getY())) {
1061 oldMouseX = mouseX;
1062 oldMouseY = mouseY;
1063 mouseX = mouse.getX();
1064 mouseY = mouse.getY();
1065 }
1066
1067 // See if we need to switch focus to another window or the menu
1068 checkSwitchFocus((TMouseEvent) event);
1069 }
1070
1071 // Handle menu events
1072 if ((activeMenu != null) && !(event instanceof TCommandEvent)) {
1073 TMenu menu = activeMenu;
1074
1075 if (event instanceof TMouseEvent) {
1076 TMouseEvent mouse = (TMouseEvent) event;
1077
1078 while (subMenus.size() > 0) {
1079 TMenu subMenu = subMenus.get(subMenus.size() - 1);
1080 if (subMenu.mouseWouldHit(mouse)) {
1081 break;
1082 }
1083 if ((mouse.getType() == TMouseEvent.Type.MOUSE_MOTION)
1084 && (!mouse.isMouse1())
1085 && (!mouse.isMouse2())
1086 && (!mouse.isMouse3())
1087 && (!mouse.isMouseWheelUp())
1088 && (!mouse.isMouseWheelDown())
1089 ) {
1090 break;
1091 }
1092 // We navigated away from a sub-menu, so close it
1093 closeSubMenu();
1094 }
1095
1096 // Convert the mouse relative x/y to menu coordinates
1097 assert (mouse.getX() == mouse.getAbsoluteX());
1098 assert (mouse.getY() == mouse.getAbsoluteY());
1099 if (subMenus.size() > 0) {
1100 menu = subMenus.get(subMenus.size() - 1);
1101 }
1102 mouse.setX(mouse.getX() - menu.getX());
1103 mouse.setY(mouse.getY() - menu.getY());
1104 }
1105 menu.handleEvent(event);
1106 return;
1107 }
1108
1109 if (event instanceof TKeypressEvent) {
1110 TKeypressEvent keypress = (TKeypressEvent) event;
1111
1112 // See if this key matches an accelerator, and is not being
1113 // shortcutted by the active window, and if so dispatch the menu
1114 // event.
1115 boolean windowWillShortcut = false;
1116 if (activeWindow != null) {
1117 assert (activeWindow.isShown());
1118 if (activeWindow.isShortcutKeypress(keypress.getKey())) {
1119 // We do not process this key, it will be passed to the
1120 // window instead.
1121 windowWillShortcut = true;
1122 }
1123 }
1124
1125 if (!windowWillShortcut && !modalWindowActive()) {
1126 TKeypress keypressLowercase = keypress.getKey().toLowerCase();
1127 TMenuItem item = null;
1128 synchronized (accelerators) {
1129 item = accelerators.get(keypressLowercase);
1130 }
1131 if (item != null) {
1132 if (item.isEnabled()) {
1133 // Let the menu item dispatch
1134 item.dispatch();
1135 return;
1136 }
1137 }
1138
1139 // Handle the keypress
1140 if (onKeypress(keypress)) {
1141 return;
1142 }
1143 }
1144 }
1145
1146 if (event instanceof TCommandEvent) {
1147 if (onCommand((TCommandEvent) event)) {
1148 return;
1149 }
1150 }
1151
1152 if (event instanceof TMenuEvent) {
1153 if (onMenu((TMenuEvent) event)) {
1154 return;
1155 }
1156 }
1157
1158 // Dispatch events to the active window -------------------------------
1159 boolean dispatchToDesktop = true;
1160 TWindow window = activeWindow;
1161 if (window != null) {
1162 assert (window.isActive());
1163 assert (window.isShown());
1164 if (event instanceof TMouseEvent) {
1165 TMouseEvent mouse = (TMouseEvent) event;
1166 // Convert the mouse relative x/y to window coordinates
1167 assert (mouse.getX() == mouse.getAbsoluteX());
1168 assert (mouse.getY() == mouse.getAbsoluteY());
1169 mouse.setX(mouse.getX() - window.getX());
1170 mouse.setY(mouse.getY() - window.getY());
1171
1172 if (window.mouseWouldHit(mouse)) {
1173 dispatchToDesktop = false;
1174 }
1175 } else if (event instanceof TKeypressEvent) {
1176 dispatchToDesktop = false;
1177 }
1178
1179 if (debugEvents) {
1180 System.err.printf("TApplication dispatch event: %s\n",
1181 event);
1182 }
1183 window.handleEvent(event);
1184 }
1185 if (dispatchToDesktop) {
1186 // This event is fair game for the desktop to process.
1187 if (desktop != null) {
1188 desktop.handleEvent(event);
1189 }
1190 }
1191 }
1192
1193 /**
1194 * Dispatch one event to the appropriate widget or application-level
1195 * event handler. This is the secondary event handler used by certain
1196 * special dialogs (currently TMessageBox and TFileOpenBox).
1197 *
1198 * @param event the input event to consume
1199 * @see #primaryHandleEvent(TInputEvent event)
1200 */
1201 private void secondaryHandleEvent(final TInputEvent event) {
1202 // Peek at the mouse position
1203 if (event instanceof TMouseEvent) {
1204 TMouseEvent mouse = (TMouseEvent) event;
1205 if ((mouseX != mouse.getX()) || (mouseY != mouse.getY())) {
1206 oldMouseX = mouseX;
1207 oldMouseY = mouseY;
1208 mouseX = mouse.getX();
1209 mouseY = mouse.getY();
1210 }
1211 }
1212
1213 secondaryEventReceiver.handleEvent(event);
1214 }
1215
1216 /**
1217 * Enable a widget to override the primary event thread.
1218 *
1219 * @param widget widget that will receive events
1220 */
1221 public final void enableSecondaryEventReceiver(final TWidget widget) {
1222 if (debugThreads) {
1223 System.err.println(System.currentTimeMillis() +
1224 " enableSecondaryEventReceiver()");
1225 }
1226
1227 assert (secondaryEventReceiver == null);
1228 assert (secondaryEventHandler == null);
1229 assert ((widget instanceof TMessageBox)
1230 || (widget instanceof TFileOpenBox));
1231 secondaryEventReceiver = widget;
1232 secondaryEventHandler = new WidgetEventHandler(this, false);
1233
1234 (new Thread(secondaryEventHandler)).start();
1235 }
1236
1237 /**
1238 * Yield to the secondary thread.
1239 */
1240 public final void yield() {
1241 assert (secondaryEventReceiver != null);
1242
1243 while (secondaryEventReceiver != null) {
1244 synchronized (primaryEventHandler) {
1245 try {
1246 primaryEventHandler.wait();
1247 } catch (InterruptedException e) {
1248 // SQUASH
1249 }
1250 }
1251 }
1252 }
1253
1254 /**
1255 * Do stuff when there is no user input.
1256 */
1257 private void doIdle() {
1258 if (debugThreads) {
1259 System.err.printf(System.currentTimeMillis() + " " +
1260 Thread.currentThread() + " doIdle()\n");
1261 }
1262
1263 synchronized (timers) {
1264
1265 if (debugThreads) {
1266 System.err.printf(System.currentTimeMillis() + " " +
1267 Thread.currentThread() + " doIdle() 2\n");
1268 }
1269
1270 // Run any timers that have timed out
1271 Date now = new Date();
1272 List<TTimer> keepTimers = new LinkedList<TTimer>();
1273 for (TTimer timer: timers) {
1274 if (timer.getNextTick().getTime() <= now.getTime()) {
1275 // Something might change, so repaint the screen.
1276 repaint = true;
1277 timer.tick();
1278 if (timer.recurring) {
1279 keepTimers.add(timer);
1280 }
1281 } else {
1282 keepTimers.add(timer);
1283 }
1284 }
1285 timers = keepTimers;
1286 }
1287
1288 // Call onIdle's
1289 for (TWindow window: windows) {
1290 window.onIdle();
1291 }
1292 if (desktop != null) {
1293 desktop.onIdle();
1294 }
1295 }
1296
1297 // ------------------------------------------------------------------------
1298 // TWindow management -----------------------------------------------------
1299 // ------------------------------------------------------------------------
1300
1301 /**
1302 * Return the total number of windows.
1303 *
1304 * @return the total number of windows
1305 */
1306 public final int windowCount() {
1307 return windows.size();
1308 }
1309
1310 /**
1311 * Return the number of windows that are showing.
1312 *
1313 * @return the number of windows that are showing on screen
1314 */
1315 public final int shownWindowCount() {
1316 int n = 0;
1317 for (TWindow w: windows) {
1318 if (w.isShown()) {
1319 n++;
1320 }
1321 }
1322 return n;
1323 }
1324
1325 /**
1326 * Return the number of windows that are hidden.
1327 *
1328 * @return the number of windows that are hidden
1329 */
1330 public final int hiddenWindowCount() {
1331 int n = 0;
1332 for (TWindow w: windows) {
1333 if (w.isHidden()) {
1334 n++;
1335 }
1336 }
1337 return n;
1338 }
1339
1340 /**
1341 * Check if a window instance is in this application's window list.
1342 *
1343 * @param window window to look for
1344 * @return true if this window is in the list
1345 */
1346 public final boolean hasWindow(final TWindow window) {
1347 if (windows.size() == 0) {
1348 return false;
1349 }
1350 for (TWindow w: windows) {
1351 if (w == window) {
1352 assert (window.getApplication() == this);
1353 return true;
1354 }
1355 }
1356 return false;
1357 }
1358
1359 /**
1360 * Activate a window: bring it to the top and have it receive events.
1361 *
1362 * @param window the window to become the new active window
1363 */
1364 public void activateWindow(final TWindow window) {
1365 if (hasWindow(window) == false) {
1366 /*
1367 * Someone has a handle to a window I don't have. Ignore this
1368 * request.
1369 */
1370 return;
1371 }
1372
1373 // Whatever window might be moving/dragging, stop it now.
1374 for (TWindow w: windows) {
1375 if (w.inMovements()) {
1376 w.stopMovements();
1377 }
1378 }
1379
1380 assert (windows.size() > 0);
1381
1382 if (window.isHidden()) {
1383 // Unhiding will also activate.
1384 showWindow(window);
1385 return;
1386 }
1387 assert (window.isShown());
1388
1389 if (windows.size() == 1) {
1390 assert (window == windows.get(0));
1391 if (activeWindow == null) {
1392 activeWindow = window;
1393 window.setZ(0);
1394 activeWindow.setActive(true);
1395 activeWindow.onFocus();
1396 }
1397
1398 assert (window.isActive());
1399 assert (activeWindow == window);
1400 return;
1401 }
1402
1403 if (activeWindow == window) {
1404 assert (window.isActive());
1405
1406 // Window is already active, do nothing.
1407 return;
1408 }
1409
1410 assert (!window.isActive());
1411 if (activeWindow != null) {
1412 assert (activeWindow.getZ() == 0);
1413
1414 activeWindow.onUnfocus();
1415 activeWindow.setActive(false);
1416 activeWindow.setZ(window.getZ());
1417 }
1418 activeWindow = window;
1419 activeWindow.setZ(0);
1420 activeWindow.setActive(true);
1421 activeWindow.onFocus();
1422 return;
1423 }
1424
1425 /**
1426 * Hide a window.
1427 *
1428 * @param window the window to hide
1429 */
1430 public void hideWindow(final TWindow window) {
1431 if (hasWindow(window) == false) {
1432 /*
1433 * Someone has a handle to a window I don't have. Ignore this
1434 * request.
1435 */
1436 return;
1437 }
1438
1439 // Whatever window might be moving/dragging, stop it now.
1440 for (TWindow w: windows) {
1441 if (w.inMovements()) {
1442 w.stopMovements();
1443 }
1444 }
1445
1446 assert (windows.size() > 0);
1447
1448 if (!window.hidden) {
1449 if (window == activeWindow) {
1450 if (shownWindowCount() > 1) {
1451 switchWindow(true);
1452 } else {
1453 activeWindow = null;
1454 window.setActive(false);
1455 window.onUnfocus();
1456 }
1457 }
1458 window.hidden = true;
1459 window.onHide();
1460 }
1461 }
1462
1463 /**
1464 * Show a window.
1465 *
1466 * @param window the window to show
1467 */
1468 public void showWindow(final TWindow window) {
1469 if (hasWindow(window) == false) {
1470 /*
1471 * Someone has a handle to a window I don't have. Ignore this
1472 * request.
1473 */
1474 return;
1475 }
1476
1477 // Whatever window might be moving/dragging, stop it now.
1478 for (TWindow w: windows) {
1479 if (w.inMovements()) {
1480 w.stopMovements();
1481 }
1482 }
1483
1484 assert (windows.size() > 0);
1485
1486 if (window.hidden) {
1487 window.hidden = false;
1488 window.onShow();
1489 activateWindow(window);
1490 }
1491 }
1492
1493 /**
1494 * Close window. Note that the window's destructor is NOT called by this
1495 * method, instead the GC is assumed to do the cleanup.
1496 *
1497 * @param window the window to remove
1498 */
1499 public final void closeWindow(final TWindow window) {
1500 if (hasWindow(window) == false) {
1501 /*
1502 * Someone has a handle to a window I don't have. Ignore this
1503 * request.
1504 */
1505 return;
1506 }
1507
1508 synchronized (windows) {
1509 // Whatever window might be moving/dragging, stop it now.
1510 for (TWindow w: windows) {
1511 if (w.inMovements()) {
1512 w.stopMovements();
1513 }
1514 }
1515
1516 int z = window.getZ();
1517 window.setZ(-1);
1518 window.onUnfocus();
1519 Collections.sort(windows);
1520 windows.remove(0);
1521 activeWindow = null;
1522 for (TWindow w: windows) {
1523 if (w.getZ() > z) {
1524 w.setZ(w.getZ() - 1);
1525 if (w.getZ() == 0) {
1526 w.setActive(true);
1527 w.onFocus();
1528 assert (activeWindow == null);
1529 activeWindow = w;
1530 } else {
1531 if (w.isActive()) {
1532 w.setActive(false);
1533 w.onUnfocus();
1534 }
1535 }
1536 }
1537 }
1538 }
1539
1540 // Perform window cleanup
1541 window.onClose();
1542
1543 // Check if we are closing a TMessageBox or similar
1544 if (secondaryEventReceiver != null) {
1545 assert (secondaryEventHandler != null);
1546
1547 // Do not send events to the secondaryEventReceiver anymore, the
1548 // window is closed.
1549 secondaryEventReceiver = null;
1550
1551 // Wake the secondary thread, it will wake the primary as it
1552 // exits.
1553 synchronized (secondaryEventHandler) {
1554 secondaryEventHandler.notify();
1555 }
1556 }
1557
1558 // Permit desktop to be active if it is the only thing left.
1559 if (desktop != null) {
1560 if (windows.size() == 0) {
1561 desktop.setActive(true);
1562 }
1563 }
1564 }
1565
1566 /**
1567 * Switch to the next window.
1568 *
1569 * @param forward if true, then switch to the next window in the list,
1570 * otherwise switch to the previous window in the list
1571 */
1572 public final void switchWindow(final boolean forward) {
1573 // Only switch if there are multiple visible windows
1574 if (shownWindowCount() < 2) {
1575 return;
1576 }
1577 assert (activeWindow != null);
1578
1579 synchronized (windows) {
1580 // Whatever window might be moving/dragging, stop it now.
1581 for (TWindow w: windows) {
1582 if (w.inMovements()) {
1583 w.stopMovements();
1584 }
1585 }
1586
1587 // Swap z/active between active window and the next in the list
1588 int activeWindowI = -1;
1589 for (int i = 0; i < windows.size(); i++) {
1590 if (windows.get(i) == activeWindow) {
1591 assert (activeWindow.isActive());
1592 activeWindowI = i;
1593 break;
1594 } else {
1595 assert (!windows.get(0).isActive());
1596 }
1597 }
1598 assert (activeWindowI >= 0);
1599
1600 // Do not switch if a window is modal
1601 if (activeWindow.isModal()) {
1602 return;
1603 }
1604
1605 int nextWindowI = activeWindowI;
1606 for (;;) {
1607 if (forward) {
1608 nextWindowI++;
1609 nextWindowI %= windows.size();
1610 } else {
1611 nextWindowI--;
1612 if (nextWindowI < 0) {
1613 nextWindowI = windows.size() - 1;
1614 }
1615 }
1616
1617 if (windows.get(nextWindowI).isShown()) {
1618 activateWindow(windows.get(nextWindowI));
1619 break;
1620 }
1621 }
1622 } // synchronized (windows)
1623
1624 }
1625
1626 /**
1627 * Add a window to my window list and make it active.
1628 *
1629 * @param window new window to add
1630 */
1631 public final void addWindow(final TWindow window) {
1632
1633 // Do not add menu windows to the window list.
1634 if (window instanceof TMenu) {
1635 return;
1636 }
1637
1638 // Do not add the desktop to the window list.
1639 if (window instanceof TDesktop) {
1640 return;
1641 }
1642
1643 synchronized (windows) {
1644 // Whatever window might be moving/dragging, stop it now.
1645 for (TWindow w: windows) {
1646 if (w.inMovements()) {
1647 w.stopMovements();
1648 }
1649 }
1650
1651 // Do not allow a modal window to spawn a non-modal window. If a
1652 // modal window is active, then this window will become modal
1653 // too.
1654 if (modalWindowActive()) {
1655 window.flags |= TWindow.MODAL;
1656 window.flags |= TWindow.CENTERED;
1657 window.hidden = false;
1658 }
1659 if (window.isShown()) {
1660 for (TWindow w: windows) {
1661 if (w.isActive()) {
1662 w.setActive(false);
1663 w.onUnfocus();
1664 }
1665 w.setZ(w.getZ() + 1);
1666 }
1667 }
1668 windows.add(window);
1669 if (window.isShown()) {
1670 activeWindow = window;
1671 activeWindow.setZ(0);
1672 activeWindow.setActive(true);
1673 activeWindow.onFocus();
1674 }
1675
1676 if (((window.flags & TWindow.CENTERED) == 0)
1677 && smartWindowPlacement) {
1678
1679 doSmartPlacement(window);
1680 }
1681 }
1682
1683 // Desktop cannot be active over any other window.
1684 if (desktop != null) {
1685 desktop.setActive(false);
1686 }
1687 }
1688
1689 /**
1690 * Check if there is a system-modal window on top.
1691 *
1692 * @return true if the active window is modal
1693 */
1694 private boolean modalWindowActive() {
1695 if (windows.size() == 0) {
1696 return false;
1697 }
1698
1699 for (TWindow w: windows) {
1700 if (w.isModal()) {
1701 return true;
1702 }
1703 }
1704
1705 return false;
1706 }
1707
1708 /**
1709 * Close all open windows.
1710 */
1711 private void closeAllWindows() {
1712 // Don't do anything if we are in the menu
1713 if (activeMenu != null) {
1714 return;
1715 }
1716 while (windows.size() > 0) {
1717 closeWindow(windows.get(0));
1718 }
1719 }
1720
1721 /**
1722 * Re-layout the open windows as non-overlapping tiles. This produces
1723 * almost the same results as Turbo Pascal 7.0's IDE.
1724 */
1725 private void tileWindows() {
1726 synchronized (windows) {
1727 // Don't do anything if we are in the menu
1728 if (activeMenu != null) {
1729 return;
1730 }
1731 int z = windows.size();
1732 if (z == 0) {
1733 return;
1734 }
1735 int a = 0;
1736 int b = 0;
1737 a = (int)(Math.sqrt(z));
1738 int c = 0;
1739 while (c < a) {
1740 b = (z - c) / a;
1741 if (((a * b) + c) == z) {
1742 break;
1743 }
1744 c++;
1745 }
1746 assert (a > 0);
1747 assert (b > 0);
1748 assert (c < a);
1749 int newWidth = (getScreen().getWidth() / a);
1750 int newHeight1 = ((getScreen().getHeight() - 1) / b);
1751 int newHeight2 = ((getScreen().getHeight() - 1) / (b + c));
1752
1753 List<TWindow> sorted = new LinkedList<TWindow>(windows);
1754 Collections.sort(sorted);
1755 Collections.reverse(sorted);
1756 for (int i = 0; i < sorted.size(); i++) {
1757 int logicalX = i / b;
1758 int logicalY = i % b;
1759 if (i >= ((a - 1) * b)) {
1760 logicalX = a - 1;
1761 logicalY = i - ((a - 1) * b);
1762 }
1763
1764 TWindow w = sorted.get(i);
1765 w.setX(logicalX * newWidth);
1766 w.setWidth(newWidth);
1767 if (i >= ((a - 1) * b)) {
1768 w.setY((logicalY * newHeight2) + 1);
1769 w.setHeight(newHeight2);
1770 } else {
1771 w.setY((logicalY * newHeight1) + 1);
1772 w.setHeight(newHeight1);
1773 }
1774 }
1775 }
1776 }
1777
1778 /**
1779 * Re-layout the open windows as overlapping cascaded windows.
1780 */
1781 private void cascadeWindows() {
1782 synchronized (windows) {
1783 // Don't do anything if we are in the menu
1784 if (activeMenu != null) {
1785 return;
1786 }
1787 int x = 0;
1788 int y = 1;
1789 List<TWindow> sorted = new LinkedList<TWindow>(windows);
1790 Collections.sort(sorted);
1791 Collections.reverse(sorted);
1792 for (TWindow window: sorted) {
1793 window.setX(x);
1794 window.setY(y);
1795 x++;
1796 y++;
1797 if (x > getScreen().getWidth()) {
1798 x = 0;
1799 }
1800 if (y >= getScreen().getHeight()) {
1801 y = 1;
1802 }
1803 }
1804 }
1805 }
1806
1807 /**
1808 * Place a window to minimize its overlap with other windows.
1809 *
1810 * @param window the window to place
1811 */
1812 public final void doSmartPlacement(final TWindow window) {
1813 // This is a pretty dumb algorithm, but seems to work. The hardest
1814 // part is computing these "overlap" values seeking a minimum average
1815 // overlap.
1816 int xMin = 0;
1817 int yMin = desktopTop;
1818 int xMax = getScreen().getWidth() - window.getWidth() + 1;
1819 int yMax = desktopBottom - window.getHeight() + 1;
1820 if (xMax < xMin) {
1821 xMax = xMin;
1822 }
1823 if (yMax < yMin) {
1824 yMax = yMin;
1825 }
1826
1827 if ((xMin == xMax) && (yMin == yMax)) {
1828 // No work to do, bail out.
1829 return;
1830 }
1831
1832 // Compute the overlap matrix without the new window.
1833 int width = getScreen().getWidth();
1834 int height = getScreen().getHeight();
1835 int overlapMatrix[][] = new int[width][height];
1836 for (TWindow w: windows) {
1837 if (window == w) {
1838 continue;
1839 }
1840 for (int x = w.getX(); x < w.getX() + w.getWidth(); x++) {
1841 if (x >= width) {
1842 continue;
1843 }
1844 for (int y = w.getY(); y < w.getY() + w.getHeight(); y++) {
1845 if (y >= height) {
1846 continue;
1847 }
1848 overlapMatrix[x][y]++;
1849 }
1850 }
1851 }
1852
1853 long oldOverlapTotal = 0;
1854 long oldOverlapN = 0;
1855 for (int x = 0; x < width; x++) {
1856 for (int y = 0; y < height; y++) {
1857 oldOverlapTotal += overlapMatrix[x][y];
1858 if (overlapMatrix[x][y] > 0) {
1859 oldOverlapN++;
1860 }
1861 }
1862 }
1863
1864
1865 double oldOverlapAvg = (double) oldOverlapTotal / (double) oldOverlapN;
1866 boolean first = true;
1867 int windowX = window.getX();
1868 int windowY = window.getY();
1869
1870 // For each possible (x, y) position for the new window, compute a
1871 // new overlap matrix.
1872 for (int x = xMin; x < xMax; x++) {
1873 for (int y = yMin; y < yMax; y++) {
1874
1875 // Start with the matrix minus this window.
1876 int newMatrix[][] = new int[width][height];
1877 for (int mx = 0; mx < width; mx++) {
1878 for (int my = 0; my < height; my++) {
1879 newMatrix[mx][my] = overlapMatrix[mx][my];
1880 }
1881 }
1882
1883 // Add this window's values to the new overlap matrix.
1884 long newOverlapTotal = 0;
1885 long newOverlapN = 0;
1886 // Start by adding each new cell.
1887 for (int wx = x; wx < x + window.getWidth(); wx++) {
1888 if (wx >= width) {
1889 continue;
1890 }
1891 for (int wy = y; wy < y + window.getHeight(); wy++) {
1892 if (wy >= height) {
1893 continue;
1894 }
1895 newMatrix[wx][wy]++;
1896 }
1897 }
1898 // Now figure out the new value for total coverage.
1899 for (int mx = 0; mx < width; mx++) {
1900 for (int my = 0; my < height; my++) {
1901 newOverlapTotal += newMatrix[x][y];
1902 if (newMatrix[mx][my] > 0) {
1903 newOverlapN++;
1904 }
1905 }
1906 }
1907 double newOverlapAvg = (double) newOverlapTotal / (double) newOverlapN;
1908
1909 if (first) {
1910 // First time: just record what we got.
1911 oldOverlapAvg = newOverlapAvg;
1912 first = false;
1913 } else {
1914 // All other times: pick a new best (x, y) and save the
1915 // overlap value.
1916 if (newOverlapAvg < oldOverlapAvg) {
1917 windowX = x;
1918 windowY = y;
1919 oldOverlapAvg = newOverlapAvg;
1920 }
1921 }
1922
1923 } // for (int x = xMin; x < xMax; x++)
1924
1925 } // for (int y = yMin; y < yMax; y++)
1926
1927 // Finally, set the window's new coordinates.
1928 window.setX(windowX);
1929 window.setY(windowY);
1930 }
1931
1932 // ------------------------------------------------------------------------
1933 // TMenu management -------------------------------------------------------
1934 // ------------------------------------------------------------------------
1935
1936 /**
1937 * Check if a mouse event would hit either the active menu or any open
1938 * sub-menus.
1939 *
1940 * @param mouse mouse event
1941 * @return true if the mouse would hit the active menu or an open
1942 * sub-menu
1943 */
1944 private boolean mouseOnMenu(final TMouseEvent mouse) {
1945 assert (activeMenu != null);
1946 List<TMenu> menus = new LinkedList<TMenu>(subMenus);
1947 Collections.reverse(menus);
1948 for (TMenu menu: menus) {
1949 if (menu.mouseWouldHit(mouse)) {
1950 return true;
1951 }
1952 }
1953 return activeMenu.mouseWouldHit(mouse);
1954 }
1955
1956 /**
1957 * See if we need to switch window or activate the menu based on
1958 * a mouse click.
1959 *
1960 * @param mouse mouse event
1961 */
1962 private void checkSwitchFocus(final TMouseEvent mouse) {
1963
1964 if ((mouse.getType() == TMouseEvent.Type.MOUSE_DOWN)
1965 && (activeMenu != null)
1966 && (mouse.getAbsoluteY() != 0)
1967 && (!mouseOnMenu(mouse))
1968 ) {
1969 // They clicked outside the active menu, turn it off
1970 activeMenu.setActive(false);
1971 activeMenu = null;
1972 for (TMenu menu: subMenus) {
1973 menu.setActive(false);
1974 }
1975 subMenus.clear();
1976 // Continue checks
1977 }
1978
1979 // See if they hit the menu bar
1980 if ((mouse.getType() == TMouseEvent.Type.MOUSE_DOWN)
1981 && (mouse.isMouse1())
1982 && (!modalWindowActive())
1983 && (mouse.getAbsoluteY() == 0)
1984 ) {
1985
1986 for (TMenu menu: subMenus) {
1987 menu.setActive(false);
1988 }
1989 subMenus.clear();
1990
1991 // They selected the menu, go activate it
1992 for (TMenu menu: menus) {
1993 if ((mouse.getAbsoluteX() >= menu.getX())
1994 && (mouse.getAbsoluteX() < menu.getX()
1995 + menu.getTitle().length() + 2)
1996 ) {
1997 menu.setActive(true);
1998 activeMenu = menu;
1999 } else {
2000 menu.setActive(false);
2001 }
2002 }
2003 return;
2004 }
2005
2006 // See if they hit the menu bar
2007 if ((mouse.getType() == TMouseEvent.Type.MOUSE_MOTION)
2008 && (mouse.isMouse1())
2009 && (activeMenu != null)
2010 && (mouse.getAbsoluteY() == 0)
2011 ) {
2012
2013 TMenu oldMenu = activeMenu;
2014 for (TMenu menu: subMenus) {
2015 menu.setActive(false);
2016 }
2017 subMenus.clear();
2018
2019 // See if we should switch menus
2020 for (TMenu menu: menus) {
2021 if ((mouse.getAbsoluteX() >= menu.getX())
2022 && (mouse.getAbsoluteX() < menu.getX()
2023 + menu.getTitle().length() + 2)
2024 ) {
2025 menu.setActive(true);
2026 activeMenu = menu;
2027 }
2028 }
2029 if (oldMenu != activeMenu) {
2030 // They switched menus
2031 oldMenu.setActive(false);
2032 }
2033 return;
2034 }
2035
2036 // If a menu is still active, don't switch windows
2037 if (activeMenu != null) {
2038 return;
2039 }
2040
2041 // Only switch if there are multiple windows
2042 if (windows.size() < 2) {
2043 return;
2044 }
2045
2046 if (((focusFollowsMouse == true)
2047 && (mouse.getType() == TMouseEvent.Type.MOUSE_MOTION))
2048 || (mouse.getType() == TMouseEvent.Type.MOUSE_UP)
2049 ) {
2050 synchronized (windows) {
2051 Collections.sort(windows);
2052 if (windows.get(0).isModal()) {
2053 // Modal windows don't switch
2054 return;
2055 }
2056
2057 for (TWindow window: windows) {
2058 assert (!window.isModal());
2059
2060 if (window.isHidden()) {
2061 assert (!window.isActive());
2062 continue;
2063 }
2064
2065 if (window.mouseWouldHit(mouse)) {
2066 if (window == windows.get(0)) {
2067 // Clicked on the same window, nothing to do
2068 assert (window.isActive());
2069 return;
2070 }
2071
2072 // We will be switching to another window
2073 assert (windows.get(0).isActive());
2074 assert (windows.get(0) == activeWindow);
2075 assert (!window.isActive());
2076 activeWindow.onUnfocus();
2077 activeWindow.setActive(false);
2078 activeWindow.setZ(window.getZ());
2079 activeWindow = window;
2080 window.setZ(0);
2081 window.setActive(true);
2082 window.onFocus();
2083 return;
2084 }
2085 }
2086 }
2087
2088 // Clicked on the background, nothing to do
2089 return;
2090 }
2091
2092 // Nothing to do: this isn't a mouse up, or focus isn't following
2093 // mouse.
2094 return;
2095 }
2096
2097 /**
2098 * Turn off the menu.
2099 */
2100 public final void closeMenu() {
2101 if (activeMenu != null) {
2102 activeMenu.setActive(false);
2103 activeMenu = null;
2104 for (TMenu menu: subMenus) {
2105 menu.setActive(false);
2106 }
2107 subMenus.clear();
2108 }
2109 }
2110
2111 /**
2112 * Get a (shallow) copy of the menu list.
2113 *
2114 * @return a copy of the menu list
2115 */
2116 public final List<TMenu> getAllMenus() {
2117 return new LinkedList<TMenu>(menus);
2118 }
2119
2120 /**
2121 * Add a top-level menu to the list.
2122 *
2123 * @param menu the menu to add
2124 * @throws IllegalArgumentException if the menu is already used in
2125 * another TApplication
2126 */
2127 public final void addMenu(final TMenu menu) {
2128 if ((menu.getApplication() != null)
2129 && (menu.getApplication() != this)
2130 ) {
2131 throw new IllegalArgumentException("Menu " + menu + " is already " +
2132 "part of application " + menu.getApplication());
2133 }
2134 closeMenu();
2135 menus.add(menu);
2136 recomputeMenuX();
2137 }
2138
2139 /**
2140 * Remove a top-level menu from the list.
2141 *
2142 * @param menu the menu to remove
2143 * @throws IllegalArgumentException if the menu is already used in
2144 * another TApplication
2145 */
2146 public final void removeMenu(final TMenu menu) {
2147 if ((menu.getApplication() != null)
2148 && (menu.getApplication() != this)
2149 ) {
2150 throw new IllegalArgumentException("Menu " + menu + " is already " +
2151 "part of application " + menu.getApplication());
2152 }
2153 closeMenu();
2154 menus.remove(menu);
2155 recomputeMenuX();
2156 }
2157
2158 /**
2159 * Turn off a sub-menu.
2160 */
2161 public final void closeSubMenu() {
2162 assert (activeMenu != null);
2163 TMenu item = subMenus.get(subMenus.size() - 1);
2164 assert (item != null);
2165 item.setActive(false);
2166 subMenus.remove(subMenus.size() - 1);
2167 }
2168
2169 /**
2170 * Switch to the next menu.
2171 *
2172 * @param forward if true, then switch to the next menu in the list,
2173 * otherwise switch to the previous menu in the list
2174 */
2175 public final void switchMenu(final boolean forward) {
2176 assert (activeMenu != null);
2177
2178 for (TMenu menu: subMenus) {
2179 menu.setActive(false);
2180 }
2181 subMenus.clear();
2182
2183 for (int i = 0; i < menus.size(); i++) {
2184 if (activeMenu == menus.get(i)) {
2185 if (forward) {
2186 if (i < menus.size() - 1) {
2187 i++;
2188 }
2189 } else {
2190 if (i > 0) {
2191 i--;
2192 }
2193 }
2194 activeMenu.setActive(false);
2195 activeMenu = menus.get(i);
2196 activeMenu.setActive(true);
2197 return;
2198 }
2199 }
2200 }
2201
2202 /**
2203 * Add a menu item to the global list. If it has a keyboard accelerator,
2204 * that will be added the global hash.
2205 *
2206 * @param item the menu item
2207 */
2208 public final void addMenuItem(final TMenuItem item) {
2209 menuItems.add(item);
2210
2211 TKeypress key = item.getKey();
2212 if (key != null) {
2213 synchronized (accelerators) {
2214 assert (accelerators.get(key) == null);
2215 accelerators.put(key.toLowerCase(), item);
2216 }
2217 }
2218 }
2219
2220 /**
2221 * Disable one menu item.
2222 *
2223 * @param id the menu item ID
2224 */
2225 public final void disableMenuItem(final int id) {
2226 for (TMenuItem item: menuItems) {
2227 if (item.getId() == id) {
2228 item.setEnabled(false);
2229 }
2230 }
2231 }
2232
2233 /**
2234 * Disable the range of menu items with ID's between lower and upper,
2235 * inclusive.
2236 *
2237 * @param lower the lowest menu item ID
2238 * @param upper the highest menu item ID
2239 */
2240 public final void disableMenuItems(final int lower, final int upper) {
2241 for (TMenuItem item: menuItems) {
2242 if ((item.getId() >= lower) && (item.getId() <= upper)) {
2243 item.setEnabled(false);
2244 }
2245 }
2246 }
2247
2248 /**
2249 * Enable one menu item.
2250 *
2251 * @param id the menu item ID
2252 */
2253 public final void enableMenuItem(final int id) {
2254 for (TMenuItem item: menuItems) {
2255 if (item.getId() == id) {
2256 item.setEnabled(true);
2257 }
2258 }
2259 }
2260
2261 /**
2262 * Enable the range of menu items with ID's between lower and upper,
2263 * inclusive.
2264 *
2265 * @param lower the lowest menu item ID
2266 * @param upper the highest menu item ID
2267 */
2268 public final void enableMenuItems(final int lower, final int upper) {
2269 for (TMenuItem item: menuItems) {
2270 if ((item.getId() >= lower) && (item.getId() <= upper)) {
2271 item.setEnabled(true);
2272 }
2273 }
2274 }
2275
2276 /**
2277 * Recompute menu x positions based on their title length.
2278 */
2279 public final void recomputeMenuX() {
2280 int x = 0;
2281 for (TMenu menu: menus) {
2282 menu.setX(x);
2283 x += menu.getTitle().length() + 2;
2284 }
2285 }
2286
2287 /**
2288 * Post an event to process and turn off the menu.
2289 *
2290 * @param event new event to add to the queue
2291 */
2292 public final void postMenuEvent(final TInputEvent event) {
2293 synchronized (this) {
2294 synchronized (fillEventQueue) {
2295 fillEventQueue.add(event);
2296 }
2297 if (debugThreads) {
2298 System.err.println(System.currentTimeMillis() + " " +
2299 Thread.currentThread() + " postMenuEvent() wake up main");
2300 }
2301 closeMenu();
2302 this.notify();
2303 }
2304 }
2305
2306 /**
2307 * Add a sub-menu to the list of open sub-menus.
2308 *
2309 * @param menu sub-menu
2310 */
2311 public final void addSubMenu(final TMenu menu) {
2312 subMenus.add(menu);
2313 }
2314
2315 /**
2316 * Convenience function to add a top-level menu.
2317 *
2318 * @param title menu title
2319 * @return the new menu
2320 */
2321 public final TMenu addMenu(final String title) {
2322 int x = 0;
2323 int y = 0;
2324 TMenu menu = new TMenu(this, x, y, title);
2325 menus.add(menu);
2326 recomputeMenuX();
2327 return menu;
2328 }
2329
2330 /**
2331 * Convenience function to add a default "File" menu.
2332 *
2333 * @return the new menu
2334 */
2335 public final TMenu addFileMenu() {
2336 TMenu fileMenu = addMenu("&File");
2337 fileMenu.addDefaultItem(TMenu.MID_OPEN_FILE);
2338 fileMenu.addSeparator();
2339 fileMenu.addDefaultItem(TMenu.MID_SHELL);
2340 fileMenu.addDefaultItem(TMenu.MID_EXIT);
2341 TStatusBar statusBar = fileMenu.newStatusBar("File-management " +
2342 "commands (Open, Save, Print, etc.)");
2343 statusBar.addShortcutKeypress(kbF1, cmHelp, "Help");
2344 return fileMenu;
2345 }
2346
2347 /**
2348 * Convenience function to add a default "Edit" menu.
2349 *
2350 * @return the new menu
2351 */
2352 public final TMenu addEditMenu() {
2353 TMenu editMenu = addMenu("&Edit");
2354 editMenu.addDefaultItem(TMenu.MID_CUT);
2355 editMenu.addDefaultItem(TMenu.MID_COPY);
2356 editMenu.addDefaultItem(TMenu.MID_PASTE);
2357 editMenu.addDefaultItem(TMenu.MID_CLEAR);
2358 TStatusBar statusBar = editMenu.newStatusBar("Editor operations, " +
2359 "undo, and Clipboard access");
2360 statusBar.addShortcutKeypress(kbF1, cmHelp, "Help");
2361 return editMenu;
2362 }
2363
2364 /**
2365 * Convenience function to add a default "Window" menu.
2366 *
2367 * @return the new menu
2368 */
2369 public final TMenu addWindowMenu() {
2370 TMenu windowMenu = addMenu("&Window");
2371 windowMenu.addDefaultItem(TMenu.MID_TILE);
2372 windowMenu.addDefaultItem(TMenu.MID_CASCADE);
2373 windowMenu.addDefaultItem(TMenu.MID_CLOSE_ALL);
2374 windowMenu.addSeparator();
2375 windowMenu.addDefaultItem(TMenu.MID_WINDOW_MOVE);
2376 windowMenu.addDefaultItem(TMenu.MID_WINDOW_ZOOM);
2377 windowMenu.addDefaultItem(TMenu.MID_WINDOW_NEXT);
2378 windowMenu.addDefaultItem(TMenu.MID_WINDOW_PREVIOUS);
2379 windowMenu.addDefaultItem(TMenu.MID_WINDOW_CLOSE);
2380 TStatusBar statusBar = windowMenu.newStatusBar("Open, arrange, and " +
2381 "list windows");
2382 statusBar.addShortcutKeypress(kbF1, cmHelp, "Help");
2383 return windowMenu;
2384 }
2385
2386 /**
2387 * Convenience function to add a default "Help" menu.
2388 *
2389 * @return the new menu
2390 */
2391 public final TMenu addHelpMenu() {
2392 TMenu helpMenu = addMenu("&Help");
2393 helpMenu.addDefaultItem(TMenu.MID_HELP_CONTENTS);
2394 helpMenu.addDefaultItem(TMenu.MID_HELP_INDEX);
2395 helpMenu.addDefaultItem(TMenu.MID_HELP_SEARCH);
2396 helpMenu.addDefaultItem(TMenu.MID_HELP_PREVIOUS);
2397 helpMenu.addDefaultItem(TMenu.MID_HELP_HELP);
2398 helpMenu.addDefaultItem(TMenu.MID_HELP_ACTIVE_FILE);
2399 helpMenu.addSeparator();
2400 helpMenu.addDefaultItem(TMenu.MID_ABOUT);
2401 TStatusBar statusBar = helpMenu.newStatusBar("Access online help");
2402 statusBar.addShortcutKeypress(kbF1, cmHelp, "Help");
2403 return helpMenu;
2404 }
2405
2406 // ------------------------------------------------------------------------
2407 // Event handlers ---------------------------------------------------------
2408 // ------------------------------------------------------------------------
2409
2410 /**
2411 * Method that TApplication subclasses can override to handle menu or
2412 * posted command events.
2413 *
2414 * @param command command event
2415 * @return if true, this event was consumed
2416 */
2417 protected boolean onCommand(final TCommandEvent command) {
2418 // Default: handle cmExit
2419 if (command.equals(cmExit)) {
2420 if (messageBox("Confirmation", "Exit application?",
2421 TMessageBox.Type.YESNO).getResult() == TMessageBox.Result.YES) {
2422 exit();
2423 }
2424 return true;
2425 }
2426
2427 if (command.equals(cmShell)) {
2428 openTerminal(0, 0, TWindow.RESIZABLE);
2429 return true;
2430 }
2431
2432 if (command.equals(cmTile)) {
2433 tileWindows();
2434 return true;
2435 }
2436 if (command.equals(cmCascade)) {
2437 cascadeWindows();
2438 return true;
2439 }
2440 if (command.equals(cmCloseAll)) {
2441 closeAllWindows();
2442 return true;
2443 }
2444
2445 if (command.equals(cmMenu)) {
2446 if (!modalWindowActive() && (activeMenu == null)) {
2447 if (menus.size() > 0) {
2448 menus.get(0).setActive(true);
2449 activeMenu = menus.get(0);
2450 return true;
2451 }
2452 }
2453 }
2454
2455 return false;
2456 }
2457
2458 /**
2459 * Method that TApplication subclasses can override to handle menu
2460 * events.
2461 *
2462 * @param menu menu event
2463 * @return if true, this event was consumed
2464 */
2465 protected boolean onMenu(final TMenuEvent menu) {
2466
2467 // Default: handle MID_EXIT
2468 if (menu.getId() == TMenu.MID_EXIT) {
2469 if (messageBox("Confirmation", "Exit application?",
2470 TMessageBox.Type.YESNO).getResult() == TMessageBox.Result.YES) {
2471 exit();
2472 }
2473 return true;
2474 }
2475
2476 if (menu.getId() == TMenu.MID_SHELL) {
2477 openTerminal(0, 0, TWindow.RESIZABLE);
2478 return true;
2479 }
2480
2481 if (menu.getId() == TMenu.MID_TILE) {
2482 tileWindows();
2483 return true;
2484 }
2485 if (menu.getId() == TMenu.MID_CASCADE) {
2486 cascadeWindows();
2487 return true;
2488 }
2489 if (menu.getId() == TMenu.MID_CLOSE_ALL) {
2490 closeAllWindows();
2491 return true;
2492 }
2493 if (menu.getId() == TMenu.MID_ABOUT) {
2494 showAboutDialog();
2495 return true;
2496 }
2497 return false;
2498 }
2499
2500 /**
2501 * Method that TApplication subclasses can override to handle keystrokes.
2502 *
2503 * @param keypress keystroke event
2504 * @return if true, this event was consumed
2505 */
2506 protected boolean onKeypress(final TKeypressEvent keypress) {
2507 // Default: only menu shortcuts
2508
2509 // Process Alt-F, Alt-E, etc. menu shortcut keys
2510 if (!keypress.getKey().isFnKey()
2511 && keypress.getKey().isAlt()
2512 && !keypress.getKey().isCtrl()
2513 && (activeMenu == null)
2514 && !modalWindowActive()
2515 ) {
2516
2517 assert (subMenus.size() == 0);
2518
2519 for (TMenu menu: menus) {
2520 if (Character.toLowerCase(menu.getMnemonic().getShortcut())
2521 == Character.toLowerCase(keypress.getKey().getChar())
2522 ) {
2523 activeMenu = menu;
2524 menu.setActive(true);
2525 return true;
2526 }
2527 }
2528 }
2529
2530 return false;
2531 }
2532
2533 // ------------------------------------------------------------------------
2534 // TTimer management ------------------------------------------------------
2535 // ------------------------------------------------------------------------
2536
2537 /**
2538 * Get the amount of time I can sleep before missing a Timer tick.
2539 *
2540 * @param timeout = initial (maximum) timeout in millis
2541 * @return number of milliseconds between now and the next timer event
2542 */
2543 private long getSleepTime(final long timeout) {
2544 Date now = new Date();
2545 long nowTime = now.getTime();
2546 long sleepTime = timeout;
2547
2548 synchronized (timers) {
2549 for (TTimer timer: timers) {
2550 long nextTickTime = timer.getNextTick().getTime();
2551 if (nextTickTime < nowTime) {
2552 return 0;
2553 }
2554
2555 long timeDifference = nextTickTime - nowTime;
2556 if (timeDifference < sleepTime) {
2557 sleepTime = timeDifference;
2558 }
2559 }
2560 }
2561
2562 assert (sleepTime >= 0);
2563 assert (sleepTime <= timeout);
2564 return sleepTime;
2565 }
2566
2567 /**
2568 * Convenience function to add a timer.
2569 *
2570 * @param duration number of milliseconds to wait between ticks
2571 * @param recurring if true, re-schedule this timer after every tick
2572 * @param action function to call when button is pressed
2573 * @return the timer
2574 */
2575 public final TTimer addTimer(final long duration, final boolean recurring,
2576 final TAction action) {
2577
2578 TTimer timer = new TTimer(duration, recurring, action);
2579 synchronized (timers) {
2580 timers.add(timer);
2581 }
2582 return timer;
2583 }
2584
2585 /**
2586 * Convenience function to remove a timer.
2587 *
2588 * @param timer timer to remove
2589 */
2590 public final void removeTimer(final TTimer timer) {
2591 synchronized (timers) {
2592 timers.remove(timer);
2593 }
2594 }
2595
2596 // ------------------------------------------------------------------------
2597 // Other TWindow constructors ---------------------------------------------
2598 // ------------------------------------------------------------------------
2599
2600 /**
2601 * Convenience function to spawn a message box.
2602 *
2603 * @param title window title, will be centered along the top border
2604 * @param caption message to display. Use embedded newlines to get a
2605 * multi-line box.
2606 * @return the new message box
2607 */
2608 public final TMessageBox messageBox(final String title,
2609 final String caption) {
2610
2611 return new TMessageBox(this, title, caption, TMessageBox.Type.OK);
2612 }
2613
2614 /**
2615 * Convenience function to spawn a message box.
2616 *
2617 * @param title window title, will be centered along the top border
2618 * @param caption message to display. Use embedded newlines to get a
2619 * multi-line box.
2620 * @param type one of the TMessageBox.Type constants. Default is
2621 * Type.OK.
2622 * @return the new message box
2623 */
2624 public final TMessageBox messageBox(final String title,
2625 final String caption, final TMessageBox.Type type) {
2626
2627 return new TMessageBox(this, title, caption, type);
2628 }
2629
2630 /**
2631 * Convenience function to spawn an input box.
2632 *
2633 * @param title window title, will be centered along the top border
2634 * @param caption message to display. Use embedded newlines to get a
2635 * multi-line box.
2636 * @return the new input box
2637 */
2638 public final TInputBox inputBox(final String title, final String caption) {
2639
2640 return new TInputBox(this, title, caption);
2641 }
2642
2643 /**
2644 * Convenience function to spawn an input box.
2645 *
2646 * @param title window title, will be centered along the top border
2647 * @param caption message to display. Use embedded newlines to get a
2648 * multi-line box.
2649 * @param text initial text to seed the field with
2650 * @return the new input box
2651 */
2652 public final TInputBox inputBox(final String title, final String caption,
2653 final String text) {
2654
2655 return new TInputBox(this, title, caption, text);
2656 }
2657
2658 /**
2659 * Convenience function to open a terminal window.
2660 *
2661 * @param x column relative to parent
2662 * @param y row relative to parent
2663 * @return the terminal new window
2664 */
2665 public final TTerminalWindow openTerminal(final int x, final int y) {
2666 return openTerminal(x, y, TWindow.RESIZABLE);
2667 }
2668
2669 /**
2670 * Convenience function to open a terminal window.
2671 *
2672 * @param x column relative to parent
2673 * @param y row relative to parent
2674 * @param flags mask of CENTERED, MODAL, or RESIZABLE
2675 * @return the terminal new window
2676 */
2677 public final TTerminalWindow openTerminal(final int x, final int y,
2678 final int flags) {
2679
2680 return new TTerminalWindow(this, x, y, flags);
2681 }
2682
2683 /**
2684 * Convenience function to spawn an file open box.
2685 *
2686 * @param path path of selected file
2687 * @return the result of the new file open box
2688 * @throws IOException if java.io operation throws
2689 */
2690 public final String fileOpenBox(final String path) throws IOException {
2691
2692 TFileOpenBox box = new TFileOpenBox(this, path, TFileOpenBox.Type.OPEN);
2693 return box.getFilename();
2694 }
2695
2696 /**
2697 * Convenience function to spawn an file open box.
2698 *
2699 * @param path path of selected file
2700 * @param type one of the Type constants
2701 * @return the result of the new file open box
2702 * @throws IOException if java.io operation throws
2703 */
2704 public final String fileOpenBox(final String path,
2705 final TFileOpenBox.Type type) throws IOException {
2706
2707 TFileOpenBox box = new TFileOpenBox(this, path, type);
2708 return box.getFilename();
2709 }
2710
2711 /**
2712 * Convenience function to create a new window and make it active.
2713 * Window will be located at (0, 0).
2714 *
2715 * @param title window title, will be centered along the top border
2716 * @param width width of window
2717 * @param height height of window
2718 */
2719 public final TWindow addWindow(final String title, final int width,
2720 final int height) {
2721
2722 TWindow window = new TWindow(this, title, 0, 0, width, height);
2723 return window;
2724 }
2725 /**
2726 * Convenience function to create a new window and make it active.
2727 * Window will be located at (0, 0).
2728 *
2729 * @param title window title, will be centered along the top border
2730 * @param width width of window
2731 * @param height height of window
2732 * @param flags bitmask of RESIZABLE, CENTERED, or MODAL
2733 */
2734 public final TWindow addWindow(final String title,
2735 final int width, final int height, final int flags) {
2736
2737 TWindow window = new TWindow(this, title, 0, 0, width, height, flags);
2738 return window;
2739 }
2740
2741 /**
2742 * Convenience function to create a new window and make it active.
2743 *
2744 * @param title window title, will be centered along the top border
2745 * @param x column relative to parent
2746 * @param y row relative to parent
2747 * @param width width of window
2748 * @param height height of window
2749 */
2750 public final TWindow addWindow(final String title,
2751 final int x, final int y, final int width, final int height) {
2752
2753 TWindow window = new TWindow(this, title, x, y, width, height);
2754 return window;
2755 }
2756
2757 /**
2758 * Convenience function to create a new window and make it active.
2759 *
2760 * @param title window title, will be centered along the top border
2761 * @param x column relative to parent
2762 * @param y row relative to parent
2763 * @param width width of window
2764 * @param height height of window
2765 * @param flags mask of RESIZABLE, CENTERED, or MODAL
2766 */
2767 public final TWindow addWindow(final String title,
2768 final int x, final int y, final int width, final int height,
2769 final int flags) {
2770
2771 TWindow window = new TWindow(this, title, x, y, width, height, flags);
2772 return window;
2773 }
2774
2775 }