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