Change build scripts
[jvcard.git] / src / com / googlecode / lanterna / gui2 / MultiWindowTextGUI.java
1 /*
2 * This file is part of lanterna (http://code.google.com/p/lanterna/).
3 *
4 * lanterna is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU Lesser General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU Lesser General Public License for more details.
13 *
14 * You should have received a copy of the GNU Lesser General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 *
17 * Copyright (C) 2010-2015 Martin
18 */
19 package com.googlecode.lanterna.gui2;
20
21 import com.googlecode.lanterna.TextCharacter;
22 import com.googlecode.lanterna.graphics.BasicTextImage;
23 import com.googlecode.lanterna.graphics.TextImage;
24 import com.googlecode.lanterna.input.KeyStroke;
25 import com.googlecode.lanterna.input.KeyType;
26 import com.googlecode.lanterna.screen.Screen;
27 import com.googlecode.lanterna.TerminalPosition;
28 import com.googlecode.lanterna.TerminalSize;
29 import com.googlecode.lanterna.TextColor;
30 import com.googlecode.lanterna.screen.VirtualScreen;
31
32 import java.io.EOFException;
33 import java.io.IOException;
34 import java.util.*;
35
36 /**
37 * This is the main Text GUI implementation built into Lanterna, supporting multiple tiled windows and a dynamic
38 * background area that can be fully customized. If you want to create a text-based GUI with windows and controls,
39 * it's very likely this is what you want to use.
40 *
41 * @author Martin
42 */
43 public class MultiWindowTextGUI extends AbstractTextGUI implements WindowBasedTextGUI {
44 private final VirtualScreen virtualScreen;
45 private final WindowManager windowManager;
46 private final BasePane backgroundPane;
47 private final List<Window> windows;
48 private final IdentityHashMap<Window, TextImage> windowRenderBufferCache;
49 private final WindowPostRenderer postRenderer;
50
51 private Window activeWindow;
52 private boolean eofWhenNoWindows;
53
54 /**
55 * Creates a new {@code MultiWindowTextGUI} that uses the specified {@code Screen} as the backend for all drawing
56 * operations. The screen will be automatically wrapped in a {@code VirtualScreen} in order to deal with GUIs
57 * becoming too big to fit the terminal. The background area of the GUI will be solid blue.
58 * @param screen Screen to use as the backend for drawing operations
59 */
60 public MultiWindowTextGUI(Screen screen) {
61 this(screen, TextColor.ANSI.BLUE);
62 }
63
64 /**
65 * Creates a new {@code MultiWindowTextGUI} that uses the specified {@code Screen} as the backend for all drawing
66 * operations. The screen will be automatically wrapped in a {@code VirtualScreen} in order to deal with GUIs
67 * becoming too big to fit the terminal. The background area of the GUI will be solid blue
68 * @param guiThreadFactory Factory implementation to use when creating the {@code TextGUIThread}
69 * @param screen Screen to use as the backend for drawing operations
70 */
71 public MultiWindowTextGUI(TextGUIThreadFactory guiThreadFactory, Screen screen) {
72 this(guiThreadFactory,
73 screen,
74 new DefaultWindowManager(),
75 new WindowShadowRenderer(),
76 new EmptySpace(TextColor.ANSI.BLUE));
77 }
78
79 /**
80 * Creates a new {@code MultiWindowTextGUI} that uses the specified {@code Screen} as the backend for all drawing
81 * operations. The screen will be automatically wrapped in a {@code VirtualScreen} in order to deal with GUIs
82 * becoming too big to fit the terminal. The background area of the GUI is a solid color as decided by the
83 * {@code backgroundColor} parameter.
84 * @param screen Screen to use as the backend for drawing operations
85 * @param backgroundColor Color to use for the GUI background
86 */
87 public MultiWindowTextGUI(
88 Screen screen,
89 TextColor backgroundColor) {
90
91 this(screen, new DefaultWindowManager(), new EmptySpace(backgroundColor));
92 }
93
94 /**
95 * Creates a new {@code MultiWindowTextGUI} that uses the specified {@code Screen} as the backend for all drawing
96 * operations. The screen will be automatically wrapped in a {@code VirtualScreen} in order to deal with GUIs
97 * becoming too big to fit the terminal. The background area of the GUI is the component passed in as the
98 * {@code background} parameter, forced to full size.
99 * @param screen Screen to use as the backend for drawing operations
100 * @param windowManager Window manager implementation to use
101 * @param background Component to use as the background of the GUI, behind all the windows
102 */
103 public MultiWindowTextGUI(
104 Screen screen,
105 WindowManager windowManager,
106 Component background) {
107
108 this(screen, windowManager, new WindowShadowRenderer(), background);
109 }
110
111 /**
112 * Creates a new {@code MultiWindowTextGUI} that uses the specified {@code Screen} as the backend for all drawing
113 * operations. The screen will be automatically wrapped in a {@code VirtualScreen} in order to deal with GUIs
114 * becoming too big to fit the terminal. The background area of the GUI is the component passed in as the
115 * {@code background} parameter, forced to full size.
116 * @param screen Screen to use as the backend for drawing operations
117 * @param windowManager Window manager implementation to use
118 * @param postRenderer {@code WindowPostRenderer} object to invoke after each window has been drawn
119 * @param background Component to use as the background of the GUI, behind all the windows
120 */
121 public MultiWindowTextGUI(
122 Screen screen,
123 WindowManager windowManager,
124 WindowPostRenderer postRenderer,
125 Component background) {
126
127 this(new SameTextGUIThread.Factory(), screen, windowManager, postRenderer, background);
128 }
129
130 /**
131 * Creates a new {@code MultiWindowTextGUI} that uses the specified {@code Screen} as the backend for all drawing
132 * operations. The screen will be automatically wrapped in a {@code VirtualScreen} in order to deal with GUIs
133 * becoming too big to fit the terminal. The background area of the GUI is the component passed in as the
134 * {@code background} parameter, forced to full size.
135 * @param guiThreadFactory Factory implementation to use when creating the {@code TextGUIThread}
136 * @param screen Screen to use as the backend for drawing operations
137 * @param windowManager Window manager implementation to use
138 * @param postRenderer {@code WindowPostRenderer} object to invoke after each window has been drawn
139 * @param background Component to use as the background of the GUI, behind all the windows
140 */
141 public MultiWindowTextGUI(
142 TextGUIThreadFactory guiThreadFactory,
143 Screen screen,
144 WindowManager windowManager,
145 WindowPostRenderer postRenderer,
146 Component background) {
147
148 this(guiThreadFactory, new VirtualScreen(screen), windowManager, postRenderer, background);
149 }
150
151 private MultiWindowTextGUI(
152 TextGUIThreadFactory guiThreadFactory,
153 VirtualScreen screen,
154 WindowManager windowManager,
155 WindowPostRenderer postRenderer,
156 Component background) {
157
158 super(guiThreadFactory, screen);
159 if(windowManager == null) {
160 throw new IllegalArgumentException("Creating a window-based TextGUI requires a WindowManager");
161 }
162 if(background == null) {
163 //Use a sensible default instead of throwing
164 background = new EmptySpace(TextColor.ANSI.BLUE);
165 }
166 this.virtualScreen = screen;
167 this.windowManager = windowManager;
168 this.backgroundPane = new AbstractBasePane() {
169 @Override
170 public TextGUI getTextGUI() {
171 return MultiWindowTextGUI.this;
172 }
173
174 @Override
175 public TerminalPosition toGlobal(TerminalPosition localPosition) {
176 return localPosition;
177 }
178
179 public TerminalPosition fromGlobal(TerminalPosition globalPosition) {
180 return globalPosition;
181 }
182 };
183 this.backgroundPane.setComponent(background);
184 this.windows = new LinkedList<Window>();
185 this.windowRenderBufferCache = new IdentityHashMap<Window, TextImage>();
186 this.postRenderer = postRenderer;
187 this.eofWhenNoWindows = false;
188 }
189
190 @Override
191 public synchronized boolean isPendingUpdate() {
192 for(Window window: windows) {
193 if(window.isInvalid()) {
194 return true;
195 }
196 }
197 return super.isPendingUpdate() || backgroundPane.isInvalid() || windowManager.isInvalid();
198 }
199
200 @Override
201 public synchronized void updateScreen() throws IOException {
202 TerminalSize minimumTerminalSize = TerminalSize.ZERO;
203 for(Window window: windows) {
204 if(window.isVisible()) {
205 if (window.getHints().contains(Window.Hint.FULL_SCREEN) ||
206 window.getHints().contains(Window.Hint.FIT_TERMINAL_WINDOW) ||
207 window.getHints().contains(Window.Hint.EXPANDED)) {
208 //Don't take full screen windows or auto-sized windows into account
209 continue;
210 }
211 TerminalPosition lastPosition = window.getPosition();
212 minimumTerminalSize = minimumTerminalSize.max(
213 //Add position to size to get the bottom-right corner of the window
214 window.getDecoratedSize().withRelative(
215 Math.max(lastPosition.getColumn(), 0),
216 Math.max(lastPosition.getRow(), 0)));
217 }
218 }
219 virtualScreen.setMinimumSize(minimumTerminalSize);
220 super.updateScreen();
221 }
222
223 @Override
224 protected synchronized KeyStroke readKeyStroke() throws IOException {
225 KeyStroke keyStroke = super.pollInput();
226 if(eofWhenNoWindows && keyStroke == null && windows.isEmpty()) {
227 return new KeyStroke(KeyType.EOF);
228 }
229 else if(keyStroke != null) {
230 return keyStroke;
231 }
232 else {
233 return super.readKeyStroke();
234 }
235 }
236
237 @Override
238 protected synchronized void drawGUI(TextGUIGraphics graphics) {
239 backgroundPane.draw(graphics);
240 getWindowManager().prepareWindows(this, Collections.unmodifiableList(windows), graphics.getSize());
241 for(Window window: windows) {
242 if (window.isVisible()) {
243 // First draw windows to a buffer, then copy it to the real destination. This is to make physical off-screen
244 // drawing work better. Store the buffers in a cache so we don't have to re-create them every time.
245 TextImage textImage = windowRenderBufferCache.get(window);
246 if (textImage == null || !textImage.getSize().equals(window.getDecoratedSize())) {
247 textImage = new BasicTextImage(window.getDecoratedSize());
248 windowRenderBufferCache.put(window, textImage);
249 }
250 TextGUIGraphics windowGraphics = new TextGUIGraphics(this, textImage.newTextGraphics(), graphics.getTheme());
251
252 TerminalPosition contentOffset = TerminalPosition.TOP_LEFT_CORNER;
253 if (!window.getHints().contains(Window.Hint.NO_DECORATIONS)) {
254 WindowDecorationRenderer decorationRenderer = getWindowManager().getWindowDecorationRenderer(window);
255 windowGraphics = decorationRenderer.draw(this, windowGraphics, window);
256 contentOffset = decorationRenderer.getOffset(window);
257 }
258
259 window.draw(windowGraphics);
260 window.setContentOffset(contentOffset);
261 Borders.joinLinesWithFrame(windowGraphics);
262
263 graphics.drawImage(window.getPosition(), textImage);
264
265 if (postRenderer != null && !window.getHints().contains(Window.Hint.NO_POST_RENDERING)) {
266 postRenderer.postRender(graphics, this, window);
267 }
268 }
269 }
270
271 // Purge the render buffer cache from windows that have been removed
272 windowRenderBufferCache.keySet().retainAll(windows);
273 }
274
275 @Override
276 public synchronized TerminalPosition getCursorPosition() {
277 Window activeWindow = getActiveWindow();
278 if(activeWindow != null) {
279 return activeWindow.toGlobal(activeWindow.getCursorPosition());
280 }
281 else {
282 return backgroundPane.getCursorPosition();
283 }
284 }
285
286 /**
287 * Sets whether the TextGUI should return EOF when you try to read input while there are no windows in the window
288 * manager. Setting this to true (on by default) will make the GUI automatically exit when the last window has been
289 * closed.
290 * @param eofWhenNoWindows Should the GUI return EOF when there are no windows left
291 */
292 public void setEOFWhenNoWindows(boolean eofWhenNoWindows) {
293 this.eofWhenNoWindows = eofWhenNoWindows;
294 }
295
296 /**
297 * Returns whether the TextGUI should return EOF when you try to read input while there are no windows in the window
298 * manager. When this is true (true by default) will make the GUI automatically exit when the last window has been
299 * closed.
300 * @return Should the GUI return EOF when there are no windows left
301 */
302 public boolean isEOFWhenNoWindows() {
303 return eofWhenNoWindows;
304 }
305
306 @Override
307 public synchronized Interactable getFocusedInteractable() {
308 Window activeWindow = getActiveWindow();
309 if(activeWindow != null) {
310 return activeWindow.getFocusedInteractable();
311 }
312 else {
313 return backgroundPane.getFocusedInteractable();
314 }
315 }
316
317 @Override
318 public synchronized boolean handleInput(KeyStroke keyStroke) {
319 Window activeWindow = getActiveWindow();
320 if(activeWindow != null) {
321 return activeWindow.handleInput(keyStroke);
322 }
323 else {
324 return backgroundPane.handleInput(keyStroke);
325 }
326 }
327
328 @Override
329 public WindowManager getWindowManager() {
330 return windowManager;
331 }
332
333 @Override
334 public synchronized WindowBasedTextGUI addWindow(Window window) {
335 //To protect against NPE if the user forgot to set a content component
336 if(window.getComponent() == null) {
337 window.setComponent(new EmptySpace(TerminalSize.ONE));
338 }
339
340 if(window.getTextGUI() != null) {
341 window.getTextGUI().removeWindow(window);
342 }
343 window.setTextGUI(this);
344 windowManager.onAdded(this, window, windows);
345 if(!windows.contains(window)) {
346 windows.add(window);
347 }
348 if(!window.getHints().contains(Window.Hint.NO_FOCUS)) {
349 setActiveWindow(window);
350 }
351 invalidate();
352 return this;
353 }
354
355 @Override
356 public WindowBasedTextGUI addWindowAndWait(Window window) {
357 addWindow(window);
358 window.waitUntilClosed();
359 return this;
360 }
361
362 @Override
363 public synchronized WindowBasedTextGUI removeWindow(Window window) {
364 if(!windows.remove(window)) {
365 //Didn't contain this window
366 return this;
367 }
368 window.setTextGUI(null);
369 windowManager.onRemoved(this, window, windows);
370 if(activeWindow == window) {
371 //Go backward in reverse and find the first suitable window
372 for(int index = windows.size() - 1; index >= 0; index--) {
373 Window candidate = windows.get(index);
374 if(!candidate.getHints().contains(Window.Hint.NO_FOCUS)) {
375 setActiveWindow(candidate);
376 break;
377 }
378 }
379 }
380 invalidate();
381 return this;
382 }
383
384 @Override
385 public void waitForWindowToClose(Window window) {
386 while(window.getTextGUI() != null) {
387 boolean sleep = true;
388 TextGUIThread guiThread = getGUIThread();
389 if(Thread.currentThread() == guiThread.getThread()) {
390 try {
391 sleep = !guiThread.processEventsAndUpdate();
392 }
393 catch(EOFException ignore) {
394 //The GUI has closed so allow exit
395 break;
396 }
397 catch(IOException e) {
398 throw new RuntimeException("Unexpected IOException while waiting for window to close", e);
399 }
400 }
401 if(sleep) {
402 try {
403 Thread.sleep(1);
404 }
405 catch(InterruptedException ignore) {}
406 }
407 }
408 }
409
410 @Override
411 public synchronized Collection<Window> getWindows() {
412 return Collections.unmodifiableList(new ArrayList<Window>(windows));
413 }
414
415 @Override
416 public synchronized MultiWindowTextGUI setActiveWindow(Window activeWindow) {
417 this.activeWindow = activeWindow;
418 return this;
419 }
420
421 @Override
422 public synchronized Window getActiveWindow() {
423 return activeWindow;
424 }
425
426 @Override
427 public BasePane getBackgroundPane() {
428 return backgroundPane;
429 }
430
431 @Override
432 public Screen getScreen() {
433 return virtualScreen;
434 }
435
436 @Override
437 public WindowPostRenderer getWindowPostRenderer() {
438 return postRenderer;
439 }
440
441 @Override
442 public synchronized WindowBasedTextGUI moveToTop(Window window) {
443 if(!windows.contains(window)) {
444 throw new IllegalArgumentException("Window " + window + " isn't in MultiWindowTextGUI " + this);
445 }
446 windows.remove(window);
447 windows.add(window);
448 invalidate();
449 return this;
450 }
451
452 /**
453 * Switches the active window by cyclically shuffling the window list. If {@code reverse} parameter is {@code false}
454 * then the current top window is placed at the bottom of the stack and the window immediately behind it is the new
455 * top. If {@code reverse} is set to {@code true} then the window at the bottom of the stack is moved up to the
456 * front and the previous top window will be immediately below it
457 * @param reverse Direction to cycle through the windows
458 * @return Itself
459 */
460 public synchronized WindowBasedTextGUI cycleActiveWindow(boolean reverse) {
461 if(windows.isEmpty() || windows.size() == 1 || activeWindow.getHints().contains(Window.Hint.MODAL)) {
462 return this;
463 }
464 Window originalActiveWindow = activeWindow;
465 Window nextWindow = getNextWindow(reverse, originalActiveWindow);
466 while(nextWindow.getHints().contains(Window.Hint.NO_FOCUS)) {
467 nextWindow = getNextWindow(reverse, nextWindow);
468 if(nextWindow == originalActiveWindow) {
469 return this;
470 }
471 }
472
473 if(reverse) {
474 moveToTop(nextWindow);
475 }
476 else {
477 windows.remove(originalActiveWindow);
478 windows.add(0, originalActiveWindow);
479 }
480 setActiveWindow(nextWindow);
481 return this;
482 }
483
484 private Window getNextWindow(boolean reverse, Window window) {
485 int index = windows.indexOf(window);
486 if(reverse) {
487 if(++index >= windows.size()) {
488 index = 0;
489 }
490 }
491 else {
492 if(--index < 0) {
493 index = windows.size() - 1;
494 }
495 }
496 return windows.get(index);
497 }
498 }