2 * Jexer - Java Text User Interface - demonstration program
6 * Author: Kevin Lamonte, <a href="mailto:kevin.lamonte@gmail.com">kevin.lamonte@gmail.com</a>
8 * License: LGPLv3 or later
10 * Copyright: This module is licensed under the GNU Lesser General
11 * Public License Version 3. Please see the file "COPYING" in this
12 * directory for more information about the GNU Lesser General Public
15 * Copyright (C) 2015 Kevin Lamonte
17 * This program is free software; you can redistribute it and/or
18 * modify it under the terms of the GNU Lesser General Public License
19 * as published by the Free Software Foundation; either version 3 of
20 * the License, or (at your option) any later version.
22 * This program is distributed in the hope that it will be useful, but
23 * WITHOUT ANY WARRANTY; without even the implied warranty of
24 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
25 * General Public License for more details.
27 * You should have received a copy of the GNU Lesser General Public
28 * License along with this program; if not, see
29 * http://www.gnu.org/licenses/, or write to the Free Software
30 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
35 import java
.io
.InputStream
;
36 import java
.io
.OutputStream
;
37 import java
.io
.UnsupportedEncodingException
;
38 import java
.util
.LinkedList
;
39 import java
.util
.List
;
41 import jexer
.bits
.CellAttributes
;
42 import jexer
.bits
.ColorTheme
;
43 import jexer
.bits
.GraphicsChars
;
44 import jexer
.event
.TCommandEvent
;
45 import jexer
.event
.TInputEvent
;
46 import jexer
.event
.TKeypressEvent
;
47 import jexer
.event
.TMouseEvent
;
48 import jexer
.event
.TResizeEvent
;
49 import jexer
.backend
.Backend
;
50 import jexer
.backend
.ECMA48Backend
;
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 public Backend backend
;
65 * Actual mouse coordinate X
70 * Actual mouse coordinate Y
75 * Event queue that will be drained by either primary or secondary Fiber
77 private List
<TInputEvent
> eventQueue
;
80 * Windows and widgets pull colors from this ColorTheme.
82 public ColorTheme theme
;
85 * When true, exit the application.
87 public boolean quit
= false;
90 * When true, repaint the entire screen.
92 public boolean repaint
= true;
95 * When true, just flush updates from the screen.
97 public boolean flush
= false;
100 * Y coordinate of the top edge of the desktop.
102 static public final int desktopTop
= 1;
105 * Y coordinate of the bottom edge of the desktop.
107 public int desktopBottom
;
110 * Public constructor.
112 * @param input an InputStream connected to the remote user, or null for
113 * System.in. If System.in is used, then on non-Windows systems it will
114 * be put in raw mode; shutdown() will (blindly!) put System.in in cooked
115 * mode. input is always converted to a Reader with UTF-8 encoding.
116 * @param output an OutputStream connected to the remote user, or null
117 * for System.out. output is always converted to a Writer with UTF-8
120 public TApplication(InputStream input
, OutputStream output
) throws UnsupportedEncodingException
{
122 backend
= new ECMA48Backend(input
, output
);
123 theme
= new ColorTheme();
124 desktopBottom
= backend
.screen
.getHeight() - 1;
125 eventQueue
= new LinkedList
<TInputEvent
>();
129 * Invert the cell at the mouse pointer position.
131 private void drawMouse() {
132 CellAttributes attr
= backend
.screen
.getAttrXY(mouseX
, mouseY
);
133 attr
.foreColor
= attr
.foreColor
.invert();
134 attr
.backColor
= attr
.backColor
.invert();
135 backend
.screen
.putAttrXY(mouseX
, mouseY
, attr
, false);
139 if (windows.length == 0) {
143 // TODO: remove this repaint after the above if (windows.length == 0)
144 // can be used again.
151 final public void drawAll() {
152 if ((flush
) && (!repaint
)) {
153 backend
.flushScreen();
162 // If true, the cursor is not visible
163 boolean cursor
= false;
165 // Start with a clean screen
166 backend
.screen
.clear();
168 // Draw the background
169 CellAttributes background
= theme
.getColor("tapplication.background");
170 backend
.screen
.putAll(GraphicsChars
.HATCH
, background
);
173 // Draw each window in reverse Z order
174 TWindow [] sorted = windows.dup;
176 foreach (w; sorted) {
180 // Draw the blank menubar line - reset the screen clipping first so
181 // it won't trim it out.
182 backend.screen.resetClipping();
183 backend.screen.hLineXY(0, 0, backend.screen.getWidth(), ' ',
184 theme.getColor("tmenu"));
185 // Now draw the menus.
188 CellAttributes menuColor;
189 CellAttributes menuMnemonicColor;
191 menuColor = theme.getColor("tmenu.highlighted");
192 menuMnemonicColor = theme.getColor("tmenu.mnemonic.highlighted");
194 menuColor = theme.getColor("tmenu");
195 menuMnemonicColor = theme.getColor("tmenu.mnemonic");
197 // Draw the menu title
198 backend.screen.hLineXY(x, 0, cast(int)m.title.length + 2, ' ',
200 backend.screen.putStrXY(x + 1, 0, m.title, menuColor);
201 // Draw the highlight character
202 backend.screen.putCharXY(x + 1 + m.mnemonic.shortcutIdx, 0,
203 m.mnemonic.shortcut, menuMnemonicColor);
207 // Reset the screen clipping so we can draw the next title.
208 backend.screen.resetClipping();
210 x += m.title.length + 2;
213 foreach (m; subMenus) {
214 // Reset the screen clipping so we can draw the next sub-menu.
215 backend.screen.resetClipping();
220 // Draw the mouse pointer
224 // Place the cursor if it is visible
225 TWidget activeWidget = null;
226 if (sorted.length > 0) {
227 activeWidget = sorted[$ - 1].getActiveChild();
228 if (activeWidget.hasCursor) {
229 backend.screen.putCursor(true, activeWidget.getCursorAbsoluteX(),
230 activeWidget.getCursorAbsoluteY());
236 if (cursor == false) {
237 backend.screen.hideCursor();
241 // Flush the screen contents
242 backend
.flushScreen();
249 * Run this application until it exits, using stdin and stdout
251 public final void run() {
252 List
<TInputEvent
> events
= new LinkedList
<TInputEvent
>();
254 while (quit
== false) {
255 // Timeout is in milliseconds, so default timeout after 1 second
257 int timeout
= getSleepTime(1000);
259 if (eventQueue
.size() > 0) {
260 // Do not wait if there are definitely events waiting to be
261 // processed or a screen redraw to do.
265 // Pull any pending input events
266 backend
.getEvents(events
, timeout
);
267 metaHandleEvents(events
);
270 // Process timers and call doIdle()'s
279 // Shutdown the fibers
280 eventQueue.length = 0;
281 if (secondaryEventFiber !is null) {
282 assert(secondaryEventReceiver !is null);
283 secondaryEventReceiver = null;
284 if (secondaryEventFiber.state == Fiber.State.HOLD) {
285 // Wake up the secondary handler so that it can exit.
286 secondaryEventFiber.call();
290 if (primaryEventFiber.state == Fiber.State.HOLD) {
291 // Wake up the primary handler so that it can exit.
292 primaryEventFiber.call();
300 * Peek at certain application-level events, add to eventQueue, and wake
301 * up the consuming Fiber.
303 * @param events the input events to consume
305 private void metaHandleEvents(List
<TInputEvent
> events
) {
307 for (TInputEvent event
: events
) {
310 System.err.printf(String.format("metaHandleEvents event: %s\n",
311 event)); System.err.flush();
315 // Do no more processing if the application is already trying
321 if (event
instanceof TKeypressEvent
) {
322 TKeypressEvent keypress
= (TKeypressEvent
)event
;
323 if (keypress
.key
.equals(kbAltX
)) {
330 // Special application-wide events -------------------------------
333 if (event
instanceof TCommandEvent
) {
334 TCommandEvent command
= (TCommandEvent
)event
;
335 if (command
.cmd
.equals(cmAbort
)) {
342 if (event
instanceof TResizeEvent
) {
343 TResizeEvent resize
= (TResizeEvent
)event
;
344 backend
.screen
.setDimensions(resize
.width
, resize
.height
);
345 desktopBottom
= backend
.screen
.getHeight() - 1;
352 // Peek at the mouse position
353 if (event
instanceof TMouseEvent
) {
354 TMouseEvent mouse
= (TMouseEvent
)event
;
355 if ((mouseX
!= mouse
.x
) || (mouseY
!= mouse
.y
)) {
364 // Put into the main queue
367 // Have one of the two consumer Fibers peel the events off
369 if (secondaryEventFiber !is null) {
370 assert(secondaryEventFiber.state == Fiber.State.HOLD);
372 // Wake up the secondary handler for these events
373 secondaryEventFiber.call();
375 assert(primaryEventFiber.state == Fiber.State.HOLD);
377 // Wake up the primary handler for these events
378 primaryEventFiber.call();
382 } // for (TInputEvent event: events)
387 * Do stuff when there is no user input.
389 private void doIdle() {
391 // Now run any timers that have timed out
392 auto now = Clock.currTime;
393 TTimer [] keepTimers;
394 foreach (t; timers) {
395 if (t.nextTick < now) {
397 if (t.recurring == true) {
407 foreach (w; windows) {
414 * Get the amount of time I can sleep before missing a Timer tick.
416 * @param timeout = initial (maximum) timeout
417 * @return number of milliseconds between now and the next timer event
419 protected int getSleepTime(int timeout
) {
421 auto now = Clock.currTime;
422 auto sleepTime = dur!("msecs")(timeout);
423 foreach (t; timers) {
424 if (t.nextTick < now) {
427 if ((t.nextTick > now) &&
428 ((t.nextTick - now) < sleepTime)
430 sleepTime = t.nextTick - now;
433 assert(sleepTime.total!("msecs")() >= 0);
434 return cast(uint)sleepTime.total!("msecs")();
436 // TODO: fix timers. Until then, come back after 250 millis.