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