TTerminalWindow working
[fanfix.git] / src / jexer / TApplication.java
CommitLineData
7d4115a5 1/**
7b5261bc 2 * Jexer - Java Text User Interface
7d4115a5
KL
3 *
4 * License: LGPLv3 or later
5 *
7b5261bc
KL
6 * This module is licensed under the GNU Lesser General Public License
7 * Version 3. Please see the file "COPYING" in this directory for more
8 * information about the GNU Lesser General Public License Version 3.
7d4115a5
KL
9 *
10 * Copyright (C) 2015 Kevin Lamonte
11 *
12 * This program is free software; you can redistribute it and/or
13 * modify it under the terms of the GNU Lesser General Public License
14 * as published by the Free Software Foundation; either version 3 of
15 * the License, or (at your option) any later version.
16 *
17 * This program is distributed in the hope that it will be useful, but
18 * WITHOUT ANY WARRANTY; without even the implied warranty of
19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
20 * General Public License for more details.
21 *
22 * You should have received a copy of the GNU Lesser General Public
23 * License along with this program; if not, see
24 * http://www.gnu.org/licenses/, or write to the Free Software
25 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
26 * 02110-1301 USA
7b5261bc
KL
27 *
28 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
29 * @version 1
7d4115a5
KL
30 */
31package jexer;
32
4328bb42
KL
33import java.io.InputStream;
34import java.io.OutputStream;
35import java.io.UnsupportedEncodingException;
a06459bd 36import java.util.Collections;
d502a0e9 37import java.util.Date;
e826b451 38import java.util.HashMap;
c6940ed9 39import java.util.ArrayList;
4328bb42
KL
40import java.util.LinkedList;
41import java.util.List;
e826b451 42import java.util.Map;
4328bb42
KL
43
44import jexer.bits.CellAttributes;
45import jexer.bits.ColorTheme;
46import jexer.bits.GraphicsChars;
47import jexer.event.TCommandEvent;
48import jexer.event.TInputEvent;
49import jexer.event.TKeypressEvent;
fca67db0 50import jexer.event.TMenuEvent;
4328bb42
KL
51import jexer.event.TMouseEvent;
52import jexer.event.TResizeEvent;
53import jexer.backend.Backend;
1ac2ccb1 54import jexer.backend.AWTBackend;
4328bb42 55import jexer.backend.ECMA48Backend;
48e27807 56import jexer.io.Screen;
928811d8
KL
57import jexer.menu.TMenu;
58import jexer.menu.TMenuItem;
4328bb42
KL
59import static jexer.TCommand.*;
60import static jexer.TKeypress.*;
61
7d4115a5
KL
62/**
63 * TApplication sets up a full Text User Interface application.
64 */
65public class TApplication {
66
99144c71
KL
67 /**
68 * If true, emit thread stuff to System.err.
69 */
70 private static final boolean debugThreads = false;
71
c6940ed9
KL
72 /**
73 * WidgetEventHandler is the main event consumer loop. There are at most
74 * two such threads in existence: the primary for normal case and a
75 * secondary that is used for TMessageBox, TInputBox, and similar.
76 */
77 private class WidgetEventHandler implements Runnable {
78 /**
79 * The main application.
80 */
81 private TApplication application;
82
83 /**
84 * Whether or not this WidgetEventHandler is the primary or secondary
85 * thread.
86 */
87 private boolean primary = true;
88
89 /**
90 * Public constructor.
91 *
92 * @param application the main application
93 * @param primary if true, this is the primary event handler thread
94 */
95 public WidgetEventHandler(final TApplication application,
96 final boolean primary) {
97
98 this.application = application;
99 this.primary = primary;
100 }
101
102 /**
103 * The consumer loop.
104 */
105 public void run() {
106
107 // Loop forever
108 while (!application.quit) {
109
110 // Wait until application notifies me
111 while (!application.quit) {
112 try {
113 synchronized (application.drainEventQueue) {
114 if (application.drainEventQueue.size() > 0) {
115 break;
116 }
117 }
118 synchronized (application) {
119 application.wait();
120 if ((!primary)
121 && (application.secondaryEventReceiver == null)
122 ) {
123 // Secondary thread, time to exit
124 return;
125 }
126 break;
127 }
128 } catch (InterruptedException e) {
129 // SQUASH
130 }
131 }
132
133 // Pull all events off the queue
134 for (;;) {
135 TInputEvent event = null;
136 synchronized (application.drainEventQueue) {
137 if (application.drainEventQueue.size() == 0) {
138 break;
139 }
140 event = application.drainEventQueue.remove(0);
141 }
99144c71
KL
142 // Wait for drawAll() or doIdle() to be done, then handle
143 // the event.
144 boolean oldLock = lockHandleEvent();
145 assert (oldLock == false);
c6940ed9
KL
146 if (primary) {
147 primaryHandleEvent(event);
148 } else {
149 secondaryHandleEvent(event);
150 }
151 if ((!primary)
152 && (application.secondaryEventReceiver == null)
153 ) {
99144c71
KL
154 // Secondary thread, time to exit.
155
156 // DO NOT UNLOCK. Primary thread just came back from
157 // primaryHandleEvent() and will unlock in the else
158 // block below.
c6940ed9 159 return;
99144c71
KL
160 } else {
161 // Unlock. Either I am primary thread, or I am
162 // secondary thread and still running.
163 oldLock = unlockHandleEvent();
164 assert (oldLock == true);
c6940ed9
KL
165 }
166 }
167 } // while (true) (main runnable loop)
168 }
169 }
170
171 /**
172 * The primary event handler thread.
173 */
174 private WidgetEventHandler primaryEventHandler;
175
176 /**
177 * The secondary event handler thread.
178 */
179 private WidgetEventHandler secondaryEventHandler;
180
181 /**
182 * The widget receiving events from the secondary event handler thread.
183 */
184 private TWidget secondaryEventReceiver;
185
99144c71
KL
186 /**
187 * Spinlock for the primary and secondary event handlers.
188 * WidgetEventHandler.run() is responsible for setting this value.
189 */
190 private volatile boolean insideHandleEvent = false;
191
192 /**
193 * Set the insideHandleEvent flag to true. lockoutEventHandlers() will
194 * spin indefinitely until unlockHandleEvent() is called.
195 *
196 * @return the old value of insideHandleEvent
197 */
198 private boolean lockHandleEvent() {
199 if (debugThreads) {
200 System.err.printf(" >> lockHandleEvent(): oldValue %s",
201 insideHandleEvent);
202 }
203 boolean oldValue = true;
204
205 synchronized (this) {
206 // Wait for TApplication.run() to finish using the global state
207 // before allowing further event processing.
208 while (lockoutHandleEvent == true);
209
210 oldValue = insideHandleEvent;
211 insideHandleEvent = true;
212 }
213
214 if (debugThreads) {
215 System.err.printf(" ***\n");
216 }
217 return oldValue;
218 }
219
220 /**
221 * Set the insideHandleEvent flag to false. lockoutEventHandlers() will
222 * spin indefinitely until unlockHandleEvent() is called.
223 *
224 * @return the old value of insideHandleEvent
225 */
226 private boolean unlockHandleEvent() {
227 if (debugThreads) {
228 System.err.printf(" << unlockHandleEvent(): oldValue %s\n",
229 insideHandleEvent);
230 }
231 synchronized (this) {
232 boolean oldValue = insideHandleEvent;
233 insideHandleEvent = false;
234 return oldValue;
235 }
236 }
237
238 /**
239 * Spinlock for the primary and secondary event handlers. When true, the
240 * event handlers will spinlock wait before calling handleEvent().
241 */
242 private volatile boolean lockoutHandleEvent = false;
243
244 /**
245 * TApplication.run() needs to be able rely on the global data structures
246 * being intact when calling doIdle() and drawAll(). Tell the event
247 * handlers to wait for an unlock before handling their events.
248 */
249 private void stopEventHandlers() {
250 if (debugThreads) {
251 System.err.printf(">> stopEventHandlers()");
252 }
253
254 lockoutHandleEvent = true;
255 // Wait for the last event to finish processing before returning
256 // control to TApplication.run().
257 while (insideHandleEvent == true);
258
259 if (debugThreads) {
260 System.err.printf(" XXX\n");
261 }
262 }
263
264 /**
265 * TApplication.run() needs to be able rely on the global data structures
266 * being intact when calling doIdle() and drawAll(). Tell the event
267 * handlers that it is now OK to handle their events.
268 */
269 private void startEventHandlers() {
270 if (debugThreads) {
271 System.err.printf("<< startEventHandlers()\n");
272 }
273 lockoutHandleEvent = false;
274 }
275
7d4115a5 276 /**
4328bb42
KL
277 * Access to the physical screen, keyboard, and mouse.
278 */
7b5261bc 279 private Backend backend;
4328bb42 280
48e27807
KL
281 /**
282 * Get the Screen.
283 *
284 * @return the Screen
285 */
286 public final Screen getScreen() {
287 return backend.getScreen();
288 }
289
4328bb42 290 /**
7b5261bc 291 * Actual mouse coordinate X.
4328bb42
KL
292 */
293 private int mouseX;
294
295 /**
7b5261bc 296 * Actual mouse coordinate Y.
4328bb42
KL
297 */
298 private int mouseY;
299
300 /**
8e688b92 301 * Event queue that is filled by run().
4328bb42 302 */
8e688b92
KL
303 private List<TInputEvent> fillEventQueue;
304
305 /**
306 * Event queue that will be drained by either primary or secondary
307 * Thread.
308 */
309 private List<TInputEvent> drainEventQueue;
4328bb42 310
fca67db0
KL
311 /**
312 * Top-level menus in this application.
313 */
314 private List<TMenu> menus;
315
316 /**
317 * Stack of activated sub-menus in this application.
318 */
319 private List<TMenu> subMenus;
320
321 /**
322 * The currently acive menu.
323 */
324 private TMenu activeMenu = null;
325
e826b451
KL
326 /**
327 * Active keyboard accelerators.
328 */
329 private Map<TKeypress, TMenuItem> accelerators;
330
4328bb42
KL
331 /**
332 * Windows and widgets pull colors from this ColorTheme.
333 */
7b5261bc
KL
334 private ColorTheme theme;
335
336 /**
337 * Get the color theme.
338 *
339 * @return the theme
340 */
341 public final ColorTheme getTheme() {
342 return theme;
343 }
4328bb42 344
a06459bd
KL
345 /**
346 * The top-level windows (but not menus).
347 */
fca67db0 348 private List<TWindow> windows;
a06459bd 349
d502a0e9
KL
350 /**
351 * Timers that are being ticked.
352 */
353 private List<TTimer> timers;
354
4328bb42
KL
355 /**
356 * When true, exit the application.
357 */
48e27807 358 private boolean quit = false;
4328bb42
KL
359
360 /**
361 * When true, repaint the entire screen.
362 */
48e27807
KL
363 private boolean repaint = true;
364
365 /**
366 * Request full repaint on next screen refresh.
367 */
fca67db0 368 public final void setRepaint() {
48e27807
KL
369 repaint = true;
370 }
4328bb42
KL
371
372 /**
373 * When true, just flush updates from the screen.
7d4115a5 374 */
48e27807 375 private boolean flush = false;
4328bb42
KL
376
377 /**
7b5261bc
KL
378 * Y coordinate of the top edge of the desktop. For now this is a
379 * constant. Someday it would be nice to have a multi-line menu or
380 * toolbars.
4328bb42 381 */
48e27807
KL
382 private static final int desktopTop = 1;
383
384 /**
385 * Get Y coordinate of the top edge of the desktop.
386 *
387 * @return Y coordinate of the top edge of the desktop
388 */
389 public final int getDesktopTop() {
390 return desktopTop;
391 }
4328bb42
KL
392
393 /**
394 * Y coordinate of the bottom edge of the desktop.
395 */
48e27807
KL
396 private int desktopBottom;
397
398 /**
399 * Get Y coordinate of the bottom edge of the desktop.
400 *
401 * @return Y coordinate of the bottom edge of the desktop
402 */
403 public final int getDesktopBottom() {
404 return desktopBottom;
405 }
4328bb42
KL
406
407 /**
408 * Public constructor.
409 *
410 * @param input an InputStream connected to the remote user, or null for
411 * System.in. If System.in is used, then on non-Windows systems it will
412 * be put in raw mode; shutdown() will (blindly!) put System.in in cooked
413 * mode. input is always converted to a Reader with UTF-8 encoding.
414 * @param output an OutputStream connected to the remote user, or null
415 * for System.out. output is always converted to a Writer with UTF-8
416 * encoding.
7b5261bc
KL
417 * @throws UnsupportedEncodingException if an exception is thrown when
418 * creating the InputStreamReader
4328bb42 419 */
7b5261bc
KL
420 public TApplication(final InputStream input,
421 final OutputStream output) throws UnsupportedEncodingException {
4328bb42 422
30bd4abd
KL
423 // AWT is the default backend on Windows unless explicitly overridden
424 // by jexer.AWT.
425 boolean useAWT = false;
426 if (System.getProperty("os.name").startsWith("Windows")) {
427 useAWT = true;
428 }
429 if (System.getProperty("jexer.AWT") != null) {
430 if (System.getProperty("jexer.AWT", "false").equals("true")) {
431 useAWT = true;
432 } else {
433 useAWT = false;
434 }
435 }
436
437
438 if (useAWT) {
1ac2ccb1
KL
439 backend = new AWTBackend();
440 } else {
441 backend = new ECMA48Backend(input, output);
442 }
8e688b92
KL
443 theme = new ColorTheme();
444 desktopBottom = getScreen().getHeight() - 1;
c6940ed9
KL
445 fillEventQueue = new ArrayList<TInputEvent>();
446 drainEventQueue = new ArrayList<TInputEvent>();
8e688b92
KL
447 windows = new LinkedList<TWindow>();
448 menus = new LinkedList<TMenu>();
449 subMenus = new LinkedList<TMenu>();
d502a0e9 450 timers = new LinkedList<TTimer>();
e826b451 451 accelerators = new HashMap<TKeypress, TMenuItem>();
c6940ed9
KL
452
453 // Setup the main consumer thread
454 primaryEventHandler = new WidgetEventHandler(this, true);
455 (new Thread(primaryEventHandler)).start();
4328bb42
KL
456 }
457
458 /**
459 * Invert the cell at the mouse pointer position.
460 */
461 private void drawMouse() {
a06459bd 462 CellAttributes attr = getScreen().getAttrXY(mouseX, mouseY);
7b5261bc
KL
463 attr.setForeColor(attr.getForeColor().invert());
464 attr.setBackColor(attr.getBackColor().invert());
a06459bd 465 getScreen().putAttrXY(mouseX, mouseY, attr, false);
7b5261bc
KL
466 flush = true;
467
a06459bd 468 if (windows.size() == 0) {
7b5261bc
KL
469 repaint = true;
470 }
4328bb42
KL
471 }
472
473 /**
474 * Draw everything.
475 */
7b5261bc 476 public final void drawAll() {
99144c71
KL
477 if (debugThreads) {
478 System.err.printf("drawAll() enter\n");
479 }
480
7b5261bc
KL
481 if ((flush) && (!repaint)) {
482 backend.flushScreen();
483 flush = false;
484 return;
485 }
486
487 if (!repaint) {
488 return;
489 }
490
99144c71
KL
491 if (debugThreads) {
492 System.err.printf("drawAll() REDRAW\n");
493 }
494
7b5261bc
KL
495 // If true, the cursor is not visible
496 boolean cursor = false;
497
498 // Start with a clean screen
a06459bd 499 getScreen().clear();
7b5261bc
KL
500
501 // Draw the background
502 CellAttributes background = theme.getColor("tapplication.background");
a06459bd 503 getScreen().putAll(GraphicsChars.HATCH, background);
7b5261bc 504
7b5261bc 505 // Draw each window in reverse Z order
a06459bd
KL
506 List<TWindow> sorted = new LinkedList<TWindow>(windows);
507 Collections.sort(sorted);
508 Collections.reverse(sorted);
509 for (TWindow window: sorted) {
510 window.drawChildren();
7b5261bc
KL
511 }
512
513 // Draw the blank menubar line - reset the screen clipping first so
514 // it won't trim it out.
a06459bd
KL
515 getScreen().resetClipping();
516 getScreen().hLineXY(0, 0, getScreen().getWidth(), ' ',
7b5261bc
KL
517 theme.getColor("tmenu"));
518 // Now draw the menus.
519 int x = 1;
fca67db0 520 for (TMenu menu: menus) {
7b5261bc
KL
521 CellAttributes menuColor;
522 CellAttributes menuMnemonicColor;
fca67db0 523 if (menu.getActive()) {
7b5261bc
KL
524 menuColor = theme.getColor("tmenu.highlighted");
525 menuMnemonicColor = theme.getColor("tmenu.mnemonic.highlighted");
526 } else {
527 menuColor = theme.getColor("tmenu");
528 menuMnemonicColor = theme.getColor("tmenu.mnemonic");
529 }
530 // Draw the menu title
fca67db0 531 getScreen().hLineXY(x, 0, menu.getTitle().length() + 2, ' ',
7b5261bc 532 menuColor);
fca67db0 533 getScreen().putStrXY(x + 1, 0, menu.getTitle(), menuColor);
7b5261bc 534 // Draw the highlight character
fca67db0
KL
535 getScreen().putCharXY(x + 1 + menu.getMnemonic().getShortcutIdx(),
536 0, menu.getMnemonic().getShortcut(), menuMnemonicColor);
7b5261bc 537
fca67db0 538 if (menu.getActive()) {
a06459bd 539 menu.drawChildren();
7b5261bc 540 // Reset the screen clipping so we can draw the next title.
a06459bd 541 getScreen().resetClipping();
7b5261bc 542 }
fca67db0 543 x += menu.getTitle().length() + 2;
7b5261bc
KL
544 }
545
a06459bd 546 for (TMenu menu: subMenus) {
7b5261bc 547 // Reset the screen clipping so we can draw the next sub-menu.
a06459bd
KL
548 getScreen().resetClipping();
549 menu.drawChildren();
7b5261bc 550 }
7b5261bc
KL
551
552 // Draw the mouse pointer
553 drawMouse();
554
7b5261bc
KL
555 // Place the cursor if it is visible
556 TWidget activeWidget = null;
a06459bd
KL
557 if (sorted.size() > 0) {
558 activeWidget = sorted.get(sorted.size() - 1).getActiveChild();
559 if (activeWidget.visibleCursor()) {
560 getScreen().putCursor(true, activeWidget.getCursorAbsoluteX(),
7b5261bc
KL
561 activeWidget.getCursorAbsoluteY());
562 cursor = true;
563 }
564 }
565
566 // Kill the cursor
fca67db0 567 if (!cursor) {
a06459bd 568 getScreen().hideCursor();
7b5261bc 569 }
7b5261bc
KL
570
571 // Flush the screen contents
572 backend.flushScreen();
573
574 repaint = false;
575 flush = false;
4328bb42
KL
576 }
577
578 /**
7b5261bc 579 * Run this application until it exits.
4328bb42
KL
580 */
581 public final void run() {
7b5261bc
KL
582 while (!quit) {
583 // Timeout is in milliseconds, so default timeout after 1 second
584 // of inactivity.
585 int timeout = getSleepTime(1000);
586
8e688b92
KL
587 // See if there are any definitely events waiting to be processed
588 // or a screen redraw to do. If so, do not wait if there is no
589 // I/O coming in.
590 synchronized (drainEventQueue) {
591 if (drainEventQueue.size() > 0) {
592 timeout = 0;
593 }
594 }
595 synchronized (fillEventQueue) {
596 if (fillEventQueue.size() > 0) {
597 timeout = 0;
598 }
7b5261bc
KL
599 }
600
8e688b92
KL
601 // Pull any pending I/O events
602 backend.getEvents(fillEventQueue, timeout);
603
604 // Dispatch each event to the appropriate handler, one at a time.
605 for (;;) {
606 TInputEvent event = null;
607 synchronized (fillEventQueue) {
608 if (fillEventQueue.size() == 0) {
609 break;
610 }
611 event = fillEventQueue.remove(0);
612 }
613 metaHandleEvent(event);
614 }
7b5261bc 615
99144c71
KL
616 // Prevent stepping on the primary or secondary event handler.
617 stopEventHandlers();
618
7b5261bc
KL
619 // Process timers and call doIdle()'s
620 doIdle();
621
622 // Update the screen
87a17f3c
KL
623 synchronized (getScreen()) {
624 drawAll();
625 }
99144c71
KL
626
627 // Let the event handlers run again.
628 startEventHandlers();
7b5261bc
KL
629 }
630
c6940ed9
KL
631 // Shutdown the consumer threads
632 synchronized (this) {
633 this.notifyAll();
7b5261bc
KL
634 }
635
7b5261bc 636 backend.shutdown();
4328bb42
KL
637 }
638
639 /**
640 * Peek at certain application-level events, add to eventQueue, and wake
8e688b92 641 * up the consuming Thread.
4328bb42 642 *
8e688b92 643 * @param event the input event to consume
4328bb42 644 */
8e688b92 645 private void metaHandleEvent(final TInputEvent event) {
7b5261bc 646
8e688b92 647 /*
e826b451
KL
648 System.err.printf(String.format("metaHandleEvents event: %s\n",
649 event)); System.err.flush();
8e688b92 650 */
7b5261bc 651
8e688b92
KL
652 if (quit) {
653 // Do no more processing if the application is already trying
654 // to exit.
655 return;
656 }
7b5261bc 657
8e688b92 658 // Special application-wide events -------------------------------
7b5261bc 659
8e688b92
KL
660 // Abort everything
661 if (event instanceof TCommandEvent) {
662 TCommandEvent command = (TCommandEvent) event;
663 if (command.getCmd().equals(cmAbort)) {
664 quit = true;
665 return;
7b5261bc 666 }
8e688b92 667 }
7b5261bc 668
8e688b92
KL
669 // Screen resize
670 if (event instanceof TResizeEvent) {
671 TResizeEvent resize = (TResizeEvent) event;
672 getScreen().setDimensions(resize.getWidth(),
673 resize.getHeight());
674 desktopBottom = getScreen().getHeight() - 1;
675 repaint = true;
676 mouseX = 0;
677 mouseY = 0;
678 return;
679 }
7b5261bc 680
8e688b92
KL
681 // Peek at the mouse position
682 if (event instanceof TMouseEvent) {
683 TMouseEvent mouse = (TMouseEvent) event;
684 if ((mouseX != mouse.getX()) || (mouseY != mouse.getY())) {
685 mouseX = mouse.getX();
686 mouseY = mouse.getY();
687 drawMouse();
7b5261bc 688 }
8e688b92 689 }
7b5261bc 690
8e688b92 691 // Put into the main queue
c6940ed9
KL
692 synchronized (drainEventQueue) {
693 drainEventQueue.add(event);
694 }
4328bb42 695
c6940ed9
KL
696 // Wake all threads: primary thread will either be consuming events
697 // again or waiting in yield(), and secondary thread will either not
698 // exist or consuming events.
699 synchronized (this) {
700 this.notifyAll();
701 }
4328bb42
KL
702 }
703
a06459bd
KL
704 /**
705 * Dispatch one event to the appropriate widget or application-level
fca67db0
KL
706 * event handler. This is the primary event handler, it has the normal
707 * application-wide event handling.
a06459bd
KL
708 *
709 * @param event the input event to consume
fca67db0 710 * @see #secondaryHandleEvent(TInputEvent event)
a06459bd 711 */
fca67db0
KL
712 private void primaryHandleEvent(final TInputEvent event) {
713
714 // System.err.printf("Handle event: %s\n", event);
715
716 // Special application-wide events -----------------------------------
717
718 // Peek at the mouse position
719 if (event instanceof TMouseEvent) {
720 // See if we need to switch focus to another window or the menu
721 checkSwitchFocus((TMouseEvent) event);
722 }
723
724 // Handle menu events
725 if ((activeMenu != null) && !(event instanceof TCommandEvent)) {
726 TMenu menu = activeMenu;
727
728 if (event instanceof TMouseEvent) {
729 TMouseEvent mouse = (TMouseEvent) event;
730
731 while (subMenus.size() > 0) {
732 TMenu subMenu = subMenus.get(subMenus.size() - 1);
733 if (subMenu.mouseWouldHit(mouse)) {
734 break;
735 }
736 if ((mouse.getType() == TMouseEvent.Type.MOUSE_MOTION)
737 && (!mouse.getMouse1())
738 && (!mouse.getMouse2())
739 && (!mouse.getMouse3())
740 && (!mouse.getMouseWheelUp())
741 && (!mouse.getMouseWheelDown())
742 ) {
743 break;
744 }
745 // We navigated away from a sub-menu, so close it
746 closeSubMenu();
747 }
748
749 // Convert the mouse relative x/y to menu coordinates
750 assert (mouse.getX() == mouse.getAbsoluteX());
751 assert (mouse.getY() == mouse.getAbsoluteY());
752 if (subMenus.size() > 0) {
753 menu = subMenus.get(subMenus.size() - 1);
754 }
755 mouse.setX(mouse.getX() - menu.getX());
756 mouse.setY(mouse.getY() - menu.getY());
757 }
758 menu.handleEvent(event);
759 return;
760 }
a06459bd 761
fca67db0
KL
762 if (event instanceof TKeypressEvent) {
763 TKeypressEvent keypress = (TKeypressEvent) event;
e826b451 764
fca67db0
KL
765 // See if this key matches an accelerator, and if so dispatch the
766 // menu event.
767 TKeypress keypressLowercase = keypress.getKey().toLowerCase();
e826b451
KL
768 TMenuItem item = null;
769 synchronized (accelerators) {
770 item = accelerators.get(keypressLowercase);
771 }
fca67db0
KL
772 if (item != null) {
773 // Let the menu item dispatch
774 item.dispatch();
775 return;
776 } else {
777 // Handle the keypress
778 if (onKeypress(keypress)) {
779 return;
780 }
781 }
782 }
a06459bd 783
fca67db0
KL
784 if (event instanceof TCommandEvent) {
785 if (onCommand((TCommandEvent) event)) {
786 return;
787 }
788 }
789
790 if (event instanceof TMenuEvent) {
791 if (onMenu((TMenuEvent) event)) {
792 return;
793 }
794 }
795
796 // Dispatch events to the active window -------------------------------
797 for (TWindow window: windows) {
798 if (window.getActive()) {
a06459bd
KL
799 if (event instanceof TMouseEvent) {
800 TMouseEvent mouse = (TMouseEvent) event;
fca67db0
KL
801 // Convert the mouse relative x/y to window coordinates
802 assert (mouse.getX() == mouse.getAbsoluteX());
803 assert (mouse.getY() == mouse.getAbsoluteY());
804 mouse.setX(mouse.getX() - window.getX());
805 mouse.setY(mouse.getY() - window.getY());
806 }
807 // System.err("TApplication dispatch event: %s\n", event);
808 window.handleEvent(event);
809 break;
810 }
811 }
812 }
813 /**
814 * Dispatch one event to the appropriate widget or application-level
815 * event handler. This is the secondary event handler used by certain
816 * special dialogs (currently TMessageBox and TFileOpenBox).
817 *
818 * @param event the input event to consume
819 * @see #primaryHandleEvent(TInputEvent event)
820 */
821 private void secondaryHandleEvent(final TInputEvent event) {
c6940ed9
KL
822 secondaryEventReceiver.handleEvent(event);
823 }
824
825 /**
826 * Enable a widget to override the primary event thread.
827 *
828 * @param widget widget that will receive events
829 */
830 public final void enableSecondaryEventReceiver(final TWidget widget) {
831 assert (secondaryEventReceiver == null);
832 assert (secondaryEventHandler == null);
833 assert (widget instanceof TMessageBox);
834 secondaryEventReceiver = widget;
835 secondaryEventHandler = new WidgetEventHandler(this, false);
836 (new Thread(secondaryEventHandler)).start();
837
838 // Refresh
839 repaint = true;
840 }
841
842 /**
843 * Yield to the secondary thread.
844 */
845 public final void yield() {
846 assert (secondaryEventReceiver != null);
99144c71
KL
847 // This is where we handoff the event handler lock from the primary
848 // to secondary thread. We unlock here, and in a future loop the
849 // secondary thread locks again. When it gives up, we have the
850 // single lock back.
851 boolean oldLock = unlockHandleEvent();
852 assert (oldLock == true);
853
c6940ed9
KL
854 while (secondaryEventReceiver != null) {
855 synchronized (this) {
856 try {
857 this.wait();
858 } catch (InterruptedException e) {
859 // SQUASH
860 }
861 }
862 }
a06459bd
KL
863 }
864
4328bb42
KL
865 /**
866 * Do stuff when there is no user input.
867 */
868 private void doIdle() {
99144c71
KL
869 if (debugThreads) {
870 System.err.printf("doIdle()\n");
871 }
872
7b5261bc 873 // Now run any timers that have timed out
d502a0e9
KL
874 Date now = new Date();
875 List<TTimer> keepTimers = new LinkedList<TTimer>();
876 for (TTimer timer: timers) {
877 if (timer.getNextTick().getTime() < now.getTime()) {
878 timer.tick();
c6940ed9 879 if (timer.recurring) {
d502a0e9 880 keepTimers.add(timer);
7b5261bc
KL
881 }
882 } else {
d502a0e9 883 keepTimers.add(timer);
7b5261bc
KL
884 }
885 }
886 timers = keepTimers;
887
888 // Call onIdle's
d502a0e9
KL
889 for (TWindow window: windows) {
890 window.onIdle();
7b5261bc 891 }
4328bb42 892 }
7d4115a5 893
4328bb42
KL
894 /**
895 * Get the amount of time I can sleep before missing a Timer tick.
896 *
897 * @param timeout = initial (maximum) timeout
898 * @return number of milliseconds between now and the next timer event
899 */
7b5261bc 900 protected int getSleepTime(final int timeout) {
d502a0e9
KL
901 Date now = new Date();
902 long sleepTime = timeout;
903 for (TTimer timer: timers) {
904 if (timer.getNextTick().getTime() < now.getTime()) {
7b5261bc
KL
905 return 0;
906 }
d502a0e9
KL
907 if ((timer.getNextTick().getTime() > now.getTime())
908 && ((timer.getNextTick().getTime() - now.getTime()) < sleepTime)
7b5261bc 909 ) {
d502a0e9 910 sleepTime = timer.getNextTick().getTime() - now.getTime();
7b5261bc
KL
911 }
912 }
d502a0e9
KL
913 assert (sleepTime >= 0);
914 return (int)sleepTime;
7d4115a5 915 }
4328bb42 916
48e27807
KL
917 /**
918 * Close window. Note that the window's destructor is NOT called by this
919 * method, instead the GC is assumed to do the cleanup.
920 *
921 * @param window the window to remove
922 */
923 public final void closeWindow(final TWindow window) {
fca67db0
KL
924 int z = window.getZ();
925 window.setZ(-1);
926 Collections.sort(windows);
927 windows.remove(0);
48e27807 928 TWindow activeWindow = null;
fca67db0
KL
929 for (TWindow w: windows) {
930 if (w.getZ() > z) {
931 w.setZ(w.getZ() - 1);
932 if (w.getZ() == 0) {
933 w.setActive(true);
934 assert (activeWindow == null);
48e27807
KL
935 activeWindow = w;
936 } else {
fca67db0 937 w.setActive(false);
48e27807
KL
938 }
939 }
940 }
941
942 // Perform window cleanup
943 window.onClose();
944
945 // Refresh screen
946 repaint = true;
947
948 // Check if we are closing a TMessageBox or similar
c6940ed9
KL
949 if (secondaryEventReceiver != null) {
950 assert (secondaryEventHandler != null);
48e27807
KL
951
952 // Do not send events to the secondaryEventReceiver anymore, the
953 // window is closed.
954 secondaryEventReceiver = null;
955
c6940ed9
KL
956 // Wake all threads: primary thread will be consuming events
957 // again, and secondary thread will exit.
958 synchronized (this) {
959 this.notifyAll();
48e27807
KL
960 }
961 }
48e27807
KL
962 }
963
964 /**
965 * Switch to the next window.
966 *
967 * @param forward if true, then switch to the next window in the list,
968 * otherwise switch to the previous window in the list
969 */
970 public final void switchWindow(final boolean forward) {
48e27807 971 // Only switch if there are multiple windows
fca67db0 972 if (windows.size() < 2) {
48e27807
KL
973 return;
974 }
975
fca67db0
KL
976 // Swap z/active between active window and the next in the list
977 int activeWindowI = -1;
978 for (int i = 0; i < windows.size(); i++) {
979 if (windows.get(i).getActive()) {
48e27807
KL
980 activeWindowI = i;
981 break;
982 }
983 }
fca67db0 984 assert (activeWindowI >= 0);
48e27807
KL
985
986 // Do not switch if a window is modal
fca67db0 987 if (windows.get(activeWindowI).isModal()) {
48e27807
KL
988 return;
989 }
990
fca67db0 991 int nextWindowI;
48e27807 992 if (forward) {
fca67db0 993 nextWindowI = (activeWindowI + 1) % windows.size();
48e27807
KL
994 } else {
995 if (activeWindowI == 0) {
fca67db0 996 nextWindowI = windows.size() - 1;
48e27807
KL
997 } else {
998 nextWindowI = activeWindowI - 1;
999 }
1000 }
fca67db0
KL
1001 windows.get(activeWindowI).setActive(false);
1002 windows.get(activeWindowI).setZ(windows.get(nextWindowI).getZ());
1003 windows.get(nextWindowI).setZ(0);
1004 windows.get(nextWindowI).setActive(true);
48e27807
KL
1005
1006 // Refresh
1007 repaint = true;
48e27807
KL
1008 }
1009
1010 /**
1011 * Add a window to my window list and make it active.
1012 *
1013 * @param window new window to add
1014 */
1015 public final void addWindow(final TWindow window) {
48e27807 1016 // Do not allow a modal window to spawn a non-modal window
a06459bd
KL
1017 if ((windows.size() > 0) && (windows.get(0).isModal())) {
1018 assert (window.isModal());
48e27807 1019 }
a06459bd 1020 for (TWindow w: windows) {
fca67db0 1021 w.setActive(false);
a06459bd 1022 w.setZ(w.getZ() + 1);
48e27807 1023 }
a06459bd 1024 windows.add(window);
fca67db0 1025 window.setActive(true);
a06459bd 1026 window.setZ(0);
48e27807
KL
1027 }
1028
fca67db0
KL
1029 /**
1030 * Check if there is a system-modal window on top.
1031 *
1032 * @return true if the active window is modal
1033 */
1034 private boolean modalWindowActive() {
1035 if (windows.size() == 0) {
1036 return false;
1037 }
1038 return windows.get(windows.size() - 1).isModal();
1039 }
1040
1041 /**
1042 * Check if a mouse event would hit either the active menu or any open
1043 * sub-menus.
1044 *
1045 * @param mouse mouse event
1046 * @return true if the mouse would hit the active menu or an open
1047 * sub-menu
1048 */
1049 private boolean mouseOnMenu(final TMouseEvent mouse) {
1050 assert (activeMenu != null);
1051 List<TMenu> menus = new LinkedList<TMenu>(subMenus);
1052 Collections.reverse(menus);
1053 for (TMenu menu: menus) {
1054 if (menu.mouseWouldHit(mouse)) {
1055 return true;
1056 }
1057 }
1058 return activeMenu.mouseWouldHit(mouse);
1059 }
1060
1061 /**
1062 * See if we need to switch window or activate the menu based on
1063 * a mouse click.
1064 *
1065 * @param mouse mouse event
1066 */
1067 private void checkSwitchFocus(final TMouseEvent mouse) {
1068
1069 if ((mouse.getType() == TMouseEvent.Type.MOUSE_DOWN)
1070 && (activeMenu != null)
1071 && (mouse.getAbsoluteY() != 0)
1072 && (!mouseOnMenu(mouse))
1073 ) {
1074 // They clicked outside the active menu, turn it off
1075 activeMenu.setActive(false);
1076 activeMenu = null;
1077 for (TMenu menu: subMenus) {
1078 menu.setActive(false);
1079 }
1080 subMenus.clear();
1081 // Continue checks
1082 }
1083
1084 // See if they hit the menu bar
1085 if ((mouse.getType() == TMouseEvent.Type.MOUSE_DOWN)
1086 && (mouse.getMouse1())
1087 && (!modalWindowActive())
1088 && (mouse.getAbsoluteY() == 0)
1089 ) {
1090
1091 for (TMenu menu: subMenus) {
1092 menu.setActive(false);
1093 }
1094 subMenus.clear();
1095
1096 // They selected the menu, go activate it
1097 for (TMenu menu: menus) {
1098 if ((mouse.getAbsoluteX() >= menu.getX())
1099 && (mouse.getAbsoluteX() < menu.getX()
1100 + menu.getTitle().length() + 2)
1101 ) {
1102 menu.setActive(true);
1103 activeMenu = menu;
1104 } else {
1105 menu.setActive(false);
1106 }
1107 }
1108 repaint = true;
1109 return;
1110 }
1111
1112 // See if they hit the menu bar
1113 if ((mouse.getType() == TMouseEvent.Type.MOUSE_MOTION)
1114 && (mouse.getMouse1())
1115 && (activeMenu != null)
1116 && (mouse.getAbsoluteY() == 0)
1117 ) {
1118
1119 TMenu oldMenu = activeMenu;
1120 for (TMenu menu: subMenus) {
1121 menu.setActive(false);
1122 }
1123 subMenus.clear();
1124
1125 // See if we should switch menus
1126 for (TMenu menu: menus) {
1127 if ((mouse.getAbsoluteX() >= menu.getX())
1128 && (mouse.getAbsoluteX() < menu.getX()
1129 + menu.getTitle().length() + 2)
1130 ) {
1131 menu.setActive(true);
1132 activeMenu = menu;
1133 }
1134 }
1135 if (oldMenu != activeMenu) {
1136 // They switched menus
1137 oldMenu.setActive(false);
1138 }
1139 repaint = true;
1140 return;
1141 }
1142
1143 // Only switch if there are multiple windows
1144 if (windows.size() < 2) {
1145 return;
1146 }
1147
1148 // Switch on the upclick
1149 if (mouse.getType() != TMouseEvent.Type.MOUSE_UP) {
1150 return;
1151 }
1152
1153 Collections.sort(windows);
1154 if (windows.get(0).isModal()) {
1155 // Modal windows don't switch
1156 return;
1157 }
1158
1159 for (TWindow window: windows) {
1160 assert (!window.isModal());
1161 if (window.mouseWouldHit(mouse)) {
1162 if (window == windows.get(0)) {
1163 // Clicked on the same window, nothing to do
1164 return;
1165 }
1166
1167 // We will be switching to another window
1168 assert (windows.get(0).getActive());
1169 assert (!window.getActive());
1170 windows.get(0).setActive(false);
1171 windows.get(0).setZ(window.getZ());
1172 window.setZ(0);
1173 window.setActive(true);
1174 repaint = true;
1175 return;
1176 }
1177 }
1178
1179 // Clicked on the background, nothing to do
1180 return;
1181 }
1182
1183 /**
1184 * Turn off the menu.
1185 */
928811d8 1186 public final void closeMenu() {
fca67db0
KL
1187 if (activeMenu != null) {
1188 activeMenu.setActive(false);
1189 activeMenu = null;
1190 for (TMenu menu: subMenus) {
1191 menu.setActive(false);
1192 }
1193 subMenus.clear();
1194 }
1195 repaint = true;
1196 }
1197
1198 /**
1199 * Turn off a sub-menu.
1200 */
928811d8 1201 public final void closeSubMenu() {
fca67db0
KL
1202 assert (activeMenu != null);
1203 TMenu item = subMenus.get(subMenus.size() - 1);
1204 assert (item != null);
1205 item.setActive(false);
1206 subMenus.remove(subMenus.size() - 1);
1207 repaint = true;
1208 }
1209
1210 /**
1211 * Switch to the next menu.
1212 *
1213 * @param forward if true, then switch to the next menu in the list,
1214 * otherwise switch to the previous menu in the list
1215 */
928811d8 1216 public final void switchMenu(final boolean forward) {
fca67db0
KL
1217 assert (activeMenu != null);
1218
1219 for (TMenu menu: subMenus) {
1220 menu.setActive(false);
1221 }
1222 subMenus.clear();
1223
1224 for (int i = 0; i < menus.size(); i++) {
1225 if (activeMenu == menus.get(i)) {
1226 if (forward) {
1227 if (i < menus.size() - 1) {
1228 i++;
1229 }
1230 } else {
1231 if (i > 0) {
1232 i--;
1233 }
1234 }
1235 activeMenu.setActive(false);
1236 activeMenu = menus.get(i);
1237 activeMenu.setActive(true);
1238 repaint = true;
1239 return;
1240 }
1241 }
1242 }
1243
1244 /**
1245 * Method that TApplication subclasses can override to handle menu or
1246 * posted command events.
1247 *
1248 * @param command command event
1249 * @return if true, this event was consumed
1250 */
1251 protected boolean onCommand(final TCommandEvent command) {
fca67db0
KL
1252 // Default: handle cmExit
1253 if (command.equals(cmExit)) {
1254 if (messageBox("Confirmation", "Exit application?",
c6940ed9 1255 TMessageBox.Type.YESNO).getResult() == TMessageBox.Result.YES) {
fca67db0
KL
1256 quit = true;
1257 }
1258 repaint = true;
1259 return true;
1260 }
1261
1262 if (command.equals(cmShell)) {
34a42e78 1263 openTerminal(0, 0, TWindow.RESIZABLE);
fca67db0
KL
1264 repaint = true;
1265 return true;
1266 }
1267
1268 if (command.equals(cmTile)) {
1269 tileWindows();
1270 repaint = true;
1271 return true;
1272 }
1273 if (command.equals(cmCascade)) {
1274 cascadeWindows();
1275 repaint = true;
1276 return true;
1277 }
1278 if (command.equals(cmCloseAll)) {
1279 closeAllWindows();
1280 repaint = true;
1281 return true;
1282 }
c6940ed9 1283
fca67db0
KL
1284 return false;
1285 }
1286
1287 /**
1288 * Method that TApplication subclasses can override to handle menu
1289 * events.
1290 *
1291 * @param menu menu event
1292 * @return if true, this event was consumed
1293 */
1294 protected boolean onMenu(final TMenuEvent menu) {
1295
fca67db0 1296 // Default: handle MID_EXIT
8e688b92 1297 if (menu.getId() == TMenu.MID_EXIT) {
fca67db0 1298 if (messageBox("Confirmation", "Exit application?",
c6940ed9 1299 TMessageBox.Type.YESNO).getResult() == TMessageBox.Result.YES) {
fca67db0
KL
1300 quit = true;
1301 }
1302 // System.err.printf("onMenu MID_EXIT result: quit = %s\n", quit);
1303 repaint = true;
1304 return true;
1305 }
1306
34a42e78
KL
1307 if (menu.getId() == TMenu.MID_SHELL) {
1308 openTerminal(0, 0, TWindow.RESIZABLE);
fca67db0
KL
1309 repaint = true;
1310 return true;
1311 }
1312
8e688b92 1313 if (menu.getId() == TMenu.MID_TILE) {
fca67db0
KL
1314 tileWindows();
1315 repaint = true;
1316 return true;
1317 }
8e688b92 1318 if (menu.getId() == TMenu.MID_CASCADE) {
fca67db0
KL
1319 cascadeWindows();
1320 repaint = true;
1321 return true;
1322 }
8e688b92 1323 if (menu.getId() == TMenu.MID_CLOSE_ALL) {
fca67db0
KL
1324 closeAllWindows();
1325 repaint = true;
1326 return true;
1327 }
fca67db0
KL
1328 return false;
1329 }
1330
1331 /**
1332 * Method that TApplication subclasses can override to handle keystrokes.
1333 *
1334 * @param keypress keystroke event
1335 * @return if true, this event was consumed
1336 */
1337 protected boolean onKeypress(final TKeypressEvent keypress) {
1338 // Default: only menu shortcuts
1339
1340 // Process Alt-F, Alt-E, etc. menu shortcut keys
1341 if (!keypress.getKey().getIsKey()
1342 && keypress.getKey().getAlt()
1343 && !keypress.getKey().getCtrl()
1344 && (activeMenu == null)
1345 ) {
1346
1347 assert (subMenus.size() == 0);
1348
1349 for (TMenu menu: menus) {
1350 if (Character.toLowerCase(menu.getMnemonic().getShortcut())
1351 == Character.toLowerCase(keypress.getKey().getCh())
1352 ) {
1353 activeMenu = menu;
1354 menu.setActive(true);
1355 repaint = true;
1356 return true;
1357 }
1358 }
1359 }
1360
1361 return false;
1362 }
48e27807 1363
928811d8
KL
1364 /**
1365 * Add a keyboard accelerator to the global hash.
1366 *
1367 * @param item menu item this accelerator relates to
1368 * @param keypress keypress that will dispatch a TMenuEvent
1369 */
1370 public final void addAccelerator(final TMenuItem item,
1371 final TKeypress keypress) {
e826b451
KL
1372
1373 // System.err.printf("addAccelerator: key %s item %s\n", keypress, item);
1374
1375 synchronized (accelerators) {
1376 assert (accelerators.get(keypress) == null);
1377 accelerators.put(keypress, item);
1378 }
928811d8
KL
1379 }
1380
1381 /**
1382 * Recompute menu x positions based on their title length.
1383 */
1384 public final void recomputeMenuX() {
1385 int x = 0;
1386 for (TMenu menu: menus) {
1387 menu.setX(x);
1388 x += menu.getTitle().length() + 2;
1389 }
1390 }
1391
1392 /**
1393 * Post an event to process and turn off the menu.
1394 *
1395 * @param event new event to add to the queue
1396 */
1397 public final void addMenuEvent(final TInputEvent event) {
8e688b92
KL
1398 synchronized (fillEventQueue) {
1399 fillEventQueue.add(event);
1400 }
928811d8
KL
1401 closeMenu();
1402 }
1403
1404 /**
1405 * Add a sub-menu to the list of open sub-menus.
1406 *
1407 * @param menu sub-menu
1408 */
1409 public final void addSubMenu(final TMenu menu) {
1410 subMenus.add(menu);
1411 }
1412
8e688b92
KL
1413 /**
1414 * Convenience function to add a top-level menu.
1415 *
1416 * @param title menu title
1417 * @return the new menu
1418 */
87a17f3c 1419 public final TMenu addMenu(final String title) {
8e688b92
KL
1420 int x = 0;
1421 int y = 0;
1422 TMenu menu = new TMenu(this, x, y, title);
1423 menus.add(menu);
1424 recomputeMenuX();
1425 return menu;
1426 }
1427
1428 /**
1429 * Convenience function to add a default "File" menu.
1430 *
1431 * @return the new menu
1432 */
1433 public final TMenu addFileMenu() {
1434 TMenu fileMenu = addMenu("&File");
1435 fileMenu.addDefaultItem(TMenu.MID_OPEN_FILE);
1436 fileMenu.addSeparator();
1437 fileMenu.addDefaultItem(TMenu.MID_SHELL);
1438 fileMenu.addDefaultItem(TMenu.MID_EXIT);
1439 return fileMenu;
1440 }
1441
1442 /**
1443 * Convenience function to add a default "Edit" menu.
1444 *
1445 * @return the new menu
1446 */
1447 public final TMenu addEditMenu() {
1448 TMenu editMenu = addMenu("&Edit");
1449 editMenu.addDefaultItem(TMenu.MID_CUT);
1450 editMenu.addDefaultItem(TMenu.MID_COPY);
1451 editMenu.addDefaultItem(TMenu.MID_PASTE);
1452 editMenu.addDefaultItem(TMenu.MID_CLEAR);
1453 return editMenu;
1454 }
1455
1456 /**
1457 * Convenience function to add a default "Window" menu.
1458 *
1459 * @return the new menu
1460 */
c6940ed9 1461 public final TMenu addWindowMenu() {
8e688b92
KL
1462 TMenu windowMenu = addMenu("&Window");
1463 windowMenu.addDefaultItem(TMenu.MID_TILE);
1464 windowMenu.addDefaultItem(TMenu.MID_CASCADE);
1465 windowMenu.addDefaultItem(TMenu.MID_CLOSE_ALL);
1466 windowMenu.addSeparator();
1467 windowMenu.addDefaultItem(TMenu.MID_WINDOW_MOVE);
1468 windowMenu.addDefaultItem(TMenu.MID_WINDOW_ZOOM);
1469 windowMenu.addDefaultItem(TMenu.MID_WINDOW_NEXT);
1470 windowMenu.addDefaultItem(TMenu.MID_WINDOW_PREVIOUS);
1471 windowMenu.addDefaultItem(TMenu.MID_WINDOW_CLOSE);
1472 return windowMenu;
1473 }
1474
1475 /**
1476 * Close all open windows.
1477 */
1478 private void closeAllWindows() {
1479 // Don't do anything if we are in the menu
1480 if (activeMenu != null) {
1481 return;
1482 }
1483 for (TWindow window: windows) {
1484 closeWindow(window);
1485 }
1486 }
1487
1488 /**
1489 * Re-layout the open windows as non-overlapping tiles. This produces
1490 * almost the same results as Turbo Pascal 7.0's IDE.
1491 */
1492 private void tileWindows() {
1493 // Don't do anything if we are in the menu
1494 if (activeMenu != null) {
1495 return;
1496 }
1497 int z = windows.size();
1498 if (z == 0) {
1499 return;
1500 }
1501 int a = 0;
1502 int b = 0;
1503 a = (int)(Math.sqrt(z));
1504 int c = 0;
1505 while (c < a) {
1506 b = (z - c) / a;
1507 if (((a * b) + c) == z) {
1508 break;
1509 }
1510 c++;
1511 }
1512 assert (a > 0);
1513 assert (b > 0);
1514 assert (c < a);
1515 int newWidth = (getScreen().getWidth() / a);
1516 int newHeight1 = ((getScreen().getHeight() - 1) / b);
1517 int newHeight2 = ((getScreen().getHeight() - 1) / (b + c));
8e688b92
KL
1518
1519 List<TWindow> sorted = new LinkedList<TWindow>(windows);
1520 Collections.sort(sorted);
1521 Collections.reverse(sorted);
1522 for (int i = 0; i < sorted.size(); i++) {
1523 int logicalX = i / b;
1524 int logicalY = i % b;
1525 if (i >= ((a - 1) * b)) {
1526 logicalX = a - 1;
1527 logicalY = i - ((a - 1) * b);
1528 }
1529
1530 TWindow w = sorted.get(i);
1531 w.setX(logicalX * newWidth);
1532 w.setWidth(newWidth);
1533 if (i >= ((a - 1) * b)) {
1534 w.setY((logicalY * newHeight2) + 1);
1535 w.setHeight(newHeight2);
1536 } else {
1537 w.setY((logicalY * newHeight1) + 1);
1538 w.setHeight(newHeight1);
1539 }
1540 }
1541 }
1542
1543 /**
1544 * Re-layout the open windows as overlapping cascaded windows.
1545 */
1546 private void cascadeWindows() {
1547 // Don't do anything if we are in the menu
1548 if (activeMenu != null) {
1549 return;
1550 }
1551 int x = 0;
1552 int y = 1;
1553 List<TWindow> sorted = new LinkedList<TWindow>(windows);
1554 Collections.sort(sorted);
1555 Collections.reverse(sorted);
d502a0e9
KL
1556 for (TWindow window: sorted) {
1557 window.setX(x);
1558 window.setY(y);
8e688b92
KL
1559 x++;
1560 y++;
1561 if (x > getScreen().getWidth()) {
1562 x = 0;
1563 }
1564 if (y >= getScreen().getHeight()) {
1565 y = 1;
1566 }
1567 }
1568 }
1569
d502a0e9
KL
1570 /**
1571 * Convenience function to add a timer.
1572 *
1573 * @param duration number of milliseconds to wait between ticks
1574 * @param recurring if true, re-schedule this timer after every tick
1575 * @param action function to call when button is pressed
c6940ed9 1576 * @return the timer
d502a0e9
KL
1577 */
1578 public final TTimer addTimer(final long duration, final boolean recurring,
1579 final TAction action) {
1580
1581 TTimer timer = new TTimer(duration, recurring, action);
1582 synchronized (timers) {
1583 timers.add(timer);
1584 }
1585 return timer;
1586 }
1587
1588 /**
1589 * Convenience function to remove a timer.
1590 *
1591 * @param timer timer to remove
1592 */
1593 public final void removeTimer(final TTimer timer) {
1594 synchronized (timers) {
1595 timers.remove(timer);
1596 }
1597 }
1598
c6940ed9
KL
1599 /**
1600 * Convenience function to spawn a message box.
1601 *
1602 * @param title window title, will be centered along the top border
1603 * @param caption message to display. Use embedded newlines to get a
1604 * multi-line box.
1605 * @return the new message box
1606 */
1607 public final TMessageBox messageBox(final String title,
1608 final String caption) {
1609
1610 return new TMessageBox(this, title, caption, TMessageBox.Type.OK);
1611 }
1612
1613 /**
1614 * Convenience function to spawn a message box.
1615 *
1616 * @param title window title, will be centered along the top border
1617 * @param caption message to display. Use embedded newlines to get a
1618 * multi-line box.
1619 * @param type one of the TMessageBox.Type constants. Default is
1620 * Type.OK.
1621 * @return the new message box
1622 */
1623 public final TMessageBox messageBox(final String title,
1624 final String caption, final TMessageBox.Type type) {
1625
1626 return new TMessageBox(this, title, caption, type);
1627 }
1628
1629 /**
1630 * Convenience function to spawn an input box.
1631 *
1632 * @param title window title, will be centered along the top border
1633 * @param caption message to display. Use embedded newlines to get a
1634 * multi-line box.
1635 * @return the new input box
1636 */
1637 public final TInputBox inputBox(final String title, final String caption) {
1638
1639 return new TInputBox(this, title, caption);
1640 }
1641
1642 /**
1643 * Convenience function to spawn an input box.
1644 *
1645 * @param title window title, will be centered along the top border
1646 * @param caption message to display. Use embedded newlines to get a
1647 * multi-line box.
1648 * @param text initial text to seed the field with
1649 * @return the new input box
1650 */
1651 public final TInputBox inputBox(final String title, final String caption,
1652 final String text) {
1653
1654 return new TInputBox(this, title, caption, text);
1655 }
1ac2ccb1 1656
34a42e78
KL
1657 /**
1658 * Convenience function to open a terminal window.
1659 *
1660 * @param x column relative to parent
1661 * @param y row relative to parent
1662 * @return the terminal new window
1663 */
1664 public final TTerminalWindow openTerminal(final int x, final int y) {
1665 return openTerminal(x, y, TWindow.RESIZABLE);
1666 }
1667
1668 /**
1669 * Convenience function to open a terminal window.
1670 *
1671 * @param x column relative to parent
1672 * @param y row relative to parent
1673 * @param flags mask of CENTERED, MODAL, or RESIZABLE
1674 * @return the terminal new window
1675 */
1676 public final TTerminalWindow openTerminal(final int x, final int y,
1677 final int flags) {
1678
1679 return new TTerminalWindow(this, x, y, flags);
1680 }
1681
7d4115a5 1682}