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