#68 fix ColorTheme.load()
[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 *
a69ed767 6 * Copyright (C) 2019 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
e23ea538 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;
e23ea538 38import java.text.MessageFormat;
a69ed767 39import java.util.ArrayList;
a06459bd 40import java.util.Collections;
d502a0e9 41import java.util.Date;
e826b451 42import java.util.HashMap;
4328bb42
KL
43import java.util.LinkedList;
44import java.util.List;
e826b451 45import java.util.Map;
339652cc 46import java.util.ResourceBundle;
4328bb42 47
a69ed767 48import jexer.bits.Cell;
4328bb42 49import jexer.bits.CellAttributes;
54eaded0 50import jexer.bits.Clipboard;
4328bb42 51import jexer.bits.ColorTheme;
e820d5dd 52import jexer.bits.StringUtils;
4328bb42
KL
53import jexer.event.TCommandEvent;
54import jexer.event.TInputEvent;
55import jexer.event.TKeypressEvent;
fca67db0 56import jexer.event.TMenuEvent;
4328bb42
KL
57import jexer.event.TMouseEvent;
58import jexer.event.TResizeEvent;
59import jexer.backend.Backend;
be72cb5c 60import jexer.backend.MultiBackend;
a69ed767 61import jexer.backend.Screen;
a4406f4e 62import jexer.backend.SwingBackend;
4328bb42 63import jexer.backend.ECMA48Backend;
3e074355 64import jexer.backend.TWindowBackend;
928811d8
KL
65import jexer.menu.TMenu;
66import jexer.menu.TMenuItem;
1dac6b8d 67import jexer.menu.TSubMenu;
4328bb42 68import static jexer.TCommand.*;
2ce6dab2 69import static jexer.TKeypress.*;
4328bb42 70
7d4115a5 71/**
42873e30
KL
72 * TApplication is the main driver class for a full Text User Interface
73 * application. It manages windows, provides a menu bar and status bar, and
74 * processes events received from the user.
7d4115a5 75 */
a4406f4e 76public class TApplication implements Runnable {
7d4115a5 77
339652cc
KL
78 /**
79 * Translated strings.
80 */
81 private static final ResourceBundle i18n = ResourceBundle.getBundle(TApplication.class.getName());
82
2ce6dab2 83 // ------------------------------------------------------------------------
d36057df 84 // Constants --------------------------------------------------------------
2ce6dab2
KL
85 // ------------------------------------------------------------------------
86
99144c71
KL
87 /**
88 * If true, emit thread stuff to System.err.
89 */
90 private static final boolean debugThreads = false;
91
a83fea2b
KL
92 /**
93 * If true, emit events being processed to System.err.
94 */
95 private static final boolean debugEvents = false;
96
a7986f7b
KL
97 /**
98 * If true, do "smart placement" on new windows that are not specified to
99 * be centered.
100 */
101 private static final boolean smartWindowPlacement = true;
102
a4406f4e
KL
103 /**
104 * Two backend types are available.
105 */
106 public static enum BackendType {
107 /**
108 * A Swing JFrame.
109 */
110 SWING,
111
112 /**
113 * An ECMA48 / ANSI X3.64 / XTERM style terminal.
114 */
115 ECMA48,
116
117 /**
329fd62e 118 * Synonym for ECMA48.
a4406f4e
KL
119 */
120 XTERM
121 }
122
2ce6dab2 123 // ------------------------------------------------------------------------
d36057df 124 // Variables --------------------------------------------------------------
2ce6dab2
KL
125 // ------------------------------------------------------------------------
126
d36057df
KL
127 /**
128 * The primary event handler thread.
129 */
130 private volatile WidgetEventHandler primaryEventHandler;
131
132 /**
133 * The secondary event handler thread.
134 */
135 private volatile WidgetEventHandler secondaryEventHandler;
136
d14e2d78
KL
137 /**
138 * The screen handler thread.
139 */
140 private volatile ScreenHandler screenHandler;
141
d36057df
KL
142 /**
143 * The widget receiving events from the secondary event handler thread.
144 */
145 private volatile TWidget secondaryEventReceiver;
146
147 /**
148 * Access to the physical screen, keyboard, and mouse.
149 */
150 private Backend backend;
151
54eaded0
KL
152 /**
153 * The clipboard for copy and paste.
154 */
155 private Clipboard clipboard = new Clipboard();
156
d36057df
KL
157 /**
158 * Actual mouse coordinate X.
159 */
160 private int mouseX;
161
162 /**
163 * Actual mouse coordinate Y.
164 */
165 private int mouseY;
166
a69ed767
KL
167 /**
168 * Old drawn version of mouse coordinate X.
169 */
170 private int oldDrawnMouseX;
171
172 /**
173 * Old drawn version mouse coordinate Y.
174 */
175 private int oldDrawnMouseY;
176
177 /**
178 * Old drawn version mouse cell.
179 */
180 private Cell oldDrawnMouseCell = new Cell();
181
d36057df
KL
182 /**
183 * The last mouse up click time, used to determine if this is a mouse
184 * double-click.
185 */
186 private long lastMouseUpTime;
187
188 /**
189 * The amount of millis between mouse up events to assume a double-click.
190 */
191 private long doubleClickTime = 250;
192
193 /**
194 * Event queue that is filled by run().
195 */
196 private List<TInputEvent> fillEventQueue;
197
198 /**
199 * Event queue that will be drained by either primary or secondary
200 * Thread.
201 */
202 private List<TInputEvent> drainEventQueue;
203
204 /**
205 * Top-level menus in this application.
206 */
207 private List<TMenu> menus;
208
209 /**
210 * Stack of activated sub-menus in this application.
211 */
212 private List<TMenu> subMenus;
213
214 /**
215 * The currently active menu.
216 */
217 private TMenu activeMenu = null;
218
219 /**
220 * Active keyboard accelerators.
221 */
222 private Map<TKeypress, TMenuItem> accelerators;
223
224 /**
225 * All menu items.
226 */
227 private List<TMenuItem> menuItems;
228
229 /**
230 * Windows and widgets pull colors from this ColorTheme.
231 */
232 private ColorTheme theme;
233
234 /**
235 * The top-level windows (but not menus).
236 */
237 private List<TWindow> windows;
238
d36057df
KL
239 /**
240 * Timers that are being ticked.
241 */
242 private List<TTimer> timers;
243
244 /**
245 * When true, the application has been started.
246 */
247 private volatile boolean started = false;
248
249 /**
250 * When true, exit the application.
251 */
252 private volatile boolean quit = false;
253
254 /**
255 * When true, repaint the entire screen.
256 */
257 private volatile boolean repaint = true;
258
259 /**
260 * Y coordinate of the top edge of the desktop. For now this is a
261 * constant. Someday it would be nice to have a multi-line menu or
262 * toolbars.
263 */
2bb26984 264 private int desktopTop = 1;
d36057df
KL
265
266 /**
267 * Y coordinate of the bottom edge of the desktop.
268 */
269 private int desktopBottom;
270
271 /**
272 * An optional TDesktop background window that is drawn underneath
273 * everything else.
274 */
275 private TDesktop desktop;
276
277 /**
278 * If true, focus follows mouse: windows automatically raised if the
279 * mouse passes over them.
280 */
281 private boolean focusFollowsMouse = false;
282
80b1b7b5
KL
283 /**
284 * If true, display a text-based mouse cursor.
285 */
286 private boolean textMouse = true;
287
288 /**
289 * If true, hide the mouse after typing a keystroke.
290 */
291 private boolean hideMouseWhenTyping = false;
292
293 /**
294 * If true, the mouse should not be displayed because a keystroke was
295 * typed.
296 */
297 private boolean typingHidMouse = false;
298
2bb26984
KL
299 /**
300 * If true, hide the status bar.
301 */
302 private boolean hideStatusBar = false;
303
304 /**
305 * If true, hide the menu bar.
306 */
307 private boolean hideMenuBar = false;
308
a69ed767
KL
309 /**
310 * The list of commands to run before the next I/O check.
311 */
312 private List<Runnable> invokeLaters = new LinkedList<Runnable>();
313
ea544542
KL
314 /**
315 * The last time the screen was resized.
316 */
317 private long screenResizeTime = 0;
318
54eaded0
KL
319 /**
320 * If true, screen selection is a rectangle.
321 */
322 private boolean screenSelectionRectangle = false;
323
324 /**
325 * If true, the mouse is dragging a screen selection.
326 */
327 private boolean inScreenSelection = false;
328
329 /**
330 * Screen selection starting X.
331 */
332 private int screenSelectionX0;
333
334 /**
335 * Screen selection starting Y.
336 */
337 private int screenSelectionY0;
338
339 /**
340 * Screen selection ending X.
341 */
342 private int screenSelectionX1;
343
344 /**
345 * Screen selection ending Y.
346 */
347 private int screenSelectionY1;
348
c6940ed9
KL
349 /**
350 * WidgetEventHandler is the main event consumer loop. There are at most
351 * two such threads in existence: the primary for normal case and a
352 * secondary that is used for TMessageBox, TInputBox, and similar.
353 */
354 private class WidgetEventHandler implements Runnable {
355 /**
356 * The main application.
357 */
358 private TApplication application;
359
360 /**
361 * Whether or not this WidgetEventHandler is the primary or secondary
362 * thread.
363 */
364 private boolean primary = true;
365
366 /**
367 * Public constructor.
368 *
369 * @param application the main application
370 * @param primary if true, this is the primary event handler thread
371 */
372 public WidgetEventHandler(final TApplication application,
373 final boolean primary) {
374
375 this.application = application;
376 this.primary = primary;
377 }
378
379 /**
380 * The consumer loop.
381 */
382 public void run() {
a69ed767
KL
383 // Wrap everything in a try, so that if we go belly up we can let
384 // the user have their terminal back.
385 try {
386 runImpl();
387 } catch (Throwable t) {
388 this.application.restoreConsole();
389 t.printStackTrace();
390 this.application.exit();
391 }
392 }
393
394 /**
395 * The consumer loop.
396 */
397 private void runImpl() {
be72cb5c 398 boolean first = true;
c6940ed9
KL
399
400 // Loop forever
401 while (!application.quit) {
402
403 // Wait until application notifies me
404 while (!application.quit) {
405 try {
406 synchronized (application.drainEventQueue) {
407 if (application.drainEventQueue.size() > 0) {
408 break;
409 }
410 }
92554d64 411
be72cb5c
KL
412 long timeout = 0;
413 if (first) {
414 first = false;
415 } else {
416 timeout = application.getSleepTime(1000);
417 }
418
419 if (timeout == 0) {
420 // A timer needs to fire, break out.
421 break;
422 }
92554d64 423
be72cb5c 424 if (debugThreads) {
a69ed767 425 System.err.printf("%d %s %s %s sleep %d millis\n",
be72cb5c 426 System.currentTimeMillis(), this,
a69ed767
KL
427 primary ? "primary" : "secondary",
428 Thread.currentThread(), timeout);
be72cb5c 429 }
92554d64 430
be72cb5c
KL
431 synchronized (this) {
432 this.wait(timeout);
433 }
92554d64 434
be72cb5c 435 if (debugThreads) {
a69ed767 436 System.err.printf("%d %s %s %s AWAKE\n",
be72cb5c 437 System.currentTimeMillis(), this,
a69ed767
KL
438 primary ? "primary" : "secondary",
439 Thread.currentThread());
be72cb5c
KL
440 }
441
442 if ((!primary)
443 && (application.secondaryEventReceiver == null)
444 ) {
445 // Secondary thread, emergency exit. If we got
446 // here then something went wrong with the
447 // handoff between yield() and closeWindow().
448 synchronized (application.primaryEventHandler) {
449 application.primaryEventHandler.notify();
c6940ed9 450 }
be72cb5c
KL
451 application.secondaryEventHandler = null;
452 throw new RuntimeException("secondary exited " +
453 "at wrong time");
c6940ed9 454 }
be72cb5c 455 break;
c6940ed9
KL
456 } catch (InterruptedException e) {
457 // SQUASH
458 }
be72cb5c 459 } // while (!application.quit)
ef368bd0 460
c6940ed9
KL
461 // Pull all events off the queue
462 for (;;) {
463 TInputEvent event = null;
464 synchronized (application.drainEventQueue) {
465 if (application.drainEventQueue.size() == 0) {
466 break;
467 }
468 event = application.drainEventQueue.remove(0);
469 }
be72cb5c
KL
470
471 // We will have an event to process, so repaint the
472 // screen at the end.
bd8d51fa 473 application.repaint = true;
be72cb5c 474
c6940ed9
KL
475 if (primary) {
476 primaryHandleEvent(event);
477 } else {
478 secondaryHandleEvent(event);
479 }
480 if ((!primary)
481 && (application.secondaryEventReceiver == null)
482 ) {
99144c71
KL
483 // Secondary thread, time to exit.
484
a69ed767
KL
485 // Eliminate my reference so that wakeEventHandler()
486 // resumes working on the primary.
487 application.secondaryEventHandler = null;
488
b9724916
KL
489 // We are ready to exit, wake up the primary thread.
490 // Remember that it is currently sleeping inside its
491 // primaryHandleEvent().
92554d64
KL
492 synchronized (application.primaryEventHandler) {
493 application.primaryEventHandler.notify();
494 }
92554d64
KL
495
496 // All done!
c6940ed9
KL
497 return;
498 }
92554d64 499
be72cb5c 500 } // for (;;)
ef368bd0 501
be72cb5c
KL
502 // Fire timers, update screen.
503 if (!quit) {
504 application.finishEventProcessing();
c6940ed9 505 }
92554d64 506
c6940ed9
KL
507 } // while (true) (main runnable loop)
508 }
509 }
510
d14e2d78
KL
511 /**
512 * ScreenHandler pushes screen updates to the physical device.
513 */
514 private class ScreenHandler implements Runnable {
515 /**
516 * The main application.
517 */
518 private TApplication application;
519
520 /**
521 * The dirty flag.
522 */
523 private boolean dirty = false;
524
525 /**
526 * Public constructor.
527 *
528 * @param application the main application
529 */
530 public ScreenHandler(final TApplication application) {
531 this.application = application;
532 }
533
534 /**
535 * The screen update loop.
536 */
537 public void run() {
538 // Wrap everything in a try, so that if we go belly up we can let
539 // the user have their terminal back.
540 try {
541 runImpl();
542 } catch (Throwable t) {
543 this.application.restoreConsole();
544 t.printStackTrace();
545 this.application.exit();
546 }
547 }
548
549 /**
550 * The update loop.
551 */
552 private void runImpl() {
553
554 // Loop forever
555 while (!application.quit) {
556
557 // Wait until application notifies me
558 while (!application.quit) {
559 try {
560 synchronized (this) {
561 if (dirty) {
562 dirty = false;
563 break;
564 }
565
566 // Always check within 50 milliseconds.
567 this.wait(50);
568 }
569 } catch (InterruptedException e) {
570 // SQUASH
571 }
572 } // while (!application.quit)
573
e820d5dd 574 // Flush the screen contents
d14e2d78
KL
575 if (debugThreads) {
576 System.err.printf("%d %s backend.flushScreen()\n",
577 System.currentTimeMillis(), Thread.currentThread());
578 }
579 synchronized (getScreen()) {
580 backend.flushScreen();
581 }
582 } // while (true) (main runnable loop)
583
584 // Shutdown the user I/O thread(s)
585 backend.shutdown();
586 }
587
588 /**
589 * Set the dirty flag.
590 */
591 public void setDirty() {
592 synchronized (this) {
593 dirty = true;
594 }
595 }
596
597 }
598
d36057df
KL
599 // ------------------------------------------------------------------------
600 // Constructors -----------------------------------------------------------
601 // ------------------------------------------------------------------------
c6940ed9
KL
602
603 /**
d36057df
KL
604 * Public constructor.
605 *
606 * @param backendType BackendType.XTERM, BackendType.ECMA48 or
607 * BackendType.SWING
608 * @param windowWidth the number of text columns to start with
609 * @param windowHeight the number of text rows to start with
610 * @param fontSize the size in points
611 * @throws UnsupportedEncodingException if an exception is thrown when
612 * creating the InputStreamReader
c6940ed9 613 */
d36057df
KL
614 public TApplication(final BackendType backendType, final int windowWidth,
615 final int windowHeight, final int fontSize)
616 throws UnsupportedEncodingException {
c6940ed9 617
d36057df
KL
618 switch (backendType) {
619 case SWING:
620 backend = new SwingBackend(this, windowWidth, windowHeight,
621 fontSize);
622 break;
623 case XTERM:
624 // Fall through...
625 case ECMA48:
626 backend = new ECMA48Backend(this, null, null, windowWidth,
627 windowHeight, fontSize);
628 break;
629 default:
630 throw new IllegalArgumentException("Invalid backend type: "
631 + backendType);
632 }
633 TApplicationImpl();
634 }
c6940ed9 635
92554d64 636 /**
d36057df
KL
637 * Public constructor.
638 *
639 * @param backendType BackendType.XTERM, BackendType.ECMA48 or
640 * BackendType.SWING
641 * @throws UnsupportedEncodingException if an exception is thrown when
642 * creating the InputStreamReader
92554d64 643 */
d36057df
KL
644 public TApplication(final BackendType backendType)
645 throws UnsupportedEncodingException {
b2d49e0f 646
d36057df
KL
647 switch (backendType) {
648 case SWING:
649 // The default SwingBackend is 80x25, 20 pt font. If you want to
650 // change that, you can pass the extra arguments to the
651 // SwingBackend constructor here. For example, if you wanted
652 // 90x30, 16 pt font:
653 //
654 // backend = new SwingBackend(this, 90, 30, 16);
655 backend = new SwingBackend(this);
656 break;
657 case XTERM:
658 // Fall through...
659 case ECMA48:
660 backend = new ECMA48Backend(this, null, null);
661 break;
662 default:
663 throw new IllegalArgumentException("Invalid backend type: "
664 + backendType);
92554d64 665 }
d36057df 666 TApplicationImpl();
92554d64
KL
667 }
668
7d4115a5 669 /**
d36057df 670 * Public constructor. The backend type will be BackendType.ECMA48.
55d2b2c2 671 *
d36057df
KL
672 * @param input an InputStream connected to the remote user, or null for
673 * System.in. If System.in is used, then on non-Windows systems it will
674 * be put in raw mode; shutdown() will (blindly!) put System.in in cooked
675 * mode. input is always converted to a Reader with UTF-8 encoding.
676 * @param output an OutputStream connected to the remote user, or null
677 * for System.out. output is always converted to a Writer with UTF-8
678 * encoding.
679 * @throws UnsupportedEncodingException if an exception is thrown when
680 * creating the InputStreamReader
55d2b2c2 681 */
d36057df
KL
682 public TApplication(final InputStream input,
683 final OutputStream output) throws UnsupportedEncodingException {
684
685 backend = new ECMA48Backend(this, input, output);
686 TApplicationImpl();
55d2b2c2
KL
687 }
688
48e27807 689 /**
d36057df 690 * Public constructor. The backend type will be BackendType.ECMA48.
48e27807 691 *
d36057df
KL
692 * @param input the InputStream underlying 'reader'. Its available()
693 * method is used to determine if reader.read() will block or not.
694 * @param reader a Reader connected to the remote user.
695 * @param writer a PrintWriter connected to the remote user.
696 * @param setRawMode if true, set System.in into raw mode with stty.
697 * This should in general not be used. It is here solely for Demo3,
698 * which uses System.in.
699 * @throws IllegalArgumentException if input, reader, or writer are null.
48e27807 700 */
d36057df
KL
701 public TApplication(final InputStream input, final Reader reader,
702 final PrintWriter writer, final boolean setRawMode) {
703
704 backend = new ECMA48Backend(this, input, reader, writer, setRawMode);
705 TApplicationImpl();
48e27807
KL
706 }
707
4328bb42 708 /**
d36057df
KL
709 * Public constructor. The backend type will be BackendType.ECMA48.
710 *
711 * @param input the InputStream underlying 'reader'. Its available()
712 * method is used to determine if reader.read() will block or not.
713 * @param reader a Reader connected to the remote user.
714 * @param writer a PrintWriter connected to the remote user.
715 * @throws IllegalArgumentException if input, reader, or writer are null.
4328bb42 716 */
d36057df
KL
717 public TApplication(final InputStream input, final Reader reader,
718 final PrintWriter writer) {
4328bb42 719
d36057df
KL
720 this(input, reader, writer, false);
721 }
4328bb42 722
bd8d51fa 723 /**
d36057df
KL
724 * Public constructor. This hook enables use with new non-Jexer
725 * backends.
726 *
727 * @param backend a Backend that is already ready to go.
bd8d51fa 728 */
d36057df
KL
729 public TApplication(final Backend backend) {
730 this.backend = backend;
731 backend.setListener(this);
732 TApplicationImpl();
733 }
bd8d51fa
KL
734
735 /**
d36057df 736 * Finish construction once the backend is set.
bd8d51fa 737 */
d36057df 738 private void TApplicationImpl() {
2bb26984
KL
739 // Text block mouse option
740 if (System.getProperty("jexer.textMouse", "true").equals("false")) {
741 textMouse = false;
742 }
743
744 // Hide mouse when typing option
745 if (System.getProperty("jexer.hideMouseWhenTyping",
746 "false").equals("true")) {
747
748 hideMouseWhenTyping = true;
749 }
750
751 // Hide status bar option
752 if (System.getProperty("jexer.hideStatusBar",
753 "false").equals("true")) {
754 hideStatusBar = true;
755 }
756
757 // Hide menu bar option
758 if (System.getProperty("jexer.hideMenuBar", "false").equals("true")) {
759 hideMenuBar = true;
760 }
761
d36057df 762 theme = new ColorTheme();
2bb26984
KL
763 desktopTop = (hideMenuBar ? 0 : 1);
764 desktopBottom = getScreen().getHeight() - 1 + (hideStatusBar ? 1 : 0);
a69ed767
KL
765 fillEventQueue = new LinkedList<TInputEvent>();
766 drainEventQueue = new LinkedList<TInputEvent>();
d36057df 767 windows = new LinkedList<TWindow>();
a69ed767
KL
768 menus = new ArrayList<TMenu>();
769 subMenus = new ArrayList<TMenu>();
d36057df
KL
770 timers = new LinkedList<TTimer>();
771 accelerators = new HashMap<TKeypress, TMenuItem>();
a69ed767 772 menuItems = new LinkedList<TMenuItem>();
d36057df 773 desktop = new TDesktop(this);
bd8d51fa 774
d36057df
KL
775 // Special case: the Swing backend needs to have a timer to drive its
776 // blink state.
777 if ((backend instanceof SwingBackend)
778 || (backend instanceof MultiBackend)
779 ) {
780 // Default to 500 millis, unless a SwingBackend has its own
781 // value.
782 long millis = 500;
783 if (backend instanceof SwingBackend) {
784 millis = ((SwingBackend) backend).getBlinkMillis();
785 }
786 if (millis > 0) {
787 addTimer(millis, true,
788 new TAction() {
789 public void DO() {
790 TApplication.this.doRepaint();
791 }
792 }
793 );
794 }
795 }
80b1b7b5 796
d36057df 797 }
b6faeac0 798
d36057df
KL
799 // ------------------------------------------------------------------------
800 // Runnable ---------------------------------------------------------------
801 // ------------------------------------------------------------------------
b6faeac0 802
4328bb42 803 /**
d36057df 804 * Run this application until it exits.
4328bb42 805 */
d36057df 806 public void run() {
abb84744
KL
807 // System.err.println("*** TApplication.run() begins ***");
808
d14e2d78
KL
809 // Start the screen updater thread
810 screenHandler = new ScreenHandler(this);
811 (new Thread(screenHandler)).start();
812
d36057df
KL
813 // Start the main consumer thread
814 primaryEventHandler = new WidgetEventHandler(this, true);
815 (new Thread(primaryEventHandler)).start();
8e688b92 816
d36057df 817 started = true;
4328bb42 818
d36057df
KL
819 while (!quit) {
820 synchronized (this) {
821 boolean doWait = false;
fca67db0 822
d36057df
KL
823 if (!backend.hasEvents()) {
824 synchronized (fillEventQueue) {
825 if (fillEventQueue.size() == 0) {
826 doWait = true;
827 }
828 }
829 }
fca67db0 830
d36057df
KL
831 if (doWait) {
832 // No I/O to dispatch, so wait until the backend
833 // provides new I/O.
834 try {
835 if (debugThreads) {
836 System.err.println(System.currentTimeMillis() +
a69ed767 837 " " + Thread.currentThread() + " MAIN sleep");
d36057df 838 }
fca67db0 839
d36057df 840 this.wait();
e826b451 841
d36057df
KL
842 if (debugThreads) {
843 System.err.println(System.currentTimeMillis() +
a69ed767 844 " " + Thread.currentThread() + " MAIN AWAKE");
d36057df
KL
845 }
846 } catch (InterruptedException e) {
847 // I'm awake and don't care why, let's see what's
848 // going on out there.
849 }
850 }
efb7af1f 851
d36057df 852 } // synchronized (this)
7b5261bc 853
d36057df
KL
854 synchronized (fillEventQueue) {
855 // Pull any pending I/O events
856 backend.getEvents(fillEventQueue);
4328bb42 857
d36057df
KL
858 // Dispatch each event to the appropriate handler, one at a
859 // time.
860 for (;;) {
861 TInputEvent event = null;
862 if (fillEventQueue.size() == 0) {
863 break;
864 }
865 event = fillEventQueue.remove(0);
866 metaHandleEvent(event);
867 }
868 }
a06459bd 869
d36057df
KL
870 // Wake a consumer thread if we have any pending events.
871 if (drainEventQueue.size() > 0) {
872 wakeEventHandler();
873 }
92453213 874
d36057df 875 } // while (!quit)
d502a0e9 876
d36057df
KL
877 // Shutdown the event consumer threads
878 if (secondaryEventHandler != null) {
879 synchronized (secondaryEventHandler) {
880 secondaryEventHandler.notify();
881 }
882 }
883 if (primaryEventHandler != null) {
884 synchronized (primaryEventHandler) {
885 primaryEventHandler.notify();
886 }
887 }
b2d49e0f 888
d36057df
KL
889 // Close all the windows. This gives them an opportunity to release
890 // resources.
891 closeAllWindows();
4328bb42 892
955c55b7
KL
893 // Close the desktop.
894 if (desktop != null) {
895 setDesktop(null);
896 }
897
abb84744
KL
898 // Give the overarching application an opportunity to release
899 // resources.
900 onExit();
901
902 // System.err.println("*** TApplication.run() exits ***");
be72cb5c
KL
903 }
904
d36057df
KL
905 // ------------------------------------------------------------------------
906 // Event handlers ---------------------------------------------------------
907 // ------------------------------------------------------------------------
48e27807
KL
908
909 /**
d36057df
KL
910 * Method that TApplication subclasses can override to handle menu or
911 * posted command events.
48e27807 912 *
d36057df
KL
913 * @param command command event
914 * @return if true, this event was consumed
48e27807 915 */
d36057df
KL
916 protected boolean onCommand(final TCommandEvent command) {
917 // Default: handle cmExit
918 if (command.equals(cmExit)) {
919 if (messageBox(i18n.getString("exitDialogTitle"),
920 i18n.getString("exitDialogText"),
a69ed767
KL
921 TMessageBox.Type.YESNO).isYes()) {
922
d36057df
KL
923 exit();
924 }
925 return true;
926 }
48e27807 927
d36057df
KL
928 if (command.equals(cmShell)) {
929 openTerminal(0, 0, TWindow.RESIZABLE);
930 return true;
931 }
4328bb42 932
d36057df
KL
933 if (command.equals(cmTile)) {
934 tileWindows();
935 return true;
936 }
937 if (command.equals(cmCascade)) {
938 cascadeWindows();
939 return true;
940 }
941 if (command.equals(cmCloseAll)) {
942 closeAllWindows();
943 return true;
944 }
0ee88b6d 945
2bb26984 946 if (command.equals(cmMenu) && (hideMenuBar == false)) {
d36057df
KL
947 if (!modalWindowActive() && (activeMenu == null)) {
948 if (menus.size() > 0) {
949 menus.get(0).setActive(true);
950 activeMenu = menus.get(0);
951 return true;
952 }
953 }
0ee88b6d 954 }
0ee88b6d 955
d36057df 956 return false;
0ee88b6d
KL
957 }
958
92453213 959 /**
d36057df
KL
960 * Method that TApplication subclasses can override to handle menu
961 * events.
92453213 962 *
d36057df
KL
963 * @param menu menu event
964 * @return if true, this event was consumed
92453213 965 */
d36057df 966 protected boolean onMenu(final TMenuEvent menu) {
92453213 967
d36057df
KL
968 // Default: handle MID_EXIT
969 if (menu.getId() == TMenu.MID_EXIT) {
970 if (messageBox(i18n.getString("exitDialogTitle"),
971 i18n.getString("exitDialogText"),
a69ed767
KL
972 TMessageBox.Type.YESNO).isYes()) {
973
d36057df
KL
974 exit();
975 }
976 return true;
977 }
92453213 978
d36057df
KL
979 if (menu.getId() == TMenu.MID_SHELL) {
980 openTerminal(0, 0, TWindow.RESIZABLE);
981 return true;
982 }
72fca17b 983
d36057df
KL
984 if (menu.getId() == TMenu.MID_TILE) {
985 tileWindows();
986 return true;
987 }
988 if (menu.getId() == TMenu.MID_CASCADE) {
989 cascadeWindows();
990 return true;
991 }
992 if (menu.getId() == TMenu.MID_CLOSE_ALL) {
993 closeAllWindows();
994 return true;
995 }
e23ea538
KL
996 if (menu.getId() == TMenu.MID_ABOUT) {
997 showAboutDialog();
998 return true;
999 }
d36057df 1000 if (menu.getId() == TMenu.MID_REPAINT) {
a69ed767 1001 getScreen().clearPhysical();
d36057df
KL
1002 doRepaint();
1003 return true;
1004 }
e23ea538
KL
1005 if (menu.getId() == TMenu.MID_VIEW_IMAGE) {
1006 openImage();
1007 return true;
1008 }
a75902fa 1009 if (menu.getId() == TMenu.MID_SCREEN_OPTIONS) {
e23ea538
KL
1010 new TFontChooserWindow(this);
1011 return true;
1012 }
51e46b3e
KL
1013
1014 if (menu.getId() == TMenu.MID_CUT) {
1015 postMenuEvent(new TCommandEvent(cmCut));
1016 return true;
1017 }
1018 if (menu.getId() == TMenu.MID_COPY) {
1019 postMenuEvent(new TCommandEvent(cmCopy));
1020 return true;
1021 }
1022 if (menu.getId() == TMenu.MID_PASTE) {
1023 postMenuEvent(new TCommandEvent(cmPaste));
1024 return true;
1025 }
1026 if (menu.getId() == TMenu.MID_CLEAR) {
1027 postMenuEvent(new TCommandEvent(cmClear));
1028 return true;
1029 }
1030
d36057df 1031 return false;
72fca17b
KL
1032 }
1033
1034 /**
d36057df 1035 * Method that TApplication subclasses can override to handle keystrokes.
72fca17b 1036 *
d36057df
KL
1037 * @param keypress keystroke event
1038 * @return if true, this event was consumed
72fca17b 1039 */
d36057df
KL
1040 protected boolean onKeypress(final TKeypressEvent keypress) {
1041 // Default: only menu shortcuts
72fca17b 1042
d36057df
KL
1043 // Process Alt-F, Alt-E, etc. menu shortcut keys
1044 if (!keypress.getKey().isFnKey()
1045 && keypress.getKey().isAlt()
1046 && !keypress.getKey().isCtrl()
1047 && (activeMenu == null)
1048 && !modalWindowActive()
2bb26984 1049 && (hideMenuBar == false)
d36057df 1050 ) {
2ce6dab2 1051
d36057df 1052 assert (subMenus.size() == 0);
2ce6dab2 1053
d36057df
KL
1054 for (TMenu menu: menus) {
1055 if (Character.toLowerCase(menu.getMnemonic().getShortcut())
1056 == Character.toLowerCase(keypress.getKey().getChar())
1057 ) {
1058 activeMenu = menu;
1059 menu.setActive(true);
1060 return true;
1061 }
1062 }
1063 }
1064
1065 return false;
1066 }
2ce6dab2 1067
eb29bbb5 1068 /**
d36057df 1069 * Process background events, and update the screen.
eb29bbb5 1070 */
d36057df
KL
1071 private void finishEventProcessing() {
1072 if (debugThreads) {
1073 System.err.printf(System.currentTimeMillis() + " " +
1074 Thread.currentThread() + " finishEventProcessing()\n");
1075 }
eb29bbb5 1076
51e46b3e
KL
1077 // See if we need to enable/disable the edit menu.
1078 EditMenuUser widget = null;
1079 if (activeMenu == null) {
3d3e09e6 1080 TWindow activeWindow = getActiveWindow();
51e46b3e
KL
1081 if (activeWindow != null) {
1082 if (activeWindow.getActiveChild() instanceof EditMenuUser) {
1083 widget = (EditMenuUser) activeWindow.getActiveChild();
1084 }
1085 } else if (desktop != null) {
1086 if (desktop.getActiveChild() instanceof EditMenuUser) {
1087 widget = (EditMenuUser) desktop.getActiveChild();
1088 }
1089 }
1090 if (widget == null) {
1091 disableMenuItem(TMenu.MID_CUT);
1092 disableMenuItem(TMenu.MID_COPY);
1093 disableMenuItem(TMenu.MID_PASTE);
1094 disableMenuItem(TMenu.MID_CLEAR);
1095 } else {
1096 if (widget.isEditMenuCut()) {
1097 enableMenuItem(TMenu.MID_CUT);
1098 } else {
1099 disableMenuItem(TMenu.MID_CUT);
1100 }
1101 if (widget.isEditMenuCopy()) {
1102 enableMenuItem(TMenu.MID_COPY);
1103 } else {
1104 disableMenuItem(TMenu.MID_COPY);
1105 }
1106 if (widget.isEditMenuPaste()) {
1107 enableMenuItem(TMenu.MID_PASTE);
1108 } else {
1109 disableMenuItem(TMenu.MID_PASTE);
1110 }
1111 if (widget.isEditMenuClear()) {
1112 enableMenuItem(TMenu.MID_CLEAR);
1113 } else {
1114 disableMenuItem(TMenu.MID_CLEAR);
1115 }
1116 }
1117 }
1118
d36057df
KL
1119 // Process timers and call doIdle()'s
1120 doIdle();
1121
1122 // Update the screen
1123 synchronized (getScreen()) {
1124 drawAll();
1125 }
1126
d14e2d78
KL
1127 // Wake up the screen repainter
1128 wakeScreenHandler();
1129
d36057df
KL
1130 if (debugThreads) {
1131 System.err.printf(System.currentTimeMillis() + " " +
1132 Thread.currentThread() + " finishEventProcessing() END\n");
eb29bbb5 1133 }
eb29bbb5
KL
1134 }
1135
4328bb42 1136 /**
d36057df
KL
1137 * Peek at certain application-level events, add to eventQueue, and wake
1138 * up the consuming Thread.
4328bb42 1139 *
d36057df 1140 * @param event the input event to consume
a4406f4e 1141 */
d36057df 1142 private void metaHandleEvent(final TInputEvent event) {
a4406f4e 1143
d36057df
KL
1144 if (debugEvents) {
1145 System.err.printf(String.format("metaHandleEvents event: %s\n",
1146 event)); System.err.flush();
a4406f4e 1147 }
6985c572 1148
d36057df
KL
1149 if (quit) {
1150 // Do no more processing if the application is already trying
1151 // to exit.
1152 return;
1153 }
30bd4abd 1154
d36057df 1155 // Special application-wide events -------------------------------
c6940ed9 1156
d36057df
KL
1157 // Abort everything
1158 if (event instanceof TCommandEvent) {
1159 TCommandEvent command = (TCommandEvent) event;
abb84744 1160 if (command.equals(cmAbort)) {
d36057df
KL
1161 exit();
1162 return;
be72cb5c
KL
1163 }
1164 }
4328bb42 1165
d36057df
KL
1166 synchronized (drainEventQueue) {
1167 // Screen resize
1168 if (event instanceof TResizeEvent) {
1169 TResizeEvent resize = (TResizeEvent) event;
1170 synchronized (getScreen()) {
ea544542
KL
1171 if ((System.currentTimeMillis() - screenResizeTime >= 15)
1172 || (resize.getWidth() < getScreen().getWidth())
1173 || (resize.getHeight() < getScreen().getHeight())
1174 ) {
1175 getScreen().setDimensions(resize.getWidth(),
1176 resize.getHeight());
1177 screenResizeTime = System.currentTimeMillis();
1178 }
d36057df 1179 desktopBottom = getScreen().getHeight() - 1;
2bb26984
KL
1180 if (hideStatusBar) {
1181 desktopBottom++;
1182 }
d36057df
KL
1183 mouseX = 0;
1184 mouseY = 0;
d36057df
KL
1185 }
1186 if (desktop != null) {
2bc32111
KL
1187 desktop.setDimensions(0, desktopTop, resize.getWidth(),
1188 (desktopBottom - desktopTop));
5434cb2b 1189 desktop.onResize(resize);
d36057df 1190 }
2ce6dab2 1191
d36057df
KL
1192 // Change menu edges if needed.
1193 recomputeMenuX();
be72cb5c 1194
d36057df
KL
1195 // We are dirty, redraw the screen.
1196 doRepaint();
be72cb5c 1197
d36057df
KL
1198 /*
1199 System.err.println("New screen: " + resize.getWidth() +
1200 " x " + resize.getHeight());
1201 */
1202 return;
1203 }
be72cb5c 1204
d36057df
KL
1205 // Put into the main queue
1206 drainEventQueue.add(event);
be72cb5c
KL
1207 }
1208 }
1209
4328bb42 1210 /**
d36057df
KL
1211 * Dispatch one event to the appropriate widget or application-level
1212 * event handler. This is the primary event handler, it has the normal
1213 * application-wide event handling.
bd8d51fa 1214 *
d36057df
KL
1215 * @param event the input event to consume
1216 * @see #secondaryHandleEvent(TInputEvent event)
4328bb42 1217 */
d36057df
KL
1218 private void primaryHandleEvent(final TInputEvent event) {
1219
1220 if (debugEvents) {
a69ed767
KL
1221 System.err.printf("%s primaryHandleEvent: %s\n",
1222 Thread.currentThread(), event);
7b5261bc 1223 }
d36057df 1224 TMouseEvent doubleClick = null;
4328bb42 1225
d36057df 1226 // Special application-wide events -----------------------------------
339652cc 1227
80b1b7b5
KL
1228 if (event instanceof TKeypressEvent) {
1229 if (hideMouseWhenTyping) {
1230 typingHidMouse = true;
1231 }
1232 }
1233
d36057df
KL
1234 // Peek at the mouse position
1235 if (event instanceof TMouseEvent) {
80b1b7b5
KL
1236 typingHidMouse = false;
1237
d36057df 1238 TMouseEvent mouse = (TMouseEvent) event;
54eaded0
KL
1239 if (mouse.isMouse1() && (mouse.isShift() || mouse.isCtrl())) {
1240 // Screen selection.
1241 if (inScreenSelection) {
1242 screenSelectionX1 = mouse.getX();
1243 screenSelectionY1 = mouse.getY();
1244 } else {
1245 inScreenSelection = true;
1246 screenSelectionX0 = mouse.getX();
1247 screenSelectionY0 = mouse.getY();
1248 screenSelectionX1 = mouse.getX();
1249 screenSelectionY1 = mouse.getY();
1250 screenSelectionRectangle = mouse.isCtrl();
1251 }
1252 } else {
1253 if (inScreenSelection) {
1254 getScreen().copySelection(clipboard, screenSelectionX0,
1255 screenSelectionY0, screenSelectionX1, screenSelectionY1,
1256 screenSelectionRectangle);
1257 }
1258 inScreenSelection = false;
1259 }
1260
d36057df 1261 if ((mouseX != mouse.getX()) || (mouseY != mouse.getY())) {
d36057df
KL
1262 mouseX = mouse.getX();
1263 mouseY = mouse.getY();
1264 } else {
e23ea538
KL
1265 if ((mouse.getType() == TMouseEvent.Type.MOUSE_DOWN)
1266 && (!mouse.isMouseWheelUp())
1267 && (!mouse.isMouseWheelDown())
1268 ) {
d36057df
KL
1269 if ((mouse.getTime().getTime() - lastMouseUpTime) <
1270 doubleClickTime) {
99144c71 1271
d36057df
KL
1272 // This is a double-click.
1273 doubleClick = new TMouseEvent(TMouseEvent.Type.
1274 MOUSE_DOUBLE_CLICK,
1275 mouse.getX(), mouse.getY(),
1276 mouse.getAbsoluteX(), mouse.getAbsoluteY(),
1277 mouse.isMouse1(), mouse.isMouse2(),
1278 mouse.isMouse3(),
6e9daafb
KL
1279 mouse.isMouseWheelUp(), mouse.isMouseWheelDown(),
1280 mouse.isAlt(), mouse.isCtrl(), mouse.isShift());
d36057df
KL
1281
1282 } else {
1283 // The first click of a potential double-click.
1284 lastMouseUpTime = mouse.getTime().getTime();
1285 }
1d14ffab 1286 }
bd8d51fa 1287 }
7b5261bc 1288
d36057df
KL
1289 // See if we need to switch focus to another window or the menu
1290 checkSwitchFocus((TMouseEvent) event);
99144c71
KL
1291 }
1292
d36057df
KL
1293 // Handle menu events
1294 if ((activeMenu != null) && !(event instanceof TCommandEvent)) {
1295 TMenu menu = activeMenu;
7b5261bc 1296
d36057df
KL
1297 if (event instanceof TMouseEvent) {
1298 TMouseEvent mouse = (TMouseEvent) event;
7b5261bc 1299
d36057df
KL
1300 while (subMenus.size() > 0) {
1301 TMenu subMenu = subMenus.get(subMenus.size() - 1);
1302 if (subMenu.mouseWouldHit(mouse)) {
1303 break;
1304 }
1305 if ((mouse.getType() == TMouseEvent.Type.MOUSE_MOTION)
1306 && (!mouse.isMouse1())
1307 && (!mouse.isMouse2())
1308 && (!mouse.isMouse3())
1309 && (!mouse.isMouseWheelUp())
1310 && (!mouse.isMouseWheelDown())
1311 ) {
1312 break;
1313 }
1314 // We navigated away from a sub-menu, so close it
1315 closeSubMenu();
1316 }
7b5261bc 1317
d36057df
KL
1318 // Convert the mouse relative x/y to menu coordinates
1319 assert (mouse.getX() == mouse.getAbsoluteX());
1320 assert (mouse.getY() == mouse.getAbsoluteY());
1321 if (subMenus.size() > 0) {
1322 menu = subMenus.get(subMenus.size() - 1);
1323 }
1324 mouse.setX(mouse.getX() - menu.getX());
1325 mouse.setY(mouse.getY() - menu.getY());
7b5261bc 1326 }
d36057df
KL
1327 menu.handleEvent(event);
1328 return;
7b5261bc 1329 }
7b5261bc 1330
d36057df
KL
1331 if (event instanceof TKeypressEvent) {
1332 TKeypressEvent keypress = (TKeypressEvent) event;
2ce6dab2 1333
d36057df
KL
1334 // See if this key matches an accelerator, and is not being
1335 // shortcutted by the active window, and if so dispatch the menu
1336 // event.
1337 boolean windowWillShortcut = false;
3d3e09e6 1338 TWindow activeWindow = getActiveWindow();
d36057df
KL
1339 if (activeWindow != null) {
1340 assert (activeWindow.isShown());
1341 if (activeWindow.isShortcutKeypress(keypress.getKey())) {
1342 // We do not process this key, it will be passed to the
1343 // window instead.
1344 windowWillShortcut = true;
1345 }
1346 }
7b5261bc 1347
d36057df
KL
1348 if (!windowWillShortcut && !modalWindowActive()) {
1349 TKeypress keypressLowercase = keypress.getKey().toLowerCase();
1350 TMenuItem item = null;
1351 synchronized (accelerators) {
1352 item = accelerators.get(keypressLowercase);
1353 }
1354 if (item != null) {
1355 if (item.isEnabled()) {
1356 // Let the menu item dispatch
1357 item.dispatch();
1358 return;
339652cc 1359 }
be72cb5c 1360 }
d36057df
KL
1361
1362 // Handle the keypress
1363 if (onKeypress(keypress)) {
1364 return;
1365 }
7b5261bc
KL
1366 }
1367 }
1368
d36057df
KL
1369 if (event instanceof TCommandEvent) {
1370 if (onCommand((TCommandEvent) event)) {
1371 return;
1372 }
7b5261bc 1373 }
7b5261bc 1374
d36057df
KL
1375 if (event instanceof TMenuEvent) {
1376 if (onMenu((TMenuEvent) event)) {
1377 return;
1378 }
1d14ffab 1379 }
7b5261bc 1380
d36057df
KL
1381 // Dispatch events to the active window -------------------------------
1382 boolean dispatchToDesktop = true;
3d3e09e6 1383 TWindow window = getActiveWindow();
d36057df
KL
1384 if (window != null) {
1385 assert (window.isActive());
1386 assert (window.isShown());
1387 if (event instanceof TMouseEvent) {
1388 TMouseEvent mouse = (TMouseEvent) event;
1389 // Convert the mouse relative x/y to window coordinates
1390 assert (mouse.getX() == mouse.getAbsoluteX());
1391 assert (mouse.getY() == mouse.getAbsoluteY());
1392 mouse.setX(mouse.getX() - window.getX());
1393 mouse.setY(mouse.getY() - window.getY());
4328bb42 1394
d36057df
KL
1395 if (doubleClick != null) {
1396 doubleClick.setX(doubleClick.getX() - window.getX());
1397 doubleClick.setY(doubleClick.getY() - window.getY());
1398 }
2ce6dab2 1399
d36057df
KL
1400 if (window.mouseWouldHit(mouse)) {
1401 dispatchToDesktop = false;
1402 }
1403 } else if (event instanceof TKeypressEvent) {
1404 dispatchToDesktop = false;
5ffeabcc
KL
1405 } else if (event instanceof TMenuEvent) {
1406 dispatchToDesktop = false;
d36057df
KL
1407 }
1408
1409 if (debugEvents) {
1410 System.err.printf("TApplication dispatch event: %s\n",
1411 event);
1412 }
1413 window.handleEvent(event);
1414 if (doubleClick != null) {
1415 window.handleEvent(doubleClick);
1416 }
1417 }
1418 if (dispatchToDesktop) {
1419 // This event is fair game for the desktop to process.
1420 if (desktop != null) {
1421 desktop.handleEvent(event);
1422 if (doubleClick != null) {
1423 desktop.handleEvent(doubleClick);
1424 }
1425 }
be72cb5c 1426 }
42873e30
KL
1427 }
1428
4328bb42 1429 /**
d36057df
KL
1430 * Dispatch one event to the appropriate widget or application-level
1431 * event handler. This is the secondary event handler used by certain
1432 * special dialogs (currently TMessageBox and TFileOpenBox).
1433 *
1434 * @param event the input event to consume
1435 * @see #primaryHandleEvent(TInputEvent event)
4328bb42 1436 */
d36057df
KL
1437 private void secondaryHandleEvent(final TInputEvent event) {
1438 TMouseEvent doubleClick = null;
2027327c 1439
a69ed767
KL
1440 if (debugEvents) {
1441 System.err.printf("%s secondaryHandleEvent: %s\n",
1442 Thread.currentThread(), event);
1443 }
1444
d36057df
KL
1445 // Peek at the mouse position
1446 if (event instanceof TMouseEvent) {
80b1b7b5
KL
1447 typingHidMouse = false;
1448
d36057df
KL
1449 TMouseEvent mouse = (TMouseEvent) event;
1450 if ((mouseX != mouse.getX()) || (mouseY != mouse.getY())) {
d36057df
KL
1451 mouseX = mouse.getX();
1452 mouseY = mouse.getY();
1453 } else {
e23ea538
KL
1454 if ((mouse.getType() == TMouseEvent.Type.MOUSE_DOWN)
1455 && (!mouse.isMouseWheelUp())
1456 && (!mouse.isMouseWheelDown())
1457 ) {
d36057df
KL
1458 if ((mouse.getTime().getTime() - lastMouseUpTime) <
1459 doubleClickTime) {
b2d49e0f 1460
d36057df
KL
1461 // This is a double-click.
1462 doubleClick = new TMouseEvent(TMouseEvent.Type.
1463 MOUSE_DOUBLE_CLICK,
1464 mouse.getX(), mouse.getY(),
1465 mouse.getAbsoluteX(), mouse.getAbsoluteY(),
1466 mouse.isMouse1(), mouse.isMouse2(),
1467 mouse.isMouse3(),
6e9daafb
KL
1468 mouse.isMouseWheelUp(), mouse.isMouseWheelDown(),
1469 mouse.isAlt(), mouse.isCtrl(), mouse.isShift());
be72cb5c 1470
d36057df
KL
1471 } else {
1472 // The first click of a potential double-click.
1473 lastMouseUpTime = mouse.getTime().getTime();
6358f6e5 1474 }
be72cb5c 1475 }
d36057df
KL
1476 }
1477 }
be72cb5c 1478
d36057df 1479 secondaryEventReceiver.handleEvent(event);
5255f69c
KL
1480 // Note that it is possible for secondaryEventReceiver to be null
1481 // now, because its handleEvent() might have finished out on the
1482 // secondary thread. So put any extra processing inside a null
1483 // check.
1484 if (secondaryEventReceiver != null) {
1485 if (doubleClick != null) {
1486 secondaryEventReceiver.handleEvent(doubleClick);
1487 }
d36057df
KL
1488 }
1489 }
be72cb5c 1490
d36057df
KL
1491 /**
1492 * Enable a widget to override the primary event thread.
1493 *
1494 * @param widget widget that will receive events
1495 */
1496 public final void enableSecondaryEventReceiver(final TWidget widget) {
1497 if (debugThreads) {
1498 System.err.println(System.currentTimeMillis() +
1499 " enableSecondaryEventReceiver()");
1500 }
be72cb5c 1501
d36057df
KL
1502 assert (secondaryEventReceiver == null);
1503 assert (secondaryEventHandler == null);
1504 assert ((widget instanceof TMessageBox)
1505 || (widget instanceof TFileOpenBox));
1506 secondaryEventReceiver = widget;
1507 secondaryEventHandler = new WidgetEventHandler(this, false);
1508
1509 (new Thread(secondaryEventHandler)).start();
1510 }
1511
1512 /**
1513 * Yield to the secondary thread.
1514 */
1515 public final void yield() {
a69ed767
KL
1516 if (debugThreads) {
1517 System.err.printf(System.currentTimeMillis() + " " +
1518 Thread.currentThread() + " yield()\n");
1519 }
1520
d36057df
KL
1521 assert (secondaryEventReceiver != null);
1522
1523 while (secondaryEventReceiver != null) {
1524 synchronized (primaryEventHandler) {
1525 try {
1526 primaryEventHandler.wait();
1527 } catch (InterruptedException e) {
1528 // SQUASH
8e688b92 1529 }
d36057df
KL
1530 }
1531 }
1532 }
7b5261bc 1533
d36057df
KL
1534 /**
1535 * Do stuff when there is no user input.
1536 */
1537 private void doIdle() {
1538 if (debugThreads) {
1539 System.err.printf(System.currentTimeMillis() + " " +
1540 Thread.currentThread() + " doIdle()\n");
1541 }
ef368bd0 1542
d36057df 1543 synchronized (timers) {
8e688b92 1544
d36057df
KL
1545 if (debugThreads) {
1546 System.err.printf(System.currentTimeMillis() + " " +
1547 Thread.currentThread() + " doIdle() 2\n");
1548 }
1549
1550 // Run any timers that have timed out
1551 Date now = new Date();
1552 List<TTimer> keepTimers = new LinkedList<TTimer>();
1553 for (TTimer timer: timers) {
1554 if (timer.getNextTick().getTime() <= now.getTime()) {
1555 // Something might change, so repaint the screen.
1556 repaint = true;
1557 timer.tick();
1558 if (timer.recurring) {
1559 keepTimers.add(timer);
be72cb5c 1560 }
d36057df
KL
1561 } else {
1562 keepTimers.add(timer);
8e688b92 1563 }
8e688b92 1564 }
e394cb85
KL
1565 timers.clear();
1566 timers.addAll(keepTimers);
d36057df 1567 }
7b5261bc 1568
d36057df
KL
1569 // Call onIdle's
1570 for (TWindow window: windows) {
1571 window.onIdle();
1572 }
1573 if (desktop != null) {
1574 desktop.onIdle();
1575 }
a69ed767 1576
3c5921e6
KL
1577 // Run any invokeLaters. We make a copy, and run that, because one
1578 // of these Runnables might add call TApplication.invokeLater().
1579 List<Runnable> invokes = new ArrayList<Runnable>();
a69ed767 1580 synchronized (invokeLaters) {
3c5921e6 1581 invokes.addAll(invokeLaters);
a69ed767
KL
1582 invokeLaters.clear();
1583 }
3c5921e6
KL
1584 for (Runnable invoke: invokes) {
1585 invoke.run();
1586 }
1587 doRepaint();
a69ed767 1588
d36057df 1589 }
92554d64 1590
d36057df
KL
1591 /**
1592 * Wake the sleeping active event handler.
1593 */
1594 private void wakeEventHandler() {
1595 if (!started) {
1596 return;
1597 }
92554d64 1598
92554d64
KL
1599 if (secondaryEventHandler != null) {
1600 synchronized (secondaryEventHandler) {
1601 secondaryEventHandler.notify();
1602 }
d36057df
KL
1603 } else {
1604 assert (primaryEventHandler != null);
92554d64
KL
1605 synchronized (primaryEventHandler) {
1606 primaryEventHandler.notify();
1607 }
7b5261bc 1608 }
d36057df 1609 }
7b5261bc 1610
d14e2d78
KL
1611 /**
1612 * Wake the sleeping screen handler.
1613 */
1614 private void wakeScreenHandler() {
1615 if (!started) {
1616 return;
1617 }
1618
1619 synchronized (screenHandler) {
1620 screenHandler.notify();
1621 }
1622 }
1623
d36057df
KL
1624 // ------------------------------------------------------------------------
1625 // TApplication -----------------------------------------------------------
1626 // ------------------------------------------------------------------------
92554d64 1627
a69ed767
KL
1628 /**
1629 * Place a command on the run queue, and run it before the next round of
1630 * checking I/O.
1631 *
1632 * @param command the command to run later
1633 */
1634 public void invokeLater(final Runnable command) {
1635 synchronized (invokeLaters) {
1636 invokeLaters.add(command);
1637 }
e23ea538 1638 doRepaint();
a69ed767
KL
1639 }
1640
1641 /**
1642 * Restore the console to sane defaults. This is meant to be used for
1643 * improper exits (e.g. a caught exception in main()), and should not be
1644 * necessary for normal program termination.
1645 */
1646 public void restoreConsole() {
1647 if (backend != null) {
1648 if (backend instanceof ECMA48Backend) {
1649 backend.shutdown();
1650 }
1651 }
1652 }
1653
d36057df
KL
1654 /**
1655 * Get the Backend.
1656 *
1657 * @return the Backend
1658 */
1659 public final Backend getBackend() {
1660 return backend;
4328bb42
KL
1661 }
1662
1663 /**
d36057df 1664 * Get the Screen.
4328bb42 1665 *
d36057df 1666 * @return the Screen
4328bb42 1667 */
d36057df
KL
1668 public final Screen getScreen() {
1669 if (backend instanceof TWindowBackend) {
1670 // We are being rendered to a TWindow. We can't use its
1671 // getScreen() method because that is how it is rendering to a
1672 // hardware backend somewhere. Instead use its getOtherScreen()
1673 // method.
1674 return ((TWindowBackend) backend).getOtherScreen();
1675 } else {
1676 return backend.getScreen();
8e688b92 1677 }
d36057df 1678 }
7b5261bc 1679
d36057df
KL
1680 /**
1681 * Get the color theme.
1682 *
1683 * @return the theme
1684 */
1685 public final ColorTheme getTheme() {
1686 return theme;
1687 }
7b5261bc 1688
51e46b3e
KL
1689 /**
1690 * Get the clipboard.
1691 *
1692 * @return the clipboard
1693 */
1694 public final Clipboard getClipboard() {
1695 return clipboard;
1696 }
1697
d36057df
KL
1698 /**
1699 * Repaint the screen on the next update.
1700 */
1701 public void doRepaint() {
1702 repaint = true;
1703 wakeEventHandler();
1704 }
68c5cd6b 1705
d36057df
KL
1706 /**
1707 * Get Y coordinate of the top edge of the desktop.
1708 *
1709 * @return Y coordinate of the top edge of the desktop
1710 */
1711 public final int getDesktopTop() {
1712 return desktopTop;
1713 }
68c5cd6b 1714
d36057df
KL
1715 /**
1716 * Get Y coordinate of the bottom edge of the desktop.
1717 *
1718 * @return Y coordinate of the bottom edge of the desktop
1719 */
1720 public final int getDesktopBottom() {
1721 return desktopBottom;
1722 }
7b5261bc 1723
d36057df
KL
1724 /**
1725 * Set the TDesktop instance.
1726 *
1727 * @param desktop a TDesktop instance, or null to remove the one that is
1728 * set
1729 */
1730 public final void setDesktop(final TDesktop desktop) {
1731 if (this.desktop != null) {
955c55b7
KL
1732 this.desktop.onPreClose();
1733 this.desktop.onUnfocus();
d36057df 1734 this.desktop.onClose();
be72cb5c 1735 }
d36057df 1736 this.desktop = desktop;
4328bb42
KL
1737 }
1738
a06459bd 1739 /**
d36057df 1740 * Get the TDesktop instance.
a06459bd 1741 *
d36057df 1742 * @return the desktop, or null if it is not set
a06459bd 1743 */
d36057df
KL
1744 public final TDesktop getDesktop() {
1745 return desktop;
1746 }
fca67db0 1747
d36057df
KL
1748 /**
1749 * Get the current active window.
1750 *
1751 * @return the active window, or null if it is not set
1752 */
1753 public final TWindow getActiveWindow() {
3d3e09e6
KL
1754 for (TWindow window: windows) {
1755 if (window.isShown() && window.isActive()) {
1756 return window;
1757 }
1758 }
1759 return null;
d36057df 1760 }
fca67db0 1761
d36057df
KL
1762 /**
1763 * Get a (shallow) copy of the window list.
1764 *
1765 * @return a copy of the list of windows for this application
1766 */
1767 public final List<TWindow> getAllWindows() {
a69ed767 1768 List<TWindow> result = new ArrayList<TWindow>();
d36057df
KL
1769 result.addAll(windows);
1770 return result;
1771 }
b6faeac0 1772
d36057df
KL
1773 /**
1774 * Get focusFollowsMouse flag.
1775 *
1776 * @return true if focus follows mouse: windows automatically raised if
1777 * the mouse passes over them
1778 */
1779 public boolean getFocusFollowsMouse() {
1780 return focusFollowsMouse;
1781 }
b6faeac0 1782
d36057df
KL
1783 /**
1784 * Set focusFollowsMouse flag.
1785 *
1786 * @param focusFollowsMouse if true, focus follows mouse: windows
1787 * automatically raised if the mouse passes over them
1788 */
1789 public void setFocusFollowsMouse(final boolean focusFollowsMouse) {
1790 this.focusFollowsMouse = focusFollowsMouse;
1791 }
e8a11f98 1792
e23ea538
KL
1793 /**
1794 * Display the about dialog.
1795 */
1796 protected void showAboutDialog() {
1797 String version = getClass().getPackage().getImplementationVersion();
1798 if (version == null) {
1799 // This is Java 9+, use a hardcoded string here.
c334c9a2 1800 version = "1.0.0";
e23ea538
KL
1801 }
1802 messageBox(i18n.getString("aboutDialogTitle"),
1803 MessageFormat.format(i18n.getString("aboutDialogText"), version),
1804 TMessageBox.Type.OK);
1805 }
1806
1807 /**
1808 * Handle the Tool | Open image menu item.
1809 */
1810 private void openImage() {
1811 try {
1812 List<String> filters = new ArrayList<String>();
1813 filters.add("^.*\\.[Jj][Pp][Gg]$");
1814 filters.add("^.*\\.[Jj][Pp][Ee][Gg]$");
1815 filters.add("^.*\\.[Pp][Nn][Gg]$");
1816 filters.add("^.*\\.[Gg][Ii][Ff]$");
1817 filters.add("^.*\\.[Bb][Mm][Pp]$");
1818 String filename = fileOpenBox(".", TFileOpenBox.Type.OPEN, filters);
1819 if (filename != null) {
1820 new TImageWindow(this, new File(filename));
1821 }
1822 } catch (IOException e) {
1823 // Show this exception to the user.
1824 new TExceptionDialog(this, e);
1825 }
1826 }
1827
9696a8f6
KL
1828 /**
1829 * Check if application is still running.
1830 *
1831 * @return true if the application is running
1832 */
1833 public final boolean isRunning() {
1834 if (quit == true) {
1835 return false;
1836 }
1837 return true;
1838 }
1839
d36057df
KL
1840 // ------------------------------------------------------------------------
1841 // Screen refresh loop ----------------------------------------------------
1842 // ------------------------------------------------------------------------
fca67db0 1843
d36057df 1844 /**
aac4f7d6 1845 * Draw the text mouse at position.
d36057df
KL
1846 *
1847 * @param x column position
1848 * @param y row position
1849 */
aac4f7d6 1850 private void drawTextMouse(final int x, final int y) {
3d3e09e6 1851 TWindow activeWindow = getActiveWindow();
3af53a35 1852
d36057df 1853 if (debugThreads) {
aac4f7d6 1854 System.err.printf("%d %s drawTextMouse() %d %d\n",
d36057df 1855 System.currentTimeMillis(), Thread.currentThread(), x, y);
978a5d8f
KL
1856
1857 if (activeWindow != null) {
1858 System.err.println("activeWindow.hasHiddenMouse() " +
1859 activeWindow.hasHiddenMouse());
1860 }
1861 }
1862
1863 // If this cell is on top of a visible window that has requested a
1864 // hidden mouse, bail out.
1865 if ((activeWindow != null) && (activeMenu == null)) {
1866 if ((activeWindow.hasHiddenMouse() == true)
1867 && (x > activeWindow.getX())
1868 && (x < activeWindow.getX() + activeWindow.getWidth() - 1)
1869 && (y > activeWindow.getY())
1870 && (y < activeWindow.getY() + activeWindow.getHeight() - 1)
1871 ) {
9ad2ce4f
KL
1872 return;
1873 }
1874 }
1875
1876 // If this cell is on top of the desktop, and the desktop has
1877 // requested a hidden mouse, bail out.
1878 if ((desktop != null) && (activeWindow == null) && (activeMenu == null)) {
1879 if ((desktop.hasHiddenMouse() == true)
1880 && (x > desktop.getX())
1881 && (x < desktop.getX() + desktop.getWidth() - 1)
1882 && (y > desktop.getY())
1883 && (y < desktop.getY() + desktop.getHeight() - 1)
1884 ) {
978a5d8f
KL
1885 return;
1886 }
d36057df 1887 }
978a5d8f 1888
aac4f7d6 1889 getScreen().invertCell(x, y);
d36057df 1890 }
fca67db0 1891
d36057df
KL
1892 /**
1893 * Draw everything.
1894 */
1895 private void drawAll() {
1896 boolean menuIsActive = false;
fca67db0 1897
d36057df
KL
1898 if (debugThreads) {
1899 System.err.printf("%d %s drawAll() enter\n",
1900 System.currentTimeMillis(), Thread.currentThread());
fca67db0 1901 }
a06459bd 1902
a69ed767 1903 // I don't think this does anything useful anymore...
d36057df
KL
1904 if (!repaint) {
1905 if (debugThreads) {
1906 System.err.printf("%d %s drawAll() !repaint\n",
1907 System.currentTimeMillis(), Thread.currentThread());
e826b451 1908 }
a69ed767
KL
1909 if ((oldDrawnMouseX != mouseX) || (oldDrawnMouseY != mouseY)) {
1910 if (debugThreads) {
1911 System.err.printf("%d %s drawAll() !repaint MOUSE\n",
1912 System.currentTimeMillis(), Thread.currentThread());
fca67db0 1913 }
a69ed767
KL
1914
1915 // The only thing that has happened is the mouse moved.
1916
1917 // Redraw the old cell at that position, and save the cell at
1918 // the new mouse position.
1919 if (debugThreads) {
1920 System.err.printf("%d %s restoreImage() %d %d\n",
1921 System.currentTimeMillis(), Thread.currentThread(),
1922 oldDrawnMouseX, oldDrawnMouseY);
2ce6dab2 1923 }
a69ed767
KL
1924 oldDrawnMouseCell.restoreImage();
1925 getScreen().putCharXY(oldDrawnMouseX, oldDrawnMouseY,
1926 oldDrawnMouseCell);
1927 oldDrawnMouseCell = getScreen().getCharXY(mouseX, mouseY);
e6469faa 1928 if (backend instanceof ECMA48Backend) {
a69ed767
KL
1929 // Special case: the entire row containing the mouse has
1930 // to be re-drawn if it has any image data, AND any rows
1931 // in between.
1932 if (oldDrawnMouseY != mouseY) {
1933 for (int i = oldDrawnMouseY; ;) {
1934 getScreen().unsetImageRow(i);
1935 if (i == mouseY) {
1936 break;
1937 }
1938 if (oldDrawnMouseY < mouseY) {
1939 i++;
1940 } else {
1941 i--;
1942 }
1943 }
1944 } else {
1945 getScreen().unsetImageRow(mouseY);
1946 }
1947 }
1948
54eaded0
KL
1949 if (inScreenSelection) {
1950 getScreen().setSelection(screenSelectionX0,
1951 screenSelectionY0, screenSelectionX1, screenSelectionY1,
1952 screenSelectionRectangle);
1953 }
1954
80b1b7b5
KL
1955 if ((textMouse == true) && (typingHidMouse == false)) {
1956 // Draw mouse at the new position.
aac4f7d6 1957 drawTextMouse(mouseX, mouseY);
80b1b7b5 1958 }
a69ed767
KL
1959
1960 oldDrawnMouseX = mouseX;
1961 oldDrawnMouseY = mouseY;
1962 }
e6469faa 1963 if (getScreen().isDirty()) {
d14e2d78 1964 screenHandler.setDirty();
fca67db0 1965 }
a69ed767 1966 return;
fca67db0
KL
1967 }
1968
d36057df
KL
1969 if (debugThreads) {
1970 System.err.printf("%d %s drawAll() REDRAW\n",
1971 System.currentTimeMillis(), Thread.currentThread());
fca67db0
KL
1972 }
1973
d36057df
KL
1974 // If true, the cursor is not visible
1975 boolean cursor = false;
92453213 1976
d36057df
KL
1977 // Start with a clean screen
1978 getScreen().clear();
b6faeac0 1979
d36057df
KL
1980 // Draw the desktop
1981 if (desktop != null) {
1982 desktop.drawChildren();
1983 }
0ee88b6d 1984
d36057df 1985 // Draw each window in reverse Z order
a69ed767 1986 List<TWindow> sorted = new ArrayList<TWindow>(windows);
d36057df
KL
1987 Collections.sort(sorted);
1988 TWindow topLevel = null;
1989 if (sorted.size() > 0) {
1990 topLevel = sorted.get(0);
1991 }
1992 Collections.reverse(sorted);
1993 for (TWindow window: sorted) {
1994 if (window.isShown()) {
1995 window.drawChildren();
b6faeac0 1996 }
fca67db0 1997 }
d36057df 1998
2bb26984 1999 if (hideMenuBar == false) {
0ee88b6d 2000
2bb26984
KL
2001 // Draw the blank menubar line - reset the screen clipping first
2002 // so it won't trim it out.
d36057df 2003 getScreen().resetClipping();
2bb26984
KL
2004 getScreen().hLineXY(0, 0, getScreen().getWidth(), ' ',
2005 theme.getColor("tmenu"));
2006 // Now draw the menus.
2007 int x = 1;
2008 for (TMenu menu: menus) {
2009 CellAttributes menuColor;
2010 CellAttributes menuMnemonicColor;
2011 if (menu.isActive()) {
2012 menuIsActive = true;
2013 menuColor = theme.getColor("tmenu.highlighted");
2014 menuMnemonicColor = theme.getColor("tmenu.mnemonic.highlighted");
2015 topLevel = menu;
2016 } else {
2017 menuColor = theme.getColor("tmenu");
2018 menuMnemonicColor = theme.getColor("tmenu.mnemonic");
2019 }
2020 // Draw the menu title
2021 getScreen().hLineXY(x, 0,
2022 StringUtils.width(menu.getTitle()) + 2, ' ', menuColor);
2023 getScreen().putStringXY(x + 1, 0, menu.getTitle(), menuColor);
2024 // Draw the highlight character
2025 getScreen().putCharXY(x + 1 +
2026 menu.getMnemonic().getScreenShortcutIdx(),
2027 0, menu.getMnemonic().getShortcut(), menuMnemonicColor);
2028
2029 if (menu.isActive()) {
2030 ((TWindow) menu).drawChildren();
2031 // Reset the screen clipping so we can draw the next
2032 // title.
2033 getScreen().resetClipping();
2034 }
2035 x += StringUtils.width(menu.getTitle()) + 2;
2036 }
2037
2038 for (TMenu menu: subMenus) {
2039 // Reset the screen clipping so we can draw the next
2040 // sub-menu.
2041 getScreen().resetClipping();
2042 ((TWindow) menu).drawChildren();
2043 }
d36057df 2044 }
a69ed767 2045 getScreen().resetClipping();
b6faeac0 2046
2bb26984
KL
2047 if (hideStatusBar == false) {
2048 // Draw the status bar of the top-level window
2049 TStatusBar statusBar = null;
2050 if (topLevel != null) {
3c5921e6
KL
2051 if (topLevel.isShown()) {
2052 statusBar = topLevel.getStatusBar();
2053 }
2bb26984
KL
2054 }
2055 if (statusBar != null) {
2056 getScreen().resetClipping();
2057 statusBar.setWidth(getScreen().getWidth());
2058 statusBar.setY(getScreen().getHeight() - topLevel.getY());
2059 statusBar.draw();
2060 } else {
2061 CellAttributes barColor = new CellAttributes();
2062 barColor.setTo(getTheme().getColor("tstatusbar.text"));
2063 getScreen().hLineXY(0, desktopBottom, getScreen().getWidth(),
2064 ' ', barColor);
2065 }
d36057df 2066 }
b6faeac0 2067
d36057df 2068 // Draw the mouse pointer
a69ed767
KL
2069 if (debugThreads) {
2070 System.err.printf("%d %s restoreImage() %d %d\n",
2071 System.currentTimeMillis(), Thread.currentThread(),
2072 oldDrawnMouseX, oldDrawnMouseY);
2073 }
2074 oldDrawnMouseCell = getScreen().getCharXY(mouseX, mouseY);
e6469faa 2075 if (backend instanceof ECMA48Backend) {
a69ed767
KL
2076 // Special case: the entire row containing the mouse has to be
2077 // re-drawn if it has any image data, AND any rows in between.
2078 if (oldDrawnMouseY != mouseY) {
2079 for (int i = oldDrawnMouseY; ;) {
2080 getScreen().unsetImageRow(i);
2081 if (i == mouseY) {
2082 break;
2083 }
2084 if (oldDrawnMouseY < mouseY) {
2085 i++;
2086 } else {
2087 i--;
2088 }
2089 }
2090 } else {
2091 getScreen().unsetImageRow(mouseY);
2092 }
2093 }
54eaded0
KL
2094
2095 if (inScreenSelection) {
2096 getScreen().setSelection(screenSelectionX0, screenSelectionY0,
2097 screenSelectionX1, screenSelectionY1, screenSelectionRectangle);
2098 }
2099
80b1b7b5 2100 if ((textMouse == true) && (typingHidMouse == false)) {
aac4f7d6 2101 drawTextMouse(mouseX, mouseY);
80b1b7b5 2102 }
a69ed767
KL
2103 oldDrawnMouseX = mouseX;
2104 oldDrawnMouseY = mouseY;
b6faeac0 2105
d36057df
KL
2106 // Place the cursor if it is visible
2107 if (!menuIsActive) {
2bc32111
KL
2108
2109 int visibleWindowCount = 0;
2110 for (TWindow window: sorted) {
2111 if (window.isShown()) {
2112 visibleWindowCount++;
2113 }
2114 }
2115 if (visibleWindowCount == 0) {
2116 // No windows are visible, only the desktop. Allow it to
2117 // have the cursor.
2118 if (desktop != null) {
2119 sorted.add(desktop);
2120 }
2121 }
2122
d36057df
KL
2123 TWidget activeWidget = null;
2124 if (sorted.size() > 0) {
2125 activeWidget = sorted.get(sorted.size() - 1).getActiveChild();
2bc32111
KL
2126 int cursorClipTop = desktopTop;
2127 int cursorClipBottom = desktopBottom;
d36057df 2128 if (activeWidget.isCursorVisible()) {
2bc32111
KL
2129 if ((activeWidget.getCursorAbsoluteY() <= cursorClipBottom)
2130 && (activeWidget.getCursorAbsoluteY() >= cursorClipTop)
d36057df
KL
2131 ) {
2132 getScreen().putCursor(true,
2133 activeWidget.getCursorAbsoluteX(),
2134 activeWidget.getCursorAbsoluteY());
2135 cursor = true;
b6faeac0 2136 } else {
a69ed767
KL
2137 // Turn off the cursor. Also place it at 0,0.
2138 getScreen().putCursor(false, 0, 0);
d36057df 2139 cursor = false;
b6faeac0
KL
2140 }
2141 }
e8a11f98
KL
2142 }
2143 }
2144
d36057df
KL
2145 // Kill the cursor
2146 if (!cursor) {
2147 getScreen().hideCursor();
b6faeac0 2148 }
c6940ed9 2149
e6469faa 2150 if (getScreen().isDirty()) {
d14e2d78 2151 screenHandler.setDirty();
be72cb5c 2152 }
d36057df 2153 repaint = false;
a06459bd
KL
2154 }
2155
4328bb42 2156 /**
d36057df 2157 * Force this application to exit.
4328bb42 2158 */
d36057df
KL
2159 public void exit() {
2160 quit = true;
2161 synchronized (this) {
2162 this.notify();
92453213 2163 }
4328bb42 2164 }
7d4115a5 2165
abb84744
KL
2166 /**
2167 * Subclasses can use this hook to cleanup resources. Called as the last
2168 * step of TApplication.run().
2169 */
2170 public void onExit() {
2171 // Default does nothing.
2172 }
2173
2ce6dab2
KL
2174 // ------------------------------------------------------------------------
2175 // TWindow management -----------------------------------------------------
2176 // ------------------------------------------------------------------------
4328bb42 2177
92453213
KL
2178 /**
2179 * Return the total number of windows.
2180 *
2181 * @return the total number of windows
2182 */
2183 public final int windowCount() {
2184 return windows.size();
2185 }
2186
2187 /**
8c236a98 2188 * Return the number of windows that are showing.
92453213 2189 *
8c236a98 2190 * @return the number of windows that are showing on screen
92453213
KL
2191 */
2192 public final int shownWindowCount() {
2193 int n = 0;
2194 for (TWindow w: windows) {
2195 if (w.isShown()) {
2196 n++;
2197 }
2198 }
2199 return n;
2200 }
2201
8c236a98
KL
2202 /**
2203 * Return the number of windows that are hidden.
2204 *
2205 * @return the number of windows that are hidden
2206 */
2207 public final int hiddenWindowCount() {
2208 int n = 0;
2209 for (TWindow w: windows) {
2210 if (w.isHidden()) {
2211 n++;
2212 }
2213 }
2214 return n;
2215 }
2216
92453213
KL
2217 /**
2218 * Check if a window instance is in this application's window list.
2219 *
2220 * @param window window to look for
2221 * @return true if this window is in the list
2222 */
2223 public final boolean hasWindow(final TWindow window) {
2224 if (windows.size() == 0) {
2225 return false;
2226 }
2227 for (TWindow w: windows) {
2228 if (w == window) {
8c236a98 2229 assert (window.getApplication() == this);
92453213
KL
2230 return true;
2231 }
2232 }
2233 return false;
2234 }
2235
2236 /**
2237 * Activate a window: bring it to the top and have it receive events.
2238 *
2239 * @param window the window to become the new active window
2240 */
3d3e09e6 2241 public final void activateWindow(final TWindow window) {
92453213
KL
2242 if (hasWindow(window) == false) {
2243 /*
2244 * Someone has a handle to a window I don't have. Ignore this
2245 * request.
2246 */
2247 return;
2248 }
2249
3d3e09e6
KL
2250 if (modalWindowActive() && !window.isModal()) {
2251 // Do not activate a non-modal on top of a modal.
2252 return;
fe0770f9
KL
2253 }
2254
3d3e09e6
KL
2255 synchronized (windows) {
2256 // Whatever window might be moving/dragging, stop it now.
2257 for (TWindow w: windows) {
2258 if (w.inMovements()) {
2259 w.stopMovements();
2260 }
2261 }
92453213 2262
3d3e09e6 2263 assert (windows.size() > 0);
92453213 2264
3d3e09e6
KL
2265 if (window.isHidden()) {
2266 // Unhiding will also activate.
2267 showWindow(window);
2268 return;
92453213 2269 }
3d3e09e6 2270 assert (window.isShown());
92453213 2271
3d3e09e6
KL
2272 if (windows.size() == 1) {
2273 assert (window == windows.get(0));
2274 window.setZ(0);
2275 window.setActive(true);
2276 window.onFocus();
2277 return;
2278 }
92453213 2279
3d3e09e6
KL
2280 if (getActiveWindow() == window) {
2281 assert (window.isActive());
92453213 2282
3d3e09e6
KL
2283 // Window is already active, do nothing.
2284 return;
2285 }
92453213 2286
3d3e09e6 2287 assert (!window.isActive());
a69ed767 2288
3d3e09e6
KL
2289 window.setZ(-1);
2290 Collections.sort(windows);
2291 int newZ = 0;
a69ed767 2292 for (TWindow w: windows) {
3d3e09e6
KL
2293 w.setZ(newZ);
2294 newZ++;
2295 if ((w != window) && w.isActive()) {
2296 w.onUnfocus();
a69ed767 2297 }
3d3e09e6 2298 w.setActive(false);
a69ed767 2299 }
3d3e09e6
KL
2300 window.setActive(true);
2301 window.onFocus();
2302
2303 } // synchronized (windows)
499fdccf 2304
92453213
KL
2305 return;
2306 }
2307
2308 /**
2309 * Hide a window.
2310 *
2311 * @param window the window to hide
2312 */
2313 public void hideWindow(final TWindow window) {
2314 if (hasWindow(window) == false) {
2315 /*
2316 * Someone has a handle to a window I don't have. Ignore this
2317 * request.
2318 */
2319 return;
2320 }
2321
3d3e09e6
KL
2322 synchronized (windows) {
2323
2324 // Whatever window might be moving/dragging, stop it now.
2325 for (TWindow w: windows) {
2326 if (w.inMovements()) {
2327 w.stopMovements();
2328 }
fe0770f9 2329 }
fe0770f9 2330
3d3e09e6 2331 assert (windows.size() > 0);
92453213 2332
3d3e09e6
KL
2333 if (window.hidden) {
2334 return;
92453213 2335 }
3d3e09e6
KL
2336
2337 window.setActive(false);
92453213
KL
2338 window.hidden = true;
2339 window.onHide();
3d3e09e6
KL
2340
2341 TWindow activeWindow = null;
2342 for (TWindow w: windows) {
2343 if (w.isShown()) {
2344 activeWindow = w;
2345 break;
2346 }
2347 }
2348 assert (activeWindow != window);
2349 if (activeWindow != null) {
2350 activateWindow(activeWindow);
2351 }
2352
2353 } // synchronized (windows)
2354
92453213
KL
2355 }
2356
2357 /**
2358 * Show a window.
2359 *
2360 * @param window the window to show
2361 */
2362 public void showWindow(final TWindow window) {
2363 if (hasWindow(window) == false) {
2364 /*
2365 * Someone has a handle to a window I don't have. Ignore this
2366 * request.
2367 */
2368 return;
2369 }
2370
92453213
KL
2371 if (window.hidden) {
2372 window.hidden = false;
2373 window.onShow();
2374 activateWindow(window);
2375 }
3d3e09e6 2376
92453213
KL
2377 }
2378
48e27807 2379 /**
3d3e09e6 2380 * Close window.
48e27807
KL
2381 *
2382 * @param window the window to remove
2383 */
2384 public final void closeWindow(final TWindow window) {
92453213
KL
2385 if (hasWindow(window) == false) {
2386 /*
2387 * Someone has a handle to a window I don't have. Ignore this
2388 * request.
2389 */
2390 return;
2391 }
2392
a69ed767
KL
2393 // Let window know that it is about to be closed, while it is still
2394 // visible on screen.
2395 window.onPreClose();
2396
bb35d919 2397 synchronized (windows) {
fe0770f9 2398
3d3e09e6 2399 window.stopMovements();
efb7af1f 2400 window.onUnfocus();
e23ea538 2401 windows.remove(window);
bb35d919 2402 Collections.sort(windows);
e23ea538 2403
3d3e09e6
KL
2404 TWindow nextWindow = null;
2405 int newZ = 0;
bb35d919 2406 for (TWindow w: windows) {
3d3e09e6 2407 w.stopMovements();
e23ea538
KL
2408 w.setZ(newZ);
2409 newZ++;
3eacc236
KL
2410
2411 // Do not activate a hidden window.
2412 if (w.isHidden()) {
2413 continue;
2414 }
3d3e09e6
KL
2415 if (nextWindow == null) {
2416 nextWindow = w;
2417 } else {
2418 if (w.isActive()) {
2419 w.setActive(false);
2420 w.onUnfocus();
2421 }
e23ea538 2422 }
3d3e09e6 2423 }
e23ea538 2424
3d3e09e6
KL
2425 if (nextWindow != null) {
2426 nextWindow.setActive(true);
2427 nextWindow.onFocus();
48e27807 2428 }
3d3e09e6
KL
2429
2430 } // synchronized (windows)
48e27807
KL
2431
2432 // Perform window cleanup
2433 window.onClose();
2434
48e27807 2435 // Check if we are closing a TMessageBox or similar
c6940ed9
KL
2436 if (secondaryEventReceiver != null) {
2437 assert (secondaryEventHandler != null);
48e27807
KL
2438
2439 // Do not send events to the secondaryEventReceiver anymore, the
2440 // window is closed.
2441 secondaryEventReceiver = null;
2442
92554d64
KL
2443 // Wake the secondary thread, it will wake the primary as it
2444 // exits.
2445 synchronized (secondaryEventHandler) {
2446 secondaryEventHandler.notify();
48e27807 2447 }
3d3e09e6
KL
2448
2449 } // synchronized (windows)
92453213
KL
2450
2451 // Permit desktop to be active if it is the only thing left.
2452 if (desktop != null) {
2453 if (windows.size() == 0) {
2454 desktop.setActive(true);
2455 }
2456 }
48e27807
KL
2457 }
2458
2459 /**
2460 * Switch to the next window.
2461 *
2462 * @param forward if true, then switch to the next window in the list,
2463 * otherwise switch to the previous window in the list
2464 */
2465 public final void switchWindow(final boolean forward) {
8c236a98
KL
2466 // Only switch if there are multiple visible windows
2467 if (shownWindowCount() < 2) {
48e27807
KL
2468 return;
2469 }
3d3e09e6
KL
2470
2471 if (modalWindowActive()) {
2472 // Do not switch if a window is modal
2473 return;
2474 }
48e27807 2475
bb35d919
KL
2476 synchronized (windows) {
2477
3d3e09e6
KL
2478 TWindow window = windows.get(0);
2479 do {
2480 assert (window != null);
2481 if (forward) {
2482 window.setZ(windows.size());
92453213 2483 } else {
3d3e09e6
KL
2484 TWindow lastWindow = windows.get(windows.size() - 1);
2485 lastWindow.setZ(-1);
bb35d919 2486 }
48e27807 2487
3d3e09e6
KL
2488 Collections.sort(windows);
2489 int newZ = 0;
2490 for (TWindow w: windows) {
2491 w.setZ(newZ);
2492 newZ++;
bb35d919 2493 }
bb35d919 2494
3d3e09e6
KL
2495 window = windows.get(0);
2496 } while (!window.isShown());
2497
2498 // The next visible window is now on top. Renumber the list.
2499 for (TWindow w: windows) {
2500 w.stopMovements();
2501 if ((w != window) && w.isActive()) {
2502 assert (w.isShown());
2503 w.setActive(false);
2504 w.onUnfocus();
8c236a98
KL
2505 }
2506 }
48e27807 2507
3d3e09e6
KL
2508 // Next visible window is on top.
2509 assert (window.isShown());
2510 window.setActive(true);
2511 window.onFocus();
2512
2513 } // synchronized (windows)
48e27807
KL
2514 }
2515
2516 /**
051e2913
KL
2517 * Add a window to my window list and make it active. Note package
2518 * private access.
48e27807
KL
2519 *
2520 * @param window new window to add
2521 */
051e2913 2522 final void addWindowToApplication(final TWindow window) {
a7986f7b
KL
2523
2524 // Do not add menu windows to the window list.
2525 if (window instanceof TMenu) {
2526 return;
2527 }
2528
0ee88b6d
KL
2529 // Do not add the desktop to the window list.
2530 if (window instanceof TDesktop) {
2531 return;
2532 }
2533
bb35d919 2534 synchronized (windows) {
051e2913
KL
2535 if (windows.contains(window)) {
2536 throw new IllegalArgumentException("Window " + window +
2537 " is already in window list");
2538 }
2539
fe0770f9
KL
2540 // Whatever window might be moving/dragging, stop it now.
2541 for (TWindow w: windows) {
2542 if (w.inMovements()) {
2543 w.stopMovements();
2544 }
2545 }
2546
2ce6dab2
KL
2547 // Do not allow a modal window to spawn a non-modal window. If a
2548 // modal window is active, then this window will become modal
2549 // too.
2550 if (modalWindowActive()) {
2551 window.flags |= TWindow.MODAL;
a7986f7b 2552 window.flags |= TWindow.CENTERED;
92453213 2553 window.hidden = false;
bb35d919 2554 }
92453213
KL
2555 if (window.isShown()) {
2556 for (TWindow w: windows) {
2557 if (w.isActive()) {
2558 w.setActive(false);
2559 w.onUnfocus();
2560 }
2561 w.setZ(w.getZ() + 1);
efb7af1f 2562 }
3d3e09e6
KL
2563 window.setZ(0);
2564 window.setActive(true);
2565 window.onFocus();
2566 windows.add(0, window);
2567 } else {
2568 window.setZ(windows.size());
2569 windows.add(window);
92453213 2570 }
a7986f7b
KL
2571
2572 if (((window.flags & TWindow.CENTERED) == 0)
d36057df
KL
2573 && ((window.flags & TWindow.ABSOLUTEXY) == 0)
2574 && (smartWindowPlacement == true)
2bb26984 2575 && (!(window instanceof TDesktop))
d36057df 2576 ) {
a7986f7b
KL
2577
2578 doSmartPlacement(window);
2579 }
48e27807 2580 }
92453213
KL
2581
2582 // Desktop cannot be active over any other window.
2583 if (desktop != null) {
2584 desktop.setActive(false);
2585 }
3d3e09e6 2586
48e27807
KL
2587 }
2588
fca67db0
KL
2589 /**
2590 * Check if there is a system-modal window on top.
2591 *
2592 * @return true if the active window is modal
2593 */
2594 private boolean modalWindowActive() {
2595 if (windows.size() == 0) {
2596 return false;
2597 }
2ce6dab2
KL
2598
2599 for (TWindow w: windows) {
2600 if (w.isModal()) {
2601 return true;
2602 }
2603 }
2604
2605 return false;
2606 }
2607
9696a8f6
KL
2608 /**
2609 * Check if there is a window with overridden menu flag on top.
2610 *
2611 * @return true if the active window is overriding the menu
2612 */
2613 private boolean overrideMenuWindowActive() {
3d3e09e6 2614 TWindow activeWindow = getActiveWindow();
9696a8f6
KL
2615 if (activeWindow != null) {
2616 if (activeWindow.hasOverriddenMenu()) {
2617 return true;
2618 }
2619 }
2620
2621 return false;
2622 }
2623
2ce6dab2
KL
2624 /**
2625 * Close all open windows.
2626 */
2627 private void closeAllWindows() {
2628 // Don't do anything if we are in the menu
2629 if (activeMenu != null) {
2630 return;
2631 }
2632 while (windows.size() > 0) {
2633 closeWindow(windows.get(0));
2634 }
fca67db0
KL
2635 }
2636
2ce6dab2
KL
2637 /**
2638 * Re-layout the open windows as non-overlapping tiles. This produces
2639 * almost the same results as Turbo Pascal 7.0's IDE.
2640 */
2641 private void tileWindows() {
2642 synchronized (windows) {
2643 // Don't do anything if we are in the menu
2644 if (activeMenu != null) {
2645 return;
2646 }
2647 int z = windows.size();
2648 if (z == 0) {
2649 return;
2650 }
2651 int a = 0;
2652 int b = 0;
2653 a = (int)(Math.sqrt(z));
2654 int c = 0;
2655 while (c < a) {
2656 b = (z - c) / a;
2657 if (((a * b) + c) == z) {
2658 break;
2659 }
2660 c++;
2661 }
2662 assert (a > 0);
2663 assert (b > 0);
2664 assert (c < a);
2665 int newWidth = (getScreen().getWidth() / a);
2666 int newHeight1 = ((getScreen().getHeight() - 1) / b);
2667 int newHeight2 = ((getScreen().getHeight() - 1) / (b + c));
2668
a69ed767 2669 List<TWindow> sorted = new ArrayList<TWindow>(windows);
2ce6dab2
KL
2670 Collections.sort(sorted);
2671 Collections.reverse(sorted);
2672 for (int i = 0; i < sorted.size(); i++) {
2673 int logicalX = i / b;
2674 int logicalY = i % b;
2675 if (i >= ((a - 1) * b)) {
2676 logicalX = a - 1;
2677 logicalY = i - ((a - 1) * b);
2678 }
2679
2680 TWindow w = sorted.get(i);
7d922e0d
KL
2681 int oldWidth = w.getWidth();
2682 int oldHeight = w.getHeight();
2683
2ce6dab2
KL
2684 w.setX(logicalX * newWidth);
2685 w.setWidth(newWidth);
2686 if (i >= ((a - 1) * b)) {
2687 w.setY((logicalY * newHeight2) + 1);
2688 w.setHeight(newHeight2);
2689 } else {
2690 w.setY((logicalY * newHeight1) + 1);
2691 w.setHeight(newHeight1);
2692 }
7d922e0d
KL
2693 if ((w.getWidth() != oldWidth)
2694 || (w.getHeight() != oldHeight)
2695 ) {
2696 w.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET,
2697 w.getWidth(), w.getHeight()));
2698 }
2ce6dab2
KL
2699 }
2700 }
2701 }
2702
2703 /**
2704 * Re-layout the open windows as overlapping cascaded windows.
2705 */
2706 private void cascadeWindows() {
2707 synchronized (windows) {
2708 // Don't do anything if we are in the menu
2709 if (activeMenu != null) {
2710 return;
2711 }
2712 int x = 0;
2713 int y = 1;
a69ed767 2714 List<TWindow> sorted = new ArrayList<TWindow>(windows);
2ce6dab2
KL
2715 Collections.sort(sorted);
2716 Collections.reverse(sorted);
2717 for (TWindow window: sorted) {
2718 window.setX(x);
2719 window.setY(y);
2720 x++;
2721 y++;
2722 if (x > getScreen().getWidth()) {
2723 x = 0;
2724 }
2725 if (y >= getScreen().getHeight()) {
2726 y = 1;
2727 }
2728 }
2729 }
2730 }
2731
a7986f7b
KL
2732 /**
2733 * Place a window to minimize its overlap with other windows.
2734 *
2735 * @param window the window to place
2736 */
2737 public final void doSmartPlacement(final TWindow window) {
2738 // This is a pretty dumb algorithm, but seems to work. The hardest
2739 // part is computing these "overlap" values seeking a minimum average
2740 // overlap.
2741 int xMin = 0;
2742 int yMin = desktopTop;
2743 int xMax = getScreen().getWidth() - window.getWidth() + 1;
2744 int yMax = desktopBottom - window.getHeight() + 1;
2745 if (xMax < xMin) {
2746 xMax = xMin;
2747 }
2748 if (yMax < yMin) {
2749 yMax = yMin;
2750 }
2751
2752 if ((xMin == xMax) && (yMin == yMax)) {
2753 // No work to do, bail out.
2754 return;
2755 }
2756
2757 // Compute the overlap matrix without the new window.
2758 int width = getScreen().getWidth();
2759 int height = getScreen().getHeight();
2760 int overlapMatrix[][] = new int[width][height];
2761 for (TWindow w: windows) {
2762 if (window == w) {
2763 continue;
2764 }
2765 for (int x = w.getX(); x < w.getX() + w.getWidth(); x++) {
9ff1c0e3
KL
2766 if (x < 0) {
2767 continue;
2768 }
8c236a98 2769 if (x >= width) {
a7986f7b
KL
2770 continue;
2771 }
2772 for (int y = w.getY(); y < w.getY() + w.getHeight(); y++) {
9ff1c0e3
KL
2773 if (y < 0) {
2774 continue;
2775 }
8c236a98 2776 if (y >= height) {
a7986f7b
KL
2777 continue;
2778 }
2779 overlapMatrix[x][y]++;
2780 }
2781 }
2782 }
2783
2784 long oldOverlapTotal = 0;
2785 long oldOverlapN = 0;
2786 for (int x = 0; x < width; x++) {
2787 for (int y = 0; y < height; y++) {
2788 oldOverlapTotal += overlapMatrix[x][y];
2789 if (overlapMatrix[x][y] > 0) {
2790 oldOverlapN++;
2791 }
2792 }
2793 }
2794
2795
2796 double oldOverlapAvg = (double) oldOverlapTotal / (double) oldOverlapN;
2797 boolean first = true;
2798 int windowX = window.getX();
2799 int windowY = window.getY();
2800
2801 // For each possible (x, y) position for the new window, compute a
2802 // new overlap matrix.
2803 for (int x = xMin; x < xMax; x++) {
2804 for (int y = yMin; y < yMax; y++) {
2805
2806 // Start with the matrix minus this window.
2807 int newMatrix[][] = new int[width][height];
2808 for (int mx = 0; mx < width; mx++) {
2809 for (int my = 0; my < height; my++) {
2810 newMatrix[mx][my] = overlapMatrix[mx][my];
2811 }
2812 }
2813
2814 // Add this window's values to the new overlap matrix.
2815 long newOverlapTotal = 0;
2816 long newOverlapN = 0;
2817 // Start by adding each new cell.
2818 for (int wx = x; wx < x + window.getWidth(); wx++) {
8c236a98 2819 if (wx >= width) {
a7986f7b
KL
2820 continue;
2821 }
2822 for (int wy = y; wy < y + window.getHeight(); wy++) {
8c236a98 2823 if (wy >= height) {
a7986f7b
KL
2824 continue;
2825 }
2826 newMatrix[wx][wy]++;
2827 }
2828 }
2829 // Now figure out the new value for total coverage.
2830 for (int mx = 0; mx < width; mx++) {
2831 for (int my = 0; my < height; my++) {
2832 newOverlapTotal += newMatrix[x][y];
2833 if (newMatrix[mx][my] > 0) {
2834 newOverlapN++;
2835 }
2836 }
2837 }
2838 double newOverlapAvg = (double) newOverlapTotal / (double) newOverlapN;
2839
2840 if (first) {
2841 // First time: just record what we got.
2842 oldOverlapAvg = newOverlapAvg;
2843 first = false;
2844 } else {
2845 // All other times: pick a new best (x, y) and save the
2846 // overlap value.
2847 if (newOverlapAvg < oldOverlapAvg) {
2848 windowX = x;
2849 windowY = y;
2850 oldOverlapAvg = newOverlapAvg;
2851 }
2852 }
2853
2854 } // for (int x = xMin; x < xMax; x++)
2855
2856 } // for (int y = yMin; y < yMax; y++)
2857
2858 // Finally, set the window's new coordinates.
2859 window.setX(windowX);
2860 window.setY(windowY);
2861 }
2862
a69ed767 2863 // ------------------------------------------------------------------------
2ce6dab2
KL
2864 // TMenu management -------------------------------------------------------
2865 // ------------------------------------------------------------------------
2866
fca67db0
KL
2867 /**
2868 * Check if a mouse event would hit either the active menu or any open
2869 * sub-menus.
2870 *
2871 * @param mouse mouse event
2872 * @return true if the mouse would hit the active menu or an open
2873 * sub-menu
2874 */
2875 private boolean mouseOnMenu(final TMouseEvent mouse) {
2876 assert (activeMenu != null);
a69ed767 2877 List<TMenu> menus = new ArrayList<TMenu>(subMenus);
fca67db0
KL
2878 Collections.reverse(menus);
2879 for (TMenu menu: menus) {
2880 if (menu.mouseWouldHit(mouse)) {
2881 return true;
2882 }
2883 }
2884 return activeMenu.mouseWouldHit(mouse);
2885 }
2886
2887 /**
2888 * See if we need to switch window or activate the menu based on
2889 * a mouse click.
2890 *
2891 * @param mouse mouse event
2892 */
2893 private void checkSwitchFocus(final TMouseEvent mouse) {
2894
2895 if ((mouse.getType() == TMouseEvent.Type.MOUSE_DOWN)
2896 && (activeMenu != null)
2897 && (mouse.getAbsoluteY() != 0)
2898 && (!mouseOnMenu(mouse))
2899 ) {
2900 // They clicked outside the active menu, turn it off
2901 activeMenu.setActive(false);
2902 activeMenu = null;
2903 for (TMenu menu: subMenus) {
2904 menu.setActive(false);
2905 }
2906 subMenus.clear();
2907 // Continue checks
2908 }
2909
2910 // See if they hit the menu bar
2911 if ((mouse.getType() == TMouseEvent.Type.MOUSE_DOWN)
7c870d89 2912 && (mouse.isMouse1())
fca67db0 2913 && (!modalWindowActive())
9696a8f6 2914 && (!overrideMenuWindowActive())
fca67db0 2915 && (mouse.getAbsoluteY() == 0)
2bb26984 2916 && (hideMenuBar == false)
fca67db0
KL
2917 ) {
2918
2919 for (TMenu menu: subMenus) {
2920 menu.setActive(false);
2921 }
2922 subMenus.clear();
2923
2924 // They selected the menu, go activate it
2925 for (TMenu menu: menus) {
159f076d
KL
2926 if ((mouse.getAbsoluteX() >= menu.getTitleX())
2927 && (mouse.getAbsoluteX() < menu.getTitleX()
e820d5dd 2928 + StringUtils.width(menu.getTitle()) + 2)
fca67db0
KL
2929 ) {
2930 menu.setActive(true);
2931 activeMenu = menu;
2932 } else {
2933 menu.setActive(false);
2934 }
2935 }
fca67db0
KL
2936 return;
2937 }
2938
2939 // See if they hit the menu bar
2940 if ((mouse.getType() == TMouseEvent.Type.MOUSE_MOTION)
7c870d89 2941 && (mouse.isMouse1())
fca67db0
KL
2942 && (activeMenu != null)
2943 && (mouse.getAbsoluteY() == 0)
2bb26984 2944 && (hideMenuBar == false)
fca67db0
KL
2945 ) {
2946
2947 TMenu oldMenu = activeMenu;
2948 for (TMenu menu: subMenus) {
2949 menu.setActive(false);
2950 }
2951 subMenus.clear();
2952
2953 // See if we should switch menus
2954 for (TMenu menu: menus) {
159f076d
KL
2955 if ((mouse.getAbsoluteX() >= menu.getTitleX())
2956 && (mouse.getAbsoluteX() < menu.getTitleX()
e820d5dd 2957 + StringUtils.width(menu.getTitle()) + 2)
fca67db0
KL
2958 ) {
2959 menu.setActive(true);
2960 activeMenu = menu;
2961 }
2962 }
2963 if (oldMenu != activeMenu) {
2964 // They switched menus
2965 oldMenu.setActive(false);
2966 }
fca67db0
KL
2967 return;
2968 }
2969
72fca17b
KL
2970 // If a menu is still active, don't switch windows
2971 if (activeMenu != null) {
fca67db0
KL
2972 return;
2973 }
2974
72fca17b
KL
2975 // Only switch if there are multiple windows
2976 if (windows.size() < 2) {
fca67db0
KL
2977 return;
2978 }
2979
72fca17b
KL
2980 if (((focusFollowsMouse == true)
2981 && (mouse.getType() == TMouseEvent.Type.MOUSE_MOTION))
e23ea538 2982 || (mouse.getType() == TMouseEvent.Type.MOUSE_DOWN)
72fca17b
KL
2983 ) {
2984 synchronized (windows) {
72fca17b
KL
2985 if (windows.get(0).isModal()) {
2986 // Modal windows don't switch
2987 return;
2988 }
fca67db0 2989
72fca17b
KL
2990 for (TWindow window: windows) {
2991 assert (!window.isModal());
92453213 2992
72fca17b
KL
2993 if (window.isHidden()) {
2994 assert (!window.isActive());
2995 continue;
2996 }
92453213 2997
72fca17b 2998 if (window.mouseWouldHit(mouse)) {
3d3e09e6 2999 activateWindow(window);
bb35d919
KL
3000 return;
3001 }
fca67db0 3002 }
fca67db0 3003 }
72fca17b
KL
3004
3005 // Clicked on the background, nothing to do
3006 return;
fca67db0
KL
3007 }
3008
72fca17b
KL
3009 // Nothing to do: this isn't a mouse up, or focus isn't following
3010 // mouse.
fca67db0
KL
3011 return;
3012 }
3013
3014 /**
3015 * Turn off the menu.
3016 */
928811d8 3017 public final void closeMenu() {
fca67db0
KL
3018 if (activeMenu != null) {
3019 activeMenu.setActive(false);
3020 activeMenu = null;
3021 for (TMenu menu: subMenus) {
3022 menu.setActive(false);
3023 }
3024 subMenus.clear();
3025 }
fca67db0
KL
3026 }
3027
e8a11f98
KL
3028 /**
3029 * Get a (shallow) copy of the menu list.
3030 *
3031 * @return a copy of the menu list
3032 */
3033 public final List<TMenu> getAllMenus() {
a69ed767 3034 return new ArrayList<TMenu>(menus);
e8a11f98
KL
3035 }
3036
3037 /**
3038 * Add a top-level menu to the list.
3039 *
3040 * @param menu the menu to add
3041 * @throws IllegalArgumentException if the menu is already used in
3042 * another TApplication
3043 */
3044 public final void addMenu(final TMenu menu) {
3045 if ((menu.getApplication() != null)
3046 && (menu.getApplication() != this)
3047 ) {
3048 throw new IllegalArgumentException("Menu " + menu + " is already " +
3049 "part of application " + menu.getApplication());
3050 }
3051 closeMenu();
3052 menus.add(menu);
3053 recomputeMenuX();
3054 }
3055
3056 /**
3057 * Remove a top-level menu from the list.
3058 *
3059 * @param menu the menu to remove
3060 * @throws IllegalArgumentException if the menu is already used in
3061 * another TApplication
3062 */
3063 public final void removeMenu(final TMenu menu) {
3064 if ((menu.getApplication() != null)
3065 && (menu.getApplication() != this)
3066 ) {
3067 throw new IllegalArgumentException("Menu " + menu + " is already " +
3068 "part of application " + menu.getApplication());
3069 }
3070 closeMenu();
3071 menus.remove(menu);
3072 recomputeMenuX();
3073 }
3074
fca67db0
KL
3075 /**
3076 * Turn off a sub-menu.
3077 */
928811d8 3078 public final void closeSubMenu() {
fca67db0
KL
3079 assert (activeMenu != null);
3080 TMenu item = subMenus.get(subMenus.size() - 1);
3081 assert (item != null);
3082 item.setActive(false);
3083 subMenus.remove(subMenus.size() - 1);
fca67db0
KL
3084 }
3085
3086 /**
3087 * Switch to the next menu.
3088 *
3089 * @param forward if true, then switch to the next menu in the list,
3090 * otherwise switch to the previous menu in the list
3091 */
928811d8 3092 public final void switchMenu(final boolean forward) {
fca67db0 3093 assert (activeMenu != null);
2bb26984 3094 assert (hideMenuBar == false);
fca67db0
KL
3095
3096 for (TMenu menu: subMenus) {
3097 menu.setActive(false);
3098 }
3099 subMenus.clear();
3100
3101 for (int i = 0; i < menus.size(); i++) {
3102 if (activeMenu == menus.get(i)) {
3103 if (forward) {
3104 if (i < menus.size() - 1) {
3105 i++;
a69ed767
KL
3106 } else {
3107 i = 0;
fca67db0
KL
3108 }
3109 } else {
3110 if (i > 0) {
3111 i--;
a69ed767
KL
3112 } else {
3113 i = menus.size() - 1;
fca67db0
KL
3114 }
3115 }
3116 activeMenu.setActive(false);
3117 activeMenu = menus.get(i);
3118 activeMenu.setActive(true);
fca67db0
KL
3119 return;
3120 }
3121 }
3122 }
3123
928811d8 3124 /**
efb7af1f
KL
3125 * Add a menu item to the global list. If it has a keyboard accelerator,
3126 * that will be added the global hash.
928811d8 3127 *
efb7af1f 3128 * @param item the menu item
928811d8 3129 */
efb7af1f
KL
3130 public final void addMenuItem(final TMenuItem item) {
3131 menuItems.add(item);
3132
3133 TKeypress key = item.getKey();
3134 if (key != null) {
3135 synchronized (accelerators) {
3136 assert (accelerators.get(key) == null);
3137 accelerators.put(key.toLowerCase(), item);
3138 }
3139 }
3140 }
3141
3142 /**
3143 * Disable one menu item.
3144 *
3145 * @param id the menu item ID
3146 */
3147 public final void disableMenuItem(final int id) {
3148 for (TMenuItem item: menuItems) {
3149 if (item.getId() == id) {
3150 item.setEnabled(false);
3151 }
3152 }
3153 }
e826b451 3154
efb7af1f
KL
3155 /**
3156 * Disable the range of menu items with ID's between lower and upper,
3157 * inclusive.
3158 *
3159 * @param lower the lowest menu item ID
3160 * @param upper the highest menu item ID
3161 */
3162 public final void disableMenuItems(final int lower, final int upper) {
3163 for (TMenuItem item: menuItems) {
3164 if ((item.getId() >= lower) && (item.getId() <= upper)) {
3165 item.setEnabled(false);
a69ed767 3166 item.getParent().activate(0);
efb7af1f
KL
3167 }
3168 }
3169 }
3170
3171 /**
3172 * Enable one menu item.
3173 *
3174 * @param id the menu item ID
3175 */
3176 public final void enableMenuItem(final int id) {
3177 for (TMenuItem item: menuItems) {
3178 if (item.getId() == id) {
3179 item.setEnabled(true);
a69ed767 3180 item.getParent().activate(0);
efb7af1f
KL
3181 }
3182 }
3183 }
3184
3185 /**
3186 * Enable the range of menu items with ID's between lower and upper,
3187 * inclusive.
3188 *
3189 * @param lower the lowest menu item ID
3190 * @param upper the highest menu item ID
3191 */
3192 public final void enableMenuItems(final int lower, final int upper) {
3193 for (TMenuItem item: menuItems) {
3194 if ((item.getId() >= lower) && (item.getId() <= upper)) {
3195 item.setEnabled(true);
a69ed767 3196 item.getParent().activate(0);
efb7af1f 3197 }
e826b451 3198 }
928811d8
KL
3199 }
3200
77961919
KL
3201 /**
3202 * Get the menu item associated with this ID.
3203 *
3204 * @param id the menu item ID
3205 * @return the menu item, or null if not found
3206 */
3207 public final TMenuItem getMenuItem(final int id) {
3208 for (TMenuItem item: menuItems) {
3209 if (item.getId() == id) {
3210 return item;
3211 }
3212 }
3213 return null;
3214 }
3215
928811d8
KL
3216 /**
3217 * Recompute menu x positions based on their title length.
3218 */
3219 public final void recomputeMenuX() {
3220 int x = 0;
3221 for (TMenu menu: menus) {
3222 menu.setX(x);
159f076d 3223 menu.setTitleX(x);
e820d5dd 3224 x += StringUtils.width(menu.getTitle()) + 2;
68c5cd6b
KL
3225
3226 // Don't let the menu window exceed the screen width
3227 int rightEdge = menu.getX() + menu.getWidth();
3228 if (rightEdge > getScreen().getWidth()) {
3229 menu.setX(getScreen().getWidth() - menu.getWidth());
3230 }
928811d8
KL
3231 }
3232 }
3233
b2d49e0f
KL
3234 /**
3235 * Post an event to process.
3236 *
3237 * @param event new event to add to the queue
3238 */
3239 public final void postEvent(final TInputEvent event) {
3240 synchronized (this) {
3241 synchronized (fillEventQueue) {
3242 fillEventQueue.add(event);
3243 }
3244 if (debugThreads) {
3245 System.err.println(System.currentTimeMillis() + " " +
3246 Thread.currentThread() + " postEvent() wake up main");
3247 }
3248 this.notify();
3249 }
3250 }
3251
928811d8
KL
3252 /**
3253 * Post an event to process and turn off the menu.
3254 *
3255 * @param event new event to add to the queue
3256 */
5dfd1c11 3257 public final void postMenuEvent(final TInputEvent event) {
be72cb5c
KL
3258 synchronized (this) {
3259 synchronized (fillEventQueue) {
3260 fillEventQueue.add(event);
3261 }
3262 if (debugThreads) {
3263 System.err.println(System.currentTimeMillis() + " " +
3264 Thread.currentThread() + " postMenuEvent() wake up main");
3265 }
3266 closeMenu();
3267 this.notify();
8e688b92 3268 }
928811d8
KL
3269 }
3270
3271 /**
3272 * Add a sub-menu to the list of open sub-menus.
3273 *
3274 * @param menu sub-menu
3275 */
3276 public final void addSubMenu(final TMenu menu) {
3277 subMenus.add(menu);
3278 }
3279
8e688b92
KL
3280 /**
3281 * Convenience function to add a top-level menu.
3282 *
3283 * @param title menu title
3284 * @return the new menu
3285 */
87a17f3c 3286 public final TMenu addMenu(final String title) {
8e688b92
KL
3287 int x = 0;
3288 int y = 0;
3289 TMenu menu = new TMenu(this, x, y, title);
3290 menus.add(menu);
3291 recomputeMenuX();
3292 return menu;
3293 }
3294
e23ea538
KL
3295 /**
3296 * Convenience function to add a default tools (hamburger) menu.
3297 *
3298 * @return the new menu
3299 */
3300 public final TMenu addToolMenu() {
3301 TMenu toolMenu = addMenu(i18n.getString("toolMenuTitle"));
3302 toolMenu.addDefaultItem(TMenu.MID_REPAINT);
3303 toolMenu.addDefaultItem(TMenu.MID_VIEW_IMAGE);
a75902fa 3304 toolMenu.addDefaultItem(TMenu.MID_SCREEN_OPTIONS);
e23ea538
KL
3305 TStatusBar toolStatusBar = toolMenu.newStatusBar(i18n.
3306 getString("toolMenuStatus"));
3307 toolStatusBar.addShortcutKeypress(kbF1, cmHelp, i18n.getString("Help"));
3308 return toolMenu;
3309 }
3310
8e688b92
KL
3311 /**
3312 * Convenience function to add a default "File" menu.
3313 *
3314 * @return the new menu
3315 */
3316 public final TMenu addFileMenu() {
339652cc 3317 TMenu fileMenu = addMenu(i18n.getString("fileMenuTitle"));
8e688b92 3318 fileMenu.addDefaultItem(TMenu.MID_SHELL);
b9724916 3319 fileMenu.addSeparator();
8e688b92 3320 fileMenu.addDefaultItem(TMenu.MID_EXIT);
339652cc
KL
3321 TStatusBar statusBar = fileMenu.newStatusBar(i18n.
3322 getString("fileMenuStatus"));
3323 statusBar.addShortcutKeypress(kbF1, cmHelp, i18n.getString("Help"));
8e688b92
KL
3324 return fileMenu;
3325 }
3326
3327 /**
3328 * Convenience function to add a default "Edit" menu.
3329 *
3330 * @return the new menu
3331 */
3332 public final TMenu addEditMenu() {
339652cc 3333 TMenu editMenu = addMenu(i18n.getString("editMenuTitle"));
51e46b3e
KL
3334 editMenu.addDefaultItem(TMenu.MID_CUT, false);
3335 editMenu.addDefaultItem(TMenu.MID_COPY, false);
3336 editMenu.addDefaultItem(TMenu.MID_PASTE, false);
3337 editMenu.addDefaultItem(TMenu.MID_CLEAR, false);
339652cc
KL
3338 TStatusBar statusBar = editMenu.newStatusBar(i18n.
3339 getString("editMenuStatus"));
3340 statusBar.addShortcutKeypress(kbF1, cmHelp, i18n.getString("Help"));
8e688b92
KL
3341 return editMenu;
3342 }
3343
3344 /**
3345 * Convenience function to add a default "Window" menu.
3346 *
3347 * @return the new menu
3348 */
c6940ed9 3349 public final TMenu addWindowMenu() {
339652cc 3350 TMenu windowMenu = addMenu(i18n.getString("windowMenuTitle"));
8e688b92
KL
3351 windowMenu.addDefaultItem(TMenu.MID_TILE);
3352 windowMenu.addDefaultItem(TMenu.MID_CASCADE);
3353 windowMenu.addDefaultItem(TMenu.MID_CLOSE_ALL);
3354 windowMenu.addSeparator();
3355 windowMenu.addDefaultItem(TMenu.MID_WINDOW_MOVE);
3356 windowMenu.addDefaultItem(TMenu.MID_WINDOW_ZOOM);
3357 windowMenu.addDefaultItem(TMenu.MID_WINDOW_NEXT);
3358 windowMenu.addDefaultItem(TMenu.MID_WINDOW_PREVIOUS);
3359 windowMenu.addDefaultItem(TMenu.MID_WINDOW_CLOSE);
339652cc
KL
3360 TStatusBar statusBar = windowMenu.newStatusBar(i18n.
3361 getString("windowMenuStatus"));
3362 statusBar.addShortcutKeypress(kbF1, cmHelp, i18n.getString("Help"));
8e688b92
KL
3363 return windowMenu;
3364 }
3365
55d2b2c2
KL
3366 /**
3367 * Convenience function to add a default "Help" menu.
3368 *
3369 * @return the new menu
3370 */
3371 public final TMenu addHelpMenu() {
339652cc 3372 TMenu helpMenu = addMenu(i18n.getString("helpMenuTitle"));
55d2b2c2
KL
3373 helpMenu.addDefaultItem(TMenu.MID_HELP_CONTENTS);
3374 helpMenu.addDefaultItem(TMenu.MID_HELP_INDEX);
3375 helpMenu.addDefaultItem(TMenu.MID_HELP_SEARCH);
3376 helpMenu.addDefaultItem(TMenu.MID_HELP_PREVIOUS);
3377 helpMenu.addDefaultItem(TMenu.MID_HELP_HELP);
3378 helpMenu.addDefaultItem(TMenu.MID_HELP_ACTIVE_FILE);
3379 helpMenu.addSeparator();
3380 helpMenu.addDefaultItem(TMenu.MID_ABOUT);
339652cc
KL
3381 TStatusBar statusBar = helpMenu.newStatusBar(i18n.
3382 getString("helpMenuStatus"));
3383 statusBar.addShortcutKeypress(kbF1, cmHelp, i18n.getString("Help"));
55d2b2c2
KL
3384 return helpMenu;
3385 }
3386
1dac6b8d
KL
3387 /**
3388 * Convenience function to add a default "Table" menu.
3389 *
3390 * @return the new menu
3391 */
3392 public final TMenu addTableMenu() {
3393 TMenu tableMenu = addMenu(i18n.getString("tableMenuTitle"));
2b427404
KL
3394 tableMenu.addDefaultItem(TMenu.MID_TABLE_RENAME_COLUMN, false);
3395 tableMenu.addDefaultItem(TMenu.MID_TABLE_RENAME_ROW, false);
3396 tableMenu.addSeparator();
3397
77961919
KL
3398 TSubMenu viewMenu = tableMenu.addSubMenu(i18n.
3399 getString("tableSubMenuView"));
3400 viewMenu.addDefaultItem(TMenu.MID_TABLE_VIEW_ROW_LABELS, false);
3401 viewMenu.addDefaultItem(TMenu.MID_TABLE_VIEW_COLUMN_LABELS, false);
3402 viewMenu.addDefaultItem(TMenu.MID_TABLE_VIEW_HIGHLIGHT_ROW, false);
3403 viewMenu.addDefaultItem(TMenu.MID_TABLE_VIEW_HIGHLIGHT_COLUMN, false);
3404
1dac6b8d
KL
3405 TSubMenu borderMenu = tableMenu.addSubMenu(i18n.
3406 getString("tableSubMenuBorders"));
3407 borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_NONE, false);
3408 borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_ALL, false);
e9bb3c1e
KL
3409 borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_CELL_NONE, false);
3410 borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_CELL_ALL, false);
1dac6b8d
KL
3411 borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_RIGHT, false);
3412 borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_LEFT, false);
3413 borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_TOP, false);
3414 borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_BOTTOM, false);
3415 borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_DOUBLE_BOTTOM, false);
3416 borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_THICK_BOTTOM, false);
3417 TSubMenu deleteMenu = tableMenu.addSubMenu(i18n.
3418 getString("tableSubMenuDelete"));
3419 deleteMenu.addDefaultItem(TMenu.MID_TABLE_DELETE_LEFT, false);
3420 deleteMenu.addDefaultItem(TMenu.MID_TABLE_DELETE_UP, false);
3421 deleteMenu.addDefaultItem(TMenu.MID_TABLE_DELETE_ROW, false);
3422 deleteMenu.addDefaultItem(TMenu.MID_TABLE_DELETE_COLUMN, false);
3423 TSubMenu insertMenu = tableMenu.addSubMenu(i18n.
3424 getString("tableSubMenuInsert"));
3425 insertMenu.addDefaultItem(TMenu.MID_TABLE_INSERT_LEFT, false);
3426 insertMenu.addDefaultItem(TMenu.MID_TABLE_INSERT_RIGHT, false);
3427 insertMenu.addDefaultItem(TMenu.MID_TABLE_INSERT_ABOVE, false);
3428 insertMenu.addDefaultItem(TMenu.MID_TABLE_INSERT_BELOW, false);
3429 TSubMenu columnMenu = tableMenu.addSubMenu(i18n.
3430 getString("tableSubMenuColumn"));
3431 columnMenu.addDefaultItem(TMenu.MID_TABLE_COLUMN_NARROW, false);
3432 columnMenu.addDefaultItem(TMenu.MID_TABLE_COLUMN_WIDEN, false);
3433 TSubMenu fileMenu = tableMenu.addSubMenu(i18n.
3434 getString("tableSubMenuFile"));
f528c340 3435 fileMenu.addDefaultItem(TMenu.MID_TABLE_FILE_OPEN_CSV, false);
1dac6b8d
KL
3436 fileMenu.addDefaultItem(TMenu.MID_TABLE_FILE_SAVE_CSV, false);
3437 fileMenu.addDefaultItem(TMenu.MID_TABLE_FILE_SAVE_TEXT, false);
3438
3439 TStatusBar statusBar = tableMenu.newStatusBar(i18n.
3440 getString("tableMenuStatus"));
3441 statusBar.addShortcutKeypress(kbF1, cmHelp, i18n.getString("Help"));
3442 return tableMenu;
3443 }
3444
2ce6dab2
KL
3445 // ------------------------------------------------------------------------
3446 // TTimer management ------------------------------------------------------
3447 // ------------------------------------------------------------------------
3448
8e688b92 3449 /**
2ce6dab2
KL
3450 * Get the amount of time I can sleep before missing a Timer tick.
3451 *
3452 * @param timeout = initial (maximum) timeout in millis
3453 * @return number of milliseconds between now and the next timer event
8e688b92 3454 */
2ce6dab2
KL
3455 private long getSleepTime(final long timeout) {
3456 Date now = new Date();
3457 long nowTime = now.getTime();
3458 long sleepTime = timeout;
2ce6dab2 3459
be72cb5c
KL
3460 synchronized (timers) {
3461 for (TTimer timer: timers) {
3462 long nextTickTime = timer.getNextTick().getTime();
3463 if (nextTickTime < nowTime) {
3464 return 0;
3465 }
3466
3467 long timeDifference = nextTickTime - nowTime;
3468 if (timeDifference < sleepTime) {
3469 sleepTime = timeDifference;
3470 }
8e688b92
KL
3471 }
3472 }
be72cb5c 3473
2ce6dab2
KL
3474 assert (sleepTime >= 0);
3475 assert (sleepTime <= timeout);
3476 return sleepTime;
8e688b92
KL
3477 }
3478
d502a0e9
KL
3479 /**
3480 * Convenience function to add a timer.
3481 *
3482 * @param duration number of milliseconds to wait between ticks
3483 * @param recurring if true, re-schedule this timer after every tick
3484 * @param action function to call when button is pressed
c6940ed9 3485 * @return the timer
d502a0e9
KL
3486 */
3487 public final TTimer addTimer(final long duration, final boolean recurring,
3488 final TAction action) {
3489
3490 TTimer timer = new TTimer(duration, recurring, action);
3491 synchronized (timers) {
3492 timers.add(timer);
3493 }
3494 return timer;
3495 }
3496
3497 /**
3498 * Convenience function to remove a timer.
3499 *
3500 * @param timer timer to remove
3501 */
3502 public final void removeTimer(final TTimer timer) {
3503 synchronized (timers) {
3504 timers.remove(timer);
3505 }
3506 }
3507
2ce6dab2
KL
3508 // ------------------------------------------------------------------------
3509 // Other TWindow constructors ---------------------------------------------
3510 // ------------------------------------------------------------------------
3511
c6940ed9
KL
3512 /**
3513 * Convenience function to spawn a message box.
3514 *
3515 * @param title window title, will be centered along the top border
3516 * @param caption message to display. Use embedded newlines to get a
3517 * multi-line box.
3518 * @return the new message box
3519 */
3520 public final TMessageBox messageBox(final String title,
3521 final String caption) {
3522
3523 return new TMessageBox(this, title, caption, TMessageBox.Type.OK);
3524 }
3525
3526 /**
3527 * Convenience function to spawn a message box.
3528 *
3529 * @param title window title, will be centered along the top border
3530 * @param caption message to display. Use embedded newlines to get a
3531 * multi-line box.
3532 * @param type one of the TMessageBox.Type constants. Default is
3533 * Type.OK.
3534 * @return the new message box
3535 */
3536 public final TMessageBox messageBox(final String title,
3537 final String caption, final TMessageBox.Type type) {
3538
3539 return new TMessageBox(this, title, caption, type);
3540 }
3541
3542 /**
3543 * Convenience function to spawn an input box.
3544 *
3545 * @param title window title, will be centered along the top border
3546 * @param caption message to display. Use embedded newlines to get a
3547 * multi-line box.
3548 * @return the new input box
3549 */
3550 public final TInputBox inputBox(final String title, final String caption) {
3551
3552 return new TInputBox(this, title, caption);
3553 }
3554
3555 /**
3556 * Convenience function to spawn an input box.
3557 *
3558 * @param title window title, will be centered along the top border
3559 * @param caption message to display. Use embedded newlines to get a
3560 * multi-line box.
3561 * @param text initial text to seed the field with
3562 * @return the new input box
3563 */
3564 public final TInputBox inputBox(final String title, final String caption,
3565 final String text) {
3566
3567 return new TInputBox(this, title, caption, text);
3568 }
1ac2ccb1 3569
72b6bd90
KL
3570 /**
3571 * Convenience function to spawn an input box.
3572 *
3573 * @param title window title, will be centered along the top border
3574 * @param caption message to display. Use embedded newlines to get a
3575 * multi-line box.
3576 * @param text initial text to seed the field with
3577 * @param type one of the Type constants. Default is Type.OK.
3578 * @return the new input box
3579 */
3580 public final TInputBox inputBox(final String title, final String caption,
3581 final String text, final TInputBox.Type type) {
3582
3583 return new TInputBox(this, title, caption, text, type);
3584 }
3585
34a42e78
KL
3586 /**
3587 * Convenience function to open a terminal window.
3588 *
3589 * @param x column relative to parent
3590 * @param y row relative to parent
3591 * @return the terminal new window
3592 */
3593 public final TTerminalWindow openTerminal(final int x, final int y) {
3594 return openTerminal(x, y, TWindow.RESIZABLE);
3595 }
3596
a69ed767
KL
3597 /**
3598 * Convenience function to open a terminal window.
3599 *
3600 * @param x column relative to parent
3601 * @param y row relative to parent
3602 * @param closeOnExit if true, close the window when the command exits
3603 * @return the terminal new window
3604 */
3605 public final TTerminalWindow openTerminal(final int x, final int y,
3606 final boolean closeOnExit) {
3607
3608 return openTerminal(x, y, TWindow.RESIZABLE, closeOnExit);
3609 }
3610
34a42e78
KL
3611 /**
3612 * Convenience function to open a terminal window.
3613 *
3614 * @param x column relative to parent
3615 * @param y row relative to parent
3616 * @param flags mask of CENTERED, MODAL, or RESIZABLE
3617 * @return the terminal new window
3618 */
3619 public final TTerminalWindow openTerminal(final int x, final int y,
3620 final int flags) {
3621
3622 return new TTerminalWindow(this, x, y, flags);
3623 }
3624
a69ed767
KL
3625 /**
3626 * Convenience function to open a terminal window.
3627 *
3628 * @param x column relative to parent
3629 * @param y row relative to parent
3630 * @param flags mask of CENTERED, MODAL, or RESIZABLE
3631 * @param closeOnExit if true, close the window when the command exits
3632 * @return the terminal new window
3633 */
3634 public final TTerminalWindow openTerminal(final int x, final int y,
3635 final int flags, final boolean closeOnExit) {
3636
3637 return new TTerminalWindow(this, x, y, flags, closeOnExit);
3638 }
3639
6f8ff91a
KL
3640 /**
3641 * Convenience function to open a terminal window and execute a custom
3642 * command line inside it.
3643 *
3644 * @param x column relative to parent
3645 * @param y row relative to parent
3646 * @param commandLine the command line to execute
3647 * @return the terminal new window
3648 */
3649 public final TTerminalWindow openTerminal(final int x, final int y,
3650 final String commandLine) {
3651
3652 return openTerminal(x, y, TWindow.RESIZABLE, commandLine);
3653 }
3654
a69ed767
KL
3655 /**
3656 * Convenience function to open a terminal window and execute a custom
3657 * command line inside it.
3658 *
3659 * @param x column relative to parent
3660 * @param y row relative to parent
3661 * @param commandLine the command line to execute
3662 * @param closeOnExit if true, close the window when the command exits
3663 * @return the terminal new window
3664 */
3665 public final TTerminalWindow openTerminal(final int x, final int y,
3666 final String commandLine, final boolean closeOnExit) {
3667
3668 return openTerminal(x, y, TWindow.RESIZABLE, commandLine, closeOnExit);
3669 }
3670
a0d734e6
KL
3671 /**
3672 * Convenience function to open a terminal window and execute a custom
3673 * command line inside it.
3674 *
3675 * @param x column relative to parent
3676 * @param y row relative to parent
3677 * @param flags mask of CENTERED, MODAL, or RESIZABLE
3678 * @param command the command line to execute
3679 * @return the terminal new window
3680 */
3681 public final TTerminalWindow openTerminal(final int x, final int y,
3682 final int flags, final String [] command) {
3683
3684 return new TTerminalWindow(this, x, y, flags, command);
3685 }
3686
a69ed767
KL
3687 /**
3688 * Convenience function to open a terminal window and execute a custom
3689 * command line inside it.
3690 *
3691 * @param x column relative to parent
3692 * @param y row relative to parent
3693 * @param flags mask of CENTERED, MODAL, or RESIZABLE
3694 * @param command the command line to execute
3695 * @param closeOnExit if true, close the window when the command exits
3696 * @return the terminal new window
3697 */
3698 public final TTerminalWindow openTerminal(final int x, final int y,
3699 final int flags, final String [] command, final boolean closeOnExit) {
3700
3701 return new TTerminalWindow(this, x, y, flags, command, closeOnExit);
3702 }
3703
6f8ff91a
KL
3704 /**
3705 * Convenience function to open a terminal window and execute a custom
3706 * command line inside it.
3707 *
3708 * @param x column relative to parent
3709 * @param y row relative to parent
3710 * @param flags mask of CENTERED, MODAL, or RESIZABLE
3711 * @param commandLine the command line to execute
3712 * @return the terminal new window
3713 */
3714 public final TTerminalWindow openTerminal(final int x, final int y,
3715 final int flags, final String commandLine) {
3716
00691e80 3717 return new TTerminalWindow(this, x, y, flags, commandLine.split("\\s+"));
6f8ff91a
KL
3718 }
3719
a69ed767
KL
3720 /**
3721 * Convenience function to open a terminal window and execute a custom
3722 * command line inside it.
3723 *
3724 * @param x column relative to parent
3725 * @param y row relative to parent
3726 * @param flags mask of CENTERED, MODAL, or RESIZABLE
3727 * @param commandLine the command line to execute
3728 * @param closeOnExit if true, close the window when the command exits
3729 * @return the terminal new window
3730 */
3731 public final TTerminalWindow openTerminal(final int x, final int y,
3732 final int flags, final String commandLine, final boolean closeOnExit) {
3733
00691e80 3734 return new TTerminalWindow(this, x, y, flags, commandLine.split("\\s+"),
a69ed767
KL
3735 closeOnExit);
3736 }
3737
0d47c546
KL
3738 /**
3739 * Convenience function to spawn an file open box.
3740 *
3741 * @param path path of selected file
3742 * @return the result of the new file open box
329fd62e 3743 * @throws IOException if java.io operation throws
0d47c546
KL
3744 */
3745 public final String fileOpenBox(final String path) throws IOException {
3746
3747 TFileOpenBox box = new TFileOpenBox(this, path, TFileOpenBox.Type.OPEN);
3748 return box.getFilename();
3749 }
3750
3751 /**
3752 * Convenience function to spawn an file open box.
3753 *
3754 * @param path path of selected file
3755 * @param type one of the Type constants
3756 * @return the result of the new file open box
329fd62e 3757 * @throws IOException if java.io operation throws
0d47c546
KL
3758 */
3759 public final String fileOpenBox(final String path,
3760 final TFileOpenBox.Type type) throws IOException {
3761
3762 TFileOpenBox box = new TFileOpenBox(this, path, type);
3763 return box.getFilename();
3764 }
3765
a69ed767
KL
3766 /**
3767 * Convenience function to spawn a file open box.
3768 *
3769 * @param path path of selected file
3770 * @param type one of the Type constants
3771 * @param filter a string that files must match to be displayed
3772 * @return the result of the new file open box
3773 * @throws IOException of a java.io operation throws
3774 */
3775 public final String fileOpenBox(final String path,
3776 final TFileOpenBox.Type type, final String filter) throws IOException {
3777
3778 ArrayList<String> filters = new ArrayList<String>();
3779 filters.add(filter);
3780
3781 TFileOpenBox box = new TFileOpenBox(this, path, type, filters);
3782 return box.getFilename();
3783 }
3784
3785 /**
3786 * Convenience function to spawn a file open box.
3787 *
3788 * @param path path of selected file
3789 * @param type one of the Type constants
3790 * @param filters a list of strings that files must match to be displayed
3791 * @return the result of the new file open box
3792 * @throws IOException of a java.io operation throws
3793 */
3794 public final String fileOpenBox(final String path,
3795 final TFileOpenBox.Type type,
3796 final List<String> filters) throws IOException {
3797
3798 TFileOpenBox box = new TFileOpenBox(this, path, type, filters);
3799 return box.getFilename();
3800 }
3801
92453213
KL
3802 /**
3803 * Convenience function to create a new window and make it active.
3804 * Window will be located at (0, 0).
3805 *
3806 * @param title window title, will be centered along the top border
3807 * @param width width of window
3808 * @param height height of window
43ad7b6c 3809 * @return the new window
92453213
KL
3810 */
3811 public final TWindow addWindow(final String title, final int width,
3812 final int height) {
3813
3814 TWindow window = new TWindow(this, title, 0, 0, width, height);
3815 return window;
3816 }
1978ad50 3817
92453213
KL
3818 /**
3819 * Convenience function to create a new window and make it active.
3820 * Window will be located at (0, 0).
3821 *
3822 * @param title window title, will be centered along the top border
3823 * @param width width of window
3824 * @param height height of window
3825 * @param flags bitmask of RESIZABLE, CENTERED, or MODAL
43ad7b6c 3826 * @return the new window
92453213
KL
3827 */
3828 public final TWindow addWindow(final String title,
3829 final int width, final int height, final int flags) {
3830
3831 TWindow window = new TWindow(this, title, 0, 0, width, height, flags);
3832 return window;
3833 }
3834
3835 /**
3836 * Convenience function to create a new window and make it active.
3837 *
3838 * @param title window title, will be centered along the top border
3839 * @param x column relative to parent
3840 * @param y row relative to parent
3841 * @param width width of window
3842 * @param height height of window
43ad7b6c 3843 * @return the new window
92453213
KL
3844 */
3845 public final TWindow addWindow(final String title,
3846 final int x, final int y, final int width, final int height) {
3847
3848 TWindow window = new TWindow(this, title, x, y, width, height);
3849 return window;
3850 }
3851
3852 /**
3853 * Convenience function to create a new window and make it active.
3854 *
92453213
KL
3855 * @param title window title, will be centered along the top border
3856 * @param x column relative to parent
3857 * @param y row relative to parent
3858 * @param width width of window
3859 * @param height height of window
3860 * @param flags mask of RESIZABLE, CENTERED, or MODAL
43ad7b6c 3861 * @return the new window
92453213
KL
3862 */
3863 public final TWindow addWindow(final String title,
3864 final int x, final int y, final int width, final int height,
3865 final int flags) {
3866
3867 TWindow window = new TWindow(this, title, x, y, width, height, flags);
3868 return window;
3869 }
3870
7d4115a5 3871}