#11 NOCLOSEBOX flag
[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 /**
8c236a98 1281 * Return the number of windows that are showing.
92453213 1282 *
8c236a98 1283 * @return the number of windows that are showing on screen
92453213
KL
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
8c236a98
KL
1295 /**
1296 * Return the number of windows that are hidden.
1297 *
1298 * @return the number of windows that are hidden
1299 */
1300 public final int hiddenWindowCount() {
1301 int n = 0;
1302 for (TWindow w: windows) {
1303 if (w.isHidden()) {
1304 n++;
1305 }
1306 }
1307 return n;
1308 }
1309
92453213
KL
1310 /**
1311 * Check if a window instance is in this application's window list.
1312 *
1313 * @param window window to look for
1314 * @return true if this window is in the list
1315 */
1316 public final boolean hasWindow(final TWindow window) {
1317 if (windows.size() == 0) {
1318 return false;
1319 }
1320 for (TWindow w: windows) {
1321 if (w == window) {
8c236a98 1322 assert (window.getApplication() == this);
92453213
KL
1323 return true;
1324 }
1325 }
1326 return false;
1327 }
1328
1329 /**
1330 * Activate a window: bring it to the top and have it receive events.
1331 *
1332 * @param window the window to become the new active window
1333 */
1334 public void activateWindow(final TWindow window) {
1335 if (hasWindow(window) == false) {
1336 /*
1337 * Someone has a handle to a window I don't have. Ignore this
1338 * request.
1339 */
1340 return;
1341 }
1342
1343 assert (windows.size() > 0);
1344
1345 if (window.isHidden()) {
1346 // Unhiding will also activate.
1347 showWindow(window);
1348 return;
1349 }
1350 assert (window.isShown());
1351
1352 if (windows.size() == 1) {
1353 assert (window == windows.get(0));
1354 if (activeWindow == null) {
1355 activeWindow = window;
1356 window.setZ(0);
1357 activeWindow.setActive(true);
1358 activeWindow.onFocus();
1359 }
1360
1361 assert (window.isActive());
1362 assert (activeWindow == window);
1363 return;
1364 }
1365
1366 if (activeWindow == window) {
1367 assert (window.isActive());
1368
1369 // Window is already active, do nothing.
1370 return;
1371 }
1372
1373 assert (!window.isActive());
1374 if (activeWindow != null) {
1375 assert (activeWindow.getZ() == 0);
1376
1377 activeWindow.onUnfocus();
1378 activeWindow.setActive(false);
1379 activeWindow.setZ(window.getZ());
1380 }
1381 activeWindow = window;
1382 activeWindow.setZ(0);
1383 activeWindow.setActive(true);
1384 activeWindow.onFocus();
1385 return;
1386 }
1387
1388 /**
1389 * Hide a window.
1390 *
1391 * @param window the window to hide
1392 */
1393 public void hideWindow(final TWindow window) {
1394 if (hasWindow(window) == false) {
1395 /*
1396 * Someone has a handle to a window I don't have. Ignore this
1397 * request.
1398 */
1399 return;
1400 }
1401
1402 assert (windows.size() > 0);
1403
1404 if (!window.hidden) {
1405 if (window == activeWindow) {
1406 if (shownWindowCount() > 1) {
1407 switchWindow(true);
1408 } else {
1409 activeWindow = null;
1410 window.setActive(false);
1411 window.onUnfocus();
1412 }
1413 }
1414 window.hidden = true;
1415 window.onHide();
1416 }
1417 }
1418
1419 /**
1420 * Show a window.
1421 *
1422 * @param window the window to show
1423 */
1424 public void showWindow(final TWindow window) {
1425 if (hasWindow(window) == false) {
1426 /*
1427 * Someone has a handle to a window I don't have. Ignore this
1428 * request.
1429 */
1430 return;
1431 }
1432
1433 assert (windows.size() > 0);
1434
1435 if (window.hidden) {
1436 window.hidden = false;
1437 window.onShow();
1438 activateWindow(window);
1439 }
1440 }
1441
48e27807
KL
1442 /**
1443 * Close window. Note that the window's destructor is NOT called by this
1444 * method, instead the GC is assumed to do the cleanup.
1445 *
1446 * @param window the window to remove
1447 */
1448 public final void closeWindow(final TWindow window) {
92453213
KL
1449 if (hasWindow(window) == false) {
1450 /*
1451 * Someone has a handle to a window I don't have. Ignore this
1452 * request.
1453 */
1454 return;
1455 }
1456
bb35d919
KL
1457 synchronized (windows) {
1458 int z = window.getZ();
1459 window.setZ(-1);
efb7af1f 1460 window.onUnfocus();
bb35d919
KL
1461 Collections.sort(windows);
1462 windows.remove(0);
92453213 1463 activeWindow = null;
bb35d919
KL
1464 for (TWindow w: windows) {
1465 if (w.getZ() > z) {
1466 w.setZ(w.getZ() - 1);
1467 if (w.getZ() == 0) {
1468 w.setActive(true);
efb7af1f 1469 w.onFocus();
bb35d919
KL
1470 assert (activeWindow == null);
1471 activeWindow = w;
1472 } else {
efb7af1f
KL
1473 if (w.isActive()) {
1474 w.setActive(false);
1475 w.onUnfocus();
1476 }
bb35d919 1477 }
48e27807
KL
1478 }
1479 }
1480 }
1481
1482 // Perform window cleanup
1483 window.onClose();
1484
48e27807 1485 // Check if we are closing a TMessageBox or similar
c6940ed9
KL
1486 if (secondaryEventReceiver != null) {
1487 assert (secondaryEventHandler != null);
48e27807
KL
1488
1489 // Do not send events to the secondaryEventReceiver anymore, the
1490 // window is closed.
1491 secondaryEventReceiver = null;
1492
92554d64
KL
1493 // Wake the secondary thread, it will wake the primary as it
1494 // exits.
1495 synchronized (secondaryEventHandler) {
1496 secondaryEventHandler.notify();
48e27807
KL
1497 }
1498 }
92453213
KL
1499
1500 // Permit desktop to be active if it is the only thing left.
1501 if (desktop != null) {
1502 if (windows.size() == 0) {
1503 desktop.setActive(true);
1504 }
1505 }
48e27807
KL
1506 }
1507
1508 /**
1509 * Switch to the next window.
1510 *
1511 * @param forward if true, then switch to the next window in the list,
1512 * otherwise switch to the previous window in the list
1513 */
1514 public final void switchWindow(final boolean forward) {
8c236a98
KL
1515 // Only switch if there are multiple visible windows
1516 if (shownWindowCount() < 2) {
48e27807
KL
1517 return;
1518 }
92453213 1519 assert (activeWindow != null);
48e27807 1520
bb35d919
KL
1521 synchronized (windows) {
1522
1523 // Swap z/active between active window and the next in the list
1524 int activeWindowI = -1;
1525 for (int i = 0; i < windows.size(); i++) {
92453213
KL
1526 if (windows.get(i) == activeWindow) {
1527 assert (activeWindow.isActive());
bb35d919
KL
1528 activeWindowI = i;
1529 break;
92453213
KL
1530 } else {
1531 assert (!windows.get(0).isActive());
bb35d919 1532 }
48e27807 1533 }
bb35d919 1534 assert (activeWindowI >= 0);
48e27807 1535
bb35d919 1536 // Do not switch if a window is modal
92453213 1537 if (activeWindow.isModal()) {
bb35d919
KL
1538 return;
1539 }
48e27807 1540
8c236a98
KL
1541 int nextWindowI = activeWindowI;
1542 for (;;) {
1543 if (forward) {
1544 nextWindowI++;
1545 nextWindowI %= windows.size();
bb35d919 1546 } else {
8c236a98
KL
1547 nextWindowI--;
1548 if (nextWindowI < 0) {
1549 nextWindowI = windows.size() - 1;
1550 }
bb35d919 1551 }
bb35d919 1552
8c236a98
KL
1553 if (windows.get(nextWindowI).isShown()) {
1554 activateWindow(windows.get(nextWindowI));
1555 break;
1556 }
1557 }
bb35d919 1558 } // synchronized (windows)
48e27807 1559
48e27807
KL
1560 }
1561
1562 /**
1563 * Add a window to my window list and make it active.
1564 *
1565 * @param window new window to add
1566 */
1567 public final void addWindow(final TWindow window) {
a7986f7b
KL
1568
1569 // Do not add menu windows to the window list.
1570 if (window instanceof TMenu) {
1571 return;
1572 }
1573
0ee88b6d
KL
1574 // Do not add the desktop to the window list.
1575 if (window instanceof TDesktop) {
1576 return;
1577 }
1578
bb35d919 1579 synchronized (windows) {
2ce6dab2
KL
1580 // Do not allow a modal window to spawn a non-modal window. If a
1581 // modal window is active, then this window will become modal
1582 // too.
1583 if (modalWindowActive()) {
1584 window.flags |= TWindow.MODAL;
a7986f7b 1585 window.flags |= TWindow.CENTERED;
92453213 1586 window.hidden = false;
bb35d919 1587 }
92453213
KL
1588 if (window.isShown()) {
1589 for (TWindow w: windows) {
1590 if (w.isActive()) {
1591 w.setActive(false);
1592 w.onUnfocus();
1593 }
1594 w.setZ(w.getZ() + 1);
efb7af1f 1595 }
bb35d919
KL
1596 }
1597 windows.add(window);
92453213
KL
1598 if (window.isShown()) {
1599 activeWindow = window;
1600 activeWindow.setZ(0);
1601 activeWindow.setActive(true);
1602 activeWindow.onFocus();
1603 }
a7986f7b
KL
1604
1605 if (((window.flags & TWindow.CENTERED) == 0)
1606 && smartWindowPlacement) {
1607
1608 doSmartPlacement(window);
1609 }
48e27807 1610 }
92453213
KL
1611
1612 // Desktop cannot be active over any other window.
1613 if (desktop != null) {
1614 desktop.setActive(false);
1615 }
48e27807
KL
1616 }
1617
fca67db0
KL
1618 /**
1619 * Check if there is a system-modal window on top.
1620 *
1621 * @return true if the active window is modal
1622 */
1623 private boolean modalWindowActive() {
1624 if (windows.size() == 0) {
1625 return false;
1626 }
2ce6dab2
KL
1627
1628 for (TWindow w: windows) {
1629 if (w.isModal()) {
1630 return true;
1631 }
1632 }
1633
1634 return false;
1635 }
1636
1637 /**
1638 * Close all open windows.
1639 */
1640 private void closeAllWindows() {
1641 // Don't do anything if we are in the menu
1642 if (activeMenu != null) {
1643 return;
1644 }
1645 while (windows.size() > 0) {
1646 closeWindow(windows.get(0));
1647 }
fca67db0
KL
1648 }
1649
2ce6dab2
KL
1650 /**
1651 * Re-layout the open windows as non-overlapping tiles. This produces
1652 * almost the same results as Turbo Pascal 7.0's IDE.
1653 */
1654 private void tileWindows() {
1655 synchronized (windows) {
1656 // Don't do anything if we are in the menu
1657 if (activeMenu != null) {
1658 return;
1659 }
1660 int z = windows.size();
1661 if (z == 0) {
1662 return;
1663 }
1664 int a = 0;
1665 int b = 0;
1666 a = (int)(Math.sqrt(z));
1667 int c = 0;
1668 while (c < a) {
1669 b = (z - c) / a;
1670 if (((a * b) + c) == z) {
1671 break;
1672 }
1673 c++;
1674 }
1675 assert (a > 0);
1676 assert (b > 0);
1677 assert (c < a);
1678 int newWidth = (getScreen().getWidth() / a);
1679 int newHeight1 = ((getScreen().getHeight() - 1) / b);
1680 int newHeight2 = ((getScreen().getHeight() - 1) / (b + c));
1681
1682 List<TWindow> sorted = new LinkedList<TWindow>(windows);
1683 Collections.sort(sorted);
1684 Collections.reverse(sorted);
1685 for (int i = 0; i < sorted.size(); i++) {
1686 int logicalX = i / b;
1687 int logicalY = i % b;
1688 if (i >= ((a - 1) * b)) {
1689 logicalX = a - 1;
1690 logicalY = i - ((a - 1) * b);
1691 }
1692
1693 TWindow w = sorted.get(i);
1694 w.setX(logicalX * newWidth);
1695 w.setWidth(newWidth);
1696 if (i >= ((a - 1) * b)) {
1697 w.setY((logicalY * newHeight2) + 1);
1698 w.setHeight(newHeight2);
1699 } else {
1700 w.setY((logicalY * newHeight1) + 1);
1701 w.setHeight(newHeight1);
1702 }
1703 }
1704 }
1705 }
1706
1707 /**
1708 * Re-layout the open windows as overlapping cascaded windows.
1709 */
1710 private void cascadeWindows() {
1711 synchronized (windows) {
1712 // Don't do anything if we are in the menu
1713 if (activeMenu != null) {
1714 return;
1715 }
1716 int x = 0;
1717 int y = 1;
1718 List<TWindow> sorted = new LinkedList<TWindow>(windows);
1719 Collections.sort(sorted);
1720 Collections.reverse(sorted);
1721 for (TWindow window: sorted) {
1722 window.setX(x);
1723 window.setY(y);
1724 x++;
1725 y++;
1726 if (x > getScreen().getWidth()) {
1727 x = 0;
1728 }
1729 if (y >= getScreen().getHeight()) {
1730 y = 1;
1731 }
1732 }
1733 }
1734 }
1735
a7986f7b
KL
1736 /**
1737 * Place a window to minimize its overlap with other windows.
1738 *
1739 * @param window the window to place
1740 */
1741 public final void doSmartPlacement(final TWindow window) {
1742 // This is a pretty dumb algorithm, but seems to work. The hardest
1743 // part is computing these "overlap" values seeking a minimum average
1744 // overlap.
1745 int xMin = 0;
1746 int yMin = desktopTop;
1747 int xMax = getScreen().getWidth() - window.getWidth() + 1;
1748 int yMax = desktopBottom - window.getHeight() + 1;
1749 if (xMax < xMin) {
1750 xMax = xMin;
1751 }
1752 if (yMax < yMin) {
1753 yMax = yMin;
1754 }
1755
1756 if ((xMin == xMax) && (yMin == yMax)) {
1757 // No work to do, bail out.
1758 return;
1759 }
1760
1761 // Compute the overlap matrix without the new window.
1762 int width = getScreen().getWidth();
1763 int height = getScreen().getHeight();
1764 int overlapMatrix[][] = new int[width][height];
1765 for (TWindow w: windows) {
1766 if (window == w) {
1767 continue;
1768 }
1769 for (int x = w.getX(); x < w.getX() + w.getWidth(); x++) {
8c236a98 1770 if (x >= width) {
a7986f7b
KL
1771 continue;
1772 }
1773 for (int y = w.getY(); y < w.getY() + w.getHeight(); y++) {
8c236a98 1774 if (y >= height) {
a7986f7b
KL
1775 continue;
1776 }
1777 overlapMatrix[x][y]++;
1778 }
1779 }
1780 }
1781
1782 long oldOverlapTotal = 0;
1783 long oldOverlapN = 0;
1784 for (int x = 0; x < width; x++) {
1785 for (int y = 0; y < height; y++) {
1786 oldOverlapTotal += overlapMatrix[x][y];
1787 if (overlapMatrix[x][y] > 0) {
1788 oldOverlapN++;
1789 }
1790 }
1791 }
1792
1793
1794 double oldOverlapAvg = (double) oldOverlapTotal / (double) oldOverlapN;
1795 boolean first = true;
1796 int windowX = window.getX();
1797 int windowY = window.getY();
1798
1799 // For each possible (x, y) position for the new window, compute a
1800 // new overlap matrix.
1801 for (int x = xMin; x < xMax; x++) {
1802 for (int y = yMin; y < yMax; y++) {
1803
1804 // Start with the matrix minus this window.
1805 int newMatrix[][] = new int[width][height];
1806 for (int mx = 0; mx < width; mx++) {
1807 for (int my = 0; my < height; my++) {
1808 newMatrix[mx][my] = overlapMatrix[mx][my];
1809 }
1810 }
1811
1812 // Add this window's values to the new overlap matrix.
1813 long newOverlapTotal = 0;
1814 long newOverlapN = 0;
1815 // Start by adding each new cell.
1816 for (int wx = x; wx < x + window.getWidth(); wx++) {
8c236a98 1817 if (wx >= width) {
a7986f7b
KL
1818 continue;
1819 }
1820 for (int wy = y; wy < y + window.getHeight(); wy++) {
8c236a98 1821 if (wy >= height) {
a7986f7b
KL
1822 continue;
1823 }
1824 newMatrix[wx][wy]++;
1825 }
1826 }
1827 // Now figure out the new value for total coverage.
1828 for (int mx = 0; mx < width; mx++) {
1829 for (int my = 0; my < height; my++) {
1830 newOverlapTotal += newMatrix[x][y];
1831 if (newMatrix[mx][my] > 0) {
1832 newOverlapN++;
1833 }
1834 }
1835 }
1836 double newOverlapAvg = (double) newOverlapTotal / (double) newOverlapN;
1837
1838 if (first) {
1839 // First time: just record what we got.
1840 oldOverlapAvg = newOverlapAvg;
1841 first = false;
1842 } else {
1843 // All other times: pick a new best (x, y) and save the
1844 // overlap value.
1845 if (newOverlapAvg < oldOverlapAvg) {
1846 windowX = x;
1847 windowY = y;
1848 oldOverlapAvg = newOverlapAvg;
1849 }
1850 }
1851
1852 } // for (int x = xMin; x < xMax; x++)
1853
1854 } // for (int y = yMin; y < yMax; y++)
1855
1856 // Finally, set the window's new coordinates.
1857 window.setX(windowX);
1858 window.setY(windowY);
1859 }
1860
2ce6dab2
KL
1861 // ------------------------------------------------------------------------
1862 // TMenu management -------------------------------------------------------
1863 // ------------------------------------------------------------------------
1864
fca67db0
KL
1865 /**
1866 * Check if a mouse event would hit either the active menu or any open
1867 * sub-menus.
1868 *
1869 * @param mouse mouse event
1870 * @return true if the mouse would hit the active menu or an open
1871 * sub-menu
1872 */
1873 private boolean mouseOnMenu(final TMouseEvent mouse) {
1874 assert (activeMenu != null);
1875 List<TMenu> menus = new LinkedList<TMenu>(subMenus);
1876 Collections.reverse(menus);
1877 for (TMenu menu: menus) {
1878 if (menu.mouseWouldHit(mouse)) {
1879 return true;
1880 }
1881 }
1882 return activeMenu.mouseWouldHit(mouse);
1883 }
1884
1885 /**
1886 * See if we need to switch window or activate the menu based on
1887 * a mouse click.
1888 *
1889 * @param mouse mouse event
1890 */
1891 private void checkSwitchFocus(final TMouseEvent mouse) {
1892
1893 if ((mouse.getType() == TMouseEvent.Type.MOUSE_DOWN)
1894 && (activeMenu != null)
1895 && (mouse.getAbsoluteY() != 0)
1896 && (!mouseOnMenu(mouse))
1897 ) {
1898 // They clicked outside the active menu, turn it off
1899 activeMenu.setActive(false);
1900 activeMenu = null;
1901 for (TMenu menu: subMenus) {
1902 menu.setActive(false);
1903 }
1904 subMenus.clear();
1905 // Continue checks
1906 }
1907
1908 // See if they hit the menu bar
1909 if ((mouse.getType() == TMouseEvent.Type.MOUSE_DOWN)
7c870d89 1910 && (mouse.isMouse1())
fca67db0
KL
1911 && (!modalWindowActive())
1912 && (mouse.getAbsoluteY() == 0)
1913 ) {
1914
1915 for (TMenu menu: subMenus) {
1916 menu.setActive(false);
1917 }
1918 subMenus.clear();
1919
1920 // They selected the menu, go activate it
1921 for (TMenu menu: menus) {
1922 if ((mouse.getAbsoluteX() >= menu.getX())
1923 && (mouse.getAbsoluteX() < menu.getX()
1924 + menu.getTitle().length() + 2)
1925 ) {
1926 menu.setActive(true);
1927 activeMenu = menu;
1928 } else {
1929 menu.setActive(false);
1930 }
1931 }
fca67db0
KL
1932 return;
1933 }
1934
1935 // See if they hit the menu bar
1936 if ((mouse.getType() == TMouseEvent.Type.MOUSE_MOTION)
7c870d89 1937 && (mouse.isMouse1())
fca67db0
KL
1938 && (activeMenu != null)
1939 && (mouse.getAbsoluteY() == 0)
1940 ) {
1941
1942 TMenu oldMenu = activeMenu;
1943 for (TMenu menu: subMenus) {
1944 menu.setActive(false);
1945 }
1946 subMenus.clear();
1947
1948 // See if we should switch menus
1949 for (TMenu menu: menus) {
1950 if ((mouse.getAbsoluteX() >= menu.getX())
1951 && (mouse.getAbsoluteX() < menu.getX()
1952 + menu.getTitle().length() + 2)
1953 ) {
1954 menu.setActive(true);
1955 activeMenu = menu;
1956 }
1957 }
1958 if (oldMenu != activeMenu) {
1959 // They switched menus
1960 oldMenu.setActive(false);
1961 }
fca67db0
KL
1962 return;
1963 }
1964
1965 // Only switch if there are multiple windows
1966 if (windows.size() < 2) {
1967 return;
1968 }
1969
1970 // Switch on the upclick
1971 if (mouse.getType() != TMouseEvent.Type.MOUSE_UP) {
1972 return;
1973 }
1974
bb35d919
KL
1975 synchronized (windows) {
1976 Collections.sort(windows);
1977 if (windows.get(0).isModal()) {
1978 // Modal windows don't switch
1979 return;
1980 }
fca67db0 1981
bb35d919
KL
1982 for (TWindow window: windows) {
1983 assert (!window.isModal());
92453213
KL
1984
1985 if (window.isHidden()) {
1986 assert (!window.isActive());
1987 continue;
1988 }
1989
bb35d919
KL
1990 if (window.mouseWouldHit(mouse)) {
1991 if (window == windows.get(0)) {
1992 // Clicked on the same window, nothing to do
92453213 1993 assert (window.isActive());
bb35d919
KL
1994 return;
1995 }
1996
1997 // We will be switching to another window
7c870d89 1998 assert (windows.get(0).isActive());
92453213 1999 assert (windows.get(0) == activeWindow);
7c870d89 2000 assert (!window.isActive());
92453213
KL
2001 activeWindow.onUnfocus();
2002 activeWindow.setActive(false);
2003 activeWindow.setZ(window.getZ());
2004 activeWindow = window;
bb35d919
KL
2005 window.setZ(0);
2006 window.setActive(true);
efb7af1f 2007 window.onFocus();
fca67db0
KL
2008 return;
2009 }
fca67db0
KL
2010 }
2011 }
2012
2013 // Clicked on the background, nothing to do
2014 return;
2015 }
2016
2017 /**
2018 * Turn off the menu.
2019 */
928811d8 2020 public final void closeMenu() {
fca67db0
KL
2021 if (activeMenu != null) {
2022 activeMenu.setActive(false);
2023 activeMenu = null;
2024 for (TMenu menu: subMenus) {
2025 menu.setActive(false);
2026 }
2027 subMenus.clear();
2028 }
fca67db0
KL
2029 }
2030
2031 /**
2032 * Turn off a sub-menu.
2033 */
928811d8 2034 public final void closeSubMenu() {
fca67db0
KL
2035 assert (activeMenu != null);
2036 TMenu item = subMenus.get(subMenus.size() - 1);
2037 assert (item != null);
2038 item.setActive(false);
2039 subMenus.remove(subMenus.size() - 1);
fca67db0
KL
2040 }
2041
2042 /**
2043 * Switch to the next menu.
2044 *
2045 * @param forward if true, then switch to the next menu in the list,
2046 * otherwise switch to the previous menu in the list
2047 */
928811d8 2048 public final void switchMenu(final boolean forward) {
fca67db0
KL
2049 assert (activeMenu != null);
2050
2051 for (TMenu menu: subMenus) {
2052 menu.setActive(false);
2053 }
2054 subMenus.clear();
2055
2056 for (int i = 0; i < menus.size(); i++) {
2057 if (activeMenu == menus.get(i)) {
2058 if (forward) {
2059 if (i < menus.size() - 1) {
2060 i++;
2061 }
2062 } else {
2063 if (i > 0) {
2064 i--;
2065 }
2066 }
2067 activeMenu.setActive(false);
2068 activeMenu = menus.get(i);
2069 activeMenu.setActive(true);
fca67db0
KL
2070 return;
2071 }
2072 }
2073 }
2074
928811d8 2075 /**
efb7af1f
KL
2076 * Add a menu item to the global list. If it has a keyboard accelerator,
2077 * that will be added the global hash.
928811d8 2078 *
efb7af1f 2079 * @param item the menu item
928811d8 2080 */
efb7af1f
KL
2081 public final void addMenuItem(final TMenuItem item) {
2082 menuItems.add(item);
2083
2084 TKeypress key = item.getKey();
2085 if (key != null) {
2086 synchronized (accelerators) {
2087 assert (accelerators.get(key) == null);
2088 accelerators.put(key.toLowerCase(), item);
2089 }
2090 }
2091 }
2092
2093 /**
2094 * Disable one menu item.
2095 *
2096 * @param id the menu item ID
2097 */
2098 public final void disableMenuItem(final int id) {
2099 for (TMenuItem item: menuItems) {
2100 if (item.getId() == id) {
2101 item.setEnabled(false);
2102 }
2103 }
2104 }
e826b451 2105
efb7af1f
KL
2106 /**
2107 * Disable the range of menu items with ID's between lower and upper,
2108 * inclusive.
2109 *
2110 * @param lower the lowest menu item ID
2111 * @param upper the highest menu item ID
2112 */
2113 public final void disableMenuItems(final int lower, final int upper) {
2114 for (TMenuItem item: menuItems) {
2115 if ((item.getId() >= lower) && (item.getId() <= upper)) {
2116 item.setEnabled(false);
2117 }
2118 }
2119 }
2120
2121 /**
2122 * Enable one menu item.
2123 *
2124 * @param id the menu item ID
2125 */
2126 public final void enableMenuItem(final int id) {
2127 for (TMenuItem item: menuItems) {
2128 if (item.getId() == id) {
2129 item.setEnabled(true);
2130 }
2131 }
2132 }
2133
2134 /**
2135 * Enable the range of menu items with ID's between lower and upper,
2136 * inclusive.
2137 *
2138 * @param lower the lowest menu item ID
2139 * @param upper the highest menu item ID
2140 */
2141 public final void enableMenuItems(final int lower, final int upper) {
2142 for (TMenuItem item: menuItems) {
2143 if ((item.getId() >= lower) && (item.getId() <= upper)) {
2144 item.setEnabled(true);
2145 }
e826b451 2146 }
928811d8
KL
2147 }
2148
2149 /**
2150 * Recompute menu x positions based on their title length.
2151 */
2152 public final void recomputeMenuX() {
2153 int x = 0;
2154 for (TMenu menu: menus) {
2155 menu.setX(x);
2156 x += menu.getTitle().length() + 2;
2157 }
2158 }
2159
2160 /**
2161 * Post an event to process and turn off the menu.
2162 *
2163 * @param event new event to add to the queue
2164 */
5dfd1c11 2165 public final void postMenuEvent(final TInputEvent event) {
8e688b92
KL
2166 synchronized (fillEventQueue) {
2167 fillEventQueue.add(event);
2168 }
928811d8
KL
2169 closeMenu();
2170 }
2171
2172 /**
2173 * Add a sub-menu to the list of open sub-menus.
2174 *
2175 * @param menu sub-menu
2176 */
2177 public final void addSubMenu(final TMenu menu) {
2178 subMenus.add(menu);
2179 }
2180
8e688b92
KL
2181 /**
2182 * Convenience function to add a top-level menu.
2183 *
2184 * @param title menu title
2185 * @return the new menu
2186 */
87a17f3c 2187 public final TMenu addMenu(final String title) {
8e688b92
KL
2188 int x = 0;
2189 int y = 0;
2190 TMenu menu = new TMenu(this, x, y, title);
2191 menus.add(menu);
2192 recomputeMenuX();
2193 return menu;
2194 }
2195
2196 /**
2197 * Convenience function to add a default "File" menu.
2198 *
2199 * @return the new menu
2200 */
2201 public final TMenu addFileMenu() {
2202 TMenu fileMenu = addMenu("&File");
2203 fileMenu.addDefaultItem(TMenu.MID_OPEN_FILE);
2204 fileMenu.addSeparator();
2205 fileMenu.addDefaultItem(TMenu.MID_SHELL);
2206 fileMenu.addDefaultItem(TMenu.MID_EXIT);
2ce6dab2
KL
2207 TStatusBar statusBar = fileMenu.newStatusBar("File-management " +
2208 "commands (Open, Save, Print, etc.)");
2209 statusBar.addShortcutKeypress(kbF1, cmHelp, "Help");
8e688b92
KL
2210 return fileMenu;
2211 }
2212
2213 /**
2214 * Convenience function to add a default "Edit" menu.
2215 *
2216 * @return the new menu
2217 */
2218 public final TMenu addEditMenu() {
2219 TMenu editMenu = addMenu("&Edit");
2220 editMenu.addDefaultItem(TMenu.MID_CUT);
2221 editMenu.addDefaultItem(TMenu.MID_COPY);
2222 editMenu.addDefaultItem(TMenu.MID_PASTE);
2223 editMenu.addDefaultItem(TMenu.MID_CLEAR);
2ce6dab2
KL
2224 TStatusBar statusBar = editMenu.newStatusBar("Editor operations, " +
2225 "undo, and Clipboard access");
2226 statusBar.addShortcutKeypress(kbF1, cmHelp, "Help");
8e688b92
KL
2227 return editMenu;
2228 }
2229
2230 /**
2231 * Convenience function to add a default "Window" menu.
2232 *
2233 * @return the new menu
2234 */
c6940ed9 2235 public final TMenu addWindowMenu() {
8e688b92
KL
2236 TMenu windowMenu = addMenu("&Window");
2237 windowMenu.addDefaultItem(TMenu.MID_TILE);
2238 windowMenu.addDefaultItem(TMenu.MID_CASCADE);
2239 windowMenu.addDefaultItem(TMenu.MID_CLOSE_ALL);
2240 windowMenu.addSeparator();
2241 windowMenu.addDefaultItem(TMenu.MID_WINDOW_MOVE);
2242 windowMenu.addDefaultItem(TMenu.MID_WINDOW_ZOOM);
2243 windowMenu.addDefaultItem(TMenu.MID_WINDOW_NEXT);
2244 windowMenu.addDefaultItem(TMenu.MID_WINDOW_PREVIOUS);
2245 windowMenu.addDefaultItem(TMenu.MID_WINDOW_CLOSE);
2ce6dab2
KL
2246 TStatusBar statusBar = windowMenu.newStatusBar("Open, arrange, and " +
2247 "list windows");
2248 statusBar.addShortcutKeypress(kbF1, cmHelp, "Help");
8e688b92
KL
2249 return windowMenu;
2250 }
2251
55d2b2c2
KL
2252 /**
2253 * Convenience function to add a default "Help" menu.
2254 *
2255 * @return the new menu
2256 */
2257 public final TMenu addHelpMenu() {
2258 TMenu helpMenu = addMenu("&Help");
2259 helpMenu.addDefaultItem(TMenu.MID_HELP_CONTENTS);
2260 helpMenu.addDefaultItem(TMenu.MID_HELP_INDEX);
2261 helpMenu.addDefaultItem(TMenu.MID_HELP_SEARCH);
2262 helpMenu.addDefaultItem(TMenu.MID_HELP_PREVIOUS);
2263 helpMenu.addDefaultItem(TMenu.MID_HELP_HELP);
2264 helpMenu.addDefaultItem(TMenu.MID_HELP_ACTIVE_FILE);
2265 helpMenu.addSeparator();
2266 helpMenu.addDefaultItem(TMenu.MID_ABOUT);
2ce6dab2
KL
2267 TStatusBar statusBar = helpMenu.newStatusBar("Access online help");
2268 statusBar.addShortcutKeypress(kbF1, cmHelp, "Help");
55d2b2c2
KL
2269 return helpMenu;
2270 }
2271
2ce6dab2
KL
2272 // ------------------------------------------------------------------------
2273 // Event handlers ---------------------------------------------------------
2274 // ------------------------------------------------------------------------
2275
8e688b92 2276 /**
2ce6dab2
KL
2277 * Method that TApplication subclasses can override to handle menu or
2278 * posted command events.
2279 *
2280 * @param command command event
2281 * @return if true, this event was consumed
8e688b92 2282 */
2ce6dab2
KL
2283 protected boolean onCommand(final TCommandEvent command) {
2284 // Default: handle cmExit
2285 if (command.equals(cmExit)) {
2286 if (messageBox("Confirmation", "Exit application?",
2287 TMessageBox.Type.YESNO).getResult() == TMessageBox.Result.YES) {
2288 quit = true;
2289 }
2290 return true;
8e688b92 2291 }
2ce6dab2
KL
2292
2293 if (command.equals(cmShell)) {
2294 openTerminal(0, 0, TWindow.RESIZABLE);
2295 return true;
2296 }
2297
2298 if (command.equals(cmTile)) {
2299 tileWindows();
2300 return true;
2301 }
2302 if (command.equals(cmCascade)) {
2303 cascadeWindows();
2304 return true;
2305 }
2306 if (command.equals(cmCloseAll)) {
2307 closeAllWindows();
2308 return true;
8e688b92 2309 }
2ce6dab2
KL
2310
2311 return false;
8e688b92
KL
2312 }
2313
2314 /**
2ce6dab2
KL
2315 * Method that TApplication subclasses can override to handle menu
2316 * events.
2317 *
2318 * @param menu menu event
2319 * @return if true, this event was consumed
8e688b92 2320 */
2ce6dab2
KL
2321 protected boolean onMenu(final TMenuEvent menu) {
2322
2323 // Default: handle MID_EXIT
2324 if (menu.getId() == TMenu.MID_EXIT) {
2325 if (messageBox("Confirmation", "Exit application?",
2326 TMessageBox.Type.YESNO).getResult() == TMessageBox.Result.YES) {
2327 quit = true;
8e688b92 2328 }
2ce6dab2
KL
2329 return true;
2330 }
bb35d919 2331
2ce6dab2
KL
2332 if (menu.getId() == TMenu.MID_SHELL) {
2333 openTerminal(0, 0, TWindow.RESIZABLE);
2334 return true;
2335 }
8e688b92 2336
2ce6dab2
KL
2337 if (menu.getId() == TMenu.MID_TILE) {
2338 tileWindows();
2339 return true;
2340 }
2341 if (menu.getId() == TMenu.MID_CASCADE) {
2342 cascadeWindows();
2343 return true;
2344 }
2345 if (menu.getId() == TMenu.MID_CLOSE_ALL) {
2346 closeAllWindows();
2347 return true;
2348 }
2349 if (menu.getId() == TMenu.MID_ABOUT) {
2350 showAboutDialog();
2351 return true;
2352 }
2353 return false;
2354 }
2355
2356 /**
2357 * Method that TApplication subclasses can override to handle keystrokes.
2358 *
2359 * @param keypress keystroke event
2360 * @return if true, this event was consumed
2361 */
2362 protected boolean onKeypress(final TKeypressEvent keypress) {
2363 // Default: only menu shortcuts
2364
2365 // Process Alt-F, Alt-E, etc. menu shortcut keys
2366 if (!keypress.getKey().isFnKey()
2367 && keypress.getKey().isAlt()
2368 && !keypress.getKey().isCtrl()
2369 && (activeMenu == null)
2370 && !modalWindowActive()
2371 ) {
2372
2373 assert (subMenus.size() == 0);
2374
2375 for (TMenu menu: menus) {
2376 if (Character.toLowerCase(menu.getMnemonic().getShortcut())
2377 == Character.toLowerCase(keypress.getKey().getChar())
2378 ) {
2379 activeMenu = menu;
2380 menu.setActive(true);
2381 return true;
bb35d919 2382 }
8e688b92
KL
2383 }
2384 }
2ce6dab2
KL
2385
2386 return false;
8e688b92
KL
2387 }
2388
2ce6dab2
KL
2389 // ------------------------------------------------------------------------
2390 // TTimer management ------------------------------------------------------
2391 // ------------------------------------------------------------------------
2392
8e688b92 2393 /**
2ce6dab2
KL
2394 * Get the amount of time I can sleep before missing a Timer tick.
2395 *
2396 * @param timeout = initial (maximum) timeout in millis
2397 * @return number of milliseconds between now and the next timer event
8e688b92 2398 */
2ce6dab2
KL
2399 private long getSleepTime(final long timeout) {
2400 Date now = new Date();
2401 long nowTime = now.getTime();
2402 long sleepTime = timeout;
2403 for (TTimer timer: timers) {
2404 long nextTickTime = timer.getNextTick().getTime();
2405 if (nextTickTime < nowTime) {
2406 return 0;
8e688b92 2407 }
2ce6dab2
KL
2408
2409 long timeDifference = nextTickTime - nowTime;
2410 if (timeDifference < sleepTime) {
2411 sleepTime = timeDifference;
8e688b92
KL
2412 }
2413 }
2ce6dab2
KL
2414 assert (sleepTime >= 0);
2415 assert (sleepTime <= timeout);
2416 return sleepTime;
8e688b92
KL
2417 }
2418
d502a0e9
KL
2419 /**
2420 * Convenience function to add a timer.
2421 *
2422 * @param duration number of milliseconds to wait between ticks
2423 * @param recurring if true, re-schedule this timer after every tick
2424 * @param action function to call when button is pressed
c6940ed9 2425 * @return the timer
d502a0e9
KL
2426 */
2427 public final TTimer addTimer(final long duration, final boolean recurring,
2428 final TAction action) {
2429
2430 TTimer timer = new TTimer(duration, recurring, action);
2431 synchronized (timers) {
2432 timers.add(timer);
2433 }
2434 return timer;
2435 }
2436
2437 /**
2438 * Convenience function to remove a timer.
2439 *
2440 * @param timer timer to remove
2441 */
2442 public final void removeTimer(final TTimer timer) {
2443 synchronized (timers) {
2444 timers.remove(timer);
2445 }
2446 }
2447
2ce6dab2
KL
2448 // ------------------------------------------------------------------------
2449 // Other TWindow constructors ---------------------------------------------
2450 // ------------------------------------------------------------------------
2451
c6940ed9
KL
2452 /**
2453 * Convenience function to spawn a message box.
2454 *
2455 * @param title window title, will be centered along the top border
2456 * @param caption message to display. Use embedded newlines to get a
2457 * multi-line box.
2458 * @return the new message box
2459 */
2460 public final TMessageBox messageBox(final String title,
2461 final String caption) {
2462
2463 return new TMessageBox(this, title, caption, TMessageBox.Type.OK);
2464 }
2465
2466 /**
2467 * Convenience function to spawn a message box.
2468 *
2469 * @param title window title, will be centered along the top border
2470 * @param caption message to display. Use embedded newlines to get a
2471 * multi-line box.
2472 * @param type one of the TMessageBox.Type constants. Default is
2473 * Type.OK.
2474 * @return the new message box
2475 */
2476 public final TMessageBox messageBox(final String title,
2477 final String caption, final TMessageBox.Type type) {
2478
2479 return new TMessageBox(this, title, caption, type);
2480 }
2481
2482 /**
2483 * Convenience function to spawn an input box.
2484 *
2485 * @param title window title, will be centered along the top border
2486 * @param caption message to display. Use embedded newlines to get a
2487 * multi-line box.
2488 * @return the new input box
2489 */
2490 public final TInputBox inputBox(final String title, final String caption) {
2491
2492 return new TInputBox(this, title, caption);
2493 }
2494
2495 /**
2496 * Convenience function to spawn an input box.
2497 *
2498 * @param title window title, will be centered along the top border
2499 * @param caption message to display. Use embedded newlines to get a
2500 * multi-line box.
2501 * @param text initial text to seed the field with
2502 * @return the new input box
2503 */
2504 public final TInputBox inputBox(final String title, final String caption,
2505 final String text) {
2506
2507 return new TInputBox(this, title, caption, text);
2508 }
1ac2ccb1 2509
34a42e78
KL
2510 /**
2511 * Convenience function to open a terminal window.
2512 *
2513 * @param x column relative to parent
2514 * @param y row relative to parent
2515 * @return the terminal new window
2516 */
2517 public final TTerminalWindow openTerminal(final int x, final int y) {
2518 return openTerminal(x, y, TWindow.RESIZABLE);
2519 }
2520
2521 /**
2522 * Convenience function to open a terminal window.
2523 *
2524 * @param x column relative to parent
2525 * @param y row relative to parent
2526 * @param flags mask of CENTERED, MODAL, or RESIZABLE
2527 * @return the terminal new window
2528 */
2529 public final TTerminalWindow openTerminal(final int x, final int y,
2530 final int flags) {
2531
2532 return new TTerminalWindow(this, x, y, flags);
2533 }
2534
0d47c546
KL
2535 /**
2536 * Convenience function to spawn an file open box.
2537 *
2538 * @param path path of selected file
2539 * @return the result of the new file open box
329fd62e 2540 * @throws IOException if java.io operation throws
0d47c546
KL
2541 */
2542 public final String fileOpenBox(final String path) throws IOException {
2543
2544 TFileOpenBox box = new TFileOpenBox(this, path, TFileOpenBox.Type.OPEN);
2545 return box.getFilename();
2546 }
2547
2548 /**
2549 * Convenience function to spawn an file open box.
2550 *
2551 * @param path path of selected file
2552 * @param type one of the Type constants
2553 * @return the result of the new file open box
329fd62e 2554 * @throws IOException if java.io operation throws
0d47c546
KL
2555 */
2556 public final String fileOpenBox(final String path,
2557 final TFileOpenBox.Type type) throws IOException {
2558
2559 TFileOpenBox box = new TFileOpenBox(this, path, type);
2560 return box.getFilename();
2561 }
2562
92453213
KL
2563 /**
2564 * Convenience function to create a new window and make it active.
2565 * Window will be located at (0, 0).
2566 *
2567 * @param title window title, will be centered along the top border
2568 * @param width width of window
2569 * @param height height of window
2570 */
2571 public final TWindow addWindow(final String title, final int width,
2572 final int height) {
2573
2574 TWindow window = new TWindow(this, title, 0, 0, width, height);
2575 return window;
2576 }
2577 /**
2578 * Convenience function to create a new window and make it active.
2579 * Window will be located at (0, 0).
2580 *
2581 * @param title window title, will be centered along the top border
2582 * @param width width of window
2583 * @param height height of window
2584 * @param flags bitmask of RESIZABLE, CENTERED, or MODAL
2585 */
2586 public final TWindow addWindow(final String title,
2587 final int width, final int height, final int flags) {
2588
2589 TWindow window = new TWindow(this, title, 0, 0, width, height, flags);
2590 return window;
2591 }
2592
2593 /**
2594 * Convenience function to create a new window and make it active.
2595 *
2596 * @param title window title, will be centered along the top border
2597 * @param x column relative to parent
2598 * @param y row relative to parent
2599 * @param width width of window
2600 * @param height height of window
2601 */
2602 public final TWindow addWindow(final String title,
2603 final int x, final int y, final int width, final int height) {
2604
2605 TWindow window = new TWindow(this, title, x, y, width, height);
2606 return window;
2607 }
2608
2609 /**
2610 * Convenience function to create a new window and make it active.
2611 *
92453213
KL
2612 * @param title window title, will be centered along the top border
2613 * @param x column relative to parent
2614 * @param y row relative to parent
2615 * @param width width of window
2616 * @param height height of window
2617 * @param flags mask of RESIZABLE, CENTERED, or MODAL
2618 */
2619 public final TWindow addWindow(final String title,
2620 final int x, final int y, final int width, final int height,
2621 final int flags) {
2622
2623 TWindow window = new TWindow(this, title, x, y, width, height, flags);
2624 return window;
2625 }
2626
7d4115a5 2627}