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