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 is filled by run().
89 private List
<TInputEvent
> fillEventQueue
;
92 * Event queue that will be drained by either primary or secondary
95 private List
<TInputEvent
> drainEventQueue
;
98 * Top-level menus in this application.
100 private List
<TMenu
> menus
;
103 * Stack of activated sub-menus in this application.
105 private List
<TMenu
> subMenus
;
108 * The currently acive menu.
110 private TMenu activeMenu
= null;
113 * Windows and widgets pull colors from this ColorTheme.
115 private ColorTheme theme
;
118 * Get the color theme.
122 public final ColorTheme
getTheme() {
127 * The top-level windows (but not menus).
129 private List
<TWindow
> windows
;
132 * When true, exit the application.
134 private boolean quit
= false;
137 * When true, repaint the entire screen.
139 private boolean repaint
= true;
142 * Request full repaint on next screen refresh.
144 public final void setRepaint() {
149 * When true, just flush updates from the screen.
151 private boolean flush
= false;
154 * Y coordinate of the top edge of the desktop. For now this is a
155 * constant. Someday it would be nice to have a multi-line menu or
158 private static final int desktopTop
= 1;
161 * Get Y coordinate of the top edge of the desktop.
163 * @return Y coordinate of the top edge of the desktop
165 public final int getDesktopTop() {
170 * Y coordinate of the bottom edge of the desktop.
172 private int desktopBottom
;
175 * Get Y coordinate of the bottom edge of the desktop.
177 * @return Y coordinate of the bottom edge of the desktop
179 public final int getDesktopBottom() {
180 return desktopBottom
;
184 * Public constructor.
186 * @param input an InputStream connected to the remote user, or null for
187 * System.in. If System.in is used, then on non-Windows systems it will
188 * be put in raw mode; shutdown() will (blindly!) put System.in in cooked
189 * mode. input is always converted to a Reader with UTF-8 encoding.
190 * @param output an OutputStream connected to the remote user, or null
191 * for System.out. output is always converted to a Writer with UTF-8
193 * @throws UnsupportedEncodingException if an exception is thrown when
194 * creating the InputStreamReader
196 public TApplication(final InputStream input
,
197 final OutputStream output
) throws UnsupportedEncodingException
{
199 backend
= new ECMA48Backend(input
, output
);
200 theme
= new ColorTheme();
201 desktopBottom
= getScreen().getHeight() - 1;
202 fillEventQueue
= new LinkedList
<TInputEvent
>();
203 drainEventQueue
= new LinkedList
<TInputEvent
>();
204 windows
= new LinkedList
<TWindow
>();
205 menus
= new LinkedList
<TMenu
>();
206 subMenus
= new LinkedList
<TMenu
>();
210 * Invert the cell at the mouse pointer position.
212 private void drawMouse() {
213 CellAttributes attr
= getScreen().getAttrXY(mouseX
, mouseY
);
214 attr
.setForeColor(attr
.getForeColor().invert());
215 attr
.setBackColor(attr
.getBackColor().invert());
216 getScreen().putAttrXY(mouseX
, mouseY
, attr
, false);
219 if (windows
.size() == 0) {
227 public final void drawAll() {
228 if ((flush
) && (!repaint
)) {
229 backend
.flushScreen();
238 // If true, the cursor is not visible
239 boolean cursor
= false;
241 // Start with a clean screen
244 // Draw the background
245 CellAttributes background
= theme
.getColor("tapplication.background");
246 getScreen().putAll(GraphicsChars
.HATCH
, background
);
248 // Draw each window in reverse Z order
249 List
<TWindow
> sorted
= new LinkedList
<TWindow
>(windows
);
250 Collections
.sort(sorted
);
251 Collections
.reverse(sorted
);
252 for (TWindow window
: sorted
) {
253 window
.drawChildren();
256 // Draw the blank menubar line - reset the screen clipping first so
257 // it won't trim it out.
258 getScreen().resetClipping();
259 getScreen().hLineXY(0, 0, getScreen().getWidth(), ' ',
260 theme
.getColor("tmenu"));
261 // Now draw the menus.
263 for (TMenu menu
: menus
) {
264 CellAttributes menuColor
;
265 CellAttributes menuMnemonicColor
;
266 if (menu
.getActive()) {
267 menuColor
= theme
.getColor("tmenu.highlighted");
268 menuMnemonicColor
= theme
.getColor("tmenu.mnemonic.highlighted");
270 menuColor
= theme
.getColor("tmenu");
271 menuMnemonicColor
= theme
.getColor("tmenu.mnemonic");
273 // Draw the menu title
274 getScreen().hLineXY(x
, 0, menu
.getTitle().length() + 2, ' ',
276 getScreen().putStrXY(x
+ 1, 0, menu
.getTitle(), menuColor
);
277 // Draw the highlight character
278 getScreen().putCharXY(x
+ 1 + menu
.getMnemonic().getShortcutIdx(),
279 0, menu
.getMnemonic().getShortcut(), menuMnemonicColor
);
281 if (menu
.getActive()) {
283 // Reset the screen clipping so we can draw the next title.
284 getScreen().resetClipping();
286 x
+= menu
.getTitle().length() + 2;
289 for (TMenu menu
: subMenus
) {
290 // Reset the screen clipping so we can draw the next sub-menu.
291 getScreen().resetClipping();
295 // Draw the mouse pointer
298 // Place the cursor if it is visible
299 TWidget activeWidget
= null;
300 if (sorted
.size() > 0) {
301 activeWidget
= sorted
.get(sorted
.size() - 1).getActiveChild();
302 if (activeWidget
.visibleCursor()) {
303 getScreen().putCursor(true, activeWidget
.getCursorAbsoluteX(),
304 activeWidget
.getCursorAbsoluteY());
311 getScreen().hideCursor();
314 // Flush the screen contents
315 backend
.flushScreen();
322 * Run this application until it exits.
324 public final void run() {
326 // Timeout is in milliseconds, so default timeout after 1 second
328 int timeout
= getSleepTime(1000);
330 // See if there are any definitely events waiting to be processed
331 // or a screen redraw to do. If so, do not wait if there is no
333 synchronized (drainEventQueue
) {
334 if (drainEventQueue
.size() > 0) {
338 synchronized (fillEventQueue
) {
339 if (fillEventQueue
.size() > 0) {
344 // Pull any pending I/O events
345 backend
.getEvents(fillEventQueue
, timeout
);
347 // Dispatch each event to the appropriate handler, one at a time.
349 TInputEvent event
= null;
350 synchronized (fillEventQueue
) {
351 if (fillEventQueue
.size() == 0) {
354 event
= fillEventQueue
.remove(0);
356 metaHandleEvent(event
);
359 // Process timers and call doIdle()'s
368 // Shutdown the fibers
369 eventQueue.length = 0;
370 if (secondaryEventFiber !is null) {
371 assert(secondaryEventReceiver !is null);
372 secondaryEventReceiver = null;
373 if (secondaryEventFiber.state == Fiber.State.HOLD) {
374 // Wake up the secondary handler so that it can exit.
375 secondaryEventFiber.call();
379 if (primaryEventFiber.state == Fiber.State.HOLD) {
380 // Wake up the primary handler so that it can exit.
381 primaryEventFiber.call();
389 * Peek at certain application-level events, add to eventQueue, and wake
390 * up the consuming Thread.
392 * @param event the input event to consume
394 private void metaHandleEvent(final TInputEvent event
) {
397 System.err.printf(String.format("metaHandleEvents event: %s\n",
398 event)); System.err.flush();
402 // Do no more processing if the application is already trying
408 if (event
instanceof TKeypressEvent
) {
409 TKeypressEvent keypress
= (TKeypressEvent
) event
;
410 if (keypress
.equals(kbAltX
)) {
417 // Special application-wide events -------------------------------
420 if (event
instanceof TCommandEvent
) {
421 TCommandEvent command
= (TCommandEvent
) event
;
422 if (command
.getCmd().equals(cmAbort
)) {
429 if (event
instanceof TResizeEvent
) {
430 TResizeEvent resize
= (TResizeEvent
) event
;
431 getScreen().setDimensions(resize
.getWidth(),
433 desktopBottom
= getScreen().getHeight() - 1;
440 // Peek at the mouse position
441 if (event
instanceof TMouseEvent
) {
442 TMouseEvent mouse
= (TMouseEvent
) event
;
443 if ((mouseX
!= mouse
.getX()) || (mouseY
!= mouse
.getY())) {
444 mouseX
= mouse
.getX();
445 mouseY
= mouse
.getY();
450 // TODO: change to two separate threads
451 primaryHandleEvent(event
);
455 // Put into the main queue
458 // Have one of the two consumer Fibers peel the events off
460 if (secondaryEventFiber !is null) {
461 assert(secondaryEventFiber.state == Fiber.State.HOLD);
463 // Wake up the secondary handler for these events
464 secondaryEventFiber.call();
466 assert(primaryEventFiber.state == Fiber.State.HOLD);
468 // Wake up the primary handler for these events
469 primaryEventFiber.call();
476 * Dispatch one event to the appropriate widget or application-level
477 * event handler. This is the primary event handler, it has the normal
478 * application-wide event handling.
480 * @param event the input event to consume
481 * @see #secondaryHandleEvent(TInputEvent event)
483 private void primaryHandleEvent(final TInputEvent event
) {
485 // System.err.printf("Handle event: %s\n", event);
487 // Special application-wide events -----------------------------------
489 // Peek at the mouse position
490 if (event
instanceof TMouseEvent
) {
491 // See if we need to switch focus to another window or the menu
492 checkSwitchFocus((TMouseEvent
) event
);
495 // Handle menu events
496 if ((activeMenu
!= null) && !(event
instanceof TCommandEvent
)) {
497 TMenu menu
= activeMenu
;
499 if (event
instanceof TMouseEvent
) {
500 TMouseEvent mouse
= (TMouseEvent
) event
;
502 while (subMenus
.size() > 0) {
503 TMenu subMenu
= subMenus
.get(subMenus
.size() - 1);
504 if (subMenu
.mouseWouldHit(mouse
)) {
507 if ((mouse
.getType() == TMouseEvent
.Type
.MOUSE_MOTION
)
508 && (!mouse
.getMouse1())
509 && (!mouse
.getMouse2())
510 && (!mouse
.getMouse3())
511 && (!mouse
.getMouseWheelUp())
512 && (!mouse
.getMouseWheelDown())
516 // We navigated away from a sub-menu, so close it
520 // Convert the mouse relative x/y to menu coordinates
521 assert (mouse
.getX() == mouse
.getAbsoluteX());
522 assert (mouse
.getY() == mouse
.getAbsoluteY());
523 if (subMenus
.size() > 0) {
524 menu
= subMenus
.get(subMenus
.size() - 1);
526 mouse
.setX(mouse
.getX() - menu
.getX());
527 mouse
.setY(mouse
.getY() - menu
.getY());
529 menu
.handleEvent(event
);
536 if (event instanceof TKeypressEvent) {
537 TKeypressEvent keypress = (TKeypressEvent) event;
538 // See if this key matches an accelerator, and if so dispatch the
540 TKeypress keypressLowercase = keypress.getKey().toLowerCase();
541 TMenuItem item = accelerators.get(keypressLowercase);
543 // Let the menu item dispatch
547 // Handle the keypress
548 if (onKeypress(keypress)) {
555 if (event
instanceof TCommandEvent
) {
556 if (onCommand((TCommandEvent
) event
)) {
561 if (event
instanceof TMenuEvent
) {
562 if (onMenu((TMenuEvent
) event
)) {
567 // Dispatch events to the active window -------------------------------
568 for (TWindow window
: windows
) {
569 if (window
.getActive()) {
570 if (event
instanceof TMouseEvent
) {
571 TMouseEvent mouse
= (TMouseEvent
) event
;
572 // Convert the mouse relative x/y to window coordinates
573 assert (mouse
.getX() == mouse
.getAbsoluteX());
574 assert (mouse
.getY() == mouse
.getAbsoluteY());
575 mouse
.setX(mouse
.getX() - window
.getX());
576 mouse
.setY(mouse
.getY() - window
.getY());
578 // System.err("TApplication dispatch event: %s\n", event);
579 window
.handleEvent(event
);
585 * Dispatch one event to the appropriate widget or application-level
586 * event handler. This is the secondary event handler used by certain
587 * special dialogs (currently TMessageBox and TFileOpenBox).
589 * @param event the input event to consume
590 * @see #primaryHandleEvent(TInputEvent event)
592 private void secondaryHandleEvent(final TInputEvent event
) {
597 * Do stuff when there is no user input.
599 private void doIdle() {
602 // Now run any timers that have timed out
603 auto now = Clock.currTime;
604 TTimer [] keepTimers;
605 foreach (t; timers) {
606 if (t.nextTick < now) {
608 if (t.recurring == true) {
618 foreach (w; windows) {
625 * Get the amount of time I can sleep before missing a Timer tick.
627 * @param timeout = initial (maximum) timeout
628 * @return number of milliseconds between now and the next timer event
630 protected int getSleepTime(final int timeout
) {
632 auto now = Clock.currTime;
633 auto sleepTime = dur!("msecs")(timeout);
634 foreach (t; timers) {
635 if (t.nextTick < now) {
638 if ((t.nextTick > now) &&
639 ((t.nextTick - now) < sleepTime)
641 sleepTime = t.nextTick - now;
644 assert(sleepTime.total!("msecs")() >= 0);
645 return cast(uint)sleepTime.total!("msecs")();
647 // TODO: fix timers. Until then, come back after 250 millis.
652 * Close window. Note that the window's destructor is NOT called by this
653 * method, instead the GC is assumed to do the cleanup.
655 * @param window the window to remove
657 public final void closeWindow(final TWindow window
) {
658 int z
= window
.getZ();
660 Collections
.sort(windows
);
662 TWindow activeWindow
= null;
663 for (TWindow w
: windows
) {
665 w
.setZ(w
.getZ() - 1);
668 assert (activeWindow
== null);
676 // Perform window cleanup
686 // Check if we are closing a TMessageBox or similar
687 if (secondaryEventReceiver !is null) {
688 assert(secondaryEventFiber !is null);
690 // Do not send events to the secondaryEventReceiver anymore, the
692 secondaryEventReceiver = null;
694 // Special case: if this is called while executing on a
695 // secondaryEventFiber, call it so that widgetEventHandler() can
697 if (secondaryEventFiber.state == Fiber.State.HOLD) {
698 secondaryEventFiber.call();
700 secondaryEventFiber = null;
702 // Unfreeze the logic in handleEvent()
703 if (primaryEventFiber.state == Fiber.State.HOLD) {
704 primaryEventFiber.call();
711 * Switch to the next window.
713 * @param forward if true, then switch to the next window in the list,
714 * otherwise switch to the previous window in the list
716 public final void switchWindow(final boolean forward
) {
717 // Only switch if there are multiple windows
718 if (windows
.size() < 2) {
722 // Swap z/active between active window and the next in the list
723 int activeWindowI
= -1;
724 for (int i
= 0; i
< windows
.size(); i
++) {
725 if (windows
.get(i
).getActive()) {
730 assert (activeWindowI
>= 0);
732 // Do not switch if a window is modal
733 if (windows
.get(activeWindowI
).isModal()) {
739 nextWindowI
= (activeWindowI
+ 1) % windows
.size();
741 if (activeWindowI
== 0) {
742 nextWindowI
= windows
.size() - 1;
744 nextWindowI
= activeWindowI
- 1;
747 windows
.get(activeWindowI
).setActive(false);
748 windows
.get(activeWindowI
).setZ(windows
.get(nextWindowI
).getZ());
749 windows
.get(nextWindowI
).setZ(0);
750 windows
.get(nextWindowI
).setActive(true);
757 * Add a window to my window list and make it active.
759 * @param window new window to add
761 public final void addWindow(final TWindow window
) {
762 // Do not allow a modal window to spawn a non-modal window
763 if ((windows
.size() > 0) && (windows
.get(0).isModal())) {
764 assert (window
.isModal());
766 for (TWindow w
: windows
) {
768 w
.setZ(w
.getZ() + 1);
771 window
.setActive(true);
776 * Check if there is a system-modal window on top.
778 * @return true if the active window is modal
780 private boolean modalWindowActive() {
781 if (windows
.size() == 0) {
784 return windows
.get(windows
.size() - 1).isModal();
788 * Check if a mouse event would hit either the active menu or any open
791 * @param mouse mouse event
792 * @return true if the mouse would hit the active menu or an open
795 private boolean mouseOnMenu(final TMouseEvent mouse
) {
796 assert (activeMenu
!= null);
797 List
<TMenu
> menus
= new LinkedList
<TMenu
>(subMenus
);
798 Collections
.reverse(menus
);
799 for (TMenu menu
: menus
) {
800 if (menu
.mouseWouldHit(mouse
)) {
804 return activeMenu
.mouseWouldHit(mouse
);
808 * See if we need to switch window or activate the menu based on
811 * @param mouse mouse event
813 private void checkSwitchFocus(final TMouseEvent mouse
) {
815 if ((mouse
.getType() == TMouseEvent
.Type
.MOUSE_DOWN
)
816 && (activeMenu
!= null)
817 && (mouse
.getAbsoluteY() != 0)
818 && (!mouseOnMenu(mouse
))
820 // They clicked outside the active menu, turn it off
821 activeMenu
.setActive(false);
823 for (TMenu menu
: subMenus
) {
824 menu
.setActive(false);
830 // See if they hit the menu bar
831 if ((mouse
.getType() == TMouseEvent
.Type
.MOUSE_DOWN
)
832 && (mouse
.getMouse1())
833 && (!modalWindowActive())
834 && (mouse
.getAbsoluteY() == 0)
837 for (TMenu menu
: subMenus
) {
838 menu
.setActive(false);
842 // They selected the menu, go activate it
843 for (TMenu menu
: menus
) {
844 if ((mouse
.getAbsoluteX() >= menu
.getX())
845 && (mouse
.getAbsoluteX() < menu
.getX()
846 + menu
.getTitle().length() + 2)
848 menu
.setActive(true);
851 menu
.setActive(false);
858 // See if they hit the menu bar
859 if ((mouse
.getType() == TMouseEvent
.Type
.MOUSE_MOTION
)
860 && (mouse
.getMouse1())
861 && (activeMenu
!= null)
862 && (mouse
.getAbsoluteY() == 0)
865 TMenu oldMenu
= activeMenu
;
866 for (TMenu menu
: subMenus
) {
867 menu
.setActive(false);
871 // See if we should switch menus
872 for (TMenu menu
: menus
) {
873 if ((mouse
.getAbsoluteX() >= menu
.getX())
874 && (mouse
.getAbsoluteX() < menu
.getX()
875 + menu
.getTitle().length() + 2)
877 menu
.setActive(true);
881 if (oldMenu
!= activeMenu
) {
882 // They switched menus
883 oldMenu
.setActive(false);
889 // Only switch if there are multiple windows
890 if (windows
.size() < 2) {
894 // Switch on the upclick
895 if (mouse
.getType() != TMouseEvent
.Type
.MOUSE_UP
) {
899 Collections
.sort(windows
);
900 if (windows
.get(0).isModal()) {
901 // Modal windows don't switch
905 for (TWindow window
: windows
) {
906 assert (!window
.isModal());
907 if (window
.mouseWouldHit(mouse
)) {
908 if (window
== windows
.get(0)) {
909 // Clicked on the same window, nothing to do
913 // We will be switching to another window
914 assert (windows
.get(0).getActive());
915 assert (!window
.getActive());
916 windows
.get(0).setActive(false);
917 windows
.get(0).setZ(window
.getZ());
919 window
.setActive(true);
925 // Clicked on the background, nothing to do
932 public final void closeMenu() {
933 if (activeMenu
!= null) {
934 activeMenu
.setActive(false);
936 for (TMenu menu
: subMenus
) {
937 menu
.setActive(false);
945 * Turn off a sub-menu.
947 public final void closeSubMenu() {
948 assert (activeMenu
!= null);
949 TMenu item
= subMenus
.get(subMenus
.size() - 1);
950 assert (item
!= null);
951 item
.setActive(false);
952 subMenus
.remove(subMenus
.size() - 1);
957 * Switch to the next menu.
959 * @param forward if true, then switch to the next menu in the list,
960 * otherwise switch to the previous menu in the list
962 public final void switchMenu(final boolean forward
) {
963 assert (activeMenu
!= null);
965 for (TMenu menu
: subMenus
) {
966 menu
.setActive(false);
970 for (int i
= 0; i
< menus
.size(); i
++) {
971 if (activeMenu
== menus
.get(i
)) {
973 if (i
< menus
.size() - 1) {
981 activeMenu
.setActive(false);
982 activeMenu
= menus
.get(i
);
983 activeMenu
.setActive(true);
991 * Method that TApplication subclasses can override to handle menu or
992 * posted command events.
994 * @param command command event
995 * @return if true, this event was consumed
997 protected boolean onCommand(final TCommandEvent command
) {
1000 // Default: handle cmExit
1001 if (command.equals(cmExit)) {
1002 if (messageBox("Confirmation", "Exit application?",
1003 TMessageBox.Type.YESNO).result == TMessageBox.Result.YES) {
1010 if (command.equals(cmShell)) {
1011 openTerminal(0, 0, TWindow.Flag.RESIZABLE);
1016 if (command.equals(cmTile)) {
1021 if (command.equals(cmCascade)) {
1026 if (command.equals(cmCloseAll)) {
1036 * Method that TApplication subclasses can override to handle menu
1039 * @param menu menu event
1040 * @return if true, this event was consumed
1042 protected boolean onMenu(final TMenuEvent menu
) {
1044 // Default: handle MID_EXIT
1045 if (menu
.getId() == TMenu
.MID_EXIT
) {
1048 if (messageBox("Confirmation", "Exit application?",
1049 TMessageBox.Type.YESNO).result == TMessageBox.Result.YES) {
1052 // System.err.printf("onMenu MID_EXIT result: quit = %s\n", quit);
1063 if (menu.id == TMenu.MID_SHELL) {
1064 openTerminal(0, 0, TWindow.Flag.RESIZABLE);
1070 if (menu
.getId() == TMenu
.MID_TILE
) {
1075 if (menu
.getId() == TMenu
.MID_CASCADE
) {
1080 if (menu
.getId() == TMenu
.MID_CLOSE_ALL
) {
1089 * Method that TApplication subclasses can override to handle keystrokes.
1091 * @param keypress keystroke event
1092 * @return if true, this event was consumed
1094 protected boolean onKeypress(final TKeypressEvent keypress
) {
1095 // Default: only menu shortcuts
1097 // Process Alt-F, Alt-E, etc. menu shortcut keys
1098 if (!keypress
.getKey().getIsKey()
1099 && keypress
.getKey().getAlt()
1100 && !keypress
.getKey().getCtrl()
1101 && (activeMenu
== null)
1104 assert (subMenus
.size() == 0);
1106 for (TMenu menu
: menus
) {
1107 if (Character
.toLowerCase(menu
.getMnemonic().getShortcut())
1108 == Character
.toLowerCase(keypress
.getKey().getCh())
1111 menu
.setActive(true);
1122 * Add a keyboard accelerator to the global hash.
1124 * @param item menu item this accelerator relates to
1125 * @param keypress keypress that will dispatch a TMenuEvent
1127 public final void addAccelerator(final TMenuItem item
,
1128 final TKeypress keypress
) {
1131 assert((keypress in accelerators) is null);
1132 accelerators[keypress] = item;
1137 * Recompute menu x positions based on their title length.
1139 public final void recomputeMenuX() {
1141 for (TMenu menu
: menus
) {
1143 x
+= menu
.getTitle().length() + 2;
1148 * Post an event to process and turn off the menu.
1150 * @param event new event to add to the queue
1152 public final void addMenuEvent(final TInputEvent event
) {
1153 synchronized (fillEventQueue
) {
1154 fillEventQueue
.add(event
);
1160 * Add a sub-menu to the list of open sub-menus.
1162 * @param menu sub-menu
1164 public final void addSubMenu(final TMenu menu
) {
1169 * Convenience function to add a top-level menu.
1171 * @param title menu title
1172 * @return the new menu
1174 public final TMenu
addMenu(String title
) {
1177 TMenu menu
= new TMenu(this, x
, y
, title
);
1184 * Convenience function to add a default "File" menu.
1186 * @return the new menu
1188 public final TMenu
addFileMenu() {
1189 TMenu fileMenu
= addMenu("&File");
1190 fileMenu
.addDefaultItem(TMenu
.MID_OPEN_FILE
);
1191 fileMenu
.addSeparator();
1192 fileMenu
.addDefaultItem(TMenu
.MID_SHELL
);
1193 fileMenu
.addDefaultItem(TMenu
.MID_EXIT
);
1198 * Convenience function to add a default "Edit" menu.
1200 * @return the new menu
1202 public final TMenu
addEditMenu() {
1203 TMenu editMenu
= addMenu("&Edit");
1204 editMenu
.addDefaultItem(TMenu
.MID_CUT
);
1205 editMenu
.addDefaultItem(TMenu
.MID_COPY
);
1206 editMenu
.addDefaultItem(TMenu
.MID_PASTE
);
1207 editMenu
.addDefaultItem(TMenu
.MID_CLEAR
);
1212 * Convenience function to add a default "Window" menu.
1214 * @return the new menu
1216 final public TMenu
addWindowMenu() {
1217 TMenu windowMenu
= addMenu("&Window");
1218 windowMenu
.addDefaultItem(TMenu
.MID_TILE
);
1219 windowMenu
.addDefaultItem(TMenu
.MID_CASCADE
);
1220 windowMenu
.addDefaultItem(TMenu
.MID_CLOSE_ALL
);
1221 windowMenu
.addSeparator();
1222 windowMenu
.addDefaultItem(TMenu
.MID_WINDOW_MOVE
);
1223 windowMenu
.addDefaultItem(TMenu
.MID_WINDOW_ZOOM
);
1224 windowMenu
.addDefaultItem(TMenu
.MID_WINDOW_NEXT
);
1225 windowMenu
.addDefaultItem(TMenu
.MID_WINDOW_PREVIOUS
);
1226 windowMenu
.addDefaultItem(TMenu
.MID_WINDOW_CLOSE
);
1231 * Close all open windows.
1233 private void closeAllWindows() {
1234 // Don't do anything if we are in the menu
1235 if (activeMenu
!= null) {
1238 for (TWindow window
: windows
) {
1239 closeWindow(window
);
1244 * Re-layout the open windows as non-overlapping tiles. This produces
1245 * almost the same results as Turbo Pascal 7.0's IDE.
1247 private void tileWindows() {
1248 // Don't do anything if we are in the menu
1249 if (activeMenu
!= null) {
1252 int z
= windows
.size();
1258 a
= (int)(Math
.sqrt(z
));
1262 if (((a
* b
) + c
) == z
) {
1270 int newWidth
= (getScreen().getWidth() / a
);
1271 int newHeight1
= ((getScreen().getHeight() - 1) / b
);
1272 int newHeight2
= ((getScreen().getHeight() - 1) / (b
+ c
));
1273 // System.err.printf("Z %s a %s b %s c %s newWidth %s newHeight1 %s newHeight2 %s",
1274 // z, a, b, c, newWidth, newHeight1, newHeight2);
1276 List
<TWindow
> sorted
= new LinkedList
<TWindow
>(windows
);
1277 Collections
.sort(sorted
);
1278 Collections
.reverse(sorted
);
1279 for (int i
= 0; i
< sorted
.size(); i
++) {
1280 int logicalX
= i
/ b
;
1281 int logicalY
= i
% b
;
1282 if (i
>= ((a
- 1) * b
)) {
1284 logicalY
= i
- ((a
- 1) * b
);
1287 TWindow w
= sorted
.get(i
);
1288 w
.setX(logicalX
* newWidth
);
1289 w
.setWidth(newWidth
);
1290 if (i
>= ((a
- 1) * b
)) {
1291 w
.setY((logicalY
* newHeight2
) + 1);
1292 w
.setHeight(newHeight2
);
1294 w
.setY((logicalY
* newHeight1
) + 1);
1295 w
.setHeight(newHeight1
);
1301 * Re-layout the open windows as overlapping cascaded windows.
1303 private void cascadeWindows() {
1304 // Don't do anything if we are in the menu
1305 if (activeMenu
!= null) {
1310 List
<TWindow
> sorted
= new LinkedList
<TWindow
>(windows
);
1311 Collections
.sort(sorted
);
1312 Collections
.reverse(sorted
);
1313 for (TWindow w
: sorted
) {
1318 if (x
> getScreen().getWidth()) {
1321 if (y
>= getScreen().getHeight()) {