#14 TDesktop bug fixes, more TWindow API
[fanfix.git] / src / jexer / TApplication.java
CommitLineData
daa4106c 1/*
7b5261bc 2 * Jexer - Java Text User Interface
7d4115a5 3 *
e16dda65 4 * The MIT License (MIT)
7d4115a5 5 *
a2018e99 6 * Copyright (C) 2017 Kevin Lamonte
7d4115a5 7 *
e16dda65
KL
8 * Permission is hereby granted, free of charge, to any person obtaining a
9 * copy of this software and associated documentation files (the "Software"),
10 * to deal in the Software without restriction, including without limitation
11 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
12 * and/or sell copies of the Software, and to permit persons to whom the
13 * Software is furnished to do so, subject to the following conditions:
7d4115a5 14 *
e16dda65
KL
15 * The above copyright notice and this permission notice shall be included in
16 * all copies or substantial portions of the Software.
7d4115a5 17 *
e16dda65
KL
18 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
21 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
24 * DEALINGS IN THE SOFTWARE.
7b5261bc
KL
25 *
26 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
27 * @version 1
7d4115a5
KL
28 */
29package jexer;
30
4328bb42 31import java.io.InputStream;
0d47c546 32import java.io.IOException;
4328bb42 33import java.io.OutputStream;
6985c572
KL
34import java.io.PrintWriter;
35import java.io.Reader;
4328bb42 36import java.io.UnsupportedEncodingException;
a06459bd 37import java.util.Collections;
d502a0e9 38import java.util.Date;
e826b451 39import java.util.HashMap;
c6940ed9 40import java.util.ArrayList;
4328bb42
KL
41import java.util.LinkedList;
42import java.util.List;
e826b451 43import java.util.Map;
4328bb42
KL
44
45import jexer.bits.CellAttributes;
46import jexer.bits.ColorTheme;
47import jexer.bits.GraphicsChars;
48import jexer.event.TCommandEvent;
49import jexer.event.TInputEvent;
50import jexer.event.TKeypressEvent;
fca67db0 51import jexer.event.TMenuEvent;
4328bb42
KL
52import jexer.event.TMouseEvent;
53import jexer.event.TResizeEvent;
54import jexer.backend.Backend;
a4406f4e 55import jexer.backend.SwingBackend;
4328bb42 56import jexer.backend.ECMA48Backend;
48e27807 57import jexer.io.Screen;
928811d8
KL
58import jexer.menu.TMenu;
59import jexer.menu.TMenuItem;
4328bb42 60import static jexer.TCommand.*;
2ce6dab2 61import static jexer.TKeypress.*;
4328bb42 62
7d4115a5
KL
63/**
64 * TApplication sets up a full Text User Interface application.
65 */
a4406f4e 66public class TApplication implements Runnable {
7d4115a5 67
2ce6dab2
KL
68 // ------------------------------------------------------------------------
69 // Public constants -------------------------------------------------------
70 // ------------------------------------------------------------------------
71
99144c71
KL
72 /**
73 * If true, emit thread stuff to System.err.
74 */
75 private static final boolean debugThreads = false;
76
a83fea2b
KL
77 /**
78 * If true, emit events being processed to System.err.
79 */
80 private static final boolean debugEvents = false;
81
a7986f7b
KL
82 /**
83 * If true, do "smart placement" on new windows that are not specified to
84 * be centered.
85 */
86 private static final boolean smartWindowPlacement = true;
87
a4406f4e
KL
88 /**
89 * Two backend types are available.
90 */
91 public static enum BackendType {
92 /**
93 * A Swing JFrame.
94 */
95 SWING,
96
97 /**
98 * An ECMA48 / ANSI X3.64 / XTERM style terminal.
99 */
100 ECMA48,
101
102 /**
329fd62e 103 * Synonym for ECMA48.
a4406f4e
KL
104 */
105 XTERM
106 }
107
2ce6dab2
KL
108 // ------------------------------------------------------------------------
109 // Primary/secondary event handlers ---------------------------------------
110 // ------------------------------------------------------------------------
111
c6940ed9
KL
112 /**
113 * WidgetEventHandler is the main event consumer loop. There are at most
114 * two such threads in existence: the primary for normal case and a
115 * secondary that is used for TMessageBox, TInputBox, and similar.
116 */
117 private class WidgetEventHandler implements Runnable {
118 /**
119 * The main application.
120 */
121 private TApplication application;
122
123 /**
124 * Whether or not this WidgetEventHandler is the primary or secondary
125 * thread.
126 */
127 private boolean primary = true;
128
129 /**
130 * Public constructor.
131 *
132 * @param application the main application
133 * @param primary if true, this is the primary event handler thread
134 */
135 public WidgetEventHandler(final TApplication application,
136 final boolean primary) {
137
138 this.application = application;
139 this.primary = primary;
140 }
141
142 /**
143 * The consumer loop.
144 */
145 public void run() {
146
147 // Loop forever
148 while (!application.quit) {
149
150 // Wait until application notifies me
151 while (!application.quit) {
152 try {
153 synchronized (application.drainEventQueue) {
154 if (application.drainEventQueue.size() > 0) {
155 break;
156 }
157 }
92554d64
KL
158
159 synchronized (this) {
bd8d51fa
KL
160 if (debugThreads) {
161 System.err.printf("%s %s sleep\n", this,
162 primary ? "primary" : "secondary");
163 }
92554d64
KL
164
165 this.wait();
166
bd8d51fa
KL
167 if (debugThreads) {
168 System.err.printf("%s %s AWAKE\n", this,
169 primary ? "primary" : "secondary");
170 }
92554d64 171
c6940ed9
KL
172 if ((!primary)
173 && (application.secondaryEventReceiver == null)
174 ) {
92554d64
KL
175 // Secondary thread, emergency exit. If we
176 // got here then something went wrong with
177 // the handoff between yield() and
178 // closeWindow().
92554d64
KL
179 synchronized (application.primaryEventHandler) {
180 application.primaryEventHandler.notify();
181 }
182 application.secondaryEventHandler = null;
bd8d51fa
KL
183 throw new RuntimeException(
184 "secondary exited at wrong time");
c6940ed9
KL
185 }
186 break;
187 }
188 } catch (InterruptedException e) {
189 // SQUASH
190 }
191 }
192
ef368bd0
KL
193 // Wait for drawAll() or doIdle() to be done, then handle the
194 // events.
195 boolean oldLock = lockHandleEvent();
196 assert (oldLock == false);
197
c6940ed9
KL
198 // Pull all events off the queue
199 for (;;) {
200 TInputEvent event = null;
201 synchronized (application.drainEventQueue) {
202 if (application.drainEventQueue.size() == 0) {
203 break;
204 }
205 event = application.drainEventQueue.remove(0);
206 }
bd8d51fa 207 application.repaint = true;
c6940ed9
KL
208 if (primary) {
209 primaryHandleEvent(event);
210 } else {
211 secondaryHandleEvent(event);
212 }
213 if ((!primary)
214 && (application.secondaryEventReceiver == null)
215 ) {
99144c71
KL
216 // Secondary thread, time to exit.
217
218 // DO NOT UNLOCK. Primary thread just came back from
219 // primaryHandleEvent() and will unlock in the else
92554d64
KL
220 // block below. Just wake it up.
221 synchronized (application.primaryEventHandler) {
222 application.primaryEventHandler.notify();
223 }
224 // Now eliminate my reference so that
225 // wakeEventHandler() resumes working on the primary.
226 application.secondaryEventHandler = null;
227
228 // All done!
c6940ed9
KL
229 return;
230 }
92554d64
KL
231 } // for (;;)
232
ef368bd0
KL
233 // Unlock. Either I am primary thread, or I am secondary
234 // thread and still running.
235 oldLock = unlockHandleEvent();
236 assert (oldLock == true);
237
92554d64
KL
238 // I have done some work of some kind. Tell the main run()
239 // loop to wake up now.
240 synchronized (application) {
241 application.notify();
c6940ed9 242 }
92554d64 243
c6940ed9
KL
244 } // while (true) (main runnable loop)
245 }
246 }
247
248 /**
249 * The primary event handler thread.
250 */
92554d64 251 private volatile WidgetEventHandler primaryEventHandler;
c6940ed9
KL
252
253 /**
254 * The secondary event handler thread.
255 */
92554d64 256 private volatile WidgetEventHandler secondaryEventHandler;
c6940ed9
KL
257
258 /**
259 * The widget receiving events from the secondary event handler thread.
260 */
92554d64 261 private volatile TWidget secondaryEventReceiver;
c6940ed9 262
99144c71
KL
263 /**
264 * Spinlock for the primary and secondary event handlers.
265 * WidgetEventHandler.run() is responsible for setting this value.
266 */
267 private volatile boolean insideHandleEvent = false;
268
92554d64
KL
269 /**
270 * Wake the sleeping active event handler.
271 */
272 private void wakeEventHandler() {
273 if (secondaryEventHandler != null) {
274 synchronized (secondaryEventHandler) {
275 secondaryEventHandler.notify();
276 }
277 } else {
278 assert (primaryEventHandler != null);
279 synchronized (primaryEventHandler) {
280 primaryEventHandler.notify();
281 }
282 }
283 }
284
99144c71
KL
285 /**
286 * Set the insideHandleEvent flag to true. lockoutEventHandlers() will
287 * spin indefinitely until unlockHandleEvent() is called.
288 *
289 * @return the old value of insideHandleEvent
290 */
291 private boolean lockHandleEvent() {
292 if (debugThreads) {
293 System.err.printf(" >> lockHandleEvent(): oldValue %s",
294 insideHandleEvent);
295 }
296 boolean oldValue = true;
297
298 synchronized (this) {
299 // Wait for TApplication.run() to finish using the global state
300 // before allowing further event processing.
ef368bd0
KL
301 while (lockoutHandleEvent == true) {
302 try {
303 // Backoff so that the backend can finish its work.
304 Thread.sleep(5);
305 } catch (InterruptedException e) {
306 // SQUASH
307 }
308 }
99144c71
KL
309
310 oldValue = insideHandleEvent;
311 insideHandleEvent = true;
312 }
313
314 if (debugThreads) {
315 System.err.printf(" ***\n");
316 }
317 return oldValue;
318 }
319
320 /**
321 * Set the insideHandleEvent flag to false. lockoutEventHandlers() will
322 * spin indefinitely until unlockHandleEvent() is called.
323 *
324 * @return the old value of insideHandleEvent
325 */
326 private boolean unlockHandleEvent() {
327 if (debugThreads) {
328 System.err.printf(" << unlockHandleEvent(): oldValue %s\n",
329 insideHandleEvent);
330 }
331 synchronized (this) {
332 boolean oldValue = insideHandleEvent;
333 insideHandleEvent = false;
334 return oldValue;
335 }
336 }
337
338 /**
339 * Spinlock for the primary and secondary event handlers. When true, the
340 * event handlers will spinlock wait before calling handleEvent().
341 */
342 private volatile boolean lockoutHandleEvent = false;
343
344 /**
345 * TApplication.run() needs to be able rely on the global data structures
346 * being intact when calling doIdle() and drawAll(). Tell the event
347 * handlers to wait for an unlock before handling their events.
348 */
349 private void stopEventHandlers() {
350 if (debugThreads) {
351 System.err.printf(">> stopEventHandlers()");
352 }
353
354 lockoutHandleEvent = true;
355 // Wait for the last event to finish processing before returning
356 // control to TApplication.run().
ef368bd0
KL
357 while (insideHandleEvent == true) {
358 try {
359 // Backoff so that the event handler can finish its work.
360 Thread.sleep(1);
361 } catch (InterruptedException e) {
362 // SQUASH
363 }
364 }
99144c71
KL
365
366 if (debugThreads) {
367 System.err.printf(" XXX\n");
368 }
369 }
370
371 /**
372 * TApplication.run() needs to be able rely on the global data structures
373 * being intact when calling doIdle() and drawAll(). Tell the event
374 * handlers that it is now OK to handle their events.
375 */
376 private void startEventHandlers() {
377 if (debugThreads) {
378 System.err.printf("<< startEventHandlers()\n");
379 }
380 lockoutHandleEvent = false;
381 }
382
2ce6dab2
KL
383 // ------------------------------------------------------------------------
384 // TApplication attributes ------------------------------------------------
385 // ------------------------------------------------------------------------
386
7d4115a5 387 /**
4328bb42
KL
388 * Access to the physical screen, keyboard, and mouse.
389 */
7b5261bc 390 private Backend backend;
4328bb42 391
55d2b2c2
KL
392 /**
393 * Get the Backend.
394 *
395 * @return the Backend
396 */
397 public final Backend getBackend() {
398 return backend;
399 }
400
48e27807
KL
401 /**
402 * Get the Screen.
403 *
404 * @return the Screen
405 */
406 public final Screen getScreen() {
407 return backend.getScreen();
408 }
409
4328bb42 410 /**
7b5261bc 411 * Actual mouse coordinate X.
4328bb42
KL
412 */
413 private int mouseX;
414
415 /**
7b5261bc 416 * Actual mouse coordinate Y.
4328bb42
KL
417 */
418 private int mouseY;
419
bd8d51fa
KL
420 /**
421 * Old version of mouse coordinate X.
422 */
423 private int oldMouseX;
424
425 /**
426 * Old version mouse coordinate Y.
427 */
428 private int oldMouseY;
429
4328bb42 430 /**
8e688b92 431 * Event queue that is filled by run().
4328bb42 432 */
8e688b92
KL
433 private List<TInputEvent> fillEventQueue;
434
435 /**
436 * Event queue that will be drained by either primary or secondary
437 * Thread.
438 */
439 private List<TInputEvent> drainEventQueue;
4328bb42 440
fca67db0
KL
441 /**
442 * Top-level menus in this application.
443 */
444 private List<TMenu> menus;
445
446 /**
447 * Stack of activated sub-menus in this application.
448 */
449 private List<TMenu> subMenus;
450
451 /**
92453213 452 * The currently active menu.
fca67db0
KL
453 */
454 private TMenu activeMenu = null;
455
e826b451
KL
456 /**
457 * Active keyboard accelerators.
458 */
459 private Map<TKeypress, TMenuItem> accelerators;
460
efb7af1f
KL
461 /**
462 * All menu items.
463 */
464 private List<TMenuItem> menuItems;
465
4328bb42
KL
466 /**
467 * Windows and widgets pull colors from this ColorTheme.
468 */
7b5261bc
KL
469 private ColorTheme theme;
470
471 /**
472 * Get the color theme.
473 *
474 * @return the theme
475 */
476 public final ColorTheme getTheme() {
477 return theme;
478 }
4328bb42 479
a06459bd
KL
480 /**
481 * The top-level windows (but not menus).
482 */
fca67db0 483 private List<TWindow> windows;
a06459bd 484
92453213
KL
485 /**
486 * The currently acive window.
487 */
488 private TWindow activeWindow = null;
489
d502a0e9
KL
490 /**
491 * Timers that are being ticked.
492 */
493 private List<TTimer> timers;
494
4328bb42
KL
495 /**
496 * When true, exit the application.
497 */
92554d64 498 private volatile boolean quit = false;
4328bb42
KL
499
500 /**
501 * When true, repaint the entire screen.
502 */
92554d64 503 private volatile boolean repaint = true;
4328bb42 504
4328bb42 505 /**
7b5261bc
KL
506 * Y coordinate of the top edge of the desktop. For now this is a
507 * constant. Someday it would be nice to have a multi-line menu or
508 * toolbars.
4328bb42 509 */
48e27807
KL
510 private static final int desktopTop = 1;
511
512 /**
513 * Get Y coordinate of the top edge of the desktop.
514 *
515 * @return Y coordinate of the top edge of the desktop
516 */
517 public final int getDesktopTop() {
518 return desktopTop;
519 }
4328bb42
KL
520
521 /**
522 * Y coordinate of the bottom edge of the desktop.
523 */
48e27807
KL
524 private int desktopBottom;
525
526 /**
527 * Get Y coordinate of the bottom edge of the desktop.
528 *
529 * @return Y coordinate of the bottom edge of the desktop
530 */
531 public final int getDesktopBottom() {
532 return desktopBottom;
533 }
4328bb42 534
0ee88b6d
KL
535 /**
536 * An optional TDesktop background window that is drawn underneath
537 * everything else.
538 */
539 private TDesktop desktop;
540
541 /**
542 * Set the TDesktop instance.
543 *
544 * @param desktop a TDesktop instance, or null to remove the one that is
545 * set
546 */
547 public final void setDesktop(final TDesktop desktop) {
548 if (this.desktop != null) {
549 this.desktop.onClose();
550 }
551 this.desktop = desktop;
552 }
553
554 /**
555 * Get the TDesktop instance.
556 *
557 * @return the desktop, or null if it is not set
558 */
559 public final TDesktop getDesktop() {
560 return desktop;
561 }
562
92453213
KL
563 /**
564 * Get the current active window.
565 *
566 * @return the active window, or null if it is not set
567 */
568 public final TWindow getActiveWindow() {
569 return activeWindow;
570 }
571
572 /**
573 * Get the list of windows.
574 *
575 * @return a copy of the list of windows for this application
576 */
577 public final List<TWindow> getAllWindows() {
578 List<TWindow> result = new LinkedList<TWindow>();
579 result.addAll(windows);
580 return result;
581 }
582
2ce6dab2
KL
583 // ------------------------------------------------------------------------
584 // General behavior -------------------------------------------------------
585 // ------------------------------------------------------------------------
586
587 /**
588 * Display the about dialog.
589 */
590 protected void showAboutDialog() {
591 messageBox("About", "Jexer Version " +
592 this.getClass().getPackage().getImplementationVersion(),
593 TMessageBox.Type.OK);
594 }
595
596 // ------------------------------------------------------------------------
597 // Constructors -----------------------------------------------------------
598 // ------------------------------------------------------------------------
599
4328bb42
KL
600 /**
601 * Public constructor.
602 *
a4406f4e
KL
603 * @param backendType BackendType.XTERM, BackendType.ECMA48 or
604 * BackendType.SWING
605 * @throws UnsupportedEncodingException if an exception is thrown when
606 * creating the InputStreamReader
607 */
608 public TApplication(final BackendType backendType)
609 throws UnsupportedEncodingException {
610
611 switch (backendType) {
612 case SWING:
613 backend = new SwingBackend(this);
614 break;
615 case XTERM:
616 // Fall through...
617 case ECMA48:
618 backend = new ECMA48Backend(this, null, null);
329fd62e
KL
619 break;
620 default:
621 throw new IllegalArgumentException("Invalid backend type: "
622 + backendType);
a4406f4e
KL
623 }
624 TApplicationImpl();
625 }
626
627 /**
628 * Public constructor. The backend type will be BackendType.ECMA48.
629 *
4328bb42
KL
630 * @param input an InputStream connected to the remote user, or null for
631 * System.in. If System.in is used, then on non-Windows systems it will
632 * be put in raw mode; shutdown() will (blindly!) put System.in in cooked
633 * mode. input is always converted to a Reader with UTF-8 encoding.
634 * @param output an OutputStream connected to the remote user, or null
635 * for System.out. output is always converted to a Writer with UTF-8
636 * encoding.
7b5261bc
KL
637 * @throws UnsupportedEncodingException if an exception is thrown when
638 * creating the InputStreamReader
4328bb42 639 */
7b5261bc
KL
640 public TApplication(final InputStream input,
641 final OutputStream output) throws UnsupportedEncodingException {
4328bb42 642
a4406f4e
KL
643 backend = new ECMA48Backend(this, input, output);
644 TApplicationImpl();
645 }
30bd4abd 646
6985c572
KL
647 /**
648 * Public constructor. The backend type will be BackendType.ECMA48.
649 *
650 * @param input the InputStream underlying 'reader'. Its available()
651 * method is used to determine if reader.read() will block or not.
652 * @param reader a Reader connected to the remote user.
653 * @param writer a PrintWriter connected to the remote user.
654 * @param setRawMode if true, set System.in into raw mode with stty.
655 * This should in general not be used. It is here solely for Demo3,
656 * which uses System.in.
657 * @throws IllegalArgumentException if input, reader, or writer are null.
658 */
659 public TApplication(final InputStream input, final Reader reader,
660 final PrintWriter writer, final boolean setRawMode) {
661
662 backend = new ECMA48Backend(this, input, reader, writer, setRawMode);
663 TApplicationImpl();
664 }
665
666 /**
667 * Public constructor. The backend type will be BackendType.ECMA48.
668 *
669 * @param input the InputStream underlying 'reader'. Its available()
670 * method is used to determine if reader.read() will block or not.
671 * @param reader a Reader connected to the remote user.
672 * @param writer a PrintWriter connected to the remote user.
673 * @throws IllegalArgumentException if input, reader, or writer are null.
674 */
675 public TApplication(final InputStream input, final Reader reader,
676 final PrintWriter writer) {
677
678 this(input, reader, writer, false);
679 }
680
a4406f4e
KL
681 /**
682 * Public constructor. This hook enables use with new non-Jexer
683 * backends.
684 *
685 * @param backend a Backend that is already ready to go.
686 */
687 public TApplication(final Backend backend) {
688 this.backend = backend;
689 TApplicationImpl();
690 }
30bd4abd 691
a4406f4e
KL
692 /**
693 * Finish construction once the backend is set.
694 */
695 private void TApplicationImpl() {
8e688b92
KL
696 theme = new ColorTheme();
697 desktopBottom = getScreen().getHeight() - 1;
c6940ed9
KL
698 fillEventQueue = new ArrayList<TInputEvent>();
699 drainEventQueue = new ArrayList<TInputEvent>();
8e688b92
KL
700 windows = new LinkedList<TWindow>();
701 menus = new LinkedList<TMenu>();
702 subMenus = new LinkedList<TMenu>();
d502a0e9 703 timers = new LinkedList<TTimer>();
e826b451 704 accelerators = new HashMap<TKeypress, TMenuItem>();
efb7af1f 705 menuItems = new ArrayList<TMenuItem>();
0ee88b6d 706 desktop = new TDesktop(this);
c6940ed9
KL
707
708 // Setup the main consumer thread
709 primaryEventHandler = new WidgetEventHandler(this, true);
710 (new Thread(primaryEventHandler)).start();
4328bb42
KL
711 }
712
2ce6dab2
KL
713 // ------------------------------------------------------------------------
714 // Screen refresh loop ----------------------------------------------------
715 // ------------------------------------------------------------------------
716
4328bb42 717 /**
bd8d51fa
KL
718 * Invert the cell color at a position. This is used to track the mouse.
719 *
720 * @param x column position
721 * @param y row position
4328bb42 722 */
bd8d51fa 723 private void invertCell(final int x, final int y) {
1d14ffab
KL
724 if (debugThreads) {
725 System.err.printf("invertCell() %d %d\n", x, y);
7b5261bc 726 }
1d14ffab
KL
727 CellAttributes attr = getScreen().getAttrXY(x, y);
728 attr.setForeColor(attr.getForeColor().invert());
729 attr.setBackColor(attr.getBackColor().invert());
730 getScreen().putAttrXY(x, y, attr, false);
4328bb42
KL
731 }
732
733 /**
734 * Draw everything.
735 */
7c870d89 736 private void drawAll() {
99144c71
KL
737 if (debugThreads) {
738 System.err.printf("drawAll() enter\n");
739 }
740
bd8d51fa 741 if (!repaint) {
1d14ffab
KL
742 if (debugThreads) {
743 System.err.printf("drawAll() !repaint\n");
bd8d51fa 744 }
1d14ffab
KL
745 synchronized (getScreen()) {
746 if ((oldMouseX != mouseX) || (oldMouseY != mouseY)) {
747 // The only thing that has happened is the mouse moved.
748 // Clear the old position and draw the new position.
749 invertCell(oldMouseX, oldMouseY);
750 invertCell(mouseX, mouseY);
751 oldMouseX = mouseX;
752 oldMouseY = mouseY;
753 }
754 if (getScreen().isDirty()) {
755 backend.flushScreen();
756 }
757 return;
bd8d51fa 758 }
7b5261bc
KL
759 }
760
99144c71
KL
761 if (debugThreads) {
762 System.err.printf("drawAll() REDRAW\n");
763 }
764
7b5261bc
KL
765 // If true, the cursor is not visible
766 boolean cursor = false;
767
768 // Start with a clean screen
a06459bd 769 getScreen().clear();
7b5261bc 770
0ee88b6d
KL
771 // Draw the desktop
772 if (desktop != null) {
773 desktop.drawChildren();
774 }
7b5261bc 775
7b5261bc 776 // Draw each window in reverse Z order
a06459bd
KL
777 List<TWindow> sorted = new LinkedList<TWindow>(windows);
778 Collections.sort(sorted);
e685a47d
KL
779 TWindow topLevel = null;
780 if (sorted.size() > 0) {
781 topLevel = sorted.get(0);
782 }
a06459bd
KL
783 Collections.reverse(sorted);
784 for (TWindow window: sorted) {
92453213
KL
785 if (window.isShown()) {
786 window.drawChildren();
787 }
7b5261bc
KL
788 }
789
790 // Draw the blank menubar line - reset the screen clipping first so
791 // it won't trim it out.
a06459bd
KL
792 getScreen().resetClipping();
793 getScreen().hLineXY(0, 0, getScreen().getWidth(), ' ',
7b5261bc
KL
794 theme.getColor("tmenu"));
795 // Now draw the menus.
796 int x = 1;
fca67db0 797 for (TMenu menu: menus) {
7b5261bc
KL
798 CellAttributes menuColor;
799 CellAttributes menuMnemonicColor;
7c870d89 800 if (menu.isActive()) {
7b5261bc
KL
801 menuColor = theme.getColor("tmenu.highlighted");
802 menuMnemonicColor = theme.getColor("tmenu.mnemonic.highlighted");
2ce6dab2 803 topLevel = menu;
7b5261bc
KL
804 } else {
805 menuColor = theme.getColor("tmenu");
806 menuMnemonicColor = theme.getColor("tmenu.mnemonic");
807 }
808 // Draw the menu title
fca67db0 809 getScreen().hLineXY(x, 0, menu.getTitle().length() + 2, ' ',
7b5261bc 810 menuColor);
0d47c546 811 getScreen().putStringXY(x + 1, 0, menu.getTitle(), menuColor);
7b5261bc 812 // Draw the highlight character
fca67db0
KL
813 getScreen().putCharXY(x + 1 + menu.getMnemonic().getShortcutIdx(),
814 0, menu.getMnemonic().getShortcut(), menuMnemonicColor);
7b5261bc 815
7c870d89 816 if (menu.isActive()) {
a06459bd 817 menu.drawChildren();
7b5261bc 818 // Reset the screen clipping so we can draw the next title.
a06459bd 819 getScreen().resetClipping();
7b5261bc 820 }
fca67db0 821 x += menu.getTitle().length() + 2;
7b5261bc
KL
822 }
823
a06459bd 824 for (TMenu menu: subMenus) {
7b5261bc 825 // Reset the screen clipping so we can draw the next sub-menu.
a06459bd
KL
826 getScreen().resetClipping();
827 menu.drawChildren();
7b5261bc 828 }
7b5261bc 829
2ce6dab2 830 // Draw the status bar of the top-level window
e685a47d
KL
831 TStatusBar statusBar = null;
832 if (topLevel != null) {
833 statusBar = topLevel.getStatusBar();
834 }
2ce6dab2
KL
835 if (statusBar != null) {
836 getScreen().resetClipping();
837 statusBar.setWidth(getScreen().getWidth());
838 statusBar.setY(getScreen().getHeight() - topLevel.getY());
839 statusBar.draw();
840 } else {
841 CellAttributes barColor = new CellAttributes();
842 barColor.setTo(getTheme().getColor("tstatusbar.text"));
843 getScreen().hLineXY(0, desktopBottom, getScreen().getWidth(), ' ',
844 barColor);
845 }
846
7b5261bc 847 // Draw the mouse pointer
bd8d51fa 848 invertCell(mouseX, mouseY);
1d14ffab
KL
849 oldMouseX = mouseX;
850 oldMouseY = mouseY;
7b5261bc 851
7b5261bc
KL
852 // Place the cursor if it is visible
853 TWidget activeWidget = null;
a06459bd
KL
854 if (sorted.size() > 0) {
855 activeWidget = sorted.get(sorted.size() - 1).getActiveChild();
7c870d89 856 if (activeWidget.isCursorVisible()) {
a06459bd 857 getScreen().putCursor(true, activeWidget.getCursorAbsoluteX(),
7b5261bc
KL
858 activeWidget.getCursorAbsoluteY());
859 cursor = true;
860 }
861 }
862
863 // Kill the cursor
fca67db0 864 if (!cursor) {
a06459bd 865 getScreen().hideCursor();
7b5261bc 866 }
7b5261bc
KL
867
868 // Flush the screen contents
1d14ffab
KL
869 if (getScreen().isDirty()) {
870 backend.flushScreen();
871 }
7b5261bc
KL
872
873 repaint = false;
4328bb42
KL
874 }
875
2ce6dab2
KL
876 // ------------------------------------------------------------------------
877 // Main loop --------------------------------------------------------------
878 // ------------------------------------------------------------------------
879
4328bb42 880 /**
7b5261bc 881 * Run this application until it exits.
4328bb42 882 */
a4406f4e 883 public void run() {
7b5261bc
KL
884 while (!quit) {
885 // Timeout is in milliseconds, so default timeout after 1 second
886 // of inactivity.
92453213 887 long timeout = 1000;
92554d64
KL
888
889 // If I've got no updates to render, wait for something from the
890 // backend or a timer.
bd8d51fa
KL
891 if (!repaint
892 && ((mouseX == oldMouseX) && (mouseY == oldMouseY))
893 ) {
e3dfbd23
KL
894 // Never sleep longer than 50 millis. We need time for
895 // windows with background tasks to update the display, and
896 // still flip buffers reasonably quickly in
897 // backend.flushPhysical().
898 timeout = getSleepTime(50);
8e688b92 899 }
92554d64
KL
900
901 if (timeout > 0) {
902 // As of now, I've got nothing to do: no I/O, nothing from
903 // the consumer threads, no timers that need to run ASAP. So
904 // wait until either the backend or the consumer threads have
905 // something to do.
906 try {
6358f6e5
KL
907 if (debugThreads) {
908 System.err.println("sleep " + timeout + " millis");
909 }
92554d64
KL
910 synchronized (this) {
911 this.wait(timeout);
912 }
913 } catch (InterruptedException e) {
914 // I'm awake and don't care why, let's see what's going
915 // on out there.
8e688b92 916 }
bd8d51fa 917 repaint = true;
7b5261bc
KL
918 }
919
ef368bd0
KL
920 // Prevent stepping on the primary or secondary event handler.
921 stopEventHandlers();
922
8e688b92 923 // Pull any pending I/O events
92554d64 924 backend.getEvents(fillEventQueue);
8e688b92
KL
925
926 // Dispatch each event to the appropriate handler, one at a time.
927 for (;;) {
928 TInputEvent event = null;
ef368bd0
KL
929 if (fillEventQueue.size() == 0) {
930 break;
8e688b92 931 }
ef368bd0 932 event = fillEventQueue.remove(0);
8e688b92
KL
933 metaHandleEvent(event);
934 }
7b5261bc 935
92554d64 936 // Wake a consumer thread if we have any pending events.
ef368bd0
KL
937 if (drainEventQueue.size() > 0) {
938 wakeEventHandler();
92554d64
KL
939 }
940
7b5261bc
KL
941 // Process timers and call doIdle()'s
942 doIdle();
943
944 // Update the screen
87a17f3c
KL
945 synchronized (getScreen()) {
946 drawAll();
947 }
99144c71
KL
948
949 // Let the event handlers run again.
950 startEventHandlers();
7b5261bc 951
92554d64
KL
952 } // while (!quit)
953
954 // Shutdown the event consumer threads
955 if (secondaryEventHandler != null) {
956 synchronized (secondaryEventHandler) {
957 secondaryEventHandler.notify();
958 }
959 }
960 if (primaryEventHandler != null) {
961 synchronized (primaryEventHandler) {
962 primaryEventHandler.notify();
963 }
7b5261bc
KL
964 }
965
92554d64 966 // Shutdown the user I/O thread(s)
7b5261bc 967 backend.shutdown();
92554d64
KL
968
969 // Close all the windows. This gives them an opportunity to release
970 // resources.
971 closeAllWindows();
972
4328bb42
KL
973 }
974
975 /**
976 * Peek at certain application-level events, add to eventQueue, and wake
8e688b92 977 * up the consuming Thread.
4328bb42 978 *
8e688b92 979 * @param event the input event to consume
4328bb42 980 */
8e688b92 981 private void metaHandleEvent(final TInputEvent event) {
7b5261bc 982
a83fea2b
KL
983 if (debugEvents) {
984 System.err.printf(String.format("metaHandleEvents event: %s\n",
985 event)); System.err.flush();
986 }
7b5261bc 987
8e688b92
KL
988 if (quit) {
989 // Do no more processing if the application is already trying
990 // to exit.
991 return;
992 }
7b5261bc 993
8e688b92 994 // Special application-wide events -------------------------------
7b5261bc 995
8e688b92
KL
996 // Abort everything
997 if (event instanceof TCommandEvent) {
998 TCommandEvent command = (TCommandEvent) event;
999 if (command.getCmd().equals(cmAbort)) {
1000 quit = true;
1001 return;
7b5261bc 1002 }
8e688b92 1003 }
7b5261bc 1004
8e688b92
KL
1005 // Screen resize
1006 if (event instanceof TResizeEvent) {
1007 TResizeEvent resize = (TResizeEvent) event;
bd8d51fa
KL
1008 synchronized (getScreen()) {
1009 getScreen().setDimensions(resize.getWidth(),
1010 resize.getHeight());
1011 desktopBottom = getScreen().getHeight() - 1;
1012 mouseX = 0;
1013 mouseY = 0;
1014 oldMouseX = 0;
1015 oldMouseY = 0;
1016 }
0ee88b6d
KL
1017 if (desktop != null) {
1018 desktop.setDimensions(0, 0, resize.getWidth(),
1019 resize.getHeight() - 1);
1020 }
8e688b92
KL
1021 return;
1022 }
7b5261bc 1023
8e688b92
KL
1024 // Peek at the mouse position
1025 if (event instanceof TMouseEvent) {
1026 TMouseEvent mouse = (TMouseEvent) event;
bd8d51fa
KL
1027 synchronized (getScreen()) {
1028 if ((mouseX != mouse.getX()) || (mouseY != mouse.getY())) {
1029 oldMouseX = mouseX;
1030 oldMouseY = mouseY;
1031 mouseX = mouse.getX();
1032 mouseY = mouse.getY();
1033 }
7b5261bc 1034 }
8e688b92 1035 }
7b5261bc 1036
bd8d51fa 1037 // Put into the main queue
ef368bd0 1038 drainEventQueue.add(event);
4328bb42
KL
1039 }
1040
a06459bd
KL
1041 /**
1042 * Dispatch one event to the appropriate widget or application-level
fca67db0
KL
1043 * event handler. This is the primary event handler, it has the normal
1044 * application-wide event handling.
a06459bd
KL
1045 *
1046 * @param event the input event to consume
fca67db0 1047 * @see #secondaryHandleEvent(TInputEvent event)
a06459bd 1048 */
fca67db0
KL
1049 private void primaryHandleEvent(final TInputEvent event) {
1050
a83fea2b
KL
1051 if (debugEvents) {
1052 System.err.printf("Handle event: %s\n", event);
1053 }
fca67db0
KL
1054
1055 // Special application-wide events -----------------------------------
1056
1057 // Peek at the mouse position
1058 if (event instanceof TMouseEvent) {
1059 // See if we need to switch focus to another window or the menu
1060 checkSwitchFocus((TMouseEvent) event);
1061 }
1062
1063 // Handle menu events
1064 if ((activeMenu != null) && !(event instanceof TCommandEvent)) {
1065 TMenu menu = activeMenu;
1066
1067 if (event instanceof TMouseEvent) {
1068 TMouseEvent mouse = (TMouseEvent) event;
1069
1070 while (subMenus.size() > 0) {
1071 TMenu subMenu = subMenus.get(subMenus.size() - 1);
1072 if (subMenu.mouseWouldHit(mouse)) {
1073 break;
1074 }
1075 if ((mouse.getType() == TMouseEvent.Type.MOUSE_MOTION)
7c870d89
KL
1076 && (!mouse.isMouse1())
1077 && (!mouse.isMouse2())
1078 && (!mouse.isMouse3())
1079 && (!mouse.isMouseWheelUp())
1080 && (!mouse.isMouseWheelDown())
fca67db0
KL
1081 ) {
1082 break;
1083 }
1084 // We navigated away from a sub-menu, so close it
1085 closeSubMenu();
1086 }
1087
1088 // Convert the mouse relative x/y to menu coordinates
1089 assert (mouse.getX() == mouse.getAbsoluteX());
1090 assert (mouse.getY() == mouse.getAbsoluteY());
1091 if (subMenus.size() > 0) {
1092 menu = subMenus.get(subMenus.size() - 1);
1093 }
1094 mouse.setX(mouse.getX() - menu.getX());
1095 mouse.setY(mouse.getY() - menu.getY());
1096 }
1097 menu.handleEvent(event);
1098 return;
1099 }
a06459bd 1100
fca67db0
KL
1101 if (event instanceof TKeypressEvent) {
1102 TKeypressEvent keypress = (TKeypressEvent) event;
e826b451 1103
5dfd1c11
KL
1104 // See if this key matches an accelerator, and is not being
1105 // shortcutted by the active window, and if so dispatch the menu
1106 // event.
1107 boolean windowWillShortcut = false;
92453213
KL
1108 if (activeWindow != null) {
1109 assert (activeWindow.isShown());
1110 if (activeWindow.isShortcutKeypress(keypress.getKey())) {
1111 // We do not process this key, it will be passed to the
1112 // window instead.
1113 windowWillShortcut = true;
5dfd1c11 1114 }
e826b451 1115 }
5dfd1c11 1116
2ce6dab2 1117 if (!windowWillShortcut && !modalWindowActive()) {
5dfd1c11
KL
1118 TKeypress keypressLowercase = keypress.getKey().toLowerCase();
1119 TMenuItem item = null;
1120 synchronized (accelerators) {
1121 item = accelerators.get(keypressLowercase);
1122 }
1123 if (item != null) {
1124 if (item.isEnabled()) {
1125 // Let the menu item dispatch
1126 item.dispatch();
1127 return;
1128 }
fca67db0 1129 }
5dfd1c11 1130
2ce6dab2
KL
1131 // Handle the keypress
1132 if (onKeypress(keypress)) {
1133 return;
1134 }
bd8d51fa 1135 }
fca67db0 1136 }
a06459bd 1137
fca67db0
KL
1138 if (event instanceof TCommandEvent) {
1139 if (onCommand((TCommandEvent) event)) {
1140 return;
1141 }
1142 }
1143
1144 if (event instanceof TMenuEvent) {
1145 if (onMenu((TMenuEvent) event)) {
1146 return;
1147 }
1148 }
1149
1150 // Dispatch events to the active window -------------------------------
0ee88b6d 1151 boolean dispatchToDesktop = true;
92453213
KL
1152 TWindow window = activeWindow;
1153 if (window != null) {
1154 assert (window.isActive());
1155 assert (window.isShown());
1156 if (event instanceof TMouseEvent) {
1157 TMouseEvent mouse = (TMouseEvent) event;
1158 // Convert the mouse relative x/y to window coordinates
1159 assert (mouse.getX() == mouse.getAbsoluteX());
1160 assert (mouse.getY() == mouse.getAbsoluteY());
1161 mouse.setX(mouse.getX() - window.getX());
1162 mouse.setY(mouse.getY() - window.getY());
1163
1164 if (window.mouseWouldHit(mouse)) {
0ee88b6d 1165 dispatchToDesktop = false;
fca67db0 1166 }
92453213
KL
1167 } else if (event instanceof TKeypressEvent) {
1168 dispatchToDesktop = false;
1169 }
0ee88b6d 1170
92453213
KL
1171 if (debugEvents) {
1172 System.err.printf("TApplication dispatch event: %s\n",
1173 event);
fca67db0 1174 }
92453213 1175 window.handleEvent(event);
fca67db0 1176 }
0ee88b6d
KL
1177 if (dispatchToDesktop) {
1178 // This event is fair game for the desktop to process.
1179 if (desktop != null) {
1180 desktop.handleEvent(event);
1181 }
1182 }
fca67db0 1183 }
0ee88b6d 1184
fca67db0
KL
1185 /**
1186 * Dispatch one event to the appropriate widget or application-level
1187 * event handler. This is the secondary event handler used by certain
1188 * special dialogs (currently TMessageBox and TFileOpenBox).
1189 *
1190 * @param event the input event to consume
1191 * @see #primaryHandleEvent(TInputEvent event)
1192 */
1193 private void secondaryHandleEvent(final TInputEvent event) {
c6940ed9
KL
1194 secondaryEventReceiver.handleEvent(event);
1195 }
1196
1197 /**
1198 * Enable a widget to override the primary event thread.
1199 *
1200 * @param widget widget that will receive events
1201 */
1202 public final void enableSecondaryEventReceiver(final TWidget widget) {
1203 assert (secondaryEventReceiver == null);
1204 assert (secondaryEventHandler == null);
a043164f
KL
1205 assert ((widget instanceof TMessageBox)
1206 || (widget instanceof TFileOpenBox));
c6940ed9
KL
1207 secondaryEventReceiver = widget;
1208 secondaryEventHandler = new WidgetEventHandler(this, false);
1209 (new Thread(secondaryEventHandler)).start();
c6940ed9
KL
1210 }
1211
1212 /**
1213 * Yield to the secondary thread.
1214 */
1215 public final void yield() {
1216 assert (secondaryEventReceiver != null);
99144c71
KL
1217 // This is where we handoff the event handler lock from the primary
1218 // to secondary thread. We unlock here, and in a future loop the
1219 // secondary thread locks again. When it gives up, we have the
1220 // single lock back.
1221 boolean oldLock = unlockHandleEvent();
329fd62e 1222 assert (oldLock);
99144c71 1223
c6940ed9 1224 while (secondaryEventReceiver != null) {
92554d64 1225 synchronized (primaryEventHandler) {
c6940ed9 1226 try {
92554d64 1227 primaryEventHandler.wait();
c6940ed9
KL
1228 } catch (InterruptedException e) {
1229 // SQUASH
1230 }
1231 }
1232 }
a06459bd
KL
1233 }
1234
4328bb42
KL
1235 /**
1236 * Do stuff when there is no user input.
1237 */
1238 private void doIdle() {
99144c71
KL
1239 if (debugThreads) {
1240 System.err.printf("doIdle()\n");
1241 }
1242
7b5261bc 1243 // Now run any timers that have timed out
d502a0e9
KL
1244 Date now = new Date();
1245 List<TTimer> keepTimers = new LinkedList<TTimer>();
1246 for (TTimer timer: timers) {
92554d64 1247 if (timer.getNextTick().getTime() <= now.getTime()) {
d502a0e9 1248 timer.tick();
c6940ed9 1249 if (timer.recurring) {
d502a0e9 1250 keepTimers.add(timer);
7b5261bc
KL
1251 }
1252 } else {
d502a0e9 1253 keepTimers.add(timer);
7b5261bc
KL
1254 }
1255 }
1256 timers = keepTimers;
1257
1258 // Call onIdle's
d502a0e9
KL
1259 for (TWindow window: windows) {
1260 window.onIdle();
7b5261bc 1261 }
92453213
KL
1262 if (desktop != null) {
1263 desktop.onIdle();
1264 }
4328bb42 1265 }
7d4115a5 1266
2ce6dab2
KL
1267 // ------------------------------------------------------------------------
1268 // TWindow management -----------------------------------------------------
1269 // ------------------------------------------------------------------------
4328bb42 1270
92453213
KL
1271 /**
1272 * Return the total number of windows.
1273 *
1274 * @return the total number of windows
1275 */
1276 public final int windowCount() {
1277 return windows.size();
1278 }
1279
1280 /**
1281 * Return the number of windows that are visible.
1282 *
1283 * @return the number of windows that are visible
1284 */
1285 public final int shownWindowCount() {
1286 int n = 0;
1287 for (TWindow w: windows) {
1288 if (w.isShown()) {
1289 n++;
1290 }
1291 }
1292 return n;
1293 }
1294
1295 /**
1296 * Check if a window instance is in this application's window list.
1297 *
1298 * @param window window to look for
1299 * @return true if this window is in the list
1300 */
1301 public final boolean hasWindow(final TWindow window) {
1302 if (windows.size() == 0) {
1303 return false;
1304 }
1305 for (TWindow w: windows) {
1306 if (w == window) {
1307 return true;
1308 }
1309 }
1310 return false;
1311 }
1312
1313 /**
1314 * Activate a window: bring it to the top and have it receive events.
1315 *
1316 * @param window the window to become the new active window
1317 */
1318 public void activateWindow(final TWindow window) {
1319 if (hasWindow(window) == false) {
1320 /*
1321 * Someone has a handle to a window I don't have. Ignore this
1322 * request.
1323 */
1324 return;
1325 }
1326
1327 assert (windows.size() > 0);
1328
1329 if (window.isHidden()) {
1330 // Unhiding will also activate.
1331 showWindow(window);
1332 return;
1333 }
1334 assert (window.isShown());
1335
1336 if (windows.size() == 1) {
1337 assert (window == windows.get(0));
1338 if (activeWindow == null) {
1339 activeWindow = window;
1340 window.setZ(0);
1341 activeWindow.setActive(true);
1342 activeWindow.onFocus();
1343 }
1344
1345 assert (window.isActive());
1346 assert (activeWindow == window);
1347 return;
1348 }
1349
1350 if (activeWindow == window) {
1351 assert (window.isActive());
1352
1353 // Window is already active, do nothing.
1354 return;
1355 }
1356
1357 assert (!window.isActive());
1358 if (activeWindow != null) {
1359 assert (activeWindow.getZ() == 0);
1360
1361 activeWindow.onUnfocus();
1362 activeWindow.setActive(false);
1363 activeWindow.setZ(window.getZ());
1364 }
1365 activeWindow = window;
1366 activeWindow.setZ(0);
1367 activeWindow.setActive(true);
1368 activeWindow.onFocus();
1369 return;
1370 }
1371
1372 /**
1373 * Hide a window.
1374 *
1375 * @param window the window to hide
1376 */
1377 public void hideWindow(final TWindow window) {
1378 if (hasWindow(window) == false) {
1379 /*
1380 * Someone has a handle to a window I don't have. Ignore this
1381 * request.
1382 */
1383 return;
1384 }
1385
1386 assert (windows.size() > 0);
1387
1388 if (!window.hidden) {
1389 if (window == activeWindow) {
1390 if (shownWindowCount() > 1) {
1391 switchWindow(true);
1392 } else {
1393 activeWindow = null;
1394 window.setActive(false);
1395 window.onUnfocus();
1396 }
1397 }
1398 window.hidden = true;
1399 window.onHide();
1400 }
1401 }
1402
1403 /**
1404 * Show a window.
1405 *
1406 * @param window the window to show
1407 */
1408 public void showWindow(final TWindow window) {
1409 if (hasWindow(window) == false) {
1410 /*
1411 * Someone has a handle to a window I don't have. Ignore this
1412 * request.
1413 */
1414 return;
1415 }
1416
1417 assert (windows.size() > 0);
1418
1419 if (window.hidden) {
1420 window.hidden = false;
1421 window.onShow();
1422 activateWindow(window);
1423 }
1424 }
1425
48e27807
KL
1426 /**
1427 * Close window. Note that the window's destructor is NOT called by this
1428 * method, instead the GC is assumed to do the cleanup.
1429 *
1430 * @param window the window to remove
1431 */
1432 public final void closeWindow(final TWindow window) {
92453213
KL
1433 if (hasWindow(window) == false) {
1434 /*
1435 * Someone has a handle to a window I don't have. Ignore this
1436 * request.
1437 */
1438 return;
1439 }
1440
bb35d919
KL
1441 synchronized (windows) {
1442 int z = window.getZ();
1443 window.setZ(-1);
efb7af1f 1444 window.onUnfocus();
bb35d919
KL
1445 Collections.sort(windows);
1446 windows.remove(0);
92453213 1447 activeWindow = null;
bb35d919
KL
1448 for (TWindow w: windows) {
1449 if (w.getZ() > z) {
1450 w.setZ(w.getZ() - 1);
1451 if (w.getZ() == 0) {
1452 w.setActive(true);
efb7af1f 1453 w.onFocus();
bb35d919
KL
1454 assert (activeWindow == null);
1455 activeWindow = w;
1456 } else {
efb7af1f
KL
1457 if (w.isActive()) {
1458 w.setActive(false);
1459 w.onUnfocus();
1460 }
bb35d919 1461 }
48e27807
KL
1462 }
1463 }
1464 }
1465
1466 // Perform window cleanup
1467 window.onClose();
1468
48e27807 1469 // Check if we are closing a TMessageBox or similar
c6940ed9
KL
1470 if (secondaryEventReceiver != null) {
1471 assert (secondaryEventHandler != null);
48e27807
KL
1472
1473 // Do not send events to the secondaryEventReceiver anymore, the
1474 // window is closed.
1475 secondaryEventReceiver = null;
1476
92554d64
KL
1477 // Wake the secondary thread, it will wake the primary as it
1478 // exits.
1479 synchronized (secondaryEventHandler) {
1480 secondaryEventHandler.notify();
48e27807
KL
1481 }
1482 }
92453213
KL
1483
1484 // Permit desktop to be active if it is the only thing left.
1485 if (desktop != null) {
1486 if (windows.size() == 0) {
1487 desktop.setActive(true);
1488 }
1489 }
48e27807
KL
1490 }
1491
1492 /**
1493 * Switch to the next window.
1494 *
1495 * @param forward if true, then switch to the next window in the list,
1496 * otherwise switch to the previous window in the list
1497 */
1498 public final void switchWindow(final boolean forward) {
48e27807 1499 // Only switch if there are multiple windows
fca67db0 1500 if (windows.size() < 2) {
48e27807
KL
1501 return;
1502 }
92453213 1503 assert (activeWindow != null);
48e27807 1504
bb35d919
KL
1505 synchronized (windows) {
1506
1507 // Swap z/active between active window and the next in the list
1508 int activeWindowI = -1;
1509 for (int i = 0; i < windows.size(); i++) {
92453213
KL
1510 if (windows.get(i) == activeWindow) {
1511 assert (activeWindow.isActive());
bb35d919
KL
1512 activeWindowI = i;
1513 break;
92453213
KL
1514 } else {
1515 assert (!windows.get(0).isActive());
bb35d919 1516 }
48e27807 1517 }
bb35d919 1518 assert (activeWindowI >= 0);
48e27807 1519
bb35d919 1520 // Do not switch if a window is modal
92453213 1521 if (activeWindow.isModal()) {
bb35d919
KL
1522 return;
1523 }
48e27807 1524
bb35d919
KL
1525 int nextWindowI;
1526 if (forward) {
1527 nextWindowI = (activeWindowI + 1) % windows.size();
48e27807 1528 } else {
bb35d919
KL
1529 if (activeWindowI == 0) {
1530 nextWindowI = windows.size() - 1;
1531 } else {
1532 nextWindowI = activeWindowI - 1;
1533 }
48e27807 1534 }
bb35d919 1535
92453213 1536 activateWindow(windows.get(nextWindowI));
bb35d919 1537 } // synchronized (windows)
48e27807 1538
48e27807
KL
1539 }
1540
1541 /**
1542 * Add a window to my window list and make it active.
1543 *
1544 * @param window new window to add
1545 */
1546 public final void addWindow(final TWindow window) {
a7986f7b
KL
1547
1548 // Do not add menu windows to the window list.
1549 if (window instanceof TMenu) {
1550 return;
1551 }
1552
0ee88b6d
KL
1553 // Do not add the desktop to the window list.
1554 if (window instanceof TDesktop) {
1555 return;
1556 }
1557
bb35d919 1558 synchronized (windows) {
2ce6dab2
KL
1559 // Do not allow a modal window to spawn a non-modal window. If a
1560 // modal window is active, then this window will become modal
1561 // too.
1562 if (modalWindowActive()) {
1563 window.flags |= TWindow.MODAL;
a7986f7b 1564 window.flags |= TWindow.CENTERED;
92453213 1565 window.hidden = false;
bb35d919 1566 }
92453213
KL
1567 if (window.isShown()) {
1568 for (TWindow w: windows) {
1569 if (w.isActive()) {
1570 w.setActive(false);
1571 w.onUnfocus();
1572 }
1573 w.setZ(w.getZ() + 1);
efb7af1f 1574 }
bb35d919
KL
1575 }
1576 windows.add(window);
92453213
KL
1577 if (window.isShown()) {
1578 activeWindow = window;
1579 activeWindow.setZ(0);
1580 activeWindow.setActive(true);
1581 activeWindow.onFocus();
1582 }
a7986f7b
KL
1583
1584 if (((window.flags & TWindow.CENTERED) == 0)
1585 && smartWindowPlacement) {
1586
1587 doSmartPlacement(window);
1588 }
48e27807 1589 }
92453213
KL
1590
1591 // Desktop cannot be active over any other window.
1592 if (desktop != null) {
1593 desktop.setActive(false);
1594 }
48e27807
KL
1595 }
1596
fca67db0
KL
1597 /**
1598 * Check if there is a system-modal window on top.
1599 *
1600 * @return true if the active window is modal
1601 */
1602 private boolean modalWindowActive() {
1603 if (windows.size() == 0) {
1604 return false;
1605 }
2ce6dab2
KL
1606
1607 for (TWindow w: windows) {
1608 if (w.isModal()) {
1609 return true;
1610 }
1611 }
1612
1613 return false;
1614 }
1615
1616 /**
1617 * Close all open windows.
1618 */
1619 private void closeAllWindows() {
1620 // Don't do anything if we are in the menu
1621 if (activeMenu != null) {
1622 return;
1623 }
1624 while (windows.size() > 0) {
1625 closeWindow(windows.get(0));
1626 }
fca67db0
KL
1627 }
1628
2ce6dab2
KL
1629 /**
1630 * Re-layout the open windows as non-overlapping tiles. This produces
1631 * almost the same results as Turbo Pascal 7.0's IDE.
1632 */
1633 private void tileWindows() {
1634 synchronized (windows) {
1635 // Don't do anything if we are in the menu
1636 if (activeMenu != null) {
1637 return;
1638 }
1639 int z = windows.size();
1640 if (z == 0) {
1641 return;
1642 }
1643 int a = 0;
1644 int b = 0;
1645 a = (int)(Math.sqrt(z));
1646 int c = 0;
1647 while (c < a) {
1648 b = (z - c) / a;
1649 if (((a * b) + c) == z) {
1650 break;
1651 }
1652 c++;
1653 }
1654 assert (a > 0);
1655 assert (b > 0);
1656 assert (c < a);
1657 int newWidth = (getScreen().getWidth() / a);
1658 int newHeight1 = ((getScreen().getHeight() - 1) / b);
1659 int newHeight2 = ((getScreen().getHeight() - 1) / (b + c));
1660
1661 List<TWindow> sorted = new LinkedList<TWindow>(windows);
1662 Collections.sort(sorted);
1663 Collections.reverse(sorted);
1664 for (int i = 0; i < sorted.size(); i++) {
1665 int logicalX = i / b;
1666 int logicalY = i % b;
1667 if (i >= ((a - 1) * b)) {
1668 logicalX = a - 1;
1669 logicalY = i - ((a - 1) * b);
1670 }
1671
1672 TWindow w = sorted.get(i);
1673 w.setX(logicalX * newWidth);
1674 w.setWidth(newWidth);
1675 if (i >= ((a - 1) * b)) {
1676 w.setY((logicalY * newHeight2) + 1);
1677 w.setHeight(newHeight2);
1678 } else {
1679 w.setY((logicalY * newHeight1) + 1);
1680 w.setHeight(newHeight1);
1681 }
1682 }
1683 }
1684 }
1685
1686 /**
1687 * Re-layout the open windows as overlapping cascaded windows.
1688 */
1689 private void cascadeWindows() {
1690 synchronized (windows) {
1691 // Don't do anything if we are in the menu
1692 if (activeMenu != null) {
1693 return;
1694 }
1695 int x = 0;
1696 int y = 1;
1697 List<TWindow> sorted = new LinkedList<TWindow>(windows);
1698 Collections.sort(sorted);
1699 Collections.reverse(sorted);
1700 for (TWindow window: sorted) {
1701 window.setX(x);
1702 window.setY(y);
1703 x++;
1704 y++;
1705 if (x > getScreen().getWidth()) {
1706 x = 0;
1707 }
1708 if (y >= getScreen().getHeight()) {
1709 y = 1;
1710 }
1711 }
1712 }
1713 }
1714
a7986f7b
KL
1715 /**
1716 * Place a window to minimize its overlap with other windows.
1717 *
1718 * @param window the window to place
1719 */
1720 public final void doSmartPlacement(final TWindow window) {
1721 // This is a pretty dumb algorithm, but seems to work. The hardest
1722 // part is computing these "overlap" values seeking a minimum average
1723 // overlap.
1724 int xMin = 0;
1725 int yMin = desktopTop;
1726 int xMax = getScreen().getWidth() - window.getWidth() + 1;
1727 int yMax = desktopBottom - window.getHeight() + 1;
1728 if (xMax < xMin) {
1729 xMax = xMin;
1730 }
1731 if (yMax < yMin) {
1732 yMax = yMin;
1733 }
1734
1735 if ((xMin == xMax) && (yMin == yMax)) {
1736 // No work to do, bail out.
1737 return;
1738 }
1739
1740 // Compute the overlap matrix without the new window.
1741 int width = getScreen().getWidth();
1742 int height = getScreen().getHeight();
1743 int overlapMatrix[][] = new int[width][height];
1744 for (TWindow w: windows) {
1745 if (window == w) {
1746 continue;
1747 }
1748 for (int x = w.getX(); x < w.getX() + w.getWidth(); x++) {
1749 if (x == width) {
1750 continue;
1751 }
1752 for (int y = w.getY(); y < w.getY() + w.getHeight(); y++) {
1753 if (y == height) {
1754 continue;
1755 }
1756 overlapMatrix[x][y]++;
1757 }
1758 }
1759 }
1760
1761 long oldOverlapTotal = 0;
1762 long oldOverlapN = 0;
1763 for (int x = 0; x < width; x++) {
1764 for (int y = 0; y < height; y++) {
1765 oldOverlapTotal += overlapMatrix[x][y];
1766 if (overlapMatrix[x][y] > 0) {
1767 oldOverlapN++;
1768 }
1769 }
1770 }
1771
1772
1773 double oldOverlapAvg = (double) oldOverlapTotal / (double) oldOverlapN;
1774 boolean first = true;
1775 int windowX = window.getX();
1776 int windowY = window.getY();
1777
1778 // For each possible (x, y) position for the new window, compute a
1779 // new overlap matrix.
1780 for (int x = xMin; x < xMax; x++) {
1781 for (int y = yMin; y < yMax; y++) {
1782
1783 // Start with the matrix minus this window.
1784 int newMatrix[][] = new int[width][height];
1785 for (int mx = 0; mx < width; mx++) {
1786 for (int my = 0; my < height; my++) {
1787 newMatrix[mx][my] = overlapMatrix[mx][my];
1788 }
1789 }
1790
1791 // Add this window's values to the new overlap matrix.
1792 long newOverlapTotal = 0;
1793 long newOverlapN = 0;
1794 // Start by adding each new cell.
1795 for (int wx = x; wx < x + window.getWidth(); wx++) {
1796 if (wx == width) {
1797 continue;
1798 }
1799 for (int wy = y; wy < y + window.getHeight(); wy++) {
1800 if (wy == height) {
1801 continue;
1802 }
1803 newMatrix[wx][wy]++;
1804 }
1805 }
1806 // Now figure out the new value for total coverage.
1807 for (int mx = 0; mx < width; mx++) {
1808 for (int my = 0; my < height; my++) {
1809 newOverlapTotal += newMatrix[x][y];
1810 if (newMatrix[mx][my] > 0) {
1811 newOverlapN++;
1812 }
1813 }
1814 }
1815 double newOverlapAvg = (double) newOverlapTotal / (double) newOverlapN;
1816
1817 if (first) {
1818 // First time: just record what we got.
1819 oldOverlapAvg = newOverlapAvg;
1820 first = false;
1821 } else {
1822 // All other times: pick a new best (x, y) and save the
1823 // overlap value.
1824 if (newOverlapAvg < oldOverlapAvg) {
1825 windowX = x;
1826 windowY = y;
1827 oldOverlapAvg = newOverlapAvg;
1828 }
1829 }
1830
1831 } // for (int x = xMin; x < xMax; x++)
1832
1833 } // for (int y = yMin; y < yMax; y++)
1834
1835 // Finally, set the window's new coordinates.
1836 window.setX(windowX);
1837 window.setY(windowY);
1838 }
1839
2ce6dab2
KL
1840 // ------------------------------------------------------------------------
1841 // TMenu management -------------------------------------------------------
1842 // ------------------------------------------------------------------------
1843
fca67db0
KL
1844 /**
1845 * Check if a mouse event would hit either the active menu or any open
1846 * sub-menus.
1847 *
1848 * @param mouse mouse event
1849 * @return true if the mouse would hit the active menu or an open
1850 * sub-menu
1851 */
1852 private boolean mouseOnMenu(final TMouseEvent mouse) {
1853 assert (activeMenu != null);
1854 List<TMenu> menus = new LinkedList<TMenu>(subMenus);
1855 Collections.reverse(menus);
1856 for (TMenu menu: menus) {
1857 if (menu.mouseWouldHit(mouse)) {
1858 return true;
1859 }
1860 }
1861 return activeMenu.mouseWouldHit(mouse);
1862 }
1863
1864 /**
1865 * See if we need to switch window or activate the menu based on
1866 * a mouse click.
1867 *
1868 * @param mouse mouse event
1869 */
1870 private void checkSwitchFocus(final TMouseEvent mouse) {
1871
1872 if ((mouse.getType() == TMouseEvent.Type.MOUSE_DOWN)
1873 && (activeMenu != null)
1874 && (mouse.getAbsoluteY() != 0)
1875 && (!mouseOnMenu(mouse))
1876 ) {
1877 // They clicked outside the active menu, turn it off
1878 activeMenu.setActive(false);
1879 activeMenu = null;
1880 for (TMenu menu: subMenus) {
1881 menu.setActive(false);
1882 }
1883 subMenus.clear();
1884 // Continue checks
1885 }
1886
1887 // See if they hit the menu bar
1888 if ((mouse.getType() == TMouseEvent.Type.MOUSE_DOWN)
7c870d89 1889 && (mouse.isMouse1())
fca67db0
KL
1890 && (!modalWindowActive())
1891 && (mouse.getAbsoluteY() == 0)
1892 ) {
1893
1894 for (TMenu menu: subMenus) {
1895 menu.setActive(false);
1896 }
1897 subMenus.clear();
1898
1899 // They selected the menu, go activate it
1900 for (TMenu menu: menus) {
1901 if ((mouse.getAbsoluteX() >= menu.getX())
1902 && (mouse.getAbsoluteX() < menu.getX()
1903 + menu.getTitle().length() + 2)
1904 ) {
1905 menu.setActive(true);
1906 activeMenu = menu;
1907 } else {
1908 menu.setActive(false);
1909 }
1910 }
fca67db0
KL
1911 return;
1912 }
1913
1914 // See if they hit the menu bar
1915 if ((mouse.getType() == TMouseEvent.Type.MOUSE_MOTION)
7c870d89 1916 && (mouse.isMouse1())
fca67db0
KL
1917 && (activeMenu != null)
1918 && (mouse.getAbsoluteY() == 0)
1919 ) {
1920
1921 TMenu oldMenu = activeMenu;
1922 for (TMenu menu: subMenus) {
1923 menu.setActive(false);
1924 }
1925 subMenus.clear();
1926
1927 // See if we should switch menus
1928 for (TMenu menu: menus) {
1929 if ((mouse.getAbsoluteX() >= menu.getX())
1930 && (mouse.getAbsoluteX() < menu.getX()
1931 + menu.getTitle().length() + 2)
1932 ) {
1933 menu.setActive(true);
1934 activeMenu = menu;
1935 }
1936 }
1937 if (oldMenu != activeMenu) {
1938 // They switched menus
1939 oldMenu.setActive(false);
1940 }
fca67db0
KL
1941 return;
1942 }
1943
1944 // Only switch if there are multiple windows
1945 if (windows.size() < 2) {
1946 return;
1947 }
1948
1949 // Switch on the upclick
1950 if (mouse.getType() != TMouseEvent.Type.MOUSE_UP) {
1951 return;
1952 }
1953
bb35d919
KL
1954 synchronized (windows) {
1955 Collections.sort(windows);
1956 if (windows.get(0).isModal()) {
1957 // Modal windows don't switch
1958 return;
1959 }
fca67db0 1960
bb35d919
KL
1961 for (TWindow window: windows) {
1962 assert (!window.isModal());
92453213
KL
1963
1964 if (window.isHidden()) {
1965 assert (!window.isActive());
1966 continue;
1967 }
1968
bb35d919
KL
1969 if (window.mouseWouldHit(mouse)) {
1970 if (window == windows.get(0)) {
1971 // Clicked on the same window, nothing to do
92453213 1972 assert (window.isActive());
bb35d919
KL
1973 return;
1974 }
1975
1976 // We will be switching to another window
7c870d89 1977 assert (windows.get(0).isActive());
92453213 1978 assert (windows.get(0) == activeWindow);
7c870d89 1979 assert (!window.isActive());
92453213
KL
1980 activeWindow.onUnfocus();
1981 activeWindow.setActive(false);
1982 activeWindow.setZ(window.getZ());
1983 activeWindow = window;
bb35d919
KL
1984 window.setZ(0);
1985 window.setActive(true);
efb7af1f 1986 window.onFocus();
fca67db0
KL
1987 return;
1988 }
fca67db0
KL
1989 }
1990 }
1991
1992 // Clicked on the background, nothing to do
1993 return;
1994 }
1995
1996 /**
1997 * Turn off the menu.
1998 */
928811d8 1999 public final void closeMenu() {
fca67db0
KL
2000 if (activeMenu != null) {
2001 activeMenu.setActive(false);
2002 activeMenu = null;
2003 for (TMenu menu: subMenus) {
2004 menu.setActive(false);
2005 }
2006 subMenus.clear();
2007 }
fca67db0
KL
2008 }
2009
2010 /**
2011 * Turn off a sub-menu.
2012 */
928811d8 2013 public final void closeSubMenu() {
fca67db0
KL
2014 assert (activeMenu != null);
2015 TMenu item = subMenus.get(subMenus.size() - 1);
2016 assert (item != null);
2017 item.setActive(false);
2018 subMenus.remove(subMenus.size() - 1);
fca67db0
KL
2019 }
2020
2021 /**
2022 * Switch to the next menu.
2023 *
2024 * @param forward if true, then switch to the next menu in the list,
2025 * otherwise switch to the previous menu in the list
2026 */
928811d8 2027 public final void switchMenu(final boolean forward) {
fca67db0
KL
2028 assert (activeMenu != null);
2029
2030 for (TMenu menu: subMenus) {
2031 menu.setActive(false);
2032 }
2033 subMenus.clear();
2034
2035 for (int i = 0; i < menus.size(); i++) {
2036 if (activeMenu == menus.get(i)) {
2037 if (forward) {
2038 if (i < menus.size() - 1) {
2039 i++;
2040 }
2041 } else {
2042 if (i > 0) {
2043 i--;
2044 }
2045 }
2046 activeMenu.setActive(false);
2047 activeMenu = menus.get(i);
2048 activeMenu.setActive(true);
fca67db0
KL
2049 return;
2050 }
2051 }
2052 }
2053
928811d8 2054 /**
efb7af1f
KL
2055 * Add a menu item to the global list. If it has a keyboard accelerator,
2056 * that will be added the global hash.
928811d8 2057 *
efb7af1f 2058 * @param item the menu item
928811d8 2059 */
efb7af1f
KL
2060 public final void addMenuItem(final TMenuItem item) {
2061 menuItems.add(item);
2062
2063 TKeypress key = item.getKey();
2064 if (key != null) {
2065 synchronized (accelerators) {
2066 assert (accelerators.get(key) == null);
2067 accelerators.put(key.toLowerCase(), item);
2068 }
2069 }
2070 }
2071
2072 /**
2073 * Disable one menu item.
2074 *
2075 * @param id the menu item ID
2076 */
2077 public final void disableMenuItem(final int id) {
2078 for (TMenuItem item: menuItems) {
2079 if (item.getId() == id) {
2080 item.setEnabled(false);
2081 }
2082 }
2083 }
e826b451 2084
efb7af1f
KL
2085 /**
2086 * Disable the range of menu items with ID's between lower and upper,
2087 * inclusive.
2088 *
2089 * @param lower the lowest menu item ID
2090 * @param upper the highest menu item ID
2091 */
2092 public final void disableMenuItems(final int lower, final int upper) {
2093 for (TMenuItem item: menuItems) {
2094 if ((item.getId() >= lower) && (item.getId() <= upper)) {
2095 item.setEnabled(false);
2096 }
2097 }
2098 }
2099
2100 /**
2101 * Enable one menu item.
2102 *
2103 * @param id the menu item ID
2104 */
2105 public final void enableMenuItem(final int id) {
2106 for (TMenuItem item: menuItems) {
2107 if (item.getId() == id) {
2108 item.setEnabled(true);
2109 }
2110 }
2111 }
2112
2113 /**
2114 * Enable the range of menu items with ID's between lower and upper,
2115 * inclusive.
2116 *
2117 * @param lower the lowest menu item ID
2118 * @param upper the highest menu item ID
2119 */
2120 public final void enableMenuItems(final int lower, final int upper) {
2121 for (TMenuItem item: menuItems) {
2122 if ((item.getId() >= lower) && (item.getId() <= upper)) {
2123 item.setEnabled(true);
2124 }
e826b451 2125 }
928811d8
KL
2126 }
2127
2128 /**
2129 * Recompute menu x positions based on their title length.
2130 */
2131 public final void recomputeMenuX() {
2132 int x = 0;
2133 for (TMenu menu: menus) {
2134 menu.setX(x);
2135 x += menu.getTitle().length() + 2;
2136 }
2137 }
2138
2139 /**
2140 * Post an event to process and turn off the menu.
2141 *
2142 * @param event new event to add to the queue
2143 */
5dfd1c11 2144 public final void postMenuEvent(final TInputEvent event) {
8e688b92
KL
2145 synchronized (fillEventQueue) {
2146 fillEventQueue.add(event);
2147 }
928811d8
KL
2148 closeMenu();
2149 }
2150
2151 /**
2152 * Add a sub-menu to the list of open sub-menus.
2153 *
2154 * @param menu sub-menu
2155 */
2156 public final void addSubMenu(final TMenu menu) {
2157 subMenus.add(menu);
2158 }
2159
8e688b92
KL
2160 /**
2161 * Convenience function to add a top-level menu.
2162 *
2163 * @param title menu title
2164 * @return the new menu
2165 */
87a17f3c 2166 public final TMenu addMenu(final String title) {
8e688b92
KL
2167 int x = 0;
2168 int y = 0;
2169 TMenu menu = new TMenu(this, x, y, title);
2170 menus.add(menu);
2171 recomputeMenuX();
2172 return menu;
2173 }
2174
2175 /**
2176 * Convenience function to add a default "File" menu.
2177 *
2178 * @return the new menu
2179 */
2180 public final TMenu addFileMenu() {
2181 TMenu fileMenu = addMenu("&File");
2182 fileMenu.addDefaultItem(TMenu.MID_OPEN_FILE);
2183 fileMenu.addSeparator();
2184 fileMenu.addDefaultItem(TMenu.MID_SHELL);
2185 fileMenu.addDefaultItem(TMenu.MID_EXIT);
2ce6dab2
KL
2186 TStatusBar statusBar = fileMenu.newStatusBar("File-management " +
2187 "commands (Open, Save, Print, etc.)");
2188 statusBar.addShortcutKeypress(kbF1, cmHelp, "Help");
8e688b92
KL
2189 return fileMenu;
2190 }
2191
2192 /**
2193 * Convenience function to add a default "Edit" menu.
2194 *
2195 * @return the new menu
2196 */
2197 public final TMenu addEditMenu() {
2198 TMenu editMenu = addMenu("&Edit");
2199 editMenu.addDefaultItem(TMenu.MID_CUT);
2200 editMenu.addDefaultItem(TMenu.MID_COPY);
2201 editMenu.addDefaultItem(TMenu.MID_PASTE);
2202 editMenu.addDefaultItem(TMenu.MID_CLEAR);
2ce6dab2
KL
2203 TStatusBar statusBar = editMenu.newStatusBar("Editor operations, " +
2204 "undo, and Clipboard access");
2205 statusBar.addShortcutKeypress(kbF1, cmHelp, "Help");
8e688b92
KL
2206 return editMenu;
2207 }
2208
2209 /**
2210 * Convenience function to add a default "Window" menu.
2211 *
2212 * @return the new menu
2213 */
c6940ed9 2214 public final TMenu addWindowMenu() {
8e688b92
KL
2215 TMenu windowMenu = addMenu("&Window");
2216 windowMenu.addDefaultItem(TMenu.MID_TILE);
2217 windowMenu.addDefaultItem(TMenu.MID_CASCADE);
2218 windowMenu.addDefaultItem(TMenu.MID_CLOSE_ALL);
2219 windowMenu.addSeparator();
2220 windowMenu.addDefaultItem(TMenu.MID_WINDOW_MOVE);
2221 windowMenu.addDefaultItem(TMenu.MID_WINDOW_ZOOM);
2222 windowMenu.addDefaultItem(TMenu.MID_WINDOW_NEXT);
2223 windowMenu.addDefaultItem(TMenu.MID_WINDOW_PREVIOUS);
2224 windowMenu.addDefaultItem(TMenu.MID_WINDOW_CLOSE);
2ce6dab2
KL
2225 TStatusBar statusBar = windowMenu.newStatusBar("Open, arrange, and " +
2226 "list windows");
2227 statusBar.addShortcutKeypress(kbF1, cmHelp, "Help");
8e688b92
KL
2228 return windowMenu;
2229 }
2230
55d2b2c2
KL
2231 /**
2232 * Convenience function to add a default "Help" menu.
2233 *
2234 * @return the new menu
2235 */
2236 public final TMenu addHelpMenu() {
2237 TMenu helpMenu = addMenu("&Help");
2238 helpMenu.addDefaultItem(TMenu.MID_HELP_CONTENTS);
2239 helpMenu.addDefaultItem(TMenu.MID_HELP_INDEX);
2240 helpMenu.addDefaultItem(TMenu.MID_HELP_SEARCH);
2241 helpMenu.addDefaultItem(TMenu.MID_HELP_PREVIOUS);
2242 helpMenu.addDefaultItem(TMenu.MID_HELP_HELP);
2243 helpMenu.addDefaultItem(TMenu.MID_HELP_ACTIVE_FILE);
2244 helpMenu.addSeparator();
2245 helpMenu.addDefaultItem(TMenu.MID_ABOUT);
2ce6dab2
KL
2246 TStatusBar statusBar = helpMenu.newStatusBar("Access online help");
2247 statusBar.addShortcutKeypress(kbF1, cmHelp, "Help");
55d2b2c2
KL
2248 return helpMenu;
2249 }
2250
2ce6dab2
KL
2251 // ------------------------------------------------------------------------
2252 // Event handlers ---------------------------------------------------------
2253 // ------------------------------------------------------------------------
2254
8e688b92 2255 /**
2ce6dab2
KL
2256 * Method that TApplication subclasses can override to handle menu or
2257 * posted command events.
2258 *
2259 * @param command command event
2260 * @return if true, this event was consumed
8e688b92 2261 */
2ce6dab2
KL
2262 protected boolean onCommand(final TCommandEvent command) {
2263 // Default: handle cmExit
2264 if (command.equals(cmExit)) {
2265 if (messageBox("Confirmation", "Exit application?",
2266 TMessageBox.Type.YESNO).getResult() == TMessageBox.Result.YES) {
2267 quit = true;
2268 }
2269 return true;
8e688b92 2270 }
2ce6dab2
KL
2271
2272 if (command.equals(cmShell)) {
2273 openTerminal(0, 0, TWindow.RESIZABLE);
2274 return true;
2275 }
2276
2277 if (command.equals(cmTile)) {
2278 tileWindows();
2279 return true;
2280 }
2281 if (command.equals(cmCascade)) {
2282 cascadeWindows();
2283 return true;
2284 }
2285 if (command.equals(cmCloseAll)) {
2286 closeAllWindows();
2287 return true;
8e688b92 2288 }
2ce6dab2
KL
2289
2290 return false;
8e688b92
KL
2291 }
2292
2293 /**
2ce6dab2
KL
2294 * Method that TApplication subclasses can override to handle menu
2295 * events.
2296 *
2297 * @param menu menu event
2298 * @return if true, this event was consumed
8e688b92 2299 */
2ce6dab2
KL
2300 protected boolean onMenu(final TMenuEvent menu) {
2301
2302 // Default: handle MID_EXIT
2303 if (menu.getId() == TMenu.MID_EXIT) {
2304 if (messageBox("Confirmation", "Exit application?",
2305 TMessageBox.Type.YESNO).getResult() == TMessageBox.Result.YES) {
2306 quit = true;
8e688b92 2307 }
2ce6dab2
KL
2308 return true;
2309 }
bb35d919 2310
2ce6dab2
KL
2311 if (menu.getId() == TMenu.MID_SHELL) {
2312 openTerminal(0, 0, TWindow.RESIZABLE);
2313 return true;
2314 }
8e688b92 2315
2ce6dab2
KL
2316 if (menu.getId() == TMenu.MID_TILE) {
2317 tileWindows();
2318 return true;
2319 }
2320 if (menu.getId() == TMenu.MID_CASCADE) {
2321 cascadeWindows();
2322 return true;
2323 }
2324 if (menu.getId() == TMenu.MID_CLOSE_ALL) {
2325 closeAllWindows();
2326 return true;
2327 }
2328 if (menu.getId() == TMenu.MID_ABOUT) {
2329 showAboutDialog();
2330 return true;
2331 }
2332 return false;
2333 }
2334
2335 /**
2336 * Method that TApplication subclasses can override to handle keystrokes.
2337 *
2338 * @param keypress keystroke event
2339 * @return if true, this event was consumed
2340 */
2341 protected boolean onKeypress(final TKeypressEvent keypress) {
2342 // Default: only menu shortcuts
2343
2344 // Process Alt-F, Alt-E, etc. menu shortcut keys
2345 if (!keypress.getKey().isFnKey()
2346 && keypress.getKey().isAlt()
2347 && !keypress.getKey().isCtrl()
2348 && (activeMenu == null)
2349 && !modalWindowActive()
2350 ) {
2351
2352 assert (subMenus.size() == 0);
2353
2354 for (TMenu menu: menus) {
2355 if (Character.toLowerCase(menu.getMnemonic().getShortcut())
2356 == Character.toLowerCase(keypress.getKey().getChar())
2357 ) {
2358 activeMenu = menu;
2359 menu.setActive(true);
2360 return true;
bb35d919 2361 }
8e688b92
KL
2362 }
2363 }
2ce6dab2
KL
2364
2365 return false;
8e688b92
KL
2366 }
2367
2ce6dab2
KL
2368 // ------------------------------------------------------------------------
2369 // TTimer management ------------------------------------------------------
2370 // ------------------------------------------------------------------------
2371
8e688b92 2372 /**
2ce6dab2
KL
2373 * Get the amount of time I can sleep before missing a Timer tick.
2374 *
2375 * @param timeout = initial (maximum) timeout in millis
2376 * @return number of milliseconds between now and the next timer event
8e688b92 2377 */
2ce6dab2
KL
2378 private long getSleepTime(final long timeout) {
2379 Date now = new Date();
2380 long nowTime = now.getTime();
2381 long sleepTime = timeout;
2382 for (TTimer timer: timers) {
2383 long nextTickTime = timer.getNextTick().getTime();
2384 if (nextTickTime < nowTime) {
2385 return 0;
8e688b92 2386 }
2ce6dab2
KL
2387
2388 long timeDifference = nextTickTime - nowTime;
2389 if (timeDifference < sleepTime) {
2390 sleepTime = timeDifference;
8e688b92
KL
2391 }
2392 }
2ce6dab2
KL
2393 assert (sleepTime >= 0);
2394 assert (sleepTime <= timeout);
2395 return sleepTime;
8e688b92
KL
2396 }
2397
d502a0e9
KL
2398 /**
2399 * Convenience function to add a timer.
2400 *
2401 * @param duration number of milliseconds to wait between ticks
2402 * @param recurring if true, re-schedule this timer after every tick
2403 * @param action function to call when button is pressed
c6940ed9 2404 * @return the timer
d502a0e9
KL
2405 */
2406 public final TTimer addTimer(final long duration, final boolean recurring,
2407 final TAction action) {
2408
2409 TTimer timer = new TTimer(duration, recurring, action);
2410 synchronized (timers) {
2411 timers.add(timer);
2412 }
2413 return timer;
2414 }
2415
2416 /**
2417 * Convenience function to remove a timer.
2418 *
2419 * @param timer timer to remove
2420 */
2421 public final void removeTimer(final TTimer timer) {
2422 synchronized (timers) {
2423 timers.remove(timer);
2424 }
2425 }
2426
2ce6dab2
KL
2427 // ------------------------------------------------------------------------
2428 // Other TWindow constructors ---------------------------------------------
2429 // ------------------------------------------------------------------------
2430
c6940ed9
KL
2431 /**
2432 * Convenience function to spawn a message box.
2433 *
2434 * @param title window title, will be centered along the top border
2435 * @param caption message to display. Use embedded newlines to get a
2436 * multi-line box.
2437 * @return the new message box
2438 */
2439 public final TMessageBox messageBox(final String title,
2440 final String caption) {
2441
2442 return new TMessageBox(this, title, caption, TMessageBox.Type.OK);
2443 }
2444
2445 /**
2446 * Convenience function to spawn a message box.
2447 *
2448 * @param title window title, will be centered along the top border
2449 * @param caption message to display. Use embedded newlines to get a
2450 * multi-line box.
2451 * @param type one of the TMessageBox.Type constants. Default is
2452 * Type.OK.
2453 * @return the new message box
2454 */
2455 public final TMessageBox messageBox(final String title,
2456 final String caption, final TMessageBox.Type type) {
2457
2458 return new TMessageBox(this, title, caption, type);
2459 }
2460
2461 /**
2462 * Convenience function to spawn an input box.
2463 *
2464 * @param title window title, will be centered along the top border
2465 * @param caption message to display. Use embedded newlines to get a
2466 * multi-line box.
2467 * @return the new input box
2468 */
2469 public final TInputBox inputBox(final String title, final String caption) {
2470
2471 return new TInputBox(this, title, caption);
2472 }
2473
2474 /**
2475 * Convenience function to spawn an input box.
2476 *
2477 * @param title window title, will be centered along the top border
2478 * @param caption message to display. Use embedded newlines to get a
2479 * multi-line box.
2480 * @param text initial text to seed the field with
2481 * @return the new input box
2482 */
2483 public final TInputBox inputBox(final String title, final String caption,
2484 final String text) {
2485
2486 return new TInputBox(this, title, caption, text);
2487 }
1ac2ccb1 2488
34a42e78
KL
2489 /**
2490 * Convenience function to open a terminal window.
2491 *
2492 * @param x column relative to parent
2493 * @param y row relative to parent
2494 * @return the terminal new window
2495 */
2496 public final TTerminalWindow openTerminal(final int x, final int y) {
2497 return openTerminal(x, y, TWindow.RESIZABLE);
2498 }
2499
2500 /**
2501 * Convenience function to open a terminal window.
2502 *
2503 * @param x column relative to parent
2504 * @param y row relative to parent
2505 * @param flags mask of CENTERED, MODAL, or RESIZABLE
2506 * @return the terminal new window
2507 */
2508 public final TTerminalWindow openTerminal(final int x, final int y,
2509 final int flags) {
2510
2511 return new TTerminalWindow(this, x, y, flags);
2512 }
2513
0d47c546
KL
2514 /**
2515 * Convenience function to spawn an file open box.
2516 *
2517 * @param path path of selected file
2518 * @return the result of the new file open box
329fd62e 2519 * @throws IOException if java.io operation throws
0d47c546
KL
2520 */
2521 public final String fileOpenBox(final String path) throws IOException {
2522
2523 TFileOpenBox box = new TFileOpenBox(this, path, TFileOpenBox.Type.OPEN);
2524 return box.getFilename();
2525 }
2526
2527 /**
2528 * Convenience function to spawn an file open box.
2529 *
2530 * @param path path of selected file
2531 * @param type one of the Type constants
2532 * @return the result of the new file open box
329fd62e 2533 * @throws IOException if java.io operation throws
0d47c546
KL
2534 */
2535 public final String fileOpenBox(final String path,
2536 final TFileOpenBox.Type type) throws IOException {
2537
2538 TFileOpenBox box = new TFileOpenBox(this, path, type);
2539 return box.getFilename();
2540 }
2541
92453213
KL
2542 /**
2543 * Convenience function to create a new window and make it active.
2544 * Window will be located at (0, 0).
2545 *
2546 * @param title window title, will be centered along the top border
2547 * @param width width of window
2548 * @param height height of window
2549 */
2550 public final TWindow addWindow(final String title, final int width,
2551 final int height) {
2552
2553 TWindow window = new TWindow(this, title, 0, 0, width, height);
2554 return window;
2555 }
2556 /**
2557 * Convenience function to create a new window and make it active.
2558 * Window will be located at (0, 0).
2559 *
2560 * @param title window title, will be centered along the top border
2561 * @param width width of window
2562 * @param height height of window
2563 * @param flags bitmask of RESIZABLE, CENTERED, or MODAL
2564 */
2565 public final TWindow addWindow(final String title,
2566 final int width, final int height, final int flags) {
2567
2568 TWindow window = new TWindow(this, title, 0, 0, width, height, flags);
2569 return window;
2570 }
2571
2572 /**
2573 * Convenience function to create a new window and make it active.
2574 *
2575 * @param title window title, will be centered along the top border
2576 * @param x column relative to parent
2577 * @param y row relative to parent
2578 * @param width width of window
2579 * @param height height of window
2580 */
2581 public final TWindow addWindow(final String title,
2582 final int x, final int y, final int width, final int height) {
2583
2584 TWindow window = new TWindow(this, title, x, y, width, height);
2585 return window;
2586 }
2587
2588 /**
2589 * Convenience function to create a new window and make it active.
2590 *
2591 * @param application TApplication that manages this window
2592 * @param title window title, will be centered along the top border
2593 * @param x column relative to parent
2594 * @param y row relative to parent
2595 * @param width width of window
2596 * @param height height of window
2597 * @param flags mask of RESIZABLE, CENTERED, or MODAL
2598 */
2599 public final TWindow addWindow(final String title,
2600 final int x, final int y, final int width, final int height,
2601 final int flags) {
2602
2603 TWindow window = new TWindow(this, title, x, y, width, height, flags);
2604 return window;
2605 }
2606
7d4115a5 2607}