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