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 static jexer
.TCommand
.*;
50 import static jexer
.TKeypress
.*;
53 * TApplication sets up a full Text User Interface application.
55 public class TApplication
{
58 * Access to the physical screen, keyboard, and mouse.
60 private Backend backend
;
63 * Actual mouse coordinate X.
68 * Actual mouse coordinate Y.
73 * Event queue that will be drained by either primary or secondary Fiber.
75 private List
<TInputEvent
> eventQueue
;
78 * Windows and widgets pull colors from this ColorTheme.
80 private ColorTheme theme
;
83 * Get the color theme.
87 public final ColorTheme
getTheme() {
92 * When true, exit the application.
94 public boolean quit
= false;
97 * When true, repaint the entire screen.
99 public boolean repaint
= true;
102 * When true, just flush updates from the screen.
104 public boolean flush
= false;
107 * Y coordinate of the top edge of the desktop. For now this is a
108 * constant. Someday it would be nice to have a multi-line menu or
111 public static final int desktopTop
= 1;
114 * Y coordinate of the bottom edge of the desktop.
116 public int desktopBottom
;
119 * Public constructor.
121 * @param input an InputStream connected to the remote user, or null for
122 * System.in. If System.in is used, then on non-Windows systems it will
123 * be put in raw mode; shutdown() will (blindly!) put System.in in cooked
124 * mode. input is always converted to a Reader with UTF-8 encoding.
125 * @param output an OutputStream connected to the remote user, or null
126 * for System.out. output is always converted to a Writer with UTF-8
128 * @throws UnsupportedEncodingException if an exception is thrown when
129 * creating the InputStreamReader
131 public TApplication(final InputStream input
,
132 final OutputStream output
) throws UnsupportedEncodingException
{
134 backend
= new ECMA48Backend(input
, output
);
135 theme
= new ColorTheme();
136 desktopBottom
= backend
.getScreen().getHeight() - 1;
137 eventQueue
= new LinkedList
<TInputEvent
>();
141 * Invert the cell at the mouse pointer position.
143 private void drawMouse() {
144 CellAttributes attr
= backend
.getScreen().getAttrXY(mouseX
, mouseY
);
145 attr
.setForeColor(attr
.getForeColor().invert());
146 attr
.setBackColor(attr
.getBackColor().invert());
147 backend
.getScreen().putAttrXY(mouseX
, mouseY
, attr
, false);
151 if (windows.length == 0) {
155 // TODO: remove this repaint after the above if (windows.length == 0)
156 // can be used again.
163 public final void drawAll() {
164 if ((flush
) && (!repaint
)) {
165 backend
.flushScreen();
174 // If true, the cursor is not visible
175 boolean cursor
= false;
177 // Start with a clean screen
178 backend
.getScreen().clear();
180 // Draw the background
181 CellAttributes background
= theme
.getColor("tapplication.background");
182 backend
.getScreen().putAll(GraphicsChars
.HATCH
, background
);
185 // Draw each window in reverse Z order
186 TWindow [] sorted = windows.dup;
188 foreach (w; sorted) {
192 // Draw the blank menubar line - reset the screen clipping first so
193 // it won't trim it out.
194 backend.getScreen().resetClipping();
195 backend.getScreen().hLineXY(0, 0, backend.getScreen().getWidth(), ' ',
196 theme.getColor("tmenu"));
197 // Now draw the menus.
200 CellAttributes menuColor;
201 CellAttributes menuMnemonicColor;
203 menuColor = theme.getColor("tmenu.highlighted");
204 menuMnemonicColor = theme.getColor("tmenu.mnemonic.highlighted");
206 menuColor = theme.getColor("tmenu");
207 menuMnemonicColor = theme.getColor("tmenu.mnemonic");
209 // Draw the menu title
210 backend.getScreen().hLineXY(x, 0, cast(int)m.title.length + 2, ' ',
212 backend.getScreen().putStrXY(x + 1, 0, m.title, menuColor);
213 // Draw the highlight character
214 backend.getScreen().putCharXY(x + 1 + m.mnemonic.shortcutIdx, 0,
215 m.mnemonic.shortcut, menuMnemonicColor);
219 // Reset the screen clipping so we can draw the next title.
220 backend.getScreen().resetClipping();
222 x += m.title.length + 2;
225 foreach (m; subMenus) {
226 // Reset the screen clipping so we can draw the next sub-menu.
227 backend.getScreen().resetClipping();
232 // Draw the mouse pointer
236 // Place the cursor if it is visible
237 TWidget activeWidget = null;
238 if (sorted.length > 0) {
239 activeWidget = sorted[$ - 1].getActiveChild();
240 if (activeWidget.hasCursor) {
241 backend.getScreen().putCursor(true, activeWidget.getCursorAbsoluteX(),
242 activeWidget.getCursorAbsoluteY());
248 if (cursor == false) {
249 backend.getScreen().hideCursor();
253 // Flush the screen contents
254 backend
.flushScreen();
261 * Run this application until it exits.
263 public final void run() {
264 List
<TInputEvent
> events
= new LinkedList
<TInputEvent
>();
267 // Timeout is in milliseconds, so default timeout after 1 second
269 int timeout
= getSleepTime(1000);
271 if (eventQueue
.size() > 0) {
272 // Do not wait if there are definitely events waiting to be
273 // processed or a screen redraw to do.
277 // Pull any pending input events
278 backend
.getEvents(events
, timeout
);
279 metaHandleEvents(events
);
282 // Process timers and call doIdle()'s
291 // Shutdown the fibers
292 eventQueue.length = 0;
293 if (secondaryEventFiber !is null) {
294 assert(secondaryEventReceiver !is null);
295 secondaryEventReceiver = null;
296 if (secondaryEventFiber.state == Fiber.State.HOLD) {
297 // Wake up the secondary handler so that it can exit.
298 secondaryEventFiber.call();
302 if (primaryEventFiber.state == Fiber.State.HOLD) {
303 // Wake up the primary handler so that it can exit.
304 primaryEventFiber.call();
312 * Peek at certain application-level events, add to eventQueue, and wake
313 * up the consuming Fiber.
315 * @param events the input events to consume
317 private void metaHandleEvents(final List
<TInputEvent
> events
) {
319 for (TInputEvent event
: events
) {
322 System.err.printf(String.format("metaHandleEvents event: %s\n",
323 event)); System.err.flush();
327 // Do no more processing if the application is already trying
333 if (event
instanceof TKeypressEvent
) {
334 TKeypressEvent keypress
= (TKeypressEvent
) event
;
335 if (keypress
.key
.equals(kbAltX
)) {
342 // Special application-wide events -------------------------------
345 if (event
instanceof TCommandEvent
) {
346 TCommandEvent command
= (TCommandEvent
) event
;
347 if (command
.getCmd().equals(cmAbort
)) {
354 if (event
instanceof TResizeEvent
) {
355 TResizeEvent resize
= (TResizeEvent
) event
;
356 backend
.getScreen().setDimensions(resize
.getWidth(),
358 desktopBottom
= backend
.getScreen().getHeight() - 1;
365 // Peek at the mouse position
366 if (event
instanceof TMouseEvent
) {
367 TMouseEvent mouse
= (TMouseEvent
) event
;
368 if ((mouseX
!= mouse
.x
) || (mouseY
!= mouse
.y
)) {
377 // Put into the main queue
380 // Have one of the two consumer Fibers peel the events off
382 if (secondaryEventFiber !is null) {
383 assert(secondaryEventFiber.state == Fiber.State.HOLD);
385 // Wake up the secondary handler for these events
386 secondaryEventFiber.call();
388 assert(primaryEventFiber.state == Fiber.State.HOLD);
390 // Wake up the primary handler for these events
391 primaryEventFiber.call();
395 } // for (TInputEvent event: events)
400 * Do stuff when there is no user input.
402 private void doIdle() {
404 // Now run any timers that have timed out
405 auto now = Clock.currTime;
406 TTimer [] keepTimers;
407 foreach (t; timers) {
408 if (t.nextTick < now) {
410 if (t.recurring == true) {
420 foreach (w; windows) {
427 * Get the amount of time I can sleep before missing a Timer tick.
429 * @param timeout = initial (maximum) timeout
430 * @return number of milliseconds between now and the next timer event
432 protected int getSleepTime(final int timeout
) {
434 auto now = Clock.currTime;
435 auto sleepTime = dur!("msecs")(timeout);
436 foreach (t; timers) {
437 if (t.nextTick < now) {
440 if ((t.nextTick > now) &&
441 ((t.nextTick - now) < sleepTime)
443 sleepTime = t.nextTick - now;
446 assert(sleepTime.total!("msecs")() >= 0);
447 return cast(uint)sleepTime.total!("msecs")();
449 // TODO: fix timers. Until then, come back after 250 millis.