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