update roadmap
[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;
4328bb42
KL
37import java.util.LinkedList;
38import java.util.List;
39
40import jexer.bits.CellAttributes;
41import jexer.bits.ColorTheme;
42import jexer.bits.GraphicsChars;
43import jexer.event.TCommandEvent;
44import jexer.event.TInputEvent;
45import jexer.event.TKeypressEvent;
fca67db0 46import jexer.event.TMenuEvent;
4328bb42
KL
47import jexer.event.TMouseEvent;
48import jexer.event.TResizeEvent;
49import jexer.backend.Backend;
50import jexer.backend.ECMA48Backend;
48e27807 51import jexer.io.Screen;
928811d8
KL
52import jexer.menu.TMenu;
53import jexer.menu.TMenuItem;
4328bb42
KL
54import static jexer.TCommand.*;
55import static jexer.TKeypress.*;
56
7d4115a5
KL
57/**
58 * TApplication sets up a full Text User Interface application.
59 */
60public class TApplication {
61
62 /**
4328bb42
KL
63 * Access to the physical screen, keyboard, and mouse.
64 */
7b5261bc 65 private Backend backend;
4328bb42 66
48e27807
KL
67 /**
68 * Get the Screen.
69 *
70 * @return the Screen
71 */
72 public final Screen getScreen() {
73 return backend.getScreen();
74 }
75
4328bb42 76 /**
7b5261bc 77 * Actual mouse coordinate X.
4328bb42
KL
78 */
79 private int mouseX;
80
81 /**
7b5261bc 82 * Actual mouse coordinate Y.
4328bb42
KL
83 */
84 private int mouseY;
85
86 /**
7b5261bc 87 * Event queue that will be drained by either primary or secondary Fiber.
4328bb42
KL
88 */
89 private List<TInputEvent> eventQueue;
90
fca67db0
KL
91 /**
92 * Top-level menus in this application.
93 */
94 private List<TMenu> menus;
95
96 /**
97 * Stack of activated sub-menus in this application.
98 */
99 private List<TMenu> subMenus;
100
101 /**
102 * The currently acive menu.
103 */
104 private TMenu activeMenu = null;
105
4328bb42
KL
106 /**
107 * Windows and widgets pull colors from this ColorTheme.
108 */
7b5261bc
KL
109 private ColorTheme theme;
110
111 /**
112 * Get the color theme.
113 *
114 * @return the theme
115 */
116 public final ColorTheme getTheme() {
117 return theme;
118 }
4328bb42 119
a06459bd
KL
120 /**
121 * The top-level windows (but not menus).
122 */
fca67db0 123 private List<TWindow> windows;
a06459bd 124
4328bb42
KL
125 /**
126 * When true, exit the application.
127 */
48e27807 128 private boolean quit = false;
4328bb42
KL
129
130 /**
131 * When true, repaint the entire screen.
132 */
48e27807
KL
133 private boolean repaint = true;
134
135 /**
136 * Request full repaint on next screen refresh.
137 */
fca67db0 138 public final void setRepaint() {
48e27807
KL
139 repaint = true;
140 }
4328bb42
KL
141
142 /**
143 * When true, just flush updates from the screen.
7d4115a5 144 */
48e27807 145 private boolean flush = false;
4328bb42
KL
146
147 /**
7b5261bc
KL
148 * Y coordinate of the top edge of the desktop. For now this is a
149 * constant. Someday it would be nice to have a multi-line menu or
150 * toolbars.
4328bb42 151 */
48e27807
KL
152 private static final int desktopTop = 1;
153
154 /**
155 * Get Y coordinate of the top edge of the desktop.
156 *
157 * @return Y coordinate of the top edge of the desktop
158 */
159 public final int getDesktopTop() {
160 return desktopTop;
161 }
4328bb42
KL
162
163 /**
164 * Y coordinate of the bottom edge of the desktop.
165 */
48e27807
KL
166 private int desktopBottom;
167
168 /**
169 * Get Y coordinate of the bottom edge of the desktop.
170 *
171 * @return Y coordinate of the bottom edge of the desktop
172 */
173 public final int getDesktopBottom() {
174 return desktopBottom;
175 }
4328bb42
KL
176
177 /**
178 * Public constructor.
179 *
180 * @param input an InputStream connected to the remote user, or null for
181 * System.in. If System.in is used, then on non-Windows systems it will
182 * be put in raw mode; shutdown() will (blindly!) put System.in in cooked
183 * mode. input is always converted to a Reader with UTF-8 encoding.
184 * @param output an OutputStream connected to the remote user, or null
185 * for System.out. output is always converted to a Writer with UTF-8
186 * encoding.
7b5261bc
KL
187 * @throws UnsupportedEncodingException if an exception is thrown when
188 * creating the InputStreamReader
4328bb42 189 */
7b5261bc
KL
190 public TApplication(final InputStream input,
191 final OutputStream output) throws UnsupportedEncodingException {
4328bb42 192
7b5261bc
KL
193 backend = new ECMA48Backend(input, output);
194 theme = new ColorTheme();
a06459bd 195 desktopBottom = getScreen().getHeight() - 1;
7b5261bc 196 eventQueue = new LinkedList<TInputEvent>();
a06459bd 197 windows = new LinkedList<TWindow>();
fca67db0
KL
198 menus = new LinkedList<TMenu>();
199 subMenus = new LinkedList<TMenu>();
4328bb42
KL
200 }
201
202 /**
203 * Invert the cell at the mouse pointer position.
204 */
205 private void drawMouse() {
a06459bd 206 CellAttributes attr = getScreen().getAttrXY(mouseX, mouseY);
7b5261bc
KL
207 attr.setForeColor(attr.getForeColor().invert());
208 attr.setBackColor(attr.getBackColor().invert());
a06459bd 209 getScreen().putAttrXY(mouseX, mouseY, attr, false);
7b5261bc
KL
210 flush = true;
211
a06459bd 212 if (windows.size() == 0) {
7b5261bc
KL
213 repaint = true;
214 }
4328bb42
KL
215 }
216
217 /**
218 * Draw everything.
219 */
7b5261bc
KL
220 public final void drawAll() {
221 if ((flush) && (!repaint)) {
222 backend.flushScreen();
223 flush = false;
224 return;
225 }
226
227 if (!repaint) {
228 return;
229 }
230
231 // If true, the cursor is not visible
232 boolean cursor = false;
233
234 // Start with a clean screen
a06459bd 235 getScreen().clear();
7b5261bc
KL
236
237 // Draw the background
238 CellAttributes background = theme.getColor("tapplication.background");
a06459bd 239 getScreen().putAll(GraphicsChars.HATCH, background);
7b5261bc 240
7b5261bc 241 // Draw each window in reverse Z order
a06459bd
KL
242 List<TWindow> sorted = new LinkedList<TWindow>(windows);
243 Collections.sort(sorted);
244 Collections.reverse(sorted);
245 for (TWindow window: sorted) {
246 window.drawChildren();
7b5261bc
KL
247 }
248
249 // Draw the blank menubar line - reset the screen clipping first so
250 // it won't trim it out.
a06459bd
KL
251 getScreen().resetClipping();
252 getScreen().hLineXY(0, 0, getScreen().getWidth(), ' ',
7b5261bc
KL
253 theme.getColor("tmenu"));
254 // Now draw the menus.
255 int x = 1;
fca67db0 256 for (TMenu menu: menus) {
7b5261bc
KL
257 CellAttributes menuColor;
258 CellAttributes menuMnemonicColor;
fca67db0 259 if (menu.getActive()) {
7b5261bc
KL
260 menuColor = theme.getColor("tmenu.highlighted");
261 menuMnemonicColor = theme.getColor("tmenu.mnemonic.highlighted");
262 } else {
263 menuColor = theme.getColor("tmenu");
264 menuMnemonicColor = theme.getColor("tmenu.mnemonic");
265 }
266 // Draw the menu title
fca67db0 267 getScreen().hLineXY(x, 0, menu.getTitle().length() + 2, ' ',
7b5261bc 268 menuColor);
fca67db0 269 getScreen().putStrXY(x + 1, 0, menu.getTitle(), menuColor);
7b5261bc 270 // Draw the highlight character
fca67db0
KL
271 getScreen().putCharXY(x + 1 + menu.getMnemonic().getShortcutIdx(),
272 0, menu.getMnemonic().getShortcut(), menuMnemonicColor);
7b5261bc 273
fca67db0 274 if (menu.getActive()) {
a06459bd 275 menu.drawChildren();
7b5261bc 276 // Reset the screen clipping so we can draw the next title.
a06459bd 277 getScreen().resetClipping();
7b5261bc 278 }
fca67db0 279 x += menu.getTitle().length() + 2;
7b5261bc
KL
280 }
281
a06459bd 282 for (TMenu menu: subMenus) {
7b5261bc 283 // Reset the screen clipping so we can draw the next sub-menu.
a06459bd
KL
284 getScreen().resetClipping();
285 menu.drawChildren();
7b5261bc 286 }
7b5261bc
KL
287
288 // Draw the mouse pointer
289 drawMouse();
290
7b5261bc
KL
291 // Place the cursor if it is visible
292 TWidget activeWidget = null;
a06459bd
KL
293 if (sorted.size() > 0) {
294 activeWidget = sorted.get(sorted.size() - 1).getActiveChild();
295 if (activeWidget.visibleCursor()) {
296 getScreen().putCursor(true, activeWidget.getCursorAbsoluteX(),
7b5261bc
KL
297 activeWidget.getCursorAbsoluteY());
298 cursor = true;
299 }
300 }
301
302 // Kill the cursor
fca67db0 303 if (!cursor) {
a06459bd 304 getScreen().hideCursor();
7b5261bc 305 }
7b5261bc
KL
306
307 // Flush the screen contents
308 backend.flushScreen();
309
310 repaint = false;
311 flush = false;
4328bb42
KL
312 }
313
314 /**
7b5261bc 315 * Run this application until it exits.
4328bb42
KL
316 */
317 public final void run() {
7b5261bc
KL
318 List<TInputEvent> events = new LinkedList<TInputEvent>();
319
320 while (!quit) {
321 // Timeout is in milliseconds, so default timeout after 1 second
322 // of inactivity.
323 int timeout = getSleepTime(1000);
324
325 if (eventQueue.size() > 0) {
326 // Do not wait if there are definitely events waiting to be
327 // processed or a screen redraw to do.
328 timeout = 0;
329 }
330
331 // Pull any pending input events
332 backend.getEvents(events, timeout);
333 metaHandleEvents(events);
334 events.clear();
335
336 // Process timers and call doIdle()'s
337 doIdle();
338
339 // Update the screen
340 drawAll();
341 }
342
343 /*
344
345 // Shutdown the fibers
346 eventQueue.length = 0;
347 if (secondaryEventFiber !is null) {
348 assert(secondaryEventReceiver !is null);
349 secondaryEventReceiver = null;
350 if (secondaryEventFiber.state == Fiber.State.HOLD) {
351 // Wake up the secondary handler so that it can exit.
352 secondaryEventFiber.call();
353 }
354 }
355
356 if (primaryEventFiber.state == Fiber.State.HOLD) {
357 // Wake up the primary handler so that it can exit.
358 primaryEventFiber.call();
359 }
360 */
361
362 backend.shutdown();
4328bb42
KL
363 }
364
365 /**
366 * Peek at certain application-level events, add to eventQueue, and wake
367 * up the consuming Fiber.
368 *
369 * @param events the input events to consume
370 */
7b5261bc
KL
371 private void metaHandleEvents(final List<TInputEvent> events) {
372
373 for (TInputEvent event: events) {
374
375 /*
376 System.err.printf(String.format("metaHandleEvents event: %s\n",
377 event)); System.err.flush();
378 */
379
380 if (quit) {
381 // Do no more processing if the application is already trying
382 // to exit.
383 return;
384 }
385
386 // DEBUG
387 if (event instanceof TKeypressEvent) {
388 TKeypressEvent keypress = (TKeypressEvent) event;
b299e69c 389 if (keypress.equals(kbAltX)) {
7b5261bc
KL
390 quit = true;
391 return;
392 }
393 }
394 // DEBUG
395
396 // Special application-wide events -------------------------------
397
398 // Abort everything
399 if (event instanceof TCommandEvent) {
400 TCommandEvent command = (TCommandEvent) event;
401 if (command.getCmd().equals(cmAbort)) {
402 quit = true;
403 return;
404 }
405 }
406
407 // Screen resize
408 if (event instanceof TResizeEvent) {
409 TResizeEvent resize = (TResizeEvent) event;
a06459bd 410 getScreen().setDimensions(resize.getWidth(),
7b5261bc 411 resize.getHeight());
a06459bd 412 desktopBottom = getScreen().getHeight() - 1;
7b5261bc
KL
413 repaint = true;
414 mouseX = 0;
415 mouseY = 0;
416 continue;
417 }
418
419 // Peek at the mouse position
420 if (event instanceof TMouseEvent) {
421 TMouseEvent mouse = (TMouseEvent) event;
d4a29741
KL
422 if ((mouseX != mouse.getX()) || (mouseY != mouse.getY())) {
423 mouseX = mouse.getX();
424 mouseY = mouse.getY();
7b5261bc
KL
425 drawMouse();
426 }
427 }
428
a06459bd 429 // TODO: change to two separate threads
fca67db0
KL
430 primaryHandleEvent(event);
431
7b5261bc
KL
432 /*
433
434 // Put into the main queue
435 addEvent(event);
436
437 // Have one of the two consumer Fibers peel the events off
438 // the queue.
439 if (secondaryEventFiber !is null) {
440 assert(secondaryEventFiber.state == Fiber.State.HOLD);
441
442 // Wake up the secondary handler for these events
443 secondaryEventFiber.call();
444 } else {
445 assert(primaryEventFiber.state == Fiber.State.HOLD);
446
447 // Wake up the primary handler for these events
448 primaryEventFiber.call();
449 }
450 */
451
452 } // for (TInputEvent event: events)
4328bb42
KL
453
454 }
455
a06459bd
KL
456 /**
457 * Dispatch one event to the appropriate widget or application-level
fca67db0
KL
458 * event handler. This is the primary event handler, it has the normal
459 * application-wide event handling.
a06459bd
KL
460 *
461 * @param event the input event to consume
fca67db0 462 * @see #secondaryHandleEvent(TInputEvent event)
a06459bd 463 */
fca67db0
KL
464 private void primaryHandleEvent(final TInputEvent event) {
465
466 // System.err.printf("Handle event: %s\n", event);
467
468 // Special application-wide events -----------------------------------
469
470 // Peek at the mouse position
471 if (event instanceof TMouseEvent) {
472 // See if we need to switch focus to another window or the menu
473 checkSwitchFocus((TMouseEvent) event);
474 }
475
476 // Handle menu events
477 if ((activeMenu != null) && !(event instanceof TCommandEvent)) {
478 TMenu menu = activeMenu;
479
480 if (event instanceof TMouseEvent) {
481 TMouseEvent mouse = (TMouseEvent) event;
482
483 while (subMenus.size() > 0) {
484 TMenu subMenu = subMenus.get(subMenus.size() - 1);
485 if (subMenu.mouseWouldHit(mouse)) {
486 break;
487 }
488 if ((mouse.getType() == TMouseEvent.Type.MOUSE_MOTION)
489 && (!mouse.getMouse1())
490 && (!mouse.getMouse2())
491 && (!mouse.getMouse3())
492 && (!mouse.getMouseWheelUp())
493 && (!mouse.getMouseWheelDown())
494 ) {
495 break;
496 }
497 // We navigated away from a sub-menu, so close it
498 closeSubMenu();
499 }
500
501 // Convert the mouse relative x/y to menu coordinates
502 assert (mouse.getX() == mouse.getAbsoluteX());
503 assert (mouse.getY() == mouse.getAbsoluteY());
504 if (subMenus.size() > 0) {
505 menu = subMenus.get(subMenus.size() - 1);
506 }
507 mouse.setX(mouse.getX() - menu.getX());
508 mouse.setY(mouse.getY() - menu.getY());
509 }
510 menu.handleEvent(event);
511 return;
512 }
a06459bd
KL
513
514 /*
fca67db0
KL
515 TODO
516
517 if (event instanceof TKeypressEvent) {
518 TKeypressEvent keypress = (TKeypressEvent) event;
519 // See if this key matches an accelerator, and if so dispatch the
520 // menu event.
521 TKeypress keypressLowercase = keypress.getKey().toLowerCase();
522 TMenuItem item = accelerators.get(keypressLowercase);
523 if (item != null) {
524 // Let the menu item dispatch
525 item.dispatch();
526 return;
527 } else {
528 // Handle the keypress
529 if (onKeypress(keypress)) {
530 return;
531 }
532 }
533 }
a06459bd
KL
534 */
535
fca67db0
KL
536 if (event instanceof TCommandEvent) {
537 if (onCommand((TCommandEvent) event)) {
538 return;
539 }
540 }
541
542 if (event instanceof TMenuEvent) {
543 if (onMenu((TMenuEvent) event)) {
544 return;
545 }
546 }
547
548 // Dispatch events to the active window -------------------------------
549 for (TWindow window: windows) {
550 if (window.getActive()) {
a06459bd
KL
551 if (event instanceof TMouseEvent) {
552 TMouseEvent mouse = (TMouseEvent) event;
fca67db0
KL
553 // Convert the mouse relative x/y to window coordinates
554 assert (mouse.getX() == mouse.getAbsoluteX());
555 assert (mouse.getY() == mouse.getAbsoluteY());
556 mouse.setX(mouse.getX() - window.getX());
557 mouse.setY(mouse.getY() - window.getY());
558 }
559 // System.err("TApplication dispatch event: %s\n", event);
560 window.handleEvent(event);
561 break;
562 }
563 }
564 }
565 /**
566 * Dispatch one event to the appropriate widget or application-level
567 * event handler. This is the secondary event handler used by certain
568 * special dialogs (currently TMessageBox and TFileOpenBox).
569 *
570 * @param event the input event to consume
571 * @see #primaryHandleEvent(TInputEvent event)
572 */
573 private void secondaryHandleEvent(final TInputEvent event) {
574 // TODO
a06459bd
KL
575 }
576
4328bb42
KL
577 /**
578 * Do stuff when there is no user input.
579 */
580 private void doIdle() {
7b5261bc 581 /*
a06459bd 582 TODO
7b5261bc
KL
583 // Now run any timers that have timed out
584 auto now = Clock.currTime;
585 TTimer [] keepTimers;
586 foreach (t; timers) {
587 if (t.nextTick < now) {
588 t.tick();
589 if (t.recurring == true) {
590 keepTimers ~= t;
591 }
592 } else {
593 keepTimers ~= t;
594 }
595 }
596 timers = keepTimers;
597
598 // Call onIdle's
599 foreach (w; windows) {
600 w.onIdle();
601 }
602 */
4328bb42 603 }
7d4115a5 604
4328bb42
KL
605 /**
606 * Get the amount of time I can sleep before missing a Timer tick.
607 *
608 * @param timeout = initial (maximum) timeout
609 * @return number of milliseconds between now and the next timer event
610 */
7b5261bc
KL
611 protected int getSleepTime(final int timeout) {
612 /*
613 auto now = Clock.currTime;
614 auto sleepTime = dur!("msecs")(timeout);
615 foreach (t; timers) {
616 if (t.nextTick < now) {
617 return 0;
618 }
619 if ((t.nextTick > now) &&
620 ((t.nextTick - now) < sleepTime)
621 ) {
622 sleepTime = t.nextTick - now;
623 }
624 }
625 assert(sleepTime.total!("msecs")() >= 0);
626 return cast(uint)sleepTime.total!("msecs")();
627 */
628 // TODO: fix timers. Until then, come back after 250 millis.
629 return 250;
7d4115a5 630 }
4328bb42 631
48e27807
KL
632 /**
633 * Close window. Note that the window's destructor is NOT called by this
634 * method, instead the GC is assumed to do the cleanup.
635 *
636 * @param window the window to remove
637 */
638 public final void closeWindow(final TWindow window) {
fca67db0
KL
639 int z = window.getZ();
640 window.setZ(-1);
641 Collections.sort(windows);
642 windows.remove(0);
48e27807 643 TWindow activeWindow = null;
fca67db0
KL
644 for (TWindow w: windows) {
645 if (w.getZ() > z) {
646 w.setZ(w.getZ() - 1);
647 if (w.getZ() == 0) {
648 w.setActive(true);
649 assert (activeWindow == null);
48e27807
KL
650 activeWindow = w;
651 } else {
fca67db0 652 w.setActive(false);
48e27807
KL
653 }
654 }
655 }
656
657 // Perform window cleanup
658 window.onClose();
659
660 // Refresh screen
661 repaint = true;
662
fca67db0
KL
663 /*
664 TODO
665
666
48e27807
KL
667 // Check if we are closing a TMessageBox or similar
668 if (secondaryEventReceiver !is null) {
669 assert(secondaryEventFiber !is null);
670
671 // Do not send events to the secondaryEventReceiver anymore, the
672 // window is closed.
673 secondaryEventReceiver = null;
674
675 // Special case: if this is called while executing on a
676 // secondaryEventFiber, call it so that widgetEventHandler() can
677 // terminate.
678 if (secondaryEventFiber.state == Fiber.State.HOLD) {
679 secondaryEventFiber.call();
680 }
681 secondaryEventFiber = null;
682
683 // Unfreeze the logic in handleEvent()
684 if (primaryEventFiber.state == Fiber.State.HOLD) {
685 primaryEventFiber.call();
686 }
687 }
688 */
689 }
690
691 /**
692 * Switch to the next window.
693 *
694 * @param forward if true, then switch to the next window in the list,
695 * otherwise switch to the previous window in the list
696 */
697 public final void switchWindow(final boolean forward) {
48e27807 698 // Only switch if there are multiple windows
fca67db0 699 if (windows.size() < 2) {
48e27807
KL
700 return;
701 }
702
fca67db0
KL
703 // Swap z/active between active window and the next in the list
704 int activeWindowI = -1;
705 for (int i = 0; i < windows.size(); i++) {
706 if (windows.get(i).getActive()) {
48e27807
KL
707 activeWindowI = i;
708 break;
709 }
710 }
fca67db0 711 assert (activeWindowI >= 0);
48e27807
KL
712
713 // Do not switch if a window is modal
fca67db0 714 if (windows.get(activeWindowI).isModal()) {
48e27807
KL
715 return;
716 }
717
fca67db0 718 int nextWindowI;
48e27807 719 if (forward) {
fca67db0 720 nextWindowI = (activeWindowI + 1) % windows.size();
48e27807
KL
721 } else {
722 if (activeWindowI == 0) {
fca67db0 723 nextWindowI = windows.size() - 1;
48e27807
KL
724 } else {
725 nextWindowI = activeWindowI - 1;
726 }
727 }
fca67db0
KL
728 windows.get(activeWindowI).setActive(false);
729 windows.get(activeWindowI).setZ(windows.get(nextWindowI).getZ());
730 windows.get(nextWindowI).setZ(0);
731 windows.get(nextWindowI).setActive(true);
48e27807
KL
732
733 // Refresh
734 repaint = true;
48e27807
KL
735 }
736
737 /**
738 * Add a window to my window list and make it active.
739 *
740 * @param window new window to add
741 */
742 public final void addWindow(final TWindow window) {
48e27807 743 // Do not allow a modal window to spawn a non-modal window
a06459bd
KL
744 if ((windows.size() > 0) && (windows.get(0).isModal())) {
745 assert (window.isModal());
48e27807 746 }
a06459bd 747 for (TWindow w: windows) {
fca67db0 748 w.setActive(false);
a06459bd 749 w.setZ(w.getZ() + 1);
48e27807 750 }
a06459bd 751 windows.add(window);
fca67db0 752 window.setActive(true);
a06459bd 753 window.setZ(0);
48e27807
KL
754 }
755
fca67db0
KL
756 /**
757 * Check if there is a system-modal window on top.
758 *
759 * @return true if the active window is modal
760 */
761 private boolean modalWindowActive() {
762 if (windows.size() == 0) {
763 return false;
764 }
765 return windows.get(windows.size() - 1).isModal();
766 }
767
768 /**
769 * Check if a mouse event would hit either the active menu or any open
770 * sub-menus.
771 *
772 * @param mouse mouse event
773 * @return true if the mouse would hit the active menu or an open
774 * sub-menu
775 */
776 private boolean mouseOnMenu(final TMouseEvent mouse) {
777 assert (activeMenu != null);
778 List<TMenu> menus = new LinkedList<TMenu>(subMenus);
779 Collections.reverse(menus);
780 for (TMenu menu: menus) {
781 if (menu.mouseWouldHit(mouse)) {
782 return true;
783 }
784 }
785 return activeMenu.mouseWouldHit(mouse);
786 }
787
788 /**
789 * See if we need to switch window or activate the menu based on
790 * a mouse click.
791 *
792 * @param mouse mouse event
793 */
794 private void checkSwitchFocus(final TMouseEvent mouse) {
795
796 if ((mouse.getType() == TMouseEvent.Type.MOUSE_DOWN)
797 && (activeMenu != null)
798 && (mouse.getAbsoluteY() != 0)
799 && (!mouseOnMenu(mouse))
800 ) {
801 // They clicked outside the active menu, turn it off
802 activeMenu.setActive(false);
803 activeMenu = null;
804 for (TMenu menu: subMenus) {
805 menu.setActive(false);
806 }
807 subMenus.clear();
808 // Continue checks
809 }
810
811 // See if they hit the menu bar
812 if ((mouse.getType() == TMouseEvent.Type.MOUSE_DOWN)
813 && (mouse.getMouse1())
814 && (!modalWindowActive())
815 && (mouse.getAbsoluteY() == 0)
816 ) {
817
818 for (TMenu menu: subMenus) {
819 menu.setActive(false);
820 }
821 subMenus.clear();
822
823 // They selected the menu, go activate it
824 for (TMenu menu: menus) {
825 if ((mouse.getAbsoluteX() >= menu.getX())
826 && (mouse.getAbsoluteX() < menu.getX()
827 + menu.getTitle().length() + 2)
828 ) {
829 menu.setActive(true);
830 activeMenu = menu;
831 } else {
832 menu.setActive(false);
833 }
834 }
835 repaint = true;
836 return;
837 }
838
839 // See if they hit the menu bar
840 if ((mouse.getType() == TMouseEvent.Type.MOUSE_MOTION)
841 && (mouse.getMouse1())
842 && (activeMenu != null)
843 && (mouse.getAbsoluteY() == 0)
844 ) {
845
846 TMenu oldMenu = activeMenu;
847 for (TMenu menu: subMenus) {
848 menu.setActive(false);
849 }
850 subMenus.clear();
851
852 // See if we should switch menus
853 for (TMenu menu: menus) {
854 if ((mouse.getAbsoluteX() >= menu.getX())
855 && (mouse.getAbsoluteX() < menu.getX()
856 + menu.getTitle().length() + 2)
857 ) {
858 menu.setActive(true);
859 activeMenu = menu;
860 }
861 }
862 if (oldMenu != activeMenu) {
863 // They switched menus
864 oldMenu.setActive(false);
865 }
866 repaint = true;
867 return;
868 }
869
870 // Only switch if there are multiple windows
871 if (windows.size() < 2) {
872 return;
873 }
874
875 // Switch on the upclick
876 if (mouse.getType() != TMouseEvent.Type.MOUSE_UP) {
877 return;
878 }
879
880 Collections.sort(windows);
881 if (windows.get(0).isModal()) {
882 // Modal windows don't switch
883 return;
884 }
885
886 for (TWindow window: windows) {
887 assert (!window.isModal());
888 if (window.mouseWouldHit(mouse)) {
889 if (window == windows.get(0)) {
890 // Clicked on the same window, nothing to do
891 return;
892 }
893
894 // We will be switching to another window
895 assert (windows.get(0).getActive());
896 assert (!window.getActive());
897 windows.get(0).setActive(false);
898 windows.get(0).setZ(window.getZ());
899 window.setZ(0);
900 window.setActive(true);
901 repaint = true;
902 return;
903 }
904 }
905
906 // Clicked on the background, nothing to do
907 return;
908 }
909
910 /**
911 * Turn off the menu.
912 */
928811d8 913 public final void closeMenu() {
fca67db0
KL
914 if (activeMenu != null) {
915 activeMenu.setActive(false);
916 activeMenu = null;
917 for (TMenu menu: subMenus) {
918 menu.setActive(false);
919 }
920 subMenus.clear();
921 }
922 repaint = true;
923 }
924
925 /**
926 * Turn off a sub-menu.
927 */
928811d8 928 public final void closeSubMenu() {
fca67db0
KL
929 assert (activeMenu != null);
930 TMenu item = subMenus.get(subMenus.size() - 1);
931 assert (item != null);
932 item.setActive(false);
933 subMenus.remove(subMenus.size() - 1);
934 repaint = true;
935 }
936
937 /**
938 * Switch to the next menu.
939 *
940 * @param forward if true, then switch to the next menu in the list,
941 * otherwise switch to the previous menu in the list
942 */
928811d8 943 public final void switchMenu(final boolean forward) {
fca67db0
KL
944 assert (activeMenu != null);
945
946 for (TMenu menu: subMenus) {
947 menu.setActive(false);
948 }
949 subMenus.clear();
950
951 for (int i = 0; i < menus.size(); i++) {
952 if (activeMenu == menus.get(i)) {
953 if (forward) {
954 if (i < menus.size() - 1) {
955 i++;
956 }
957 } else {
958 if (i > 0) {
959 i--;
960 }
961 }
962 activeMenu.setActive(false);
963 activeMenu = menus.get(i);
964 activeMenu.setActive(true);
965 repaint = true;
966 return;
967 }
968 }
969 }
970
971 /**
972 * Method that TApplication subclasses can override to handle menu or
973 * posted command events.
974 *
975 * @param command command event
976 * @return if true, this event was consumed
977 */
978 protected boolean onCommand(final TCommandEvent command) {
979 /*
980 TODO
981 // Default: handle cmExit
982 if (command.equals(cmExit)) {
983 if (messageBox("Confirmation", "Exit application?",
984 TMessageBox.Type.YESNO).result == TMessageBox.Result.YES) {
985 quit = true;
986 }
987 repaint = true;
988 return true;
989 }
990
991 if (command.equals(cmShell)) {
992 openTerminal(0, 0, TWindow.Flag.RESIZABLE);
993 repaint = true;
994 return true;
995 }
996
997 if (command.equals(cmTile)) {
998 tileWindows();
999 repaint = true;
1000 return true;
1001 }
1002 if (command.equals(cmCascade)) {
1003 cascadeWindows();
1004 repaint = true;
1005 return true;
1006 }
1007 if (command.equals(cmCloseAll)) {
1008 closeAllWindows();
1009 repaint = true;
1010 return true;
1011 }
1012 */
1013 return false;
1014 }
1015
1016 /**
1017 * Method that TApplication subclasses can override to handle menu
1018 * events.
1019 *
1020 * @param menu menu event
1021 * @return if true, this event was consumed
1022 */
1023 protected boolean onMenu(final TMenuEvent menu) {
1024
1025 /*
1026 TODO
1027 // Default: handle MID_EXIT
1028 if (menu.id == TMenu.MID_EXIT) {
1029 if (messageBox("Confirmation", "Exit application?",
1030 TMessageBox.Type.YESNO).result == TMessageBox.Result.YES) {
1031 quit = true;
1032 }
1033 // System.err.printf("onMenu MID_EXIT result: quit = %s\n", quit);
1034 repaint = true;
1035 return true;
1036 }
1037
1038 if (menu.id == TMenu.MID_SHELL) {
1039 openTerminal(0, 0, TWindow.Flag.RESIZABLE);
1040 repaint = true;
1041 return true;
1042 }
1043
1044 if (menu.id == TMenu.MID_TILE) {
1045 tileWindows();
1046 repaint = true;
1047 return true;
1048 }
1049 if (menu.id == TMenu.MID_CASCADE) {
1050 cascadeWindows();
1051 repaint = true;
1052 return true;
1053 }
1054 if (menu.id == TMenu.MID_CLOSE_ALL) {
1055 closeAllWindows();
1056 repaint = true;
1057 return true;
1058 }
1059 */
1060 return false;
1061 }
1062
1063 /**
1064 * Method that TApplication subclasses can override to handle keystrokes.
1065 *
1066 * @param keypress keystroke event
1067 * @return if true, this event was consumed
1068 */
1069 protected boolean onKeypress(final TKeypressEvent keypress) {
1070 // Default: only menu shortcuts
1071
1072 // Process Alt-F, Alt-E, etc. menu shortcut keys
1073 if (!keypress.getKey().getIsKey()
1074 && keypress.getKey().getAlt()
1075 && !keypress.getKey().getCtrl()
1076 && (activeMenu == null)
1077 ) {
1078
1079 assert (subMenus.size() == 0);
1080
1081 for (TMenu menu: menus) {
1082 if (Character.toLowerCase(menu.getMnemonic().getShortcut())
1083 == Character.toLowerCase(keypress.getKey().getCh())
1084 ) {
1085 activeMenu = menu;
1086 menu.setActive(true);
1087 repaint = true;
1088 return true;
1089 }
1090 }
1091 }
1092
1093 return false;
1094 }
48e27807 1095
928811d8
KL
1096 /**
1097 * Add a keyboard accelerator to the global hash.
1098 *
1099 * @param item menu item this accelerator relates to
1100 * @param keypress keypress that will dispatch a TMenuEvent
1101 */
1102 public final void addAccelerator(final TMenuItem item,
1103 final TKeypress keypress) {
1104 /*
1105 TODO
1106 assert((keypress in accelerators) is null);
1107 accelerators[keypress] = item;
1108 */
1109 }
1110
1111 /**
1112 * Recompute menu x positions based on their title length.
1113 */
1114 public final void recomputeMenuX() {
1115 int x = 0;
1116 for (TMenu menu: menus) {
1117 menu.setX(x);
1118 x += menu.getTitle().length() + 2;
1119 }
1120 }
1121
1122 /**
1123 * Post an event to process and turn off the menu.
1124 *
1125 * @param event new event to add to the queue
1126 */
1127 public final void addMenuEvent(final TInputEvent event) {
1128 /*
1129 TODO - synchronize correctly
1130 eventQueue ~= event;
1131 */
1132 closeMenu();
1133 }
1134
1135 /**
1136 * Add a sub-menu to the list of open sub-menus.
1137 *
1138 * @param menu sub-menu
1139 */
1140 public final void addSubMenu(final TMenu menu) {
1141 subMenus.add(menu);
1142 }
1143
7d4115a5 1144}