Fix Swing triple-buffering
[fanfix.git] / src / jexer / TApplication.java
CommitLineData
daa4106c 1/*
7b5261bc 2 * Jexer - Java Text User Interface
7d4115a5 3 *
e16dda65 4 * The MIT License (MIT)
7d4115a5 5 *
e16dda65 6 * Copyright (C) 2016 Kevin Lamonte
7d4115a5 7 *
e16dda65
KL
8 * Permission is hereby granted, free of charge, to any person obtaining a
9 * copy of this software and associated documentation files (the "Software"),
10 * to deal in the Software without restriction, including without limitation
11 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
12 * and/or sell copies of the Software, and to permit persons to whom the
13 * Software is furnished to do so, subject to the following conditions:
7d4115a5 14 *
e16dda65
KL
15 * The above copyright notice and this permission notice shall be included in
16 * all copies or substantial portions of the Software.
7d4115a5 17 *
e16dda65
KL
18 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
21 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
24 * DEALINGS IN THE SOFTWARE.
7b5261bc
KL
25 *
26 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
27 * @version 1
7d4115a5
KL
28 */
29package jexer;
30
4328bb42 31import java.io.InputStream;
0d47c546 32import java.io.IOException;
4328bb42 33import java.io.OutputStream;
6985c572
KL
34import java.io.PrintWriter;
35import java.io.Reader;
4328bb42 36import java.io.UnsupportedEncodingException;
a06459bd 37import java.util.Collections;
d502a0e9 38import java.util.Date;
e826b451 39import java.util.HashMap;
c6940ed9 40import java.util.ArrayList;
4328bb42
KL
41import java.util.LinkedList;
42import java.util.List;
e826b451 43import java.util.Map;
4328bb42
KL
44
45import jexer.bits.CellAttributes;
46import jexer.bits.ColorTheme;
47import jexer.bits.GraphicsChars;
48import jexer.event.TCommandEvent;
49import jexer.event.TInputEvent;
50import jexer.event.TKeypressEvent;
fca67db0 51import jexer.event.TMenuEvent;
4328bb42
KL
52import jexer.event.TMouseEvent;
53import jexer.event.TResizeEvent;
54import jexer.backend.Backend;
a4406f4e 55import jexer.backend.SwingBackend;
4328bb42 56import jexer.backend.ECMA48Backend;
48e27807 57import jexer.io.Screen;
928811d8
KL
58import jexer.menu.TMenu;
59import jexer.menu.TMenuItem;
4328bb42 60import static jexer.TCommand.*;
4328bb42 61
7d4115a5
KL
62/**
63 * TApplication sets up a full Text User Interface application.
64 */
a4406f4e 65public class TApplication implements Runnable {
7d4115a5 66
99144c71
KL
67 /**
68 * If true, emit thread stuff to System.err.
69 */
70 private static final boolean debugThreads = false;
71
a83fea2b
KL
72 /**
73 * If true, emit events being processed to System.err.
74 */
75 private static final boolean debugEvents = false;
76
a4406f4e
KL
77 /**
78 * Two backend types are available.
79 */
80 public static enum BackendType {
81 /**
82 * A Swing JFrame.
83 */
84 SWING,
85
86 /**
87 * An ECMA48 / ANSI X3.64 / XTERM style terminal.
88 */
89 ECMA48,
90
91 /**
329fd62e 92 * Synonym for ECMA48.
a4406f4e
KL
93 */
94 XTERM
95 }
96
c6940ed9
KL
97 /**
98 * WidgetEventHandler is the main event consumer loop. There are at most
99 * two such threads in existence: the primary for normal case and a
100 * secondary that is used for TMessageBox, TInputBox, and similar.
101 */
102 private class WidgetEventHandler implements Runnable {
103 /**
104 * The main application.
105 */
106 private TApplication application;
107
108 /**
109 * Whether or not this WidgetEventHandler is the primary or secondary
110 * thread.
111 */
112 private boolean primary = true;
113
114 /**
115 * Public constructor.
116 *
117 * @param application the main application
118 * @param primary if true, this is the primary event handler thread
119 */
120 public WidgetEventHandler(final TApplication application,
121 final boolean primary) {
122
123 this.application = application;
124 this.primary = primary;
125 }
126
127 /**
128 * The consumer loop.
129 */
130 public void run() {
131
132 // Loop forever
133 while (!application.quit) {
134
135 // Wait until application notifies me
136 while (!application.quit) {
137 try {
138 synchronized (application.drainEventQueue) {
139 if (application.drainEventQueue.size() > 0) {
140 break;
141 }
142 }
92554d64
KL
143
144 synchronized (this) {
bd8d51fa
KL
145 if (debugThreads) {
146 System.err.printf("%s %s sleep\n", this,
147 primary ? "primary" : "secondary");
148 }
92554d64
KL
149
150 this.wait();
151
bd8d51fa
KL
152 if (debugThreads) {
153 System.err.printf("%s %s AWAKE\n", this,
154 primary ? "primary" : "secondary");
155 }
92554d64 156
c6940ed9
KL
157 if ((!primary)
158 && (application.secondaryEventReceiver == null)
159 ) {
92554d64
KL
160 // Secondary thread, emergency exit. If we
161 // got here then something went wrong with
162 // the handoff between yield() and
163 // closeWindow().
92554d64
KL
164 synchronized (application.primaryEventHandler) {
165 application.primaryEventHandler.notify();
166 }
167 application.secondaryEventHandler = null;
bd8d51fa
KL
168 throw new RuntimeException(
169 "secondary exited at wrong time");
c6940ed9
KL
170 }
171 break;
172 }
173 } catch (InterruptedException e) {
174 // SQUASH
175 }
176 }
177
ef368bd0
KL
178 // Wait for drawAll() or doIdle() to be done, then handle the
179 // events.
180 boolean oldLock = lockHandleEvent();
181 assert (oldLock == false);
182
c6940ed9
KL
183 // Pull all events off the queue
184 for (;;) {
185 TInputEvent event = null;
186 synchronized (application.drainEventQueue) {
187 if (application.drainEventQueue.size() == 0) {
188 break;
189 }
190 event = application.drainEventQueue.remove(0);
191 }
bd8d51fa 192 application.repaint = true;
c6940ed9
KL
193 if (primary) {
194 primaryHandleEvent(event);
195 } else {
196 secondaryHandleEvent(event);
197 }
198 if ((!primary)
199 && (application.secondaryEventReceiver == null)
200 ) {
99144c71
KL
201 // Secondary thread, time to exit.
202
203 // DO NOT UNLOCK. Primary thread just came back from
204 // primaryHandleEvent() and will unlock in the else
92554d64
KL
205 // block below. Just wake it up.
206 synchronized (application.primaryEventHandler) {
207 application.primaryEventHandler.notify();
208 }
209 // Now eliminate my reference so that
210 // wakeEventHandler() resumes working on the primary.
211 application.secondaryEventHandler = null;
212
213 // All done!
c6940ed9
KL
214 return;
215 }
92554d64
KL
216 } // for (;;)
217
ef368bd0
KL
218 // Unlock. Either I am primary thread, or I am secondary
219 // thread and still running.
220 oldLock = unlockHandleEvent();
221 assert (oldLock == true);
222
92554d64
KL
223 // I have done some work of some kind. Tell the main run()
224 // loop to wake up now.
225 synchronized (application) {
226 application.notify();
c6940ed9 227 }
92554d64 228
c6940ed9
KL
229 } // while (true) (main runnable loop)
230 }
231 }
232
233 /**
234 * The primary event handler thread.
235 */
92554d64 236 private volatile WidgetEventHandler primaryEventHandler;
c6940ed9
KL
237
238 /**
239 * The secondary event handler thread.
240 */
92554d64 241 private volatile WidgetEventHandler secondaryEventHandler;
c6940ed9
KL
242
243 /**
244 * The widget receiving events from the secondary event handler thread.
245 */
92554d64 246 private volatile TWidget secondaryEventReceiver;
c6940ed9 247
99144c71
KL
248 /**
249 * Spinlock for the primary and secondary event handlers.
250 * WidgetEventHandler.run() is responsible for setting this value.
251 */
252 private volatile boolean insideHandleEvent = false;
253
92554d64
KL
254 /**
255 * Wake the sleeping active event handler.
256 */
257 private void wakeEventHandler() {
258 if (secondaryEventHandler != null) {
259 synchronized (secondaryEventHandler) {
260 secondaryEventHandler.notify();
261 }
262 } else {
263 assert (primaryEventHandler != null);
264 synchronized (primaryEventHandler) {
265 primaryEventHandler.notify();
266 }
267 }
268 }
269
99144c71
KL
270 /**
271 * Set the insideHandleEvent flag to true. lockoutEventHandlers() will
272 * spin indefinitely until unlockHandleEvent() is called.
273 *
274 * @return the old value of insideHandleEvent
275 */
276 private boolean lockHandleEvent() {
277 if (debugThreads) {
278 System.err.printf(" >> lockHandleEvent(): oldValue %s",
279 insideHandleEvent);
280 }
281 boolean oldValue = true;
282
283 synchronized (this) {
284 // Wait for TApplication.run() to finish using the global state
285 // before allowing further event processing.
ef368bd0
KL
286 while (lockoutHandleEvent == true) {
287 try {
288 // Backoff so that the backend can finish its work.
289 Thread.sleep(5);
290 } catch (InterruptedException e) {
291 // SQUASH
292 }
293 }
99144c71
KL
294
295 oldValue = insideHandleEvent;
296 insideHandleEvent = true;
297 }
298
299 if (debugThreads) {
300 System.err.printf(" ***\n");
301 }
302 return oldValue;
303 }
304
305 /**
306 * Set the insideHandleEvent flag to false. lockoutEventHandlers() will
307 * spin indefinitely until unlockHandleEvent() is called.
308 *
309 * @return the old value of insideHandleEvent
310 */
311 private boolean unlockHandleEvent() {
312 if (debugThreads) {
313 System.err.printf(" << unlockHandleEvent(): oldValue %s\n",
314 insideHandleEvent);
315 }
316 synchronized (this) {
317 boolean oldValue = insideHandleEvent;
318 insideHandleEvent = false;
319 return oldValue;
320 }
321 }
322
323 /**
324 * Spinlock for the primary and secondary event handlers. When true, the
325 * event handlers will spinlock wait before calling handleEvent().
326 */
327 private volatile boolean lockoutHandleEvent = false;
328
329 /**
330 * TApplication.run() needs to be able rely on the global data structures
331 * being intact when calling doIdle() and drawAll(). Tell the event
332 * handlers to wait for an unlock before handling their events.
333 */
334 private void stopEventHandlers() {
335 if (debugThreads) {
336 System.err.printf(">> stopEventHandlers()");
337 }
338
339 lockoutHandleEvent = true;
340 // Wait for the last event to finish processing before returning
341 // control to TApplication.run().
ef368bd0
KL
342 while (insideHandleEvent == true) {
343 try {
344 // Backoff so that the event handler can finish its work.
345 Thread.sleep(1);
346 } catch (InterruptedException e) {
347 // SQUASH
348 }
349 }
99144c71
KL
350
351 if (debugThreads) {
352 System.err.printf(" XXX\n");
353 }
354 }
355
356 /**
357 * TApplication.run() needs to be able rely on the global data structures
358 * being intact when calling doIdle() and drawAll(). Tell the event
359 * handlers that it is now OK to handle their events.
360 */
361 private void startEventHandlers() {
362 if (debugThreads) {
363 System.err.printf("<< startEventHandlers()\n");
364 }
365 lockoutHandleEvent = false;
366 }
367
7d4115a5 368 /**
4328bb42
KL
369 * Access to the physical screen, keyboard, and mouse.
370 */
7b5261bc 371 private Backend backend;
4328bb42 372
48e27807
KL
373 /**
374 * Get the Screen.
375 *
376 * @return the Screen
377 */
378 public final Screen getScreen() {
379 return backend.getScreen();
380 }
381
4328bb42 382 /**
7b5261bc 383 * Actual mouse coordinate X.
4328bb42
KL
384 */
385 private int mouseX;
386
387 /**
7b5261bc 388 * Actual mouse coordinate Y.
4328bb42
KL
389 */
390 private int mouseY;
391
bd8d51fa
KL
392 /**
393 * Old version of mouse coordinate X.
394 */
395 private int oldMouseX;
396
397 /**
398 * Old version mouse coordinate Y.
399 */
400 private int oldMouseY;
401
4328bb42 402 /**
8e688b92 403 * Event queue that is filled by run().
4328bb42 404 */
8e688b92
KL
405 private List<TInputEvent> fillEventQueue;
406
407 /**
408 * Event queue that will be drained by either primary or secondary
409 * Thread.
410 */
411 private List<TInputEvent> drainEventQueue;
4328bb42 412
fca67db0
KL
413 /**
414 * Top-level menus in this application.
415 */
416 private List<TMenu> menus;
417
418 /**
419 * Stack of activated sub-menus in this application.
420 */
421 private List<TMenu> subMenus;
422
423 /**
424 * The currently acive menu.
425 */
426 private TMenu activeMenu = null;
427
e826b451
KL
428 /**
429 * Active keyboard accelerators.
430 */
431 private Map<TKeypress, TMenuItem> accelerators;
432
efb7af1f
KL
433 /**
434 * All menu items.
435 */
436 private List<TMenuItem> menuItems;
437
4328bb42
KL
438 /**
439 * Windows and widgets pull colors from this ColorTheme.
440 */
7b5261bc
KL
441 private ColorTheme theme;
442
443 /**
444 * Get the color theme.
445 *
446 * @return the theme
447 */
448 public final ColorTheme getTheme() {
449 return theme;
450 }
4328bb42 451
a06459bd
KL
452 /**
453 * The top-level windows (but not menus).
454 */
fca67db0 455 private List<TWindow> windows;
a06459bd 456
d502a0e9
KL
457 /**
458 * Timers that are being ticked.
459 */
460 private List<TTimer> timers;
461
4328bb42
KL
462 /**
463 * When true, exit the application.
464 */
92554d64 465 private volatile boolean quit = false;
4328bb42
KL
466
467 /**
468 * When true, repaint the entire screen.
469 */
92554d64 470 private volatile boolean repaint = true;
4328bb42 471
4328bb42 472 /**
7b5261bc
KL
473 * Y coordinate of the top edge of the desktop. For now this is a
474 * constant. Someday it would be nice to have a multi-line menu or
475 * toolbars.
4328bb42 476 */
48e27807
KL
477 private static final int desktopTop = 1;
478
479 /**
480 * Get Y coordinate of the top edge of the desktop.
481 *
482 * @return Y coordinate of the top edge of the desktop
483 */
484 public final int getDesktopTop() {
485 return desktopTop;
486 }
4328bb42
KL
487
488 /**
489 * Y coordinate of the bottom edge of the desktop.
490 */
48e27807
KL
491 private int desktopBottom;
492
493 /**
494 * Get Y coordinate of the bottom edge of the desktop.
495 *
496 * @return Y coordinate of the bottom edge of the desktop
497 */
498 public final int getDesktopBottom() {
499 return desktopBottom;
500 }
4328bb42
KL
501
502 /**
503 * Public constructor.
504 *
a4406f4e
KL
505 * @param backendType BackendType.XTERM, BackendType.ECMA48 or
506 * BackendType.SWING
507 * @throws UnsupportedEncodingException if an exception is thrown when
508 * creating the InputStreamReader
509 */
510 public TApplication(final BackendType backendType)
511 throws UnsupportedEncodingException {
512
513 switch (backendType) {
514 case SWING:
515 backend = new SwingBackend(this);
516 break;
517 case XTERM:
518 // Fall through...
519 case ECMA48:
520 backend = new ECMA48Backend(this, null, null);
329fd62e
KL
521 break;
522 default:
523 throw new IllegalArgumentException("Invalid backend type: "
524 + backendType);
a4406f4e
KL
525 }
526 TApplicationImpl();
527 }
528
529 /**
530 * Public constructor. The backend type will be BackendType.ECMA48.
531 *
4328bb42
KL
532 * @param input an InputStream connected to the remote user, or null for
533 * System.in. If System.in is used, then on non-Windows systems it will
534 * be put in raw mode; shutdown() will (blindly!) put System.in in cooked
535 * mode. input is always converted to a Reader with UTF-8 encoding.
536 * @param output an OutputStream connected to the remote user, or null
537 * for System.out. output is always converted to a Writer with UTF-8
538 * encoding.
7b5261bc
KL
539 * @throws UnsupportedEncodingException if an exception is thrown when
540 * creating the InputStreamReader
4328bb42 541 */
7b5261bc
KL
542 public TApplication(final InputStream input,
543 final OutputStream output) throws UnsupportedEncodingException {
4328bb42 544
a4406f4e
KL
545 backend = new ECMA48Backend(this, input, output);
546 TApplicationImpl();
547 }
30bd4abd 548
6985c572
KL
549 /**
550 * Public constructor. The backend type will be BackendType.ECMA48.
551 *
552 * @param input the InputStream underlying 'reader'. Its available()
553 * method is used to determine if reader.read() will block or not.
554 * @param reader a Reader connected to the remote user.
555 * @param writer a PrintWriter connected to the remote user.
556 * @param setRawMode if true, set System.in into raw mode with stty.
557 * This should in general not be used. It is here solely for Demo3,
558 * which uses System.in.
559 * @throws IllegalArgumentException if input, reader, or writer are null.
560 */
561 public TApplication(final InputStream input, final Reader reader,
562 final PrintWriter writer, final boolean setRawMode) {
563
564 backend = new ECMA48Backend(this, input, reader, writer, setRawMode);
565 TApplicationImpl();
566 }
567
568 /**
569 * Public constructor. The backend type will be BackendType.ECMA48.
570 *
571 * @param input the InputStream underlying 'reader'. Its available()
572 * method is used to determine if reader.read() will block or not.
573 * @param reader a Reader connected to the remote user.
574 * @param writer a PrintWriter connected to the remote user.
575 * @throws IllegalArgumentException if input, reader, or writer are null.
576 */
577 public TApplication(final InputStream input, final Reader reader,
578 final PrintWriter writer) {
579
580 this(input, reader, writer, false);
581 }
582
a4406f4e
KL
583 /**
584 * Public constructor. This hook enables use with new non-Jexer
585 * backends.
586 *
587 * @param backend a Backend that is already ready to go.
588 */
589 public TApplication(final Backend backend) {
590 this.backend = backend;
591 TApplicationImpl();
592 }
30bd4abd 593
a4406f4e
KL
594 /**
595 * Finish construction once the backend is set.
596 */
597 private void TApplicationImpl() {
8e688b92
KL
598 theme = new ColorTheme();
599 desktopBottom = getScreen().getHeight() - 1;
c6940ed9
KL
600 fillEventQueue = new ArrayList<TInputEvent>();
601 drainEventQueue = new ArrayList<TInputEvent>();
8e688b92
KL
602 windows = new LinkedList<TWindow>();
603 menus = new LinkedList<TMenu>();
604 subMenus = new LinkedList<TMenu>();
d502a0e9 605 timers = new LinkedList<TTimer>();
e826b451 606 accelerators = new HashMap<TKeypress, TMenuItem>();
efb7af1f 607 menuItems = new ArrayList<TMenuItem>();
c6940ed9
KL
608
609 // Setup the main consumer thread
610 primaryEventHandler = new WidgetEventHandler(this, true);
611 (new Thread(primaryEventHandler)).start();
4328bb42
KL
612 }
613
614 /**
bd8d51fa
KL
615 * Invert the cell color at a position. This is used to track the mouse.
616 *
617 * @param x column position
618 * @param y row position
4328bb42 619 */
bd8d51fa 620 private void invertCell(final int x, final int y) {
1d14ffab
KL
621 if (debugThreads) {
622 System.err.printf("invertCell() %d %d\n", x, y);
7b5261bc 623 }
1d14ffab
KL
624 CellAttributes attr = getScreen().getAttrXY(x, y);
625 attr.setForeColor(attr.getForeColor().invert());
626 attr.setBackColor(attr.getBackColor().invert());
627 getScreen().putAttrXY(x, y, attr, false);
4328bb42
KL
628 }
629
630 /**
631 * Draw everything.
632 */
7c870d89 633 private void drawAll() {
99144c71
KL
634 if (debugThreads) {
635 System.err.printf("drawAll() enter\n");
636 }
637
bd8d51fa 638 if (!repaint) {
1d14ffab
KL
639 if (debugThreads) {
640 System.err.printf("drawAll() !repaint\n");
bd8d51fa 641 }
1d14ffab
KL
642 synchronized (getScreen()) {
643 if ((oldMouseX != mouseX) || (oldMouseY != mouseY)) {
644 // The only thing that has happened is the mouse moved.
645 // Clear the old position and draw the new position.
646 invertCell(oldMouseX, oldMouseY);
647 invertCell(mouseX, mouseY);
648 oldMouseX = mouseX;
649 oldMouseY = mouseY;
650 }
651 if (getScreen().isDirty()) {
652 backend.flushScreen();
653 }
654 return;
bd8d51fa 655 }
7b5261bc
KL
656 }
657
99144c71
KL
658 if (debugThreads) {
659 System.err.printf("drawAll() REDRAW\n");
660 }
661
7b5261bc
KL
662 // If true, the cursor is not visible
663 boolean cursor = false;
664
665 // Start with a clean screen
a06459bd 666 getScreen().clear();
7b5261bc
KL
667
668 // Draw the background
669 CellAttributes background = theme.getColor("tapplication.background");
a06459bd 670 getScreen().putAll(GraphicsChars.HATCH, background);
7b5261bc 671
7b5261bc 672 // Draw each window in reverse Z order
a06459bd
KL
673 List<TWindow> sorted = new LinkedList<TWindow>(windows);
674 Collections.sort(sorted);
675 Collections.reverse(sorted);
676 for (TWindow window: sorted) {
677 window.drawChildren();
7b5261bc
KL
678 }
679
680 // Draw the blank menubar line - reset the screen clipping first so
681 // it won't trim it out.
a06459bd
KL
682 getScreen().resetClipping();
683 getScreen().hLineXY(0, 0, getScreen().getWidth(), ' ',
7b5261bc
KL
684 theme.getColor("tmenu"));
685 // Now draw the menus.
686 int x = 1;
fca67db0 687 for (TMenu menu: menus) {
7b5261bc
KL
688 CellAttributes menuColor;
689 CellAttributes menuMnemonicColor;
7c870d89 690 if (menu.isActive()) {
7b5261bc
KL
691 menuColor = theme.getColor("tmenu.highlighted");
692 menuMnemonicColor = theme.getColor("tmenu.mnemonic.highlighted");
693 } else {
694 menuColor = theme.getColor("tmenu");
695 menuMnemonicColor = theme.getColor("tmenu.mnemonic");
696 }
697 // Draw the menu title
fca67db0 698 getScreen().hLineXY(x, 0, menu.getTitle().length() + 2, ' ',
7b5261bc 699 menuColor);
0d47c546 700 getScreen().putStringXY(x + 1, 0, menu.getTitle(), menuColor);
7b5261bc 701 // Draw the highlight character
fca67db0
KL
702 getScreen().putCharXY(x + 1 + menu.getMnemonic().getShortcutIdx(),
703 0, menu.getMnemonic().getShortcut(), menuMnemonicColor);
7b5261bc 704
7c870d89 705 if (menu.isActive()) {
a06459bd 706 menu.drawChildren();
7b5261bc 707 // Reset the screen clipping so we can draw the next title.
a06459bd 708 getScreen().resetClipping();
7b5261bc 709 }
fca67db0 710 x += menu.getTitle().length() + 2;
7b5261bc
KL
711 }
712
a06459bd 713 for (TMenu menu: subMenus) {
7b5261bc 714 // Reset the screen clipping so we can draw the next sub-menu.
a06459bd
KL
715 getScreen().resetClipping();
716 menu.drawChildren();
7b5261bc 717 }
7b5261bc
KL
718
719 // Draw the mouse pointer
bd8d51fa 720 invertCell(mouseX, mouseY);
1d14ffab
KL
721 oldMouseX = mouseX;
722 oldMouseY = mouseY;
7b5261bc 723
7b5261bc
KL
724 // Place the cursor if it is visible
725 TWidget activeWidget = null;
a06459bd
KL
726 if (sorted.size() > 0) {
727 activeWidget = sorted.get(sorted.size() - 1).getActiveChild();
7c870d89 728 if (activeWidget.isCursorVisible()) {
a06459bd 729 getScreen().putCursor(true, activeWidget.getCursorAbsoluteX(),
7b5261bc
KL
730 activeWidget.getCursorAbsoluteY());
731 cursor = true;
732 }
733 }
734
735 // Kill the cursor
fca67db0 736 if (!cursor) {
a06459bd 737 getScreen().hideCursor();
7b5261bc 738 }
7b5261bc
KL
739
740 // Flush the screen contents
1d14ffab
KL
741 if (getScreen().isDirty()) {
742 backend.flushScreen();
743 }
7b5261bc
KL
744
745 repaint = false;
4328bb42
KL
746 }
747
748 /**
7b5261bc 749 * Run this application until it exits.
4328bb42 750 */
a4406f4e 751 public void run() {
7b5261bc
KL
752 while (!quit) {
753 // Timeout is in milliseconds, so default timeout after 1 second
754 // of inactivity.
92554d64
KL
755 long timeout = 0;
756
757 // If I've got no updates to render, wait for something from the
758 // backend or a timer.
bd8d51fa
KL
759 if (!repaint
760 && ((mouseX == oldMouseX) && (mouseY == oldMouseY))
761 ) {
e3dfbd23
KL
762 // Never sleep longer than 50 millis. We need time for
763 // windows with background tasks to update the display, and
764 // still flip buffers reasonably quickly in
765 // backend.flushPhysical().
766 timeout = getSleepTime(50);
8e688b92 767 }
92554d64
KL
768
769 if (timeout > 0) {
770 // As of now, I've got nothing to do: no I/O, nothing from
771 // the consumer threads, no timers that need to run ASAP. So
772 // wait until either the backend or the consumer threads have
773 // something to do.
774 try {
775 synchronized (this) {
776 this.wait(timeout);
777 }
778 } catch (InterruptedException e) {
779 // I'm awake and don't care why, let's see what's going
780 // on out there.
8e688b92 781 }
bd8d51fa 782 repaint = true;
7b5261bc
KL
783 }
784
ef368bd0
KL
785 // Prevent stepping on the primary or secondary event handler.
786 stopEventHandlers();
787
8e688b92 788 // Pull any pending I/O events
92554d64 789 backend.getEvents(fillEventQueue);
8e688b92
KL
790
791 // Dispatch each event to the appropriate handler, one at a time.
792 for (;;) {
793 TInputEvent event = null;
ef368bd0
KL
794 if (fillEventQueue.size() == 0) {
795 break;
8e688b92 796 }
ef368bd0 797 event = fillEventQueue.remove(0);
8e688b92
KL
798 metaHandleEvent(event);
799 }
7b5261bc 800
92554d64 801 // Wake a consumer thread if we have any pending events.
ef368bd0
KL
802 if (drainEventQueue.size() > 0) {
803 wakeEventHandler();
92554d64
KL
804 }
805
7b5261bc
KL
806 // Process timers and call doIdle()'s
807 doIdle();
808
809 // Update the screen
87a17f3c
KL
810 synchronized (getScreen()) {
811 drawAll();
812 }
99144c71
KL
813
814 // Let the event handlers run again.
815 startEventHandlers();
7b5261bc 816
92554d64
KL
817 } // while (!quit)
818
819 // Shutdown the event consumer threads
820 if (secondaryEventHandler != null) {
821 synchronized (secondaryEventHandler) {
822 secondaryEventHandler.notify();
823 }
824 }
825 if (primaryEventHandler != null) {
826 synchronized (primaryEventHandler) {
827 primaryEventHandler.notify();
828 }
7b5261bc
KL
829 }
830
92554d64 831 // Shutdown the user I/O thread(s)
7b5261bc 832 backend.shutdown();
92554d64
KL
833
834 // Close all the windows. This gives them an opportunity to release
835 // resources.
836 closeAllWindows();
837
4328bb42
KL
838 }
839
840 /**
841 * Peek at certain application-level events, add to eventQueue, and wake
8e688b92 842 * up the consuming Thread.
4328bb42 843 *
8e688b92 844 * @param event the input event to consume
4328bb42 845 */
8e688b92 846 private void metaHandleEvent(final TInputEvent event) {
7b5261bc 847
a83fea2b
KL
848 if (debugEvents) {
849 System.err.printf(String.format("metaHandleEvents event: %s\n",
850 event)); System.err.flush();
851 }
7b5261bc 852
8e688b92
KL
853 if (quit) {
854 // Do no more processing if the application is already trying
855 // to exit.
856 return;
857 }
7b5261bc 858
8e688b92 859 // Special application-wide events -------------------------------
7b5261bc 860
8e688b92
KL
861 // Abort everything
862 if (event instanceof TCommandEvent) {
863 TCommandEvent command = (TCommandEvent) event;
864 if (command.getCmd().equals(cmAbort)) {
865 quit = true;
866 return;
7b5261bc 867 }
8e688b92 868 }
7b5261bc 869
8e688b92
KL
870 // Screen resize
871 if (event instanceof TResizeEvent) {
872 TResizeEvent resize = (TResizeEvent) event;
bd8d51fa
KL
873 synchronized (getScreen()) {
874 getScreen().setDimensions(resize.getWidth(),
875 resize.getHeight());
876 desktopBottom = getScreen().getHeight() - 1;
877 mouseX = 0;
878 mouseY = 0;
879 oldMouseX = 0;
880 oldMouseY = 0;
881 }
8e688b92
KL
882 return;
883 }
7b5261bc 884
8e688b92
KL
885 // Peek at the mouse position
886 if (event instanceof TMouseEvent) {
887 TMouseEvent mouse = (TMouseEvent) event;
bd8d51fa
KL
888 synchronized (getScreen()) {
889 if ((mouseX != mouse.getX()) || (mouseY != mouse.getY())) {
890 oldMouseX = mouseX;
891 oldMouseY = mouseY;
892 mouseX = mouse.getX();
893 mouseY = mouse.getY();
894 }
7b5261bc 895 }
8e688b92 896 }
7b5261bc 897
bd8d51fa 898 // Put into the main queue
ef368bd0 899 drainEventQueue.add(event);
4328bb42
KL
900 }
901
a06459bd
KL
902 /**
903 * Dispatch one event to the appropriate widget or application-level
fca67db0
KL
904 * event handler. This is the primary event handler, it has the normal
905 * application-wide event handling.
a06459bd
KL
906 *
907 * @param event the input event to consume
fca67db0 908 * @see #secondaryHandleEvent(TInputEvent event)
a06459bd 909 */
fca67db0
KL
910 private void primaryHandleEvent(final TInputEvent event) {
911
a83fea2b
KL
912 if (debugEvents) {
913 System.err.printf("Handle event: %s\n", event);
914 }
fca67db0
KL
915
916 // Special application-wide events -----------------------------------
917
918 // Peek at the mouse position
919 if (event instanceof TMouseEvent) {
920 // See if we need to switch focus to another window or the menu
921 checkSwitchFocus((TMouseEvent) event);
922 }
923
924 // Handle menu events
925 if ((activeMenu != null) && !(event instanceof TCommandEvent)) {
926 TMenu menu = activeMenu;
927
928 if (event instanceof TMouseEvent) {
929 TMouseEvent mouse = (TMouseEvent) event;
930
931 while (subMenus.size() > 0) {
932 TMenu subMenu = subMenus.get(subMenus.size() - 1);
933 if (subMenu.mouseWouldHit(mouse)) {
934 break;
935 }
936 if ((mouse.getType() == TMouseEvent.Type.MOUSE_MOTION)
7c870d89
KL
937 && (!mouse.isMouse1())
938 && (!mouse.isMouse2())
939 && (!mouse.isMouse3())
940 && (!mouse.isMouseWheelUp())
941 && (!mouse.isMouseWheelDown())
fca67db0
KL
942 ) {
943 break;
944 }
945 // We navigated away from a sub-menu, so close it
946 closeSubMenu();
947 }
948
949 // Convert the mouse relative x/y to menu coordinates
950 assert (mouse.getX() == mouse.getAbsoluteX());
951 assert (mouse.getY() == mouse.getAbsoluteY());
952 if (subMenus.size() > 0) {
953 menu = subMenus.get(subMenus.size() - 1);
954 }
955 mouse.setX(mouse.getX() - menu.getX());
956 mouse.setY(mouse.getY() - menu.getY());
957 }
958 menu.handleEvent(event);
959 return;
960 }
a06459bd 961
fca67db0
KL
962 if (event instanceof TKeypressEvent) {
963 TKeypressEvent keypress = (TKeypressEvent) event;
e826b451 964
fca67db0
KL
965 // See if this key matches an accelerator, and if so dispatch the
966 // menu event.
967 TKeypress keypressLowercase = keypress.getKey().toLowerCase();
e826b451
KL
968 TMenuItem item = null;
969 synchronized (accelerators) {
970 item = accelerators.get(keypressLowercase);
971 }
fca67db0 972 if (item != null) {
7c870d89 973 if (item.isEnabled()) {
bd8d51fa
KL
974 // Let the menu item dispatch
975 item.dispatch();
fca67db0
KL
976 return;
977 }
978 }
bd8d51fa
KL
979 // Handle the keypress
980 if (onKeypress(keypress)) {
981 return;
982 }
fca67db0 983 }
a06459bd 984
fca67db0
KL
985 if (event instanceof TCommandEvent) {
986 if (onCommand((TCommandEvent) event)) {
987 return;
988 }
989 }
990
991 if (event instanceof TMenuEvent) {
992 if (onMenu((TMenuEvent) event)) {
993 return;
994 }
995 }
996
997 // Dispatch events to the active window -------------------------------
998 for (TWindow window: windows) {
7c870d89 999 if (window.isActive()) {
a06459bd
KL
1000 if (event instanceof TMouseEvent) {
1001 TMouseEvent mouse = (TMouseEvent) event;
fca67db0
KL
1002 // Convert the mouse relative x/y to window coordinates
1003 assert (mouse.getX() == mouse.getAbsoluteX());
1004 assert (mouse.getY() == mouse.getAbsoluteY());
1005 mouse.setX(mouse.getX() - window.getX());
1006 mouse.setY(mouse.getY() - window.getY());
1007 }
a83fea2b
KL
1008 if (debugEvents) {
1009 System.err.printf("TApplication dispatch event: %s\n",
1010 event);
1011 }
fca67db0
KL
1012 window.handleEvent(event);
1013 break;
1014 }
1015 }
1016 }
1017 /**
1018 * Dispatch one event to the appropriate widget or application-level
1019 * event handler. This is the secondary event handler used by certain
1020 * special dialogs (currently TMessageBox and TFileOpenBox).
1021 *
1022 * @param event the input event to consume
1023 * @see #primaryHandleEvent(TInputEvent event)
1024 */
1025 private void secondaryHandleEvent(final TInputEvent event) {
c6940ed9
KL
1026 secondaryEventReceiver.handleEvent(event);
1027 }
1028
1029 /**
1030 * Enable a widget to override the primary event thread.
1031 *
1032 * @param widget widget that will receive events
1033 */
1034 public final void enableSecondaryEventReceiver(final TWidget widget) {
1035 assert (secondaryEventReceiver == null);
1036 assert (secondaryEventHandler == null);
a043164f
KL
1037 assert ((widget instanceof TMessageBox)
1038 || (widget instanceof TFileOpenBox));
c6940ed9
KL
1039 secondaryEventReceiver = widget;
1040 secondaryEventHandler = new WidgetEventHandler(this, false);
1041 (new Thread(secondaryEventHandler)).start();
c6940ed9
KL
1042 }
1043
1044 /**
1045 * Yield to the secondary thread.
1046 */
1047 public final void yield() {
1048 assert (secondaryEventReceiver != null);
99144c71
KL
1049 // This is where we handoff the event handler lock from the primary
1050 // to secondary thread. We unlock here, and in a future loop the
1051 // secondary thread locks again. When it gives up, we have the
1052 // single lock back.
1053 boolean oldLock = unlockHandleEvent();
329fd62e 1054 assert (oldLock);
99144c71 1055
c6940ed9 1056 while (secondaryEventReceiver != null) {
92554d64 1057 synchronized (primaryEventHandler) {
c6940ed9 1058 try {
92554d64 1059 primaryEventHandler.wait();
c6940ed9
KL
1060 } catch (InterruptedException e) {
1061 // SQUASH
1062 }
1063 }
1064 }
a06459bd
KL
1065 }
1066
4328bb42
KL
1067 /**
1068 * Do stuff when there is no user input.
1069 */
1070 private void doIdle() {
99144c71
KL
1071 if (debugThreads) {
1072 System.err.printf("doIdle()\n");
1073 }
1074
7b5261bc 1075 // Now run any timers that have timed out
d502a0e9
KL
1076 Date now = new Date();
1077 List<TTimer> keepTimers = new LinkedList<TTimer>();
1078 for (TTimer timer: timers) {
92554d64 1079 if (timer.getNextTick().getTime() <= now.getTime()) {
d502a0e9 1080 timer.tick();
c6940ed9 1081 if (timer.recurring) {
d502a0e9 1082 keepTimers.add(timer);
7b5261bc
KL
1083 }
1084 } else {
d502a0e9 1085 keepTimers.add(timer);
7b5261bc
KL
1086 }
1087 }
1088 timers = keepTimers;
1089
1090 // Call onIdle's
d502a0e9
KL
1091 for (TWindow window: windows) {
1092 window.onIdle();
7b5261bc 1093 }
4328bb42 1094 }
7d4115a5 1095
4328bb42
KL
1096 /**
1097 * Get the amount of time I can sleep before missing a Timer tick.
1098 *
92554d64 1099 * @param timeout = initial (maximum) timeout in millis
4328bb42
KL
1100 * @return number of milliseconds between now and the next timer event
1101 */
bd8d51fa 1102 private long getSleepTime(final long timeout) {
d502a0e9 1103 Date now = new Date();
92554d64 1104 long nowTime = now.getTime();
d502a0e9
KL
1105 long sleepTime = timeout;
1106 for (TTimer timer: timers) {
92554d64
KL
1107 long nextTickTime = timer.getNextTick().getTime();
1108 if (nextTickTime < nowTime) {
7b5261bc
KL
1109 return 0;
1110 }
92554d64
KL
1111
1112 long timeDifference = nextTickTime - nowTime;
1113 if (timeDifference < sleepTime) {
1114 sleepTime = timeDifference;
7b5261bc
KL
1115 }
1116 }
d502a0e9 1117 assert (sleepTime >= 0);
92554d64
KL
1118 assert (sleepTime <= timeout);
1119 return sleepTime;
7d4115a5 1120 }
4328bb42 1121
48e27807
KL
1122 /**
1123 * Close window. Note that the window's destructor is NOT called by this
1124 * method, instead the GC is assumed to do the cleanup.
1125 *
1126 * @param window the window to remove
1127 */
1128 public final void closeWindow(final TWindow window) {
bb35d919
KL
1129 synchronized (windows) {
1130 int z = window.getZ();
1131 window.setZ(-1);
efb7af1f 1132 window.onUnfocus();
bb35d919
KL
1133 Collections.sort(windows);
1134 windows.remove(0);
1135 TWindow activeWindow = null;
1136 for (TWindow w: windows) {
1137 if (w.getZ() > z) {
1138 w.setZ(w.getZ() - 1);
1139 if (w.getZ() == 0) {
1140 w.setActive(true);
efb7af1f 1141 w.onFocus();
bb35d919
KL
1142 assert (activeWindow == null);
1143 activeWindow = w;
1144 } else {
efb7af1f
KL
1145 if (w.isActive()) {
1146 w.setActive(false);
1147 w.onUnfocus();
1148 }
bb35d919 1149 }
48e27807
KL
1150 }
1151 }
1152 }
1153
1154 // Perform window cleanup
1155 window.onClose();
1156
48e27807 1157 // Check if we are closing a TMessageBox or similar
c6940ed9
KL
1158 if (secondaryEventReceiver != null) {
1159 assert (secondaryEventHandler != null);
48e27807
KL
1160
1161 // Do not send events to the secondaryEventReceiver anymore, the
1162 // window is closed.
1163 secondaryEventReceiver = null;
1164
92554d64
KL
1165 // Wake the secondary thread, it will wake the primary as it
1166 // exits.
1167 synchronized (secondaryEventHandler) {
1168 secondaryEventHandler.notify();
48e27807
KL
1169 }
1170 }
48e27807
KL
1171 }
1172
1173 /**
1174 * Switch to the next window.
1175 *
1176 * @param forward if true, then switch to the next window in the list,
1177 * otherwise switch to the previous window in the list
1178 */
1179 public final void switchWindow(final boolean forward) {
48e27807 1180 // Only switch if there are multiple windows
fca67db0 1181 if (windows.size() < 2) {
48e27807
KL
1182 return;
1183 }
1184
bb35d919
KL
1185 synchronized (windows) {
1186
1187 // Swap z/active between active window and the next in the list
1188 int activeWindowI = -1;
1189 for (int i = 0; i < windows.size(); i++) {
7c870d89 1190 if (windows.get(i).isActive()) {
bb35d919
KL
1191 activeWindowI = i;
1192 break;
1193 }
48e27807 1194 }
bb35d919 1195 assert (activeWindowI >= 0);
48e27807 1196
bb35d919
KL
1197 // Do not switch if a window is modal
1198 if (windows.get(activeWindowI).isModal()) {
1199 return;
1200 }
48e27807 1201
bb35d919
KL
1202 int nextWindowI;
1203 if (forward) {
1204 nextWindowI = (activeWindowI + 1) % windows.size();
48e27807 1205 } else {
bb35d919
KL
1206 if (activeWindowI == 0) {
1207 nextWindowI = windows.size() - 1;
1208 } else {
1209 nextWindowI = activeWindowI - 1;
1210 }
48e27807 1211 }
bb35d919
KL
1212 windows.get(activeWindowI).setActive(false);
1213 windows.get(activeWindowI).setZ(windows.get(nextWindowI).getZ());
efb7af1f 1214 windows.get(activeWindowI).onUnfocus();
bb35d919
KL
1215 windows.get(nextWindowI).setZ(0);
1216 windows.get(nextWindowI).setActive(true);
efb7af1f 1217 windows.get(nextWindowI).onFocus();
bb35d919
KL
1218
1219 } // synchronized (windows)
48e27807 1220
48e27807
KL
1221 }
1222
1223 /**
1224 * Add a window to my window list and make it active.
1225 *
1226 * @param window new window to add
1227 */
1228 public final void addWindow(final TWindow window) {
bb35d919
KL
1229 synchronized (windows) {
1230 // Do not allow a modal window to spawn a non-modal window
1231 if ((windows.size() > 0) && (windows.get(0).isModal())) {
1232 assert (window.isModal());
1233 }
1234 for (TWindow w: windows) {
efb7af1f
KL
1235 if (w.isActive()) {
1236 w.setActive(false);
1237 w.onUnfocus();
1238 }
bb35d919
KL
1239 w.setZ(w.getZ() + 1);
1240 }
1241 windows.add(window);
bb35d919 1242 window.setZ(0);
efb7af1f
KL
1243 window.setActive(true);
1244 window.onFocus();
48e27807 1245 }
48e27807
KL
1246 }
1247
fca67db0
KL
1248 /**
1249 * Check if there is a system-modal window on top.
1250 *
1251 * @return true if the active window is modal
1252 */
1253 private boolean modalWindowActive() {
1254 if (windows.size() == 0) {
1255 return false;
1256 }
1257 return windows.get(windows.size() - 1).isModal();
1258 }
1259
1260 /**
1261 * Check if a mouse event would hit either the active menu or any open
1262 * sub-menus.
1263 *
1264 * @param mouse mouse event
1265 * @return true if the mouse would hit the active menu or an open
1266 * sub-menu
1267 */
1268 private boolean mouseOnMenu(final TMouseEvent mouse) {
1269 assert (activeMenu != null);
1270 List<TMenu> menus = new LinkedList<TMenu>(subMenus);
1271 Collections.reverse(menus);
1272 for (TMenu menu: menus) {
1273 if (menu.mouseWouldHit(mouse)) {
1274 return true;
1275 }
1276 }
1277 return activeMenu.mouseWouldHit(mouse);
1278 }
1279
1280 /**
1281 * See if we need to switch window or activate the menu based on
1282 * a mouse click.
1283 *
1284 * @param mouse mouse event
1285 */
1286 private void checkSwitchFocus(final TMouseEvent mouse) {
1287
1288 if ((mouse.getType() == TMouseEvent.Type.MOUSE_DOWN)
1289 && (activeMenu != null)
1290 && (mouse.getAbsoluteY() != 0)
1291 && (!mouseOnMenu(mouse))
1292 ) {
1293 // They clicked outside the active menu, turn it off
1294 activeMenu.setActive(false);
1295 activeMenu = null;
1296 for (TMenu menu: subMenus) {
1297 menu.setActive(false);
1298 }
1299 subMenus.clear();
1300 // Continue checks
1301 }
1302
1303 // See if they hit the menu bar
1304 if ((mouse.getType() == TMouseEvent.Type.MOUSE_DOWN)
7c870d89 1305 && (mouse.isMouse1())
fca67db0
KL
1306 && (!modalWindowActive())
1307 && (mouse.getAbsoluteY() == 0)
1308 ) {
1309
1310 for (TMenu menu: subMenus) {
1311 menu.setActive(false);
1312 }
1313 subMenus.clear();
1314
1315 // They selected the menu, go activate it
1316 for (TMenu menu: menus) {
1317 if ((mouse.getAbsoluteX() >= menu.getX())
1318 && (mouse.getAbsoluteX() < menu.getX()
1319 + menu.getTitle().length() + 2)
1320 ) {
1321 menu.setActive(true);
1322 activeMenu = menu;
1323 } else {
1324 menu.setActive(false);
1325 }
1326 }
fca67db0
KL
1327 return;
1328 }
1329
1330 // See if they hit the menu bar
1331 if ((mouse.getType() == TMouseEvent.Type.MOUSE_MOTION)
7c870d89 1332 && (mouse.isMouse1())
fca67db0
KL
1333 && (activeMenu != null)
1334 && (mouse.getAbsoluteY() == 0)
1335 ) {
1336
1337 TMenu oldMenu = activeMenu;
1338 for (TMenu menu: subMenus) {
1339 menu.setActive(false);
1340 }
1341 subMenus.clear();
1342
1343 // See if we should switch menus
1344 for (TMenu menu: menus) {
1345 if ((mouse.getAbsoluteX() >= menu.getX())
1346 && (mouse.getAbsoluteX() < menu.getX()
1347 + menu.getTitle().length() + 2)
1348 ) {
1349 menu.setActive(true);
1350 activeMenu = menu;
1351 }
1352 }
1353 if (oldMenu != activeMenu) {
1354 // They switched menus
1355 oldMenu.setActive(false);
1356 }
fca67db0
KL
1357 return;
1358 }
1359
1360 // Only switch if there are multiple windows
1361 if (windows.size() < 2) {
1362 return;
1363 }
1364
1365 // Switch on the upclick
1366 if (mouse.getType() != TMouseEvent.Type.MOUSE_UP) {
1367 return;
1368 }
1369
bb35d919
KL
1370 synchronized (windows) {
1371 Collections.sort(windows);
1372 if (windows.get(0).isModal()) {
1373 // Modal windows don't switch
1374 return;
1375 }
fca67db0 1376
bb35d919
KL
1377 for (TWindow window: windows) {
1378 assert (!window.isModal());
1379 if (window.mouseWouldHit(mouse)) {
1380 if (window == windows.get(0)) {
1381 // Clicked on the same window, nothing to do
1382 return;
1383 }
1384
1385 // We will be switching to another window
7c870d89
KL
1386 assert (windows.get(0).isActive());
1387 assert (!window.isActive());
efb7af1f 1388 windows.get(0).onUnfocus();
bb35d919
KL
1389 windows.get(0).setActive(false);
1390 windows.get(0).setZ(window.getZ());
1391 window.setZ(0);
1392 window.setActive(true);
efb7af1f 1393 window.onFocus();
fca67db0
KL
1394 return;
1395 }
fca67db0
KL
1396 }
1397 }
1398
1399 // Clicked on the background, nothing to do
1400 return;
1401 }
1402
1403 /**
1404 * Turn off the menu.
1405 */
928811d8 1406 public final void closeMenu() {
fca67db0
KL
1407 if (activeMenu != null) {
1408 activeMenu.setActive(false);
1409 activeMenu = null;
1410 for (TMenu menu: subMenus) {
1411 menu.setActive(false);
1412 }
1413 subMenus.clear();
1414 }
fca67db0
KL
1415 }
1416
1417 /**
1418 * Turn off a sub-menu.
1419 */
928811d8 1420 public final void closeSubMenu() {
fca67db0
KL
1421 assert (activeMenu != null);
1422 TMenu item = subMenus.get(subMenus.size() - 1);
1423 assert (item != null);
1424 item.setActive(false);
1425 subMenus.remove(subMenus.size() - 1);
fca67db0
KL
1426 }
1427
1428 /**
1429 * Switch to the next menu.
1430 *
1431 * @param forward if true, then switch to the next menu in the list,
1432 * otherwise switch to the previous menu in the list
1433 */
928811d8 1434 public final void switchMenu(final boolean forward) {
fca67db0
KL
1435 assert (activeMenu != null);
1436
1437 for (TMenu menu: subMenus) {
1438 menu.setActive(false);
1439 }
1440 subMenus.clear();
1441
1442 for (int i = 0; i < menus.size(); i++) {
1443 if (activeMenu == menus.get(i)) {
1444 if (forward) {
1445 if (i < menus.size() - 1) {
1446 i++;
1447 }
1448 } else {
1449 if (i > 0) {
1450 i--;
1451 }
1452 }
1453 activeMenu.setActive(false);
1454 activeMenu = menus.get(i);
1455 activeMenu.setActive(true);
fca67db0
KL
1456 return;
1457 }
1458 }
1459 }
1460
1461 /**
1462 * Method that TApplication subclasses can override to handle menu or
1463 * posted command events.
1464 *
1465 * @param command command event
1466 * @return if true, this event was consumed
1467 */
1468 protected boolean onCommand(final TCommandEvent command) {
fca67db0
KL
1469 // Default: handle cmExit
1470 if (command.equals(cmExit)) {
1471 if (messageBox("Confirmation", "Exit application?",
c6940ed9 1472 TMessageBox.Type.YESNO).getResult() == TMessageBox.Result.YES) {
fca67db0
KL
1473 quit = true;
1474 }
fca67db0
KL
1475 return true;
1476 }
1477
1478 if (command.equals(cmShell)) {
34a42e78 1479 openTerminal(0, 0, TWindow.RESIZABLE);
fca67db0
KL
1480 return true;
1481 }
1482
1483 if (command.equals(cmTile)) {
1484 tileWindows();
fca67db0
KL
1485 return true;
1486 }
1487 if (command.equals(cmCascade)) {
1488 cascadeWindows();
fca67db0
KL
1489 return true;
1490 }
1491 if (command.equals(cmCloseAll)) {
1492 closeAllWindows();
fca67db0
KL
1493 return true;
1494 }
c6940ed9 1495
fca67db0
KL
1496 return false;
1497 }
1498
1499 /**
1500 * Method that TApplication subclasses can override to handle menu
1501 * events.
1502 *
1503 * @param menu menu event
1504 * @return if true, this event was consumed
1505 */
1506 protected boolean onMenu(final TMenuEvent menu) {
1507
fca67db0 1508 // Default: handle MID_EXIT
8e688b92 1509 if (menu.getId() == TMenu.MID_EXIT) {
fca67db0 1510 if (messageBox("Confirmation", "Exit application?",
c6940ed9 1511 TMessageBox.Type.YESNO).getResult() == TMessageBox.Result.YES) {
fca67db0
KL
1512 quit = true;
1513 }
fca67db0
KL
1514 return true;
1515 }
1516
34a42e78
KL
1517 if (menu.getId() == TMenu.MID_SHELL) {
1518 openTerminal(0, 0, TWindow.RESIZABLE);
fca67db0
KL
1519 return true;
1520 }
1521
8e688b92 1522 if (menu.getId() == TMenu.MID_TILE) {
fca67db0 1523 tileWindows();
fca67db0
KL
1524 return true;
1525 }
8e688b92 1526 if (menu.getId() == TMenu.MID_CASCADE) {
fca67db0 1527 cascadeWindows();
fca67db0
KL
1528 return true;
1529 }
8e688b92 1530 if (menu.getId() == TMenu.MID_CLOSE_ALL) {
fca67db0 1531 closeAllWindows();
fca67db0
KL
1532 return true;
1533 }
fca67db0
KL
1534 return false;
1535 }
1536
1537 /**
1538 * Method that TApplication subclasses can override to handle keystrokes.
1539 *
1540 * @param keypress keystroke event
1541 * @return if true, this event was consumed
1542 */
1543 protected boolean onKeypress(final TKeypressEvent keypress) {
1544 // Default: only menu shortcuts
1545
1546 // Process Alt-F, Alt-E, etc. menu shortcut keys
7c870d89
KL
1547 if (!keypress.getKey().isFnKey()
1548 && keypress.getKey().isAlt()
1549 && !keypress.getKey().isCtrl()
fca67db0
KL
1550 && (activeMenu == null)
1551 ) {
1552
1553 assert (subMenus.size() == 0);
1554
1555 for (TMenu menu: menus) {
1556 if (Character.toLowerCase(menu.getMnemonic().getShortcut())
7c870d89 1557 == Character.toLowerCase(keypress.getKey().getChar())
fca67db0
KL
1558 ) {
1559 activeMenu = menu;
1560 menu.setActive(true);
fca67db0
KL
1561 return true;
1562 }
1563 }
1564 }
1565
1566 return false;
1567 }
48e27807 1568
928811d8 1569 /**
efb7af1f
KL
1570 * Add a menu item to the global list. If it has a keyboard accelerator,
1571 * that will be added the global hash.
928811d8 1572 *
efb7af1f 1573 * @param item the menu item
928811d8 1574 */
efb7af1f
KL
1575 public final void addMenuItem(final TMenuItem item) {
1576 menuItems.add(item);
1577
1578 TKeypress key = item.getKey();
1579 if (key != null) {
1580 synchronized (accelerators) {
1581 assert (accelerators.get(key) == null);
1582 accelerators.put(key.toLowerCase(), item);
1583 }
1584 }
1585 }
1586
1587 /**
1588 * Disable one menu item.
1589 *
1590 * @param id the menu item ID
1591 */
1592 public final void disableMenuItem(final int id) {
1593 for (TMenuItem item: menuItems) {
1594 if (item.getId() == id) {
1595 item.setEnabled(false);
1596 }
1597 }
1598 }
e826b451 1599
efb7af1f
KL
1600 /**
1601 * Disable the range of menu items with ID's between lower and upper,
1602 * inclusive.
1603 *
1604 * @param lower the lowest menu item ID
1605 * @param upper the highest menu item ID
1606 */
1607 public final void disableMenuItems(final int lower, final int upper) {
1608 for (TMenuItem item: menuItems) {
1609 if ((item.getId() >= lower) && (item.getId() <= upper)) {
1610 item.setEnabled(false);
1611 }
1612 }
1613 }
1614
1615 /**
1616 * Enable one menu item.
1617 *
1618 * @param id the menu item ID
1619 */
1620 public final void enableMenuItem(final int id) {
1621 for (TMenuItem item: menuItems) {
1622 if (item.getId() == id) {
1623 item.setEnabled(true);
1624 }
1625 }
1626 }
1627
1628 /**
1629 * Enable the range of menu items with ID's between lower and upper,
1630 * inclusive.
1631 *
1632 * @param lower the lowest menu item ID
1633 * @param upper the highest menu item ID
1634 */
1635 public final void enableMenuItems(final int lower, final int upper) {
1636 for (TMenuItem item: menuItems) {
1637 if ((item.getId() >= lower) && (item.getId() <= upper)) {
1638 item.setEnabled(true);
1639 }
e826b451 1640 }
928811d8
KL
1641 }
1642
1643 /**
1644 * Recompute menu x positions based on their title length.
1645 */
1646 public final void recomputeMenuX() {
1647 int x = 0;
1648 for (TMenu menu: menus) {
1649 menu.setX(x);
1650 x += menu.getTitle().length() + 2;
1651 }
1652 }
1653
1654 /**
1655 * Post an event to process and turn off the menu.
1656 *
1657 * @param event new event to add to the queue
1658 */
1659 public final void addMenuEvent(final TInputEvent event) {
8e688b92
KL
1660 synchronized (fillEventQueue) {
1661 fillEventQueue.add(event);
1662 }
928811d8
KL
1663 closeMenu();
1664 }
1665
1666 /**
1667 * Add a sub-menu to the list of open sub-menus.
1668 *
1669 * @param menu sub-menu
1670 */
1671 public final void addSubMenu(final TMenu menu) {
1672 subMenus.add(menu);
1673 }
1674
8e688b92
KL
1675 /**
1676 * Convenience function to add a top-level menu.
1677 *
1678 * @param title menu title
1679 * @return the new menu
1680 */
87a17f3c 1681 public final TMenu addMenu(final String title) {
8e688b92
KL
1682 int x = 0;
1683 int y = 0;
1684 TMenu menu = new TMenu(this, x, y, title);
1685 menus.add(menu);
1686 recomputeMenuX();
1687 return menu;
1688 }
1689
1690 /**
1691 * Convenience function to add a default "File" menu.
1692 *
1693 * @return the new menu
1694 */
1695 public final TMenu addFileMenu() {
1696 TMenu fileMenu = addMenu("&File");
1697 fileMenu.addDefaultItem(TMenu.MID_OPEN_FILE);
1698 fileMenu.addSeparator();
1699 fileMenu.addDefaultItem(TMenu.MID_SHELL);
1700 fileMenu.addDefaultItem(TMenu.MID_EXIT);
1701 return fileMenu;
1702 }
1703
1704 /**
1705 * Convenience function to add a default "Edit" menu.
1706 *
1707 * @return the new menu
1708 */
1709 public final TMenu addEditMenu() {
1710 TMenu editMenu = addMenu("&Edit");
1711 editMenu.addDefaultItem(TMenu.MID_CUT);
1712 editMenu.addDefaultItem(TMenu.MID_COPY);
1713 editMenu.addDefaultItem(TMenu.MID_PASTE);
1714 editMenu.addDefaultItem(TMenu.MID_CLEAR);
1715 return editMenu;
1716 }
1717
1718 /**
1719 * Convenience function to add a default "Window" menu.
1720 *
1721 * @return the new menu
1722 */
c6940ed9 1723 public final TMenu addWindowMenu() {
8e688b92
KL
1724 TMenu windowMenu = addMenu("&Window");
1725 windowMenu.addDefaultItem(TMenu.MID_TILE);
1726 windowMenu.addDefaultItem(TMenu.MID_CASCADE);
1727 windowMenu.addDefaultItem(TMenu.MID_CLOSE_ALL);
1728 windowMenu.addSeparator();
1729 windowMenu.addDefaultItem(TMenu.MID_WINDOW_MOVE);
1730 windowMenu.addDefaultItem(TMenu.MID_WINDOW_ZOOM);
1731 windowMenu.addDefaultItem(TMenu.MID_WINDOW_NEXT);
1732 windowMenu.addDefaultItem(TMenu.MID_WINDOW_PREVIOUS);
1733 windowMenu.addDefaultItem(TMenu.MID_WINDOW_CLOSE);
1734 return windowMenu;
1735 }
1736
1737 /**
1738 * Close all open windows.
1739 */
1740 private void closeAllWindows() {
1741 // Don't do anything if we are in the menu
1742 if (activeMenu != null) {
1743 return;
1744 }
bb35d919
KL
1745
1746 synchronized (windows) {
1747 for (TWindow window: windows) {
1748 closeWindow(window);
1749 }
8e688b92
KL
1750 }
1751 }
1752
1753 /**
1754 * Re-layout the open windows as non-overlapping tiles. This produces
1755 * almost the same results as Turbo Pascal 7.0's IDE.
1756 */
1757 private void tileWindows() {
bb35d919
KL
1758 synchronized (windows) {
1759 // Don't do anything if we are in the menu
1760 if (activeMenu != null) {
1761 return;
8e688b92 1762 }
bb35d919
KL
1763 int z = windows.size();
1764 if (z == 0) {
1765 return;
1766 }
1767 int a = 0;
1768 int b = 0;
1769 a = (int)(Math.sqrt(z));
1770 int c = 0;
1771 while (c < a) {
1772 b = (z - c) / a;
1773 if (((a * b) + c) == z) {
1774 break;
1775 }
1776 c++;
8e688b92 1777 }
bb35d919
KL
1778 assert (a > 0);
1779 assert (b > 0);
1780 assert (c < a);
1781 int newWidth = (getScreen().getWidth() / a);
1782 int newHeight1 = ((getScreen().getHeight() - 1) / b);
1783 int newHeight2 = ((getScreen().getHeight() - 1) / (b + c));
1784
1785 List<TWindow> sorted = new LinkedList<TWindow>(windows);
1786 Collections.sort(sorted);
1787 Collections.reverse(sorted);
1788 for (int i = 0; i < sorted.size(); i++) {
1789 int logicalX = i / b;
1790 int logicalY = i % b;
1791 if (i >= ((a - 1) * b)) {
1792 logicalX = a - 1;
1793 logicalY = i - ((a - 1) * b);
1794 }
8e688b92 1795
bb35d919
KL
1796 TWindow w = sorted.get(i);
1797 w.setX(logicalX * newWidth);
1798 w.setWidth(newWidth);
1799 if (i >= ((a - 1) * b)) {
1800 w.setY((logicalY * newHeight2) + 1);
1801 w.setHeight(newHeight2);
1802 } else {
1803 w.setY((logicalY * newHeight1) + 1);
1804 w.setHeight(newHeight1);
1805 }
8e688b92
KL
1806 }
1807 }
1808 }
1809
1810 /**
1811 * Re-layout the open windows as overlapping cascaded windows.
1812 */
1813 private void cascadeWindows() {
bb35d919
KL
1814 synchronized (windows) {
1815 // Don't do anything if we are in the menu
1816 if (activeMenu != null) {
1817 return;
8e688b92 1818 }
bb35d919
KL
1819 int x = 0;
1820 int y = 1;
1821 List<TWindow> sorted = new LinkedList<TWindow>(windows);
1822 Collections.sort(sorted);
1823 Collections.reverse(sorted);
1824 for (TWindow window: sorted) {
1825 window.setX(x);
1826 window.setY(y);
1827 x++;
1828 y++;
1829 if (x > getScreen().getWidth()) {
1830 x = 0;
1831 }
1832 if (y >= getScreen().getHeight()) {
1833 y = 1;
1834 }
8e688b92
KL
1835 }
1836 }
1837 }
1838
d502a0e9
KL
1839 /**
1840 * Convenience function to add a timer.
1841 *
1842 * @param duration number of milliseconds to wait between ticks
1843 * @param recurring if true, re-schedule this timer after every tick
1844 * @param action function to call when button is pressed
c6940ed9 1845 * @return the timer
d502a0e9
KL
1846 */
1847 public final TTimer addTimer(final long duration, final boolean recurring,
1848 final TAction action) {
1849
1850 TTimer timer = new TTimer(duration, recurring, action);
1851 synchronized (timers) {
1852 timers.add(timer);
1853 }
1854 return timer;
1855 }
1856
1857 /**
1858 * Convenience function to remove a timer.
1859 *
1860 * @param timer timer to remove
1861 */
1862 public final void removeTimer(final TTimer timer) {
1863 synchronized (timers) {
1864 timers.remove(timer);
1865 }
1866 }
1867
c6940ed9
KL
1868 /**
1869 * Convenience function to spawn a message box.
1870 *
1871 * @param title window title, will be centered along the top border
1872 * @param caption message to display. Use embedded newlines to get a
1873 * multi-line box.
1874 * @return the new message box
1875 */
1876 public final TMessageBox messageBox(final String title,
1877 final String caption) {
1878
1879 return new TMessageBox(this, title, caption, TMessageBox.Type.OK);
1880 }
1881
1882 /**
1883 * Convenience function to spawn a message box.
1884 *
1885 * @param title window title, will be centered along the top border
1886 * @param caption message to display. Use embedded newlines to get a
1887 * multi-line box.
1888 * @param type one of the TMessageBox.Type constants. Default is
1889 * Type.OK.
1890 * @return the new message box
1891 */
1892 public final TMessageBox messageBox(final String title,
1893 final String caption, final TMessageBox.Type type) {
1894
1895 return new TMessageBox(this, title, caption, type);
1896 }
1897
1898 /**
1899 * Convenience function to spawn an input box.
1900 *
1901 * @param title window title, will be centered along the top border
1902 * @param caption message to display. Use embedded newlines to get a
1903 * multi-line box.
1904 * @return the new input box
1905 */
1906 public final TInputBox inputBox(final String title, final String caption) {
1907
1908 return new TInputBox(this, title, caption);
1909 }
1910
1911 /**
1912 * Convenience function to spawn an input box.
1913 *
1914 * @param title window title, will be centered along the top border
1915 * @param caption message to display. Use embedded newlines to get a
1916 * multi-line box.
1917 * @param text initial text to seed the field with
1918 * @return the new input box
1919 */
1920 public final TInputBox inputBox(final String title, final String caption,
1921 final String text) {
1922
1923 return new TInputBox(this, title, caption, text);
1924 }
1ac2ccb1 1925
34a42e78
KL
1926 /**
1927 * Convenience function to open a terminal window.
1928 *
1929 * @param x column relative to parent
1930 * @param y row relative to parent
1931 * @return the terminal new window
1932 */
1933 public final TTerminalWindow openTerminal(final int x, final int y) {
1934 return openTerminal(x, y, TWindow.RESIZABLE);
1935 }
1936
1937 /**
1938 * Convenience function to open a terminal window.
1939 *
1940 * @param x column relative to parent
1941 * @param y row relative to parent
1942 * @param flags mask of CENTERED, MODAL, or RESIZABLE
1943 * @return the terminal new window
1944 */
1945 public final TTerminalWindow openTerminal(final int x, final int y,
1946 final int flags) {
1947
1948 return new TTerminalWindow(this, x, y, flags);
1949 }
1950
0d47c546
KL
1951 /**
1952 * Convenience function to spawn an file open box.
1953 *
1954 * @param path path of selected file
1955 * @return the result of the new file open box
329fd62e 1956 * @throws IOException if java.io operation throws
0d47c546
KL
1957 */
1958 public final String fileOpenBox(final String path) throws IOException {
1959
1960 TFileOpenBox box = new TFileOpenBox(this, path, TFileOpenBox.Type.OPEN);
1961 return box.getFilename();
1962 }
1963
1964 /**
1965 * Convenience function to spawn an file open box.
1966 *
1967 * @param path path of selected file
1968 * @param type one of the Type constants
1969 * @return the result of the new file open box
329fd62e 1970 * @throws IOException if java.io operation throws
0d47c546
KL
1971 */
1972 public final String fileOpenBox(final String path,
1973 final TFileOpenBox.Type type) throws IOException {
1974
1975 TFileOpenBox box = new TFileOpenBox(this, path, type);
1976 return box.getFilename();
1977 }
1978
7d4115a5 1979}