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