immutable TMouseEvent
[fanfix.git] / src / jexer / TApplication.java
CommitLineData
7d4115a5 1/**
7b5261bc 2 * Jexer - Java Text User Interface
7d4115a5
KL
3 *
4 * License: LGPLv3 or later
5 *
7b5261bc
KL
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.
7d4115a5
KL
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
7b5261bc
KL
27 *
28 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
29 * @version 1
7d4115a5
KL
30 */
31package jexer;
32
4328bb42
KL
33import java.io.InputStream;
34import java.io.OutputStream;
35import java.io.UnsupportedEncodingException;
36import java.util.LinkedList;
37import java.util.List;
38
39import jexer.bits.CellAttributes;
40import jexer.bits.ColorTheme;
41import jexer.bits.GraphicsChars;
42import jexer.event.TCommandEvent;
43import jexer.event.TInputEvent;
44import jexer.event.TKeypressEvent;
45import jexer.event.TMouseEvent;
46import jexer.event.TResizeEvent;
47import jexer.backend.Backend;
48import jexer.backend.ECMA48Backend;
49import static jexer.TCommand.*;
50import static jexer.TKeypress.*;
51
7d4115a5
KL
52/**
53 * TApplication sets up a full Text User Interface application.
54 */
55public class TApplication {
56
57 /**
4328bb42
KL
58 * Access to the physical screen, keyboard, and mouse.
59 */
7b5261bc 60 private Backend backend;
4328bb42
KL
61
62 /**
7b5261bc 63 * Actual mouse coordinate X.
4328bb42
KL
64 */
65 private int mouseX;
66
67 /**
7b5261bc 68 * Actual mouse coordinate Y.
4328bb42
KL
69 */
70 private int mouseY;
71
72 /**
7b5261bc 73 * Event queue that will be drained by either primary or secondary Fiber.
4328bb42
KL
74 */
75 private List<TInputEvent> eventQueue;
76
77 /**
78 * Windows and widgets pull colors from this ColorTheme.
79 */
7b5261bc
KL
80 private ColorTheme theme;
81
82 /**
83 * Get the color theme.
84 *
85 * @return the theme
86 */
87 public final ColorTheme getTheme() {
88 return theme;
89 }
4328bb42
KL
90
91 /**
92 * When true, exit the application.
93 */
94 public boolean quit = false;
95
96 /**
97 * When true, repaint the entire screen.
98 */
99 public boolean repaint = true;
100
101 /**
102 * When true, just flush updates from the screen.
7d4115a5 103 */
4328bb42
KL
104 public boolean flush = false;
105
106 /**
7b5261bc
KL
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
109 * toolbars.
4328bb42 110 */
7b5261bc 111 public static final int desktopTop = 1;
4328bb42
KL
112
113 /**
114 * Y coordinate of the bottom edge of the desktop.
115 */
116 public int desktopBottom;
117
118 /**
119 * Public constructor.
120 *
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
127 * encoding.
7b5261bc
KL
128 * @throws UnsupportedEncodingException if an exception is thrown when
129 * creating the InputStreamReader
4328bb42 130 */
7b5261bc
KL
131 public TApplication(final InputStream input,
132 final OutputStream output) throws UnsupportedEncodingException {
4328bb42 133
7b5261bc
KL
134 backend = new ECMA48Backend(input, output);
135 theme = new ColorTheme();
136 desktopBottom = backend.getScreen().getHeight() - 1;
137 eventQueue = new LinkedList<TInputEvent>();
4328bb42
KL
138 }
139
140 /**
141 * Invert the cell at the mouse pointer position.
142 */
143 private void drawMouse() {
7b5261bc
KL
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);
148 flush = true;
149
150 /*
151 if (windows.length == 0) {
152 repaint = true;
153 }
154 */
155 // TODO: remove this repaint after the above if (windows.length == 0)
156 // can be used again.
157 repaint = true;
4328bb42
KL
158 }
159
160 /**
161 * Draw everything.
162 */
7b5261bc
KL
163 public final void drawAll() {
164 if ((flush) && (!repaint)) {
165 backend.flushScreen();
166 flush = false;
167 return;
168 }
169
170 if (!repaint) {
171 return;
172 }
173
174 // If true, the cursor is not visible
175 boolean cursor = false;
176
177 // Start with a clean screen
178 backend.getScreen().clear();
179
180 // Draw the background
181 CellAttributes background = theme.getColor("tapplication.background");
182 backend.getScreen().putAll(GraphicsChars.HATCH, background);
183
184 /*
185 // Draw each window in reverse Z order
186 TWindow [] sorted = windows.dup;
187 sorted.sort.reverse;
188 foreach (w; sorted) {
189 w.drawChildren();
190 }
191
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.
198 int x = 1;
199 foreach (m; menus) {
200 CellAttributes menuColor;
201 CellAttributes menuMnemonicColor;
202 if (m.active) {
203 menuColor = theme.getColor("tmenu.highlighted");
204 menuMnemonicColor = theme.getColor("tmenu.mnemonic.highlighted");
205 } else {
206 menuColor = theme.getColor("tmenu");
207 menuMnemonicColor = theme.getColor("tmenu.mnemonic");
208 }
209 // Draw the menu title
210 backend.getScreen().hLineXY(x, 0, cast(int)m.title.length + 2, ' ',
211 menuColor);
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);
216
217 if (m.active) {
218 m.drawChildren();
219 // Reset the screen clipping so we can draw the next title.
220 backend.getScreen().resetClipping();
221 }
222 x += m.title.length + 2;
223 }
224
225 foreach (m; subMenus) {
226 // Reset the screen clipping so we can draw the next sub-menu.
227 backend.getScreen().resetClipping();
228 m.drawChildren();
229 }
230 */
231
232 // Draw the mouse pointer
233 drawMouse();
234
235 /*
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());
243 cursor = true;
244 }
245 }
246
247 // Kill the cursor
248 if (cursor == false) {
249 backend.getScreen().hideCursor();
250 }
251 */
252
253 // Flush the screen contents
254 backend.flushScreen();
255
256 repaint = false;
257 flush = false;
4328bb42
KL
258 }
259
260 /**
7b5261bc 261 * Run this application until it exits.
4328bb42
KL
262 */
263 public final void run() {
7b5261bc
KL
264 List<TInputEvent> events = new LinkedList<TInputEvent>();
265
266 while (!quit) {
267 // Timeout is in milliseconds, so default timeout after 1 second
268 // of inactivity.
269 int timeout = getSleepTime(1000);
270
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.
274 timeout = 0;
275 }
276
277 // Pull any pending input events
278 backend.getEvents(events, timeout);
279 metaHandleEvents(events);
280 events.clear();
281
282 // Process timers and call doIdle()'s
283 doIdle();
284
285 // Update the screen
286 drawAll();
287 }
288
289 /*
290
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();
299 }
300 }
301
302 if (primaryEventFiber.state == Fiber.State.HOLD) {
303 // Wake up the primary handler so that it can exit.
304 primaryEventFiber.call();
305 }
306 */
307
308 backend.shutdown();
4328bb42
KL
309 }
310
311 /**
312 * Peek at certain application-level events, add to eventQueue, and wake
313 * up the consuming Fiber.
314 *
315 * @param events the input events to consume
316 */
7b5261bc
KL
317 private void metaHandleEvents(final List<TInputEvent> events) {
318
319 for (TInputEvent event: events) {
320
321 /*
322 System.err.printf(String.format("metaHandleEvents event: %s\n",
323 event)); System.err.flush();
324 */
325
326 if (quit) {
327 // Do no more processing if the application is already trying
328 // to exit.
329 return;
330 }
331
332 // DEBUG
333 if (event instanceof TKeypressEvent) {
334 TKeypressEvent keypress = (TKeypressEvent) event;
b299e69c 335 if (keypress.equals(kbAltX)) {
7b5261bc
KL
336 quit = true;
337 return;
338 }
339 }
340 // DEBUG
341
342 // Special application-wide events -------------------------------
343
344 // Abort everything
345 if (event instanceof TCommandEvent) {
346 TCommandEvent command = (TCommandEvent) event;
347 if (command.getCmd().equals(cmAbort)) {
348 quit = true;
349 return;
350 }
351 }
352
353 // Screen resize
354 if (event instanceof TResizeEvent) {
355 TResizeEvent resize = (TResizeEvent) event;
356 backend.getScreen().setDimensions(resize.getWidth(),
357 resize.getHeight());
358 desktopBottom = backend.getScreen().getHeight() - 1;
359 repaint = true;
360 mouseX = 0;
361 mouseY = 0;
362 continue;
363 }
364
365 // Peek at the mouse position
366 if (event instanceof TMouseEvent) {
367 TMouseEvent mouse = (TMouseEvent) event;
d4a29741
KL
368 if ((mouseX != mouse.getX()) || (mouseY != mouse.getY())) {
369 mouseX = mouse.getX();
370 mouseY = mouse.getY();
7b5261bc
KL
371 drawMouse();
372 }
373 }
374
375 /*
376
377 // Put into the main queue
378 addEvent(event);
379
380 // Have one of the two consumer Fibers peel the events off
381 // the queue.
382 if (secondaryEventFiber !is null) {
383 assert(secondaryEventFiber.state == Fiber.State.HOLD);
384
385 // Wake up the secondary handler for these events
386 secondaryEventFiber.call();
387 } else {
388 assert(primaryEventFiber.state == Fiber.State.HOLD);
389
390 // Wake up the primary handler for these events
391 primaryEventFiber.call();
392 }
393 */
394
395 } // for (TInputEvent event: events)
4328bb42
KL
396
397 }
398
399 /**
400 * Do stuff when there is no user input.
401 */
402 private void doIdle() {
7b5261bc
KL
403 /*
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) {
409 t.tick();
410 if (t.recurring == true) {
411 keepTimers ~= t;
412 }
413 } else {
414 keepTimers ~= t;
415 }
416 }
417 timers = keepTimers;
418
419 // Call onIdle's
420 foreach (w; windows) {
421 w.onIdle();
422 }
423 */
4328bb42 424 }
7d4115a5 425
4328bb42
KL
426 /**
427 * Get the amount of time I can sleep before missing a Timer tick.
428 *
429 * @param timeout = initial (maximum) timeout
430 * @return number of milliseconds between now and the next timer event
431 */
7b5261bc
KL
432 protected int getSleepTime(final int timeout) {
433 /*
434 auto now = Clock.currTime;
435 auto sleepTime = dur!("msecs")(timeout);
436 foreach (t; timers) {
437 if (t.nextTick < now) {
438 return 0;
439 }
440 if ((t.nextTick > now) &&
441 ((t.nextTick - now) < sleepTime)
442 ) {
443 sleepTime = t.nextTick - now;
444 }
445 }
446 assert(sleepTime.total!("msecs")() >= 0);
447 return cast(uint)sleepTime.total!("msecs")();
448 */
449 // TODO: fix timers. Until then, come back after 250 millis.
450 return 250;
7d4115a5 451 }
4328bb42 452
7d4115a5 453}