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