use a .summary file
[fanfix.git] / src / jexer / TApplication.java
... / ...
CommitLineData
1/*
2 * Jexer - Java Text User Interface
3 *
4 * The MIT License (MIT)
5 *
6 * Copyright (C) 2019 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.FileInputStream;
33import java.io.InputStream;
34import java.io.IOException;
35import java.io.OutputStream;
36import java.io.PrintWriter;
37import java.io.Reader;
38import java.io.UnsupportedEncodingException;
39import java.text.MessageFormat;
40import java.util.ArrayList;
41import java.util.Collections;
42import java.util.Date;
43import java.util.HashMap;
44import java.util.LinkedList;
45import java.util.List;
46import java.util.Map;
47import java.util.ResourceBundle;
48
49import jexer.bits.Cell;
50import jexer.bits.CellAttributes;
51import jexer.bits.Clipboard;
52import jexer.bits.ColorTheme;
53import jexer.bits.StringUtils;
54import jexer.event.TCommandEvent;
55import jexer.event.TInputEvent;
56import jexer.event.TKeypressEvent;
57import jexer.event.TMenuEvent;
58import jexer.event.TMouseEvent;
59import jexer.event.TResizeEvent;
60import jexer.backend.Backend;
61import jexer.backend.MultiBackend;
62import jexer.backend.Screen;
63import jexer.backend.SwingBackend;
64import jexer.backend.ECMA48Backend;
65import jexer.backend.TWindowBackend;
66import jexer.help.HelpFile;
67import jexer.help.Topic;
68import jexer.menu.TMenu;
69import jexer.menu.TMenuItem;
70import jexer.menu.TSubMenu;
71import static jexer.TCommand.*;
72import static jexer.TKeypress.*;
73
74/**
75 * TApplication is the main driver class for a full Text User Interface
76 * application. It manages windows, provides a menu bar and status bar, and
77 * processes events received from the user.
78 */
79public class TApplication implements Runnable {
80
81 /**
82 * Translated strings.
83 */
84 private static final ResourceBundle i18n = ResourceBundle.getBundle(TApplication.class.getName());
85
86 // ------------------------------------------------------------------------
87 // Constants --------------------------------------------------------------
88 // ------------------------------------------------------------------------
89
90 /**
91 * If true, emit thread stuff to System.err.
92 */
93 private static final boolean debugThreads = false;
94
95 /**
96 * If true, emit events being processed to System.err.
97 */
98 private static final boolean debugEvents = false;
99
100 /**
101 * If true, do "smart placement" on new windows that are not specified to
102 * be centered.
103 */
104 private static final boolean smartWindowPlacement = true;
105
106 /**
107 * Two backend types are available.
108 */
109 public static enum BackendType {
110 /**
111 * A Swing JFrame.
112 */
113 SWING,
114
115 /**
116 * An ECMA48 / ANSI X3.64 / XTERM style terminal.
117 */
118 ECMA48,
119
120 /**
121 * Synonym for ECMA48.
122 */
123 XTERM
124 }
125
126 // ------------------------------------------------------------------------
127 // Variables --------------------------------------------------------------
128 // ------------------------------------------------------------------------
129
130 /**
131 * The primary event handler thread.
132 */
133 private volatile WidgetEventHandler primaryEventHandler;
134
135 /**
136 * The secondary event handler thread.
137 */
138 private volatile WidgetEventHandler secondaryEventHandler;
139
140 /**
141 * The screen handler thread.
142 */
143 private volatile ScreenHandler screenHandler;
144
145 /**
146 * The widget receiving events from the secondary event handler thread.
147 */
148 private volatile TWidget secondaryEventReceiver;
149
150 /**
151 * Access to the physical screen, keyboard, and mouse.
152 */
153 private Backend backend;
154
155 /**
156 * The clipboard for copy and paste.
157 */
158 private Clipboard clipboard = new Clipboard();
159
160 /**
161 * Actual mouse coordinate X.
162 */
163 private int mouseX;
164
165 /**
166 * Actual mouse coordinate Y.
167 */
168 private int mouseY;
169
170 /**
171 * Old drawn version of mouse coordinate X.
172 */
173 private int oldDrawnMouseX;
174
175 /**
176 * Old drawn version mouse coordinate Y.
177 */
178 private int oldDrawnMouseY;
179
180 /**
181 * Old drawn version mouse cell.
182 */
183 private Cell oldDrawnMouseCell = new Cell();
184
185 /**
186 * The last mouse up click time, used to determine if this is a mouse
187 * double-click.
188 */
189 private long lastMouseUpTime;
190
191 /**
192 * The amount of millis between mouse up events to assume a double-click.
193 */
194 private long doubleClickTime = 250;
195
196 /**
197 * Event queue that is filled by run().
198 */
199 private List<TInputEvent> fillEventQueue;
200
201 /**
202 * Event queue that will be drained by either primary or secondary
203 * Thread.
204 */
205 private List<TInputEvent> drainEventQueue;
206
207 /**
208 * Top-level menus in this application.
209 */
210 private List<TMenu> menus;
211
212 /**
213 * Stack of activated sub-menus in this application.
214 */
215 private List<TMenu> subMenus;
216
217 /**
218 * The currently active menu.
219 */
220 private TMenu activeMenu = null;
221
222 /**
223 * Active keyboard accelerators.
224 */
225 private Map<TKeypress, TMenuItem> accelerators;
226
227 /**
228 * All menu items.
229 */
230 private List<TMenuItem> menuItems;
231
232 /**
233 * Windows and widgets pull colors from this ColorTheme.
234 */
235 private ColorTheme theme;
236
237 /**
238 * The top-level windows (but not menus).
239 */
240 private List<TWindow> windows;
241
242 /**
243 * Timers that are being ticked.
244 */
245 private List<TTimer> timers;
246
247 /**
248 * When true, the application has been started.
249 */
250 private volatile boolean started = false;
251
252 /**
253 * When true, exit the application.
254 */
255 private volatile boolean quit = false;
256
257 /**
258 * When true, repaint the entire screen.
259 */
260 private volatile boolean repaint = true;
261
262 /**
263 * Y coordinate of the top edge of the desktop. For now this is a
264 * constant. Someday it would be nice to have a multi-line menu or
265 * toolbars.
266 */
267 private int desktopTop = 1;
268
269 /**
270 * Y coordinate of the bottom edge of the desktop.
271 */
272 private int desktopBottom;
273
274 /**
275 * An optional TDesktop background window that is drawn underneath
276 * everything else.
277 */
278 private TDesktop desktop;
279
280 /**
281 * If true, focus follows mouse: windows automatically raised if the
282 * mouse passes over them.
283 */
284 private boolean focusFollowsMouse = false;
285
286 /**
287 * If true, display a text-based mouse cursor.
288 */
289 private boolean textMouse = true;
290
291 /**
292 * If true, hide the mouse after typing a keystroke.
293 */
294 private boolean hideMouseWhenTyping = false;
295
296 /**
297 * If true, the mouse should not be displayed because a keystroke was
298 * typed.
299 */
300 private boolean typingHidMouse = false;
301
302 /**
303 * If true, hide the status bar.
304 */
305 private boolean hideStatusBar = false;
306
307 /**
308 * If true, hide the menu bar.
309 */
310 private boolean hideMenuBar = false;
311
312 /**
313 * The list of commands to run before the next I/O check.
314 */
315 private List<Runnable> invokeLaters = new LinkedList<Runnable>();
316
317 /**
318 * The last time the screen was resized.
319 */
320 private long screenResizeTime = 0;
321
322 /**
323 * If true, screen selection is a rectangle.
324 */
325 private boolean screenSelectionRectangle = false;
326
327 /**
328 * If true, the mouse is dragging a screen selection.
329 */
330 private boolean inScreenSelection = false;
331
332 /**
333 * Screen selection starting X.
334 */
335 private int screenSelectionX0;
336
337 /**
338 * Screen selection starting Y.
339 */
340 private int screenSelectionY0;
341
342 /**
343 * Screen selection ending X.
344 */
345 private int screenSelectionX1;
346
347 /**
348 * Screen selection ending Y.
349 */
350 private int screenSelectionY1;
351
352 /**
353 * The help file data. Note package private access.
354 */
355 HelpFile helpFile;
356
357 /**
358 * The stack of help topics. Note package private access.
359 */
360 ArrayList<Topic> helpTopics = new ArrayList<Topic>();
361
362 /**
363 * WidgetEventHandler is the main event consumer loop. There are at most
364 * two such threads in existence: the primary for normal case and a
365 * secondary that is used for TMessageBox, TInputBox, and similar.
366 */
367 private class WidgetEventHandler implements Runnable {
368 /**
369 * The main application.
370 */
371 private TApplication application;
372
373 /**
374 * Whether or not this WidgetEventHandler is the primary or secondary
375 * thread.
376 */
377 private boolean primary = true;
378
379 /**
380 * Public constructor.
381 *
382 * @param application the main application
383 * @param primary if true, this is the primary event handler thread
384 */
385 public WidgetEventHandler(final TApplication application,
386 final boolean primary) {
387
388 this.application = application;
389 this.primary = primary;
390 }
391
392 /**
393 * The consumer loop.
394 */
395 public void run() {
396 // Wrap everything in a try, so that if we go belly up we can let
397 // the user have their terminal back.
398 try {
399 runImpl();
400 } catch (Throwable t) {
401 this.application.restoreConsole();
402 t.printStackTrace();
403 this.application.exit();
404 }
405 }
406
407 /**
408 * The consumer loop.
409 */
410 private void runImpl() {
411 boolean first = true;
412
413 // Loop forever
414 while (!application.quit) {
415
416 // Wait until application notifies me
417 while (!application.quit) {
418 try {
419 synchronized (application.drainEventQueue) {
420 if (application.drainEventQueue.size() > 0) {
421 break;
422 }
423 }
424
425 long timeout = 0;
426 if (first) {
427 first = false;
428 } else {
429 timeout = application.getSleepTime(1000);
430 }
431
432 if (timeout == 0) {
433 // A timer needs to fire, break out.
434 break;
435 }
436
437 if (debugThreads) {
438 System.err.printf("%d %s %s %s sleep %d millis\n",
439 System.currentTimeMillis(), this,
440 primary ? "primary" : "secondary",
441 Thread.currentThread(), timeout);
442 }
443
444 synchronized (this) {
445 this.wait(timeout);
446 }
447
448 if (debugThreads) {
449 System.err.printf("%d %s %s %s AWAKE\n",
450 System.currentTimeMillis(), this,
451 primary ? "primary" : "secondary",
452 Thread.currentThread());
453 }
454
455 if ((!primary)
456 && (application.secondaryEventReceiver == null)
457 ) {
458 // Secondary thread, emergency exit. If we got
459 // here then something went wrong with the
460 // handoff between yield() and closeWindow().
461 synchronized (application.primaryEventHandler) {
462 application.primaryEventHandler.notify();
463 }
464 application.secondaryEventHandler = null;
465 throw new RuntimeException("secondary exited " +
466 "at wrong time");
467 }
468 break;
469 } catch (InterruptedException e) {
470 // SQUASH
471 }
472 } // while (!application.quit)
473
474 // Pull all events off the queue
475 for (;;) {
476 TInputEvent event = null;
477 synchronized (application.drainEventQueue) {
478 if (application.drainEventQueue.size() == 0) {
479 break;
480 }
481 event = application.drainEventQueue.remove(0);
482 }
483
484 // We will have an event to process, so repaint the
485 // screen at the end.
486 application.repaint = true;
487
488 if (primary) {
489 primaryHandleEvent(event);
490 } else {
491 secondaryHandleEvent(event);
492 }
493 if ((!primary)
494 && (application.secondaryEventReceiver == null)
495 ) {
496 // Secondary thread, time to exit.
497
498 // Eliminate my reference so that wakeEventHandler()
499 // resumes working on the primary.
500 application.secondaryEventHandler = null;
501
502 // We are ready to exit, wake up the primary thread.
503 // Remember that it is currently sleeping inside its
504 // primaryHandleEvent().
505 synchronized (application.primaryEventHandler) {
506 application.primaryEventHandler.notify();
507 }
508
509 // All done!
510 return;
511 }
512
513 } // for (;;)
514
515 // Fire timers, update screen.
516 if (!quit) {
517 application.finishEventProcessing();
518 }
519
520 } // while (true) (main runnable loop)
521 }
522 }
523
524 /**
525 * ScreenHandler pushes screen updates to the physical device.
526 */
527 private class ScreenHandler implements Runnable {
528 /**
529 * The main application.
530 */
531 private TApplication application;
532
533 /**
534 * The dirty flag.
535 */
536 private boolean dirty = false;
537
538 /**
539 * Public constructor.
540 *
541 * @param application the main application
542 */
543 public ScreenHandler(final TApplication application) {
544 this.application = application;
545 }
546
547 /**
548 * The screen update loop.
549 */
550 public void run() {
551 // Wrap everything in a try, so that if we go belly up we can let
552 // the user have their terminal back.
553 try {
554 runImpl();
555 } catch (Throwable t) {
556 this.application.restoreConsole();
557 t.printStackTrace();
558 this.application.exit();
559 }
560 }
561
562 /**
563 * The update loop.
564 */
565 private void runImpl() {
566
567 // Loop forever
568 while (!application.quit) {
569
570 // Wait until application notifies me
571 while (!application.quit) {
572 try {
573 synchronized (this) {
574 if (dirty) {
575 dirty = false;
576 break;
577 }
578
579 // Always check within 50 milliseconds.
580 this.wait(50);
581 }
582 } catch (InterruptedException e) {
583 // SQUASH
584 }
585 } // while (!application.quit)
586
587 // Flush the screen contents
588 if (debugThreads) {
589 System.err.printf("%d %s backend.flushScreen()\n",
590 System.currentTimeMillis(), Thread.currentThread());
591 }
592 synchronized (getScreen()) {
593 backend.flushScreen();
594 }
595 } // while (true) (main runnable loop)
596
597 // Shutdown the user I/O thread(s)
598 backend.shutdown();
599 }
600
601 /**
602 * Set the dirty flag.
603 */
604 public void setDirty() {
605 synchronized (this) {
606 dirty = true;
607 }
608 }
609
610 }
611
612 // ------------------------------------------------------------------------
613 // Constructors -----------------------------------------------------------
614 // ------------------------------------------------------------------------
615
616 /**
617 * Public constructor.
618 *
619 * @param backendType BackendType.XTERM, BackendType.ECMA48 or
620 * BackendType.SWING
621 * @param windowWidth the number of text columns to start with
622 * @param windowHeight the number of text rows to start with
623 * @param fontSize the size in points
624 * @throws UnsupportedEncodingException if an exception is thrown when
625 * creating the InputStreamReader
626 */
627 public TApplication(final BackendType backendType, final int windowWidth,
628 final int windowHeight, final int fontSize)
629 throws UnsupportedEncodingException {
630
631 switch (backendType) {
632 case SWING:
633 backend = new SwingBackend(this, windowWidth, windowHeight,
634 fontSize);
635 break;
636 case XTERM:
637 // Fall through...
638 case ECMA48:
639 backend = new ECMA48Backend(this, null, null, windowWidth,
640 windowHeight, fontSize);
641 break;
642 default:
643 throw new IllegalArgumentException("Invalid backend type: "
644 + backendType);
645 }
646 TApplicationImpl();
647 }
648
649 /**
650 * Public constructor.
651 *
652 * @param backendType BackendType.XTERM, BackendType.ECMA48 or
653 * BackendType.SWING
654 * @throws UnsupportedEncodingException if an exception is thrown when
655 * creating the InputStreamReader
656 */
657 public TApplication(final BackendType backendType)
658 throws UnsupportedEncodingException {
659
660 switch (backendType) {
661 case SWING:
662 // The default SwingBackend is 80x25, 20 pt font. If you want to
663 // change that, you can pass the extra arguments to the
664 // SwingBackend constructor here. For example, if you wanted
665 // 90x30, 16 pt font:
666 //
667 // backend = new SwingBackend(this, 90, 30, 16);
668 backend = new SwingBackend(this);
669 break;
670 case XTERM:
671 // Fall through...
672 case ECMA48:
673 backend = new ECMA48Backend(this, null, null);
674 break;
675 default:
676 throw new IllegalArgumentException("Invalid backend type: "
677 + backendType);
678 }
679 TApplicationImpl();
680 }
681
682 /**
683 * Public constructor. The backend type will be BackendType.ECMA48.
684 *
685 * @param input an InputStream connected to the remote user, or null for
686 * System.in. If System.in is used, then on non-Windows systems it will
687 * be put in raw mode; shutdown() will (blindly!) put System.in in cooked
688 * mode. input is always converted to a Reader with UTF-8 encoding.
689 * @param output an OutputStream connected to the remote user, or null
690 * for System.out. output is always converted to a Writer with UTF-8
691 * encoding.
692 * @throws UnsupportedEncodingException if an exception is thrown when
693 * creating the InputStreamReader
694 */
695 public TApplication(final InputStream input,
696 final OutputStream output) throws UnsupportedEncodingException {
697
698 backend = new ECMA48Backend(this, input, output);
699 TApplicationImpl();
700 }
701
702 /**
703 * Public constructor. The backend type will be BackendType.ECMA48.
704 *
705 * @param input the InputStream underlying 'reader'. Its available()
706 * method is used to determine if reader.read() will block or not.
707 * @param reader a Reader connected to the remote user.
708 * @param writer a PrintWriter connected to the remote user.
709 * @param setRawMode if true, set System.in into raw mode with stty.
710 * This should in general not be used. It is here solely for Demo3,
711 * which uses System.in.
712 * @throws IllegalArgumentException if input, reader, or writer are null.
713 */
714 public TApplication(final InputStream input, final Reader reader,
715 final PrintWriter writer, final boolean setRawMode) {
716
717 backend = new ECMA48Backend(this, input, reader, writer, setRawMode);
718 TApplicationImpl();
719 }
720
721 /**
722 * Public constructor. The backend type will be BackendType.ECMA48.
723 *
724 * @param input the InputStream underlying 'reader'. Its available()
725 * method is used to determine if reader.read() will block or not.
726 * @param reader a Reader connected to the remote user.
727 * @param writer a PrintWriter connected to the remote user.
728 * @throws IllegalArgumentException if input, reader, or writer are null.
729 */
730 public TApplication(final InputStream input, final Reader reader,
731 final PrintWriter writer) {
732
733 this(input, reader, writer, false);
734 }
735
736 /**
737 * Public constructor. This hook enables use with new non-Jexer
738 * backends.
739 *
740 * @param backend a Backend that is already ready to go.
741 */
742 public TApplication(final Backend backend) {
743 this.backend = backend;
744 backend.setListener(this);
745 TApplicationImpl();
746 }
747
748 /**
749 * Finish construction once the backend is set.
750 */
751 private void TApplicationImpl() {
752 // Text block mouse option
753 if (System.getProperty("jexer.textMouse", "true").equals("false")) {
754 textMouse = false;
755 }
756
757 // Hide mouse when typing option
758 if (System.getProperty("jexer.hideMouseWhenTyping",
759 "false").equals("true")) {
760
761 hideMouseWhenTyping = true;
762 }
763
764 // Hide status bar option
765 if (System.getProperty("jexer.hideStatusBar",
766 "false").equals("true")) {
767 hideStatusBar = true;
768 }
769
770 // Hide menu bar option
771 if (System.getProperty("jexer.hideMenuBar", "false").equals("true")) {
772 hideMenuBar = true;
773 }
774
775 theme = new ColorTheme();
776 desktopTop = (hideMenuBar ? 0 : 1);
777 desktopBottom = getScreen().getHeight() - 1 + (hideStatusBar ? 1 : 0);
778 fillEventQueue = new LinkedList<TInputEvent>();
779 drainEventQueue = new LinkedList<TInputEvent>();
780 windows = new LinkedList<TWindow>();
781 menus = new ArrayList<TMenu>();
782 subMenus = new ArrayList<TMenu>();
783 timers = new LinkedList<TTimer>();
784 accelerators = new HashMap<TKeypress, TMenuItem>();
785 menuItems = new LinkedList<TMenuItem>();
786 desktop = new TDesktop(this);
787
788 // Special case: the Swing backend needs to have a timer to drive its
789 // blink state.
790 if ((backend instanceof SwingBackend)
791 || (backend instanceof MultiBackend)
792 ) {
793 // Default to 500 millis, unless a SwingBackend has its own
794 // value.
795 long millis = 500;
796 if (backend instanceof SwingBackend) {
797 millis = ((SwingBackend) backend).getBlinkMillis();
798 }
799 if (millis > 0) {
800 addTimer(millis, true,
801 new TAction() {
802 public void DO() {
803 TApplication.this.doRepaint();
804 }
805 }
806 );
807 }
808 }
809
810 // Load the help system
811 invokeLater(new Runnable() {
812 /*
813 * This isn't the best solution. But basically if a TApplication
814 * subclass constructor throws and needs to use TExceptionDialog,
815 * it may end up at the bottom of the window stack with a bunch
816 * of modal windows on top of it if said constructors spawn their
817 * windows also via invokeLater(). But if they don't do that,
818 * and instead just conventionally construct their windows, then
819 * this exception dialog will end up on top where it should be.
820 */
821 public void run() {
822 try {
823 ClassLoader loader = Thread.currentThread().getContextClassLoader();
824 helpFile = new HelpFile();
825 helpFile.load(loader.getResourceAsStream("help.xml"));
826 } catch (Exception e) {
827 new TExceptionDialog(TApplication.this, e);
828 }
829 }
830 });
831 }
832
833 // ------------------------------------------------------------------------
834 // Runnable ---------------------------------------------------------------
835 // ------------------------------------------------------------------------
836
837 /**
838 * Run this application until it exits.
839 */
840 public void run() {
841 // System.err.println("*** TApplication.run() begins ***");
842
843 // Start the screen updater thread
844 screenHandler = new ScreenHandler(this);
845 (new Thread(screenHandler)).start();
846
847 // Start the main consumer thread
848 primaryEventHandler = new WidgetEventHandler(this, true);
849 (new Thread(primaryEventHandler)).start();
850
851 started = true;
852
853 while (!quit) {
854 synchronized (this) {
855 boolean doWait = false;
856
857 if (!backend.hasEvents()) {
858 synchronized (fillEventQueue) {
859 if (fillEventQueue.size() == 0) {
860 doWait = true;
861 }
862 }
863 }
864
865 if (doWait) {
866 // No I/O to dispatch, so wait until the backend
867 // provides new I/O.
868 try {
869 if (debugThreads) {
870 System.err.println(System.currentTimeMillis() +
871 " " + Thread.currentThread() + " MAIN sleep");
872 }
873
874 this.wait();
875
876 if (debugThreads) {
877 System.err.println(System.currentTimeMillis() +
878 " " + Thread.currentThread() + " MAIN AWAKE");
879 }
880 } catch (InterruptedException e) {
881 // I'm awake and don't care why, let's see what's
882 // going on out there.
883 }
884 }
885
886 } // synchronized (this)
887
888 synchronized (fillEventQueue) {
889 // Pull any pending I/O events
890 backend.getEvents(fillEventQueue);
891
892 // Dispatch each event to the appropriate handler, one at a
893 // time.
894 for (;;) {
895 TInputEvent event = null;
896 if (fillEventQueue.size() == 0) {
897 break;
898 }
899 event = fillEventQueue.remove(0);
900 metaHandleEvent(event);
901 }
902 }
903
904 // Wake a consumer thread if we have any pending events.
905 if (drainEventQueue.size() > 0) {
906 wakeEventHandler();
907 }
908
909 } // while (!quit)
910
911 // Shutdown the event consumer threads
912 if (secondaryEventHandler != null) {
913 synchronized (secondaryEventHandler) {
914 secondaryEventHandler.notify();
915 }
916 }
917 if (primaryEventHandler != null) {
918 synchronized (primaryEventHandler) {
919 primaryEventHandler.notify();
920 }
921 }
922
923 // Close all the windows. This gives them an opportunity to release
924 // resources.
925 closeAllWindows();
926
927 // Close the desktop.
928 if (desktop != null) {
929 setDesktop(null);
930 }
931
932 // Give the overarching application an opportunity to release
933 // resources.
934 onExit();
935
936 // System.err.println("*** TApplication.run() exits ***");
937 }
938
939 // ------------------------------------------------------------------------
940 // Event handlers ---------------------------------------------------------
941 // ------------------------------------------------------------------------
942
943 /**
944 * Method that TApplication subclasses can override to handle menu or
945 * posted command events.
946 *
947 * @param command command event
948 * @return if true, this event was consumed
949 */
950 protected boolean onCommand(final TCommandEvent command) {
951 // Default: handle cmExit
952 if (command.equals(cmExit)) {
953 if (messageBox(i18n.getString("exitDialogTitle"),
954 i18n.getString("exitDialogText"),
955 TMessageBox.Type.YESNO).isYes()) {
956
957 exit();
958 }
959 return true;
960 }
961
962 if (command.equals(cmHelp)) {
963 if (getActiveWindow() != null) {
964 new THelpWindow(this, getActiveWindow().getHelpTopic());
965 } else {
966 new THelpWindow(this);
967 }
968 return true;
969 }
970
971 if (command.equals(cmShell)) {
972 openTerminal(0, 0, TWindow.RESIZABLE);
973 return true;
974 }
975
976 if (command.equals(cmTile)) {
977 tileWindows();
978 return true;
979 }
980 if (command.equals(cmCascade)) {
981 cascadeWindows();
982 return true;
983 }
984 if (command.equals(cmCloseAll)) {
985 closeAllWindows();
986 return true;
987 }
988
989 if (command.equals(cmMenu) && (hideMenuBar == false)) {
990 if (!modalWindowActive() && (activeMenu == null)) {
991 if (menus.size() > 0) {
992 menus.get(0).setActive(true);
993 activeMenu = menus.get(0);
994 return true;
995 }
996 }
997 }
998
999 return false;
1000 }
1001
1002 /**
1003 * Method that TApplication subclasses can override to handle menu
1004 * events.
1005 *
1006 * @param menu menu event
1007 * @return if true, this event was consumed
1008 */
1009 protected boolean onMenu(final TMenuEvent menu) {
1010
1011 // Default: handle MID_EXIT
1012 if (menu.getId() == TMenu.MID_EXIT) {
1013 if (messageBox(i18n.getString("exitDialogTitle"),
1014 i18n.getString("exitDialogText"),
1015 TMessageBox.Type.YESNO).isYes()) {
1016
1017 exit();
1018 }
1019 return true;
1020 }
1021
1022 if (menu.getId() == TMenu.MID_HELP_HELP) {
1023 new THelpWindow(this, THelpWindow.HELP_HELP);
1024 return true;
1025 }
1026
1027 if (menu.getId() == TMenu.MID_HELP_CONTENTS) {
1028 new THelpWindow(this, helpFile.getTableOfContents());
1029 return true;
1030 }
1031
1032 if (menu.getId() == TMenu.MID_HELP_INDEX) {
1033 new THelpWindow(this, helpFile.getIndex());
1034 return true;
1035 }
1036
1037 if (menu.getId() == TMenu.MID_HELP_SEARCH) {
1038 TInputBox inputBox = inputBox(i18n.
1039 getString("searchHelpInputBoxTitle"),
1040 i18n.getString("searchHelpInputBoxCaption"), "",
1041 TInputBox.Type.OKCANCEL);
1042 if (inputBox.isOk()) {
1043 new THelpWindow(this,
1044 helpFile.getSearchResults(inputBox.getText()));
1045 }
1046 return true;
1047 }
1048
1049 if (menu.getId() == TMenu.MID_HELP_PREVIOUS) {
1050 if (helpTopics.size() > 1) {
1051 Topic previous = helpTopics.remove(helpTopics.size() - 2);
1052 helpTopics.remove(helpTopics.size() - 1);
1053 new THelpWindow(this, previous);
1054 } else {
1055 new THelpWindow(this, helpFile.getTableOfContents());
1056 }
1057 return true;
1058 }
1059
1060 if (menu.getId() == TMenu.MID_HELP_ACTIVE_FILE) {
1061 try {
1062 List<String> filters = new ArrayList<String>();
1063 filters.add("^.*\\.[Xx][Mm][Ll]$");
1064 String filename = fileOpenBox(".", TFileOpenBox.Type.OPEN,
1065 filters);
1066 if (filename != null) {
1067 helpTopics = new ArrayList<Topic>();
1068 helpFile = new HelpFile();
1069 helpFile.load(new FileInputStream(filename));
1070 }
1071 } catch (Exception e) {
1072 // Show this exception to the user.
1073 new TExceptionDialog(this, e);
1074 }
1075 return true;
1076 }
1077
1078 if (menu.getId() == TMenu.MID_SHELL) {
1079 openTerminal(0, 0, TWindow.RESIZABLE);
1080 return true;
1081 }
1082
1083 if (menu.getId() == TMenu.MID_TILE) {
1084 tileWindows();
1085 return true;
1086 }
1087 if (menu.getId() == TMenu.MID_CASCADE) {
1088 cascadeWindows();
1089 return true;
1090 }
1091 if (menu.getId() == TMenu.MID_CLOSE_ALL) {
1092 closeAllWindows();
1093 return true;
1094 }
1095 if (menu.getId() == TMenu.MID_ABOUT) {
1096 showAboutDialog();
1097 return true;
1098 }
1099 if (menu.getId() == TMenu.MID_REPAINT) {
1100 getScreen().clearPhysical();
1101 doRepaint();
1102 return true;
1103 }
1104 if (menu.getId() == TMenu.MID_VIEW_IMAGE) {
1105 openImage();
1106 return true;
1107 }
1108 if (menu.getId() == TMenu.MID_SCREEN_OPTIONS) {
1109 new TFontChooserWindow(this);
1110 return true;
1111 }
1112
1113 if (menu.getId() == TMenu.MID_CUT) {
1114 postMenuEvent(new TCommandEvent(cmCut));
1115 return true;
1116 }
1117 if (menu.getId() == TMenu.MID_COPY) {
1118 postMenuEvent(new TCommandEvent(cmCopy));
1119 return true;
1120 }
1121 if (menu.getId() == TMenu.MID_PASTE) {
1122 postMenuEvent(new TCommandEvent(cmPaste));
1123 return true;
1124 }
1125 if (menu.getId() == TMenu.MID_CLEAR) {
1126 postMenuEvent(new TCommandEvent(cmClear));
1127 return true;
1128 }
1129
1130 return false;
1131 }
1132
1133 /**
1134 * Method that TApplication subclasses can override to handle keystrokes.
1135 *
1136 * @param keypress keystroke event
1137 * @return if true, this event was consumed
1138 */
1139 protected boolean onKeypress(final TKeypressEvent keypress) {
1140 // Default: only menu shortcuts
1141
1142 // Process Alt-F, Alt-E, etc. menu shortcut keys
1143 if (!keypress.getKey().isFnKey()
1144 && keypress.getKey().isAlt()
1145 && !keypress.getKey().isCtrl()
1146 && (activeMenu == null)
1147 && !modalWindowActive()
1148 && (hideMenuBar == false)
1149 ) {
1150
1151 assert (subMenus.size() == 0);
1152
1153 for (TMenu menu: menus) {
1154 if (Character.toLowerCase(menu.getMnemonic().getShortcut())
1155 == Character.toLowerCase(keypress.getKey().getChar())
1156 ) {
1157 activeMenu = menu;
1158 menu.setActive(true);
1159 return true;
1160 }
1161 }
1162 }
1163
1164 return false;
1165 }
1166
1167 /**
1168 * Process background events, and update the screen.
1169 */
1170 private void finishEventProcessing() {
1171 if (debugThreads) {
1172 System.err.printf(System.currentTimeMillis() + " " +
1173 Thread.currentThread() + " finishEventProcessing()\n");
1174 }
1175
1176 // See if we need to enable/disable the edit menu.
1177 EditMenuUser widget = null;
1178 if (activeMenu == null) {
1179 TWindow activeWindow = getActiveWindow();
1180 if (activeWindow != null) {
1181 if (activeWindow.getActiveChild() instanceof EditMenuUser) {
1182 widget = (EditMenuUser) activeWindow.getActiveChild();
1183 }
1184 } else if (desktop != null) {
1185 if (desktop.getActiveChild() instanceof EditMenuUser) {
1186 widget = (EditMenuUser) desktop.getActiveChild();
1187 }
1188 }
1189 if (widget == null) {
1190 disableMenuItem(TMenu.MID_CUT);
1191 disableMenuItem(TMenu.MID_COPY);
1192 disableMenuItem(TMenu.MID_PASTE);
1193 disableMenuItem(TMenu.MID_CLEAR);
1194 } else {
1195 if (widget.isEditMenuCut()) {
1196 enableMenuItem(TMenu.MID_CUT);
1197 } else {
1198 disableMenuItem(TMenu.MID_CUT);
1199 }
1200 if (widget.isEditMenuCopy()) {
1201 enableMenuItem(TMenu.MID_COPY);
1202 } else {
1203 disableMenuItem(TMenu.MID_COPY);
1204 }
1205 if (widget.isEditMenuPaste()) {
1206 enableMenuItem(TMenu.MID_PASTE);
1207 } else {
1208 disableMenuItem(TMenu.MID_PASTE);
1209 }
1210 if (widget.isEditMenuClear()) {
1211 enableMenuItem(TMenu.MID_CLEAR);
1212 } else {
1213 disableMenuItem(TMenu.MID_CLEAR);
1214 }
1215 }
1216 }
1217
1218 // Process timers and call doIdle()'s
1219 doIdle();
1220
1221 // Update the screen
1222 synchronized (getScreen()) {
1223 drawAll();
1224 }
1225
1226 // Wake up the screen repainter
1227 wakeScreenHandler();
1228
1229 if (debugThreads) {
1230 System.err.printf(System.currentTimeMillis() + " " +
1231 Thread.currentThread() + " finishEventProcessing() END\n");
1232 }
1233 }
1234
1235 /**
1236 * Peek at certain application-level events, add to eventQueue, and wake
1237 * up the consuming Thread.
1238 *
1239 * @param event the input event to consume
1240 */
1241 private void metaHandleEvent(final TInputEvent event) {
1242
1243 if (debugEvents) {
1244 System.err.printf(String.format("metaHandleEvents event: %s\n",
1245 event)); System.err.flush();
1246 }
1247
1248 if (quit) {
1249 // Do no more processing if the application is already trying
1250 // to exit.
1251 return;
1252 }
1253
1254 // Special application-wide events -------------------------------
1255
1256 // Abort everything
1257 if (event instanceof TCommandEvent) {
1258 TCommandEvent command = (TCommandEvent) event;
1259 if (command.equals(cmAbort)) {
1260 exit();
1261 return;
1262 }
1263 }
1264
1265 synchronized (drainEventQueue) {
1266 // Screen resize
1267 if (event instanceof TResizeEvent) {
1268 TResizeEvent resize = (TResizeEvent) event;
1269 synchronized (getScreen()) {
1270 if ((System.currentTimeMillis() - screenResizeTime >= 15)
1271 || (resize.getWidth() < getScreen().getWidth())
1272 || (resize.getHeight() < getScreen().getHeight())
1273 ) {
1274 getScreen().setDimensions(resize.getWidth(),
1275 resize.getHeight());
1276 screenResizeTime = System.currentTimeMillis();
1277 }
1278 desktopBottom = getScreen().getHeight() - 1;
1279 if (hideStatusBar) {
1280 desktopBottom++;
1281 }
1282 mouseX = 0;
1283 mouseY = 0;
1284 }
1285 if (desktop != null) {
1286 desktop.setDimensions(0, desktopTop, resize.getWidth(),
1287 (desktopBottom - desktopTop));
1288 desktop.onResize(resize);
1289 }
1290
1291 // Change menu edges if needed.
1292 recomputeMenuX();
1293
1294 // We are dirty, redraw the screen.
1295 doRepaint();
1296
1297 /*
1298 System.err.println("New screen: " + resize.getWidth() +
1299 " x " + resize.getHeight());
1300 */
1301 return;
1302 }
1303
1304 // Put into the main queue
1305 drainEventQueue.add(event);
1306 }
1307 }
1308
1309 /**
1310 * Dispatch one event to the appropriate widget or application-level
1311 * event handler. This is the primary event handler, it has the normal
1312 * application-wide event handling.
1313 *
1314 * @param event the input event to consume
1315 * @see #secondaryHandleEvent(TInputEvent event)
1316 */
1317 private void primaryHandleEvent(final TInputEvent event) {
1318
1319 if (debugEvents) {
1320 System.err.printf("%s primaryHandleEvent: %s\n",
1321 Thread.currentThread(), event);
1322 }
1323 TMouseEvent doubleClick = null;
1324
1325 // Special application-wide events -----------------------------------
1326
1327 if (event instanceof TKeypressEvent) {
1328 if (hideMouseWhenTyping) {
1329 typingHidMouse = true;
1330 }
1331 }
1332
1333 // Peek at the mouse position
1334 if (event instanceof TMouseEvent) {
1335 typingHidMouse = false;
1336
1337 TMouseEvent mouse = (TMouseEvent) event;
1338 if (mouse.isMouse1() && (mouse.isShift() || mouse.isCtrl())) {
1339 // Screen selection.
1340 if (inScreenSelection) {
1341 screenSelectionX1 = mouse.getX();
1342 screenSelectionY1 = mouse.getY();
1343 } else {
1344 inScreenSelection = true;
1345 screenSelectionX0 = mouse.getX();
1346 screenSelectionY0 = mouse.getY();
1347 screenSelectionX1 = mouse.getX();
1348 screenSelectionY1 = mouse.getY();
1349 screenSelectionRectangle = mouse.isCtrl();
1350 }
1351 } else {
1352 if (inScreenSelection) {
1353 getScreen().copySelection(clipboard, screenSelectionX0,
1354 screenSelectionY0, screenSelectionX1, screenSelectionY1,
1355 screenSelectionRectangle);
1356 }
1357 inScreenSelection = false;
1358 }
1359
1360 if ((mouseX != mouse.getX()) || (mouseY != mouse.getY())) {
1361 mouseX = mouse.getX();
1362 mouseY = mouse.getY();
1363 } else {
1364 if ((mouse.getType() == TMouseEvent.Type.MOUSE_DOWN)
1365 && (!mouse.isMouseWheelUp())
1366 && (!mouse.isMouseWheelDown())
1367 ) {
1368 if ((mouse.getTime().getTime() - lastMouseUpTime) <
1369 doubleClickTime) {
1370
1371 // This is a double-click.
1372 doubleClick = new TMouseEvent(TMouseEvent.Type.
1373 MOUSE_DOUBLE_CLICK,
1374 mouse.getX(), mouse.getY(),
1375 mouse.getAbsoluteX(), mouse.getAbsoluteY(),
1376 mouse.isMouse1(), mouse.isMouse2(),
1377 mouse.isMouse3(),
1378 mouse.isMouseWheelUp(), mouse.isMouseWheelDown(),
1379 mouse.isAlt(), mouse.isCtrl(), mouse.isShift());
1380
1381 } else {
1382 // The first click of a potential double-click.
1383 lastMouseUpTime = mouse.getTime().getTime();
1384 }
1385 }
1386 }
1387
1388 // See if we need to switch focus to another window or the menu
1389 checkSwitchFocus((TMouseEvent) event);
1390 }
1391
1392 // Handle menu events
1393 if ((activeMenu != null) && !(event instanceof TCommandEvent)) {
1394 TMenu menu = activeMenu;
1395
1396 if (event instanceof TMouseEvent) {
1397 TMouseEvent mouse = (TMouseEvent) event;
1398
1399 while (subMenus.size() > 0) {
1400 TMenu subMenu = subMenus.get(subMenus.size() - 1);
1401 if (subMenu.mouseWouldHit(mouse)) {
1402 break;
1403 }
1404 if ((mouse.getType() == TMouseEvent.Type.MOUSE_MOTION)
1405 && (!mouse.isMouse1())
1406 && (!mouse.isMouse2())
1407 && (!mouse.isMouse3())
1408 && (!mouse.isMouseWheelUp())
1409 && (!mouse.isMouseWheelDown())
1410 ) {
1411 break;
1412 }
1413 // We navigated away from a sub-menu, so close it
1414 closeSubMenu();
1415 }
1416
1417 // Convert the mouse relative x/y to menu coordinates
1418 assert (mouse.getX() == mouse.getAbsoluteX());
1419 assert (mouse.getY() == mouse.getAbsoluteY());
1420 if (subMenus.size() > 0) {
1421 menu = subMenus.get(subMenus.size() - 1);
1422 }
1423 mouse.setX(mouse.getX() - menu.getX());
1424 mouse.setY(mouse.getY() - menu.getY());
1425 }
1426 menu.handleEvent(event);
1427 return;
1428 }
1429
1430 if (event instanceof TKeypressEvent) {
1431 TKeypressEvent keypress = (TKeypressEvent) event;
1432
1433 // See if this key matches an accelerator, and is not being
1434 // shortcutted by the active window, and if so dispatch the menu
1435 // event.
1436 boolean windowWillShortcut = false;
1437 TWindow activeWindow = getActiveWindow();
1438 if (activeWindow != null) {
1439 assert (activeWindow.isShown());
1440 if (activeWindow.isShortcutKeypress(keypress.getKey())) {
1441 // We do not process this key, it will be passed to the
1442 // window instead.
1443 windowWillShortcut = true;
1444 }
1445 }
1446
1447 if (!windowWillShortcut && !modalWindowActive()) {
1448 TKeypress keypressLowercase = keypress.getKey().toLowerCase();
1449 TMenuItem item = null;
1450 synchronized (accelerators) {
1451 item = accelerators.get(keypressLowercase);
1452 }
1453 if (item != null) {
1454 if (item.isEnabled()) {
1455 // Let the menu item dispatch
1456 item.dispatch();
1457 return;
1458 }
1459 }
1460
1461 // Handle the keypress
1462 if (onKeypress(keypress)) {
1463 return;
1464 }
1465 }
1466 }
1467
1468 if (event instanceof TCommandEvent) {
1469 if (onCommand((TCommandEvent) event)) {
1470 return;
1471 }
1472 }
1473
1474 if (event instanceof TMenuEvent) {
1475 if (onMenu((TMenuEvent) event)) {
1476 return;
1477 }
1478 }
1479
1480 // Dispatch events to the active window -------------------------------
1481 boolean dispatchToDesktop = true;
1482 TWindow window = getActiveWindow();
1483 if (window != null) {
1484 assert (window.isActive());
1485 assert (window.isShown());
1486 if (event instanceof TMouseEvent) {
1487 TMouseEvent mouse = (TMouseEvent) event;
1488 // Convert the mouse relative x/y to window coordinates
1489 assert (mouse.getX() == mouse.getAbsoluteX());
1490 assert (mouse.getY() == mouse.getAbsoluteY());
1491 mouse.setX(mouse.getX() - window.getX());
1492 mouse.setY(mouse.getY() - window.getY());
1493
1494 if (doubleClick != null) {
1495 doubleClick.setX(doubleClick.getX() - window.getX());
1496 doubleClick.setY(doubleClick.getY() - window.getY());
1497 }
1498
1499 if (window.mouseWouldHit(mouse)) {
1500 dispatchToDesktop = false;
1501 }
1502 } else if (event instanceof TKeypressEvent) {
1503 dispatchToDesktop = false;
1504 } else if (event instanceof TMenuEvent) {
1505 dispatchToDesktop = false;
1506 }
1507
1508 if (debugEvents) {
1509 System.err.printf("TApplication dispatch event: %s\n",
1510 event);
1511 }
1512 window.handleEvent(event);
1513 if (doubleClick != null) {
1514 window.handleEvent(doubleClick);
1515 }
1516 }
1517 if (dispatchToDesktop) {
1518 // This event is fair game for the desktop to process.
1519 if (desktop != null) {
1520 desktop.handleEvent(event);
1521 if (doubleClick != null) {
1522 desktop.handleEvent(doubleClick);
1523 }
1524 }
1525 }
1526 }
1527
1528 /**
1529 * Dispatch one event to the appropriate widget or application-level
1530 * event handler. This is the secondary event handler used by certain
1531 * special dialogs (currently TMessageBox and TFileOpenBox).
1532 *
1533 * @param event the input event to consume
1534 * @see #primaryHandleEvent(TInputEvent event)
1535 */
1536 private void secondaryHandleEvent(final TInputEvent event) {
1537 TMouseEvent doubleClick = null;
1538
1539 if (debugEvents) {
1540 System.err.printf("%s secondaryHandleEvent: %s\n",
1541 Thread.currentThread(), event);
1542 }
1543
1544 // Peek at the mouse position
1545 if (event instanceof TMouseEvent) {
1546 typingHidMouse = false;
1547
1548 TMouseEvent mouse = (TMouseEvent) event;
1549 if ((mouseX != mouse.getX()) || (mouseY != mouse.getY())) {
1550 mouseX = mouse.getX();
1551 mouseY = mouse.getY();
1552 } else {
1553 if ((mouse.getType() == TMouseEvent.Type.MOUSE_DOWN)
1554 && (!mouse.isMouseWheelUp())
1555 && (!mouse.isMouseWheelDown())
1556 ) {
1557 if ((mouse.getTime().getTime() - lastMouseUpTime) <
1558 doubleClickTime) {
1559
1560 // This is a double-click.
1561 doubleClick = new TMouseEvent(TMouseEvent.Type.
1562 MOUSE_DOUBLE_CLICK,
1563 mouse.getX(), mouse.getY(),
1564 mouse.getAbsoluteX(), mouse.getAbsoluteY(),
1565 mouse.isMouse1(), mouse.isMouse2(),
1566 mouse.isMouse3(),
1567 mouse.isMouseWheelUp(), mouse.isMouseWheelDown(),
1568 mouse.isAlt(), mouse.isCtrl(), mouse.isShift());
1569
1570 } else {
1571 // The first click of a potential double-click.
1572 lastMouseUpTime = mouse.getTime().getTime();
1573 }
1574 }
1575 }
1576 }
1577
1578 secondaryEventReceiver.handleEvent(event);
1579 // Note that it is possible for secondaryEventReceiver to be null
1580 // now, because its handleEvent() might have finished out on the
1581 // secondary thread. So put any extra processing inside a null
1582 // check.
1583 if (secondaryEventReceiver != null) {
1584 if (doubleClick != null) {
1585 secondaryEventReceiver.handleEvent(doubleClick);
1586 }
1587 }
1588 }
1589
1590 /**
1591 * Enable a widget to override the primary event thread.
1592 *
1593 * @param widget widget that will receive events
1594 */
1595 public final void enableSecondaryEventReceiver(final TWidget widget) {
1596 if (debugThreads) {
1597 System.err.println(System.currentTimeMillis() +
1598 " enableSecondaryEventReceiver()");
1599 }
1600
1601 assert (secondaryEventReceiver == null);
1602 assert (secondaryEventHandler == null);
1603 assert ((widget instanceof TMessageBox)
1604 || (widget instanceof TFileOpenBox));
1605 secondaryEventReceiver = widget;
1606 secondaryEventHandler = new WidgetEventHandler(this, false);
1607
1608 (new Thread(secondaryEventHandler)).start();
1609 }
1610
1611 /**
1612 * Yield to the secondary thread.
1613 */
1614 public final void yield() {
1615 if (debugThreads) {
1616 System.err.printf(System.currentTimeMillis() + " " +
1617 Thread.currentThread() + " yield()\n");
1618 }
1619
1620 assert (secondaryEventReceiver != null);
1621
1622 while (secondaryEventReceiver != null) {
1623 synchronized (primaryEventHandler) {
1624 try {
1625 primaryEventHandler.wait();
1626 } catch (InterruptedException e) {
1627 // SQUASH
1628 }
1629 }
1630 }
1631 }
1632
1633 /**
1634 * Do stuff when there is no user input.
1635 */
1636 private void doIdle() {
1637 if (debugThreads) {
1638 System.err.printf(System.currentTimeMillis() + " " +
1639 Thread.currentThread() + " doIdle()\n");
1640 }
1641
1642 synchronized (timers) {
1643
1644 if (debugThreads) {
1645 System.err.printf(System.currentTimeMillis() + " " +
1646 Thread.currentThread() + " doIdle() 2\n");
1647 }
1648
1649 // Run any timers that have timed out
1650 Date now = new Date();
1651 List<TTimer> keepTimers = new LinkedList<TTimer>();
1652 for (TTimer timer: timers) {
1653 if (timer.getNextTick().getTime() <= now.getTime()) {
1654 // Something might change, so repaint the screen.
1655 repaint = true;
1656 timer.tick();
1657 if (timer.recurring) {
1658 keepTimers.add(timer);
1659 }
1660 } else {
1661 keepTimers.add(timer);
1662 }
1663 }
1664 timers.clear();
1665 timers.addAll(keepTimers);
1666 }
1667
1668 // Call onIdle's
1669 for (TWindow window: windows) {
1670 window.onIdle();
1671 }
1672 if (desktop != null) {
1673 desktop.onIdle();
1674 }
1675
1676 // Run any invokeLaters. We make a copy, and run that, because one
1677 // of these Runnables might add call TApplication.invokeLater().
1678 List<Runnable> invokes = new ArrayList<Runnable>();
1679 synchronized (invokeLaters) {
1680 invokes.addAll(invokeLaters);
1681 invokeLaters.clear();
1682 }
1683 for (Runnable invoke: invokes) {
1684 invoke.run();
1685 }
1686 doRepaint();
1687
1688 }
1689
1690 /**
1691 * Wake the sleeping active event handler.
1692 */
1693 private void wakeEventHandler() {
1694 if (!started) {
1695 return;
1696 }
1697
1698 if (secondaryEventHandler != null) {
1699 synchronized (secondaryEventHandler) {
1700 secondaryEventHandler.notify();
1701 }
1702 } else {
1703 assert (primaryEventHandler != null);
1704 synchronized (primaryEventHandler) {
1705 primaryEventHandler.notify();
1706 }
1707 }
1708 }
1709
1710 /**
1711 * Wake the sleeping screen handler.
1712 */
1713 private void wakeScreenHandler() {
1714 if (!started) {
1715 return;
1716 }
1717
1718 synchronized (screenHandler) {
1719 screenHandler.notify();
1720 }
1721 }
1722
1723 // ------------------------------------------------------------------------
1724 // TApplication -----------------------------------------------------------
1725 // ------------------------------------------------------------------------
1726
1727 /**
1728 * Place a command on the run queue, and run it before the next round of
1729 * checking I/O.
1730 *
1731 * @param command the command to run later
1732 */
1733 public void invokeLater(final Runnable command) {
1734 synchronized (invokeLaters) {
1735 invokeLaters.add(command);
1736 }
1737 doRepaint();
1738 }
1739
1740 /**
1741 * Restore the console to sane defaults. This is meant to be used for
1742 * improper exits (e.g. a caught exception in main()), and should not be
1743 * necessary for normal program termination.
1744 */
1745 public void restoreConsole() {
1746 if (backend != null) {
1747 if (backend instanceof ECMA48Backend) {
1748 backend.shutdown();
1749 }
1750 }
1751 }
1752
1753 /**
1754 * Get the Backend.
1755 *
1756 * @return the Backend
1757 */
1758 public final Backend getBackend() {
1759 return backend;
1760 }
1761
1762 /**
1763 * Get the Screen.
1764 *
1765 * @return the Screen
1766 */
1767 public final Screen getScreen() {
1768 if (backend instanceof TWindowBackend) {
1769 // We are being rendered to a TWindow. We can't use its
1770 // getScreen() method because that is how it is rendering to a
1771 // hardware backend somewhere. Instead use its getOtherScreen()
1772 // method.
1773 return ((TWindowBackend) backend).getOtherScreen();
1774 } else {
1775 return backend.getScreen();
1776 }
1777 }
1778
1779 /**
1780 * Get the color theme.
1781 *
1782 * @return the theme
1783 */
1784 public final ColorTheme getTheme() {
1785 return theme;
1786 }
1787
1788 /**
1789 * Get the clipboard.
1790 *
1791 * @return the clipboard
1792 */
1793 public final Clipboard getClipboard() {
1794 return clipboard;
1795 }
1796
1797 /**
1798 * Repaint the screen on the next update.
1799 */
1800 public void doRepaint() {
1801 repaint = true;
1802 wakeEventHandler();
1803 }
1804
1805 /**
1806 * Get Y coordinate of the top edge of the desktop.
1807 *
1808 * @return Y coordinate of the top edge of the desktop
1809 */
1810 public final int getDesktopTop() {
1811 return desktopTop;
1812 }
1813
1814 /**
1815 * Get Y coordinate of the bottom edge of the desktop.
1816 *
1817 * @return Y coordinate of the bottom edge of the desktop
1818 */
1819 public final int getDesktopBottom() {
1820 return desktopBottom;
1821 }
1822
1823 /**
1824 * Set the TDesktop instance.
1825 *
1826 * @param desktop a TDesktop instance, or null to remove the one that is
1827 * set
1828 */
1829 public final void setDesktop(final TDesktop desktop) {
1830 if (this.desktop != null) {
1831 this.desktop.onPreClose();
1832 this.desktop.onUnfocus();
1833 this.desktop.onClose();
1834 }
1835 this.desktop = desktop;
1836 }
1837
1838 /**
1839 * Get the TDesktop instance.
1840 *
1841 * @return the desktop, or null if it is not set
1842 */
1843 public final TDesktop getDesktop() {
1844 return desktop;
1845 }
1846
1847 /**
1848 * Get the current active window.
1849 *
1850 * @return the active window, or null if it is not set
1851 */
1852 public final TWindow getActiveWindow() {
1853 for (TWindow window: windows) {
1854 if (window.isShown() && window.isActive()) {
1855 return window;
1856 }
1857 }
1858 return null;
1859 }
1860
1861 /**
1862 * Get a (shallow) copy of the window list.
1863 *
1864 * @return a copy of the list of windows for this application
1865 */
1866 public final List<TWindow> getAllWindows() {
1867 List<TWindow> result = new ArrayList<TWindow>();
1868 result.addAll(windows);
1869 return result;
1870 }
1871
1872 /**
1873 * Get focusFollowsMouse flag.
1874 *
1875 * @return true if focus follows mouse: windows automatically raised if
1876 * the mouse passes over them
1877 */
1878 public boolean getFocusFollowsMouse() {
1879 return focusFollowsMouse;
1880 }
1881
1882 /**
1883 * Set focusFollowsMouse flag.
1884 *
1885 * @param focusFollowsMouse if true, focus follows mouse: windows
1886 * automatically raised if the mouse passes over them
1887 */
1888 public void setFocusFollowsMouse(final boolean focusFollowsMouse) {
1889 this.focusFollowsMouse = focusFollowsMouse;
1890 }
1891
1892 /**
1893 * Display the about dialog.
1894 */
1895 protected void showAboutDialog() {
1896 String version = getClass().getPackage().getImplementationVersion();
1897 if (version == null) {
1898 // This is Java 9+, use a hardcoded string here.
1899 version = "1.0.0";
1900 }
1901 messageBox(i18n.getString("aboutDialogTitle"),
1902 MessageFormat.format(i18n.getString("aboutDialogText"), version),
1903 TMessageBox.Type.OK);
1904 }
1905
1906 /**
1907 * Handle the Tool | Open image menu item.
1908 */
1909 private void openImage() {
1910 try {
1911 List<String> filters = new ArrayList<String>();
1912 filters.add("^.*\\.[Jj][Pp][Gg]$");
1913 filters.add("^.*\\.[Jj][Pp][Ee][Gg]$");
1914 filters.add("^.*\\.[Pp][Nn][Gg]$");
1915 filters.add("^.*\\.[Gg][Ii][Ff]$");
1916 filters.add("^.*\\.[Bb][Mm][Pp]$");
1917 String filename = fileOpenBox(".", TFileOpenBox.Type.OPEN, filters);
1918 if (filename != null) {
1919 new TImageWindow(this, new File(filename));
1920 }
1921 } catch (IOException e) {
1922 // Show this exception to the user.
1923 new TExceptionDialog(this, e);
1924 }
1925 }
1926
1927 /**
1928 * Check if application is still running.
1929 *
1930 * @return true if the application is running
1931 */
1932 public final boolean isRunning() {
1933 if (quit == true) {
1934 return false;
1935 }
1936 return true;
1937 }
1938
1939 // ------------------------------------------------------------------------
1940 // Screen refresh loop ----------------------------------------------------
1941 // ------------------------------------------------------------------------
1942
1943 /**
1944 * Draw the text mouse at position.
1945 *
1946 * @param x column position
1947 * @param y row position
1948 */
1949 private void drawTextMouse(final int x, final int y) {
1950 TWindow activeWindow = getActiveWindow();
1951
1952 if (debugThreads) {
1953 System.err.printf("%d %s drawTextMouse() %d %d\n",
1954 System.currentTimeMillis(), Thread.currentThread(), x, y);
1955
1956 if (activeWindow != null) {
1957 System.err.println("activeWindow.hasHiddenMouse() " +
1958 activeWindow.hasHiddenMouse());
1959 }
1960 }
1961
1962 // If this cell is on top of a visible window that has requested a
1963 // hidden mouse, bail out.
1964 if ((activeWindow != null) && (activeMenu == null)) {
1965 if ((activeWindow.hasHiddenMouse() == true)
1966 && (x > activeWindow.getX())
1967 && (x < activeWindow.getX() + activeWindow.getWidth() - 1)
1968 && (y > activeWindow.getY())
1969 && (y < activeWindow.getY() + activeWindow.getHeight() - 1)
1970 ) {
1971 return;
1972 }
1973 }
1974
1975 // If this cell is on top of the desktop, and the desktop has
1976 // requested a hidden mouse, bail out.
1977 if ((desktop != null) && (activeWindow == null) && (activeMenu == null)) {
1978 if ((desktop.hasHiddenMouse() == true)
1979 && (x > desktop.getX())
1980 && (x < desktop.getX() + desktop.getWidth() - 1)
1981 && (y > desktop.getY())
1982 && (y < desktop.getY() + desktop.getHeight() - 1)
1983 ) {
1984 return;
1985 }
1986 }
1987
1988 getScreen().invertCell(x, y);
1989 }
1990
1991 /**
1992 * Draw everything.
1993 */
1994 private void drawAll() {
1995 boolean menuIsActive = false;
1996
1997 if (debugThreads) {
1998 System.err.printf("%d %s drawAll() enter\n",
1999 System.currentTimeMillis(), Thread.currentThread());
2000 }
2001
2002 // I don't think this does anything useful anymore...
2003 if (!repaint) {
2004 if (debugThreads) {
2005 System.err.printf("%d %s drawAll() !repaint\n",
2006 System.currentTimeMillis(), Thread.currentThread());
2007 }
2008 if ((oldDrawnMouseX != mouseX) || (oldDrawnMouseY != mouseY)) {
2009 if (debugThreads) {
2010 System.err.printf("%d %s drawAll() !repaint MOUSE\n",
2011 System.currentTimeMillis(), Thread.currentThread());
2012 }
2013
2014 // The only thing that has happened is the mouse moved.
2015
2016 // Redraw the old cell at that position, and save the cell at
2017 // the new mouse position.
2018 if (debugThreads) {
2019 System.err.printf("%d %s restoreImage() %d %d\n",
2020 System.currentTimeMillis(), Thread.currentThread(),
2021 oldDrawnMouseX, oldDrawnMouseY);
2022 }
2023 oldDrawnMouseCell.restoreImage();
2024 getScreen().putCharXY(oldDrawnMouseX, oldDrawnMouseY,
2025 oldDrawnMouseCell);
2026 oldDrawnMouseCell = getScreen().getCharXY(mouseX, mouseY);
2027 if (backend instanceof ECMA48Backend) {
2028 // Special case: the entire row containing the mouse has
2029 // to be re-drawn if it has any image data, AND any rows
2030 // in between.
2031 if (oldDrawnMouseY != mouseY) {
2032 for (int i = oldDrawnMouseY; ;) {
2033 getScreen().unsetImageRow(i);
2034 if (i == mouseY) {
2035 break;
2036 }
2037 if (oldDrawnMouseY < mouseY) {
2038 i++;
2039 } else {
2040 i--;
2041 }
2042 }
2043 } else {
2044 getScreen().unsetImageRow(mouseY);
2045 }
2046 }
2047
2048 if (inScreenSelection) {
2049 getScreen().setSelection(screenSelectionX0,
2050 screenSelectionY0, screenSelectionX1, screenSelectionY1,
2051 screenSelectionRectangle);
2052 }
2053
2054 if ((textMouse == true) && (typingHidMouse == false)) {
2055 // Draw mouse at the new position.
2056 drawTextMouse(mouseX, mouseY);
2057 }
2058
2059 oldDrawnMouseX = mouseX;
2060 oldDrawnMouseY = mouseY;
2061 }
2062 if (getScreen().isDirty()) {
2063 screenHandler.setDirty();
2064 }
2065 return;
2066 }
2067
2068 if (debugThreads) {
2069 System.err.printf("%d %s drawAll() REDRAW\n",
2070 System.currentTimeMillis(), Thread.currentThread());
2071 }
2072
2073 // If true, the cursor is not visible
2074 boolean cursor = false;
2075
2076 // Start with a clean screen
2077 getScreen().clear();
2078
2079 // Draw the desktop
2080 if (desktop != null) {
2081 desktop.drawChildren();
2082 }
2083
2084 // Draw each window in reverse Z order
2085 List<TWindow> sorted = new ArrayList<TWindow>(windows);
2086 Collections.sort(sorted);
2087 TWindow topLevel = null;
2088 if (sorted.size() > 0) {
2089 topLevel = sorted.get(0);
2090 }
2091 Collections.reverse(sorted);
2092 for (TWindow window: sorted) {
2093 if (window.isShown()) {
2094 window.drawChildren();
2095 }
2096 }
2097
2098 if (hideMenuBar == false) {
2099
2100 // Draw the blank menubar line - reset the screen clipping first
2101 // so it won't trim it out.
2102 getScreen().resetClipping();
2103 getScreen().hLineXY(0, 0, getScreen().getWidth(), ' ',
2104 theme.getColor("tmenu"));
2105 // Now draw the menus.
2106 int x = 1;
2107 for (TMenu menu: menus) {
2108 CellAttributes menuColor;
2109 CellAttributes menuMnemonicColor;
2110 if (menu.isActive()) {
2111 menuIsActive = true;
2112 menuColor = theme.getColor("tmenu.highlighted");
2113 menuMnemonicColor = theme.getColor("tmenu.mnemonic.highlighted");
2114 topLevel = menu;
2115 } else {
2116 menuColor = theme.getColor("tmenu");
2117 menuMnemonicColor = theme.getColor("tmenu.mnemonic");
2118 }
2119 // Draw the menu title
2120 getScreen().hLineXY(x, 0,
2121 StringUtils.width(menu.getTitle()) + 2, ' ', menuColor);
2122 getScreen().putStringXY(x + 1, 0, menu.getTitle(), menuColor);
2123 // Draw the highlight character
2124 getScreen().putCharXY(x + 1 +
2125 menu.getMnemonic().getScreenShortcutIdx(),
2126 0, menu.getMnemonic().getShortcut(), menuMnemonicColor);
2127
2128 if (menu.isActive()) {
2129 ((TWindow) menu).drawChildren();
2130 // Reset the screen clipping so we can draw the next
2131 // title.
2132 getScreen().resetClipping();
2133 }
2134 x += StringUtils.width(menu.getTitle()) + 2;
2135 }
2136
2137 for (TMenu menu: subMenus) {
2138 // Reset the screen clipping so we can draw the next
2139 // sub-menu.
2140 getScreen().resetClipping();
2141 ((TWindow) menu).drawChildren();
2142 }
2143 }
2144 getScreen().resetClipping();
2145
2146 if (hideStatusBar == false) {
2147 // Draw the status bar of the top-level window
2148 TStatusBar statusBar = null;
2149 if (topLevel != null) {
2150 if (topLevel.isShown()) {
2151 statusBar = topLevel.getStatusBar();
2152 }
2153 }
2154 if (statusBar != null) {
2155 getScreen().resetClipping();
2156 statusBar.setWidth(getScreen().getWidth());
2157 statusBar.setY(getScreen().getHeight() - topLevel.getY());
2158 statusBar.draw();
2159 } else {
2160 CellAttributes barColor = new CellAttributes();
2161 barColor.setTo(getTheme().getColor("tstatusbar.text"));
2162 getScreen().hLineXY(0, desktopBottom, getScreen().getWidth(),
2163 ' ', barColor);
2164 }
2165 }
2166
2167 // Draw the mouse pointer
2168 if (debugThreads) {
2169 System.err.printf("%d %s restoreImage() %d %d\n",
2170 System.currentTimeMillis(), Thread.currentThread(),
2171 oldDrawnMouseX, oldDrawnMouseY);
2172 }
2173 oldDrawnMouseCell = getScreen().getCharXY(mouseX, mouseY);
2174 if (backend instanceof ECMA48Backend) {
2175 // Special case: the entire row containing the mouse has to be
2176 // re-drawn if it has any image data, AND any rows in between.
2177 if (oldDrawnMouseY != mouseY) {
2178 for (int i = oldDrawnMouseY; ;) {
2179 getScreen().unsetImageRow(i);
2180 if (i == mouseY) {
2181 break;
2182 }
2183 if (oldDrawnMouseY < mouseY) {
2184 i++;
2185 } else {
2186 i--;
2187 }
2188 }
2189 } else {
2190 getScreen().unsetImageRow(mouseY);
2191 }
2192 }
2193
2194 if (inScreenSelection) {
2195 getScreen().setSelection(screenSelectionX0, screenSelectionY0,
2196 screenSelectionX1, screenSelectionY1, screenSelectionRectangle);
2197 }
2198
2199 if ((textMouse == true) && (typingHidMouse == false)) {
2200 drawTextMouse(mouseX, mouseY);
2201 }
2202 oldDrawnMouseX = mouseX;
2203 oldDrawnMouseY = mouseY;
2204
2205 // Place the cursor if it is visible
2206 if (!menuIsActive) {
2207
2208 int visibleWindowCount = 0;
2209 for (TWindow window: sorted) {
2210 if (window.isShown()) {
2211 visibleWindowCount++;
2212 }
2213 }
2214 if (visibleWindowCount == 0) {
2215 // No windows are visible, only the desktop. Allow it to
2216 // have the cursor.
2217 if (desktop != null) {
2218 sorted.add(desktop);
2219 }
2220 }
2221
2222 TWidget activeWidget = null;
2223 if (sorted.size() > 0) {
2224 activeWidget = sorted.get(sorted.size() - 1).getActiveChild();
2225 int cursorClipTop = desktopTop;
2226 int cursorClipBottom = desktopBottom;
2227 if (activeWidget.isCursorVisible()) {
2228 if ((activeWidget.getCursorAbsoluteY() <= cursorClipBottom)
2229 && (activeWidget.getCursorAbsoluteY() >= cursorClipTop)
2230 ) {
2231 getScreen().putCursor(true,
2232 activeWidget.getCursorAbsoluteX(),
2233 activeWidget.getCursorAbsoluteY());
2234 cursor = true;
2235 } else {
2236 // Turn off the cursor. Also place it at 0,0.
2237 getScreen().putCursor(false, 0, 0);
2238 cursor = false;
2239 }
2240 }
2241 }
2242 }
2243
2244 // Kill the cursor
2245 if (!cursor) {
2246 getScreen().hideCursor();
2247 }
2248
2249 if (getScreen().isDirty()) {
2250 screenHandler.setDirty();
2251 }
2252 repaint = false;
2253 }
2254
2255 /**
2256 * Force this application to exit.
2257 */
2258 public void exit() {
2259 quit = true;
2260 synchronized (this) {
2261 this.notify();
2262 }
2263 }
2264
2265 /**
2266 * Subclasses can use this hook to cleanup resources. Called as the last
2267 * step of TApplication.run().
2268 */
2269 public void onExit() {
2270 // Default does nothing.
2271 }
2272
2273 // ------------------------------------------------------------------------
2274 // TWindow management -----------------------------------------------------
2275 // ------------------------------------------------------------------------
2276
2277 /**
2278 * Return the total number of windows.
2279 *
2280 * @return the total number of windows
2281 */
2282 public final int windowCount() {
2283 return windows.size();
2284 }
2285
2286 /**
2287 * Return the number of windows that are showing.
2288 *
2289 * @return the number of windows that are showing on screen
2290 */
2291 public final int shownWindowCount() {
2292 int n = 0;
2293 for (TWindow w: windows) {
2294 if (w.isShown()) {
2295 n++;
2296 }
2297 }
2298 return n;
2299 }
2300
2301 /**
2302 * Return the number of windows that are hidden.
2303 *
2304 * @return the number of windows that are hidden
2305 */
2306 public final int hiddenWindowCount() {
2307 int n = 0;
2308 for (TWindow w: windows) {
2309 if (w.isHidden()) {
2310 n++;
2311 }
2312 }
2313 return n;
2314 }
2315
2316 /**
2317 * Check if a window instance is in this application's window list.
2318 *
2319 * @param window window to look for
2320 * @return true if this window is in the list
2321 */
2322 public final boolean hasWindow(final TWindow window) {
2323 if (windows.size() == 0) {
2324 return false;
2325 }
2326 for (TWindow w: windows) {
2327 if (w == window) {
2328 assert (window.getApplication() == this);
2329 return true;
2330 }
2331 }
2332 return false;
2333 }
2334
2335 /**
2336 * Activate a window: bring it to the top and have it receive events.
2337 *
2338 * @param window the window to become the new active window
2339 */
2340 public final void activateWindow(final TWindow window) {
2341 if (hasWindow(window) == false) {
2342 /*
2343 * Someone has a handle to a window I don't have. Ignore this
2344 * request.
2345 */
2346 return;
2347 }
2348
2349 if (modalWindowActive() && !window.isModal()) {
2350 // Do not activate a non-modal on top of a modal.
2351 return;
2352 }
2353
2354 synchronized (windows) {
2355 // Whatever window might be moving/dragging, stop it now.
2356 for (TWindow w: windows) {
2357 if (w.inMovements()) {
2358 w.stopMovements();
2359 }
2360 }
2361
2362 assert (windows.size() > 0);
2363
2364 if (window.isHidden()) {
2365 // Unhiding will also activate.
2366 showWindow(window);
2367 return;
2368 }
2369 assert (window.isShown());
2370
2371 if (windows.size() == 1) {
2372 assert (window == windows.get(0));
2373 window.setZ(0);
2374 window.setActive(true);
2375 window.onFocus();
2376 return;
2377 }
2378
2379 if (getActiveWindow() == window) {
2380 assert (window.isActive());
2381
2382 // Window is already active, do nothing.
2383 return;
2384 }
2385
2386 assert (!window.isActive());
2387
2388 window.setZ(-1);
2389 Collections.sort(windows);
2390 int newZ = 0;
2391 for (TWindow w: windows) {
2392 w.setZ(newZ);
2393 newZ++;
2394 if ((w != window) && w.isActive()) {
2395 w.onUnfocus();
2396 }
2397 w.setActive(false);
2398 }
2399 window.setActive(true);
2400 window.onFocus();
2401
2402 } // synchronized (windows)
2403
2404 return;
2405 }
2406
2407 /**
2408 * Hide a window.
2409 *
2410 * @param window the window to hide
2411 */
2412 public void hideWindow(final TWindow window) {
2413 if (hasWindow(window) == false) {
2414 /*
2415 * Someone has a handle to a window I don't have. Ignore this
2416 * request.
2417 */
2418 return;
2419 }
2420
2421 synchronized (windows) {
2422
2423 // Whatever window might be moving/dragging, stop it now.
2424 for (TWindow w: windows) {
2425 if (w.inMovements()) {
2426 w.stopMovements();
2427 }
2428 }
2429
2430 assert (windows.size() > 0);
2431
2432 if (window.hidden) {
2433 return;
2434 }
2435
2436 window.setActive(false);
2437 window.hidden = true;
2438 window.onHide();
2439
2440 TWindow activeWindow = null;
2441 for (TWindow w: windows) {
2442 if (w.isShown()) {
2443 activeWindow = w;
2444 break;
2445 }
2446 }
2447 assert (activeWindow != window);
2448 if (activeWindow != null) {
2449 activateWindow(activeWindow);
2450 }
2451
2452 } // synchronized (windows)
2453
2454 }
2455
2456 /**
2457 * Show a window.
2458 *
2459 * @param window the window to show
2460 */
2461 public void showWindow(final TWindow window) {
2462 if (hasWindow(window) == false) {
2463 /*
2464 * Someone has a handle to a window I don't have. Ignore this
2465 * request.
2466 */
2467 return;
2468 }
2469
2470 if (window.hidden) {
2471 window.hidden = false;
2472 window.onShow();
2473 activateWindow(window);
2474 }
2475
2476 }
2477
2478 /**
2479 * Close window.
2480 *
2481 * @param window the window to remove
2482 */
2483 public final void closeWindow(final TWindow window) {
2484 if (hasWindow(window) == false) {
2485 /*
2486 * Someone has a handle to a window I don't have. Ignore this
2487 * request.
2488 */
2489 return;
2490 }
2491
2492 // Let window know that it is about to be closed, while it is still
2493 // visible on screen.
2494 window.onPreClose();
2495
2496 synchronized (windows) {
2497
2498 window.stopMovements();
2499 window.onUnfocus();
2500 windows.remove(window);
2501 Collections.sort(windows);
2502
2503 TWindow nextWindow = null;
2504 int newZ = 0;
2505 for (TWindow w: windows) {
2506 w.stopMovements();
2507 w.setZ(newZ);
2508 newZ++;
2509
2510 // Do not activate a hidden window.
2511 if (w.isHidden()) {
2512 continue;
2513 }
2514 if (nextWindow == null) {
2515 nextWindow = w;
2516 } else {
2517 if (w.isActive()) {
2518 w.setActive(false);
2519 w.onUnfocus();
2520 }
2521 }
2522 }
2523
2524 if (nextWindow != null) {
2525 nextWindow.setActive(true);
2526 nextWindow.onFocus();
2527 }
2528
2529 } // synchronized (windows)
2530
2531 // Perform window cleanup
2532 window.onClose();
2533
2534 // Check if we are closing a TMessageBox or similar
2535 if (secondaryEventReceiver != null) {
2536 assert (secondaryEventHandler != null);
2537
2538 // Do not send events to the secondaryEventReceiver anymore, the
2539 // window is closed.
2540 secondaryEventReceiver = null;
2541
2542 // Wake the secondary thread, it will wake the primary as it
2543 // exits.
2544 synchronized (secondaryEventHandler) {
2545 secondaryEventHandler.notify();
2546 }
2547
2548 } // synchronized (windows)
2549
2550 // Permit desktop to be active if it is the only thing left.
2551 if (desktop != null) {
2552 if (windows.size() == 0) {
2553 desktop.setActive(true);
2554 }
2555 }
2556 }
2557
2558 /**
2559 * Switch to the next window.
2560 *
2561 * @param forward if true, then switch to the next window in the list,
2562 * otherwise switch to the previous window in the list
2563 */
2564 public final void switchWindow(final boolean forward) {
2565 // Only switch if there are multiple visible windows
2566 if (shownWindowCount() < 2) {
2567 return;
2568 }
2569
2570 if (modalWindowActive()) {
2571 // Do not switch if a window is modal
2572 return;
2573 }
2574
2575 synchronized (windows) {
2576
2577 TWindow window = windows.get(0);
2578 do {
2579 assert (window != null);
2580 if (forward) {
2581 window.setZ(windows.size());
2582 } else {
2583 TWindow lastWindow = windows.get(windows.size() - 1);
2584 lastWindow.setZ(-1);
2585 }
2586
2587 Collections.sort(windows);
2588 int newZ = 0;
2589 for (TWindow w: windows) {
2590 w.setZ(newZ);
2591 newZ++;
2592 }
2593
2594 window = windows.get(0);
2595 } while (!window.isShown());
2596
2597 // The next visible window is now on top. Renumber the list.
2598 for (TWindow w: windows) {
2599 w.stopMovements();
2600 if ((w != window) && w.isActive()) {
2601 assert (w.isShown());
2602 w.setActive(false);
2603 w.onUnfocus();
2604 }
2605 }
2606
2607 // Next visible window is on top.
2608 assert (window.isShown());
2609 window.setActive(true);
2610 window.onFocus();
2611
2612 } // synchronized (windows)
2613 }
2614
2615 /**
2616 * Add a window to my window list and make it active. Note package
2617 * private access.
2618 *
2619 * @param window new window to add
2620 */
2621 final void addWindowToApplication(final TWindow window) {
2622
2623 // Do not add menu windows to the window list.
2624 if (window instanceof TMenu) {
2625 return;
2626 }
2627
2628 // Do not add the desktop to the window list.
2629 if (window instanceof TDesktop) {
2630 return;
2631 }
2632
2633 synchronized (windows) {
2634 if (windows.contains(window)) {
2635 throw new IllegalArgumentException("Window " + window +
2636 " is already in window list");
2637 }
2638
2639 // Whatever window might be moving/dragging, stop it now.
2640 for (TWindow w: windows) {
2641 if (w.inMovements()) {
2642 w.stopMovements();
2643 }
2644 }
2645
2646 // Do not allow a modal window to spawn a non-modal window. If a
2647 // modal window is active, then this window will become modal
2648 // too.
2649 if (modalWindowActive()) {
2650 window.flags |= TWindow.MODAL;
2651 window.flags |= TWindow.CENTERED;
2652 window.hidden = false;
2653 }
2654 if (window.isShown()) {
2655 for (TWindow w: windows) {
2656 if (w.isActive()) {
2657 w.setActive(false);
2658 w.onUnfocus();
2659 }
2660 w.setZ(w.getZ() + 1);
2661 }
2662 window.setZ(0);
2663 window.setActive(true);
2664 window.onFocus();
2665 windows.add(0, window);
2666 } else {
2667 window.setZ(windows.size());
2668 windows.add(window);
2669 }
2670
2671 if (((window.flags & TWindow.CENTERED) == 0)
2672 && ((window.flags & TWindow.ABSOLUTEXY) == 0)
2673 && (smartWindowPlacement == true)
2674 && (!(window instanceof TDesktop))
2675 ) {
2676
2677 doSmartPlacement(window);
2678 }
2679 }
2680
2681 // Desktop cannot be active over any other window.
2682 if (desktop != null) {
2683 desktop.setActive(false);
2684 }
2685
2686 }
2687
2688 /**
2689 * Check if there is a system-modal window on top.
2690 *
2691 * @return true if the active window is modal
2692 */
2693 private boolean modalWindowActive() {
2694 if (windows.size() == 0) {
2695 return false;
2696 }
2697
2698 for (TWindow w: windows) {
2699 if (w.isModal()) {
2700 return true;
2701 }
2702 }
2703
2704 return false;
2705 }
2706
2707 /**
2708 * Check if there is a window with overridden menu flag on top.
2709 *
2710 * @return true if the active window is overriding the menu
2711 */
2712 private boolean overrideMenuWindowActive() {
2713 TWindow activeWindow = getActiveWindow();
2714 if (activeWindow != null) {
2715 if (activeWindow.hasOverriddenMenu()) {
2716 return true;
2717 }
2718 }
2719
2720 return false;
2721 }
2722
2723 /**
2724 * Close all open windows.
2725 */
2726 private void closeAllWindows() {
2727 // Don't do anything if we are in the menu
2728 if (activeMenu != null) {
2729 return;
2730 }
2731 while (windows.size() > 0) {
2732 closeWindow(windows.get(0));
2733 }
2734 }
2735
2736 /**
2737 * Re-layout the open windows as non-overlapping tiles. This produces
2738 * almost the same results as Turbo Pascal 7.0's IDE.
2739 */
2740 private void tileWindows() {
2741 synchronized (windows) {
2742 // Don't do anything if we are in the menu
2743 if (activeMenu != null) {
2744 return;
2745 }
2746 int z = windows.size();
2747 if (z == 0) {
2748 return;
2749 }
2750 int a = 0;
2751 int b = 0;
2752 a = (int)(Math.sqrt(z));
2753 int c = 0;
2754 while (c < a) {
2755 b = (z - c) / a;
2756 if (((a * b) + c) == z) {
2757 break;
2758 }
2759 c++;
2760 }
2761 assert (a > 0);
2762 assert (b > 0);
2763 assert (c < a);
2764 int newWidth = (getScreen().getWidth() / a);
2765 int newHeight1 = ((getScreen().getHeight() - 1) / b);
2766 int newHeight2 = ((getScreen().getHeight() - 1) / (b + c));
2767
2768 List<TWindow> sorted = new ArrayList<TWindow>(windows);
2769 Collections.sort(sorted);
2770 Collections.reverse(sorted);
2771 for (int i = 0; i < sorted.size(); i++) {
2772 int logicalX = i / b;
2773 int logicalY = i % b;
2774 if (i >= ((a - 1) * b)) {
2775 logicalX = a - 1;
2776 logicalY = i - ((a - 1) * b);
2777 }
2778
2779 TWindow w = sorted.get(i);
2780 int oldWidth = w.getWidth();
2781 int oldHeight = w.getHeight();
2782
2783 w.setX(logicalX * newWidth);
2784 w.setWidth(newWidth);
2785 if (i >= ((a - 1) * b)) {
2786 w.setY((logicalY * newHeight2) + 1);
2787 w.setHeight(newHeight2);
2788 } else {
2789 w.setY((logicalY * newHeight1) + 1);
2790 w.setHeight(newHeight1);
2791 }
2792 if ((w.getWidth() != oldWidth)
2793 || (w.getHeight() != oldHeight)
2794 ) {
2795 w.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET,
2796 w.getWidth(), w.getHeight()));
2797 }
2798 }
2799 }
2800 }
2801
2802 /**
2803 * Re-layout the open windows as overlapping cascaded windows.
2804 */
2805 private void cascadeWindows() {
2806 synchronized (windows) {
2807 // Don't do anything if we are in the menu
2808 if (activeMenu != null) {
2809 return;
2810 }
2811 int x = 0;
2812 int y = 1;
2813 List<TWindow> sorted = new ArrayList<TWindow>(windows);
2814 Collections.sort(sorted);
2815 Collections.reverse(sorted);
2816 for (TWindow window: sorted) {
2817 window.setX(x);
2818 window.setY(y);
2819 x++;
2820 y++;
2821 if (x > getScreen().getWidth()) {
2822 x = 0;
2823 }
2824 if (y >= getScreen().getHeight()) {
2825 y = 1;
2826 }
2827 }
2828 }
2829 }
2830
2831 /**
2832 * Place a window to minimize its overlap with other windows.
2833 *
2834 * @param window the window to place
2835 */
2836 public final void doSmartPlacement(final TWindow window) {
2837 // This is a pretty dumb algorithm, but seems to work. The hardest
2838 // part is computing these "overlap" values seeking a minimum average
2839 // overlap.
2840 int xMin = 0;
2841 int yMin = desktopTop;
2842 int xMax = getScreen().getWidth() - window.getWidth() + 1;
2843 int yMax = desktopBottom - window.getHeight() + 1;
2844 if (xMax < xMin) {
2845 xMax = xMin;
2846 }
2847 if (yMax < yMin) {
2848 yMax = yMin;
2849 }
2850
2851 if ((xMin == xMax) && (yMin == yMax)) {
2852 // No work to do, bail out.
2853 return;
2854 }
2855
2856 // Compute the overlap matrix without the new window.
2857 int width = getScreen().getWidth();
2858 int height = getScreen().getHeight();
2859 int overlapMatrix[][] = new int[width][height];
2860 for (TWindow w: windows) {
2861 if (window == w) {
2862 continue;
2863 }
2864 for (int x = w.getX(); x < w.getX() + w.getWidth(); x++) {
2865 if (x < 0) {
2866 continue;
2867 }
2868 if (x >= width) {
2869 continue;
2870 }
2871 for (int y = w.getY(); y < w.getY() + w.getHeight(); y++) {
2872 if (y < 0) {
2873 continue;
2874 }
2875 if (y >= height) {
2876 continue;
2877 }
2878 overlapMatrix[x][y]++;
2879 }
2880 }
2881 }
2882
2883 long oldOverlapTotal = 0;
2884 long oldOverlapN = 0;
2885 for (int x = 0; x < width; x++) {
2886 for (int y = 0; y < height; y++) {
2887 oldOverlapTotal += overlapMatrix[x][y];
2888 if (overlapMatrix[x][y] > 0) {
2889 oldOverlapN++;
2890 }
2891 }
2892 }
2893
2894
2895 double oldOverlapAvg = (double) oldOverlapTotal / (double) oldOverlapN;
2896 boolean first = true;
2897 int windowX = window.getX();
2898 int windowY = window.getY();
2899
2900 // For each possible (x, y) position for the new window, compute a
2901 // new overlap matrix.
2902 for (int x = xMin; x < xMax; x++) {
2903 for (int y = yMin; y < yMax; y++) {
2904
2905 // Start with the matrix minus this window.
2906 int newMatrix[][] = new int[width][height];
2907 for (int mx = 0; mx < width; mx++) {
2908 for (int my = 0; my < height; my++) {
2909 newMatrix[mx][my] = overlapMatrix[mx][my];
2910 }
2911 }
2912
2913 // Add this window's values to the new overlap matrix.
2914 long newOverlapTotal = 0;
2915 long newOverlapN = 0;
2916 // Start by adding each new cell.
2917 for (int wx = x; wx < x + window.getWidth(); wx++) {
2918 if (wx >= width) {
2919 continue;
2920 }
2921 for (int wy = y; wy < y + window.getHeight(); wy++) {
2922 if (wy >= height) {
2923 continue;
2924 }
2925 newMatrix[wx][wy]++;
2926 }
2927 }
2928 // Now figure out the new value for total coverage.
2929 for (int mx = 0; mx < width; mx++) {
2930 for (int my = 0; my < height; my++) {
2931 newOverlapTotal += newMatrix[x][y];
2932 if (newMatrix[mx][my] > 0) {
2933 newOverlapN++;
2934 }
2935 }
2936 }
2937 double newOverlapAvg = (double) newOverlapTotal / (double) newOverlapN;
2938
2939 if (first) {
2940 // First time: just record what we got.
2941 oldOverlapAvg = newOverlapAvg;
2942 first = false;
2943 } else {
2944 // All other times: pick a new best (x, y) and save the
2945 // overlap value.
2946 if (newOverlapAvg < oldOverlapAvg) {
2947 windowX = x;
2948 windowY = y;
2949 oldOverlapAvg = newOverlapAvg;
2950 }
2951 }
2952
2953 } // for (int x = xMin; x < xMax; x++)
2954
2955 } // for (int y = yMin; y < yMax; y++)
2956
2957 // Finally, set the window's new coordinates.
2958 window.setX(windowX);
2959 window.setY(windowY);
2960 }
2961
2962 // ------------------------------------------------------------------------
2963 // TMenu management -------------------------------------------------------
2964 // ------------------------------------------------------------------------
2965
2966 /**
2967 * Check if a mouse event would hit either the active menu or any open
2968 * sub-menus.
2969 *
2970 * @param mouse mouse event
2971 * @return true if the mouse would hit the active menu or an open
2972 * sub-menu
2973 */
2974 private boolean mouseOnMenu(final TMouseEvent mouse) {
2975 assert (activeMenu != null);
2976 List<TMenu> menus = new ArrayList<TMenu>(subMenus);
2977 Collections.reverse(menus);
2978 for (TMenu menu: menus) {
2979 if (menu.mouseWouldHit(mouse)) {
2980 return true;
2981 }
2982 }
2983 return activeMenu.mouseWouldHit(mouse);
2984 }
2985
2986 /**
2987 * See if we need to switch window or activate the menu based on
2988 * a mouse click.
2989 *
2990 * @param mouse mouse event
2991 */
2992 private void checkSwitchFocus(final TMouseEvent mouse) {
2993
2994 if ((mouse.getType() == TMouseEvent.Type.MOUSE_DOWN)
2995 && (activeMenu != null)
2996 && (mouse.getAbsoluteY() != 0)
2997 && (!mouseOnMenu(mouse))
2998 ) {
2999 // They clicked outside the active menu, turn it off
3000 activeMenu.setActive(false);
3001 activeMenu = null;
3002 for (TMenu menu: subMenus) {
3003 menu.setActive(false);
3004 }
3005 subMenus.clear();
3006 // Continue checks
3007 }
3008
3009 // See if they hit the menu bar
3010 if ((mouse.getType() == TMouseEvent.Type.MOUSE_DOWN)
3011 && (mouse.isMouse1())
3012 && (!modalWindowActive())
3013 && (!overrideMenuWindowActive())
3014 && (mouse.getAbsoluteY() == 0)
3015 && (hideMenuBar == false)
3016 ) {
3017
3018 for (TMenu menu: subMenus) {
3019 menu.setActive(false);
3020 }
3021 subMenus.clear();
3022
3023 // They selected the menu, go activate it
3024 for (TMenu menu: menus) {
3025 if ((mouse.getAbsoluteX() >= menu.getTitleX())
3026 && (mouse.getAbsoluteX() < menu.getTitleX()
3027 + StringUtils.width(menu.getTitle()) + 2)
3028 ) {
3029 menu.setActive(true);
3030 activeMenu = menu;
3031 } else {
3032 menu.setActive(false);
3033 }
3034 }
3035 return;
3036 }
3037
3038 // See if they hit the menu bar
3039 if ((mouse.getType() == TMouseEvent.Type.MOUSE_MOTION)
3040 && (mouse.isMouse1())
3041 && (activeMenu != null)
3042 && (mouse.getAbsoluteY() == 0)
3043 && (hideMenuBar == false)
3044 ) {
3045
3046 TMenu oldMenu = activeMenu;
3047 for (TMenu menu: subMenus) {
3048 menu.setActive(false);
3049 }
3050 subMenus.clear();
3051
3052 // See if we should switch menus
3053 for (TMenu menu: menus) {
3054 if ((mouse.getAbsoluteX() >= menu.getTitleX())
3055 && (mouse.getAbsoluteX() < menu.getTitleX()
3056 + StringUtils.width(menu.getTitle()) + 2)
3057 ) {
3058 menu.setActive(true);
3059 activeMenu = menu;
3060 }
3061 }
3062 if (oldMenu != activeMenu) {
3063 // They switched menus
3064 oldMenu.setActive(false);
3065 }
3066 return;
3067 }
3068
3069 // If a menu is still active, don't switch windows
3070 if (activeMenu != null) {
3071 return;
3072 }
3073
3074 // Only switch if there are multiple windows
3075 if (windows.size() < 2) {
3076 return;
3077 }
3078
3079 if (((focusFollowsMouse == true)
3080 && (mouse.getType() == TMouseEvent.Type.MOUSE_MOTION))
3081 || (mouse.getType() == TMouseEvent.Type.MOUSE_DOWN)
3082 ) {
3083 synchronized (windows) {
3084 if (windows.get(0).isModal()) {
3085 // Modal windows don't switch
3086 return;
3087 }
3088
3089 for (TWindow window: windows) {
3090 assert (!window.isModal());
3091
3092 if (window.isHidden()) {
3093 assert (!window.isActive());
3094 continue;
3095 }
3096
3097 if (window.mouseWouldHit(mouse)) {
3098 activateWindow(window);
3099 return;
3100 }
3101 }
3102 }
3103
3104 // Clicked on the background, nothing to do
3105 return;
3106 }
3107
3108 // Nothing to do: this isn't a mouse up, or focus isn't following
3109 // mouse.
3110 return;
3111 }
3112
3113 /**
3114 * Turn off the menu.
3115 */
3116 public final void closeMenu() {
3117 if (activeMenu != null) {
3118 activeMenu.setActive(false);
3119 activeMenu = null;
3120 for (TMenu menu: subMenus) {
3121 menu.setActive(false);
3122 }
3123 subMenus.clear();
3124 }
3125 }
3126
3127 /**
3128 * Get a (shallow) copy of the menu list.
3129 *
3130 * @return a copy of the menu list
3131 */
3132 public final List<TMenu> getAllMenus() {
3133 return new ArrayList<TMenu>(menus);
3134 }
3135
3136 /**
3137 * Add a top-level menu to the list.
3138 *
3139 * @param menu the menu to add
3140 * @throws IllegalArgumentException if the menu is already used in
3141 * another TApplication
3142 */
3143 public final void addMenu(final TMenu menu) {
3144 if ((menu.getApplication() != null)
3145 && (menu.getApplication() != this)
3146 ) {
3147 throw new IllegalArgumentException("Menu " + menu + " is already " +
3148 "part of application " + menu.getApplication());
3149 }
3150 closeMenu();
3151 menus.add(menu);
3152 recomputeMenuX();
3153 }
3154
3155 /**
3156 * Remove a top-level menu from the list.
3157 *
3158 * @param menu the menu to remove
3159 * @throws IllegalArgumentException if the menu is already used in
3160 * another TApplication
3161 */
3162 public final void removeMenu(final TMenu menu) {
3163 if ((menu.getApplication() != null)
3164 && (menu.getApplication() != this)
3165 ) {
3166 throw new IllegalArgumentException("Menu " + menu + " is already " +
3167 "part of application " + menu.getApplication());
3168 }
3169 closeMenu();
3170 menus.remove(menu);
3171 recomputeMenuX();
3172 }
3173
3174 /**
3175 * Turn off a sub-menu.
3176 */
3177 public final void closeSubMenu() {
3178 assert (activeMenu != null);
3179 TMenu item = subMenus.get(subMenus.size() - 1);
3180 assert (item != null);
3181 item.setActive(false);
3182 subMenus.remove(subMenus.size() - 1);
3183 }
3184
3185 /**
3186 * Switch to the next menu.
3187 *
3188 * @param forward if true, then switch to the next menu in the list,
3189 * otherwise switch to the previous menu in the list
3190 */
3191 public final void switchMenu(final boolean forward) {
3192 assert (activeMenu != null);
3193 assert (hideMenuBar == false);
3194
3195 for (TMenu menu: subMenus) {
3196 menu.setActive(false);
3197 }
3198 subMenus.clear();
3199
3200 for (int i = 0; i < menus.size(); i++) {
3201 if (activeMenu == menus.get(i)) {
3202 if (forward) {
3203 if (i < menus.size() - 1) {
3204 i++;
3205 } else {
3206 i = 0;
3207 }
3208 } else {
3209 if (i > 0) {
3210 i--;
3211 } else {
3212 i = menus.size() - 1;
3213 }
3214 }
3215 activeMenu.setActive(false);
3216 activeMenu = menus.get(i);
3217 activeMenu.setActive(true);
3218 return;
3219 }
3220 }
3221 }
3222
3223 /**
3224 * Add a menu item to the global list. If it has a keyboard accelerator,
3225 * that will be added the global hash.
3226 *
3227 * @param item the menu item
3228 */
3229 public final void addMenuItem(final TMenuItem item) {
3230 menuItems.add(item);
3231
3232 TKeypress key = item.getKey();
3233 if (key != null) {
3234 synchronized (accelerators) {
3235 assert (accelerators.get(key) == null);
3236 accelerators.put(key.toLowerCase(), item);
3237 }
3238 }
3239 }
3240
3241 /**
3242 * Disable one menu item.
3243 *
3244 * @param id the menu item ID
3245 */
3246 public final void disableMenuItem(final int id) {
3247 for (TMenuItem item: menuItems) {
3248 if (item.getId() == id) {
3249 item.setEnabled(false);
3250 }
3251 }
3252 }
3253
3254 /**
3255 * Disable the range of menu items with ID's between lower and upper,
3256 * inclusive.
3257 *
3258 * @param lower the lowest menu item ID
3259 * @param upper the highest menu item ID
3260 */
3261 public final void disableMenuItems(final int lower, final int upper) {
3262 for (TMenuItem item: menuItems) {
3263 if ((item.getId() >= lower) && (item.getId() <= upper)) {
3264 item.setEnabled(false);
3265 item.getParent().activate(0);
3266 }
3267 }
3268 }
3269
3270 /**
3271 * Enable one menu item.
3272 *
3273 * @param id the menu item ID
3274 */
3275 public final void enableMenuItem(final int id) {
3276 for (TMenuItem item: menuItems) {
3277 if (item.getId() == id) {
3278 item.setEnabled(true);
3279 item.getParent().activate(0);
3280 }
3281 }
3282 }
3283
3284 /**
3285 * Enable the range of menu items with ID's between lower and upper,
3286 * inclusive.
3287 *
3288 * @param lower the lowest menu item ID
3289 * @param upper the highest menu item ID
3290 */
3291 public final void enableMenuItems(final int lower, final int upper) {
3292 for (TMenuItem item: menuItems) {
3293 if ((item.getId() >= lower) && (item.getId() <= upper)) {
3294 item.setEnabled(true);
3295 item.getParent().activate(0);
3296 }
3297 }
3298 }
3299
3300 /**
3301 * Get the menu item associated with this ID.
3302 *
3303 * @param id the menu item ID
3304 * @return the menu item, or null if not found
3305 */
3306 public final TMenuItem getMenuItem(final int id) {
3307 for (TMenuItem item: menuItems) {
3308 if (item.getId() == id) {
3309 return item;
3310 }
3311 }
3312 return null;
3313 }
3314
3315 /**
3316 * Recompute menu x positions based on their title length.
3317 */
3318 public final void recomputeMenuX() {
3319 int x = 0;
3320 for (TMenu menu: menus) {
3321 menu.setX(x);
3322 menu.setTitleX(x);
3323 x += StringUtils.width(menu.getTitle()) + 2;
3324
3325 // Don't let the menu window exceed the screen width
3326 int rightEdge = menu.getX() + menu.getWidth();
3327 if (rightEdge > getScreen().getWidth()) {
3328 menu.setX(getScreen().getWidth() - menu.getWidth());
3329 }
3330 }
3331 }
3332
3333 /**
3334 * Post an event to process.
3335 *
3336 * @param event new event to add to the queue
3337 */
3338 public final void postEvent(final TInputEvent event) {
3339 synchronized (this) {
3340 synchronized (fillEventQueue) {
3341 fillEventQueue.add(event);
3342 }
3343 if (debugThreads) {
3344 System.err.println(System.currentTimeMillis() + " " +
3345 Thread.currentThread() + " postEvent() wake up main");
3346 }
3347 this.notify();
3348 }
3349 }
3350
3351 /**
3352 * Post an event to process and turn off the menu.
3353 *
3354 * @param event new event to add to the queue
3355 */
3356 public final void postMenuEvent(final TInputEvent event) {
3357 synchronized (this) {
3358 synchronized (fillEventQueue) {
3359 fillEventQueue.add(event);
3360 }
3361 if (debugThreads) {
3362 System.err.println(System.currentTimeMillis() + " " +
3363 Thread.currentThread() + " postMenuEvent() wake up main");
3364 }
3365 closeMenu();
3366 this.notify();
3367 }
3368 }
3369
3370 /**
3371 * Add a sub-menu to the list of open sub-menus.
3372 *
3373 * @param menu sub-menu
3374 */
3375 public final void addSubMenu(final TMenu menu) {
3376 subMenus.add(menu);
3377 }
3378
3379 /**
3380 * Convenience function to add a top-level menu.
3381 *
3382 * @param title menu title
3383 * @return the new menu
3384 */
3385 public final TMenu addMenu(final String title) {
3386 int x = 0;
3387 int y = 0;
3388 TMenu menu = new TMenu(this, x, y, title);
3389 menus.add(menu);
3390 recomputeMenuX();
3391 return menu;
3392 }
3393
3394 /**
3395 * Convenience function to add a default tools (hamburger) menu.
3396 *
3397 * @return the new menu
3398 */
3399 public final TMenu addToolMenu() {
3400 TMenu toolMenu = addMenu(i18n.getString("toolMenuTitle"));
3401 toolMenu.addDefaultItem(TMenu.MID_REPAINT);
3402 toolMenu.addDefaultItem(TMenu.MID_VIEW_IMAGE);
3403 toolMenu.addDefaultItem(TMenu.MID_SCREEN_OPTIONS);
3404 TStatusBar toolStatusBar = toolMenu.newStatusBar(i18n.
3405 getString("toolMenuStatus"));
3406 toolStatusBar.addShortcutKeypress(kbF1, cmHelp, i18n.getString("Help"));
3407 return toolMenu;
3408 }
3409
3410 /**
3411 * Convenience function to add a default "File" menu.
3412 *
3413 * @return the new menu
3414 */
3415 public final TMenu addFileMenu() {
3416 TMenu fileMenu = addMenu(i18n.getString("fileMenuTitle"));
3417 fileMenu.addDefaultItem(TMenu.MID_SHELL);
3418 fileMenu.addSeparator();
3419 fileMenu.addDefaultItem(TMenu.MID_EXIT);
3420 TStatusBar statusBar = fileMenu.newStatusBar(i18n.
3421 getString("fileMenuStatus"));
3422 statusBar.addShortcutKeypress(kbF1, cmHelp, i18n.getString("Help"));
3423 return fileMenu;
3424 }
3425
3426 /**
3427 * Convenience function to add a default "Edit" menu.
3428 *
3429 * @return the new menu
3430 */
3431 public final TMenu addEditMenu() {
3432 TMenu editMenu = addMenu(i18n.getString("editMenuTitle"));
3433 editMenu.addDefaultItem(TMenu.MID_UNDO, false);
3434 editMenu.addDefaultItem(TMenu.MID_REDO, false);
3435 editMenu.addSeparator();
3436 editMenu.addDefaultItem(TMenu.MID_CUT, false);
3437 editMenu.addDefaultItem(TMenu.MID_COPY, false);
3438 editMenu.addDefaultItem(TMenu.MID_PASTE, false);
3439 editMenu.addDefaultItem(TMenu.MID_CLEAR, false);
3440 TStatusBar statusBar = editMenu.newStatusBar(i18n.
3441 getString("editMenuStatus"));
3442 statusBar.addShortcutKeypress(kbF1, cmHelp, i18n.getString("Help"));
3443 return editMenu;
3444 }
3445
3446 /**
3447 * Convenience function to add a default "Window" menu.
3448 *
3449 * @return the new menu
3450 */
3451 public final TMenu addWindowMenu() {
3452 TMenu windowMenu = addMenu(i18n.getString("windowMenuTitle"));
3453 windowMenu.addDefaultItem(TMenu.MID_TILE);
3454 windowMenu.addDefaultItem(TMenu.MID_CASCADE);
3455 windowMenu.addDefaultItem(TMenu.MID_CLOSE_ALL);
3456 windowMenu.addSeparator();
3457 windowMenu.addDefaultItem(TMenu.MID_WINDOW_MOVE);
3458 windowMenu.addDefaultItem(TMenu.MID_WINDOW_ZOOM);
3459 windowMenu.addDefaultItem(TMenu.MID_WINDOW_NEXT);
3460 windowMenu.addDefaultItem(TMenu.MID_WINDOW_PREVIOUS);
3461 windowMenu.addDefaultItem(TMenu.MID_WINDOW_CLOSE);
3462 TStatusBar statusBar = windowMenu.newStatusBar(i18n.
3463 getString("windowMenuStatus"));
3464 statusBar.addShortcutKeypress(kbF1, cmHelp, i18n.getString("Help"));
3465 return windowMenu;
3466 }
3467
3468 /**
3469 * Convenience function to add a default "Help" menu.
3470 *
3471 * @return the new menu
3472 */
3473 public final TMenu addHelpMenu() {
3474 TMenu helpMenu = addMenu(i18n.getString("helpMenuTitle"));
3475 helpMenu.addDefaultItem(TMenu.MID_HELP_CONTENTS);
3476 helpMenu.addDefaultItem(TMenu.MID_HELP_INDEX);
3477 helpMenu.addDefaultItem(TMenu.MID_HELP_SEARCH);
3478 helpMenu.addDefaultItem(TMenu.MID_HELP_PREVIOUS);
3479 helpMenu.addDefaultItem(TMenu.MID_HELP_HELP);
3480 helpMenu.addDefaultItem(TMenu.MID_HELP_ACTIVE_FILE);
3481 helpMenu.addSeparator();
3482 helpMenu.addDefaultItem(TMenu.MID_ABOUT);
3483 TStatusBar statusBar = helpMenu.newStatusBar(i18n.
3484 getString("helpMenuStatus"));
3485 statusBar.addShortcutKeypress(kbF1, cmHelp, i18n.getString("Help"));
3486 return helpMenu;
3487 }
3488
3489 /**
3490 * Convenience function to add a default "Table" menu.
3491 *
3492 * @return the new menu
3493 */
3494 public final TMenu addTableMenu() {
3495 TMenu tableMenu = addMenu(i18n.getString("tableMenuTitle"));
3496 tableMenu.addDefaultItem(TMenu.MID_TABLE_RENAME_COLUMN, false);
3497 tableMenu.addDefaultItem(TMenu.MID_TABLE_RENAME_ROW, false);
3498 tableMenu.addSeparator();
3499
3500 TSubMenu viewMenu = tableMenu.addSubMenu(i18n.
3501 getString("tableSubMenuView"));
3502 viewMenu.addDefaultItem(TMenu.MID_TABLE_VIEW_ROW_LABELS, false);
3503 viewMenu.addDefaultItem(TMenu.MID_TABLE_VIEW_COLUMN_LABELS, false);
3504 viewMenu.addDefaultItem(TMenu.MID_TABLE_VIEW_HIGHLIGHT_ROW, false);
3505 viewMenu.addDefaultItem(TMenu.MID_TABLE_VIEW_HIGHLIGHT_COLUMN, false);
3506
3507 TSubMenu borderMenu = tableMenu.addSubMenu(i18n.
3508 getString("tableSubMenuBorders"));
3509 borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_NONE, false);
3510 borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_ALL, false);
3511 borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_CELL_NONE, false);
3512 borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_CELL_ALL, false);
3513 borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_RIGHT, false);
3514 borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_LEFT, false);
3515 borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_TOP, false);
3516 borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_BOTTOM, false);
3517 borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_DOUBLE_BOTTOM, false);
3518 borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_THICK_BOTTOM, false);
3519 TSubMenu deleteMenu = tableMenu.addSubMenu(i18n.
3520 getString("tableSubMenuDelete"));
3521 deleteMenu.addDefaultItem(TMenu.MID_TABLE_DELETE_LEFT, false);
3522 deleteMenu.addDefaultItem(TMenu.MID_TABLE_DELETE_UP, false);
3523 deleteMenu.addDefaultItem(TMenu.MID_TABLE_DELETE_ROW, false);
3524 deleteMenu.addDefaultItem(TMenu.MID_TABLE_DELETE_COLUMN, false);
3525 TSubMenu insertMenu = tableMenu.addSubMenu(i18n.
3526 getString("tableSubMenuInsert"));
3527 insertMenu.addDefaultItem(TMenu.MID_TABLE_INSERT_LEFT, false);
3528 insertMenu.addDefaultItem(TMenu.MID_TABLE_INSERT_RIGHT, false);
3529 insertMenu.addDefaultItem(TMenu.MID_TABLE_INSERT_ABOVE, false);
3530 insertMenu.addDefaultItem(TMenu.MID_TABLE_INSERT_BELOW, false);
3531 TSubMenu columnMenu = tableMenu.addSubMenu(i18n.
3532 getString("tableSubMenuColumn"));
3533 columnMenu.addDefaultItem(TMenu.MID_TABLE_COLUMN_NARROW, false);
3534 columnMenu.addDefaultItem(TMenu.MID_TABLE_COLUMN_WIDEN, false);
3535 TSubMenu fileMenu = tableMenu.addSubMenu(i18n.
3536 getString("tableSubMenuFile"));
3537 fileMenu.addDefaultItem(TMenu.MID_TABLE_FILE_OPEN_CSV, false);
3538 fileMenu.addDefaultItem(TMenu.MID_TABLE_FILE_SAVE_CSV, false);
3539 fileMenu.addDefaultItem(TMenu.MID_TABLE_FILE_SAVE_TEXT, false);
3540
3541 TStatusBar statusBar = tableMenu.newStatusBar(i18n.
3542 getString("tableMenuStatus"));
3543 statusBar.addShortcutKeypress(kbF1, cmHelp, i18n.getString("Help"));
3544 return tableMenu;
3545 }
3546
3547 // ------------------------------------------------------------------------
3548 // TTimer management ------------------------------------------------------
3549 // ------------------------------------------------------------------------
3550
3551 /**
3552 * Get the amount of time I can sleep before missing a Timer tick.
3553 *
3554 * @param timeout = initial (maximum) timeout in millis
3555 * @return number of milliseconds between now and the next timer event
3556 */
3557 private long getSleepTime(final long timeout) {
3558 Date now = new Date();
3559 long nowTime = now.getTime();
3560 long sleepTime = timeout;
3561
3562 synchronized (timers) {
3563 for (TTimer timer: timers) {
3564 long nextTickTime = timer.getNextTick().getTime();
3565 if (nextTickTime < nowTime) {
3566 return 0;
3567 }
3568
3569 long timeDifference = nextTickTime - nowTime;
3570 if (timeDifference < sleepTime) {
3571 sleepTime = timeDifference;
3572 }
3573 }
3574 }
3575
3576 assert (sleepTime >= 0);
3577 assert (sleepTime <= timeout);
3578 return sleepTime;
3579 }
3580
3581 /**
3582 * Convenience function to add a timer.
3583 *
3584 * @param duration number of milliseconds to wait between ticks
3585 * @param recurring if true, re-schedule this timer after every tick
3586 * @param action function to call when button is pressed
3587 * @return the timer
3588 */
3589 public final TTimer addTimer(final long duration, final boolean recurring,
3590 final TAction action) {
3591
3592 TTimer timer = new TTimer(duration, recurring, action);
3593 synchronized (timers) {
3594 timers.add(timer);
3595 }
3596 return timer;
3597 }
3598
3599 /**
3600 * Convenience function to remove a timer.
3601 *
3602 * @param timer timer to remove
3603 */
3604 public final void removeTimer(final TTimer timer) {
3605 synchronized (timers) {
3606 timers.remove(timer);
3607 }
3608 }
3609
3610 // ------------------------------------------------------------------------
3611 // Other TWindow constructors ---------------------------------------------
3612 // ------------------------------------------------------------------------
3613
3614 /**
3615 * Convenience function to spawn a message box.
3616 *
3617 * @param title window title, will be centered along the top border
3618 * @param caption message to display. Use embedded newlines to get a
3619 * multi-line box.
3620 * @return the new message box
3621 */
3622 public final TMessageBox messageBox(final String title,
3623 final String caption) {
3624
3625 return new TMessageBox(this, title, caption, TMessageBox.Type.OK);
3626 }
3627
3628 /**
3629 * Convenience function to spawn a message box.
3630 *
3631 * @param title window title, will be centered along the top border
3632 * @param caption message to display. Use embedded newlines to get a
3633 * multi-line box.
3634 * @param type one of the TMessageBox.Type constants. Default is
3635 * Type.OK.
3636 * @return the new message box
3637 */
3638 public final TMessageBox messageBox(final String title,
3639 final String caption, final TMessageBox.Type type) {
3640
3641 return new TMessageBox(this, title, caption, type);
3642 }
3643
3644 /**
3645 * Convenience function to spawn an input box.
3646 *
3647 * @param title window title, will be centered along the top border
3648 * @param caption message to display. Use embedded newlines to get a
3649 * multi-line box.
3650 * @return the new input box
3651 */
3652 public final TInputBox inputBox(final String title, final String caption) {
3653
3654 return new TInputBox(this, title, caption);
3655 }
3656
3657 /**
3658 * Convenience function to spawn an input box.
3659 *
3660 * @param title window title, will be centered along the top border
3661 * @param caption message to display. Use embedded newlines to get a
3662 * multi-line box.
3663 * @param text initial text to seed the field with
3664 * @return the new input box
3665 */
3666 public final TInputBox inputBox(final String title, final String caption,
3667 final String text) {
3668
3669 return new TInputBox(this, title, caption, text);
3670 }
3671
3672 /**
3673 * Convenience function to spawn an input box.
3674 *
3675 * @param title window title, will be centered along the top border
3676 * @param caption message to display. Use embedded newlines to get a
3677 * multi-line box.
3678 * @param text initial text to seed the field with
3679 * @param type one of the Type constants. Default is Type.OK.
3680 * @return the new input box
3681 */
3682 public final TInputBox inputBox(final String title, final String caption,
3683 final String text, final TInputBox.Type type) {
3684
3685 return new TInputBox(this, title, caption, text, type);
3686 }
3687
3688 /**
3689 * Convenience function to open a terminal window.
3690 *
3691 * @param x column relative to parent
3692 * @param y row relative to parent
3693 * @return the terminal new window
3694 */
3695 public final TTerminalWindow openTerminal(final int x, final int y) {
3696 return openTerminal(x, y, TWindow.RESIZABLE);
3697 }
3698
3699 /**
3700 * Convenience function to open a terminal window.
3701 *
3702 * @param x column relative to parent
3703 * @param y row relative to parent
3704 * @param closeOnExit if true, close the window when the command exits
3705 * @return the terminal new window
3706 */
3707 public final TTerminalWindow openTerminal(final int x, final int y,
3708 final boolean closeOnExit) {
3709
3710 return openTerminal(x, y, TWindow.RESIZABLE, closeOnExit);
3711 }
3712
3713 /**
3714 * Convenience function to open a terminal window.
3715 *
3716 * @param x column relative to parent
3717 * @param y row relative to parent
3718 * @param flags mask of CENTERED, MODAL, or RESIZABLE
3719 * @return the terminal new window
3720 */
3721 public final TTerminalWindow openTerminal(final int x, final int y,
3722 final int flags) {
3723
3724 return new TTerminalWindow(this, x, y, flags);
3725 }
3726
3727 /**
3728 * Convenience function to open a terminal window.
3729 *
3730 * @param x column relative to parent
3731 * @param y row relative to parent
3732 * @param flags mask of CENTERED, MODAL, or RESIZABLE
3733 * @param closeOnExit if true, close the window when the command exits
3734 * @return the terminal new window
3735 */
3736 public final TTerminalWindow openTerminal(final int x, final int y,
3737 final int flags, final boolean closeOnExit) {
3738
3739 return new TTerminalWindow(this, x, y, flags, closeOnExit);
3740 }
3741
3742 /**
3743 * Convenience function to open a terminal window and execute a custom
3744 * command line inside it.
3745 *
3746 * @param x column relative to parent
3747 * @param y row relative to parent
3748 * @param commandLine the command line to execute
3749 * @return the terminal new window
3750 */
3751 public final TTerminalWindow openTerminal(final int x, final int y,
3752 final String commandLine) {
3753
3754 return openTerminal(x, y, TWindow.RESIZABLE, commandLine);
3755 }
3756
3757 /**
3758 * Convenience function to open a terminal window and execute a custom
3759 * command line inside it.
3760 *
3761 * @param x column relative to parent
3762 * @param y row relative to parent
3763 * @param commandLine the command line to execute
3764 * @param closeOnExit if true, close the window when the command exits
3765 * @return the terminal new window
3766 */
3767 public final TTerminalWindow openTerminal(final int x, final int y,
3768 final String commandLine, final boolean closeOnExit) {
3769
3770 return openTerminal(x, y, TWindow.RESIZABLE, commandLine, closeOnExit);
3771 }
3772
3773 /**
3774 * Convenience function to open a terminal window and execute a custom
3775 * command line inside it.
3776 *
3777 * @param x column relative to parent
3778 * @param y row relative to parent
3779 * @param flags mask of CENTERED, MODAL, or RESIZABLE
3780 * @param command the command line to execute
3781 * @return the terminal new window
3782 */
3783 public final TTerminalWindow openTerminal(final int x, final int y,
3784 final int flags, final String [] command) {
3785
3786 return new TTerminalWindow(this, x, y, flags, command);
3787 }
3788
3789 /**
3790 * Convenience function to open a terminal window and execute a custom
3791 * command line inside it.
3792 *
3793 * @param x column relative to parent
3794 * @param y row relative to parent
3795 * @param flags mask of CENTERED, MODAL, or RESIZABLE
3796 * @param command the command line to execute
3797 * @param closeOnExit if true, close the window when the command exits
3798 * @return the terminal new window
3799 */
3800 public final TTerminalWindow openTerminal(final int x, final int y,
3801 final int flags, final String [] command, final boolean closeOnExit) {
3802
3803 return new TTerminalWindow(this, x, y, flags, command, closeOnExit);
3804 }
3805
3806 /**
3807 * Convenience function to open a terminal window and execute a custom
3808 * command line inside it.
3809 *
3810 * @param x column relative to parent
3811 * @param y row relative to parent
3812 * @param flags mask of CENTERED, MODAL, or RESIZABLE
3813 * @param commandLine the command line to execute
3814 * @return the terminal new window
3815 */
3816 public final TTerminalWindow openTerminal(final int x, final int y,
3817 final int flags, final String commandLine) {
3818
3819 return new TTerminalWindow(this, x, y, flags, commandLine.split("\\s+"));
3820 }
3821
3822 /**
3823 * Convenience function to open a terminal window and execute a custom
3824 * command line inside it.
3825 *
3826 * @param x column relative to parent
3827 * @param y row relative to parent
3828 * @param flags mask of CENTERED, MODAL, or RESIZABLE
3829 * @param commandLine the command line to execute
3830 * @param closeOnExit if true, close the window when the command exits
3831 * @return the terminal new window
3832 */
3833 public final TTerminalWindow openTerminal(final int x, final int y,
3834 final int flags, final String commandLine, final boolean closeOnExit) {
3835
3836 return new TTerminalWindow(this, x, y, flags, commandLine.split("\\s+"),
3837 closeOnExit);
3838 }
3839
3840 /**
3841 * Convenience function to spawn an file open box.
3842 *
3843 * @param path path of selected file
3844 * @return the result of the new file open box
3845 * @throws IOException if java.io operation throws
3846 */
3847 public final String fileOpenBox(final String path) throws IOException {
3848
3849 TFileOpenBox box = new TFileOpenBox(this, path, TFileOpenBox.Type.OPEN);
3850 return box.getFilename();
3851 }
3852
3853 /**
3854 * Convenience function to spawn an file open box.
3855 *
3856 * @param path path of selected file
3857 * @param type one of the Type constants
3858 * @return the result of the new file open box
3859 * @throws IOException if java.io operation throws
3860 */
3861 public final String fileOpenBox(final String path,
3862 final TFileOpenBox.Type type) throws IOException {
3863
3864 TFileOpenBox box = new TFileOpenBox(this, path, type);
3865 return box.getFilename();
3866 }
3867
3868 /**
3869 * Convenience function to spawn a file open box.
3870 *
3871 * @param path path of selected file
3872 * @param type one of the Type constants
3873 * @param filter a string that files must match to be displayed
3874 * @return the result of the new file open box
3875 * @throws IOException of a java.io operation throws
3876 */
3877 public final String fileOpenBox(final String path,
3878 final TFileOpenBox.Type type, final String filter) throws IOException {
3879
3880 ArrayList<String> filters = new ArrayList<String>();
3881 filters.add(filter);
3882
3883 TFileOpenBox box = new TFileOpenBox(this, path, type, filters);
3884 return box.getFilename();
3885 }
3886
3887 /**
3888 * Convenience function to spawn a file open box.
3889 *
3890 * @param path path of selected file
3891 * @param type one of the Type constants
3892 * @param filters a list of strings that files must match to be displayed
3893 * @return the result of the new file open box
3894 * @throws IOException of a java.io operation throws
3895 */
3896 public final String fileOpenBox(final String path,
3897 final TFileOpenBox.Type type,
3898 final List<String> filters) throws IOException {
3899
3900 TFileOpenBox box = new TFileOpenBox(this, path, type, filters);
3901 return box.getFilename();
3902 }
3903
3904 /**
3905 * Convenience function to create a new window and make it active.
3906 * Window will be located at (0, 0).
3907 *
3908 * @param title window title, will be centered along the top border
3909 * @param width width of window
3910 * @param height height of window
3911 * @return the new window
3912 */
3913 public final TWindow addWindow(final String title, final int width,
3914 final int height) {
3915
3916 TWindow window = new TWindow(this, title, 0, 0, width, height);
3917 return window;
3918 }
3919
3920 /**
3921 * Convenience function to create a new window and make it active.
3922 * Window will be located at (0, 0).
3923 *
3924 * @param title window title, will be centered along the top border
3925 * @param width width of window
3926 * @param height height of window
3927 * @param flags bitmask of RESIZABLE, CENTERED, or MODAL
3928 * @return the new window
3929 */
3930 public final TWindow addWindow(final String title,
3931 final int width, final int height, final int flags) {
3932
3933 TWindow window = new TWindow(this, title, 0, 0, width, height, flags);
3934 return window;
3935 }
3936
3937 /**
3938 * Convenience function to create a new window and make it active.
3939 *
3940 * @param title window title, will be centered along the top border
3941 * @param x column relative to parent
3942 * @param y row relative to parent
3943 * @param width width of window
3944 * @param height height of window
3945 * @return the new window
3946 */
3947 public final TWindow addWindow(final String title,
3948 final int x, final int y, final int width, final int height) {
3949
3950 TWindow window = new TWindow(this, title, x, y, width, height);
3951 return window;
3952 }
3953
3954 /**
3955 * Convenience function to create a new window and make it active.
3956 *
3957 * @param title window title, will be centered along the top border
3958 * @param x column relative to parent
3959 * @param y row relative to parent
3960 * @param width width of window
3961 * @param height height of window
3962 * @param flags mask of RESIZABLE, CENTERED, or MODAL
3963 * @return the new window
3964 */
3965 public final TWindow addWindow(final String title,
3966 final int x, final int y, final int width, final int height,
3967 final int flags) {
3968
3969 TWindow window = new TWindow(this, title, x, y, width, height, flags);
3970 return window;
3971 }
3972
3973}