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