2 * Jexer - Java Text User Interface
4 * License: LGPLv3 or later
6 * This module is licensed under the GNU Lesser General Public License
7 * Version 3. Please see the file "COPYING" in this directory for more
8 * information about the GNU Lesser General Public License Version 3.
10 * Copyright (C) 2015 Kevin Lamonte
12 * This program is free software; you can redistribute it and/or
13 * modify it under the terms of the GNU Lesser General Public License
14 * as published by the Free Software Foundation; either version 3 of
15 * the License, or (at your option) any later version.
17 * This program is distributed in the hope that it will be useful, but
18 * WITHOUT ANY WARRANTY; without even the implied warranty of
19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
20 * General Public License for more details.
22 * You should have received a copy of the GNU Lesser General Public
23 * License along with this program; if not, see
24 * http://www.gnu.org/licenses/, or write to the Free Software
25 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
28 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
33 import java
.io
.InputStream
;
34 import java
.io
.OutputStream
;
35 import java
.io
.UnsupportedEncodingException
;
36 import java
.util
.Collections
;
37 import java
.util
.Date
;
38 import java
.util
.HashMap
;
39 import java
.util
.ArrayList
;
40 import java
.util
.LinkedList
;
41 import java
.util
.List
;
44 import jexer
.bits
.CellAttributes
;
45 import jexer
.bits
.ColorTheme
;
46 import jexer
.bits
.GraphicsChars
;
47 import jexer
.event
.TCommandEvent
;
48 import jexer
.event
.TInputEvent
;
49 import jexer
.event
.TKeypressEvent
;
50 import jexer
.event
.TMenuEvent
;
51 import jexer
.event
.TMouseEvent
;
52 import jexer
.event
.TResizeEvent
;
53 import jexer
.backend
.Backend
;
54 import jexer
.backend
.AWTBackend
;
55 import jexer
.backend
.ECMA48Backend
;
56 import jexer
.io
.Screen
;
57 import jexer
.menu
.TMenu
;
58 import jexer
.menu
.TMenuItem
;
59 import static jexer
.TCommand
.*;
60 import static jexer
.TKeypress
.*;
63 * TApplication sets up a full Text User Interface application.
65 public class TApplication
{
68 * WidgetEventHandler is the main event consumer loop. There are at most
69 * two such threads in existence: the primary for normal case and a
70 * secondary that is used for TMessageBox, TInputBox, and similar.
72 private class WidgetEventHandler
implements Runnable
{
74 * The main application.
76 private TApplication application
;
79 * Whether or not this WidgetEventHandler is the primary or secondary
82 private boolean primary
= true;
87 * @param application the main application
88 * @param primary if true, this is the primary event handler thread
90 public WidgetEventHandler(final TApplication application
,
91 final boolean primary
) {
93 this.application
= application
;
94 this.primary
= primary
;
103 while (!application
.quit
) {
105 // Wait until application notifies me
106 while (!application
.quit
) {
108 synchronized (application
.drainEventQueue
) {
109 if (application
.drainEventQueue
.size() > 0) {
113 synchronized (application
) {
116 && (application
.secondaryEventReceiver
== null)
118 // Secondary thread, time to exit
123 } catch (InterruptedException e
) {
128 // Pull all events off the queue
130 TInputEvent event
= null;
131 synchronized (application
.drainEventQueue
) {
132 if (application
.drainEventQueue
.size() == 0) {
135 event
= application
.drainEventQueue
.remove(0);
138 primaryHandleEvent(event
);
140 secondaryHandleEvent(event
);
143 && (application
.secondaryEventReceiver
== null)
145 // Secondary thread, time to exit
149 } // while (true) (main runnable loop)
154 * The primary event handler thread.
156 private WidgetEventHandler primaryEventHandler
;
159 * The secondary event handler thread.
161 private WidgetEventHandler secondaryEventHandler
;
164 * The widget receiving events from the secondary event handler thread.
166 private TWidget secondaryEventReceiver
;
169 * Access to the physical screen, keyboard, and mouse.
171 private Backend backend
;
178 public final Screen
getScreen() {
179 return backend
.getScreen();
183 * Actual mouse coordinate X.
188 * Actual mouse coordinate Y.
193 * Event queue that is filled by run().
195 private List
<TInputEvent
> fillEventQueue
;
198 * Event queue that will be drained by either primary or secondary
201 private List
<TInputEvent
> drainEventQueue
;
204 * Top-level menus in this application.
206 private List
<TMenu
> menus
;
209 * Stack of activated sub-menus in this application.
211 private List
<TMenu
> subMenus
;
214 * The currently acive menu.
216 private TMenu activeMenu
= null;
219 * Active keyboard accelerators.
221 private Map
<TKeypress
, TMenuItem
> accelerators
;
224 * Windows and widgets pull colors from this ColorTheme.
226 private ColorTheme theme
;
229 * Get the color theme.
233 public final ColorTheme
getTheme() {
238 * The top-level windows (but not menus).
240 private List
<TWindow
> windows
;
243 * Timers that are being ticked.
245 private List
<TTimer
> timers
;
248 * When true, exit the application.
250 private boolean quit
= false;
253 * When true, repaint the entire screen.
255 private boolean repaint
= true;
258 * Request full repaint on next screen refresh.
260 public final void setRepaint() {
265 * When true, just flush updates from the screen.
267 private boolean flush
= false;
270 * Y coordinate of the top edge of the desktop. For now this is a
271 * constant. Someday it would be nice to have a multi-line menu or
274 private static final int desktopTop
= 1;
277 * Get Y coordinate of the top edge of the desktop.
279 * @return Y coordinate of the top edge of the desktop
281 public final int getDesktopTop() {
286 * Y coordinate of the bottom edge of the desktop.
288 private int desktopBottom
;
291 * Get Y coordinate of the bottom edge of the desktop.
293 * @return Y coordinate of the bottom edge of the desktop
295 public final int getDesktopBottom() {
296 return desktopBottom
;
300 * Public constructor.
302 * @param input an InputStream connected to the remote user, or null for
303 * System.in. If System.in is used, then on non-Windows systems it will
304 * be put in raw mode; shutdown() will (blindly!) put System.in in cooked
305 * mode. input is always converted to a Reader with UTF-8 encoding.
306 * @param output an OutputStream connected to the remote user, or null
307 * for System.out. output is always converted to a Writer with UTF-8
309 * @throws UnsupportedEncodingException if an exception is thrown when
310 * creating the InputStreamReader
312 public TApplication(final InputStream input
,
313 final OutputStream output
) throws UnsupportedEncodingException
{
315 // AWT is the default backend on Windows unless explicitly overridden
317 boolean useAWT
= false;
318 if (System
.getProperty("os.name").startsWith("Windows")) {
321 if (System
.getProperty("jexer.AWT") != null) {
322 if (System
.getProperty("jexer.AWT", "false").equals("true")) {
331 backend
= new AWTBackend();
333 backend
= new ECMA48Backend(input
, output
);
335 theme
= new ColorTheme();
336 desktopBottom
= getScreen().getHeight() - 1;
337 fillEventQueue
= new ArrayList
<TInputEvent
>();
338 drainEventQueue
= new ArrayList
<TInputEvent
>();
339 windows
= new LinkedList
<TWindow
>();
340 menus
= new LinkedList
<TMenu
>();
341 subMenus
= new LinkedList
<TMenu
>();
342 timers
= new LinkedList
<TTimer
>();
343 accelerators
= new HashMap
<TKeypress
, TMenuItem
>();
345 // Setup the main consumer thread
346 primaryEventHandler
= new WidgetEventHandler(this, true);
347 (new Thread(primaryEventHandler
)).start();
351 * Invert the cell at the mouse pointer position.
353 private void drawMouse() {
354 CellAttributes attr
= getScreen().getAttrXY(mouseX
, mouseY
);
355 attr
.setForeColor(attr
.getForeColor().invert());
356 attr
.setBackColor(attr
.getBackColor().invert());
357 getScreen().putAttrXY(mouseX
, mouseY
, attr
, false);
360 if (windows
.size() == 0) {
368 public final void drawAll() {
369 if ((flush
) && (!repaint
)) {
370 backend
.flushScreen();
379 // If true, the cursor is not visible
380 boolean cursor
= false;
382 // Start with a clean screen
385 // Draw the background
386 CellAttributes background
= theme
.getColor("tapplication.background");
387 getScreen().putAll(GraphicsChars
.HATCH
, background
);
389 // Draw each window in reverse Z order
390 List
<TWindow
> sorted
= new LinkedList
<TWindow
>(windows
);
391 Collections
.sort(sorted
);
392 Collections
.reverse(sorted
);
393 for (TWindow window
: sorted
) {
394 window
.drawChildren();
397 // Draw the blank menubar line - reset the screen clipping first so
398 // it won't trim it out.
399 getScreen().resetClipping();
400 getScreen().hLineXY(0, 0, getScreen().getWidth(), ' ',
401 theme
.getColor("tmenu"));
402 // Now draw the menus.
404 for (TMenu menu
: menus
) {
405 CellAttributes menuColor
;
406 CellAttributes menuMnemonicColor
;
407 if (menu
.getActive()) {
408 menuColor
= theme
.getColor("tmenu.highlighted");
409 menuMnemonicColor
= theme
.getColor("tmenu.mnemonic.highlighted");
411 menuColor
= theme
.getColor("tmenu");
412 menuMnemonicColor
= theme
.getColor("tmenu.mnemonic");
414 // Draw the menu title
415 getScreen().hLineXY(x
, 0, menu
.getTitle().length() + 2, ' ',
417 getScreen().putStrXY(x
+ 1, 0, menu
.getTitle(), menuColor
);
418 // Draw the highlight character
419 getScreen().putCharXY(x
+ 1 + menu
.getMnemonic().getShortcutIdx(),
420 0, menu
.getMnemonic().getShortcut(), menuMnemonicColor
);
422 if (menu
.getActive()) {
424 // Reset the screen clipping so we can draw the next title.
425 getScreen().resetClipping();
427 x
+= menu
.getTitle().length() + 2;
430 for (TMenu menu
: subMenus
) {
431 // Reset the screen clipping so we can draw the next sub-menu.
432 getScreen().resetClipping();
436 // Draw the mouse pointer
439 // Place the cursor if it is visible
440 TWidget activeWidget
= null;
441 if (sorted
.size() > 0) {
442 activeWidget
= sorted
.get(sorted
.size() - 1).getActiveChild();
443 if (activeWidget
.visibleCursor()) {
444 getScreen().putCursor(true, activeWidget
.getCursorAbsoluteX(),
445 activeWidget
.getCursorAbsoluteY());
452 getScreen().hideCursor();
455 // Flush the screen contents
456 backend
.flushScreen();
463 * Run this application until it exits.
465 public final void run() {
467 // Timeout is in milliseconds, so default timeout after 1 second
469 int timeout
= getSleepTime(1000);
471 // See if there are any definitely events waiting to be processed
472 // or a screen redraw to do. If so, do not wait if there is no
474 synchronized (drainEventQueue
) {
475 if (drainEventQueue
.size() > 0) {
479 synchronized (fillEventQueue
) {
480 if (fillEventQueue
.size() > 0) {
485 // Pull any pending I/O events
486 backend
.getEvents(fillEventQueue
, timeout
);
488 // Dispatch each event to the appropriate handler, one at a time.
490 TInputEvent event
= null;
491 synchronized (fillEventQueue
) {
492 if (fillEventQueue
.size() == 0) {
495 event
= fillEventQueue
.remove(0);
497 metaHandleEvent(event
);
500 // Process timers and call doIdle()'s
507 // Shutdown the consumer threads
508 synchronized (this) {
516 * Peek at certain application-level events, add to eventQueue, and wake
517 * up the consuming Thread.
519 * @param event the input event to consume
521 private void metaHandleEvent(final TInputEvent event
) {
524 System.err.printf(String.format("metaHandleEvents event: %s\n",
525 event)); System.err.flush();
529 // Do no more processing if the application is already trying
534 // Special application-wide events -------------------------------
537 if (event
instanceof TCommandEvent
) {
538 TCommandEvent command
= (TCommandEvent
) event
;
539 if (command
.getCmd().equals(cmAbort
)) {
546 if (event
instanceof TResizeEvent
) {
547 TResizeEvent resize
= (TResizeEvent
) event
;
548 getScreen().setDimensions(resize
.getWidth(),
550 desktopBottom
= getScreen().getHeight() - 1;
557 // Peek at the mouse position
558 if (event
instanceof TMouseEvent
) {
559 TMouseEvent mouse
= (TMouseEvent
) event
;
560 if ((mouseX
!= mouse
.getX()) || (mouseY
!= mouse
.getY())) {
561 mouseX
= mouse
.getX();
562 mouseY
= mouse
.getY();
567 // Put into the main queue
568 synchronized (drainEventQueue
) {
569 drainEventQueue
.add(event
);
572 // Wake all threads: primary thread will either be consuming events
573 // again or waiting in yield(), and secondary thread will either not
574 // exist or consuming events.
575 synchronized (this) {
581 * Dispatch one event to the appropriate widget or application-level
582 * event handler. This is the primary event handler, it has the normal
583 * application-wide event handling.
585 * @param event the input event to consume
586 * @see #secondaryHandleEvent(TInputEvent event)
588 private void primaryHandleEvent(final TInputEvent event
) {
590 // System.err.printf("Handle event: %s\n", event);
592 // Special application-wide events -----------------------------------
594 // Peek at the mouse position
595 if (event
instanceof TMouseEvent
) {
596 // See if we need to switch focus to another window or the menu
597 checkSwitchFocus((TMouseEvent
) event
);
600 // Handle menu events
601 if ((activeMenu
!= null) && !(event
instanceof TCommandEvent
)) {
602 TMenu menu
= activeMenu
;
604 if (event
instanceof TMouseEvent
) {
605 TMouseEvent mouse
= (TMouseEvent
) event
;
607 while (subMenus
.size() > 0) {
608 TMenu subMenu
= subMenus
.get(subMenus
.size() - 1);
609 if (subMenu
.mouseWouldHit(mouse
)) {
612 if ((mouse
.getType() == TMouseEvent
.Type
.MOUSE_MOTION
)
613 && (!mouse
.getMouse1())
614 && (!mouse
.getMouse2())
615 && (!mouse
.getMouse3())
616 && (!mouse
.getMouseWheelUp())
617 && (!mouse
.getMouseWheelDown())
621 // We navigated away from a sub-menu, so close it
625 // Convert the mouse relative x/y to menu coordinates
626 assert (mouse
.getX() == mouse
.getAbsoluteX());
627 assert (mouse
.getY() == mouse
.getAbsoluteY());
628 if (subMenus
.size() > 0) {
629 menu
= subMenus
.get(subMenus
.size() - 1);
631 mouse
.setX(mouse
.getX() - menu
.getX());
632 mouse
.setY(mouse
.getY() - menu
.getY());
634 menu
.handleEvent(event
);
638 if (event
instanceof TKeypressEvent
) {
639 TKeypressEvent keypress
= (TKeypressEvent
) event
;
641 // See if this key matches an accelerator, and if so dispatch the
643 TKeypress keypressLowercase
= keypress
.getKey().toLowerCase();
644 TMenuItem item
= null;
645 synchronized (accelerators
) {
646 item
= accelerators
.get(keypressLowercase
);
649 // Let the menu item dispatch
653 // Handle the keypress
654 if (onKeypress(keypress
)) {
660 if (event
instanceof TCommandEvent
) {
661 if (onCommand((TCommandEvent
) event
)) {
666 if (event
instanceof TMenuEvent
) {
667 if (onMenu((TMenuEvent
) event
)) {
672 // Dispatch events to the active window -------------------------------
673 for (TWindow window
: windows
) {
674 if (window
.getActive()) {
675 if (event
instanceof TMouseEvent
) {
676 TMouseEvent mouse
= (TMouseEvent
) event
;
677 // Convert the mouse relative x/y to window coordinates
678 assert (mouse
.getX() == mouse
.getAbsoluteX());
679 assert (mouse
.getY() == mouse
.getAbsoluteY());
680 mouse
.setX(mouse
.getX() - window
.getX());
681 mouse
.setY(mouse
.getY() - window
.getY());
683 // System.err("TApplication dispatch event: %s\n", event);
684 window
.handleEvent(event
);
690 * Dispatch one event to the appropriate widget or application-level
691 * event handler. This is the secondary event handler used by certain
692 * special dialogs (currently TMessageBox and TFileOpenBox).
694 * @param event the input event to consume
695 * @see #primaryHandleEvent(TInputEvent event)
697 private void secondaryHandleEvent(final TInputEvent event
) {
698 secondaryEventReceiver
.handleEvent(event
);
702 * Enable a widget to override the primary event thread.
704 * @param widget widget that will receive events
706 public final void enableSecondaryEventReceiver(final TWidget widget
) {
707 assert (secondaryEventReceiver
== null);
708 assert (secondaryEventHandler
== null);
709 assert (widget
instanceof TMessageBox
);
710 secondaryEventReceiver
= widget
;
711 secondaryEventHandler
= new WidgetEventHandler(this, false);
712 (new Thread(secondaryEventHandler
)).start();
719 * Yield to the secondary thread.
721 public final void yield() {
722 assert (secondaryEventReceiver
!= null);
723 while (secondaryEventReceiver
!= null) {
724 synchronized (this) {
727 } catch (InterruptedException e
) {
735 * Do stuff when there is no user input.
737 private void doIdle() {
738 // Now run any timers that have timed out
739 Date now
= new Date();
740 List
<TTimer
> keepTimers
= new LinkedList
<TTimer
>();
741 for (TTimer timer
: timers
) {
742 if (timer
.getNextTick().getTime() < now
.getTime()) {
744 if (timer
.recurring
) {
745 keepTimers
.add(timer
);
748 keepTimers
.add(timer
);
754 for (TWindow window
: windows
) {
760 * Get the amount of time I can sleep before missing a Timer tick.
762 * @param timeout = initial (maximum) timeout
763 * @return number of milliseconds between now and the next timer event
765 protected int getSleepTime(final int timeout
) {
766 Date now
= new Date();
767 long sleepTime
= timeout
;
768 for (TTimer timer
: timers
) {
769 if (timer
.getNextTick().getTime() < now
.getTime()) {
772 if ((timer
.getNextTick().getTime() > now
.getTime())
773 && ((timer
.getNextTick().getTime() - now
.getTime()) < sleepTime
)
775 sleepTime
= timer
.getNextTick().getTime() - now
.getTime();
778 assert (sleepTime
>= 0);
779 return (int)sleepTime
;
783 * Close window. Note that the window's destructor is NOT called by this
784 * method, instead the GC is assumed to do the cleanup.
786 * @param window the window to remove
788 public final void closeWindow(final TWindow window
) {
789 int z
= window
.getZ();
791 Collections
.sort(windows
);
793 TWindow activeWindow
= null;
794 for (TWindow w
: windows
) {
796 w
.setZ(w
.getZ() - 1);
799 assert (activeWindow
== null);
807 // Perform window cleanup
813 // Check if we are closing a TMessageBox or similar
814 if (secondaryEventReceiver
!= null) {
815 assert (secondaryEventHandler
!= null);
817 // Do not send events to the secondaryEventReceiver anymore, the
819 secondaryEventReceiver
= null;
821 // Wake all threads: primary thread will be consuming events
822 // again, and secondary thread will exit.
823 synchronized (this) {
830 * Switch to the next window.
832 * @param forward if true, then switch to the next window in the list,
833 * otherwise switch to the previous window in the list
835 public final void switchWindow(final boolean forward
) {
836 // Only switch if there are multiple windows
837 if (windows
.size() < 2) {
841 // Swap z/active between active window and the next in the list
842 int activeWindowI
= -1;
843 for (int i
= 0; i
< windows
.size(); i
++) {
844 if (windows
.get(i
).getActive()) {
849 assert (activeWindowI
>= 0);
851 // Do not switch if a window is modal
852 if (windows
.get(activeWindowI
).isModal()) {
858 nextWindowI
= (activeWindowI
+ 1) % windows
.size();
860 if (activeWindowI
== 0) {
861 nextWindowI
= windows
.size() - 1;
863 nextWindowI
= activeWindowI
- 1;
866 windows
.get(activeWindowI
).setActive(false);
867 windows
.get(activeWindowI
).setZ(windows
.get(nextWindowI
).getZ());
868 windows
.get(nextWindowI
).setZ(0);
869 windows
.get(nextWindowI
).setActive(true);
876 * Add a window to my window list and make it active.
878 * @param window new window to add
880 public final void addWindow(final TWindow window
) {
881 // Do not allow a modal window to spawn a non-modal window
882 if ((windows
.size() > 0) && (windows
.get(0).isModal())) {
883 assert (window
.isModal());
885 for (TWindow w
: windows
) {
887 w
.setZ(w
.getZ() + 1);
890 window
.setActive(true);
895 * Check if there is a system-modal window on top.
897 * @return true if the active window is modal
899 private boolean modalWindowActive() {
900 if (windows
.size() == 0) {
903 return windows
.get(windows
.size() - 1).isModal();
907 * Check if a mouse event would hit either the active menu or any open
910 * @param mouse mouse event
911 * @return true if the mouse would hit the active menu or an open
914 private boolean mouseOnMenu(final TMouseEvent mouse
) {
915 assert (activeMenu
!= null);
916 List
<TMenu
> menus
= new LinkedList
<TMenu
>(subMenus
);
917 Collections
.reverse(menus
);
918 for (TMenu menu
: menus
) {
919 if (menu
.mouseWouldHit(mouse
)) {
923 return activeMenu
.mouseWouldHit(mouse
);
927 * See if we need to switch window or activate the menu based on
930 * @param mouse mouse event
932 private void checkSwitchFocus(final TMouseEvent mouse
) {
934 if ((mouse
.getType() == TMouseEvent
.Type
.MOUSE_DOWN
)
935 && (activeMenu
!= null)
936 && (mouse
.getAbsoluteY() != 0)
937 && (!mouseOnMenu(mouse
))
939 // They clicked outside the active menu, turn it off
940 activeMenu
.setActive(false);
942 for (TMenu menu
: subMenus
) {
943 menu
.setActive(false);
949 // See if they hit the menu bar
950 if ((mouse
.getType() == TMouseEvent
.Type
.MOUSE_DOWN
)
951 && (mouse
.getMouse1())
952 && (!modalWindowActive())
953 && (mouse
.getAbsoluteY() == 0)
956 for (TMenu menu
: subMenus
) {
957 menu
.setActive(false);
961 // They selected the menu, go activate it
962 for (TMenu menu
: menus
) {
963 if ((mouse
.getAbsoluteX() >= menu
.getX())
964 && (mouse
.getAbsoluteX() < menu
.getX()
965 + menu
.getTitle().length() + 2)
967 menu
.setActive(true);
970 menu
.setActive(false);
977 // See if they hit the menu bar
978 if ((mouse
.getType() == TMouseEvent
.Type
.MOUSE_MOTION
)
979 && (mouse
.getMouse1())
980 && (activeMenu
!= null)
981 && (mouse
.getAbsoluteY() == 0)
984 TMenu oldMenu
= activeMenu
;
985 for (TMenu menu
: subMenus
) {
986 menu
.setActive(false);
990 // See if we should switch menus
991 for (TMenu menu
: menus
) {
992 if ((mouse
.getAbsoluteX() >= menu
.getX())
993 && (mouse
.getAbsoluteX() < menu
.getX()
994 + menu
.getTitle().length() + 2)
996 menu
.setActive(true);
1000 if (oldMenu
!= activeMenu
) {
1001 // They switched menus
1002 oldMenu
.setActive(false);
1008 // Only switch if there are multiple windows
1009 if (windows
.size() < 2) {
1013 // Switch on the upclick
1014 if (mouse
.getType() != TMouseEvent
.Type
.MOUSE_UP
) {
1018 Collections
.sort(windows
);
1019 if (windows
.get(0).isModal()) {
1020 // Modal windows don't switch
1024 for (TWindow window
: windows
) {
1025 assert (!window
.isModal());
1026 if (window
.mouseWouldHit(mouse
)) {
1027 if (window
== windows
.get(0)) {
1028 // Clicked on the same window, nothing to do
1032 // We will be switching to another window
1033 assert (windows
.get(0).getActive());
1034 assert (!window
.getActive());
1035 windows
.get(0).setActive(false);
1036 windows
.get(0).setZ(window
.getZ());
1038 window
.setActive(true);
1044 // Clicked on the background, nothing to do
1049 * Turn off the menu.
1051 public final void closeMenu() {
1052 if (activeMenu
!= null) {
1053 activeMenu
.setActive(false);
1055 for (TMenu menu
: subMenus
) {
1056 menu
.setActive(false);
1064 * Turn off a sub-menu.
1066 public final void closeSubMenu() {
1067 assert (activeMenu
!= null);
1068 TMenu item
= subMenus
.get(subMenus
.size() - 1);
1069 assert (item
!= null);
1070 item
.setActive(false);
1071 subMenus
.remove(subMenus
.size() - 1);
1076 * Switch to the next menu.
1078 * @param forward if true, then switch to the next menu in the list,
1079 * otherwise switch to the previous menu in the list
1081 public final void switchMenu(final boolean forward
) {
1082 assert (activeMenu
!= null);
1084 for (TMenu menu
: subMenus
) {
1085 menu
.setActive(false);
1089 for (int i
= 0; i
< menus
.size(); i
++) {
1090 if (activeMenu
== menus
.get(i
)) {
1092 if (i
< menus
.size() - 1) {
1100 activeMenu
.setActive(false);
1101 activeMenu
= menus
.get(i
);
1102 activeMenu
.setActive(true);
1110 * Method that TApplication subclasses can override to handle menu or
1111 * posted command events.
1113 * @param command command event
1114 * @return if true, this event was consumed
1116 protected boolean onCommand(final TCommandEvent command
) {
1117 // Default: handle cmExit
1118 if (command
.equals(cmExit
)) {
1119 if (messageBox("Confirmation", "Exit application?",
1120 TMessageBox
.Type
.YESNO
).getResult() == TMessageBox
.Result
.YES
) {
1129 if (command.equals(cmShell)) {
1130 openTerminal(0, 0, TWindow.Flag.RESIZABLE);
1136 if (command
.equals(cmTile
)) {
1141 if (command
.equals(cmCascade
)) {
1146 if (command
.equals(cmCloseAll
)) {
1156 * Method that TApplication subclasses can override to handle menu
1159 * @param menu menu event
1160 * @return if true, this event was consumed
1162 protected boolean onMenu(final TMenuEvent menu
) {
1164 // Default: handle MID_EXIT
1165 if (menu
.getId() == TMenu
.MID_EXIT
) {
1166 if (messageBox("Confirmation", "Exit application?",
1167 TMessageBox
.Type
.YESNO
).getResult() == TMessageBox
.Result
.YES
) {
1170 // System.err.printf("onMenu MID_EXIT result: quit = %s\n", quit);
1177 if (menu.id == TMenu.MID_SHELL) {
1178 openTerminal(0, 0, TWindow.Flag.RESIZABLE);
1184 if (menu
.getId() == TMenu
.MID_TILE
) {
1189 if (menu
.getId() == TMenu
.MID_CASCADE
) {
1194 if (menu
.getId() == TMenu
.MID_CLOSE_ALL
) {
1203 * Method that TApplication subclasses can override to handle keystrokes.
1205 * @param keypress keystroke event
1206 * @return if true, this event was consumed
1208 protected boolean onKeypress(final TKeypressEvent keypress
) {
1209 // Default: only menu shortcuts
1211 // Process Alt-F, Alt-E, etc. menu shortcut keys
1212 if (!keypress
.getKey().getIsKey()
1213 && keypress
.getKey().getAlt()
1214 && !keypress
.getKey().getCtrl()
1215 && (activeMenu
== null)
1218 assert (subMenus
.size() == 0);
1220 for (TMenu menu
: menus
) {
1221 if (Character
.toLowerCase(menu
.getMnemonic().getShortcut())
1222 == Character
.toLowerCase(keypress
.getKey().getCh())
1225 menu
.setActive(true);
1236 * Add a keyboard accelerator to the global hash.
1238 * @param item menu item this accelerator relates to
1239 * @param keypress keypress that will dispatch a TMenuEvent
1241 public final void addAccelerator(final TMenuItem item
,
1242 final TKeypress keypress
) {
1244 // System.err.printf("addAccelerator: key %s item %s\n", keypress, item);
1246 synchronized (accelerators
) {
1247 assert (accelerators
.get(keypress
) == null);
1248 accelerators
.put(keypress
, item
);
1253 * Recompute menu x positions based on their title length.
1255 public final void recomputeMenuX() {
1257 for (TMenu menu
: menus
) {
1259 x
+= menu
.getTitle().length() + 2;
1264 * Post an event to process and turn off the menu.
1266 * @param event new event to add to the queue
1268 public final void addMenuEvent(final TInputEvent event
) {
1269 synchronized (fillEventQueue
) {
1270 fillEventQueue
.add(event
);
1276 * Add a sub-menu to the list of open sub-menus.
1278 * @param menu sub-menu
1280 public final void addSubMenu(final TMenu menu
) {
1285 * Convenience function to add a top-level menu.
1287 * @param title menu title
1288 * @return the new menu
1290 public final TMenu
addMenu(String title
) {
1293 TMenu menu
= new TMenu(this, x
, y
, title
);
1300 * Convenience function to add a default "File" menu.
1302 * @return the new menu
1304 public final TMenu
addFileMenu() {
1305 TMenu fileMenu
= addMenu("&File");
1306 fileMenu
.addDefaultItem(TMenu
.MID_OPEN_FILE
);
1307 fileMenu
.addSeparator();
1308 fileMenu
.addDefaultItem(TMenu
.MID_SHELL
);
1309 fileMenu
.addDefaultItem(TMenu
.MID_EXIT
);
1314 * Convenience function to add a default "Edit" menu.
1316 * @return the new menu
1318 public final TMenu
addEditMenu() {
1319 TMenu editMenu
= addMenu("&Edit");
1320 editMenu
.addDefaultItem(TMenu
.MID_CUT
);
1321 editMenu
.addDefaultItem(TMenu
.MID_COPY
);
1322 editMenu
.addDefaultItem(TMenu
.MID_PASTE
);
1323 editMenu
.addDefaultItem(TMenu
.MID_CLEAR
);
1328 * Convenience function to add a default "Window" menu.
1330 * @return the new menu
1332 public final TMenu
addWindowMenu() {
1333 TMenu windowMenu
= addMenu("&Window");
1334 windowMenu
.addDefaultItem(TMenu
.MID_TILE
);
1335 windowMenu
.addDefaultItem(TMenu
.MID_CASCADE
);
1336 windowMenu
.addDefaultItem(TMenu
.MID_CLOSE_ALL
);
1337 windowMenu
.addSeparator();
1338 windowMenu
.addDefaultItem(TMenu
.MID_WINDOW_MOVE
);
1339 windowMenu
.addDefaultItem(TMenu
.MID_WINDOW_ZOOM
);
1340 windowMenu
.addDefaultItem(TMenu
.MID_WINDOW_NEXT
);
1341 windowMenu
.addDefaultItem(TMenu
.MID_WINDOW_PREVIOUS
);
1342 windowMenu
.addDefaultItem(TMenu
.MID_WINDOW_CLOSE
);
1347 * Close all open windows.
1349 private void closeAllWindows() {
1350 // Don't do anything if we are in the menu
1351 if (activeMenu
!= null) {
1354 for (TWindow window
: windows
) {
1355 closeWindow(window
);
1360 * Re-layout the open windows as non-overlapping tiles. This produces
1361 * almost the same results as Turbo Pascal 7.0's IDE.
1363 private void tileWindows() {
1364 // Don't do anything if we are in the menu
1365 if (activeMenu
!= null) {
1368 int z
= windows
.size();
1374 a
= (int)(Math
.sqrt(z
));
1378 if (((a
* b
) + c
) == z
) {
1386 int newWidth
= (getScreen().getWidth() / a
);
1387 int newHeight1
= ((getScreen().getHeight() - 1) / b
);
1388 int newHeight2
= ((getScreen().getHeight() - 1) / (b
+ c
));
1389 // System.err.printf("Z %s a %s b %s c %s newWidth %s newHeight1 %s newHeight2 %s",
1390 // z, a, b, c, newWidth, newHeight1, newHeight2);
1392 List
<TWindow
> sorted
= new LinkedList
<TWindow
>(windows
);
1393 Collections
.sort(sorted
);
1394 Collections
.reverse(sorted
);
1395 for (int i
= 0; i
< sorted
.size(); i
++) {
1396 int logicalX
= i
/ b
;
1397 int logicalY
= i
% b
;
1398 if (i
>= ((a
- 1) * b
)) {
1400 logicalY
= i
- ((a
- 1) * b
);
1403 TWindow w
= sorted
.get(i
);
1404 w
.setX(logicalX
* newWidth
);
1405 w
.setWidth(newWidth
);
1406 if (i
>= ((a
- 1) * b
)) {
1407 w
.setY((logicalY
* newHeight2
) + 1);
1408 w
.setHeight(newHeight2
);
1410 w
.setY((logicalY
* newHeight1
) + 1);
1411 w
.setHeight(newHeight1
);
1417 * Re-layout the open windows as overlapping cascaded windows.
1419 private void cascadeWindows() {
1420 // Don't do anything if we are in the menu
1421 if (activeMenu
!= null) {
1426 List
<TWindow
> sorted
= new LinkedList
<TWindow
>(windows
);
1427 Collections
.sort(sorted
);
1428 Collections
.reverse(sorted
);
1429 for (TWindow window
: sorted
) {
1434 if (x
> getScreen().getWidth()) {
1437 if (y
>= getScreen().getHeight()) {
1444 * Convenience function to add a timer.
1446 * @param duration number of milliseconds to wait between ticks
1447 * @param recurring if true, re-schedule this timer after every tick
1448 * @param action function to call when button is pressed
1451 public final TTimer
addTimer(final long duration
, final boolean recurring
,
1452 final TAction action
) {
1454 TTimer timer
= new TTimer(duration
, recurring
, action
);
1455 synchronized (timers
) {
1462 * Convenience function to remove a timer.
1464 * @param timer timer to remove
1466 public final void removeTimer(final TTimer timer
) {
1467 synchronized (timers
) {
1468 timers
.remove(timer
);
1473 * Convenience function to spawn a message box.
1475 * @param title window title, will be centered along the top border
1476 * @param caption message to display. Use embedded newlines to get a
1478 * @return the new message box
1480 public final TMessageBox
messageBox(final String title
,
1481 final String caption
) {
1483 return new TMessageBox(this, title
, caption
, TMessageBox
.Type
.OK
);
1487 * Convenience function to spawn a message box.
1489 * @param title window title, will be centered along the top border
1490 * @param caption message to display. Use embedded newlines to get a
1492 * @param type one of the TMessageBox.Type constants. Default is
1494 * @return the new message box
1496 public final TMessageBox
messageBox(final String title
,
1497 final String caption
, final TMessageBox
.Type type
) {
1499 return new TMessageBox(this, title
, caption
, type
);
1503 * Convenience function to spawn an input box.
1505 * @param title window title, will be centered along the top border
1506 * @param caption message to display. Use embedded newlines to get a
1508 * @return the new input box
1510 public final TInputBox
inputBox(final String title
, final String caption
) {
1512 return new TInputBox(this, title
, caption
);
1516 * Convenience function to spawn an input box.
1518 * @param title window title, will be centered along the top border
1519 * @param caption message to display. Use embedded newlines to get a
1521 * @param text initial text to seed the field with
1522 * @return the new input box
1524 public final TInputBox
inputBox(final String title
, final String caption
,
1525 final String text
) {
1527 return new TInputBox(this, title
, caption
, text
);