Update main screenshot
[nikiroo-utils.git] / src / jexer / TApplication.java
... / ...
CommitLineData
1/*
2 * Jexer - Java Text User Interface
3 *
4 * The MIT License (MIT)
5 *
6 * Copyright (C) 2019 Kevin Lamonte
7 *
8 * Permission is hereby granted, free of charge, to any person obtaining a
9 * copy of this software and associated documentation files (the "Software"),
10 * to deal in the Software without restriction, including without limitation
11 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
12 * and/or sell copies of the Software, and to permit persons to whom the
13 * Software is furnished to do so, subject to the following conditions:
14 *
15 * The above copyright notice and this permission notice shall be included in
16 * all copies or substantial portions of the Software.
17 *
18 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
21 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
24 * DEALINGS IN THE SOFTWARE.
25 *
26 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
27 * @version 1
28 */
29package jexer;
30
31import java.io.File;
32import java.io.InputStream;
33import java.io.IOException;
34import java.io.OutputStream;
35import java.io.PrintWriter;
36import java.io.Reader;
37import java.io.UnsupportedEncodingException;
38import java.text.MessageFormat;
39import java.util.ArrayList;
40import java.util.Collections;
41import java.util.Date;
42import java.util.HashMap;
43import java.util.LinkedList;
44import java.util.List;
45import java.util.Map;
46import java.util.ResourceBundle;
47
48import jexer.bits.Cell;
49import jexer.bits.CellAttributes;
50import jexer.bits.ColorTheme;
51import jexer.bits.StringUtils;
52import jexer.event.TCommandEvent;
53import jexer.event.TInputEvent;
54import jexer.event.TKeypressEvent;
55import jexer.event.TMenuEvent;
56import jexer.event.TMouseEvent;
57import jexer.event.TResizeEvent;
58import jexer.backend.Backend;
59import jexer.backend.MultiBackend;
60import jexer.backend.Screen;
61import jexer.backend.SwingBackend;
62import jexer.backend.ECMA48Backend;
63import jexer.backend.TWindowBackend;
64import jexer.menu.TMenu;
65import jexer.menu.TMenuItem;
66import jexer.menu.TSubMenu;
67import static jexer.TCommand.*;
68import 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 */
75public 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 }
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 getScreen().putCharXY(x, y, cell);
1710 if ((onlyThisCell == true) || (cell.getWidth() == Cell.Width.SINGLE)) {
1711 return;
1712 }
1713
1714 // This cell is one half of a fullwidth glyph. Invert the other
1715 // half.
1716 if (cell.getWidth() == Cell.Width.LEFT) {
1717 if (x < getScreen().getWidth() - 1) {
1718 Cell rightHalf = getScreen().getCharXY(x + 1, y);
1719 if (rightHalf.getWidth() == Cell.Width.RIGHT) {
1720 invertCell(x + 1, y, true);
1721 return;
1722 }
1723 }
1724 }
1725 if (cell.getWidth() == Cell.Width.RIGHT) {
1726 if (x > 0) {
1727 Cell leftHalf = getScreen().getCharXY(x - 1, y);
1728 if (leftHalf.getWidth() == Cell.Width.LEFT) {
1729 invertCell(x - 1, y, true);
1730 }
1731 }
1732 }
1733 }
1734
1735 /**
1736 * Draw everything.
1737 */
1738 private void drawAll() {
1739 boolean menuIsActive = false;
1740
1741 if (debugThreads) {
1742 System.err.printf("%d %s drawAll() enter\n",
1743 System.currentTimeMillis(), Thread.currentThread());
1744 }
1745
1746 // I don't think this does anything useful anymore...
1747 if (!repaint) {
1748 if (debugThreads) {
1749 System.err.printf("%d %s drawAll() !repaint\n",
1750 System.currentTimeMillis(), Thread.currentThread());
1751 }
1752 if ((oldDrawnMouseX != mouseX) || (oldDrawnMouseY != mouseY)) {
1753 if (debugThreads) {
1754 System.err.printf("%d %s drawAll() !repaint MOUSE\n",
1755 System.currentTimeMillis(), Thread.currentThread());
1756 }
1757
1758 // The only thing that has happened is the mouse moved.
1759
1760 // Redraw the old cell at that position, and save the cell at
1761 // the new mouse position.
1762 if (debugThreads) {
1763 System.err.printf("%d %s restoreImage() %d %d\n",
1764 System.currentTimeMillis(), Thread.currentThread(),
1765 oldDrawnMouseX, oldDrawnMouseY);
1766 }
1767 oldDrawnMouseCell.restoreImage();
1768 getScreen().putCharXY(oldDrawnMouseX, oldDrawnMouseY,
1769 oldDrawnMouseCell);
1770 oldDrawnMouseCell = getScreen().getCharXY(mouseX, mouseY);
1771 if (backend instanceof ECMA48Backend) {
1772 // Special case: the entire row containing the mouse has
1773 // to be re-drawn if it has any image data, AND any rows
1774 // in between.
1775 if (oldDrawnMouseY != mouseY) {
1776 for (int i = oldDrawnMouseY; ;) {
1777 getScreen().unsetImageRow(i);
1778 if (i == mouseY) {
1779 break;
1780 }
1781 if (oldDrawnMouseY < mouseY) {
1782 i++;
1783 } else {
1784 i--;
1785 }
1786 }
1787 } else {
1788 getScreen().unsetImageRow(mouseY);
1789 }
1790 }
1791
1792 // Draw mouse at the new position.
1793 invertCell(mouseX, mouseY);
1794
1795 oldDrawnMouseX = mouseX;
1796 oldDrawnMouseY = mouseY;
1797 }
1798 if (getScreen().isDirty()) {
1799 screenHandler.setDirty();
1800 }
1801 return;
1802 }
1803
1804 if (debugThreads) {
1805 System.err.printf("%d %s drawAll() REDRAW\n",
1806 System.currentTimeMillis(), Thread.currentThread());
1807 }
1808
1809 // If true, the cursor is not visible
1810 boolean cursor = false;
1811
1812 // Start with a clean screen
1813 getScreen().clear();
1814
1815 // Draw the desktop
1816 if (desktop != null) {
1817 desktop.drawChildren();
1818 }
1819
1820 // Draw each window in reverse Z order
1821 List<TWindow> sorted = new ArrayList<TWindow>(windows);
1822 Collections.sort(sorted);
1823 TWindow topLevel = null;
1824 if (sorted.size() > 0) {
1825 topLevel = sorted.get(0);
1826 }
1827 Collections.reverse(sorted);
1828 for (TWindow window: sorted) {
1829 if (window.isShown()) {
1830 window.drawChildren();
1831 }
1832 }
1833
1834 // Draw the blank menubar line - reset the screen clipping first so
1835 // it won't trim it out.
1836 getScreen().resetClipping();
1837 getScreen().hLineXY(0, 0, getScreen().getWidth(), ' ',
1838 theme.getColor("tmenu"));
1839 // Now draw the menus.
1840 int x = 1;
1841 for (TMenu menu: menus) {
1842 CellAttributes menuColor;
1843 CellAttributes menuMnemonicColor;
1844 if (menu.isActive()) {
1845 menuIsActive = true;
1846 menuColor = theme.getColor("tmenu.highlighted");
1847 menuMnemonicColor = theme.getColor("tmenu.mnemonic.highlighted");
1848 topLevel = menu;
1849 } else {
1850 menuColor = theme.getColor("tmenu");
1851 menuMnemonicColor = theme.getColor("tmenu.mnemonic");
1852 }
1853 // Draw the menu title
1854 getScreen().hLineXY(x, 0, StringUtils.width(menu.getTitle()) + 2, ' ',
1855 menuColor);
1856 getScreen().putStringXY(x + 1, 0, menu.getTitle(), menuColor);
1857 // Draw the highlight character
1858 getScreen().putCharXY(x + 1 + menu.getMnemonic().getScreenShortcutIdx(),
1859 0, menu.getMnemonic().getShortcut(), menuMnemonicColor);
1860
1861 if (menu.isActive()) {
1862 ((TWindow) menu).drawChildren();
1863 // Reset the screen clipping so we can draw the next title.
1864 getScreen().resetClipping();
1865 }
1866 x += StringUtils.width(menu.getTitle()) + 2;
1867 }
1868
1869 for (TMenu menu: subMenus) {
1870 // Reset the screen clipping so we can draw the next sub-menu.
1871 getScreen().resetClipping();
1872 ((TWindow) menu).drawChildren();
1873 }
1874 getScreen().resetClipping();
1875
1876 // Draw the status bar of the top-level window
1877 TStatusBar statusBar = null;
1878 if (topLevel != null) {
1879 statusBar = topLevel.getStatusBar();
1880 }
1881 if (statusBar != null) {
1882 getScreen().resetClipping();
1883 statusBar.setWidth(getScreen().getWidth());
1884 statusBar.setY(getScreen().getHeight() - topLevel.getY());
1885 statusBar.draw();
1886 } else {
1887 CellAttributes barColor = new CellAttributes();
1888 barColor.setTo(getTheme().getColor("tstatusbar.text"));
1889 getScreen().hLineXY(0, desktopBottom, getScreen().getWidth(), ' ',
1890 barColor);
1891 }
1892
1893 // Draw the mouse pointer
1894 if (debugThreads) {
1895 System.err.printf("%d %s restoreImage() %d %d\n",
1896 System.currentTimeMillis(), Thread.currentThread(),
1897 oldDrawnMouseX, oldDrawnMouseY);
1898 }
1899 oldDrawnMouseCell = getScreen().getCharXY(mouseX, mouseY);
1900 if (backend instanceof ECMA48Backend) {
1901 // Special case: the entire row containing the mouse has to be
1902 // re-drawn if it has any image data, AND any rows in between.
1903 if (oldDrawnMouseY != mouseY) {
1904 for (int i = oldDrawnMouseY; ;) {
1905 getScreen().unsetImageRow(i);
1906 if (i == mouseY) {
1907 break;
1908 }
1909 if (oldDrawnMouseY < mouseY) {
1910 i++;
1911 } else {
1912 i--;
1913 }
1914 }
1915 } else {
1916 getScreen().unsetImageRow(mouseY);
1917 }
1918 }
1919 invertCell(mouseX, mouseY);
1920 oldDrawnMouseX = mouseX;
1921 oldDrawnMouseY = mouseY;
1922
1923 // Place the cursor if it is visible
1924 if (!menuIsActive) {
1925 TWidget activeWidget = null;
1926 if (sorted.size() > 0) {
1927 activeWidget = sorted.get(sorted.size() - 1).getActiveChild();
1928 if (activeWidget.isCursorVisible()) {
1929 if ((activeWidget.getCursorAbsoluteY() < desktopBottom)
1930 && (activeWidget.getCursorAbsoluteY() > desktopTop)
1931 ) {
1932 getScreen().putCursor(true,
1933 activeWidget.getCursorAbsoluteX(),
1934 activeWidget.getCursorAbsoluteY());
1935 cursor = true;
1936 } else {
1937 // Turn off the cursor. Also place it at 0,0.
1938 getScreen().putCursor(false, 0, 0);
1939 cursor = false;
1940 }
1941 }
1942 }
1943 }
1944
1945 // Kill the cursor
1946 if (!cursor) {
1947 getScreen().hideCursor();
1948 }
1949
1950 if (getScreen().isDirty()) {
1951 screenHandler.setDirty();
1952 }
1953 repaint = false;
1954 }
1955
1956 /**
1957 * Force this application to exit.
1958 */
1959 public void exit() {
1960 quit = true;
1961 synchronized (this) {
1962 this.notify();
1963 }
1964 }
1965
1966 /**
1967 * Subclasses can use this hook to cleanup resources. Called as the last
1968 * step of TApplication.run().
1969 */
1970 public void onExit() {
1971 // Default does nothing.
1972 }
1973
1974 // ------------------------------------------------------------------------
1975 // TWindow management -----------------------------------------------------
1976 // ------------------------------------------------------------------------
1977
1978 /**
1979 * Return the total number of windows.
1980 *
1981 * @return the total number of windows
1982 */
1983 public final int windowCount() {
1984 return windows.size();
1985 }
1986
1987 /**
1988 * Return the number of windows that are showing.
1989 *
1990 * @return the number of windows that are showing on screen
1991 */
1992 public final int shownWindowCount() {
1993 int n = 0;
1994 for (TWindow w: windows) {
1995 if (w.isShown()) {
1996 n++;
1997 }
1998 }
1999 return n;
2000 }
2001
2002 /**
2003 * Return the number of windows that are hidden.
2004 *
2005 * @return the number of windows that are hidden
2006 */
2007 public final int hiddenWindowCount() {
2008 int n = 0;
2009 for (TWindow w: windows) {
2010 if (w.isHidden()) {
2011 n++;
2012 }
2013 }
2014 return n;
2015 }
2016
2017 /**
2018 * Check if a window instance is in this application's window list.
2019 *
2020 * @param window window to look for
2021 * @return true if this window is in the list
2022 */
2023 public final boolean hasWindow(final TWindow window) {
2024 if (windows.size() == 0) {
2025 return false;
2026 }
2027 for (TWindow w: windows) {
2028 if (w == window) {
2029 assert (window.getApplication() == this);
2030 return true;
2031 }
2032 }
2033 return false;
2034 }
2035
2036 /**
2037 * Activate a window: bring it to the top and have it receive events.
2038 *
2039 * @param window the window to become the new active window
2040 */
2041 public void activateWindow(final TWindow window) {
2042 if (hasWindow(window) == false) {
2043 /*
2044 * Someone has a handle to a window I don't have. Ignore this
2045 * request.
2046 */
2047 return;
2048 }
2049
2050 // Whatever window might be moving/dragging, stop it now.
2051 for (TWindow w: windows) {
2052 if (w.inMovements()) {
2053 w.stopMovements();
2054 }
2055 }
2056
2057 assert (windows.size() > 0);
2058
2059 if (window.isHidden()) {
2060 // Unhiding will also activate.
2061 showWindow(window);
2062 return;
2063 }
2064 assert (window.isShown());
2065
2066 if (windows.size() == 1) {
2067 assert (window == windows.get(0));
2068 if (activeWindow == null) {
2069 activeWindow = window;
2070 window.setZ(0);
2071 activeWindow.setActive(true);
2072 activeWindow.onFocus();
2073 }
2074
2075 assert (window.isActive());
2076 assert (activeWindow == window);
2077 return;
2078 }
2079
2080 if (activeWindow == window) {
2081 assert (window.isActive());
2082
2083 // Window is already active, do nothing.
2084 return;
2085 }
2086
2087 assert (!window.isActive());
2088 if (activeWindow != null) {
2089 // TODO: see if this assertion is really necessary.
2090 // assert (activeWindow.getZ() == 0);
2091
2092 activeWindow.setActive(false);
2093
2094 // Increment every window Z that is on top of window
2095 for (TWindow w: windows) {
2096 if (w == window) {
2097 continue;
2098 }
2099 if (w.getZ() < window.getZ()) {
2100 w.setZ(w.getZ() + 1);
2101 }
2102 }
2103
2104 // Unset activeWindow now before unfocus, so that a window
2105 // lifecycle change inside onUnfocus() doesn't call
2106 // switchWindow() and lead to a stack overflow.
2107 TWindow oldActiveWindow = activeWindow;
2108 activeWindow = null;
2109 oldActiveWindow.onUnfocus();
2110 }
2111 activeWindow = window;
2112 activeWindow.setZ(0);
2113 activeWindow.setActive(true);
2114 activeWindow.onFocus();
2115 return;
2116 }
2117
2118 /**
2119 * Hide a window.
2120 *
2121 * @param window the window to hide
2122 */
2123 public void hideWindow(final TWindow window) {
2124 if (hasWindow(window) == false) {
2125 /*
2126 * Someone has a handle to a window I don't have. Ignore this
2127 * request.
2128 */
2129 return;
2130 }
2131
2132 // Whatever window might be moving/dragging, stop it now.
2133 for (TWindow w: windows) {
2134 if (w.inMovements()) {
2135 w.stopMovements();
2136 }
2137 }
2138
2139 assert (windows.size() > 0);
2140
2141 if (!window.hidden) {
2142 if (window == activeWindow) {
2143 if (shownWindowCount() > 1) {
2144 switchWindow(true);
2145 } else {
2146 activeWindow = null;
2147 window.setActive(false);
2148 window.onUnfocus();
2149 }
2150 }
2151 window.hidden = true;
2152 window.onHide();
2153 }
2154 }
2155
2156 /**
2157 * Show a window.
2158 *
2159 * @param window the window to show
2160 */
2161 public void showWindow(final TWindow window) {
2162 if (hasWindow(window) == false) {
2163 /*
2164 * Someone has a handle to a window I don't have. Ignore this
2165 * request.
2166 */
2167 return;
2168 }
2169
2170 // Whatever window might be moving/dragging, stop it now.
2171 for (TWindow w: windows) {
2172 if (w.inMovements()) {
2173 w.stopMovements();
2174 }
2175 }
2176
2177 assert (windows.size() > 0);
2178
2179 if (window.hidden) {
2180 window.hidden = false;
2181 window.onShow();
2182 activateWindow(window);
2183 }
2184 }
2185
2186 /**
2187 * Close window. Note that the window's destructor is NOT called by this
2188 * method, instead the GC is assumed to do the cleanup.
2189 *
2190 * @param window the window to remove
2191 */
2192 public final void closeWindow(final TWindow window) {
2193 if (hasWindow(window) == false) {
2194 /*
2195 * Someone has a handle to a window I don't have. Ignore this
2196 * request.
2197 */
2198 return;
2199 }
2200
2201 // Let window know that it is about to be closed, while it is still
2202 // visible on screen.
2203 window.onPreClose();
2204
2205 synchronized (windows) {
2206 // Whatever window might be moving/dragging, stop it now.
2207 for (TWindow w: windows) {
2208 if (w.inMovements()) {
2209 w.stopMovements();
2210 }
2211 }
2212
2213 int z = window.getZ();
2214 window.setZ(-1);
2215 window.onUnfocus();
2216 windows.remove(window);
2217 Collections.sort(windows);
2218 activeWindow = null;
2219 int newZ = 0;
2220 boolean foundNextWindow = false;
2221
2222 for (TWindow w: windows) {
2223 w.setZ(newZ);
2224 newZ++;
2225
2226 // Do not activate a hidden window.
2227 if (w.isHidden()) {
2228 continue;
2229 }
2230
2231 if (foundNextWindow == false) {
2232 foundNextWindow = true;
2233 w.setActive(true);
2234 w.onFocus();
2235 assert (activeWindow == null);
2236 activeWindow = w;
2237 continue;
2238 }
2239
2240 if (w.isActive()) {
2241 w.setActive(false);
2242 w.onUnfocus();
2243 }
2244 }
2245 }
2246
2247 // Perform window cleanup
2248 window.onClose();
2249
2250 // Check if we are closing a TMessageBox or similar
2251 if (secondaryEventReceiver != null) {
2252 assert (secondaryEventHandler != null);
2253
2254 // Do not send events to the secondaryEventReceiver anymore, the
2255 // window is closed.
2256 secondaryEventReceiver = null;
2257
2258 // Wake the secondary thread, it will wake the primary as it
2259 // exits.
2260 synchronized (secondaryEventHandler) {
2261 secondaryEventHandler.notify();
2262 }
2263 }
2264
2265 // Permit desktop to be active if it is the only thing left.
2266 if (desktop != null) {
2267 if (windows.size() == 0) {
2268 desktop.setActive(true);
2269 }
2270 }
2271 }
2272
2273 /**
2274 * Switch to the next window.
2275 *
2276 * @param forward if true, then switch to the next window in the list,
2277 * otherwise switch to the previous window in the list
2278 */
2279 public final void switchWindow(final boolean forward) {
2280 // Only switch if there are multiple visible windows
2281 if (shownWindowCount() < 2) {
2282 return;
2283 }
2284 assert (activeWindow != null);
2285
2286 synchronized (windows) {
2287 // Whatever window might be moving/dragging, stop it now.
2288 for (TWindow w: windows) {
2289 if (w.inMovements()) {
2290 w.stopMovements();
2291 }
2292 }
2293
2294 // Swap z/active between active window and the next in the list
2295 int activeWindowI = -1;
2296 for (int i = 0; i < windows.size(); i++) {
2297 if (windows.get(i) == activeWindow) {
2298 assert (activeWindow.isActive());
2299 activeWindowI = i;
2300 break;
2301 } else {
2302 assert (!windows.get(0).isActive());
2303 }
2304 }
2305 assert (activeWindowI >= 0);
2306
2307 // Do not switch if a window is modal
2308 if (activeWindow.isModal()) {
2309 return;
2310 }
2311
2312 int nextWindowI = activeWindowI;
2313 for (;;) {
2314 if (forward) {
2315 nextWindowI++;
2316 nextWindowI %= windows.size();
2317 } else {
2318 nextWindowI--;
2319 if (nextWindowI < 0) {
2320 nextWindowI = windows.size() - 1;
2321 }
2322 }
2323
2324 if (windows.get(nextWindowI).isShown()) {
2325 activateWindow(windows.get(nextWindowI));
2326 break;
2327 }
2328 }
2329 } // synchronized (windows)
2330
2331 }
2332
2333 /**
2334 * Add a window to my window list and make it active. Note package
2335 * private access.
2336 *
2337 * @param window new window to add
2338 */
2339 final void addWindowToApplication(final TWindow window) {
2340
2341 // Do not add menu windows to the window list.
2342 if (window instanceof TMenu) {
2343 return;
2344 }
2345
2346 // Do not add the desktop to the window list.
2347 if (window instanceof TDesktop) {
2348 return;
2349 }
2350
2351 synchronized (windows) {
2352 if (windows.contains(window)) {
2353 throw new IllegalArgumentException("Window " + window +
2354 " is already in window list");
2355 }
2356
2357 // Whatever window might be moving/dragging, stop it now.
2358 for (TWindow w: windows) {
2359 if (w.inMovements()) {
2360 w.stopMovements();
2361 }
2362 }
2363
2364 // Do not allow a modal window to spawn a non-modal window. If a
2365 // modal window is active, then this window will become modal
2366 // too.
2367 if (modalWindowActive()) {
2368 window.flags |= TWindow.MODAL;
2369 window.flags |= TWindow.CENTERED;
2370 window.hidden = false;
2371 }
2372 if (window.isShown()) {
2373 for (TWindow w: windows) {
2374 if (w.isActive()) {
2375 w.setActive(false);
2376 w.onUnfocus();
2377 }
2378 w.setZ(w.getZ() + 1);
2379 }
2380 }
2381 windows.add(window);
2382 if (window.isShown()) {
2383 activeWindow = window;
2384 activeWindow.setZ(0);
2385 activeWindow.setActive(true);
2386 activeWindow.onFocus();
2387 }
2388
2389 if (((window.flags & TWindow.CENTERED) == 0)
2390 && ((window.flags & TWindow.ABSOLUTEXY) == 0)
2391 && (smartWindowPlacement == true)
2392 ) {
2393
2394 doSmartPlacement(window);
2395 }
2396 }
2397
2398 // Desktop cannot be active over any other window.
2399 if (desktop != null) {
2400 desktop.setActive(false);
2401 }
2402 }
2403
2404 /**
2405 * Check if there is a system-modal window on top.
2406 *
2407 * @return true if the active window is modal
2408 */
2409 private boolean modalWindowActive() {
2410 if (windows.size() == 0) {
2411 return false;
2412 }
2413
2414 for (TWindow w: windows) {
2415 if (w.isModal()) {
2416 return true;
2417 }
2418 }
2419
2420 return false;
2421 }
2422
2423 /**
2424 * Check if there is a window with overridden menu flag on top.
2425 *
2426 * @return true if the active window is overriding the menu
2427 */
2428 private boolean overrideMenuWindowActive() {
2429 if (activeWindow != null) {
2430 if (activeWindow.hasOverriddenMenu()) {
2431 return true;
2432 }
2433 }
2434
2435 return false;
2436 }
2437
2438 /**
2439 * Close all open windows.
2440 */
2441 private void closeAllWindows() {
2442 // Don't do anything if we are in the menu
2443 if (activeMenu != null) {
2444 return;
2445 }
2446 while (windows.size() > 0) {
2447 closeWindow(windows.get(0));
2448 }
2449 }
2450
2451 /**
2452 * Re-layout the open windows as non-overlapping tiles. This produces
2453 * almost the same results as Turbo Pascal 7.0's IDE.
2454 */
2455 private void tileWindows() {
2456 synchronized (windows) {
2457 // Don't do anything if we are in the menu
2458 if (activeMenu != null) {
2459 return;
2460 }
2461 int z = windows.size();
2462 if (z == 0) {
2463 return;
2464 }
2465 int a = 0;
2466 int b = 0;
2467 a = (int)(Math.sqrt(z));
2468 int c = 0;
2469 while (c < a) {
2470 b = (z - c) / a;
2471 if (((a * b) + c) == z) {
2472 break;
2473 }
2474 c++;
2475 }
2476 assert (a > 0);
2477 assert (b > 0);
2478 assert (c < a);
2479 int newWidth = (getScreen().getWidth() / a);
2480 int newHeight1 = ((getScreen().getHeight() - 1) / b);
2481 int newHeight2 = ((getScreen().getHeight() - 1) / (b + c));
2482
2483 List<TWindow> sorted = new ArrayList<TWindow>(windows);
2484 Collections.sort(sorted);
2485 Collections.reverse(sorted);
2486 for (int i = 0; i < sorted.size(); i++) {
2487 int logicalX = i / b;
2488 int logicalY = i % b;
2489 if (i >= ((a - 1) * b)) {
2490 logicalX = a - 1;
2491 logicalY = i - ((a - 1) * b);
2492 }
2493
2494 TWindow w = sorted.get(i);
2495 int oldWidth = w.getWidth();
2496 int oldHeight = w.getHeight();
2497
2498 w.setX(logicalX * newWidth);
2499 w.setWidth(newWidth);
2500 if (i >= ((a - 1) * b)) {
2501 w.setY((logicalY * newHeight2) + 1);
2502 w.setHeight(newHeight2);
2503 } else {
2504 w.setY((logicalY * newHeight1) + 1);
2505 w.setHeight(newHeight1);
2506 }
2507 if ((w.getWidth() != oldWidth)
2508 || (w.getHeight() != oldHeight)
2509 ) {
2510 w.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET,
2511 w.getWidth(), w.getHeight()));
2512 }
2513 }
2514 }
2515 }
2516
2517 /**
2518 * Re-layout the open windows as overlapping cascaded windows.
2519 */
2520 private void cascadeWindows() {
2521 synchronized (windows) {
2522 // Don't do anything if we are in the menu
2523 if (activeMenu != null) {
2524 return;
2525 }
2526 int x = 0;
2527 int y = 1;
2528 List<TWindow> sorted = new ArrayList<TWindow>(windows);
2529 Collections.sort(sorted);
2530 Collections.reverse(sorted);
2531 for (TWindow window: sorted) {
2532 window.setX(x);
2533 window.setY(y);
2534 x++;
2535 y++;
2536 if (x > getScreen().getWidth()) {
2537 x = 0;
2538 }
2539 if (y >= getScreen().getHeight()) {
2540 y = 1;
2541 }
2542 }
2543 }
2544 }
2545
2546 /**
2547 * Place a window to minimize its overlap with other windows.
2548 *
2549 * @param window the window to place
2550 */
2551 public final void doSmartPlacement(final TWindow window) {
2552 // This is a pretty dumb algorithm, but seems to work. The hardest
2553 // part is computing these "overlap" values seeking a minimum average
2554 // overlap.
2555 int xMin = 0;
2556 int yMin = desktopTop;
2557 int xMax = getScreen().getWidth() - window.getWidth() + 1;
2558 int yMax = desktopBottom - window.getHeight() + 1;
2559 if (xMax < xMin) {
2560 xMax = xMin;
2561 }
2562 if (yMax < yMin) {
2563 yMax = yMin;
2564 }
2565
2566 if ((xMin == xMax) && (yMin == yMax)) {
2567 // No work to do, bail out.
2568 return;
2569 }
2570
2571 // Compute the overlap matrix without the new window.
2572 int width = getScreen().getWidth();
2573 int height = getScreen().getHeight();
2574 int overlapMatrix[][] = new int[width][height];
2575 for (TWindow w: windows) {
2576 if (window == w) {
2577 continue;
2578 }
2579 for (int x = w.getX(); x < w.getX() + w.getWidth(); x++) {
2580 if (x < 0) {
2581 continue;
2582 }
2583 if (x >= width) {
2584 continue;
2585 }
2586 for (int y = w.getY(); y < w.getY() + w.getHeight(); y++) {
2587 if (y < 0) {
2588 continue;
2589 }
2590 if (y >= height) {
2591 continue;
2592 }
2593 overlapMatrix[x][y]++;
2594 }
2595 }
2596 }
2597
2598 long oldOverlapTotal = 0;
2599 long oldOverlapN = 0;
2600 for (int x = 0; x < width; x++) {
2601 for (int y = 0; y < height; y++) {
2602 oldOverlapTotal += overlapMatrix[x][y];
2603 if (overlapMatrix[x][y] > 0) {
2604 oldOverlapN++;
2605 }
2606 }
2607 }
2608
2609
2610 double oldOverlapAvg = (double) oldOverlapTotal / (double) oldOverlapN;
2611 boolean first = true;
2612 int windowX = window.getX();
2613 int windowY = window.getY();
2614
2615 // For each possible (x, y) position for the new window, compute a
2616 // new overlap matrix.
2617 for (int x = xMin; x < xMax; x++) {
2618 for (int y = yMin; y < yMax; y++) {
2619
2620 // Start with the matrix minus this window.
2621 int newMatrix[][] = new int[width][height];
2622 for (int mx = 0; mx < width; mx++) {
2623 for (int my = 0; my < height; my++) {
2624 newMatrix[mx][my] = overlapMatrix[mx][my];
2625 }
2626 }
2627
2628 // Add this window's values to the new overlap matrix.
2629 long newOverlapTotal = 0;
2630 long newOverlapN = 0;
2631 // Start by adding each new cell.
2632 for (int wx = x; wx < x + window.getWidth(); wx++) {
2633 if (wx >= width) {
2634 continue;
2635 }
2636 for (int wy = y; wy < y + window.getHeight(); wy++) {
2637 if (wy >= height) {
2638 continue;
2639 }
2640 newMatrix[wx][wy]++;
2641 }
2642 }
2643 // Now figure out the new value for total coverage.
2644 for (int mx = 0; mx < width; mx++) {
2645 for (int my = 0; my < height; my++) {
2646 newOverlapTotal += newMatrix[x][y];
2647 if (newMatrix[mx][my] > 0) {
2648 newOverlapN++;
2649 }
2650 }
2651 }
2652 double newOverlapAvg = (double) newOverlapTotal / (double) newOverlapN;
2653
2654 if (first) {
2655 // First time: just record what we got.
2656 oldOverlapAvg = newOverlapAvg;
2657 first = false;
2658 } else {
2659 // All other times: pick a new best (x, y) and save the
2660 // overlap value.
2661 if (newOverlapAvg < oldOverlapAvg) {
2662 windowX = x;
2663 windowY = y;
2664 oldOverlapAvg = newOverlapAvg;
2665 }
2666 }
2667
2668 } // for (int x = xMin; x < xMax; x++)
2669
2670 } // for (int y = yMin; y < yMax; y++)
2671
2672 // Finally, set the window's new coordinates.
2673 window.setX(windowX);
2674 window.setY(windowY);
2675 }
2676
2677 // ------------------------------------------------------------------------
2678 // TMenu management -------------------------------------------------------
2679 // ------------------------------------------------------------------------
2680
2681 /**
2682 * Check if a mouse event would hit either the active menu or any open
2683 * sub-menus.
2684 *
2685 * @param mouse mouse event
2686 * @return true if the mouse would hit the active menu or an open
2687 * sub-menu
2688 */
2689 private boolean mouseOnMenu(final TMouseEvent mouse) {
2690 assert (activeMenu != null);
2691 List<TMenu> menus = new ArrayList<TMenu>(subMenus);
2692 Collections.reverse(menus);
2693 for (TMenu menu: menus) {
2694 if (menu.mouseWouldHit(mouse)) {
2695 return true;
2696 }
2697 }
2698 return activeMenu.mouseWouldHit(mouse);
2699 }
2700
2701 /**
2702 * See if we need to switch window or activate the menu based on
2703 * a mouse click.
2704 *
2705 * @param mouse mouse event
2706 */
2707 private void checkSwitchFocus(final TMouseEvent mouse) {
2708
2709 if ((mouse.getType() == TMouseEvent.Type.MOUSE_DOWN)
2710 && (activeMenu != null)
2711 && (mouse.getAbsoluteY() != 0)
2712 && (!mouseOnMenu(mouse))
2713 ) {
2714 // They clicked outside the active menu, turn it off
2715 activeMenu.setActive(false);
2716 activeMenu = null;
2717 for (TMenu menu: subMenus) {
2718 menu.setActive(false);
2719 }
2720 subMenus.clear();
2721 // Continue checks
2722 }
2723
2724 // See if they hit the menu bar
2725 if ((mouse.getType() == TMouseEvent.Type.MOUSE_DOWN)
2726 && (mouse.isMouse1())
2727 && (!modalWindowActive())
2728 && (!overrideMenuWindowActive())
2729 && (mouse.getAbsoluteY() == 0)
2730 ) {
2731
2732 for (TMenu menu: subMenus) {
2733 menu.setActive(false);
2734 }
2735 subMenus.clear();
2736
2737 // They selected the menu, go activate it
2738 for (TMenu menu: menus) {
2739 if ((mouse.getAbsoluteX() >= menu.getTitleX())
2740 && (mouse.getAbsoluteX() < menu.getTitleX()
2741 + StringUtils.width(menu.getTitle()) + 2)
2742 ) {
2743 menu.setActive(true);
2744 activeMenu = menu;
2745 } else {
2746 menu.setActive(false);
2747 }
2748 }
2749 return;
2750 }
2751
2752 // See if they hit the menu bar
2753 if ((mouse.getType() == TMouseEvent.Type.MOUSE_MOTION)
2754 && (mouse.isMouse1())
2755 && (activeMenu != null)
2756 && (mouse.getAbsoluteY() == 0)
2757 ) {
2758
2759 TMenu oldMenu = activeMenu;
2760 for (TMenu menu: subMenus) {
2761 menu.setActive(false);
2762 }
2763 subMenus.clear();
2764
2765 // See if we should switch menus
2766 for (TMenu menu: menus) {
2767 if ((mouse.getAbsoluteX() >= menu.getTitleX())
2768 && (mouse.getAbsoluteX() < menu.getTitleX()
2769 + StringUtils.width(menu.getTitle()) + 2)
2770 ) {
2771 menu.setActive(true);
2772 activeMenu = menu;
2773 }
2774 }
2775 if (oldMenu != activeMenu) {
2776 // They switched menus
2777 oldMenu.setActive(false);
2778 }
2779 return;
2780 }
2781
2782 // If a menu is still active, don't switch windows
2783 if (activeMenu != null) {
2784 return;
2785 }
2786
2787 // Only switch if there are multiple windows
2788 if (windows.size() < 2) {
2789 return;
2790 }
2791
2792 if (((focusFollowsMouse == true)
2793 && (mouse.getType() == TMouseEvent.Type.MOUSE_MOTION))
2794 || (mouse.getType() == TMouseEvent.Type.MOUSE_DOWN)
2795 ) {
2796 synchronized (windows) {
2797 Collections.sort(windows);
2798 if (windows.get(0).isModal()) {
2799 // Modal windows don't switch
2800 return;
2801 }
2802
2803 for (TWindow window: windows) {
2804 assert (!window.isModal());
2805
2806 if (window.isHidden()) {
2807 assert (!window.isActive());
2808 continue;
2809 }
2810
2811 if (window.mouseWouldHit(mouse)) {
2812 if (window == windows.get(0)) {
2813 // Clicked on the same window, nothing to do
2814 assert (window.isActive());
2815 return;
2816 }
2817
2818 // We will be switching to another window
2819 assert (windows.get(0).isActive());
2820 assert (windows.get(0) == activeWindow);
2821 assert (!window.isActive());
2822 if (activeWindow != null) {
2823 activeWindow.onUnfocus();
2824 activeWindow.setActive(false);
2825 activeWindow.setZ(window.getZ());
2826 }
2827 activeWindow = window;
2828 window.setZ(0);
2829 window.setActive(true);
2830 window.onFocus();
2831 return;
2832 }
2833 }
2834 }
2835
2836 // Clicked on the background, nothing to do
2837 return;
2838 }
2839
2840 // Nothing to do: this isn't a mouse up, or focus isn't following
2841 // mouse.
2842 return;
2843 }
2844
2845 /**
2846 * Turn off the menu.
2847 */
2848 public final void closeMenu() {
2849 if (activeMenu != null) {
2850 activeMenu.setActive(false);
2851 activeMenu = null;
2852 for (TMenu menu: subMenus) {
2853 menu.setActive(false);
2854 }
2855 subMenus.clear();
2856 }
2857 }
2858
2859 /**
2860 * Get a (shallow) copy of the menu list.
2861 *
2862 * @return a copy of the menu list
2863 */
2864 public final List<TMenu> getAllMenus() {
2865 return new ArrayList<TMenu>(menus);
2866 }
2867
2868 /**
2869 * Add a top-level menu to the list.
2870 *
2871 * @param menu the menu to add
2872 * @throws IllegalArgumentException if the menu is already used in
2873 * another TApplication
2874 */
2875 public final void addMenu(final TMenu menu) {
2876 if ((menu.getApplication() != null)
2877 && (menu.getApplication() != this)
2878 ) {
2879 throw new IllegalArgumentException("Menu " + menu + " is already " +
2880 "part of application " + menu.getApplication());
2881 }
2882 closeMenu();
2883 menus.add(menu);
2884 recomputeMenuX();
2885 }
2886
2887 /**
2888 * Remove a top-level menu from the list.
2889 *
2890 * @param menu the menu to remove
2891 * @throws IllegalArgumentException if the menu is already used in
2892 * another TApplication
2893 */
2894 public final void removeMenu(final TMenu menu) {
2895 if ((menu.getApplication() != null)
2896 && (menu.getApplication() != this)
2897 ) {
2898 throw new IllegalArgumentException("Menu " + menu + " is already " +
2899 "part of application " + menu.getApplication());
2900 }
2901 closeMenu();
2902 menus.remove(menu);
2903 recomputeMenuX();
2904 }
2905
2906 /**
2907 * Turn off a sub-menu.
2908 */
2909 public final void closeSubMenu() {
2910 assert (activeMenu != null);
2911 TMenu item = subMenus.get(subMenus.size() - 1);
2912 assert (item != null);
2913 item.setActive(false);
2914 subMenus.remove(subMenus.size() - 1);
2915 }
2916
2917 /**
2918 * Switch to the next menu.
2919 *
2920 * @param forward if true, then switch to the next menu in the list,
2921 * otherwise switch to the previous menu in the list
2922 */
2923 public final void switchMenu(final boolean forward) {
2924 assert (activeMenu != null);
2925
2926 for (TMenu menu: subMenus) {
2927 menu.setActive(false);
2928 }
2929 subMenus.clear();
2930
2931 for (int i = 0; i < menus.size(); i++) {
2932 if (activeMenu == menus.get(i)) {
2933 if (forward) {
2934 if (i < menus.size() - 1) {
2935 i++;
2936 } else {
2937 i = 0;
2938 }
2939 } else {
2940 if (i > 0) {
2941 i--;
2942 } else {
2943 i = menus.size() - 1;
2944 }
2945 }
2946 activeMenu.setActive(false);
2947 activeMenu = menus.get(i);
2948 activeMenu.setActive(true);
2949 return;
2950 }
2951 }
2952 }
2953
2954 /**
2955 * Add a menu item to the global list. If it has a keyboard accelerator,
2956 * that will be added the global hash.
2957 *
2958 * @param item the menu item
2959 */
2960 public final void addMenuItem(final TMenuItem item) {
2961 menuItems.add(item);
2962
2963 TKeypress key = item.getKey();
2964 if (key != null) {
2965 synchronized (accelerators) {
2966 assert (accelerators.get(key) == null);
2967 accelerators.put(key.toLowerCase(), item);
2968 }
2969 }
2970 }
2971
2972 /**
2973 * Disable one menu item.
2974 *
2975 * @param id the menu item ID
2976 */
2977 public final void disableMenuItem(final int id) {
2978 for (TMenuItem item: menuItems) {
2979 if (item.getId() == id) {
2980 item.setEnabled(false);
2981 }
2982 }
2983 }
2984
2985 /**
2986 * Disable the range of menu items with ID's between lower and upper,
2987 * inclusive.
2988 *
2989 * @param lower the lowest menu item ID
2990 * @param upper the highest menu item ID
2991 */
2992 public final void disableMenuItems(final int lower, final int upper) {
2993 for (TMenuItem item: menuItems) {
2994 if ((item.getId() >= lower) && (item.getId() <= upper)) {
2995 item.setEnabled(false);
2996 item.getParent().activate(0);
2997 }
2998 }
2999 }
3000
3001 /**
3002 * Enable one menu item.
3003 *
3004 * @param id the menu item ID
3005 */
3006 public final void enableMenuItem(final int id) {
3007 for (TMenuItem item: menuItems) {
3008 if (item.getId() == id) {
3009 item.setEnabled(true);
3010 item.getParent().activate(0);
3011 }
3012 }
3013 }
3014
3015 /**
3016 * Enable the range of menu items with ID's between lower and upper,
3017 * inclusive.
3018 *
3019 * @param lower the lowest menu item ID
3020 * @param upper the highest menu item ID
3021 */
3022 public final void enableMenuItems(final int lower, final int upper) {
3023 for (TMenuItem item: menuItems) {
3024 if ((item.getId() >= lower) && (item.getId() <= upper)) {
3025 item.setEnabled(true);
3026 item.getParent().activate(0);
3027 }
3028 }
3029 }
3030
3031 /**
3032 * Get the menu item associated with this ID.
3033 *
3034 * @param id the menu item ID
3035 * @return the menu item, or null if not found
3036 */
3037 public final TMenuItem getMenuItem(final int id) {
3038 for (TMenuItem item: menuItems) {
3039 if (item.getId() == id) {
3040 return item;
3041 }
3042 }
3043 return null;
3044 }
3045
3046 /**
3047 * Recompute menu x positions based on their title length.
3048 */
3049 public final void recomputeMenuX() {
3050 int x = 0;
3051 for (TMenu menu: menus) {
3052 menu.setX(x);
3053 menu.setTitleX(x);
3054 x += StringUtils.width(menu.getTitle()) + 2;
3055
3056 // Don't let the menu window exceed the screen width
3057 int rightEdge = menu.getX() + menu.getWidth();
3058 if (rightEdge > getScreen().getWidth()) {
3059 menu.setX(getScreen().getWidth() - menu.getWidth());
3060 }
3061 }
3062 }
3063
3064 /**
3065 * Post an event to process.
3066 *
3067 * @param event new event to add to the queue
3068 */
3069 public final void postEvent(final TInputEvent event) {
3070 synchronized (this) {
3071 synchronized (fillEventQueue) {
3072 fillEventQueue.add(event);
3073 }
3074 if (debugThreads) {
3075 System.err.println(System.currentTimeMillis() + " " +
3076 Thread.currentThread() + " postEvent() wake up main");
3077 }
3078 this.notify();
3079 }
3080 }
3081
3082 /**
3083 * Post an event to process and turn off the menu.
3084 *
3085 * @param event new event to add to the queue
3086 */
3087 public final void postMenuEvent(final TInputEvent event) {
3088 synchronized (this) {
3089 synchronized (fillEventQueue) {
3090 fillEventQueue.add(event);
3091 }
3092 if (debugThreads) {
3093 System.err.println(System.currentTimeMillis() + " " +
3094 Thread.currentThread() + " postMenuEvent() wake up main");
3095 }
3096 closeMenu();
3097 this.notify();
3098 }
3099 }
3100
3101 /**
3102 * Add a sub-menu to the list of open sub-menus.
3103 *
3104 * @param menu sub-menu
3105 */
3106 public final void addSubMenu(final TMenu menu) {
3107 subMenus.add(menu);
3108 }
3109
3110 /**
3111 * Convenience function to add a top-level menu.
3112 *
3113 * @param title menu title
3114 * @return the new menu
3115 */
3116 public final TMenu addMenu(final String title) {
3117 int x = 0;
3118 int y = 0;
3119 TMenu menu = new TMenu(this, x, y, title);
3120 menus.add(menu);
3121 recomputeMenuX();
3122 return menu;
3123 }
3124
3125 /**
3126 * Convenience function to add a default tools (hamburger) menu.
3127 *
3128 * @return the new menu
3129 */
3130 public final TMenu addToolMenu() {
3131 TMenu toolMenu = addMenu(i18n.getString("toolMenuTitle"));
3132 toolMenu.addDefaultItem(TMenu.MID_REPAINT);
3133 toolMenu.addDefaultItem(TMenu.MID_VIEW_IMAGE);
3134 toolMenu.addDefaultItem(TMenu.MID_SCREEN_OPTIONS);
3135 TStatusBar toolStatusBar = toolMenu.newStatusBar(i18n.
3136 getString("toolMenuStatus"));
3137 toolStatusBar.addShortcutKeypress(kbF1, cmHelp, i18n.getString("Help"));
3138 return toolMenu;
3139 }
3140
3141 /**
3142 * Convenience function to add a default "File" menu.
3143 *
3144 * @return the new menu
3145 */
3146 public final TMenu addFileMenu() {
3147 TMenu fileMenu = addMenu(i18n.getString("fileMenuTitle"));
3148 fileMenu.addDefaultItem(TMenu.MID_SHELL);
3149 fileMenu.addSeparator();
3150 fileMenu.addDefaultItem(TMenu.MID_EXIT);
3151 TStatusBar statusBar = fileMenu.newStatusBar(i18n.
3152 getString("fileMenuStatus"));
3153 statusBar.addShortcutKeypress(kbF1, cmHelp, i18n.getString("Help"));
3154 return fileMenu;
3155 }
3156
3157 /**
3158 * Convenience function to add a default "Edit" menu.
3159 *
3160 * @return the new menu
3161 */
3162 public final TMenu addEditMenu() {
3163 TMenu editMenu = addMenu(i18n.getString("editMenuTitle"));
3164 editMenu.addDefaultItem(TMenu.MID_CUT);
3165 editMenu.addDefaultItem(TMenu.MID_COPY);
3166 editMenu.addDefaultItem(TMenu.MID_PASTE);
3167 editMenu.addDefaultItem(TMenu.MID_CLEAR);
3168 TStatusBar statusBar = editMenu.newStatusBar(i18n.
3169 getString("editMenuStatus"));
3170 statusBar.addShortcutKeypress(kbF1, cmHelp, i18n.getString("Help"));
3171 return editMenu;
3172 }
3173
3174 /**
3175 * Convenience function to add a default "Window" menu.
3176 *
3177 * @return the new menu
3178 */
3179 public final TMenu addWindowMenu() {
3180 TMenu windowMenu = addMenu(i18n.getString("windowMenuTitle"));
3181 windowMenu.addDefaultItem(TMenu.MID_TILE);
3182 windowMenu.addDefaultItem(TMenu.MID_CASCADE);
3183 windowMenu.addDefaultItem(TMenu.MID_CLOSE_ALL);
3184 windowMenu.addSeparator();
3185 windowMenu.addDefaultItem(TMenu.MID_WINDOW_MOVE);
3186 windowMenu.addDefaultItem(TMenu.MID_WINDOW_ZOOM);
3187 windowMenu.addDefaultItem(TMenu.MID_WINDOW_NEXT);
3188 windowMenu.addDefaultItem(TMenu.MID_WINDOW_PREVIOUS);
3189 windowMenu.addDefaultItem(TMenu.MID_WINDOW_CLOSE);
3190 TStatusBar statusBar = windowMenu.newStatusBar(i18n.
3191 getString("windowMenuStatus"));
3192 statusBar.addShortcutKeypress(kbF1, cmHelp, i18n.getString("Help"));
3193 return windowMenu;
3194 }
3195
3196 /**
3197 * Convenience function to add a default "Help" menu.
3198 *
3199 * @return the new menu
3200 */
3201 public final TMenu addHelpMenu() {
3202 TMenu helpMenu = addMenu(i18n.getString("helpMenuTitle"));
3203 helpMenu.addDefaultItem(TMenu.MID_HELP_CONTENTS);
3204 helpMenu.addDefaultItem(TMenu.MID_HELP_INDEX);
3205 helpMenu.addDefaultItem(TMenu.MID_HELP_SEARCH);
3206 helpMenu.addDefaultItem(TMenu.MID_HELP_PREVIOUS);
3207 helpMenu.addDefaultItem(TMenu.MID_HELP_HELP);
3208 helpMenu.addDefaultItem(TMenu.MID_HELP_ACTIVE_FILE);
3209 helpMenu.addSeparator();
3210 helpMenu.addDefaultItem(TMenu.MID_ABOUT);
3211 TStatusBar statusBar = helpMenu.newStatusBar(i18n.
3212 getString("helpMenuStatus"));
3213 statusBar.addShortcutKeypress(kbF1, cmHelp, i18n.getString("Help"));
3214 return helpMenu;
3215 }
3216
3217 /**
3218 * Convenience function to add a default "Table" menu.
3219 *
3220 * @return the new menu
3221 */
3222 public final TMenu addTableMenu() {
3223 TMenu tableMenu = addMenu(i18n.getString("tableMenuTitle"));
3224 tableMenu.addDefaultItem(TMenu.MID_TABLE_RENAME_COLUMN, false);
3225 tableMenu.addDefaultItem(TMenu.MID_TABLE_RENAME_ROW, false);
3226 tableMenu.addSeparator();
3227
3228 TSubMenu viewMenu = tableMenu.addSubMenu(i18n.
3229 getString("tableSubMenuView"));
3230 viewMenu.addDefaultItem(TMenu.MID_TABLE_VIEW_ROW_LABELS, false);
3231 viewMenu.addDefaultItem(TMenu.MID_TABLE_VIEW_COLUMN_LABELS, false);
3232 viewMenu.addDefaultItem(TMenu.MID_TABLE_VIEW_HIGHLIGHT_ROW, false);
3233 viewMenu.addDefaultItem(TMenu.MID_TABLE_VIEW_HIGHLIGHT_COLUMN, false);
3234
3235 TSubMenu borderMenu = tableMenu.addSubMenu(i18n.
3236 getString("tableSubMenuBorders"));
3237 borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_NONE, false);
3238 borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_ALL, false);
3239 borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_CELL_NONE, false);
3240 borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_CELL_ALL, false);
3241 borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_RIGHT, false);
3242 borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_LEFT, false);
3243 borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_TOP, false);
3244 borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_BOTTOM, false);
3245 borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_DOUBLE_BOTTOM, false);
3246 borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_THICK_BOTTOM, false);
3247 TSubMenu deleteMenu = tableMenu.addSubMenu(i18n.
3248 getString("tableSubMenuDelete"));
3249 deleteMenu.addDefaultItem(TMenu.MID_TABLE_DELETE_LEFT, false);
3250 deleteMenu.addDefaultItem(TMenu.MID_TABLE_DELETE_UP, false);
3251 deleteMenu.addDefaultItem(TMenu.MID_TABLE_DELETE_ROW, false);
3252 deleteMenu.addDefaultItem(TMenu.MID_TABLE_DELETE_COLUMN, false);
3253 TSubMenu insertMenu = tableMenu.addSubMenu(i18n.
3254 getString("tableSubMenuInsert"));
3255 insertMenu.addDefaultItem(TMenu.MID_TABLE_INSERT_LEFT, false);
3256 insertMenu.addDefaultItem(TMenu.MID_TABLE_INSERT_RIGHT, false);
3257 insertMenu.addDefaultItem(TMenu.MID_TABLE_INSERT_ABOVE, false);
3258 insertMenu.addDefaultItem(TMenu.MID_TABLE_INSERT_BELOW, false);
3259 TSubMenu columnMenu = tableMenu.addSubMenu(i18n.
3260 getString("tableSubMenuColumn"));
3261 columnMenu.addDefaultItem(TMenu.MID_TABLE_COLUMN_NARROW, false);
3262 columnMenu.addDefaultItem(TMenu.MID_TABLE_COLUMN_WIDEN, false);
3263 TSubMenu fileMenu = tableMenu.addSubMenu(i18n.
3264 getString("tableSubMenuFile"));
3265 fileMenu.addDefaultItem(TMenu.MID_TABLE_FILE_OPEN_CSV, false);
3266 fileMenu.addDefaultItem(TMenu.MID_TABLE_FILE_SAVE_CSV, false);
3267 fileMenu.addDefaultItem(TMenu.MID_TABLE_FILE_SAVE_TEXT, false);
3268
3269 TStatusBar statusBar = tableMenu.newStatusBar(i18n.
3270 getString("tableMenuStatus"));
3271 statusBar.addShortcutKeypress(kbF1, cmHelp, i18n.getString("Help"));
3272 return tableMenu;
3273 }
3274
3275 // ------------------------------------------------------------------------
3276 // TTimer management ------------------------------------------------------
3277 // ------------------------------------------------------------------------
3278
3279 /**
3280 * Get the amount of time I can sleep before missing a Timer tick.
3281 *
3282 * @param timeout = initial (maximum) timeout in millis
3283 * @return number of milliseconds between now and the next timer event
3284 */
3285 private long getSleepTime(final long timeout) {
3286 Date now = new Date();
3287 long nowTime = now.getTime();
3288 long sleepTime = timeout;
3289
3290 synchronized (timers) {
3291 for (TTimer timer: timers) {
3292 long nextTickTime = timer.getNextTick().getTime();
3293 if (nextTickTime < nowTime) {
3294 return 0;
3295 }
3296
3297 long timeDifference = nextTickTime - nowTime;
3298 if (timeDifference < sleepTime) {
3299 sleepTime = timeDifference;
3300 }
3301 }
3302 }
3303
3304 assert (sleepTime >= 0);
3305 assert (sleepTime <= timeout);
3306 return sleepTime;
3307 }
3308
3309 /**
3310 * Convenience function to add a timer.
3311 *
3312 * @param duration number of milliseconds to wait between ticks
3313 * @param recurring if true, re-schedule this timer after every tick
3314 * @param action function to call when button is pressed
3315 * @return the timer
3316 */
3317 public final TTimer addTimer(final long duration, final boolean recurring,
3318 final TAction action) {
3319
3320 TTimer timer = new TTimer(duration, recurring, action);
3321 synchronized (timers) {
3322 timers.add(timer);
3323 }
3324 return timer;
3325 }
3326
3327 /**
3328 * Convenience function to remove a timer.
3329 *
3330 * @param timer timer to remove
3331 */
3332 public final void removeTimer(final TTimer timer) {
3333 synchronized (timers) {
3334 timers.remove(timer);
3335 }
3336 }
3337
3338 // ------------------------------------------------------------------------
3339 // Other TWindow constructors ---------------------------------------------
3340 // ------------------------------------------------------------------------
3341
3342 /**
3343 * Convenience function to spawn a message box.
3344 *
3345 * @param title window title, will be centered along the top border
3346 * @param caption message to display. Use embedded newlines to get a
3347 * multi-line box.
3348 * @return the new message box
3349 */
3350 public final TMessageBox messageBox(final String title,
3351 final String caption) {
3352
3353 return new TMessageBox(this, title, caption, TMessageBox.Type.OK);
3354 }
3355
3356 /**
3357 * Convenience function to spawn a message box.
3358 *
3359 * @param title window title, will be centered along the top border
3360 * @param caption message to display. Use embedded newlines to get a
3361 * multi-line box.
3362 * @param type one of the TMessageBox.Type constants. Default is
3363 * Type.OK.
3364 * @return the new message box
3365 */
3366 public final TMessageBox messageBox(final String title,
3367 final String caption, final TMessageBox.Type type) {
3368
3369 return new TMessageBox(this, title, caption, type);
3370 }
3371
3372 /**
3373 * Convenience function to spawn an input box.
3374 *
3375 * @param title window title, will be centered along the top border
3376 * @param caption message to display. Use embedded newlines to get a
3377 * multi-line box.
3378 * @return the new input box
3379 */
3380 public final TInputBox inputBox(final String title, final String caption) {
3381
3382 return new TInputBox(this, title, caption);
3383 }
3384
3385 /**
3386 * Convenience function to spawn an input box.
3387 *
3388 * @param title window title, will be centered along the top border
3389 * @param caption message to display. Use embedded newlines to get a
3390 * multi-line box.
3391 * @param text initial text to seed the field with
3392 * @return the new input box
3393 */
3394 public final TInputBox inputBox(final String title, final String caption,
3395 final String text) {
3396
3397 return new TInputBox(this, title, caption, text);
3398 }
3399
3400 /**
3401 * Convenience function to spawn an input box.
3402 *
3403 * @param title window title, will be centered along the top border
3404 * @param caption message to display. Use embedded newlines to get a
3405 * multi-line box.
3406 * @param text initial text to seed the field with
3407 * @param type one of the Type constants. Default is Type.OK.
3408 * @return the new input box
3409 */
3410 public final TInputBox inputBox(final String title, final String caption,
3411 final String text, final TInputBox.Type type) {
3412
3413 return new TInputBox(this, title, caption, text, type);
3414 }
3415
3416 /**
3417 * Convenience function to open a terminal window.
3418 *
3419 * @param x column relative to parent
3420 * @param y row relative to parent
3421 * @return the terminal new window
3422 */
3423 public final TTerminalWindow openTerminal(final int x, final int y) {
3424 return openTerminal(x, y, TWindow.RESIZABLE);
3425 }
3426
3427 /**
3428 * Convenience function to open a terminal window.
3429 *
3430 * @param x column relative to parent
3431 * @param y row relative to parent
3432 * @param closeOnExit if true, close the window when the command exits
3433 * @return the terminal new window
3434 */
3435 public final TTerminalWindow openTerminal(final int x, final int y,
3436 final boolean closeOnExit) {
3437
3438 return openTerminal(x, y, TWindow.RESIZABLE, closeOnExit);
3439 }
3440
3441 /**
3442 * Convenience function to open a terminal window.
3443 *
3444 * @param x column relative to parent
3445 * @param y row relative to parent
3446 * @param flags mask of CENTERED, MODAL, or RESIZABLE
3447 * @return the terminal new window
3448 */
3449 public final TTerminalWindow openTerminal(final int x, final int y,
3450 final int flags) {
3451
3452 return new TTerminalWindow(this, x, y, flags);
3453 }
3454
3455 /**
3456 * Convenience function to open a terminal window.
3457 *
3458 * @param x column relative to parent
3459 * @param y row relative to parent
3460 * @param flags mask of CENTERED, MODAL, or RESIZABLE
3461 * @param closeOnExit if true, close the window when the command exits
3462 * @return the terminal new window
3463 */
3464 public final TTerminalWindow openTerminal(final int x, final int y,
3465 final int flags, final boolean closeOnExit) {
3466
3467 return new TTerminalWindow(this, x, y, flags, closeOnExit);
3468 }
3469
3470 /**
3471 * Convenience function to open a terminal window and execute a custom
3472 * command line inside it.
3473 *
3474 * @param x column relative to parent
3475 * @param y row relative to parent
3476 * @param commandLine the command line to execute
3477 * @return the terminal new window
3478 */
3479 public final TTerminalWindow openTerminal(final int x, final int y,
3480 final String commandLine) {
3481
3482 return openTerminal(x, y, TWindow.RESIZABLE, commandLine);
3483 }
3484
3485 /**
3486 * Convenience function to open a terminal window and execute a custom
3487 * command line inside it.
3488 *
3489 * @param x column relative to parent
3490 * @param y row relative to parent
3491 * @param commandLine the command line to execute
3492 * @param closeOnExit if true, close the window when the command exits
3493 * @return the terminal new window
3494 */
3495 public final TTerminalWindow openTerminal(final int x, final int y,
3496 final String commandLine, final boolean closeOnExit) {
3497
3498 return openTerminal(x, y, TWindow.RESIZABLE, commandLine, closeOnExit);
3499 }
3500
3501 /**
3502 * Convenience function to open a terminal window and execute a custom
3503 * command line inside it.
3504 *
3505 * @param x column relative to parent
3506 * @param y row relative to parent
3507 * @param flags mask of CENTERED, MODAL, or RESIZABLE
3508 * @param command the command line to execute
3509 * @return the terminal new window
3510 */
3511 public final TTerminalWindow openTerminal(final int x, final int y,
3512 final int flags, final String [] command) {
3513
3514 return new TTerminalWindow(this, x, y, flags, command);
3515 }
3516
3517 /**
3518 * Convenience function to open a terminal window and execute a custom
3519 * command line inside it.
3520 *
3521 * @param x column relative to parent
3522 * @param y row relative to parent
3523 * @param flags mask of CENTERED, MODAL, or RESIZABLE
3524 * @param command the command line to execute
3525 * @param closeOnExit if true, close the window when the command exits
3526 * @return the terminal new window
3527 */
3528 public final TTerminalWindow openTerminal(final int x, final int y,
3529 final int flags, final String [] command, final boolean closeOnExit) {
3530
3531 return new TTerminalWindow(this, x, y, flags, command, closeOnExit);
3532 }
3533
3534 /**
3535 * Convenience function to open a terminal window and execute a custom
3536 * command line inside it.
3537 *
3538 * @param x column relative to parent
3539 * @param y row relative to parent
3540 * @param flags mask of CENTERED, MODAL, or RESIZABLE
3541 * @param commandLine the command line to execute
3542 * @return the terminal new window
3543 */
3544 public final TTerminalWindow openTerminal(final int x, final int y,
3545 final int flags, final String commandLine) {
3546
3547 return new TTerminalWindow(this, x, y, flags, commandLine.split("\\s+"));
3548 }
3549
3550 /**
3551 * Convenience function to open a terminal window and execute a custom
3552 * command line inside it.
3553 *
3554 * @param x column relative to parent
3555 * @param y row relative to parent
3556 * @param flags mask of CENTERED, MODAL, or RESIZABLE
3557 * @param commandLine the command line to execute
3558 * @param closeOnExit if true, close the window when the command exits
3559 * @return the terminal new window
3560 */
3561 public final TTerminalWindow openTerminal(final int x, final int y,
3562 final int flags, final String commandLine, final boolean closeOnExit) {
3563
3564 return new TTerminalWindow(this, x, y, flags, commandLine.split("\\s+"),
3565 closeOnExit);
3566 }
3567
3568 /**
3569 * Convenience function to spawn an file open box.
3570 *
3571 * @param path path of selected file
3572 * @return the result of the new file open box
3573 * @throws IOException if java.io operation throws
3574 */
3575 public final String fileOpenBox(final String path) throws IOException {
3576
3577 TFileOpenBox box = new TFileOpenBox(this, path, TFileOpenBox.Type.OPEN);
3578 return box.getFilename();
3579 }
3580
3581 /**
3582 * Convenience function to spawn an file open box.
3583 *
3584 * @param path path of selected file
3585 * @param type one of the Type constants
3586 * @return the result of the new file open box
3587 * @throws IOException if java.io operation throws
3588 */
3589 public final String fileOpenBox(final String path,
3590 final TFileOpenBox.Type type) throws IOException {
3591
3592 TFileOpenBox box = new TFileOpenBox(this, path, type);
3593 return box.getFilename();
3594 }
3595
3596 /**
3597 * Convenience function to spawn a file open box.
3598 *
3599 * @param path path of selected file
3600 * @param type one of the Type constants
3601 * @param filter a string that files must match to be displayed
3602 * @return the result of the new file open box
3603 * @throws IOException of a java.io operation throws
3604 */
3605 public final String fileOpenBox(final String path,
3606 final TFileOpenBox.Type type, final String filter) throws IOException {
3607
3608 ArrayList<String> filters = new ArrayList<String>();
3609 filters.add(filter);
3610
3611 TFileOpenBox box = new TFileOpenBox(this, path, type, filters);
3612 return box.getFilename();
3613 }
3614
3615 /**
3616 * Convenience function to spawn a file open box.
3617 *
3618 * @param path path of selected file
3619 * @param type one of the Type constants
3620 * @param filters a list of strings that files must match to be displayed
3621 * @return the result of the new file open box
3622 * @throws IOException of a java.io operation throws
3623 */
3624 public final String fileOpenBox(final String path,
3625 final TFileOpenBox.Type type,
3626 final List<String> filters) throws IOException {
3627
3628 TFileOpenBox box = new TFileOpenBox(this, path, type, filters);
3629 return box.getFilename();
3630 }
3631
3632 /**
3633 * Convenience function to create a new window and make it active.
3634 * Window will be located at (0, 0).
3635 *
3636 * @param title window title, will be centered along the top border
3637 * @param width width of window
3638 * @param height height of window
3639 * @return the new window
3640 */
3641 public final TWindow addWindow(final String title, final int width,
3642 final int height) {
3643
3644 TWindow window = new TWindow(this, title, 0, 0, width, height);
3645 return window;
3646 }
3647
3648 /**
3649 * Convenience function to create a new window and make it active.
3650 * Window will be located at (0, 0).
3651 *
3652 * @param title window title, will be centered along the top border
3653 * @param width width of window
3654 * @param height height of window
3655 * @param flags bitmask of RESIZABLE, CENTERED, or MODAL
3656 * @return the new window
3657 */
3658 public final TWindow addWindow(final String title,
3659 final int width, final int height, final int flags) {
3660
3661 TWindow window = new TWindow(this, title, 0, 0, width, height, flags);
3662 return window;
3663 }
3664
3665 /**
3666 * Convenience function to create a new window and make it active.
3667 *
3668 * @param title window title, will be centered along the top border
3669 * @param x column relative to parent
3670 * @param y row relative to parent
3671 * @param width width of window
3672 * @param height height of window
3673 * @return the new window
3674 */
3675 public final TWindow addWindow(final String title,
3676 final int x, final int y, final int width, final int height) {
3677
3678 TWindow window = new TWindow(this, title, x, y, width, height);
3679 return window;
3680 }
3681
3682 /**
3683 * Convenience function to create a new window and make it active.
3684 *
3685 * @param title window title, will be centered along the top border
3686 * @param x column relative to parent
3687 * @param y row relative to parent
3688 * @param width width of window
3689 * @param height height of window
3690 * @param flags mask of RESIZABLE, CENTERED, or MODAL
3691 * @return the new window
3692 */
3693 public final TWindow addWindow(final String title,
3694 final int x, final int y, final int width, final int height,
3695 final int flags) {
3696
3697 TWindow window = new TWindow(this, title, x, y, width, height, flags);
3698 return window;
3699 }
3700
3701}