Pull demo into jar
[fanfix.git] / src / jexer / TApplication.java
... / ...
CommitLineData
1/**
2 * Jexer - Java Text User Interface
3 *
4 * License: LGPLv3 or later
5 *
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.
9 *
10 * Copyright (C) 2015 Kevin Lamonte
11 *
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.
16 *
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.
21 *
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
26 * 02110-1301 USA
27 *
28 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
29 * @version 1
30 */
31package jexer;
32
33import java.io.InputStream;
34import java.io.OutputStream;
35import java.io.UnsupportedEncodingException;
36import java.util.Collections;
37import java.util.Date;
38import java.util.HashMap;
39import java.util.LinkedList;
40import java.util.List;
41import java.util.Map;
42
43import jexer.bits.CellAttributes;
44import jexer.bits.ColorTheme;
45import jexer.bits.GraphicsChars;
46import jexer.event.TCommandEvent;
47import jexer.event.TInputEvent;
48import jexer.event.TKeypressEvent;
49import jexer.event.TMenuEvent;
50import jexer.event.TMouseEvent;
51import jexer.event.TResizeEvent;
52import jexer.backend.Backend;
53import jexer.backend.ECMA48Backend;
54import jexer.io.Screen;
55import jexer.menu.TMenu;
56import jexer.menu.TMenuItem;
57import static jexer.TCommand.*;
58import static jexer.TKeypress.*;
59
60/**
61 * TApplication sets up a full Text User Interface application.
62 */
63public class TApplication {
64
65 /**
66 * Access to the physical screen, keyboard, and mouse.
67 */
68 private Backend backend;
69
70 /**
71 * Get the Screen.
72 *
73 * @return the Screen
74 */
75 public final Screen getScreen() {
76 return backend.getScreen();
77 }
78
79 /**
80 * Actual mouse coordinate X.
81 */
82 private int mouseX;
83
84 /**
85 * Actual mouse coordinate Y.
86 */
87 private int mouseY;
88
89 /**
90 * Event queue that is filled by run().
91 */
92 private List<TInputEvent> fillEventQueue;
93
94 /**
95 * Event queue that will be drained by either primary or secondary
96 * Thread.
97 */
98 private List<TInputEvent> drainEventQueue;
99
100 /**
101 * Top-level menus in this application.
102 */
103 private List<TMenu> menus;
104
105 /**
106 * Stack of activated sub-menus in this application.
107 */
108 private List<TMenu> subMenus;
109
110 /**
111 * The currently acive menu.
112 */
113 private TMenu activeMenu = null;
114
115 /**
116 * Active keyboard accelerators.
117 */
118 private Map<TKeypress, TMenuItem> accelerators;
119
120 /**
121 * Windows and widgets pull colors from this ColorTheme.
122 */
123 private ColorTheme theme;
124
125 /**
126 * Get the color theme.
127 *
128 * @return the theme
129 */
130 public final ColorTheme getTheme() {
131 return theme;
132 }
133
134 /**
135 * The top-level windows (but not menus).
136 */
137 private List<TWindow> windows;
138
139 /**
140 * Timers that are being ticked.
141 */
142 private List<TTimer> timers;
143
144 /**
145 * When true, exit the application.
146 */
147 private boolean quit = false;
148
149 /**
150 * When true, repaint the entire screen.
151 */
152 private boolean repaint = true;
153
154 /**
155 * Request full repaint on next screen refresh.
156 */
157 public final void setRepaint() {
158 repaint = true;
159 }
160
161 /**
162 * When true, just flush updates from the screen.
163 */
164 private boolean flush = false;
165
166 /**
167 * Y coordinate of the top edge of the desktop. For now this is a
168 * constant. Someday it would be nice to have a multi-line menu or
169 * toolbars.
170 */
171 private static final int desktopTop = 1;
172
173 /**
174 * Get Y coordinate of the top edge of the desktop.
175 *
176 * @return Y coordinate of the top edge of the desktop
177 */
178 public final int getDesktopTop() {
179 return desktopTop;
180 }
181
182 /**
183 * Y coordinate of the bottom edge of the desktop.
184 */
185 private int desktopBottom;
186
187 /**
188 * Get Y coordinate of the bottom edge of the desktop.
189 *
190 * @return Y coordinate of the bottom edge of the desktop
191 */
192 public final int getDesktopBottom() {
193 return desktopBottom;
194 }
195
196 /**
197 * Public constructor.
198 *
199 * @param input an InputStream connected to the remote user, or null for
200 * System.in. If System.in is used, then on non-Windows systems it will
201 * be put in raw mode; shutdown() will (blindly!) put System.in in cooked
202 * mode. input is always converted to a Reader with UTF-8 encoding.
203 * @param output an OutputStream connected to the remote user, or null
204 * for System.out. output is always converted to a Writer with UTF-8
205 * encoding.
206 * @throws UnsupportedEncodingException if an exception is thrown when
207 * creating the InputStreamReader
208 */
209 public TApplication(final InputStream input,
210 final OutputStream output) throws UnsupportedEncodingException {
211
212 backend = new ECMA48Backend(input, output);
213 theme = new ColorTheme();
214 desktopBottom = getScreen().getHeight() - 1;
215 fillEventQueue = new LinkedList<TInputEvent>();
216 drainEventQueue = new LinkedList<TInputEvent>();
217 windows = new LinkedList<TWindow>();
218 menus = new LinkedList<TMenu>();
219 subMenus = new LinkedList<TMenu>();
220 timers = new LinkedList<TTimer>();
221 accelerators = new HashMap<TKeypress, TMenuItem>();
222 }
223
224 /**
225 * Invert the cell at the mouse pointer position.
226 */
227 private void drawMouse() {
228 CellAttributes attr = getScreen().getAttrXY(mouseX, mouseY);
229 attr.setForeColor(attr.getForeColor().invert());
230 attr.setBackColor(attr.getBackColor().invert());
231 getScreen().putAttrXY(mouseX, mouseY, attr, false);
232 flush = true;
233
234 if (windows.size() == 0) {
235 repaint = true;
236 }
237 }
238
239 /**
240 * Draw everything.
241 */
242 public final void drawAll() {
243 if ((flush) && (!repaint)) {
244 backend.flushScreen();
245 flush = false;
246 return;
247 }
248
249 if (!repaint) {
250 return;
251 }
252
253 // If true, the cursor is not visible
254 boolean cursor = false;
255
256 // Start with a clean screen
257 getScreen().clear();
258
259 // Draw the background
260 CellAttributes background = theme.getColor("tapplication.background");
261 getScreen().putAll(GraphicsChars.HATCH, background);
262
263 // Draw each window in reverse Z order
264 List<TWindow> sorted = new LinkedList<TWindow>(windows);
265 Collections.sort(sorted);
266 Collections.reverse(sorted);
267 for (TWindow window: sorted) {
268 window.drawChildren();
269 }
270
271 // Draw the blank menubar line - reset the screen clipping first so
272 // it won't trim it out.
273 getScreen().resetClipping();
274 getScreen().hLineXY(0, 0, getScreen().getWidth(), ' ',
275 theme.getColor("tmenu"));
276 // Now draw the menus.
277 int x = 1;
278 for (TMenu menu: menus) {
279 CellAttributes menuColor;
280 CellAttributes menuMnemonicColor;
281 if (menu.getActive()) {
282 menuColor = theme.getColor("tmenu.highlighted");
283 menuMnemonicColor = theme.getColor("tmenu.mnemonic.highlighted");
284 } else {
285 menuColor = theme.getColor("tmenu");
286 menuMnemonicColor = theme.getColor("tmenu.mnemonic");
287 }
288 // Draw the menu title
289 getScreen().hLineXY(x, 0, menu.getTitle().length() + 2, ' ',
290 menuColor);
291 getScreen().putStrXY(x + 1, 0, menu.getTitle(), menuColor);
292 // Draw the highlight character
293 getScreen().putCharXY(x + 1 + menu.getMnemonic().getShortcutIdx(),
294 0, menu.getMnemonic().getShortcut(), menuMnemonicColor);
295
296 if (menu.getActive()) {
297 menu.drawChildren();
298 // Reset the screen clipping so we can draw the next title.
299 getScreen().resetClipping();
300 }
301 x += menu.getTitle().length() + 2;
302 }
303
304 for (TMenu menu: subMenus) {
305 // Reset the screen clipping so we can draw the next sub-menu.
306 getScreen().resetClipping();
307 menu.drawChildren();
308 }
309
310 // Draw the mouse pointer
311 drawMouse();
312
313 // Place the cursor if it is visible
314 TWidget activeWidget = null;
315 if (sorted.size() > 0) {
316 activeWidget = sorted.get(sorted.size() - 1).getActiveChild();
317 if (activeWidget.visibleCursor()) {
318 getScreen().putCursor(true, activeWidget.getCursorAbsoluteX(),
319 activeWidget.getCursorAbsoluteY());
320 cursor = true;
321 }
322 }
323
324 // Kill the cursor
325 if (!cursor) {
326 getScreen().hideCursor();
327 }
328
329 // Flush the screen contents
330 backend.flushScreen();
331
332 repaint = false;
333 flush = false;
334 }
335
336 /**
337 * Run this application until it exits.
338 */
339 public final void run() {
340 while (!quit) {
341 // Timeout is in milliseconds, so default timeout after 1 second
342 // of inactivity.
343 int timeout = getSleepTime(1000);
344
345 // See if there are any definitely events waiting to be processed
346 // or a screen redraw to do. If so, do not wait if there is no
347 // I/O coming in.
348 synchronized (drainEventQueue) {
349 if (drainEventQueue.size() > 0) {
350 timeout = 0;
351 }
352 }
353 synchronized (fillEventQueue) {
354 if (fillEventQueue.size() > 0) {
355 timeout = 0;
356 }
357 }
358
359 // Pull any pending I/O events
360 backend.getEvents(fillEventQueue, timeout);
361
362 // Dispatch each event to the appropriate handler, one at a time.
363 for (;;) {
364 TInputEvent event = null;
365 synchronized (fillEventQueue) {
366 if (fillEventQueue.size() == 0) {
367 break;
368 }
369 event = fillEventQueue.remove(0);
370 }
371 metaHandleEvent(event);
372 }
373
374 // Process timers and call doIdle()'s
375 doIdle();
376
377 // Update the screen
378 drawAll();
379 }
380
381 /*
382
383 // Shutdown the fibers
384 eventQueue.length = 0;
385 if (secondaryEventFiber !is null) {
386 assert(secondaryEventReceiver !is null);
387 secondaryEventReceiver = null;
388 if (secondaryEventFiber.state == Fiber.State.HOLD) {
389 // Wake up the secondary handler so that it can exit.
390 secondaryEventFiber.call();
391 }
392 }
393
394 if (primaryEventFiber.state == Fiber.State.HOLD) {
395 // Wake up the primary handler so that it can exit.
396 primaryEventFiber.call();
397 }
398 */
399
400 backend.shutdown();
401 }
402
403 /**
404 * Peek at certain application-level events, add to eventQueue, and wake
405 * up the consuming Thread.
406 *
407 * @param event the input event to consume
408 */
409 private void metaHandleEvent(final TInputEvent event) {
410
411 /*
412 System.err.printf(String.format("metaHandleEvents event: %s\n",
413 event)); System.err.flush();
414 */
415
416 if (quit) {
417 // Do no more processing if the application is already trying
418 // to exit.
419 return;
420 }
421
422 // Special application-wide events -------------------------------
423
424 // Abort everything
425 if (event instanceof TCommandEvent) {
426 TCommandEvent command = (TCommandEvent) event;
427 if (command.getCmd().equals(cmAbort)) {
428 quit = true;
429 return;
430 }
431 }
432
433 // Screen resize
434 if (event instanceof TResizeEvent) {
435 TResizeEvent resize = (TResizeEvent) event;
436 getScreen().setDimensions(resize.getWidth(),
437 resize.getHeight());
438 desktopBottom = getScreen().getHeight() - 1;
439 repaint = true;
440 mouseX = 0;
441 mouseY = 0;
442 return;
443 }
444
445 // Peek at the mouse position
446 if (event instanceof TMouseEvent) {
447 TMouseEvent mouse = (TMouseEvent) event;
448 if ((mouseX != mouse.getX()) || (mouseY != mouse.getY())) {
449 mouseX = mouse.getX();
450 mouseY = mouse.getY();
451 drawMouse();
452 }
453 }
454
455 // TODO: change to two separate threads
456 primaryHandleEvent(event);
457
458 /*
459
460 // Put into the main queue
461 addEvent(event);
462
463 // Have one of the two consumer Fibers peel the events off
464 // the queue.
465 if (secondaryEventFiber !is null) {
466 assert(secondaryEventFiber.state == Fiber.State.HOLD);
467
468 // Wake up the secondary handler for these events
469 secondaryEventFiber.call();
470 } else {
471 assert(primaryEventFiber.state == Fiber.State.HOLD);
472
473 // Wake up the primary handler for these events
474 primaryEventFiber.call();
475 }
476 */
477
478 }
479
480 /**
481 * Dispatch one event to the appropriate widget or application-level
482 * event handler. This is the primary event handler, it has the normal
483 * application-wide event handling.
484 *
485 * @param event the input event to consume
486 * @see #secondaryHandleEvent(TInputEvent event)
487 */
488 private void primaryHandleEvent(final TInputEvent event) {
489
490 // System.err.printf("Handle event: %s\n", event);
491
492 // Special application-wide events -----------------------------------
493
494 // Peek at the mouse position
495 if (event instanceof TMouseEvent) {
496 // See if we need to switch focus to another window or the menu
497 checkSwitchFocus((TMouseEvent) event);
498 }
499
500 // Handle menu events
501 if ((activeMenu != null) && !(event instanceof TCommandEvent)) {
502 TMenu menu = activeMenu;
503
504 if (event instanceof TMouseEvent) {
505 TMouseEvent mouse = (TMouseEvent) event;
506
507 while (subMenus.size() > 0) {
508 TMenu subMenu = subMenus.get(subMenus.size() - 1);
509 if (subMenu.mouseWouldHit(mouse)) {
510 break;
511 }
512 if ((mouse.getType() == TMouseEvent.Type.MOUSE_MOTION)
513 && (!mouse.getMouse1())
514 && (!mouse.getMouse2())
515 && (!mouse.getMouse3())
516 && (!mouse.getMouseWheelUp())
517 && (!mouse.getMouseWheelDown())
518 ) {
519 break;
520 }
521 // We navigated away from a sub-menu, so close it
522 closeSubMenu();
523 }
524
525 // Convert the mouse relative x/y to menu coordinates
526 assert (mouse.getX() == mouse.getAbsoluteX());
527 assert (mouse.getY() == mouse.getAbsoluteY());
528 if (subMenus.size() > 0) {
529 menu = subMenus.get(subMenus.size() - 1);
530 }
531 mouse.setX(mouse.getX() - menu.getX());
532 mouse.setY(mouse.getY() - menu.getY());
533 }
534 menu.handleEvent(event);
535 return;
536 }
537
538 if (event instanceof TKeypressEvent) {
539 TKeypressEvent keypress = (TKeypressEvent) event;
540
541 // See if this key matches an accelerator, and if so dispatch the
542 // menu event.
543 TKeypress keypressLowercase = keypress.getKey().toLowerCase();
544 TMenuItem item = null;
545 synchronized (accelerators) {
546 item = accelerators.get(keypressLowercase);
547 }
548 if (item != null) {
549 // Let the menu item dispatch
550 item.dispatch();
551 return;
552 } else {
553 // Handle the keypress
554 if (onKeypress(keypress)) {
555 return;
556 }
557 }
558 }
559
560 if (event instanceof TCommandEvent) {
561 if (onCommand((TCommandEvent) event)) {
562 return;
563 }
564 }
565
566 if (event instanceof TMenuEvent) {
567 if (onMenu((TMenuEvent) event)) {
568 return;
569 }
570 }
571
572 // Dispatch events to the active window -------------------------------
573 for (TWindow window: windows) {
574 if (window.getActive()) {
575 if (event instanceof TMouseEvent) {
576 TMouseEvent mouse = (TMouseEvent) event;
577 // Convert the mouse relative x/y to window coordinates
578 assert (mouse.getX() == mouse.getAbsoluteX());
579 assert (mouse.getY() == mouse.getAbsoluteY());
580 mouse.setX(mouse.getX() - window.getX());
581 mouse.setY(mouse.getY() - window.getY());
582 }
583 // System.err("TApplication dispatch event: %s\n", event);
584 window.handleEvent(event);
585 break;
586 }
587 }
588 }
589 /**
590 * Dispatch one event to the appropriate widget or application-level
591 * event handler. This is the secondary event handler used by certain
592 * special dialogs (currently TMessageBox and TFileOpenBox).
593 *
594 * @param event the input event to consume
595 * @see #primaryHandleEvent(TInputEvent event)
596 */
597 private void secondaryHandleEvent(final TInputEvent event) {
598 // TODO
599 }
600
601 /**
602 * Do stuff when there is no user input.
603 */
604 private void doIdle() {
605 // Now run any timers that have timed out
606 Date now = new Date();
607 List<TTimer> keepTimers = new LinkedList<TTimer>();
608 for (TTimer timer: timers) {
609 if (timer.getNextTick().getTime() < now.getTime()) {
610 timer.tick();
611 if (timer.recurring == true) {
612 keepTimers.add(timer);
613 }
614 } else {
615 keepTimers.add(timer);
616 }
617 }
618 timers = keepTimers;
619
620 // Call onIdle's
621 for (TWindow window: windows) {
622 window.onIdle();
623 }
624 }
625
626 /**
627 * Get the amount of time I can sleep before missing a Timer tick.
628 *
629 * @param timeout = initial (maximum) timeout
630 * @return number of milliseconds between now and the next timer event
631 */
632 protected int getSleepTime(final int timeout) {
633 Date now = new Date();
634 long sleepTime = timeout;
635 for (TTimer timer: timers) {
636 if (timer.getNextTick().getTime() < now.getTime()) {
637 return 0;
638 }
639 if ((timer.getNextTick().getTime() > now.getTime())
640 && ((timer.getNextTick().getTime() - now.getTime()) < sleepTime)
641 ) {
642 sleepTime = timer.getNextTick().getTime() - now.getTime();
643 }
644 }
645 assert (sleepTime >= 0);
646 return (int)sleepTime;
647 }
648
649 /**
650 * Close window. Note that the window's destructor is NOT called by this
651 * method, instead the GC is assumed to do the cleanup.
652 *
653 * @param window the window to remove
654 */
655 public final void closeWindow(final TWindow window) {
656 int z = window.getZ();
657 window.setZ(-1);
658 Collections.sort(windows);
659 windows.remove(0);
660 TWindow activeWindow = null;
661 for (TWindow w: windows) {
662 if (w.getZ() > z) {
663 w.setZ(w.getZ() - 1);
664 if (w.getZ() == 0) {
665 w.setActive(true);
666 assert (activeWindow == null);
667 activeWindow = w;
668 } else {
669 w.setActive(false);
670 }
671 }
672 }
673
674 // Perform window cleanup
675 window.onClose();
676
677 // Refresh screen
678 repaint = true;
679
680 /*
681 TODO
682
683
684 // Check if we are closing a TMessageBox or similar
685 if (secondaryEventReceiver !is null) {
686 assert(secondaryEventFiber !is null);
687
688 // Do not send events to the secondaryEventReceiver anymore, the
689 // window is closed.
690 secondaryEventReceiver = null;
691
692 // Special case: if this is called while executing on a
693 // secondaryEventFiber, call it so that widgetEventHandler() can
694 // terminate.
695 if (secondaryEventFiber.state == Fiber.State.HOLD) {
696 secondaryEventFiber.call();
697 }
698 secondaryEventFiber = null;
699
700 // Unfreeze the logic in handleEvent()
701 if (primaryEventFiber.state == Fiber.State.HOLD) {
702 primaryEventFiber.call();
703 }
704 }
705 */
706 }
707
708 /**
709 * Switch to the next window.
710 *
711 * @param forward if true, then switch to the next window in the list,
712 * otherwise switch to the previous window in the list
713 */
714 public final void switchWindow(final boolean forward) {
715 // Only switch if there are multiple windows
716 if (windows.size() < 2) {
717 return;
718 }
719
720 // Swap z/active between active window and the next in the list
721 int activeWindowI = -1;
722 for (int i = 0; i < windows.size(); i++) {
723 if (windows.get(i).getActive()) {
724 activeWindowI = i;
725 break;
726 }
727 }
728 assert (activeWindowI >= 0);
729
730 // Do not switch if a window is modal
731 if (windows.get(activeWindowI).isModal()) {
732 return;
733 }
734
735 int nextWindowI;
736 if (forward) {
737 nextWindowI = (activeWindowI + 1) % windows.size();
738 } else {
739 if (activeWindowI == 0) {
740 nextWindowI = windows.size() - 1;
741 } else {
742 nextWindowI = activeWindowI - 1;
743 }
744 }
745 windows.get(activeWindowI).setActive(false);
746 windows.get(activeWindowI).setZ(windows.get(nextWindowI).getZ());
747 windows.get(nextWindowI).setZ(0);
748 windows.get(nextWindowI).setActive(true);
749
750 // Refresh
751 repaint = true;
752 }
753
754 /**
755 * Add a window to my window list and make it active.
756 *
757 * @param window new window to add
758 */
759 public final void addWindow(final TWindow window) {
760 // Do not allow a modal window to spawn a non-modal window
761 if ((windows.size() > 0) && (windows.get(0).isModal())) {
762 assert (window.isModal());
763 }
764 for (TWindow w: windows) {
765 w.setActive(false);
766 w.setZ(w.getZ() + 1);
767 }
768 windows.add(window);
769 window.setActive(true);
770 window.setZ(0);
771 }
772
773 /**
774 * Check if there is a system-modal window on top.
775 *
776 * @return true if the active window is modal
777 */
778 private boolean modalWindowActive() {
779 if (windows.size() == 0) {
780 return false;
781 }
782 return windows.get(windows.size() - 1).isModal();
783 }
784
785 /**
786 * Check if a mouse event would hit either the active menu or any open
787 * sub-menus.
788 *
789 * @param mouse mouse event
790 * @return true if the mouse would hit the active menu or an open
791 * sub-menu
792 */
793 private boolean mouseOnMenu(final TMouseEvent mouse) {
794 assert (activeMenu != null);
795 List<TMenu> menus = new LinkedList<TMenu>(subMenus);
796 Collections.reverse(menus);
797 for (TMenu menu: menus) {
798 if (menu.mouseWouldHit(mouse)) {
799 return true;
800 }
801 }
802 return activeMenu.mouseWouldHit(mouse);
803 }
804
805 /**
806 * See if we need to switch window or activate the menu based on
807 * a mouse click.
808 *
809 * @param mouse mouse event
810 */
811 private void checkSwitchFocus(final TMouseEvent mouse) {
812
813 if ((mouse.getType() == TMouseEvent.Type.MOUSE_DOWN)
814 && (activeMenu != null)
815 && (mouse.getAbsoluteY() != 0)
816 && (!mouseOnMenu(mouse))
817 ) {
818 // They clicked outside the active menu, turn it off
819 activeMenu.setActive(false);
820 activeMenu = null;
821 for (TMenu menu: subMenus) {
822 menu.setActive(false);
823 }
824 subMenus.clear();
825 // Continue checks
826 }
827
828 // See if they hit the menu bar
829 if ((mouse.getType() == TMouseEvent.Type.MOUSE_DOWN)
830 && (mouse.getMouse1())
831 && (!modalWindowActive())
832 && (mouse.getAbsoluteY() == 0)
833 ) {
834
835 for (TMenu menu: subMenus) {
836 menu.setActive(false);
837 }
838 subMenus.clear();
839
840 // They selected the menu, go activate it
841 for (TMenu menu: menus) {
842 if ((mouse.getAbsoluteX() >= menu.getX())
843 && (mouse.getAbsoluteX() < menu.getX()
844 + menu.getTitle().length() + 2)
845 ) {
846 menu.setActive(true);
847 activeMenu = menu;
848 } else {
849 menu.setActive(false);
850 }
851 }
852 repaint = true;
853 return;
854 }
855
856 // See if they hit the menu bar
857 if ((mouse.getType() == TMouseEvent.Type.MOUSE_MOTION)
858 && (mouse.getMouse1())
859 && (activeMenu != null)
860 && (mouse.getAbsoluteY() == 0)
861 ) {
862
863 TMenu oldMenu = activeMenu;
864 for (TMenu menu: subMenus) {
865 menu.setActive(false);
866 }
867 subMenus.clear();
868
869 // See if we should switch menus
870 for (TMenu menu: menus) {
871 if ((mouse.getAbsoluteX() >= menu.getX())
872 && (mouse.getAbsoluteX() < menu.getX()
873 + menu.getTitle().length() + 2)
874 ) {
875 menu.setActive(true);
876 activeMenu = menu;
877 }
878 }
879 if (oldMenu != activeMenu) {
880 // They switched menus
881 oldMenu.setActive(false);
882 }
883 repaint = true;
884 return;
885 }
886
887 // Only switch if there are multiple windows
888 if (windows.size() < 2) {
889 return;
890 }
891
892 // Switch on the upclick
893 if (mouse.getType() != TMouseEvent.Type.MOUSE_UP) {
894 return;
895 }
896
897 Collections.sort(windows);
898 if (windows.get(0).isModal()) {
899 // Modal windows don't switch
900 return;
901 }
902
903 for (TWindow window: windows) {
904 assert (!window.isModal());
905 if (window.mouseWouldHit(mouse)) {
906 if (window == windows.get(0)) {
907 // Clicked on the same window, nothing to do
908 return;
909 }
910
911 // We will be switching to another window
912 assert (windows.get(0).getActive());
913 assert (!window.getActive());
914 windows.get(0).setActive(false);
915 windows.get(0).setZ(window.getZ());
916 window.setZ(0);
917 window.setActive(true);
918 repaint = true;
919 return;
920 }
921 }
922
923 // Clicked on the background, nothing to do
924 return;
925 }
926
927 /**
928 * Turn off the menu.
929 */
930 public final void closeMenu() {
931 if (activeMenu != null) {
932 activeMenu.setActive(false);
933 activeMenu = null;
934 for (TMenu menu: subMenus) {
935 menu.setActive(false);
936 }
937 subMenus.clear();
938 }
939 repaint = true;
940 }
941
942 /**
943 * Turn off a sub-menu.
944 */
945 public final void closeSubMenu() {
946 assert (activeMenu != null);
947 TMenu item = subMenus.get(subMenus.size() - 1);
948 assert (item != null);
949 item.setActive(false);
950 subMenus.remove(subMenus.size() - 1);
951 repaint = true;
952 }
953
954 /**
955 * Switch to the next menu.
956 *
957 * @param forward if true, then switch to the next menu in the list,
958 * otherwise switch to the previous menu in the list
959 */
960 public final void switchMenu(final boolean forward) {
961 assert (activeMenu != null);
962
963 for (TMenu menu: subMenus) {
964 menu.setActive(false);
965 }
966 subMenus.clear();
967
968 for (int i = 0; i < menus.size(); i++) {
969 if (activeMenu == menus.get(i)) {
970 if (forward) {
971 if (i < menus.size() - 1) {
972 i++;
973 }
974 } else {
975 if (i > 0) {
976 i--;
977 }
978 }
979 activeMenu.setActive(false);
980 activeMenu = menus.get(i);
981 activeMenu.setActive(true);
982 repaint = true;
983 return;
984 }
985 }
986 }
987
988 /**
989 * Method that TApplication subclasses can override to handle menu or
990 * posted command events.
991 *
992 * @param command command event
993 * @return if true, this event was consumed
994 */
995 protected boolean onCommand(final TCommandEvent command) {
996 /*
997 TODO
998 // Default: handle cmExit
999 if (command.equals(cmExit)) {
1000 if (messageBox("Confirmation", "Exit application?",
1001 TMessageBox.Type.YESNO).result == TMessageBox.Result.YES) {
1002 quit = true;
1003 }
1004 repaint = true;
1005 return true;
1006 }
1007
1008 if (command.equals(cmShell)) {
1009 openTerminal(0, 0, TWindow.Flag.RESIZABLE);
1010 repaint = true;
1011 return true;
1012 }
1013
1014 if (command.equals(cmTile)) {
1015 tileWindows();
1016 repaint = true;
1017 return true;
1018 }
1019 if (command.equals(cmCascade)) {
1020 cascadeWindows();
1021 repaint = true;
1022 return true;
1023 }
1024 if (command.equals(cmCloseAll)) {
1025 closeAllWindows();
1026 repaint = true;
1027 return true;
1028 }
1029 */
1030 return false;
1031 }
1032
1033 /**
1034 * Method that TApplication subclasses can override to handle menu
1035 * events.
1036 *
1037 * @param menu menu event
1038 * @return if true, this event was consumed
1039 */
1040 protected boolean onMenu(final TMenuEvent menu) {
1041
1042 // Default: handle MID_EXIT
1043 if (menu.getId() == TMenu.MID_EXIT) {
1044 /*
1045 TODO
1046 if (messageBox("Confirmation", "Exit application?",
1047 TMessageBox.Type.YESNO).result == TMessageBox.Result.YES) {
1048 quit = true;
1049 }
1050 // System.err.printf("onMenu MID_EXIT result: quit = %s\n", quit);
1051 repaint = true;
1052 return true;
1053 */
1054 quit = true;
1055 repaint = true;
1056 return true;
1057 }
1058
1059 /*
1060 TODO
1061 if (menu.id == TMenu.MID_SHELL) {
1062 openTerminal(0, 0, TWindow.Flag.RESIZABLE);
1063 repaint = true;
1064 return true;
1065 }
1066 */
1067
1068 if (menu.getId() == TMenu.MID_TILE) {
1069 tileWindows();
1070 repaint = true;
1071 return true;
1072 }
1073 if (menu.getId() == TMenu.MID_CASCADE) {
1074 cascadeWindows();
1075 repaint = true;
1076 return true;
1077 }
1078 if (menu.getId() == TMenu.MID_CLOSE_ALL) {
1079 closeAllWindows();
1080 repaint = true;
1081 return true;
1082 }
1083 return false;
1084 }
1085
1086 /**
1087 * Method that TApplication subclasses can override to handle keystrokes.
1088 *
1089 * @param keypress keystroke event
1090 * @return if true, this event was consumed
1091 */
1092 protected boolean onKeypress(final TKeypressEvent keypress) {
1093 // Default: only menu shortcuts
1094
1095 // Process Alt-F, Alt-E, etc. menu shortcut keys
1096 if (!keypress.getKey().getIsKey()
1097 && keypress.getKey().getAlt()
1098 && !keypress.getKey().getCtrl()
1099 && (activeMenu == null)
1100 ) {
1101
1102 assert (subMenus.size() == 0);
1103
1104 for (TMenu menu: menus) {
1105 if (Character.toLowerCase(menu.getMnemonic().getShortcut())
1106 == Character.toLowerCase(keypress.getKey().getCh())
1107 ) {
1108 activeMenu = menu;
1109 menu.setActive(true);
1110 repaint = true;
1111 return true;
1112 }
1113 }
1114 }
1115
1116 return false;
1117 }
1118
1119 /**
1120 * Add a keyboard accelerator to the global hash.
1121 *
1122 * @param item menu item this accelerator relates to
1123 * @param keypress keypress that will dispatch a TMenuEvent
1124 */
1125 public final void addAccelerator(final TMenuItem item,
1126 final TKeypress keypress) {
1127
1128 // System.err.printf("addAccelerator: key %s item %s\n", keypress, item);
1129
1130 synchronized (accelerators) {
1131 assert (accelerators.get(keypress) == null);
1132 accelerators.put(keypress, item);
1133 }
1134 }
1135
1136 /**
1137 * Recompute menu x positions based on their title length.
1138 */
1139 public final void recomputeMenuX() {
1140 int x = 0;
1141 for (TMenu menu: menus) {
1142 menu.setX(x);
1143 x += menu.getTitle().length() + 2;
1144 }
1145 }
1146
1147 /**
1148 * Post an event to process and turn off the menu.
1149 *
1150 * @param event new event to add to the queue
1151 */
1152 public final void addMenuEvent(final TInputEvent event) {
1153 synchronized (fillEventQueue) {
1154 fillEventQueue.add(event);
1155 }
1156 closeMenu();
1157 }
1158
1159 /**
1160 * Add a sub-menu to the list of open sub-menus.
1161 *
1162 * @param menu sub-menu
1163 */
1164 public final void addSubMenu(final TMenu menu) {
1165 subMenus.add(menu);
1166 }
1167
1168 /**
1169 * Convenience function to add a top-level menu.
1170 *
1171 * @param title menu title
1172 * @return the new menu
1173 */
1174 public final TMenu addMenu(String title) {
1175 int x = 0;
1176 int y = 0;
1177 TMenu menu = new TMenu(this, x, y, title);
1178 menus.add(menu);
1179 recomputeMenuX();
1180 return menu;
1181 }
1182
1183 /**
1184 * Convenience function to add a default "File" menu.
1185 *
1186 * @return the new menu
1187 */
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);
1194 return fileMenu;
1195 }
1196
1197 /**
1198 * Convenience function to add a default "Edit" menu.
1199 *
1200 * @return the new menu
1201 */
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);
1208 return editMenu;
1209 }
1210
1211 /**
1212 * Convenience function to add a default "Window" menu.
1213 *
1214 * @return the new menu
1215 */
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);
1227 return windowMenu;
1228 }
1229
1230 /**
1231 * Close all open windows.
1232 */
1233 private void closeAllWindows() {
1234 // Don't do anything if we are in the menu
1235 if (activeMenu != null) {
1236 return;
1237 }
1238 for (TWindow window: windows) {
1239 closeWindow(window);
1240 }
1241 }
1242
1243 /**
1244 * Re-layout the open windows as non-overlapping tiles. This produces
1245 * almost the same results as Turbo Pascal 7.0's IDE.
1246 */
1247 private void tileWindows() {
1248 // Don't do anything if we are in the menu
1249 if (activeMenu != null) {
1250 return;
1251 }
1252 int z = windows.size();
1253 if (z == 0) {
1254 return;
1255 }
1256 int a = 0;
1257 int b = 0;
1258 a = (int)(Math.sqrt(z));
1259 int c = 0;
1260 while (c < a) {
1261 b = (z - c) / a;
1262 if (((a * b) + c) == z) {
1263 break;
1264 }
1265 c++;
1266 }
1267 assert (a > 0);
1268 assert (b > 0);
1269 assert (c < a);
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);
1275
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)) {
1283 logicalX = a - 1;
1284 logicalY = i - ((a - 1) * b);
1285 }
1286
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);
1293 } else {
1294 w.setY((logicalY * newHeight1) + 1);
1295 w.setHeight(newHeight1);
1296 }
1297 }
1298 }
1299
1300 /**
1301 * Re-layout the open windows as overlapping cascaded windows.
1302 */
1303 private void cascadeWindows() {
1304 // Don't do anything if we are in the menu
1305 if (activeMenu != null) {
1306 return;
1307 }
1308 int x = 0;
1309 int y = 1;
1310 List<TWindow> sorted = new LinkedList<TWindow>(windows);
1311 Collections.sort(sorted);
1312 Collections.reverse(sorted);
1313 for (TWindow window: sorted) {
1314 window.setX(x);
1315 window.setY(y);
1316 x++;
1317 y++;
1318 if (x > getScreen().getWidth()) {
1319 x = 0;
1320 }
1321 if (y >= getScreen().getHeight()) {
1322 y = 1;
1323 }
1324 }
1325 }
1326
1327 /**
1328 * Convenience function to add a timer.
1329 *
1330 * @param duration number of milliseconds to wait between ticks
1331 * @param recurring if true, re-schedule this timer after every tick
1332 * @param action function to call when button is pressed
1333 */
1334 public final TTimer addTimer(final long duration, final boolean recurring,
1335 final TAction action) {
1336
1337 TTimer timer = new TTimer(duration, recurring, action);
1338 synchronized (timers) {
1339 timers.add(timer);
1340 }
1341 return timer;
1342 }
1343
1344 /**
1345 * Convenience function to remove a timer.
1346 *
1347 * @param timer timer to remove
1348 */
1349 public final void removeTimer(final TTimer timer) {
1350 synchronized (timers) {
1351 timers.remove(timer);
1352 }
1353 }
1354
1355}