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