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