2 * Jexer - Java Text User Interface
4 * License: LGPLv3 or later
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.
10 * Copyright (C) 2015 Kevin Lamonte
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.
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.
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
28 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
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
;
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
.*;
55 * TApplication sets up a full Text User Interface application.
57 public class TApplication
{
60 * Access to the physical screen, keyboard, and mouse.
62 private Backend backend
;
69 public final Screen
getScreen() {
70 return backend
.getScreen();
74 * Actual mouse coordinate X.
79 * Actual mouse coordinate Y.
84 * Event queue that will be drained by either primary or secondary Fiber.
86 private List
<TInputEvent
> eventQueue
;
89 * Windows and widgets pull colors from this ColorTheme.
91 private ColorTheme theme
;
94 * Get the color theme.
98 public final ColorTheme
getTheme() {
103 * The top-level windows (but not menus).
105 List
<TWindow
> windows
;
108 * When true, exit the application.
110 private boolean quit
= false;
113 * When true, repaint the entire screen.
115 private boolean repaint
= true;
118 * Request full repaint on next screen refresh.
120 public void setRepaint() {
125 * When true, just flush updates from the screen.
127 private boolean flush
= false;
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
134 private static final int desktopTop
= 1;
137 * Get Y coordinate of the top edge of the desktop.
139 * @return Y coordinate of the top edge of the desktop
141 public final int getDesktopTop() {
146 * Y coordinate of the bottom edge of the desktop.
148 private int desktopBottom
;
151 * Get Y coordinate of the bottom edge of the desktop.
153 * @return Y coordinate of the bottom edge of the desktop
155 public final int getDesktopBottom() {
156 return desktopBottom
;
160 * Public constructor.
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
169 * @throws UnsupportedEncodingException if an exception is thrown when
170 * creating the InputStreamReader
172 public TApplication(final InputStream input
,
173 final OutputStream output
) throws UnsupportedEncodingException
{
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
>();
183 * Invert the cell at the mouse pointer position.
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);
192 if (windows
.size() == 0) {
200 public final void drawAll() {
201 if ((flush
) && (!repaint
)) {
202 backend
.flushScreen();
211 // If true, the cursor is not visible
212 boolean cursor
= false;
214 // Start with a clean screen
217 // Draw the background
218 CellAttributes background
= theme
.getColor("tapplication.background");
219 getScreen().putAll(GraphicsChars
.HATCH
, background
);
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();
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.
237 for (TMenu m: menus) {
238 CellAttributes menuColor;
239 CellAttributes menuMnemonicColor;
241 menuColor = theme.getColor("tmenu.highlighted");
242 menuMnemonicColor = theme.getColor("tmenu.mnemonic.highlighted");
244 menuColor = theme.getColor("tmenu");
245 menuMnemonicColor = theme.getColor("tmenu.mnemonic");
247 // Draw the menu title
248 getScreen().hLineXY(x, 0, menu.title.length() + 2, ' ',
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);
257 // Reset the screen clipping so we can draw the next title.
258 getScreen().resetClipping();
260 x += menu.title.length + 2;
263 for (TMenu menu: subMenus) {
264 // Reset the screen clipping so we can draw the next sub-menu.
265 getScreen().resetClipping();
270 // Draw the mouse pointer
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());
285 if (cursor
== false) {
286 getScreen().hideCursor();
289 // Flush the screen contents
290 backend
.flushScreen();
297 * Run this application until it exits.
299 public final void run() {
300 List
<TInputEvent
> events
= new LinkedList
<TInputEvent
>();
303 // Timeout is in milliseconds, so default timeout after 1 second
305 int timeout
= getSleepTime(1000);
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.
313 // Pull any pending input events
314 backend
.getEvents(events
, timeout
);
315 metaHandleEvents(events
);
318 // Process timers and call doIdle()'s
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();
338 if (primaryEventFiber.state == Fiber.State.HOLD) {
339 // Wake up the primary handler so that it can exit.
340 primaryEventFiber.call();
348 * Peek at certain application-level events, add to eventQueue, and wake
349 * up the consuming Fiber.
351 * @param events the input events to consume
353 private void metaHandleEvents(final List
<TInputEvent
> events
) {
355 for (TInputEvent event
: events
) {
358 System.err.printf(String.format("metaHandleEvents event: %s\n",
359 event)); System.err.flush();
363 // Do no more processing if the application is already trying
369 if (event
instanceof TKeypressEvent
) {
370 TKeypressEvent keypress
= (TKeypressEvent
) event
;
371 if (keypress
.equals(kbAltX
)) {
378 // Special application-wide events -------------------------------
381 if (event
instanceof TCommandEvent
) {
382 TCommandEvent command
= (TCommandEvent
) event
;
383 if (command
.getCmd().equals(cmAbort
)) {
390 if (event
instanceof TResizeEvent
) {
391 TResizeEvent resize
= (TResizeEvent
) event
;
392 getScreen().setDimensions(resize
.getWidth(),
394 desktopBottom
= getScreen().getHeight() - 1;
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();
411 // TODO: change to two separate threads
416 // Put into the main queue
419 // Have one of the two consumer Fibers peel the events off
421 if (secondaryEventFiber !is null) {
422 assert(secondaryEventFiber.state == Fiber.State.HOLD);
424 // Wake up the secondary handler for these events
425 secondaryEventFiber.call();
427 assert(primaryEventFiber.state == Fiber.State.HOLD);
429 // Wake up the primary handler for these events
430 primaryEventFiber.call();
434 } // for (TInputEvent event: events)
439 * Dispatch one event to the appropriate widget or application-level
442 * @param event the input event to consume
444 private final void handleEvent(TInputEvent event
) {
447 // std.stdio.stderr.writefln("Handle event: %s", event);
449 // Special application-wide events -----------------------------------
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);
457 // Handle menu events
458 if ((activeMenu !is null) && (!cast(TCommandEvent)event)) {
459 TMenu menu = activeMenu;
460 if (auto mouse = cast(TMouseEvent)event) {
462 while (subMenus.length > 0) {
463 TMenu subMenu = subMenus[$ - 1];
464 if (subMenu.mouseWouldHit(mouse)) {
467 if ((mouse.type == TMouseEvent.Type.MOUSE_MOTION) &&
471 (!mouse.mouseWheelUp) &&
472 (!mouse.mouseWheelDown)
476 // We navigated away from a sub-menu, so close it
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];
489 menu.handleEvent(event);
493 if (auto keypress = cast(TKeypressEvent)event) {
494 // See if this key matches an accelerator, and if so dispatch the
496 TKeypress keypressLowercase = toLower(keypress.key);
497 TMenuItem *item = (keypressLowercase in accelerators);
499 // Let the menu item dispatch
503 // Handle the keypress
504 if (onKeypress(keypress)) {
510 if (auto cmd = cast(TCommandEvent)event) {
511 if (onCommand(cmd)) {
516 if (auto menu = cast(TMenuEvent)event) {
523 // Dispatch events to the active window -------------------------------
524 for (TWindow window
: windows
) {
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
);
534 // System.err("TApplication dispatch event: %s\n", event);
535 window
.handleEvent(event
);
542 * Do stuff when there is no user input.
544 private void doIdle() {
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) {
553 if (t.recurring == true) {
563 foreach (w; windows) {
570 * Get the amount of time I can sleep before missing a Timer tick.
572 * @param timeout = initial (maximum) timeout
573 * @return number of milliseconds between now and the next timer event
575 protected int getSleepTime(final int timeout
) {
577 auto now = Clock.currTime;
578 auto sleepTime = dur!("msecs")(timeout);
579 foreach (t; timers) {
580 if (t.nextTick < now) {
583 if ((t.nextTick > now) &&
584 ((t.nextTick - now) < sleepTime)
586 sleepTime = t.nextTick - now;
589 assert(sleepTime.total!("msecs")() >= 0);
590 return cast(uint)sleepTime.total!("msecs")();
592 // TODO: fix timers. Until then, come back after 250 millis.
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.
600 * @param window the window to remove
602 public final void closeWindow(final TWindow window
) {
609 windows = windows[1 .. $];
610 TWindow activeWindow = null;
611 foreach (w; windows) {
616 assert(activeWindow is null);
624 // Perform window cleanup
630 // Check if we are closing a TMessageBox or similar
631 if (secondaryEventReceiver !is null) {
632 assert(secondaryEventFiber !is null);
634 // Do not send events to the secondaryEventReceiver anymore, the
636 secondaryEventReceiver = null;
638 // Special case: if this is called while executing on a
639 // secondaryEventFiber, call it so that widgetEventHandler() can
641 if (secondaryEventFiber.state == Fiber.State.HOLD) {
642 secondaryEventFiber.call();
644 secondaryEventFiber = null;
646 // Unfreeze the logic in handleEvent()
647 if (primaryEventFiber.state == Fiber.State.HOLD) {
648 primaryEventFiber.call();
655 * Switch to the next window.
657 * @param forward if true, then switch to the next window in the list,
658 * otherwise switch to the previous window in the list
660 public final void switchWindow(final boolean forward
) {
664 // Only switch if there are multiple windows
665 if (windows.length < 2) {
669 // Swap z/active between active window and the next in the
671 ptrdiff_t activeWindowI = -1;
672 for (auto i = 0; i < windows.length; i++) {
673 if (windows[i].active) {
678 assert(activeWindowI >= 0);
680 // Do not switch if a window is modal
681 if (windows[activeWindowI].isModal()) {
687 nextWindowI = (activeWindowI + 1) % windows.length;
689 if (activeWindowI == 0) {
690 nextWindowI = windows.length - 1;
692 nextWindowI = activeWindowI - 1;
695 windows[activeWindowI].active = false;
696 windows[activeWindowI].z = windows[nextWindowI].z;
697 windows[nextWindowI].z = 0;
698 windows[nextWindowI].active = true;
706 * Add a window to my window list and make it active.
708 * @param window new window to add
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());
715 for (TWindow w
: windows
) {
717 w
.setZ(w
.getZ() + 1);
720 window
.active
= true;