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