2 * Jexer - Java Text User Interface
4 * The MIT License (MIT)
6 * Copyright (C) 2019 Kevin Lamonte
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:
15 * The above copyright notice and this permission notice shall be included in
16 * all copies or substantial portions of the Software.
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.
26 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
32 import java
.io
.InputStream
;
33 import java
.io
.IOException
;
34 import java
.io
.OutputStream
;
35 import java
.io
.PrintWriter
;
36 import java
.io
.Reader
;
37 import java
.io
.UnsupportedEncodingException
;
38 import java
.text
.MessageFormat
;
39 import java
.util
.ArrayList
;
40 import java
.util
.Collections
;
41 import java
.util
.Date
;
42 import java
.util
.HashMap
;
43 import java
.util
.LinkedList
;
44 import java
.util
.List
;
46 import java
.util
.ResourceBundle
;
48 import jexer
.bits
.Cell
;
49 import jexer
.bits
.CellAttributes
;
50 import jexer
.bits
.ColorTheme
;
51 import jexer
.bits
.StringUtils
;
52 import jexer
.event
.TCommandEvent
;
53 import jexer
.event
.TInputEvent
;
54 import jexer
.event
.TKeypressEvent
;
55 import jexer
.event
.TMenuEvent
;
56 import jexer
.event
.TMouseEvent
;
57 import jexer
.event
.TResizeEvent
;
58 import jexer
.backend
.Backend
;
59 import jexer
.backend
.MultiBackend
;
60 import jexer
.backend
.Screen
;
61 import jexer
.backend
.SwingBackend
;
62 import jexer
.backend
.ECMA48Backend
;
63 import jexer
.backend
.TWindowBackend
;
64 import jexer
.menu
.TMenu
;
65 import jexer
.menu
.TMenuItem
;
66 import jexer
.menu
.TSubMenu
;
67 import static jexer
.TCommand
.*;
68 import static jexer
.TKeypress
.*;
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.
75 public class TApplication
implements Runnable
{
80 private static final ResourceBundle i18n
= ResourceBundle
.getBundle(TApplication
.class.getName());
82 // ------------------------------------------------------------------------
83 // Constants --------------------------------------------------------------
84 // ------------------------------------------------------------------------
87 * If true, emit thread stuff to System.err.
89 private static final boolean debugThreads
= false;
92 * If true, emit events being processed to System.err.
94 private static final boolean debugEvents
= false;
97 * If true, do "smart placement" on new windows that are not specified to
100 private static final boolean smartWindowPlacement
= true;
103 * Two backend types are available.
105 public static enum BackendType
{
112 * An ECMA48 / ANSI X3.64 / XTERM style terminal.
117 * Synonym for ECMA48.
122 // ------------------------------------------------------------------------
123 // Variables --------------------------------------------------------------
124 // ------------------------------------------------------------------------
127 * The primary event handler thread.
129 private volatile WidgetEventHandler primaryEventHandler
;
132 * The secondary event handler thread.
134 private volatile WidgetEventHandler secondaryEventHandler
;
137 * The screen handler thread.
139 private volatile ScreenHandler screenHandler
;
142 * The widget receiving events from the secondary event handler thread.
144 private volatile TWidget secondaryEventReceiver
;
147 * Access to the physical screen, keyboard, and mouse.
149 private Backend backend
;
152 * Actual mouse coordinate X.
157 * Actual mouse coordinate Y.
162 * Old version of mouse coordinate X.
164 private int oldMouseX
;
167 * Old version mouse coordinate Y.
169 private int oldMouseY
;
172 * Old drawn version of mouse coordinate X.
174 private int oldDrawnMouseX
;
177 * Old drawn version mouse coordinate Y.
179 private int oldDrawnMouseY
;
182 * Old drawn version mouse cell.
184 private Cell oldDrawnMouseCell
= new Cell();
187 * The last mouse up click time, used to determine if this is a mouse
190 private long lastMouseUpTime
;
193 * The amount of millis between mouse up events to assume a double-click.
195 private long doubleClickTime
= 250;
198 * Event queue that is filled by run().
200 private List
<TInputEvent
> fillEventQueue
;
203 * Event queue that will be drained by either primary or secondary
206 private List
<TInputEvent
> drainEventQueue
;
209 * Top-level menus in this application.
211 private List
<TMenu
> menus
;
214 * Stack of activated sub-menus in this application.
216 private List
<TMenu
> subMenus
;
219 * The currently active menu.
221 private TMenu activeMenu
= null;
224 * Active keyboard accelerators.
226 private Map
<TKeypress
, TMenuItem
> accelerators
;
231 private List
<TMenuItem
> menuItems
;
234 * Windows and widgets pull colors from this ColorTheme.
236 private ColorTheme theme
;
239 * The top-level windows (but not menus).
241 private List
<TWindow
> windows
;
244 * The currently acive window.
246 private TWindow activeWindow
= null;
249 * Timers that are being ticked.
251 private List
<TTimer
> timers
;
254 * When true, the application has been started.
256 private volatile boolean started
= false;
259 * When true, exit the application.
261 private volatile boolean quit
= false;
264 * When true, repaint the entire screen.
266 private volatile boolean repaint
= true;
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
273 private static final int desktopTop
= 1;
276 * Y coordinate of the bottom edge of the desktop.
278 private int desktopBottom
;
281 * An optional TDesktop background window that is drawn underneath
284 private TDesktop desktop
;
287 * If true, focus follows mouse: windows automatically raised if the
288 * mouse passes over them.
290 private boolean focusFollowsMouse
= false;
293 * The list of commands to run before the next I/O check.
295 private List
<Runnable
> invokeLaters
= new LinkedList
<Runnable
>();
298 * WidgetEventHandler is the main event consumer loop. There are at most
299 * two such threads in existence: the primary for normal case and a
300 * secondary that is used for TMessageBox, TInputBox, and similar.
302 private class WidgetEventHandler
implements Runnable
{
304 * The main application.
306 private TApplication application
;
309 * Whether or not this WidgetEventHandler is the primary or secondary
312 private boolean primary
= true;
315 * Public constructor.
317 * @param application the main application
318 * @param primary if true, this is the primary event handler thread
320 public WidgetEventHandler(final TApplication application
,
321 final boolean primary
) {
323 this.application
= application
;
324 this.primary
= primary
;
331 // Wrap everything in a try, so that if we go belly up we can let
332 // the user have their terminal back.
335 } catch (Throwable t
) {
336 this.application
.restoreConsole();
338 this.application
.exit();
345 private void runImpl() {
346 boolean first
= true;
349 while (!application
.quit
) {
351 // Wait until application notifies me
352 while (!application
.quit
) {
354 synchronized (application
.drainEventQueue
) {
355 if (application
.drainEventQueue
.size() > 0) {
364 timeout
= application
.getSleepTime(1000);
368 // A timer needs to fire, break out.
373 System
.err
.printf("%d %s %s %s sleep %d millis\n",
374 System
.currentTimeMillis(), this,
375 primary ?
"primary" : "secondary",
376 Thread
.currentThread(), timeout
);
379 synchronized (this) {
384 System
.err
.printf("%d %s %s %s AWAKE\n",
385 System
.currentTimeMillis(), this,
386 primary ?
"primary" : "secondary",
387 Thread
.currentThread());
391 && (application
.secondaryEventReceiver
== null)
393 // Secondary thread, emergency exit. If we got
394 // here then something went wrong with the
395 // handoff between yield() and closeWindow().
396 synchronized (application
.primaryEventHandler
) {
397 application
.primaryEventHandler
.notify();
399 application
.secondaryEventHandler
= null;
400 throw new RuntimeException("secondary exited " +
404 } catch (InterruptedException e
) {
407 } // while (!application.quit)
409 // Pull all events off the queue
411 TInputEvent event
= null;
412 synchronized (application
.drainEventQueue
) {
413 if (application
.drainEventQueue
.size() == 0) {
416 event
= application
.drainEventQueue
.remove(0);
419 // We will have an event to process, so repaint the
420 // screen at the end.
421 application
.repaint
= true;
424 primaryHandleEvent(event
);
426 secondaryHandleEvent(event
);
429 && (application
.secondaryEventReceiver
== null)
431 // Secondary thread, time to exit.
433 // Eliminate my reference so that wakeEventHandler()
434 // resumes working on the primary.
435 application
.secondaryEventHandler
= null;
437 // We are ready to exit, wake up the primary thread.
438 // Remember that it is currently sleeping inside its
439 // primaryHandleEvent().
440 synchronized (application
.primaryEventHandler
) {
441 application
.primaryEventHandler
.notify();
450 // Fire timers, update screen.
452 application
.finishEventProcessing();
455 } // while (true) (main runnable loop)
460 * ScreenHandler pushes screen updates to the physical device.
462 private class ScreenHandler
implements Runnable
{
464 * The main application.
466 private TApplication application
;
471 private boolean dirty
= false;
474 * Public constructor.
476 * @param application the main application
478 public ScreenHandler(final TApplication application
) {
479 this.application
= application
;
483 * The screen update loop.
486 // Wrap everything in a try, so that if we go belly up we can let
487 // the user have their terminal back.
490 } catch (Throwable t
) {
491 this.application
.restoreConsole();
493 this.application
.exit();
500 private void runImpl() {
503 while (!application
.quit
) {
505 // Wait until application notifies me
506 while (!application
.quit
) {
508 synchronized (this) {
514 // Always check within 50 milliseconds.
517 } catch (InterruptedException e
) {
520 } // while (!application.quit)
522 // Flush the screen contents
524 System
.err
.printf("%d %s backend.flushScreen()\n",
525 System
.currentTimeMillis(), Thread
.currentThread());
527 synchronized (getScreen()) {
528 backend
.flushScreen();
530 } // while (true) (main runnable loop)
532 // Shutdown the user I/O thread(s)
537 * Set the dirty flag.
539 public void setDirty() {
540 synchronized (this) {
547 // ------------------------------------------------------------------------
548 // Constructors -----------------------------------------------------------
549 // ------------------------------------------------------------------------
552 * Public constructor.
554 * @param backendType BackendType.XTERM, BackendType.ECMA48 or
556 * @param windowWidth the number of text columns to start with
557 * @param windowHeight the number of text rows to start with
558 * @param fontSize the size in points
559 * @throws UnsupportedEncodingException if an exception is thrown when
560 * creating the InputStreamReader
562 public TApplication(final BackendType backendType
, final int windowWidth
,
563 final int windowHeight
, final int fontSize
)
564 throws UnsupportedEncodingException
{
566 switch (backendType
) {
568 backend
= new SwingBackend(this, windowWidth
, windowHeight
,
574 backend
= new ECMA48Backend(this, null, null, windowWidth
,
575 windowHeight
, fontSize
);
578 throw new IllegalArgumentException("Invalid backend type: "
585 * Public constructor.
587 * @param backendType BackendType.XTERM, BackendType.ECMA48 or
589 * @throws UnsupportedEncodingException if an exception is thrown when
590 * creating the InputStreamReader
592 public TApplication(final BackendType backendType
)
593 throws UnsupportedEncodingException
{
595 switch (backendType
) {
597 // The default SwingBackend is 80x25, 20 pt font. If you want to
598 // change that, you can pass the extra arguments to the
599 // SwingBackend constructor here. For example, if you wanted
600 // 90x30, 16 pt font:
602 // backend = new SwingBackend(this, 90, 30, 16);
603 backend
= new SwingBackend(this);
608 backend
= new ECMA48Backend(this, null, null);
611 throw new IllegalArgumentException("Invalid backend type: "
618 * Public constructor. The backend type will be BackendType.ECMA48.
620 * @param input an InputStream connected to the remote user, or null for
621 * System.in. If System.in is used, then on non-Windows systems it will
622 * be put in raw mode; shutdown() will (blindly!) put System.in in cooked
623 * mode. input is always converted to a Reader with UTF-8 encoding.
624 * @param output an OutputStream connected to the remote user, or null
625 * for System.out. output is always converted to a Writer with UTF-8
627 * @throws UnsupportedEncodingException if an exception is thrown when
628 * creating the InputStreamReader
630 public TApplication(final InputStream input
,
631 final OutputStream output
) throws UnsupportedEncodingException
{
633 backend
= new ECMA48Backend(this, input
, output
);
638 * Public constructor. The backend type will be BackendType.ECMA48.
640 * @param input the InputStream underlying 'reader'. Its available()
641 * method is used to determine if reader.read() will block or not.
642 * @param reader a Reader connected to the remote user.
643 * @param writer a PrintWriter connected to the remote user.
644 * @param setRawMode if true, set System.in into raw mode with stty.
645 * This should in general not be used. It is here solely for Demo3,
646 * which uses System.in.
647 * @throws IllegalArgumentException if input, reader, or writer are null.
649 public TApplication(final InputStream input
, final Reader reader
,
650 final PrintWriter writer
, final boolean setRawMode
) {
652 backend
= new ECMA48Backend(this, input
, reader
, writer
, setRawMode
);
657 * Public constructor. The backend type will be BackendType.ECMA48.
659 * @param input the InputStream underlying 'reader'. Its available()
660 * method is used to determine if reader.read() will block or not.
661 * @param reader a Reader connected to the remote user.
662 * @param writer a PrintWriter connected to the remote user.
663 * @throws IllegalArgumentException if input, reader, or writer are null.
665 public TApplication(final InputStream input
, final Reader reader
,
666 final PrintWriter writer
) {
668 this(input
, reader
, writer
, false);
672 * Public constructor. This hook enables use with new non-Jexer
675 * @param backend a Backend that is already ready to go.
677 public TApplication(final Backend backend
) {
678 this.backend
= backend
;
679 backend
.setListener(this);
684 * Finish construction once the backend is set.
686 private void TApplicationImpl() {
687 theme
= new ColorTheme();
688 desktopBottom
= getScreen().getHeight() - 1;
689 fillEventQueue
= new LinkedList
<TInputEvent
>();
690 drainEventQueue
= new LinkedList
<TInputEvent
>();
691 windows
= new LinkedList
<TWindow
>();
692 menus
= new ArrayList
<TMenu
>();
693 subMenus
= new ArrayList
<TMenu
>();
694 timers
= new LinkedList
<TTimer
>();
695 accelerators
= new HashMap
<TKeypress
, TMenuItem
>();
696 menuItems
= new LinkedList
<TMenuItem
>();
697 desktop
= new TDesktop(this);
699 // Special case: the Swing backend needs to have a timer to drive its
701 if ((backend
instanceof SwingBackend
)
702 || (backend
instanceof MultiBackend
)
704 // Default to 500 millis, unless a SwingBackend has its own
707 if (backend
instanceof SwingBackend
) {
708 millis
= ((SwingBackend
) backend
).getBlinkMillis();
711 addTimer(millis
, true,
714 TApplication
.this.doRepaint();
722 // ------------------------------------------------------------------------
723 // Runnable ---------------------------------------------------------------
724 // ------------------------------------------------------------------------
727 * Run this application until it exits.
730 // System.err.println("*** TApplication.run() begins ***");
732 // Start the screen updater thread
733 screenHandler
= new ScreenHandler(this);
734 (new Thread(screenHandler
)).start();
736 // Start the main consumer thread
737 primaryEventHandler
= new WidgetEventHandler(this, true);
738 (new Thread(primaryEventHandler
)).start();
743 synchronized (this) {
744 boolean doWait
= false;
746 if (!backend
.hasEvents()) {
747 synchronized (fillEventQueue
) {
748 if (fillEventQueue
.size() == 0) {
755 // No I/O to dispatch, so wait until the backend
759 System
.err
.println(System
.currentTimeMillis() +
760 " " + Thread
.currentThread() + " MAIN sleep");
766 System
.err
.println(System
.currentTimeMillis() +
767 " " + Thread
.currentThread() + " MAIN AWAKE");
769 } catch (InterruptedException e
) {
770 // I'm awake and don't care why, let's see what's
771 // going on out there.
775 } // synchronized (this)
777 synchronized (fillEventQueue
) {
778 // Pull any pending I/O events
779 backend
.getEvents(fillEventQueue
);
781 // Dispatch each event to the appropriate handler, one at a
784 TInputEvent event
= null;
785 if (fillEventQueue
.size() == 0) {
788 event
= fillEventQueue
.remove(0);
789 metaHandleEvent(event
);
793 // Wake a consumer thread if we have any pending events.
794 if (drainEventQueue
.size() > 0) {
800 // Shutdown the event consumer threads
801 if (secondaryEventHandler
!= null) {
802 synchronized (secondaryEventHandler
) {
803 secondaryEventHandler
.notify();
806 if (primaryEventHandler
!= null) {
807 synchronized (primaryEventHandler
) {
808 primaryEventHandler
.notify();
812 // Close all the windows. This gives them an opportunity to release
816 // Give the overarching application an opportunity to release
820 // System.err.println("*** TApplication.run() exits ***");
823 // ------------------------------------------------------------------------
824 // Event handlers ---------------------------------------------------------
825 // ------------------------------------------------------------------------
828 * Method that TApplication subclasses can override to handle menu or
829 * posted command events.
831 * @param command command event
832 * @return if true, this event was consumed
834 protected boolean onCommand(final TCommandEvent command
) {
835 // Default: handle cmExit
836 if (command
.equals(cmExit
)) {
837 if (messageBox(i18n
.getString("exitDialogTitle"),
838 i18n
.getString("exitDialogText"),
839 TMessageBox
.Type
.YESNO
).isYes()) {
846 if (command
.equals(cmShell
)) {
847 openTerminal(0, 0, TWindow
.RESIZABLE
);
851 if (command
.equals(cmTile
)) {
855 if (command
.equals(cmCascade
)) {
859 if (command
.equals(cmCloseAll
)) {
864 if (command
.equals(cmMenu
)) {
865 if (!modalWindowActive() && (activeMenu
== null)) {
866 if (menus
.size() > 0) {
867 menus
.get(0).setActive(true);
868 activeMenu
= menus
.get(0);
878 * Method that TApplication subclasses can override to handle menu
881 * @param menu menu event
882 * @return if true, this event was consumed
884 protected boolean onMenu(final TMenuEvent menu
) {
886 // Default: handle MID_EXIT
887 if (menu
.getId() == TMenu
.MID_EXIT
) {
888 if (messageBox(i18n
.getString("exitDialogTitle"),
889 i18n
.getString("exitDialogText"),
890 TMessageBox
.Type
.YESNO
).isYes()) {
897 if (menu
.getId() == TMenu
.MID_SHELL
) {
898 openTerminal(0, 0, TWindow
.RESIZABLE
);
902 if (menu
.getId() == TMenu
.MID_TILE
) {
906 if (menu
.getId() == TMenu
.MID_CASCADE
) {
910 if (menu
.getId() == TMenu
.MID_CLOSE_ALL
) {
914 if (menu
.getId() == TMenu
.MID_ABOUT
) {
918 if (menu
.getId() == TMenu
.MID_REPAINT
) {
919 getScreen().clearPhysical();
923 if (menu
.getId() == TMenu
.MID_VIEW_IMAGE
) {
927 if (menu
.getId() == TMenu
.MID_CHANGE_FONT
) {
928 new TFontChooserWindow(this);
935 * Method that TApplication subclasses can override to handle keystrokes.
937 * @param keypress keystroke event
938 * @return if true, this event was consumed
940 protected boolean onKeypress(final TKeypressEvent keypress
) {
941 // Default: only menu shortcuts
943 // Process Alt-F, Alt-E, etc. menu shortcut keys
944 if (!keypress
.getKey().isFnKey()
945 && keypress
.getKey().isAlt()
946 && !keypress
.getKey().isCtrl()
947 && (activeMenu
== null)
948 && !modalWindowActive()
951 assert (subMenus
.size() == 0);
953 for (TMenu menu
: menus
) {
954 if (Character
.toLowerCase(menu
.getMnemonic().getShortcut())
955 == Character
.toLowerCase(keypress
.getKey().getChar())
958 menu
.setActive(true);
968 * Process background events, and update the screen.
970 private void finishEventProcessing() {
972 System
.err
.printf(System
.currentTimeMillis() + " " +
973 Thread
.currentThread() + " finishEventProcessing()\n");
976 // Process timers and call doIdle()'s
980 synchronized (getScreen()) {
984 // Wake up the screen repainter
988 System
.err
.printf(System
.currentTimeMillis() + " " +
989 Thread
.currentThread() + " finishEventProcessing() END\n");
994 * Peek at certain application-level events, add to eventQueue, and wake
995 * up the consuming Thread.
997 * @param event the input event to consume
999 private void metaHandleEvent(final TInputEvent event
) {
1002 System
.err
.printf(String
.format("metaHandleEvents event: %s\n",
1003 event
)); System
.err
.flush();
1007 // Do no more processing if the application is already trying
1012 // Special application-wide events -------------------------------
1015 if (event
instanceof TCommandEvent
) {
1016 TCommandEvent command
= (TCommandEvent
) event
;
1017 if (command
.equals(cmAbort
)) {
1023 synchronized (drainEventQueue
) {
1025 if (event
instanceof TResizeEvent
) {
1026 TResizeEvent resize
= (TResizeEvent
) event
;
1027 synchronized (getScreen()) {
1028 getScreen().setDimensions(resize
.getWidth(),
1029 resize
.getHeight());
1030 desktopBottom
= getScreen().getHeight() - 1;
1036 if (desktop
!= null) {
1037 desktop
.setDimensions(0, 0, resize
.getWidth(),
1038 resize
.getHeight() - 1);
1039 desktop
.onResize(resize
);
1042 // Change menu edges if needed.
1045 // We are dirty, redraw the screen.
1049 System.err.println("New screen: " + resize.getWidth() +
1050 " x " + resize.getHeight());
1055 // Put into the main queue
1056 drainEventQueue
.add(event
);
1061 * Dispatch one event to the appropriate widget or application-level
1062 * event handler. This is the primary event handler, it has the normal
1063 * application-wide event handling.
1065 * @param event the input event to consume
1066 * @see #secondaryHandleEvent(TInputEvent event)
1068 private void primaryHandleEvent(final TInputEvent event
) {
1071 System
.err
.printf("%s primaryHandleEvent: %s\n",
1072 Thread
.currentThread(), event
);
1074 TMouseEvent doubleClick
= null;
1076 // Special application-wide events -----------------------------------
1078 // Peek at the mouse position
1079 if (event
instanceof TMouseEvent
) {
1080 TMouseEvent mouse
= (TMouseEvent
) event
;
1081 if ((mouseX
!= mouse
.getX()) || (mouseY
!= mouse
.getY())) {
1084 mouseX
= mouse
.getX();
1085 mouseY
= mouse
.getY();
1087 if ((mouse
.getType() == TMouseEvent
.Type
.MOUSE_DOWN
)
1088 && (!mouse
.isMouseWheelUp())
1089 && (!mouse
.isMouseWheelDown())
1091 if ((mouse
.getTime().getTime() - lastMouseUpTime
) <
1094 // This is a double-click.
1095 doubleClick
= new TMouseEvent(TMouseEvent
.Type
.
1097 mouse
.getX(), mouse
.getY(),
1098 mouse
.getAbsoluteX(), mouse
.getAbsoluteY(),
1099 mouse
.isMouse1(), mouse
.isMouse2(),
1101 mouse
.isMouseWheelUp(), mouse
.isMouseWheelDown());
1104 // The first click of a potential double-click.
1105 lastMouseUpTime
= mouse
.getTime().getTime();
1110 // See if we need to switch focus to another window or the menu
1111 checkSwitchFocus((TMouseEvent
) event
);
1114 // Handle menu events
1115 if ((activeMenu
!= null) && !(event
instanceof TCommandEvent
)) {
1116 TMenu menu
= activeMenu
;
1118 if (event
instanceof TMouseEvent
) {
1119 TMouseEvent mouse
= (TMouseEvent
) event
;
1121 while (subMenus
.size() > 0) {
1122 TMenu subMenu
= subMenus
.get(subMenus
.size() - 1);
1123 if (subMenu
.mouseWouldHit(mouse
)) {
1126 if ((mouse
.getType() == TMouseEvent
.Type
.MOUSE_MOTION
)
1127 && (!mouse
.isMouse1())
1128 && (!mouse
.isMouse2())
1129 && (!mouse
.isMouse3())
1130 && (!mouse
.isMouseWheelUp())
1131 && (!mouse
.isMouseWheelDown())
1135 // We navigated away from a sub-menu, so close it
1139 // Convert the mouse relative x/y to menu coordinates
1140 assert (mouse
.getX() == mouse
.getAbsoluteX());
1141 assert (mouse
.getY() == mouse
.getAbsoluteY());
1142 if (subMenus
.size() > 0) {
1143 menu
= subMenus
.get(subMenus
.size() - 1);
1145 mouse
.setX(mouse
.getX() - menu
.getX());
1146 mouse
.setY(mouse
.getY() - menu
.getY());
1148 menu
.handleEvent(event
);
1152 if (event
instanceof TKeypressEvent
) {
1153 TKeypressEvent keypress
= (TKeypressEvent
) event
;
1155 // See if this key matches an accelerator, and is not being
1156 // shortcutted by the active window, and if so dispatch the menu
1158 boolean windowWillShortcut
= false;
1159 if (activeWindow
!= null) {
1160 assert (activeWindow
.isShown());
1161 if (activeWindow
.isShortcutKeypress(keypress
.getKey())) {
1162 // We do not process this key, it will be passed to the
1164 windowWillShortcut
= true;
1168 if (!windowWillShortcut
&& !modalWindowActive()) {
1169 TKeypress keypressLowercase
= keypress
.getKey().toLowerCase();
1170 TMenuItem item
= null;
1171 synchronized (accelerators
) {
1172 item
= accelerators
.get(keypressLowercase
);
1175 if (item
.isEnabled()) {
1176 // Let the menu item dispatch
1182 // Handle the keypress
1183 if (onKeypress(keypress
)) {
1189 if (event
instanceof TCommandEvent
) {
1190 if (onCommand((TCommandEvent
) event
)) {
1195 if (event
instanceof TMenuEvent
) {
1196 if (onMenu((TMenuEvent
) event
)) {
1201 // Dispatch events to the active window -------------------------------
1202 boolean dispatchToDesktop
= true;
1203 TWindow window
= activeWindow
;
1204 if (window
!= null) {
1205 assert (window
.isActive());
1206 assert (window
.isShown());
1207 if (event
instanceof TMouseEvent
) {
1208 TMouseEvent mouse
= (TMouseEvent
) event
;
1209 // Convert the mouse relative x/y to window coordinates
1210 assert (mouse
.getX() == mouse
.getAbsoluteX());
1211 assert (mouse
.getY() == mouse
.getAbsoluteY());
1212 mouse
.setX(mouse
.getX() - window
.getX());
1213 mouse
.setY(mouse
.getY() - window
.getY());
1215 if (doubleClick
!= null) {
1216 doubleClick
.setX(doubleClick
.getX() - window
.getX());
1217 doubleClick
.setY(doubleClick
.getY() - window
.getY());
1220 if (window
.mouseWouldHit(mouse
)) {
1221 dispatchToDesktop
= false;
1223 } else if (event
instanceof TKeypressEvent
) {
1224 dispatchToDesktop
= false;
1228 System
.err
.printf("TApplication dispatch event: %s\n",
1231 window
.handleEvent(event
);
1232 if (doubleClick
!= null) {
1233 window
.handleEvent(doubleClick
);
1236 if (dispatchToDesktop
) {
1237 // This event is fair game for the desktop to process.
1238 if (desktop
!= null) {
1239 desktop
.handleEvent(event
);
1240 if (doubleClick
!= null) {
1241 desktop
.handleEvent(doubleClick
);
1248 * Dispatch one event to the appropriate widget or application-level
1249 * event handler. This is the secondary event handler used by certain
1250 * special dialogs (currently TMessageBox and TFileOpenBox).
1252 * @param event the input event to consume
1253 * @see #primaryHandleEvent(TInputEvent event)
1255 private void secondaryHandleEvent(final TInputEvent event
) {
1256 TMouseEvent doubleClick
= null;
1259 System
.err
.printf("%s secondaryHandleEvent: %s\n",
1260 Thread
.currentThread(), event
);
1263 // Peek at the mouse position
1264 if (event
instanceof TMouseEvent
) {
1265 TMouseEvent mouse
= (TMouseEvent
) event
;
1266 if ((mouseX
!= mouse
.getX()) || (mouseY
!= mouse
.getY())) {
1269 mouseX
= mouse
.getX();
1270 mouseY
= mouse
.getY();
1272 if ((mouse
.getType() == TMouseEvent
.Type
.MOUSE_DOWN
)
1273 && (!mouse
.isMouseWheelUp())
1274 && (!mouse
.isMouseWheelDown())
1276 if ((mouse
.getTime().getTime() - lastMouseUpTime
) <
1279 // This is a double-click.
1280 doubleClick
= new TMouseEvent(TMouseEvent
.Type
.
1282 mouse
.getX(), mouse
.getY(),
1283 mouse
.getAbsoluteX(), mouse
.getAbsoluteY(),
1284 mouse
.isMouse1(), mouse
.isMouse2(),
1286 mouse
.isMouseWheelUp(), mouse
.isMouseWheelDown());
1289 // The first click of a potential double-click.
1290 lastMouseUpTime
= mouse
.getTime().getTime();
1296 secondaryEventReceiver
.handleEvent(event
);
1297 // Note that it is possible for secondaryEventReceiver to be null
1298 // now, because its handleEvent() might have finished out on the
1299 // secondary thread. So put any extra processing inside a null
1301 if (secondaryEventReceiver
!= null) {
1302 if (doubleClick
!= null) {
1303 secondaryEventReceiver
.handleEvent(doubleClick
);
1309 * Enable a widget to override the primary event thread.
1311 * @param widget widget that will receive events
1313 public final void enableSecondaryEventReceiver(final TWidget widget
) {
1315 System
.err
.println(System
.currentTimeMillis() +
1316 " enableSecondaryEventReceiver()");
1319 assert (secondaryEventReceiver
== null);
1320 assert (secondaryEventHandler
== null);
1321 assert ((widget
instanceof TMessageBox
)
1322 || (widget
instanceof TFileOpenBox
));
1323 secondaryEventReceiver
= widget
;
1324 secondaryEventHandler
= new WidgetEventHandler(this, false);
1326 (new Thread(secondaryEventHandler
)).start();
1330 * Yield to the secondary thread.
1332 public final void yield() {
1334 System
.err
.printf(System
.currentTimeMillis() + " " +
1335 Thread
.currentThread() + " yield()\n");
1338 assert (secondaryEventReceiver
!= null);
1340 while (secondaryEventReceiver
!= null) {
1341 synchronized (primaryEventHandler
) {
1343 primaryEventHandler
.wait();
1344 } catch (InterruptedException e
) {
1352 * Do stuff when there is no user input.
1354 private void doIdle() {
1356 System
.err
.printf(System
.currentTimeMillis() + " " +
1357 Thread
.currentThread() + " doIdle()\n");
1360 synchronized (timers
) {
1363 System
.err
.printf(System
.currentTimeMillis() + " " +
1364 Thread
.currentThread() + " doIdle() 2\n");
1367 // Run any timers that have timed out
1368 Date now
= new Date();
1369 List
<TTimer
> keepTimers
= new LinkedList
<TTimer
>();
1370 for (TTimer timer
: timers
) {
1371 if (timer
.getNextTick().getTime() <= now
.getTime()) {
1372 // Something might change, so repaint the screen.
1375 if (timer
.recurring
) {
1376 keepTimers
.add(timer
);
1379 keepTimers
.add(timer
);
1383 timers
.addAll(keepTimers
);
1387 for (TWindow window
: windows
) {
1390 if (desktop
!= null) {
1394 // Run any invokeLaters
1395 synchronized (invokeLaters
) {
1396 for (Runnable invoke
: invokeLaters
) {
1399 invokeLaters
.clear();
1405 * Wake the sleeping active event handler.
1407 private void wakeEventHandler() {
1412 if (secondaryEventHandler
!= null) {
1413 synchronized (secondaryEventHandler
) {
1414 secondaryEventHandler
.notify();
1417 assert (primaryEventHandler
!= null);
1418 synchronized (primaryEventHandler
) {
1419 primaryEventHandler
.notify();
1425 * Wake the sleeping screen handler.
1427 private void wakeScreenHandler() {
1432 synchronized (screenHandler
) {
1433 screenHandler
.notify();
1437 // ------------------------------------------------------------------------
1438 // TApplication -----------------------------------------------------------
1439 // ------------------------------------------------------------------------
1442 * Place a command on the run queue, and run it before the next round of
1445 * @param command the command to run later
1447 public void invokeLater(final Runnable command
) {
1448 synchronized (invokeLaters
) {
1449 invokeLaters
.add(command
);
1455 * Restore the console to sane defaults. This is meant to be used for
1456 * improper exits (e.g. a caught exception in main()), and should not be
1457 * necessary for normal program termination.
1459 public void restoreConsole() {
1460 if (backend
!= null) {
1461 if (backend
instanceof ECMA48Backend
) {
1470 * @return the Backend
1472 public final Backend
getBackend() {
1479 * @return the Screen
1481 public final Screen
getScreen() {
1482 if (backend
instanceof TWindowBackend
) {
1483 // We are being rendered to a TWindow. We can't use its
1484 // getScreen() method because that is how it is rendering to a
1485 // hardware backend somewhere. Instead use its getOtherScreen()
1487 return ((TWindowBackend
) backend
).getOtherScreen();
1489 return backend
.getScreen();
1494 * Get the color theme.
1498 public final ColorTheme
getTheme() {
1503 * Repaint the screen on the next update.
1505 public void doRepaint() {
1511 * Get Y coordinate of the top edge of the desktop.
1513 * @return Y coordinate of the top edge of the desktop
1515 public final int getDesktopTop() {
1520 * Get Y coordinate of the bottom edge of the desktop.
1522 * @return Y coordinate of the bottom edge of the desktop
1524 public final int getDesktopBottom() {
1525 return desktopBottom
;
1529 * Set the TDesktop instance.
1531 * @param desktop a TDesktop instance, or null to remove the one that is
1534 public final void setDesktop(final TDesktop desktop
) {
1535 if (this.desktop
!= null) {
1536 this.desktop
.onClose();
1538 this.desktop
= desktop
;
1542 * Get the TDesktop instance.
1544 * @return the desktop, or null if it is not set
1546 public final TDesktop
getDesktop() {
1551 * Get the current active window.
1553 * @return the active window, or null if it is not set
1555 public final TWindow
getActiveWindow() {
1556 return activeWindow
;
1560 * Get a (shallow) copy of the window list.
1562 * @return a copy of the list of windows for this application
1564 public final List
<TWindow
> getAllWindows() {
1565 List
<TWindow
> result
= new ArrayList
<TWindow
>();
1566 result
.addAll(windows
);
1571 * Get focusFollowsMouse flag.
1573 * @return true if focus follows mouse: windows automatically raised if
1574 * the mouse passes over them
1576 public boolean getFocusFollowsMouse() {
1577 return focusFollowsMouse
;
1581 * Set focusFollowsMouse flag.
1583 * @param focusFollowsMouse if true, focus follows mouse: windows
1584 * automatically raised if the mouse passes over them
1586 public void setFocusFollowsMouse(final boolean focusFollowsMouse
) {
1587 this.focusFollowsMouse
= focusFollowsMouse
;
1591 * Display the about dialog.
1593 protected void showAboutDialog() {
1594 String version
= getClass().getPackage().getImplementationVersion();
1595 if (version
== null) {
1596 // This is Java 9+, use a hardcoded string here.
1599 messageBox(i18n
.getString("aboutDialogTitle"),
1600 MessageFormat
.format(i18n
.getString("aboutDialogText"), version
),
1601 TMessageBox
.Type
.OK
);
1605 * Handle the Tool | Open image menu item.
1607 private void openImage() {
1609 List
<String
> filters
= new ArrayList
<String
>();
1610 filters
.add("^.*\\.[Jj][Pp][Gg]$");
1611 filters
.add("^.*\\.[Jj][Pp][Ee][Gg]$");
1612 filters
.add("^.*\\.[Pp][Nn][Gg]$");
1613 filters
.add("^.*\\.[Gg][Ii][Ff]$");
1614 filters
.add("^.*\\.[Bb][Mm][Pp]$");
1615 String filename
= fileOpenBox(".", TFileOpenBox
.Type
.OPEN
, filters
);
1616 if (filename
!= null) {
1617 new TImageWindow(this, new File(filename
));
1619 } catch (IOException e
) {
1620 // Show this exception to the user.
1621 new TExceptionDialog(this, e
);
1626 * Check if application is still running.
1628 * @return true if the application is running
1630 public final boolean isRunning() {
1637 // ------------------------------------------------------------------------
1638 // Screen refresh loop ----------------------------------------------------
1639 // ------------------------------------------------------------------------
1642 * Invert the cell color at a position. This is used to track the mouse.
1644 * @param x column position
1645 * @param y row position
1647 private void invertCell(final int x
, final int y
) {
1648 invertCell(x
, y
, false);
1652 * Invert the cell color at a position. This is used to track the mouse.
1654 * @param x column position
1655 * @param y row position
1656 * @param onlyThisCell if true, only invert this cell
1658 private void invertCell(final int x
, final int y
,
1659 final boolean onlyThisCell
) {
1662 System
.err
.printf("%d %s invertCell() %d %d\n",
1663 System
.currentTimeMillis(), Thread
.currentThread(), x
, y
);
1665 if (activeWindow
!= null) {
1666 System
.err
.println("activeWindow.hasHiddenMouse() " +
1667 activeWindow
.hasHiddenMouse());
1671 // If this cell is on top of a visible window that has requested a
1672 // hidden mouse, bail out.
1673 if ((activeWindow
!= null) && (activeMenu
== null)) {
1674 if ((activeWindow
.hasHiddenMouse() == true)
1675 && (x
> activeWindow
.getX())
1676 && (x
< activeWindow
.getX() + activeWindow
.getWidth() - 1)
1677 && (y
> activeWindow
.getY())
1678 && (y
< activeWindow
.getY() + activeWindow
.getHeight() - 1)
1684 Cell cell
= getScreen().getCharXY(x
, y
);
1685 if (cell
.isImage()) {
1688 if (cell
.getForeColorRGB() < 0) {
1689 cell
.setForeColor(cell
.getForeColor().invert());
1691 cell
.setForeColorRGB(cell
.getForeColorRGB() ^
0x00ffffff);
1693 if (cell
.getBackColorRGB() < 0) {
1694 cell
.setBackColor(cell
.getBackColor().invert());
1696 cell
.setBackColorRGB(cell
.getBackColorRGB() ^
0x00ffffff);
1699 getScreen().putCharXY(x
, y
, cell
);
1700 if ((onlyThisCell
== true) || (cell
.getWidth() == Cell
.Width
.SINGLE
)) {
1704 // This cell is one half of a fullwidth glyph. Invert the other
1706 if (cell
.getWidth() == Cell
.Width
.LEFT
) {
1707 if (x
< getScreen().getWidth() - 1) {
1708 Cell rightHalf
= getScreen().getCharXY(x
+ 1, y
);
1709 if (rightHalf
.getWidth() == Cell
.Width
.RIGHT
) {
1710 invertCell(x
+ 1, y
, true);
1715 assert (cell
.getWidth() == Cell
.Width
.RIGHT
);
1718 Cell leftHalf
= getScreen().getCharXY(x
- 1, y
);
1719 if (leftHalf
.getWidth() == Cell
.Width
.LEFT
) {
1720 invertCell(x
- 1, y
, true);
1728 private void drawAll() {
1729 boolean menuIsActive
= false;
1732 System
.err
.printf("%d %s drawAll() enter\n",
1733 System
.currentTimeMillis(), Thread
.currentThread());
1736 // I don't think this does anything useful anymore...
1739 System
.err
.printf("%d %s drawAll() !repaint\n",
1740 System
.currentTimeMillis(), Thread
.currentThread());
1742 if ((oldDrawnMouseX
!= mouseX
) || (oldDrawnMouseY
!= mouseY
)) {
1744 System
.err
.printf("%d %s drawAll() !repaint MOUSE\n",
1745 System
.currentTimeMillis(), Thread
.currentThread());
1748 // The only thing that has happened is the mouse moved.
1750 // Redraw the old cell at that position, and save the cell at
1751 // the new mouse position.
1753 System
.err
.printf("%d %s restoreImage() %d %d\n",
1754 System
.currentTimeMillis(), Thread
.currentThread(),
1755 oldDrawnMouseX
, oldDrawnMouseY
);
1757 oldDrawnMouseCell
.restoreImage();
1758 getScreen().putCharXY(oldDrawnMouseX
, oldDrawnMouseY
,
1760 oldDrawnMouseCell
= getScreen().getCharXY(mouseX
, mouseY
);
1761 if (backend
instanceof ECMA48Backend
) {
1762 // Special case: the entire row containing the mouse has
1763 // to be re-drawn if it has any image data, AND any rows
1765 if (oldDrawnMouseY
!= mouseY
) {
1766 for (int i
= oldDrawnMouseY
; ;) {
1767 getScreen().unsetImageRow(i
);
1771 if (oldDrawnMouseY
< mouseY
) {
1778 getScreen().unsetImageRow(mouseY
);
1782 // Draw mouse at the new position.
1783 invertCell(mouseX
, mouseY
);
1785 oldDrawnMouseX
= mouseX
;
1786 oldDrawnMouseY
= mouseY
;
1788 if (getScreen().isDirty()) {
1789 screenHandler
.setDirty();
1795 System
.err
.printf("%d %s drawAll() REDRAW\n",
1796 System
.currentTimeMillis(), Thread
.currentThread());
1799 // If true, the cursor is not visible
1800 boolean cursor
= false;
1802 // Start with a clean screen
1803 getScreen().clear();
1806 if (desktop
!= null) {
1807 desktop
.drawChildren();
1810 // Draw each window in reverse Z order
1811 List
<TWindow
> sorted
= new ArrayList
<TWindow
>(windows
);
1812 Collections
.sort(sorted
);
1813 TWindow topLevel
= null;
1814 if (sorted
.size() > 0) {
1815 topLevel
= sorted
.get(0);
1817 Collections
.reverse(sorted
);
1818 for (TWindow window
: sorted
) {
1819 if (window
.isShown()) {
1820 window
.drawChildren();
1824 // Draw the blank menubar line - reset the screen clipping first so
1825 // it won't trim it out.
1826 getScreen().resetClipping();
1827 getScreen().hLineXY(0, 0, getScreen().getWidth(), ' ',
1828 theme
.getColor("tmenu"));
1829 // Now draw the menus.
1831 for (TMenu menu
: menus
) {
1832 CellAttributes menuColor
;
1833 CellAttributes menuMnemonicColor
;
1834 if (menu
.isActive()) {
1835 menuIsActive
= true;
1836 menuColor
= theme
.getColor("tmenu.highlighted");
1837 menuMnemonicColor
= theme
.getColor("tmenu.mnemonic.highlighted");
1840 menuColor
= theme
.getColor("tmenu");
1841 menuMnemonicColor
= theme
.getColor("tmenu.mnemonic");
1843 // Draw the menu title
1844 getScreen().hLineXY(x
, 0, StringUtils
.width(menu
.getTitle()) + 2, ' ',
1846 getScreen().putStringXY(x
+ 1, 0, menu
.getTitle(), menuColor
);
1847 // Draw the highlight character
1848 getScreen().putCharXY(x
+ 1 + menu
.getMnemonic().getShortcutIdx(),
1849 0, menu
.getMnemonic().getShortcut(), menuMnemonicColor
);
1851 if (menu
.isActive()) {
1852 ((TWindow
) menu
).drawChildren();
1853 // Reset the screen clipping so we can draw the next title.
1854 getScreen().resetClipping();
1856 x
+= StringUtils
.width(menu
.getTitle()) + 2;
1859 for (TMenu menu
: subMenus
) {
1860 // Reset the screen clipping so we can draw the next sub-menu.
1861 getScreen().resetClipping();
1862 ((TWindow
) menu
).drawChildren();
1864 getScreen().resetClipping();
1866 // Draw the status bar of the top-level window
1867 TStatusBar statusBar
= null;
1868 if (topLevel
!= null) {
1869 statusBar
= topLevel
.getStatusBar();
1871 if (statusBar
!= null) {
1872 getScreen().resetClipping();
1873 statusBar
.setWidth(getScreen().getWidth());
1874 statusBar
.setY(getScreen().getHeight() - topLevel
.getY());
1877 CellAttributes barColor
= new CellAttributes();
1878 barColor
.setTo(getTheme().getColor("tstatusbar.text"));
1879 getScreen().hLineXY(0, desktopBottom
, getScreen().getWidth(), ' ',
1883 // Draw the mouse pointer
1885 System
.err
.printf("%d %s restoreImage() %d %d\n",
1886 System
.currentTimeMillis(), Thread
.currentThread(),
1887 oldDrawnMouseX
, oldDrawnMouseY
);
1889 oldDrawnMouseCell
= getScreen().getCharXY(mouseX
, mouseY
);
1890 if (backend
instanceof ECMA48Backend
) {
1891 // Special case: the entire row containing the mouse has to be
1892 // re-drawn if it has any image data, AND any rows in between.
1893 if (oldDrawnMouseY
!= mouseY
) {
1894 for (int i
= oldDrawnMouseY
; ;) {
1895 getScreen().unsetImageRow(i
);
1899 if (oldDrawnMouseY
< mouseY
) {
1906 getScreen().unsetImageRow(mouseY
);
1909 invertCell(mouseX
, mouseY
);
1910 oldDrawnMouseX
= mouseX
;
1911 oldDrawnMouseY
= mouseY
;
1913 // Place the cursor if it is visible
1914 if (!menuIsActive
) {
1915 TWidget activeWidget
= null;
1916 if (sorted
.size() > 0) {
1917 activeWidget
= sorted
.get(sorted
.size() - 1).getActiveChild();
1918 if (activeWidget
.isCursorVisible()) {
1919 if ((activeWidget
.getCursorAbsoluteY() < desktopBottom
)
1920 && (activeWidget
.getCursorAbsoluteY() > desktopTop
)
1922 getScreen().putCursor(true,
1923 activeWidget
.getCursorAbsoluteX(),
1924 activeWidget
.getCursorAbsoluteY());
1927 // Turn off the cursor. Also place it at 0,0.
1928 getScreen().putCursor(false, 0, 0);
1937 getScreen().hideCursor();
1940 if (getScreen().isDirty()) {
1941 screenHandler
.setDirty();
1947 * Force this application to exit.
1949 public void exit() {
1951 synchronized (this) {
1957 * Subclasses can use this hook to cleanup resources. Called as the last
1958 * step of TApplication.run().
1960 public void onExit() {
1961 // Default does nothing.
1964 // ------------------------------------------------------------------------
1965 // TWindow management -----------------------------------------------------
1966 // ------------------------------------------------------------------------
1969 * Return the total number of windows.
1971 * @return the total number of windows
1973 public final int windowCount() {
1974 return windows
.size();
1978 * Return the number of windows that are showing.
1980 * @return the number of windows that are showing on screen
1982 public final int shownWindowCount() {
1984 for (TWindow w
: windows
) {
1993 * Return the number of windows that are hidden.
1995 * @return the number of windows that are hidden
1997 public final int hiddenWindowCount() {
1999 for (TWindow w
: windows
) {
2008 * Check if a window instance is in this application's window list.
2010 * @param window window to look for
2011 * @return true if this window is in the list
2013 public final boolean hasWindow(final TWindow window
) {
2014 if (windows
.size() == 0) {
2017 for (TWindow w
: windows
) {
2019 assert (window
.getApplication() == this);
2027 * Activate a window: bring it to the top and have it receive events.
2029 * @param window the window to become the new active window
2031 public void activateWindow(final TWindow window
) {
2032 if (hasWindow(window
) == false) {
2034 * Someone has a handle to a window I don't have. Ignore this
2040 // Whatever window might be moving/dragging, stop it now.
2041 for (TWindow w
: windows
) {
2042 if (w
.inMovements()) {
2047 assert (windows
.size() > 0);
2049 if (window
.isHidden()) {
2050 // Unhiding will also activate.
2054 assert (window
.isShown());
2056 if (windows
.size() == 1) {
2057 assert (window
== windows
.get(0));
2058 if (activeWindow
== null) {
2059 activeWindow
= window
;
2061 activeWindow
.setActive(true);
2062 activeWindow
.onFocus();
2065 assert (window
.isActive());
2066 assert (activeWindow
== window
);
2070 if (activeWindow
== window
) {
2071 assert (window
.isActive());
2073 // Window is already active, do nothing.
2077 assert (!window
.isActive());
2078 if (activeWindow
!= null) {
2079 // TODO: see if this assertion is really necessary.
2080 // assert (activeWindow.getZ() == 0);
2082 activeWindow
.setActive(false);
2084 // Increment every window Z that is on top of window
2085 for (TWindow w
: windows
) {
2089 if (w
.getZ() < window
.getZ()) {
2090 w
.setZ(w
.getZ() + 1);
2094 // Unset activeWindow now before unfocus, so that a window
2095 // lifecycle change inside onUnfocus() doesn't call
2096 // switchWindow() and lead to a stack overflow.
2097 TWindow oldActiveWindow
= activeWindow
;
2098 activeWindow
= null;
2099 oldActiveWindow
.onUnfocus();
2101 activeWindow
= window
;
2102 activeWindow
.setZ(0);
2103 activeWindow
.setActive(true);
2104 activeWindow
.onFocus();
2111 * @param window the window to hide
2113 public void hideWindow(final TWindow window
) {
2114 if (hasWindow(window
) == false) {
2116 * Someone has a handle to a window I don't have. Ignore this
2122 // Whatever window might be moving/dragging, stop it now.
2123 for (TWindow w
: windows
) {
2124 if (w
.inMovements()) {
2129 assert (windows
.size() > 0);
2131 if (!window
.hidden
) {
2132 if (window
== activeWindow
) {
2133 if (shownWindowCount() > 1) {
2136 activeWindow
= null;
2137 window
.setActive(false);
2141 window
.hidden
= true;
2149 * @param window the window to show
2151 public void showWindow(final TWindow window
) {
2152 if (hasWindow(window
) == false) {
2154 * Someone has a handle to a window I don't have. Ignore this
2160 // Whatever window might be moving/dragging, stop it now.
2161 for (TWindow w
: windows
) {
2162 if (w
.inMovements()) {
2167 assert (windows
.size() > 0);
2169 if (window
.hidden
) {
2170 window
.hidden
= false;
2172 activateWindow(window
);
2177 * Close window. Note that the window's destructor is NOT called by this
2178 * method, instead the GC is assumed to do the cleanup.
2180 * @param window the window to remove
2182 public final void closeWindow(final TWindow window
) {
2183 if (hasWindow(window
) == false) {
2185 * Someone has a handle to a window I don't have. Ignore this
2191 // Let window know that it is about to be closed, while it is still
2192 // visible on screen.
2193 window
.onPreClose();
2195 synchronized (windows
) {
2196 // Whatever window might be moving/dragging, stop it now.
2197 for (TWindow w
: windows
) {
2198 if (w
.inMovements()) {
2203 int z
= window
.getZ();
2206 windows
.remove(window
);
2207 Collections
.sort(windows
);
2208 activeWindow
= null;
2210 boolean foundNextWindow
= false;
2212 for (TWindow w
: windows
) {
2216 // Do not activate a hidden window.
2221 if (foundNextWindow
== false) {
2222 foundNextWindow
= true;
2225 assert (activeWindow
== null);
2237 // Perform window cleanup
2240 // Check if we are closing a TMessageBox or similar
2241 if (secondaryEventReceiver
!= null) {
2242 assert (secondaryEventHandler
!= null);
2244 // Do not send events to the secondaryEventReceiver anymore, the
2245 // window is closed.
2246 secondaryEventReceiver
= null;
2248 // Wake the secondary thread, it will wake the primary as it
2250 synchronized (secondaryEventHandler
) {
2251 secondaryEventHandler
.notify();
2255 // Permit desktop to be active if it is the only thing left.
2256 if (desktop
!= null) {
2257 if (windows
.size() == 0) {
2258 desktop
.setActive(true);
2264 * Switch to the next window.
2266 * @param forward if true, then switch to the next window in the list,
2267 * otherwise switch to the previous window in the list
2269 public final void switchWindow(final boolean forward
) {
2270 // Only switch if there are multiple visible windows
2271 if (shownWindowCount() < 2) {
2274 assert (activeWindow
!= null);
2276 synchronized (windows
) {
2277 // Whatever window might be moving/dragging, stop it now.
2278 for (TWindow w
: windows
) {
2279 if (w
.inMovements()) {
2284 // Swap z/active between active window and the next in the list
2285 int activeWindowI
= -1;
2286 for (int i
= 0; i
< windows
.size(); i
++) {
2287 if (windows
.get(i
) == activeWindow
) {
2288 assert (activeWindow
.isActive());
2292 assert (!windows
.get(0).isActive());
2295 assert (activeWindowI
>= 0);
2297 // Do not switch if a window is modal
2298 if (activeWindow
.isModal()) {
2302 int nextWindowI
= activeWindowI
;
2306 nextWindowI
%= windows
.size();
2309 if (nextWindowI
< 0) {
2310 nextWindowI
= windows
.size() - 1;
2314 if (windows
.get(nextWindowI
).isShown()) {
2315 activateWindow(windows
.get(nextWindowI
));
2319 } // synchronized (windows)
2324 * Add a window to my window list and make it active. Note package
2327 * @param window new window to add
2329 final void addWindowToApplication(final TWindow window
) {
2331 // Do not add menu windows to the window list.
2332 if (window
instanceof TMenu
) {
2336 // Do not add the desktop to the window list.
2337 if (window
instanceof TDesktop
) {
2341 synchronized (windows
) {
2342 if (windows
.contains(window
)) {
2343 throw new IllegalArgumentException("Window " + window
+
2344 " is already in window list");
2347 // Whatever window might be moving/dragging, stop it now.
2348 for (TWindow w
: windows
) {
2349 if (w
.inMovements()) {
2354 // Do not allow a modal window to spawn a non-modal window. If a
2355 // modal window is active, then this window will become modal
2357 if (modalWindowActive()) {
2358 window
.flags
|= TWindow
.MODAL
;
2359 window
.flags
|= TWindow
.CENTERED
;
2360 window
.hidden
= false;
2362 if (window
.isShown()) {
2363 for (TWindow w
: windows
) {
2368 w
.setZ(w
.getZ() + 1);
2371 windows
.add(window
);
2372 if (window
.isShown()) {
2373 activeWindow
= window
;
2374 activeWindow
.setZ(0);
2375 activeWindow
.setActive(true);
2376 activeWindow
.onFocus();
2379 if (((window
.flags
& TWindow
.CENTERED
) == 0)
2380 && ((window
.flags
& TWindow
.ABSOLUTEXY
) == 0)
2381 && (smartWindowPlacement
== true)
2384 doSmartPlacement(window
);
2388 // Desktop cannot be active over any other window.
2389 if (desktop
!= null) {
2390 desktop
.setActive(false);
2395 * Check if there is a system-modal window on top.
2397 * @return true if the active window is modal
2399 private boolean modalWindowActive() {
2400 if (windows
.size() == 0) {
2404 for (TWindow w
: windows
) {
2414 * Check if there is a window with overridden menu flag on top.
2416 * @return true if the active window is overriding the menu
2418 private boolean overrideMenuWindowActive() {
2419 if (activeWindow
!= null) {
2420 if (activeWindow
.hasOverriddenMenu()) {
2429 * Close all open windows.
2431 private void closeAllWindows() {
2432 // Don't do anything if we are in the menu
2433 if (activeMenu
!= null) {
2436 while (windows
.size() > 0) {
2437 closeWindow(windows
.get(0));
2442 * Re-layout the open windows as non-overlapping tiles. This produces
2443 * almost the same results as Turbo Pascal 7.0's IDE.
2445 private void tileWindows() {
2446 synchronized (windows
) {
2447 // Don't do anything if we are in the menu
2448 if (activeMenu
!= null) {
2451 int z
= windows
.size();
2457 a
= (int)(Math
.sqrt(z
));
2461 if (((a
* b
) + c
) == z
) {
2469 int newWidth
= (getScreen().getWidth() / a
);
2470 int newHeight1
= ((getScreen().getHeight() - 1) / b
);
2471 int newHeight2
= ((getScreen().getHeight() - 1) / (b
+ c
));
2473 List
<TWindow
> sorted
= new ArrayList
<TWindow
>(windows
);
2474 Collections
.sort(sorted
);
2475 Collections
.reverse(sorted
);
2476 for (int i
= 0; i
< sorted
.size(); i
++) {
2477 int logicalX
= i
/ b
;
2478 int logicalY
= i
% b
;
2479 if (i
>= ((a
- 1) * b
)) {
2481 logicalY
= i
- ((a
- 1) * b
);
2484 TWindow w
= sorted
.get(i
);
2485 int oldWidth
= w
.getWidth();
2486 int oldHeight
= w
.getHeight();
2488 w
.setX(logicalX
* newWidth
);
2489 w
.setWidth(newWidth
);
2490 if (i
>= ((a
- 1) * b
)) {
2491 w
.setY((logicalY
* newHeight2
) + 1);
2492 w
.setHeight(newHeight2
);
2494 w
.setY((logicalY
* newHeight1
) + 1);
2495 w
.setHeight(newHeight1
);
2497 if ((w
.getWidth() != oldWidth
)
2498 || (w
.getHeight() != oldHeight
)
2500 w
.onResize(new TResizeEvent(TResizeEvent
.Type
.WIDGET
,
2501 w
.getWidth(), w
.getHeight()));
2508 * Re-layout the open windows as overlapping cascaded windows.
2510 private void cascadeWindows() {
2511 synchronized (windows
) {
2512 // Don't do anything if we are in the menu
2513 if (activeMenu
!= null) {
2518 List
<TWindow
> sorted
= new ArrayList
<TWindow
>(windows
);
2519 Collections
.sort(sorted
);
2520 Collections
.reverse(sorted
);
2521 for (TWindow window
: sorted
) {
2526 if (x
> getScreen().getWidth()) {
2529 if (y
>= getScreen().getHeight()) {
2537 * Place a window to minimize its overlap with other windows.
2539 * @param window the window to place
2541 public final void doSmartPlacement(final TWindow window
) {
2542 // This is a pretty dumb algorithm, but seems to work. The hardest
2543 // part is computing these "overlap" values seeking a minimum average
2546 int yMin
= desktopTop
;
2547 int xMax
= getScreen().getWidth() - window
.getWidth() + 1;
2548 int yMax
= desktopBottom
- window
.getHeight() + 1;
2556 if ((xMin
== xMax
) && (yMin
== yMax
)) {
2557 // No work to do, bail out.
2561 // Compute the overlap matrix without the new window.
2562 int width
= getScreen().getWidth();
2563 int height
= getScreen().getHeight();
2564 int overlapMatrix
[][] = new int[width
][height
];
2565 for (TWindow w
: windows
) {
2569 for (int x
= w
.getX(); x
< w
.getX() + w
.getWidth(); x
++) {
2576 for (int y
= w
.getY(); y
< w
.getY() + w
.getHeight(); y
++) {
2583 overlapMatrix
[x
][y
]++;
2588 long oldOverlapTotal
= 0;
2589 long oldOverlapN
= 0;
2590 for (int x
= 0; x
< width
; x
++) {
2591 for (int y
= 0; y
< height
; y
++) {
2592 oldOverlapTotal
+= overlapMatrix
[x
][y
];
2593 if (overlapMatrix
[x
][y
] > 0) {
2600 double oldOverlapAvg
= (double) oldOverlapTotal
/ (double) oldOverlapN
;
2601 boolean first
= true;
2602 int windowX
= window
.getX();
2603 int windowY
= window
.getY();
2605 // For each possible (x, y) position for the new window, compute a
2606 // new overlap matrix.
2607 for (int x
= xMin
; x
< xMax
; x
++) {
2608 for (int y
= yMin
; y
< yMax
; y
++) {
2610 // Start with the matrix minus this window.
2611 int newMatrix
[][] = new int[width
][height
];
2612 for (int mx
= 0; mx
< width
; mx
++) {
2613 for (int my
= 0; my
< height
; my
++) {
2614 newMatrix
[mx
][my
] = overlapMatrix
[mx
][my
];
2618 // Add this window's values to the new overlap matrix.
2619 long newOverlapTotal
= 0;
2620 long newOverlapN
= 0;
2621 // Start by adding each new cell.
2622 for (int wx
= x
; wx
< x
+ window
.getWidth(); wx
++) {
2626 for (int wy
= y
; wy
< y
+ window
.getHeight(); wy
++) {
2630 newMatrix
[wx
][wy
]++;
2633 // Now figure out the new value for total coverage.
2634 for (int mx
= 0; mx
< width
; mx
++) {
2635 for (int my
= 0; my
< height
; my
++) {
2636 newOverlapTotal
+= newMatrix
[x
][y
];
2637 if (newMatrix
[mx
][my
] > 0) {
2642 double newOverlapAvg
= (double) newOverlapTotal
/ (double) newOverlapN
;
2645 // First time: just record what we got.
2646 oldOverlapAvg
= newOverlapAvg
;
2649 // All other times: pick a new best (x, y) and save the
2651 if (newOverlapAvg
< oldOverlapAvg
) {
2654 oldOverlapAvg
= newOverlapAvg
;
2658 } // for (int x = xMin; x < xMax; x++)
2660 } // for (int y = yMin; y < yMax; y++)
2662 // Finally, set the window's new coordinates.
2663 window
.setX(windowX
);
2664 window
.setY(windowY
);
2667 // ------------------------------------------------------------------------
2668 // TMenu management -------------------------------------------------------
2669 // ------------------------------------------------------------------------
2672 * Check if a mouse event would hit either the active menu or any open
2675 * @param mouse mouse event
2676 * @return true if the mouse would hit the active menu or an open
2679 private boolean mouseOnMenu(final TMouseEvent mouse
) {
2680 assert (activeMenu
!= null);
2681 List
<TMenu
> menus
= new ArrayList
<TMenu
>(subMenus
);
2682 Collections
.reverse(menus
);
2683 for (TMenu menu
: menus
) {
2684 if (menu
.mouseWouldHit(mouse
)) {
2688 return activeMenu
.mouseWouldHit(mouse
);
2692 * See if we need to switch window or activate the menu based on
2695 * @param mouse mouse event
2697 private void checkSwitchFocus(final TMouseEvent mouse
) {
2699 if ((mouse
.getType() == TMouseEvent
.Type
.MOUSE_DOWN
)
2700 && (activeMenu
!= null)
2701 && (mouse
.getAbsoluteY() != 0)
2702 && (!mouseOnMenu(mouse
))
2704 // They clicked outside the active menu, turn it off
2705 activeMenu
.setActive(false);
2707 for (TMenu menu
: subMenus
) {
2708 menu
.setActive(false);
2714 // See if they hit the menu bar
2715 if ((mouse
.getType() == TMouseEvent
.Type
.MOUSE_DOWN
)
2716 && (mouse
.isMouse1())
2717 && (!modalWindowActive())
2718 && (!overrideMenuWindowActive())
2719 && (mouse
.getAbsoluteY() == 0)
2722 for (TMenu menu
: subMenus
) {
2723 menu
.setActive(false);
2727 // They selected the menu, go activate it
2728 for (TMenu menu
: menus
) {
2729 if ((mouse
.getAbsoluteX() >= menu
.getTitleX())
2730 && (mouse
.getAbsoluteX() < menu
.getTitleX()
2731 + StringUtils
.width(menu
.getTitle()) + 2)
2733 menu
.setActive(true);
2736 menu
.setActive(false);
2742 // See if they hit the menu bar
2743 if ((mouse
.getType() == TMouseEvent
.Type
.MOUSE_MOTION
)
2744 && (mouse
.isMouse1())
2745 && (activeMenu
!= null)
2746 && (mouse
.getAbsoluteY() == 0)
2749 TMenu oldMenu
= activeMenu
;
2750 for (TMenu menu
: subMenus
) {
2751 menu
.setActive(false);
2755 // See if we should switch menus
2756 for (TMenu menu
: menus
) {
2757 if ((mouse
.getAbsoluteX() >= menu
.getTitleX())
2758 && (mouse
.getAbsoluteX() < menu
.getTitleX()
2759 + StringUtils
.width(menu
.getTitle()) + 2)
2761 menu
.setActive(true);
2765 if (oldMenu
!= activeMenu
) {
2766 // They switched menus
2767 oldMenu
.setActive(false);
2772 // If a menu is still active, don't switch windows
2773 if (activeMenu
!= null) {
2777 // Only switch if there are multiple windows
2778 if (windows
.size() < 2) {
2782 if (((focusFollowsMouse
== true)
2783 && (mouse
.getType() == TMouseEvent
.Type
.MOUSE_MOTION
))
2784 || (mouse
.getType() == TMouseEvent
.Type
.MOUSE_DOWN
)
2786 synchronized (windows
) {
2787 Collections
.sort(windows
);
2788 if (windows
.get(0).isModal()) {
2789 // Modal windows don't switch
2793 for (TWindow window
: windows
) {
2794 assert (!window
.isModal());
2796 if (window
.isHidden()) {
2797 assert (!window
.isActive());
2801 if (window
.mouseWouldHit(mouse
)) {
2802 if (window
== windows
.get(0)) {
2803 // Clicked on the same window, nothing to do
2804 assert (window
.isActive());
2808 // We will be switching to another window
2809 assert (windows
.get(0).isActive());
2810 assert (windows
.get(0) == activeWindow
);
2811 assert (!window
.isActive());
2812 if (activeWindow
!= null) {
2813 activeWindow
.onUnfocus();
2814 activeWindow
.setActive(false);
2815 activeWindow
.setZ(window
.getZ());
2817 activeWindow
= window
;
2819 window
.setActive(true);
2826 // Clicked on the background, nothing to do
2830 // Nothing to do: this isn't a mouse up, or focus isn't following
2836 * Turn off the menu.
2838 public final void closeMenu() {
2839 if (activeMenu
!= null) {
2840 activeMenu
.setActive(false);
2842 for (TMenu menu
: subMenus
) {
2843 menu
.setActive(false);
2850 * Get a (shallow) copy of the menu list.
2852 * @return a copy of the menu list
2854 public final List
<TMenu
> getAllMenus() {
2855 return new ArrayList
<TMenu
>(menus
);
2859 * Add a top-level menu to the list.
2861 * @param menu the menu to add
2862 * @throws IllegalArgumentException if the menu is already used in
2863 * another TApplication
2865 public final void addMenu(final TMenu menu
) {
2866 if ((menu
.getApplication() != null)
2867 && (menu
.getApplication() != this)
2869 throw new IllegalArgumentException("Menu " + menu
+ " is already " +
2870 "part of application " + menu
.getApplication());
2878 * Remove a top-level menu from the list.
2880 * @param menu the menu to remove
2881 * @throws IllegalArgumentException if the menu is already used in
2882 * another TApplication
2884 public final void removeMenu(final TMenu menu
) {
2885 if ((menu
.getApplication() != null)
2886 && (menu
.getApplication() != this)
2888 throw new IllegalArgumentException("Menu " + menu
+ " is already " +
2889 "part of application " + menu
.getApplication());
2897 * Turn off a sub-menu.
2899 public final void closeSubMenu() {
2900 assert (activeMenu
!= null);
2901 TMenu item
= subMenus
.get(subMenus
.size() - 1);
2902 assert (item
!= null);
2903 item
.setActive(false);
2904 subMenus
.remove(subMenus
.size() - 1);
2908 * Switch to the next menu.
2910 * @param forward if true, then switch to the next menu in the list,
2911 * otherwise switch to the previous menu in the list
2913 public final void switchMenu(final boolean forward
) {
2914 assert (activeMenu
!= null);
2916 for (TMenu menu
: subMenus
) {
2917 menu
.setActive(false);
2921 for (int i
= 0; i
< menus
.size(); i
++) {
2922 if (activeMenu
== menus
.get(i
)) {
2924 if (i
< menus
.size() - 1) {
2933 i
= menus
.size() - 1;
2936 activeMenu
.setActive(false);
2937 activeMenu
= menus
.get(i
);
2938 activeMenu
.setActive(true);
2945 * Add a menu item to the global list. If it has a keyboard accelerator,
2946 * that will be added the global hash.
2948 * @param item the menu item
2950 public final void addMenuItem(final TMenuItem item
) {
2951 menuItems
.add(item
);
2953 TKeypress key
= item
.getKey();
2955 synchronized (accelerators
) {
2956 assert (accelerators
.get(key
) == null);
2957 accelerators
.put(key
.toLowerCase(), item
);
2963 * Disable one menu item.
2965 * @param id the menu item ID
2967 public final void disableMenuItem(final int id
) {
2968 for (TMenuItem item
: menuItems
) {
2969 if (item
.getId() == id
) {
2970 item
.setEnabled(false);
2976 * Disable the range of menu items with ID's between lower and upper,
2979 * @param lower the lowest menu item ID
2980 * @param upper the highest menu item ID
2982 public final void disableMenuItems(final int lower
, final int upper
) {
2983 for (TMenuItem item
: menuItems
) {
2984 if ((item
.getId() >= lower
) && (item
.getId() <= upper
)) {
2985 item
.setEnabled(false);
2986 item
.getParent().activate(0);
2992 * Enable one menu item.
2994 * @param id the menu item ID
2996 public final void enableMenuItem(final int id
) {
2997 for (TMenuItem item
: menuItems
) {
2998 if (item
.getId() == id
) {
2999 item
.setEnabled(true);
3000 item
.getParent().activate(0);
3006 * Enable the range of menu items with ID's between lower and upper,
3009 * @param lower the lowest menu item ID
3010 * @param upper the highest menu item ID
3012 public final void enableMenuItems(final int lower
, final int upper
) {
3013 for (TMenuItem item
: menuItems
) {
3014 if ((item
.getId() >= lower
) && (item
.getId() <= upper
)) {
3015 item
.setEnabled(true);
3016 item
.getParent().activate(0);
3022 * Get the menu item associated with this ID.
3024 * @param id the menu item ID
3025 * @return the menu item, or null if not found
3027 public final TMenuItem
getMenuItem(final int id
) {
3028 for (TMenuItem item
: menuItems
) {
3029 if (item
.getId() == id
) {
3037 * Recompute menu x positions based on their title length.
3039 public final void recomputeMenuX() {
3041 for (TMenu menu
: menus
) {
3044 x
+= StringUtils
.width(menu
.getTitle()) + 2;
3046 // Don't let the menu window exceed the screen width
3047 int rightEdge
= menu
.getX() + menu
.getWidth();
3048 if (rightEdge
> getScreen().getWidth()) {
3049 menu
.setX(getScreen().getWidth() - menu
.getWidth());
3055 * Post an event to process.
3057 * @param event new event to add to the queue
3059 public final void postEvent(final TInputEvent event
) {
3060 synchronized (this) {
3061 synchronized (fillEventQueue
) {
3062 fillEventQueue
.add(event
);
3065 System
.err
.println(System
.currentTimeMillis() + " " +
3066 Thread
.currentThread() + " postEvent() wake up main");
3073 * Post an event to process and turn off the menu.
3075 * @param event new event to add to the queue
3077 public final void postMenuEvent(final TInputEvent event
) {
3078 synchronized (this) {
3079 synchronized (fillEventQueue
) {
3080 fillEventQueue
.add(event
);
3083 System
.err
.println(System
.currentTimeMillis() + " " +
3084 Thread
.currentThread() + " postMenuEvent() wake up main");
3092 * Add a sub-menu to the list of open sub-menus.
3094 * @param menu sub-menu
3096 public final void addSubMenu(final TMenu menu
) {
3101 * Convenience function to add a top-level menu.
3103 * @param title menu title
3104 * @return the new menu
3106 public final TMenu
addMenu(final String title
) {
3109 TMenu menu
= new TMenu(this, x
, y
, title
);
3116 * Convenience function to add a default tools (hamburger) menu.
3118 * @return the new menu
3120 public final TMenu
addToolMenu() {
3121 TMenu toolMenu
= addMenu(i18n
.getString("toolMenuTitle"));
3122 toolMenu
.addDefaultItem(TMenu
.MID_REPAINT
);
3123 toolMenu
.addDefaultItem(TMenu
.MID_VIEW_IMAGE
);
3124 toolMenu
.addDefaultItem(TMenu
.MID_CHANGE_FONT
);
3125 TStatusBar toolStatusBar
= toolMenu
.newStatusBar(i18n
.
3126 getString("toolMenuStatus"));
3127 toolStatusBar
.addShortcutKeypress(kbF1
, cmHelp
, i18n
.getString("Help"));
3132 * Convenience function to add a default "File" menu.
3134 * @return the new menu
3136 public final TMenu
addFileMenu() {
3137 TMenu fileMenu
= addMenu(i18n
.getString("fileMenuTitle"));
3138 fileMenu
.addDefaultItem(TMenu
.MID_SHELL
);
3139 fileMenu
.addSeparator();
3140 fileMenu
.addDefaultItem(TMenu
.MID_EXIT
);
3141 TStatusBar statusBar
= fileMenu
.newStatusBar(i18n
.
3142 getString("fileMenuStatus"));
3143 statusBar
.addShortcutKeypress(kbF1
, cmHelp
, i18n
.getString("Help"));
3148 * Convenience function to add a default "Edit" menu.
3150 * @return the new menu
3152 public final TMenu
addEditMenu() {
3153 TMenu editMenu
= addMenu(i18n
.getString("editMenuTitle"));
3154 editMenu
.addDefaultItem(TMenu
.MID_CUT
);
3155 editMenu
.addDefaultItem(TMenu
.MID_COPY
);
3156 editMenu
.addDefaultItem(TMenu
.MID_PASTE
);
3157 editMenu
.addDefaultItem(TMenu
.MID_CLEAR
);
3158 TStatusBar statusBar
= editMenu
.newStatusBar(i18n
.
3159 getString("editMenuStatus"));
3160 statusBar
.addShortcutKeypress(kbF1
, cmHelp
, i18n
.getString("Help"));
3165 * Convenience function to add a default "Window" menu.
3167 * @return the new menu
3169 public final TMenu
addWindowMenu() {
3170 TMenu windowMenu
= addMenu(i18n
.getString("windowMenuTitle"));
3171 windowMenu
.addDefaultItem(TMenu
.MID_TILE
);
3172 windowMenu
.addDefaultItem(TMenu
.MID_CASCADE
);
3173 windowMenu
.addDefaultItem(TMenu
.MID_CLOSE_ALL
);
3174 windowMenu
.addSeparator();
3175 windowMenu
.addDefaultItem(TMenu
.MID_WINDOW_MOVE
);
3176 windowMenu
.addDefaultItem(TMenu
.MID_WINDOW_ZOOM
);
3177 windowMenu
.addDefaultItem(TMenu
.MID_WINDOW_NEXT
);
3178 windowMenu
.addDefaultItem(TMenu
.MID_WINDOW_PREVIOUS
);
3179 windowMenu
.addDefaultItem(TMenu
.MID_WINDOW_CLOSE
);
3180 TStatusBar statusBar
= windowMenu
.newStatusBar(i18n
.
3181 getString("windowMenuStatus"));
3182 statusBar
.addShortcutKeypress(kbF1
, cmHelp
, i18n
.getString("Help"));
3187 * Convenience function to add a default "Help" menu.
3189 * @return the new menu
3191 public final TMenu
addHelpMenu() {
3192 TMenu helpMenu
= addMenu(i18n
.getString("helpMenuTitle"));
3193 helpMenu
.addDefaultItem(TMenu
.MID_HELP_CONTENTS
);
3194 helpMenu
.addDefaultItem(TMenu
.MID_HELP_INDEX
);
3195 helpMenu
.addDefaultItem(TMenu
.MID_HELP_SEARCH
);
3196 helpMenu
.addDefaultItem(TMenu
.MID_HELP_PREVIOUS
);
3197 helpMenu
.addDefaultItem(TMenu
.MID_HELP_HELP
);
3198 helpMenu
.addDefaultItem(TMenu
.MID_HELP_ACTIVE_FILE
);
3199 helpMenu
.addSeparator();
3200 helpMenu
.addDefaultItem(TMenu
.MID_ABOUT
);
3201 TStatusBar statusBar
= helpMenu
.newStatusBar(i18n
.
3202 getString("helpMenuStatus"));
3203 statusBar
.addShortcutKeypress(kbF1
, cmHelp
, i18n
.getString("Help"));
3208 * Convenience function to add a default "Table" menu.
3210 * @return the new menu
3212 public final TMenu
addTableMenu() {
3213 TMenu tableMenu
= addMenu(i18n
.getString("tableMenuTitle"));
3214 tableMenu
.addDefaultItem(TMenu
.MID_TABLE_RENAME_COLUMN
, false);
3215 tableMenu
.addDefaultItem(TMenu
.MID_TABLE_RENAME_ROW
, false);
3216 tableMenu
.addSeparator();
3218 TSubMenu viewMenu
= tableMenu
.addSubMenu(i18n
.
3219 getString("tableSubMenuView"));
3220 viewMenu
.addDefaultItem(TMenu
.MID_TABLE_VIEW_ROW_LABELS
, false);
3221 viewMenu
.addDefaultItem(TMenu
.MID_TABLE_VIEW_COLUMN_LABELS
, false);
3222 viewMenu
.addDefaultItem(TMenu
.MID_TABLE_VIEW_HIGHLIGHT_ROW
, false);
3223 viewMenu
.addDefaultItem(TMenu
.MID_TABLE_VIEW_HIGHLIGHT_COLUMN
, false);
3225 TSubMenu borderMenu
= tableMenu
.addSubMenu(i18n
.
3226 getString("tableSubMenuBorders"));
3227 borderMenu
.addDefaultItem(TMenu
.MID_TABLE_BORDER_NONE
, false);
3228 borderMenu
.addDefaultItem(TMenu
.MID_TABLE_BORDER_ALL
, false);
3229 borderMenu
.addDefaultItem(TMenu
.MID_TABLE_BORDER_CELL_NONE
, false);
3230 borderMenu
.addDefaultItem(TMenu
.MID_TABLE_BORDER_CELL_ALL
, false);
3231 borderMenu
.addDefaultItem(TMenu
.MID_TABLE_BORDER_RIGHT
, false);
3232 borderMenu
.addDefaultItem(TMenu
.MID_TABLE_BORDER_LEFT
, false);
3233 borderMenu
.addDefaultItem(TMenu
.MID_TABLE_BORDER_TOP
, false);
3234 borderMenu
.addDefaultItem(TMenu
.MID_TABLE_BORDER_BOTTOM
, false);
3235 borderMenu
.addDefaultItem(TMenu
.MID_TABLE_BORDER_DOUBLE_BOTTOM
, false);
3236 borderMenu
.addDefaultItem(TMenu
.MID_TABLE_BORDER_THICK_BOTTOM
, false);
3237 TSubMenu deleteMenu
= tableMenu
.addSubMenu(i18n
.
3238 getString("tableSubMenuDelete"));
3239 deleteMenu
.addDefaultItem(TMenu
.MID_TABLE_DELETE_LEFT
, false);
3240 deleteMenu
.addDefaultItem(TMenu
.MID_TABLE_DELETE_UP
, false);
3241 deleteMenu
.addDefaultItem(TMenu
.MID_TABLE_DELETE_ROW
, false);
3242 deleteMenu
.addDefaultItem(TMenu
.MID_TABLE_DELETE_COLUMN
, false);
3243 TSubMenu insertMenu
= tableMenu
.addSubMenu(i18n
.
3244 getString("tableSubMenuInsert"));
3245 insertMenu
.addDefaultItem(TMenu
.MID_TABLE_INSERT_LEFT
, false);
3246 insertMenu
.addDefaultItem(TMenu
.MID_TABLE_INSERT_RIGHT
, false);
3247 insertMenu
.addDefaultItem(TMenu
.MID_TABLE_INSERT_ABOVE
, false);
3248 insertMenu
.addDefaultItem(TMenu
.MID_TABLE_INSERT_BELOW
, false);
3249 TSubMenu columnMenu
= tableMenu
.addSubMenu(i18n
.
3250 getString("tableSubMenuColumn"));
3251 columnMenu
.addDefaultItem(TMenu
.MID_TABLE_COLUMN_NARROW
, false);
3252 columnMenu
.addDefaultItem(TMenu
.MID_TABLE_COLUMN_WIDEN
, false);
3253 TSubMenu fileMenu
= tableMenu
.addSubMenu(i18n
.
3254 getString("tableSubMenuFile"));
3255 fileMenu
.addDefaultItem(TMenu
.MID_TABLE_FILE_OPEN_CSV
, false);
3256 fileMenu
.addDefaultItem(TMenu
.MID_TABLE_FILE_SAVE_CSV
, false);
3257 fileMenu
.addDefaultItem(TMenu
.MID_TABLE_FILE_SAVE_TEXT
, false);
3259 TStatusBar statusBar
= tableMenu
.newStatusBar(i18n
.
3260 getString("tableMenuStatus"));
3261 statusBar
.addShortcutKeypress(kbF1
, cmHelp
, i18n
.getString("Help"));
3265 // ------------------------------------------------------------------------
3266 // TTimer management ------------------------------------------------------
3267 // ------------------------------------------------------------------------
3270 * Get the amount of time I can sleep before missing a Timer tick.
3272 * @param timeout = initial (maximum) timeout in millis
3273 * @return number of milliseconds between now and the next timer event
3275 private long getSleepTime(final long timeout
) {
3276 Date now
= new Date();
3277 long nowTime
= now
.getTime();
3278 long sleepTime
= timeout
;
3280 synchronized (timers
) {
3281 for (TTimer timer
: timers
) {
3282 long nextTickTime
= timer
.getNextTick().getTime();
3283 if (nextTickTime
< nowTime
) {
3287 long timeDifference
= nextTickTime
- nowTime
;
3288 if (timeDifference
< sleepTime
) {
3289 sleepTime
= timeDifference
;
3294 assert (sleepTime
>= 0);
3295 assert (sleepTime
<= timeout
);
3300 * Convenience function to add a timer.
3302 * @param duration number of milliseconds to wait between ticks
3303 * @param recurring if true, re-schedule this timer after every tick
3304 * @param action function to call when button is pressed
3307 public final TTimer
addTimer(final long duration
, final boolean recurring
,
3308 final TAction action
) {
3310 TTimer timer
= new TTimer(duration
, recurring
, action
);
3311 synchronized (timers
) {
3318 * Convenience function to remove a timer.
3320 * @param timer timer to remove
3322 public final void removeTimer(final TTimer timer
) {
3323 synchronized (timers
) {
3324 timers
.remove(timer
);
3328 // ------------------------------------------------------------------------
3329 // Other TWindow constructors ---------------------------------------------
3330 // ------------------------------------------------------------------------
3333 * Convenience function to spawn a message box.
3335 * @param title window title, will be centered along the top border
3336 * @param caption message to display. Use embedded newlines to get a
3338 * @return the new message box
3340 public final TMessageBox
messageBox(final String title
,
3341 final String caption
) {
3343 return new TMessageBox(this, title
, caption
, TMessageBox
.Type
.OK
);
3347 * Convenience function to spawn a message box.
3349 * @param title window title, will be centered along the top border
3350 * @param caption message to display. Use embedded newlines to get a
3352 * @param type one of the TMessageBox.Type constants. Default is
3354 * @return the new message box
3356 public final TMessageBox
messageBox(final String title
,
3357 final String caption
, final TMessageBox
.Type type
) {
3359 return new TMessageBox(this, title
, caption
, type
);
3363 * Convenience function to spawn an input box.
3365 * @param title window title, will be centered along the top border
3366 * @param caption message to display. Use embedded newlines to get a
3368 * @return the new input box
3370 public final TInputBox
inputBox(final String title
, final String caption
) {
3372 return new TInputBox(this, title
, caption
);
3376 * Convenience function to spawn an input box.
3378 * @param title window title, will be centered along the top border
3379 * @param caption message to display. Use embedded newlines to get a
3381 * @param text initial text to seed the field with
3382 * @return the new input box
3384 public final TInputBox
inputBox(final String title
, final String caption
,
3385 final String text
) {
3387 return new TInputBox(this, title
, caption
, text
);
3391 * Convenience function to spawn an input box.
3393 * @param title window title, will be centered along the top border
3394 * @param caption message to display. Use embedded newlines to get a
3396 * @param text initial text to seed the field with
3397 * @param type one of the Type constants. Default is Type.OK.
3398 * @return the new input box
3400 public final TInputBox
inputBox(final String title
, final String caption
,
3401 final String text
, final TInputBox
.Type type
) {
3403 return new TInputBox(this, title
, caption
, text
, type
);
3407 * Convenience function to open a terminal window.
3409 * @param x column relative to parent
3410 * @param y row relative to parent
3411 * @return the terminal new window
3413 public final TTerminalWindow
openTerminal(final int x
, final int y
) {
3414 return openTerminal(x
, y
, TWindow
.RESIZABLE
);
3418 * Convenience function to open a terminal window.
3420 * @param x column relative to parent
3421 * @param y row relative to parent
3422 * @param closeOnExit if true, close the window when the command exits
3423 * @return the terminal new window
3425 public final TTerminalWindow
openTerminal(final int x
, final int y
,
3426 final boolean closeOnExit
) {
3428 return openTerminal(x
, y
, TWindow
.RESIZABLE
, closeOnExit
);
3432 * Convenience function to open a terminal window.
3434 * @param x column relative to parent
3435 * @param y row relative to parent
3436 * @param flags mask of CENTERED, MODAL, or RESIZABLE
3437 * @return the terminal new window
3439 public final TTerminalWindow
openTerminal(final int x
, final int y
,
3442 return new TTerminalWindow(this, x
, y
, flags
);
3446 * Convenience function to open a terminal window.
3448 * @param x column relative to parent
3449 * @param y row relative to parent
3450 * @param flags mask of CENTERED, MODAL, or RESIZABLE
3451 * @param closeOnExit if true, close the window when the command exits
3452 * @return the terminal new window
3454 public final TTerminalWindow
openTerminal(final int x
, final int y
,
3455 final int flags
, final boolean closeOnExit
) {
3457 return new TTerminalWindow(this, x
, y
, flags
, closeOnExit
);
3461 * Convenience function to open a terminal window and execute a custom
3462 * command line inside it.
3464 * @param x column relative to parent
3465 * @param y row relative to parent
3466 * @param commandLine the command line to execute
3467 * @return the terminal new window
3469 public final TTerminalWindow
openTerminal(final int x
, final int y
,
3470 final String commandLine
) {
3472 return openTerminal(x
, y
, TWindow
.RESIZABLE
, commandLine
);
3476 * Convenience function to open a terminal window and execute a custom
3477 * command line inside it.
3479 * @param x column relative to parent
3480 * @param y row relative to parent
3481 * @param commandLine the command line to execute
3482 * @param closeOnExit if true, close the window when the command exits
3483 * @return the terminal new window
3485 public final TTerminalWindow
openTerminal(final int x
, final int y
,
3486 final String commandLine
, final boolean closeOnExit
) {
3488 return openTerminal(x
, y
, TWindow
.RESIZABLE
, commandLine
, closeOnExit
);
3492 * Convenience function to open a terminal window and execute a custom
3493 * command line inside it.
3495 * @param x column relative to parent
3496 * @param y row relative to parent
3497 * @param flags mask of CENTERED, MODAL, or RESIZABLE
3498 * @param command the command line to execute
3499 * @return the terminal new window
3501 public final TTerminalWindow
openTerminal(final int x
, final int y
,
3502 final int flags
, final String
[] command
) {
3504 return new TTerminalWindow(this, x
, y
, flags
, command
);
3508 * Convenience function to open a terminal window and execute a custom
3509 * command line inside it.
3511 * @param x column relative to parent
3512 * @param y row relative to parent
3513 * @param flags mask of CENTERED, MODAL, or RESIZABLE
3514 * @param command the command line to execute
3515 * @param closeOnExit if true, close the window when the command exits
3516 * @return the terminal new window
3518 public final TTerminalWindow
openTerminal(final int x
, final int y
,
3519 final int flags
, final String
[] command
, final boolean closeOnExit
) {
3521 return new TTerminalWindow(this, x
, y
, flags
, command
, closeOnExit
);
3525 * Convenience function to open a terminal window and execute a custom
3526 * command line inside it.
3528 * @param x column relative to parent
3529 * @param y row relative to parent
3530 * @param flags mask of CENTERED, MODAL, or RESIZABLE
3531 * @param commandLine the command line to execute
3532 * @return the terminal new window
3534 public final TTerminalWindow
openTerminal(final int x
, final int y
,
3535 final int flags
, final String commandLine
) {
3537 return new TTerminalWindow(this, x
, y
, flags
, commandLine
.split("\\s+"));
3541 * Convenience function to open a terminal window and execute a custom
3542 * command line inside it.
3544 * @param x column relative to parent
3545 * @param y row relative to parent
3546 * @param flags mask of CENTERED, MODAL, or RESIZABLE
3547 * @param commandLine the command line to execute
3548 * @param closeOnExit if true, close the window when the command exits
3549 * @return the terminal new window
3551 public final TTerminalWindow
openTerminal(final int x
, final int y
,
3552 final int flags
, final String commandLine
, final boolean closeOnExit
) {
3554 return new TTerminalWindow(this, x
, y
, flags
, commandLine
.split("\\s+"),
3559 * Convenience function to spawn an file open box.
3561 * @param path path of selected file
3562 * @return the result of the new file open box
3563 * @throws IOException if java.io operation throws
3565 public final String
fileOpenBox(final String path
) throws IOException
{
3567 TFileOpenBox box
= new TFileOpenBox(this, path
, TFileOpenBox
.Type
.OPEN
);
3568 return box
.getFilename();
3572 * Convenience function to spawn an file open box.
3574 * @param path path of selected file
3575 * @param type one of the Type constants
3576 * @return the result of the new file open box
3577 * @throws IOException if java.io operation throws
3579 public final String
fileOpenBox(final String path
,
3580 final TFileOpenBox
.Type type
) throws IOException
{
3582 TFileOpenBox box
= new TFileOpenBox(this, path
, type
);
3583 return box
.getFilename();
3587 * Convenience function to spawn a file open box.
3589 * @param path path of selected file
3590 * @param type one of the Type constants
3591 * @param filter a string that files must match to be displayed
3592 * @return the result of the new file open box
3593 * @throws IOException of a java.io operation throws
3595 public final String
fileOpenBox(final String path
,
3596 final TFileOpenBox
.Type type
, final String filter
) throws IOException
{
3598 ArrayList
<String
> filters
= new ArrayList
<String
>();
3599 filters
.add(filter
);
3601 TFileOpenBox box
= new TFileOpenBox(this, path
, type
, filters
);
3602 return box
.getFilename();
3606 * Convenience function to spawn a file open box.
3608 * @param path path of selected file
3609 * @param type one of the Type constants
3610 * @param filters a list of strings that files must match to be displayed
3611 * @return the result of the new file open box
3612 * @throws IOException of a java.io operation throws
3614 public final String
fileOpenBox(final String path
,
3615 final TFileOpenBox
.Type type
,
3616 final List
<String
> filters
) throws IOException
{
3618 TFileOpenBox box
= new TFileOpenBox(this, path
, type
, filters
);
3619 return box
.getFilename();
3623 * Convenience function to create a new window and make it active.
3624 * Window will be located at (0, 0).
3626 * @param title window title, will be centered along the top border
3627 * @param width width of window
3628 * @param height height of window
3629 * @return the new window
3631 public final TWindow
addWindow(final String title
, final int width
,
3634 TWindow window
= new TWindow(this, title
, 0, 0, width
, height
);
3639 * Convenience function to create a new window and make it active.
3640 * Window will be located at (0, 0).
3642 * @param title window title, will be centered along the top border
3643 * @param width width of window
3644 * @param height height of window
3645 * @param flags bitmask of RESIZABLE, CENTERED, or MODAL
3646 * @return the new window
3648 public final TWindow
addWindow(final String title
,
3649 final int width
, final int height
, final int flags
) {
3651 TWindow window
= new TWindow(this, title
, 0, 0, width
, height
, flags
);
3656 * Convenience function to create a new window and make it active.
3658 * @param title window title, will be centered along the top border
3659 * @param x column relative to parent
3660 * @param y row relative to parent
3661 * @param width width of window
3662 * @param height height of window
3663 * @return the new window
3665 public final TWindow
addWindow(final String title
,
3666 final int x
, final int y
, final int width
, final int height
) {
3668 TWindow window
= new TWindow(this, title
, x
, y
, width
, height
);
3673 * Convenience function to create a new window and make it active.
3675 * @param title window title, will be centered along the top border
3676 * @param x column relative to parent
3677 * @param y row relative to parent
3678 * @param width width of window
3679 * @param height height of window
3680 * @param flags mask of RESIZABLE, CENTERED, or MODAL
3681 * @return the new window
3683 public final TWindow
addWindow(final String title
,
3684 final int x
, final int y
, final int width
, final int height
,
3687 TWindow window
= new TWindow(this, title
, x
, y
, width
, height
, flags
);