TWindow compiles
[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;
48e27807 49import jexer.io.Screen;
4328bb42
KL
50import static jexer.TCommand.*;
51import static jexer.TKeypress.*;
52
7d4115a5
KL
53/**
54 * TApplication sets up a full Text User Interface application.
55 */
56public class TApplication {
57
58 /**
4328bb42
KL
59 * Access to the physical screen, keyboard, and mouse.
60 */
7b5261bc 61 private Backend backend;
4328bb42 62
48e27807
KL
63 /**
64 * Get the Screen.
65 *
66 * @return the Screen
67 */
68 public final Screen getScreen() {
69 return backend.getScreen();
70 }
71
4328bb42 72 /**
7b5261bc 73 * Actual mouse coordinate X.
4328bb42
KL
74 */
75 private int mouseX;
76
77 /**
7b5261bc 78 * Actual mouse coordinate Y.
4328bb42
KL
79 */
80 private int mouseY;
81
82 /**
7b5261bc 83 * Event queue that will be drained by either primary or secondary Fiber.
4328bb42
KL
84 */
85 private List<TInputEvent> eventQueue;
86
87 /**
88 * Windows and widgets pull colors from this ColorTheme.
89 */
7b5261bc
KL
90 private ColorTheme theme;
91
92 /**
93 * Get the color theme.
94 *
95 * @return the theme
96 */
97 public final ColorTheme getTheme() {
98 return theme;
99 }
4328bb42
KL
100
101 /**
102 * When true, exit the application.
103 */
48e27807 104 private boolean quit = false;
4328bb42
KL
105
106 /**
107 * When true, repaint the entire screen.
108 */
48e27807
KL
109 private boolean repaint = true;
110
111 /**
112 * Request full repaint on next screen refresh.
113 */
114 public void setRepaint() {
115 repaint = true;
116 }
4328bb42
KL
117
118 /**
119 * When true, just flush updates from the screen.
7d4115a5 120 */
48e27807 121 private boolean flush = false;
4328bb42
KL
122
123 /**
7b5261bc
KL
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
126 * toolbars.
4328bb42 127 */
48e27807
KL
128 private static final int desktopTop = 1;
129
130 /**
131 * Get Y coordinate of the top edge of the desktop.
132 *
133 * @return Y coordinate of the top edge of the desktop
134 */
135 public final int getDesktopTop() {
136 return desktopTop;
137 }
4328bb42
KL
138
139 /**
140 * Y coordinate of the bottom edge of the desktop.
141 */
48e27807
KL
142 private int desktopBottom;
143
144 /**
145 * Get Y coordinate of the bottom edge of the desktop.
146 *
147 * @return Y coordinate of the bottom edge of the desktop
148 */
149 public final int getDesktopBottom() {
150 return desktopBottom;
151 }
4328bb42
KL
152
153 /**
154 * Public constructor.
155 *
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
162 * encoding.
7b5261bc
KL
163 * @throws UnsupportedEncodingException if an exception is thrown when
164 * creating the InputStreamReader
4328bb42 165 */
7b5261bc
KL
166 public TApplication(final InputStream input,
167 final OutputStream output) throws UnsupportedEncodingException {
4328bb42 168
7b5261bc
KL
169 backend = new ECMA48Backend(input, output);
170 theme = new ColorTheme();
171 desktopBottom = backend.getScreen().getHeight() - 1;
172 eventQueue = new LinkedList<TInputEvent>();
4328bb42
KL
173 }
174
175 /**
176 * Invert the cell at the mouse pointer position.
177 */
178 private void drawMouse() {
7b5261bc
KL
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);
183 flush = true;
184
185 /*
186 if (windows.length == 0) {
187 repaint = true;
188 }
189 */
190 // TODO: remove this repaint after the above if (windows.length == 0)
191 // can be used again.
192 repaint = true;
4328bb42
KL
193 }
194
195 /**
196 * Draw everything.
197 */
7b5261bc
KL
198 public final void drawAll() {
199 if ((flush) && (!repaint)) {
200 backend.flushScreen();
201 flush = false;
202 return;
203 }
204
205 if (!repaint) {
206 return;
207 }
208
209 // If true, the cursor is not visible
210 boolean cursor = false;
211
212 // Start with a clean screen
213 backend.getScreen().clear();
214
215 // Draw the background
216 CellAttributes background = theme.getColor("tapplication.background");
217 backend.getScreen().putAll(GraphicsChars.HATCH, background);
218
219 /*
220 // Draw each window in reverse Z order
221 TWindow [] sorted = windows.dup;
222 sorted.sort.reverse;
223 foreach (w; sorted) {
224 w.drawChildren();
225 }
226
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.
233 int x = 1;
234 foreach (m; menus) {
235 CellAttributes menuColor;
236 CellAttributes menuMnemonicColor;
237 if (m.active) {
238 menuColor = theme.getColor("tmenu.highlighted");
239 menuMnemonicColor = theme.getColor("tmenu.mnemonic.highlighted");
240 } else {
241 menuColor = theme.getColor("tmenu");
242 menuMnemonicColor = theme.getColor("tmenu.mnemonic");
243 }
244 // Draw the menu title
245 backend.getScreen().hLineXY(x, 0, cast(int)m.title.length + 2, ' ',
246 menuColor);
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);
251
252 if (m.active) {
253 m.drawChildren();
254 // Reset the screen clipping so we can draw the next title.
255 backend.getScreen().resetClipping();
256 }
257 x += m.title.length + 2;
258 }
259
260 foreach (m; subMenus) {
261 // Reset the screen clipping so we can draw the next sub-menu.
262 backend.getScreen().resetClipping();
263 m.drawChildren();
264 }
265 */
266
267 // Draw the mouse pointer
268 drawMouse();
269
270 /*
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());
278 cursor = true;
279 }
280 }
281
282 // Kill the cursor
283 if (cursor == false) {
284 backend.getScreen().hideCursor();
285 }
286 */
287
288 // Flush the screen contents
289 backend.flushScreen();
290
291 repaint = false;
292 flush = false;
4328bb42
KL
293 }
294
295 /**
7b5261bc 296 * Run this application until it exits.
4328bb42
KL
297 */
298 public final void run() {
7b5261bc
KL
299 List<TInputEvent> events = new LinkedList<TInputEvent>();
300
301 while (!quit) {
302 // Timeout is in milliseconds, so default timeout after 1 second
303 // of inactivity.
304 int timeout = getSleepTime(1000);
305
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.
309 timeout = 0;
310 }
311
312 // Pull any pending input events
313 backend.getEvents(events, timeout);
314 metaHandleEvents(events);
315 events.clear();
316
317 // Process timers and call doIdle()'s
318 doIdle();
319
320 // Update the screen
321 drawAll();
322 }
323
324 /*
325
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();
334 }
335 }
336
337 if (primaryEventFiber.state == Fiber.State.HOLD) {
338 // Wake up the primary handler so that it can exit.
339 primaryEventFiber.call();
340 }
341 */
342
343 backend.shutdown();
4328bb42
KL
344 }
345
346 /**
347 * Peek at certain application-level events, add to eventQueue, and wake
348 * up the consuming Fiber.
349 *
350 * @param events the input events to consume
351 */
7b5261bc
KL
352 private void metaHandleEvents(final List<TInputEvent> events) {
353
354 for (TInputEvent event: events) {
355
356 /*
357 System.err.printf(String.format("metaHandleEvents event: %s\n",
358 event)); System.err.flush();
359 */
360
361 if (quit) {
362 // Do no more processing if the application is already trying
363 // to exit.
364 return;
365 }
366
367 // DEBUG
368 if (event instanceof TKeypressEvent) {
369 TKeypressEvent keypress = (TKeypressEvent) event;
b299e69c 370 if (keypress.equals(kbAltX)) {
7b5261bc
KL
371 quit = true;
372 return;
373 }
374 }
375 // DEBUG
376
377 // Special application-wide events -------------------------------
378
379 // Abort everything
380 if (event instanceof TCommandEvent) {
381 TCommandEvent command = (TCommandEvent) event;
382 if (command.getCmd().equals(cmAbort)) {
383 quit = true;
384 return;
385 }
386 }
387
388 // Screen resize
389 if (event instanceof TResizeEvent) {
390 TResizeEvent resize = (TResizeEvent) event;
391 backend.getScreen().setDimensions(resize.getWidth(),
392 resize.getHeight());
393 desktopBottom = backend.getScreen().getHeight() - 1;
394 repaint = true;
395 mouseX = 0;
396 mouseY = 0;
397 continue;
398 }
399
400 // Peek at the mouse position
401 if (event instanceof TMouseEvent) {
402 TMouseEvent mouse = (TMouseEvent) event;
d4a29741
KL
403 if ((mouseX != mouse.getX()) || (mouseY != mouse.getY())) {
404 mouseX = mouse.getX();
405 mouseY = mouse.getY();
7b5261bc
KL
406 drawMouse();
407 }
408 }
409
410 /*
411
412 // Put into the main queue
413 addEvent(event);
414
415 // Have one of the two consumer Fibers peel the events off
416 // the queue.
417 if (secondaryEventFiber !is null) {
418 assert(secondaryEventFiber.state == Fiber.State.HOLD);
419
420 // Wake up the secondary handler for these events
421 secondaryEventFiber.call();
422 } else {
423 assert(primaryEventFiber.state == Fiber.State.HOLD);
424
425 // Wake up the primary handler for these events
426 primaryEventFiber.call();
427 }
428 */
429
430 } // for (TInputEvent event: events)
4328bb42
KL
431
432 }
433
434 /**
435 * Do stuff when there is no user input.
436 */
437 private void doIdle() {
7b5261bc
KL
438 /*
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) {
444 t.tick();
445 if (t.recurring == true) {
446 keepTimers ~= t;
447 }
448 } else {
449 keepTimers ~= t;
450 }
451 }
452 timers = keepTimers;
453
454 // Call onIdle's
455 foreach (w; windows) {
456 w.onIdle();
457 }
458 */
4328bb42 459 }
7d4115a5 460
4328bb42
KL
461 /**
462 * Get the amount of time I can sleep before missing a Timer tick.
463 *
464 * @param timeout = initial (maximum) timeout
465 * @return number of milliseconds between now and the next timer event
466 */
7b5261bc
KL
467 protected int getSleepTime(final int timeout) {
468 /*
469 auto now = Clock.currTime;
470 auto sleepTime = dur!("msecs")(timeout);
471 foreach (t; timers) {
472 if (t.nextTick < now) {
473 return 0;
474 }
475 if ((t.nextTick > now) &&
476 ((t.nextTick - now) < sleepTime)
477 ) {
478 sleepTime = t.nextTick - now;
479 }
480 }
481 assert(sleepTime.total!("msecs")() >= 0);
482 return cast(uint)sleepTime.total!("msecs")();
483 */
484 // TODO: fix timers. Until then, come back after 250 millis.
485 return 250;
7d4115a5 486 }
4328bb42 487
48e27807
KL
488 /**
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.
491 *
492 * @param window the window to remove
493 */
494 public final void closeWindow(final TWindow window) {
495 /*
496 TODO
497
498 uint z = window.z;
499 window.z = -1;
500 windows.sort;
501 windows = windows[1 .. $];
502 TWindow activeWindow = null;
503 foreach (w; windows) {
504 if (w.z > z) {
505 w.z--;
506 if (w.z == 0) {
507 w.active = true;
508 assert(activeWindow is null);
509 activeWindow = w;
510 } else {
511 w.active = false;
512 }
513 }
514 }
515
516 // Perform window cleanup
517 window.onClose();
518
519 // Refresh screen
520 repaint = true;
521
522 // Check if we are closing a TMessageBox or similar
523 if (secondaryEventReceiver !is null) {
524 assert(secondaryEventFiber !is null);
525
526 // Do not send events to the secondaryEventReceiver anymore, the
527 // window is closed.
528 secondaryEventReceiver = null;
529
530 // Special case: if this is called while executing on a
531 // secondaryEventFiber, call it so that widgetEventHandler() can
532 // terminate.
533 if (secondaryEventFiber.state == Fiber.State.HOLD) {
534 secondaryEventFiber.call();
535 }
536 secondaryEventFiber = null;
537
538 // Unfreeze the logic in handleEvent()
539 if (primaryEventFiber.state == Fiber.State.HOLD) {
540 primaryEventFiber.call();
541 }
542 }
543 */
544 }
545
546 /**
547 * Switch to the next window.
548 *
549 * @param forward if true, then switch to the next window in the list,
550 * otherwise switch to the previous window in the list
551 */
552 public final void switchWindow(final boolean forward) {
553 /*
554 TODO
555
556 // Only switch if there are multiple windows
557 if (windows.length < 2) {
558 return;
559 }
560
561 // Swap z/active between active window and the next in the
562 // list
563 ptrdiff_t activeWindowI = -1;
564 for (auto i = 0; i < windows.length; i++) {
565 if (windows[i].active) {
566 activeWindowI = i;
567 break;
568 }
569 }
570 assert(activeWindowI >= 0);
571
572 // Do not switch if a window is modal
573 if (windows[activeWindowI].isModal()) {
574 return;
575 }
576
577 size_t nextWindowI;
578 if (forward) {
579 nextWindowI = (activeWindowI + 1) % windows.length;
580 } else {
581 if (activeWindowI == 0) {
582 nextWindowI = windows.length - 1;
583 } else {
584 nextWindowI = activeWindowI - 1;
585 }
586 }
587 windows[activeWindowI].active = false;
588 windows[activeWindowI].z = windows[nextWindowI].z;
589 windows[nextWindowI].z = 0;
590 windows[nextWindowI].active = true;
591
592 // Refresh
593 repaint = true;
594 */
595 }
596
597 /**
598 * Add a window to my window list and make it active.
599 *
600 * @param window new window to add
601 */
602 public final void addWindow(final TWindow window) {
603 /*
604 TODO
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());
608 }
609 foreach (w; windows) {
610 w.active = false;
611 w.z++;
612 }
613 windows ~= window;
614 window.active = true;
615 window.z = 0;
616 */
617 }
618
619
7d4115a5 620}