draggable window
[fanfix.git] / src / jexer / TApplication.java
1 /**
2 * Jexer - Java Text User Interface
3 *
4 * License: LGPLv3 or later
5 *
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.
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
27 *
28 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
29 * @version 1
30 */
31 package jexer;
32
33 import java.io.InputStream;
34 import java.io.OutputStream;
35 import java.io.UnsupportedEncodingException;
36 import java.util.Collections;
37 import java.util.LinkedList;
38 import java.util.List;
39
40 import jexer.bits.CellAttributes;
41 import jexer.bits.ColorTheme;
42 import jexer.bits.GraphicsChars;
43 import jexer.event.TCommandEvent;
44 import jexer.event.TInputEvent;
45 import jexer.event.TKeypressEvent;
46 import jexer.event.TMouseEvent;
47 import jexer.event.TResizeEvent;
48 import jexer.backend.Backend;
49 import jexer.backend.ECMA48Backend;
50 import jexer.io.Screen;
51 import static jexer.TCommand.*;
52 import static jexer.TKeypress.*;
53
54 /**
55 * TApplication sets up a full Text User Interface application.
56 */
57 public class TApplication {
58
59 /**
60 * Access to the physical screen, keyboard, and mouse.
61 */
62 private Backend backend;
63
64 /**
65 * Get the Screen.
66 *
67 * @return the Screen
68 */
69 public final Screen getScreen() {
70 return backend.getScreen();
71 }
72
73 /**
74 * Actual mouse coordinate X.
75 */
76 private int mouseX;
77
78 /**
79 * Actual mouse coordinate Y.
80 */
81 private int mouseY;
82
83 /**
84 * Event queue that will be drained by either primary or secondary Fiber.
85 */
86 private List<TInputEvent> eventQueue;
87
88 /**
89 * Windows and widgets pull colors from this ColorTheme.
90 */
91 private ColorTheme theme;
92
93 /**
94 * Get the color theme.
95 *
96 * @return the theme
97 */
98 public final ColorTheme getTheme() {
99 return theme;
100 }
101
102 /**
103 * The top-level windows (but not menus).
104 */
105 List<TWindow> windows;
106
107 /**
108 * When true, exit the application.
109 */
110 private boolean quit = false;
111
112 /**
113 * When true, repaint the entire screen.
114 */
115 private boolean repaint = true;
116
117 /**
118 * Request full repaint on next screen refresh.
119 */
120 public void setRepaint() {
121 repaint = true;
122 }
123
124 /**
125 * When true, just flush updates from the screen.
126 */
127 private boolean flush = false;
128
129 /**
130 * Y coordinate of the top edge of the desktop. For now this is a
131 * constant. Someday it would be nice to have a multi-line menu or
132 * toolbars.
133 */
134 private static final int desktopTop = 1;
135
136 /**
137 * Get Y coordinate of the top edge of the desktop.
138 *
139 * @return Y coordinate of the top edge of the desktop
140 */
141 public final int getDesktopTop() {
142 return desktopTop;
143 }
144
145 /**
146 * Y coordinate of the bottom edge of the desktop.
147 */
148 private int desktopBottom;
149
150 /**
151 * Get Y coordinate of the bottom edge of the desktop.
152 *
153 * @return Y coordinate of the bottom edge of the desktop
154 */
155 public final int getDesktopBottom() {
156 return desktopBottom;
157 }
158
159 /**
160 * Public constructor.
161 *
162 * @param input an InputStream connected to the remote user, or null for
163 * System.in. If System.in is used, then on non-Windows systems it will
164 * be put in raw mode; shutdown() will (blindly!) put System.in in cooked
165 * mode. input is always converted to a Reader with UTF-8 encoding.
166 * @param output an OutputStream connected to the remote user, or null
167 * for System.out. output is always converted to a Writer with UTF-8
168 * encoding.
169 * @throws UnsupportedEncodingException if an exception is thrown when
170 * creating the InputStreamReader
171 */
172 public TApplication(final InputStream input,
173 final OutputStream output) throws UnsupportedEncodingException {
174
175 backend = new ECMA48Backend(input, output);
176 theme = new ColorTheme();
177 desktopBottom = getScreen().getHeight() - 1;
178 eventQueue = new LinkedList<TInputEvent>();
179 windows = new LinkedList<TWindow>();
180 }
181
182 /**
183 * Invert the cell at the mouse pointer position.
184 */
185 private void drawMouse() {
186 CellAttributes attr = getScreen().getAttrXY(mouseX, mouseY);
187 attr.setForeColor(attr.getForeColor().invert());
188 attr.setBackColor(attr.getBackColor().invert());
189 getScreen().putAttrXY(mouseX, mouseY, attr, false);
190 flush = true;
191
192 if (windows.size() == 0) {
193 repaint = true;
194 }
195 }
196
197 /**
198 * Draw everything.
199 */
200 public final void drawAll() {
201 if ((flush) && (!repaint)) {
202 backend.flushScreen();
203 flush = false;
204 return;
205 }
206
207 if (!repaint) {
208 return;
209 }
210
211 // If true, the cursor is not visible
212 boolean cursor = false;
213
214 // Start with a clean screen
215 getScreen().clear();
216
217 // Draw the background
218 CellAttributes background = theme.getColor("tapplication.background");
219 getScreen().putAll(GraphicsChars.HATCH, background);
220
221 // Draw each window in reverse Z order
222 List<TWindow> sorted = new LinkedList<TWindow>(windows);
223 Collections.sort(sorted);
224 Collections.reverse(sorted);
225 for (TWindow window: sorted) {
226 window.drawChildren();
227 }
228
229 /*
230 // Draw the blank menubar line - reset the screen clipping first so
231 // it won't trim it out.
232 getScreen().resetClipping();
233 getScreen().hLineXY(0, 0, getScreen().getWidth(), ' ',
234 theme.getColor("tmenu"));
235 // Now draw the menus.
236 int x = 1;
237 for (TMenu m: menus) {
238 CellAttributes menuColor;
239 CellAttributes menuMnemonicColor;
240 if (menu.active) {
241 menuColor = theme.getColor("tmenu.highlighted");
242 menuMnemonicColor = theme.getColor("tmenu.mnemonic.highlighted");
243 } else {
244 menuColor = theme.getColor("tmenu");
245 menuMnemonicColor = theme.getColor("tmenu.mnemonic");
246 }
247 // Draw the menu title
248 getScreen().hLineXY(x, 0, menu.title.length() + 2, ' ',
249 menuColor);
250 getScreen().putStrXY(x + 1, 0, menu.title, menuColor);
251 // Draw the highlight character
252 getScreen().putCharXY(x + 1 + m.mnemonic.shortcutIdx, 0,
253 m.mnemonic.shortcut, menuMnemonicColor);
254
255 if (menu.active) {
256 menu.drawChildren();
257 // Reset the screen clipping so we can draw the next title.
258 getScreen().resetClipping();
259 }
260 x += menu.title.length + 2;
261 }
262
263 for (TMenu menu: subMenus) {
264 // Reset the screen clipping so we can draw the next sub-menu.
265 getScreen().resetClipping();
266 menu.drawChildren();
267 }
268 */
269
270 // Draw the mouse pointer
271 drawMouse();
272
273 // Place the cursor if it is visible
274 TWidget activeWidget = null;
275 if (sorted.size() > 0) {
276 activeWidget = sorted.get(sorted.size() - 1).getActiveChild();
277 if (activeWidget.visibleCursor()) {
278 getScreen().putCursor(true, activeWidget.getCursorAbsoluteX(),
279 activeWidget.getCursorAbsoluteY());
280 cursor = true;
281 }
282 }
283
284 // Kill the cursor
285 if (cursor == false) {
286 getScreen().hideCursor();
287 }
288
289 // Flush the screen contents
290 backend.flushScreen();
291
292 repaint = false;
293 flush = false;
294 }
295
296 /**
297 * Run this application until it exits.
298 */
299 public final void run() {
300 List<TInputEvent> events = new LinkedList<TInputEvent>();
301
302 while (!quit) {
303 // Timeout is in milliseconds, so default timeout after 1 second
304 // of inactivity.
305 int timeout = getSleepTime(1000);
306
307 if (eventQueue.size() > 0) {
308 // Do not wait if there are definitely events waiting to be
309 // processed or a screen redraw to do.
310 timeout = 0;
311 }
312
313 // Pull any pending input events
314 backend.getEvents(events, timeout);
315 metaHandleEvents(events);
316 events.clear();
317
318 // Process timers and call doIdle()'s
319 doIdle();
320
321 // Update the screen
322 drawAll();
323 }
324
325 /*
326
327 // Shutdown the fibers
328 eventQueue.length = 0;
329 if (secondaryEventFiber !is null) {
330 assert(secondaryEventReceiver !is null);
331 secondaryEventReceiver = null;
332 if (secondaryEventFiber.state == Fiber.State.HOLD) {
333 // Wake up the secondary handler so that it can exit.
334 secondaryEventFiber.call();
335 }
336 }
337
338 if (primaryEventFiber.state == Fiber.State.HOLD) {
339 // Wake up the primary handler so that it can exit.
340 primaryEventFiber.call();
341 }
342 */
343
344 backend.shutdown();
345 }
346
347 /**
348 * Peek at certain application-level events, add to eventQueue, and wake
349 * up the consuming Fiber.
350 *
351 * @param events the input events to consume
352 */
353 private void metaHandleEvents(final List<TInputEvent> events) {
354
355 for (TInputEvent event: events) {
356
357 /*
358 System.err.printf(String.format("metaHandleEvents event: %s\n",
359 event)); System.err.flush();
360 */
361
362 if (quit) {
363 // Do no more processing if the application is already trying
364 // to exit.
365 return;
366 }
367
368 // DEBUG
369 if (event instanceof TKeypressEvent) {
370 TKeypressEvent keypress = (TKeypressEvent) event;
371 if (keypress.equals(kbAltX)) {
372 quit = true;
373 return;
374 }
375 }
376 // DEBUG
377
378 // Special application-wide events -------------------------------
379
380 // Abort everything
381 if (event instanceof TCommandEvent) {
382 TCommandEvent command = (TCommandEvent) event;
383 if (command.getCmd().equals(cmAbort)) {
384 quit = true;
385 return;
386 }
387 }
388
389 // Screen resize
390 if (event instanceof TResizeEvent) {
391 TResizeEvent resize = (TResizeEvent) event;
392 getScreen().setDimensions(resize.getWidth(),
393 resize.getHeight());
394 desktopBottom = getScreen().getHeight() - 1;
395 repaint = true;
396 mouseX = 0;
397 mouseY = 0;
398 continue;
399 }
400
401 // Peek at the mouse position
402 if (event instanceof TMouseEvent) {
403 TMouseEvent mouse = (TMouseEvent) event;
404 if ((mouseX != mouse.getX()) || (mouseY != mouse.getY())) {
405 mouseX = mouse.getX();
406 mouseY = mouse.getY();
407 drawMouse();
408 }
409 }
410
411 // TODO: change to two separate threads
412 handleEvent(event);
413
414 /*
415
416 // Put into the main queue
417 addEvent(event);
418
419 // Have one of the two consumer Fibers peel the events off
420 // the queue.
421 if (secondaryEventFiber !is null) {
422 assert(secondaryEventFiber.state == Fiber.State.HOLD);
423
424 // Wake up the secondary handler for these events
425 secondaryEventFiber.call();
426 } else {
427 assert(primaryEventFiber.state == Fiber.State.HOLD);
428
429 // Wake up the primary handler for these events
430 primaryEventFiber.call();
431 }
432 */
433
434 } // for (TInputEvent event: events)
435
436 }
437
438 /**
439 * Dispatch one event to the appropriate widget or application-level
440 * event handler.
441 *
442 * @param event the input event to consume
443 */
444 private final void handleEvent(TInputEvent event) {
445
446 /*
447 // std.stdio.stderr.writefln("Handle event: %s", event);
448
449 // Special application-wide events -----------------------------------
450
451 // Peek at the mouse position
452 if (auto mouse = cast(TMouseEvent)event) {
453 // See if we need to switch focus to another window or the menu
454 checkSwitchFocus(mouse);
455 }
456
457 // Handle menu events
458 if ((activeMenu !is null) && (!cast(TCommandEvent)event)) {
459 TMenu menu = activeMenu;
460 if (auto mouse = cast(TMouseEvent)event) {
461
462 while (subMenus.length > 0) {
463 TMenu subMenu = subMenus[$ - 1];
464 if (subMenu.mouseWouldHit(mouse)) {
465 break;
466 }
467 if ((mouse.type == TMouseEvent.Type.MOUSE_MOTION) &&
468 (!mouse.mouse1) &&
469 (!mouse.mouse2) &&
470 (!mouse.mouse3) &&
471 (!mouse.mouseWheelUp) &&
472 (!mouse.mouseWheelDown)
473 ) {
474 break;
475 }
476 // We navigated away from a sub-menu, so close it
477 closeSubMenu();
478 }
479
480 // Convert the mouse relative x/y to menu coordinates
481 assert(mouse.x == mouse.absoluteX);
482 assert(mouse.y == mouse.absoluteY);
483 if (subMenus.length > 0) {
484 menu = subMenus[$ - 1];
485 }
486 mouse.x -= menu.x;
487 mouse.y -= menu.y;
488 }
489 menu.handleEvent(event);
490 return;
491 }
492
493 if (auto keypress = cast(TKeypressEvent)event) {
494 // See if this key matches an accelerator, and if so dispatch the
495 // menu event.
496 TKeypress keypressLowercase = toLower(keypress.key);
497 TMenuItem *item = (keypressLowercase in accelerators);
498 if (item !is null) {
499 // Let the menu item dispatch
500 item.dispatch();
501 return;
502 } else {
503 // Handle the keypress
504 if (onKeypress(keypress)) {
505 return;
506 }
507 }
508 }
509
510 if (auto cmd = cast(TCommandEvent)event) {
511 if (onCommand(cmd)) {
512 return;
513 }
514 }
515
516 if (auto menu = cast(TMenuEvent)event) {
517 if (onMenu(menu)) {
518 return;
519 }
520 }
521 */
522
523 // Dispatch events to the active window -------------------------------
524 for (TWindow window: windows) {
525 if (window.active) {
526 if (event instanceof TMouseEvent) {
527 TMouseEvent mouse = (TMouseEvent) event;
528 // Convert the mouse relative x/y to window coordinates
529 assert (mouse.getX() == mouse.getAbsoluteX());
530 assert (mouse.getY() == mouse.getAbsoluteY());
531 mouse.setX(mouse.getX() - window.x);
532 mouse.setY(mouse.getY() - window.y);
533 }
534 // System.err("TApplication dispatch event: %s\n", event);
535 window.handleEvent(event);
536 break;
537 }
538 }
539 }
540
541 /**
542 * Do stuff when there is no user input.
543 */
544 private void doIdle() {
545 /*
546 TODO
547 // Now run any timers that have timed out
548 auto now = Clock.currTime;
549 TTimer [] keepTimers;
550 foreach (t; timers) {
551 if (t.nextTick < now) {
552 t.tick();
553 if (t.recurring == true) {
554 keepTimers ~= t;
555 }
556 } else {
557 keepTimers ~= t;
558 }
559 }
560 timers = keepTimers;
561
562 // Call onIdle's
563 foreach (w; windows) {
564 w.onIdle();
565 }
566 */
567 }
568
569 /**
570 * Get the amount of time I can sleep before missing a Timer tick.
571 *
572 * @param timeout = initial (maximum) timeout
573 * @return number of milliseconds between now and the next timer event
574 */
575 protected int getSleepTime(final int timeout) {
576 /*
577 auto now = Clock.currTime;
578 auto sleepTime = dur!("msecs")(timeout);
579 foreach (t; timers) {
580 if (t.nextTick < now) {
581 return 0;
582 }
583 if ((t.nextTick > now) &&
584 ((t.nextTick - now) < sleepTime)
585 ) {
586 sleepTime = t.nextTick - now;
587 }
588 }
589 assert(sleepTime.total!("msecs")() >= 0);
590 return cast(uint)sleepTime.total!("msecs")();
591 */
592 // TODO: fix timers. Until then, come back after 250 millis.
593 return 250;
594 }
595
596 /**
597 * Close window. Note that the window's destructor is NOT called by this
598 * method, instead the GC is assumed to do the cleanup.
599 *
600 * @param window the window to remove
601 */
602 public final void closeWindow(final TWindow window) {
603 /*
604 TODO
605
606 uint z = window.z;
607 window.z = -1;
608 windows.sort;
609 windows = windows[1 .. $];
610 TWindow activeWindow = null;
611 foreach (w; windows) {
612 if (w.z > z) {
613 w.z--;
614 if (w.z == 0) {
615 w.active = true;
616 assert(activeWindow is null);
617 activeWindow = w;
618 } else {
619 w.active = false;
620 }
621 }
622 }
623
624 // Perform window cleanup
625 window.onClose();
626
627 // Refresh screen
628 repaint = true;
629
630 // Check if we are closing a TMessageBox or similar
631 if (secondaryEventReceiver !is null) {
632 assert(secondaryEventFiber !is null);
633
634 // Do not send events to the secondaryEventReceiver anymore, the
635 // window is closed.
636 secondaryEventReceiver = null;
637
638 // Special case: if this is called while executing on a
639 // secondaryEventFiber, call it so that widgetEventHandler() can
640 // terminate.
641 if (secondaryEventFiber.state == Fiber.State.HOLD) {
642 secondaryEventFiber.call();
643 }
644 secondaryEventFiber = null;
645
646 // Unfreeze the logic in handleEvent()
647 if (primaryEventFiber.state == Fiber.State.HOLD) {
648 primaryEventFiber.call();
649 }
650 }
651 */
652 }
653
654 /**
655 * Switch to the next window.
656 *
657 * @param forward if true, then switch to the next window in the list,
658 * otherwise switch to the previous window in the list
659 */
660 public final void switchWindow(final boolean forward) {
661 /*
662 TODO
663
664 // Only switch if there are multiple windows
665 if (windows.length < 2) {
666 return;
667 }
668
669 // Swap z/active between active window and the next in the
670 // list
671 ptrdiff_t activeWindowI = -1;
672 for (auto i = 0; i < windows.length; i++) {
673 if (windows[i].active) {
674 activeWindowI = i;
675 break;
676 }
677 }
678 assert(activeWindowI >= 0);
679
680 // Do not switch if a window is modal
681 if (windows[activeWindowI].isModal()) {
682 return;
683 }
684
685 size_t nextWindowI;
686 if (forward) {
687 nextWindowI = (activeWindowI + 1) % windows.length;
688 } else {
689 if (activeWindowI == 0) {
690 nextWindowI = windows.length - 1;
691 } else {
692 nextWindowI = activeWindowI - 1;
693 }
694 }
695 windows[activeWindowI].active = false;
696 windows[activeWindowI].z = windows[nextWindowI].z;
697 windows[nextWindowI].z = 0;
698 windows[nextWindowI].active = true;
699
700 // Refresh
701 repaint = true;
702 */
703 }
704
705 /**
706 * Add a window to my window list and make it active.
707 *
708 * @param window new window to add
709 */
710 public final void addWindow(final TWindow window) {
711 // Do not allow a modal window to spawn a non-modal window
712 if ((windows.size() > 0) && (windows.get(0).isModal())) {
713 assert (window.isModal());
714 }
715 for (TWindow w: windows) {
716 w.active = false;
717 w.setZ(w.getZ() + 1);
718 }
719 windows.add(window);
720 window.active = true;
721 window.setZ(0);
722 }
723
724
725 }