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