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