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