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
.LinkedList
;
38 import java
.util
.List
;
40 import jexer
.bits
.CellAttributes
;
41 import jexer
.bits
.ColorTheme
;
42 import jexer
.bits
.GraphicsChars
;
43 import jexer
.event
.TCommandEvent
;
44 import jexer
.event
.TInputEvent
;
45 import jexer
.event
.TKeypressEvent
;
46 import jexer
.event
.TMenuEvent
;
47 import jexer
.event
.TMouseEvent
;
48 import jexer
.event
.TResizeEvent
;
49 import jexer
.backend
.Backend
;
50 import jexer
.backend
.ECMA48Backend
;
51 import jexer
.io
.Screen
;
52 import jexer
.menu
.TMenu
;
53 import jexer
.menu
.TMenuItem
;
54 import static jexer
.TCommand
.*;
55 import static jexer
.TKeypress
.*;
58 * TApplication sets up a full Text User Interface application.
60 public class TApplication
{
63 * Access to the physical screen, keyboard, and mouse.
65 private Backend backend
;
72 public final Screen
getScreen() {
73 return backend
.getScreen();
77 * Actual mouse coordinate X.
82 * Actual mouse coordinate Y.
87 * Event queue that will be drained by either primary or secondary Fiber.
89 private List
<TInputEvent
> eventQueue
;
92 * Top-level menus in this application.
94 private List
<TMenu
> menus
;
97 * Stack of activated sub-menus in this application.
99 private List
<TMenu
> subMenus
;
102 * The currently acive menu.
104 private TMenu activeMenu
= null;
107 * Windows and widgets pull colors from this ColorTheme.
109 private ColorTheme theme
;
112 * Get the color theme.
116 public final ColorTheme
getTheme() {
121 * The top-level windows (but not menus).
123 private List
<TWindow
> windows
;
126 * When true, exit the application.
128 private boolean quit
= false;
131 * When true, repaint the entire screen.
133 private boolean repaint
= true;
136 * Request full repaint on next screen refresh.
138 public final void setRepaint() {
143 * When true, just flush updates from the screen.
145 private boolean flush
= false;
148 * Y coordinate of the top edge of the desktop. For now this is a
149 * constant. Someday it would be nice to have a multi-line menu or
152 private static final int desktopTop
= 1;
155 * Get Y coordinate of the top edge of the desktop.
157 * @return Y coordinate of the top edge of the desktop
159 public final int getDesktopTop() {
164 * Y coordinate of the bottom edge of the desktop.
166 private int desktopBottom
;
169 * Get Y coordinate of the bottom edge of the desktop.
171 * @return Y coordinate of the bottom edge of the desktop
173 public final int getDesktopBottom() {
174 return desktopBottom
;
178 * Public constructor.
180 * @param input an InputStream connected to the remote user, or null for
181 * System.in. If System.in is used, then on non-Windows systems it will
182 * be put in raw mode; shutdown() will (blindly!) put System.in in cooked
183 * mode. input is always converted to a Reader with UTF-8 encoding.
184 * @param output an OutputStream connected to the remote user, or null
185 * for System.out. output is always converted to a Writer with UTF-8
187 * @throws UnsupportedEncodingException if an exception is thrown when
188 * creating the InputStreamReader
190 public TApplication(final InputStream input
,
191 final OutputStream output
) throws UnsupportedEncodingException
{
193 backend
= new ECMA48Backend(input
, output
);
194 theme
= new ColorTheme();
195 desktopBottom
= getScreen().getHeight() - 1;
196 eventQueue
= new LinkedList
<TInputEvent
>();
197 windows
= new LinkedList
<TWindow
>();
198 menus
= new LinkedList
<TMenu
>();
199 subMenus
= new LinkedList
<TMenu
>();
203 * Invert the cell at the mouse pointer position.
205 private void drawMouse() {
206 CellAttributes attr
= getScreen().getAttrXY(mouseX
, mouseY
);
207 attr
.setForeColor(attr
.getForeColor().invert());
208 attr
.setBackColor(attr
.getBackColor().invert());
209 getScreen().putAttrXY(mouseX
, mouseY
, attr
, false);
212 if (windows
.size() == 0) {
220 public final void drawAll() {
221 if ((flush
) && (!repaint
)) {
222 backend
.flushScreen();
231 // If true, the cursor is not visible
232 boolean cursor
= false;
234 // Start with a clean screen
237 // Draw the background
238 CellAttributes background
= theme
.getColor("tapplication.background");
239 getScreen().putAll(GraphicsChars
.HATCH
, background
);
241 // Draw each window in reverse Z order
242 List
<TWindow
> sorted
= new LinkedList
<TWindow
>(windows
);
243 Collections
.sort(sorted
);
244 Collections
.reverse(sorted
);
245 for (TWindow window
: sorted
) {
246 window
.drawChildren();
249 // Draw the blank menubar line - reset the screen clipping first so
250 // it won't trim it out.
251 getScreen().resetClipping();
252 getScreen().hLineXY(0, 0, getScreen().getWidth(), ' ',
253 theme
.getColor("tmenu"));
254 // Now draw the menus.
256 for (TMenu menu
: menus
) {
257 CellAttributes menuColor
;
258 CellAttributes menuMnemonicColor
;
259 if (menu
.getActive()) {
260 menuColor
= theme
.getColor("tmenu.highlighted");
261 menuMnemonicColor
= theme
.getColor("tmenu.mnemonic.highlighted");
263 menuColor
= theme
.getColor("tmenu");
264 menuMnemonicColor
= theme
.getColor("tmenu.mnemonic");
266 // Draw the menu title
267 getScreen().hLineXY(x
, 0, menu
.getTitle().length() + 2, ' ',
269 getScreen().putStrXY(x
+ 1, 0, menu
.getTitle(), menuColor
);
270 // Draw the highlight character
271 getScreen().putCharXY(x
+ 1 + menu
.getMnemonic().getShortcutIdx(),
272 0, menu
.getMnemonic().getShortcut(), menuMnemonicColor
);
274 if (menu
.getActive()) {
276 // Reset the screen clipping so we can draw the next title.
277 getScreen().resetClipping();
279 x
+= menu
.getTitle().length() + 2;
282 for (TMenu menu
: subMenus
) {
283 // Reset the screen clipping so we can draw the next sub-menu.
284 getScreen().resetClipping();
288 // Draw the mouse pointer
291 // Place the cursor if it is visible
292 TWidget activeWidget
= null;
293 if (sorted
.size() > 0) {
294 activeWidget
= sorted
.get(sorted
.size() - 1).getActiveChild();
295 if (activeWidget
.visibleCursor()) {
296 getScreen().putCursor(true, activeWidget
.getCursorAbsoluteX(),
297 activeWidget
.getCursorAbsoluteY());
304 getScreen().hideCursor();
307 // Flush the screen contents
308 backend
.flushScreen();
315 * Run this application until it exits.
317 public final void run() {
318 List
<TInputEvent
> events
= new LinkedList
<TInputEvent
>();
321 // Timeout is in milliseconds, so default timeout after 1 second
323 int timeout
= getSleepTime(1000);
325 if (eventQueue
.size() > 0) {
326 // Do not wait if there are definitely events waiting to be
327 // processed or a screen redraw to do.
331 // Pull any pending input events
332 backend
.getEvents(events
, timeout
);
333 metaHandleEvents(events
);
336 // Process timers and call doIdle()'s
345 // Shutdown the fibers
346 eventQueue.length = 0;
347 if (secondaryEventFiber !is null) {
348 assert(secondaryEventReceiver !is null);
349 secondaryEventReceiver = null;
350 if (secondaryEventFiber.state == Fiber.State.HOLD) {
351 // Wake up the secondary handler so that it can exit.
352 secondaryEventFiber.call();
356 if (primaryEventFiber.state == Fiber.State.HOLD) {
357 // Wake up the primary handler so that it can exit.
358 primaryEventFiber.call();
366 * Peek at certain application-level events, add to eventQueue, and wake
367 * up the consuming Fiber.
369 * @param events the input events to consume
371 private void metaHandleEvents(final List
<TInputEvent
> events
) {
373 for (TInputEvent event
: events
) {
376 System.err.printf(String.format("metaHandleEvents event: %s\n",
377 event)); System.err.flush();
381 // Do no more processing if the application is already trying
387 if (event
instanceof TKeypressEvent
) {
388 TKeypressEvent keypress
= (TKeypressEvent
) event
;
389 if (keypress
.equals(kbAltX
)) {
396 // Special application-wide events -------------------------------
399 if (event
instanceof TCommandEvent
) {
400 TCommandEvent command
= (TCommandEvent
) event
;
401 if (command
.getCmd().equals(cmAbort
)) {
408 if (event
instanceof TResizeEvent
) {
409 TResizeEvent resize
= (TResizeEvent
) event
;
410 getScreen().setDimensions(resize
.getWidth(),
412 desktopBottom
= getScreen().getHeight() - 1;
419 // Peek at the mouse position
420 if (event
instanceof TMouseEvent
) {
421 TMouseEvent mouse
= (TMouseEvent
) event
;
422 if ((mouseX
!= mouse
.getX()) || (mouseY
!= mouse
.getY())) {
423 mouseX
= mouse
.getX();
424 mouseY
= mouse
.getY();
429 // TODO: change to two separate threads
430 primaryHandleEvent(event
);
434 // Put into the main queue
437 // Have one of the two consumer Fibers peel the events off
439 if (secondaryEventFiber !is null) {
440 assert(secondaryEventFiber.state == Fiber.State.HOLD);
442 // Wake up the secondary handler for these events
443 secondaryEventFiber.call();
445 assert(primaryEventFiber.state == Fiber.State.HOLD);
447 // Wake up the primary handler for these events
448 primaryEventFiber.call();
452 } // for (TInputEvent event: events)
457 * Dispatch one event to the appropriate widget or application-level
458 * event handler. This is the primary event handler, it has the normal
459 * application-wide event handling.
461 * @param event the input event to consume
462 * @see #secondaryHandleEvent(TInputEvent event)
464 private void primaryHandleEvent(final TInputEvent event
) {
466 // System.err.printf("Handle event: %s\n", event);
468 // Special application-wide events -----------------------------------
470 // Peek at the mouse position
471 if (event
instanceof TMouseEvent
) {
472 // See if we need to switch focus to another window or the menu
473 checkSwitchFocus((TMouseEvent
) event
);
476 // Handle menu events
477 if ((activeMenu
!= null) && !(event
instanceof TCommandEvent
)) {
478 TMenu menu
= activeMenu
;
480 if (event
instanceof TMouseEvent
) {
481 TMouseEvent mouse
= (TMouseEvent
) event
;
483 while (subMenus
.size() > 0) {
484 TMenu subMenu
= subMenus
.get(subMenus
.size() - 1);
485 if (subMenu
.mouseWouldHit(mouse
)) {
488 if ((mouse
.getType() == TMouseEvent
.Type
.MOUSE_MOTION
)
489 && (!mouse
.getMouse1())
490 && (!mouse
.getMouse2())
491 && (!mouse
.getMouse3())
492 && (!mouse
.getMouseWheelUp())
493 && (!mouse
.getMouseWheelDown())
497 // We navigated away from a sub-menu, so close it
501 // Convert the mouse relative x/y to menu coordinates
502 assert (mouse
.getX() == mouse
.getAbsoluteX());
503 assert (mouse
.getY() == mouse
.getAbsoluteY());
504 if (subMenus
.size() > 0) {
505 menu
= subMenus
.get(subMenus
.size() - 1);
507 mouse
.setX(mouse
.getX() - menu
.getX());
508 mouse
.setY(mouse
.getY() - menu
.getY());
510 menu
.handleEvent(event
);
517 if (event instanceof TKeypressEvent) {
518 TKeypressEvent keypress = (TKeypressEvent) event;
519 // See if this key matches an accelerator, and if so dispatch the
521 TKeypress keypressLowercase = keypress.getKey().toLowerCase();
522 TMenuItem item = accelerators.get(keypressLowercase);
524 // Let the menu item dispatch
528 // Handle the keypress
529 if (onKeypress(keypress)) {
536 if (event
instanceof TCommandEvent
) {
537 if (onCommand((TCommandEvent
) event
)) {
542 if (event
instanceof TMenuEvent
) {
543 if (onMenu((TMenuEvent
) event
)) {
548 // Dispatch events to the active window -------------------------------
549 for (TWindow window
: windows
) {
550 if (window
.getActive()) {
551 if (event
instanceof TMouseEvent
) {
552 TMouseEvent mouse
= (TMouseEvent
) event
;
553 // Convert the mouse relative x/y to window coordinates
554 assert (mouse
.getX() == mouse
.getAbsoluteX());
555 assert (mouse
.getY() == mouse
.getAbsoluteY());
556 mouse
.setX(mouse
.getX() - window
.getX());
557 mouse
.setY(mouse
.getY() - window
.getY());
559 // System.err("TApplication dispatch event: %s\n", event);
560 window
.handleEvent(event
);
566 * Dispatch one event to the appropriate widget or application-level
567 * event handler. This is the secondary event handler used by certain
568 * special dialogs (currently TMessageBox and TFileOpenBox).
570 * @param event the input event to consume
571 * @see #primaryHandleEvent(TInputEvent event)
573 private void secondaryHandleEvent(final TInputEvent event
) {
578 * Do stuff when there is no user input.
580 private void doIdle() {
583 // Now run any timers that have timed out
584 auto now = Clock.currTime;
585 TTimer [] keepTimers;
586 foreach (t; timers) {
587 if (t.nextTick < now) {
589 if (t.recurring == true) {
599 foreach (w; windows) {
606 * Get the amount of time I can sleep before missing a Timer tick.
608 * @param timeout = initial (maximum) timeout
609 * @return number of milliseconds between now and the next timer event
611 protected int getSleepTime(final int timeout
) {
613 auto now = Clock.currTime;
614 auto sleepTime = dur!("msecs")(timeout);
615 foreach (t; timers) {
616 if (t.nextTick < now) {
619 if ((t.nextTick > now) &&
620 ((t.nextTick - now) < sleepTime)
622 sleepTime = t.nextTick - now;
625 assert(sleepTime.total!("msecs")() >= 0);
626 return cast(uint)sleepTime.total!("msecs")();
628 // TODO: fix timers. Until then, come back after 250 millis.
633 * Close window. Note that the window's destructor is NOT called by this
634 * method, instead the GC is assumed to do the cleanup.
636 * @param window the window to remove
638 public final void closeWindow(final TWindow window
) {
639 int z
= window
.getZ();
641 Collections
.sort(windows
);
643 TWindow activeWindow
= null;
644 for (TWindow w
: windows
) {
646 w
.setZ(w
.getZ() - 1);
649 assert (activeWindow
== null);
657 // Perform window cleanup
667 // Check if we are closing a TMessageBox or similar
668 if (secondaryEventReceiver !is null) {
669 assert(secondaryEventFiber !is null);
671 // Do not send events to the secondaryEventReceiver anymore, the
673 secondaryEventReceiver = null;
675 // Special case: if this is called while executing on a
676 // secondaryEventFiber, call it so that widgetEventHandler() can
678 if (secondaryEventFiber.state == Fiber.State.HOLD) {
679 secondaryEventFiber.call();
681 secondaryEventFiber = null;
683 // Unfreeze the logic in handleEvent()
684 if (primaryEventFiber.state == Fiber.State.HOLD) {
685 primaryEventFiber.call();
692 * Switch to the next window.
694 * @param forward if true, then switch to the next window in the list,
695 * otherwise switch to the previous window in the list
697 public final void switchWindow(final boolean forward
) {
698 // Only switch if there are multiple windows
699 if (windows
.size() < 2) {
703 // Swap z/active between active window and the next in the list
704 int activeWindowI
= -1;
705 for (int i
= 0; i
< windows
.size(); i
++) {
706 if (windows
.get(i
).getActive()) {
711 assert (activeWindowI
>= 0);
713 // Do not switch if a window is modal
714 if (windows
.get(activeWindowI
).isModal()) {
720 nextWindowI
= (activeWindowI
+ 1) % windows
.size();
722 if (activeWindowI
== 0) {
723 nextWindowI
= windows
.size() - 1;
725 nextWindowI
= activeWindowI
- 1;
728 windows
.get(activeWindowI
).setActive(false);
729 windows
.get(activeWindowI
).setZ(windows
.get(nextWindowI
).getZ());
730 windows
.get(nextWindowI
).setZ(0);
731 windows
.get(nextWindowI
).setActive(true);
738 * Add a window to my window list and make it active.
740 * @param window new window to add
742 public final void addWindow(final TWindow window
) {
743 // Do not allow a modal window to spawn a non-modal window
744 if ((windows
.size() > 0) && (windows
.get(0).isModal())) {
745 assert (window
.isModal());
747 for (TWindow w
: windows
) {
749 w
.setZ(w
.getZ() + 1);
752 window
.setActive(true);
757 * Check if there is a system-modal window on top.
759 * @return true if the active window is modal
761 private boolean modalWindowActive() {
762 if (windows
.size() == 0) {
765 return windows
.get(windows
.size() - 1).isModal();
769 * Check if a mouse event would hit either the active menu or any open
772 * @param mouse mouse event
773 * @return true if the mouse would hit the active menu or an open
776 private boolean mouseOnMenu(final TMouseEvent mouse
) {
777 assert (activeMenu
!= null);
778 List
<TMenu
> menus
= new LinkedList
<TMenu
>(subMenus
);
779 Collections
.reverse(menus
);
780 for (TMenu menu
: menus
) {
781 if (menu
.mouseWouldHit(mouse
)) {
785 return activeMenu
.mouseWouldHit(mouse
);
789 * See if we need to switch window or activate the menu based on
792 * @param mouse mouse event
794 private void checkSwitchFocus(final TMouseEvent mouse
) {
796 if ((mouse
.getType() == TMouseEvent
.Type
.MOUSE_DOWN
)
797 && (activeMenu
!= null)
798 && (mouse
.getAbsoluteY() != 0)
799 && (!mouseOnMenu(mouse
))
801 // They clicked outside the active menu, turn it off
802 activeMenu
.setActive(false);
804 for (TMenu menu
: subMenus
) {
805 menu
.setActive(false);
811 // See if they hit the menu bar
812 if ((mouse
.getType() == TMouseEvent
.Type
.MOUSE_DOWN
)
813 && (mouse
.getMouse1())
814 && (!modalWindowActive())
815 && (mouse
.getAbsoluteY() == 0)
818 for (TMenu menu
: subMenus
) {
819 menu
.setActive(false);
823 // They selected the menu, go activate it
824 for (TMenu menu
: menus
) {
825 if ((mouse
.getAbsoluteX() >= menu
.getX())
826 && (mouse
.getAbsoluteX() < menu
.getX()
827 + menu
.getTitle().length() + 2)
829 menu
.setActive(true);
832 menu
.setActive(false);
839 // See if they hit the menu bar
840 if ((mouse
.getType() == TMouseEvent
.Type
.MOUSE_MOTION
)
841 && (mouse
.getMouse1())
842 && (activeMenu
!= null)
843 && (mouse
.getAbsoluteY() == 0)
846 TMenu oldMenu
= activeMenu
;
847 for (TMenu menu
: subMenus
) {
848 menu
.setActive(false);
852 // See if we should switch menus
853 for (TMenu menu
: menus
) {
854 if ((mouse
.getAbsoluteX() >= menu
.getX())
855 && (mouse
.getAbsoluteX() < menu
.getX()
856 + menu
.getTitle().length() + 2)
858 menu
.setActive(true);
862 if (oldMenu
!= activeMenu
) {
863 // They switched menus
864 oldMenu
.setActive(false);
870 // Only switch if there are multiple windows
871 if (windows
.size() < 2) {
875 // Switch on the upclick
876 if (mouse
.getType() != TMouseEvent
.Type
.MOUSE_UP
) {
880 Collections
.sort(windows
);
881 if (windows
.get(0).isModal()) {
882 // Modal windows don't switch
886 for (TWindow window
: windows
) {
887 assert (!window
.isModal());
888 if (window
.mouseWouldHit(mouse
)) {
889 if (window
== windows
.get(0)) {
890 // Clicked on the same window, nothing to do
894 // We will be switching to another window
895 assert (windows
.get(0).getActive());
896 assert (!window
.getActive());
897 windows
.get(0).setActive(false);
898 windows
.get(0).setZ(window
.getZ());
900 window
.setActive(true);
906 // Clicked on the background, nothing to do
913 public final void closeMenu() {
914 if (activeMenu
!= null) {
915 activeMenu
.setActive(false);
917 for (TMenu menu
: subMenus
) {
918 menu
.setActive(false);
926 * Turn off a sub-menu.
928 public final void closeSubMenu() {
929 assert (activeMenu
!= null);
930 TMenu item
= subMenus
.get(subMenus
.size() - 1);
931 assert (item
!= null);
932 item
.setActive(false);
933 subMenus
.remove(subMenus
.size() - 1);
938 * Switch to the next menu.
940 * @param forward if true, then switch to the next menu in the list,
941 * otherwise switch to the previous menu in the list
943 public final void switchMenu(final boolean forward
) {
944 assert (activeMenu
!= null);
946 for (TMenu menu
: subMenus
) {
947 menu
.setActive(false);
951 for (int i
= 0; i
< menus
.size(); i
++) {
952 if (activeMenu
== menus
.get(i
)) {
954 if (i
< menus
.size() - 1) {
962 activeMenu
.setActive(false);
963 activeMenu
= menus
.get(i
);
964 activeMenu
.setActive(true);
972 * Method that TApplication subclasses can override to handle menu or
973 * posted command events.
975 * @param command command event
976 * @return if true, this event was consumed
978 protected boolean onCommand(final TCommandEvent command
) {
981 // Default: handle cmExit
982 if (command.equals(cmExit)) {
983 if (messageBox("Confirmation", "Exit application?",
984 TMessageBox.Type.YESNO).result == TMessageBox.Result.YES) {
991 if (command.equals(cmShell)) {
992 openTerminal(0, 0, TWindow.Flag.RESIZABLE);
997 if (command.equals(cmTile)) {
1002 if (command.equals(cmCascade)) {
1007 if (command.equals(cmCloseAll)) {
1017 * Method that TApplication subclasses can override to handle menu
1020 * @param menu menu event
1021 * @return if true, this event was consumed
1023 protected boolean onMenu(final TMenuEvent menu
) {
1027 // Default: handle MID_EXIT
1028 if (menu.id == TMenu.MID_EXIT) {
1029 if (messageBox("Confirmation", "Exit application?",
1030 TMessageBox.Type.YESNO).result == TMessageBox.Result.YES) {
1033 // System.err.printf("onMenu MID_EXIT result: quit = %s\n", quit);
1038 if (menu.id == TMenu.MID_SHELL) {
1039 openTerminal(0, 0, TWindow.Flag.RESIZABLE);
1044 if (menu.id == TMenu.MID_TILE) {
1049 if (menu.id == TMenu.MID_CASCADE) {
1054 if (menu.id == TMenu.MID_CLOSE_ALL) {
1064 * Method that TApplication subclasses can override to handle keystrokes.
1066 * @param keypress keystroke event
1067 * @return if true, this event was consumed
1069 protected boolean onKeypress(final TKeypressEvent keypress
) {
1070 // Default: only menu shortcuts
1072 // Process Alt-F, Alt-E, etc. menu shortcut keys
1073 if (!keypress
.getKey().getIsKey()
1074 && keypress
.getKey().getAlt()
1075 && !keypress
.getKey().getCtrl()
1076 && (activeMenu
== null)
1079 assert (subMenus
.size() == 0);
1081 for (TMenu menu
: menus
) {
1082 if (Character
.toLowerCase(menu
.getMnemonic().getShortcut())
1083 == Character
.toLowerCase(keypress
.getKey().getCh())
1086 menu
.setActive(true);
1097 * Add a keyboard accelerator to the global hash.
1099 * @param item menu item this accelerator relates to
1100 * @param keypress keypress that will dispatch a TMenuEvent
1102 public final void addAccelerator(final TMenuItem item
,
1103 final TKeypress keypress
) {
1106 assert((keypress in accelerators) is null);
1107 accelerators[keypress] = item;
1112 * Recompute menu x positions based on their title length.
1114 public final void recomputeMenuX() {
1116 for (TMenu menu
: menus
) {
1118 x
+= menu
.getTitle().length() + 2;
1123 * Post an event to process and turn off the menu.
1125 * @param event new event to add to the queue
1127 public final void addMenuEvent(final TInputEvent event
) {
1129 TODO - synchronize correctly
1130 eventQueue ~= event;
1136 * Add a sub-menu to the list of open sub-menus.
1138 * @param menu sub-menu
1140 public final void addSubMenu(final TMenu menu
) {