Commit | Line | Data |
---|---|---|
a3b510ab NR |
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 | } |