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
.LinkedList
;
37 import java
.util
.List
;
39 import jexer
.bits
.CellAttributes
;
40 import jexer
.bits
.ColorTheme
;
41 import jexer
.bits
.GraphicsChars
;
42 import jexer
.event
.TCommandEvent
;
43 import jexer
.event
.TInputEvent
;
44 import jexer
.event
.TKeypressEvent
;
45 import jexer
.event
.TMouseEvent
;
46 import jexer
.event
.TResizeEvent
;
47 import jexer
.backend
.Backend
;
48 import jexer
.backend
.ECMA48Backend
;
49 import jexer
.io
.Screen
;
50 import static jexer
.TCommand
.*;
51 import static jexer
.TKeypress
.*;
54 * TApplication sets up a full Text User Interface application.
56 public class TApplication
{
59 * Access to the physical screen, keyboard, and mouse.
61 private Backend backend
;
68 public final Screen
getScreen() {
69 return backend
.getScreen();
73 * Actual mouse coordinate X.
78 * Actual mouse coordinate Y.
83 * Event queue that will be drained by either primary or secondary Fiber.
85 private List
<TInputEvent
> eventQueue
;
88 * Windows and widgets pull colors from this ColorTheme.
90 private ColorTheme theme
;
93 * Get the color theme.
97 public final ColorTheme
getTheme() {
102 * When true, exit the application.
104 private boolean quit
= false;
107 * When true, repaint the entire screen.
109 private boolean repaint
= true;
112 * Request full repaint on next screen refresh.
114 public void setRepaint() {
119 * When true, just flush updates from the screen.
121 private boolean flush
= false;
124 * Y coordinate of the top edge of the desktop. For now this is a
125 * constant. Someday it would be nice to have a multi-line menu or
128 private static final int desktopTop
= 1;
131 * Get Y coordinate of the top edge of the desktop.
133 * @return Y coordinate of the top edge of the desktop
135 public final int getDesktopTop() {
140 * Y coordinate of the bottom edge of the desktop.
142 private int desktopBottom
;
145 * Get Y coordinate of the bottom edge of the desktop.
147 * @return Y coordinate of the bottom edge of the desktop
149 public final int getDesktopBottom() {
150 return desktopBottom
;
154 * Public constructor.
156 * @param input an InputStream connected to the remote user, or null for
157 * System.in. If System.in is used, then on non-Windows systems it will
158 * be put in raw mode; shutdown() will (blindly!) put System.in in cooked
159 * mode. input is always converted to a Reader with UTF-8 encoding.
160 * @param output an OutputStream connected to the remote user, or null
161 * for System.out. output is always converted to a Writer with UTF-8
163 * @throws UnsupportedEncodingException if an exception is thrown when
164 * creating the InputStreamReader
166 public TApplication(final InputStream input
,
167 final OutputStream output
) throws UnsupportedEncodingException
{
169 backend
= new ECMA48Backend(input
, output
);
170 theme
= new ColorTheme();
171 desktopBottom
= backend
.getScreen().getHeight() - 1;
172 eventQueue
= new LinkedList
<TInputEvent
>();
176 * Invert the cell at the mouse pointer position.
178 private void drawMouse() {
179 CellAttributes attr
= backend
.getScreen().getAttrXY(mouseX
, mouseY
);
180 attr
.setForeColor(attr
.getForeColor().invert());
181 attr
.setBackColor(attr
.getBackColor().invert());
182 backend
.getScreen().putAttrXY(mouseX
, mouseY
, attr
, false);
186 if (windows.length == 0) {
190 // TODO: remove this repaint after the above if (windows.length == 0)
191 // can be used again.
198 public final void drawAll() {
199 if ((flush
) && (!repaint
)) {
200 backend
.flushScreen();
209 // If true, the cursor is not visible
210 boolean cursor
= false;
212 // Start with a clean screen
213 backend
.getScreen().clear();
215 // Draw the background
216 CellAttributes background
= theme
.getColor("tapplication.background");
217 backend
.getScreen().putAll(GraphicsChars
.HATCH
, background
);
220 // Draw each window in reverse Z order
221 TWindow [] sorted = windows.dup;
223 foreach (w; sorted) {
227 // Draw the blank menubar line - reset the screen clipping first so
228 // it won't trim it out.
229 backend.getScreen().resetClipping();
230 backend.getScreen().hLineXY(0, 0, backend.getScreen().getWidth(), ' ',
231 theme.getColor("tmenu"));
232 // Now draw the menus.
235 CellAttributes menuColor;
236 CellAttributes menuMnemonicColor;
238 menuColor = theme.getColor("tmenu.highlighted");
239 menuMnemonicColor = theme.getColor("tmenu.mnemonic.highlighted");
241 menuColor = theme.getColor("tmenu");
242 menuMnemonicColor = theme.getColor("tmenu.mnemonic");
244 // Draw the menu title
245 backend.getScreen().hLineXY(x, 0, cast(int)m.title.length + 2, ' ',
247 backend.getScreen().putStrXY(x + 1, 0, m.title, menuColor);
248 // Draw the highlight character
249 backend.getScreen().putCharXY(x + 1 + m.mnemonic.shortcutIdx, 0,
250 m.mnemonic.shortcut, menuMnemonicColor);
254 // Reset the screen clipping so we can draw the next title.
255 backend.getScreen().resetClipping();
257 x += m.title.length + 2;
260 foreach (m; subMenus) {
261 // Reset the screen clipping so we can draw the next sub-menu.
262 backend.getScreen().resetClipping();
267 // Draw the mouse pointer
271 // Place the cursor if it is visible
272 TWidget activeWidget = null;
273 if (sorted.length > 0) {
274 activeWidget = sorted[$ - 1].getActiveChild();
275 if (activeWidget.hasCursor) {
276 backend.getScreen().putCursor(true, activeWidget.getCursorAbsoluteX(),
277 activeWidget.getCursorAbsoluteY());
283 if (cursor == false) {
284 backend.getScreen().hideCursor();
288 // Flush the screen contents
289 backend
.flushScreen();
296 * Run this application until it exits.
298 public final void run() {
299 List
<TInputEvent
> events
= new LinkedList
<TInputEvent
>();
302 // Timeout is in milliseconds, so default timeout after 1 second
304 int timeout
= getSleepTime(1000);
306 if (eventQueue
.size() > 0) {
307 // Do not wait if there are definitely events waiting to be
308 // processed or a screen redraw to do.
312 // Pull any pending input events
313 backend
.getEvents(events
, timeout
);
314 metaHandleEvents(events
);
317 // Process timers and call doIdle()'s
326 // Shutdown the fibers
327 eventQueue.length = 0;
328 if (secondaryEventFiber !is null) {
329 assert(secondaryEventReceiver !is null);
330 secondaryEventReceiver = null;
331 if (secondaryEventFiber.state == Fiber.State.HOLD) {
332 // Wake up the secondary handler so that it can exit.
333 secondaryEventFiber.call();
337 if (primaryEventFiber.state == Fiber.State.HOLD) {
338 // Wake up the primary handler so that it can exit.
339 primaryEventFiber.call();
347 * Peek at certain application-level events, add to eventQueue, and wake
348 * up the consuming Fiber.
350 * @param events the input events to consume
352 private void metaHandleEvents(final List
<TInputEvent
> events
) {
354 for (TInputEvent event
: events
) {
357 System.err.printf(String.format("metaHandleEvents event: %s\n",
358 event)); System.err.flush();
362 // Do no more processing if the application is already trying
368 if (event
instanceof TKeypressEvent
) {
369 TKeypressEvent keypress
= (TKeypressEvent
) event
;
370 if (keypress
.equals(kbAltX
)) {
377 // Special application-wide events -------------------------------
380 if (event
instanceof TCommandEvent
) {
381 TCommandEvent command
= (TCommandEvent
) event
;
382 if (command
.getCmd().equals(cmAbort
)) {
389 if (event
instanceof TResizeEvent
) {
390 TResizeEvent resize
= (TResizeEvent
) event
;
391 backend
.getScreen().setDimensions(resize
.getWidth(),
393 desktopBottom
= backend
.getScreen().getHeight() - 1;
400 // Peek at the mouse position
401 if (event
instanceof TMouseEvent
) {
402 TMouseEvent mouse
= (TMouseEvent
) event
;
403 if ((mouseX
!= mouse
.getX()) || (mouseY
!= mouse
.getY())) {
404 mouseX
= mouse
.getX();
405 mouseY
= mouse
.getY();
412 // Put into the main queue
415 // Have one of the two consumer Fibers peel the events off
417 if (secondaryEventFiber !is null) {
418 assert(secondaryEventFiber.state == Fiber.State.HOLD);
420 // Wake up the secondary handler for these events
421 secondaryEventFiber.call();
423 assert(primaryEventFiber.state == Fiber.State.HOLD);
425 // Wake up the primary handler for these events
426 primaryEventFiber.call();
430 } // for (TInputEvent event: events)
435 * Do stuff when there is no user input.
437 private void doIdle() {
439 // Now run any timers that have timed out
440 auto now = Clock.currTime;
441 TTimer [] keepTimers;
442 foreach (t; timers) {
443 if (t.nextTick < now) {
445 if (t.recurring == true) {
455 foreach (w; windows) {
462 * Get the amount of time I can sleep before missing a Timer tick.
464 * @param timeout = initial (maximum) timeout
465 * @return number of milliseconds between now and the next timer event
467 protected int getSleepTime(final int timeout
) {
469 auto now = Clock.currTime;
470 auto sleepTime = dur!("msecs")(timeout);
471 foreach (t; timers) {
472 if (t.nextTick < now) {
475 if ((t.nextTick > now) &&
476 ((t.nextTick - now) < sleepTime)
478 sleepTime = t.nextTick - now;
481 assert(sleepTime.total!("msecs")() >= 0);
482 return cast(uint)sleepTime.total!("msecs")();
484 // TODO: fix timers. Until then, come back after 250 millis.
489 * Close window. Note that the window's destructor is NOT called by this
490 * method, instead the GC is assumed to do the cleanup.
492 * @param window the window to remove
494 public final void closeWindow(final TWindow window
) {
501 windows = windows[1 .. $];
502 TWindow activeWindow = null;
503 foreach (w; windows) {
508 assert(activeWindow is null);
516 // Perform window cleanup
522 // Check if we are closing a TMessageBox or similar
523 if (secondaryEventReceiver !is null) {
524 assert(secondaryEventFiber !is null);
526 // Do not send events to the secondaryEventReceiver anymore, the
528 secondaryEventReceiver = null;
530 // Special case: if this is called while executing on a
531 // secondaryEventFiber, call it so that widgetEventHandler() can
533 if (secondaryEventFiber.state == Fiber.State.HOLD) {
534 secondaryEventFiber.call();
536 secondaryEventFiber = null;
538 // Unfreeze the logic in handleEvent()
539 if (primaryEventFiber.state == Fiber.State.HOLD) {
540 primaryEventFiber.call();
547 * Switch to the next window.
549 * @param forward if true, then switch to the next window in the list,
550 * otherwise switch to the previous window in the list
552 public final void switchWindow(final boolean forward
) {
556 // Only switch if there are multiple windows
557 if (windows.length < 2) {
561 // Swap z/active between active window and the next in the
563 ptrdiff_t activeWindowI = -1;
564 for (auto i = 0; i < windows.length; i++) {
565 if (windows[i].active) {
570 assert(activeWindowI >= 0);
572 // Do not switch if a window is modal
573 if (windows[activeWindowI].isModal()) {
579 nextWindowI = (activeWindowI + 1) % windows.length;
581 if (activeWindowI == 0) {
582 nextWindowI = windows.length - 1;
584 nextWindowI = activeWindowI - 1;
587 windows[activeWindowI].active = false;
588 windows[activeWindowI].z = windows[nextWindowI].z;
589 windows[nextWindowI].z = 0;
590 windows[nextWindowI].active = true;
598 * Add a window to my window list and make it active.
600 * @param window new window to add
602 public final void addWindow(final TWindow window
) {
605 // Do not allow a modal window to spawn a non-modal window
606 if ((windows.length > 0) && (windows[0].isModal())) {
607 assert(window.isModal());
609 foreach (w; windows) {
614 window.active = true;