2 * This file is part of lanterna (http://code.google.com/p/lanterna/).
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.
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.
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/>.
17 * Copyright (C) 2010-2015 Martin
19 package com
.googlecode
.lanterna
.gui2
;
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
;
32 import java
.io
.EOFException
;
33 import java
.io
.IOException
;
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.
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
;
51 private Window activeWindow
;
52 private boolean eofWhenNoWindows
;
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
60 public MultiWindowTextGUI(Screen screen
) {
61 this(screen
, TextColor
.ANSI
.BLUE
);
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
71 public MultiWindowTextGUI(TextGUIThreadFactory guiThreadFactory
, Screen screen
) {
72 this(guiThreadFactory
,
74 new DefaultWindowManager(),
75 new WindowShadowRenderer(),
76 new EmptySpace(TextColor
.ANSI
.BLUE
));
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
87 public MultiWindowTextGUI(
89 TextColor backgroundColor
) {
91 this(screen
, new DefaultWindowManager(), new EmptySpace(backgroundColor
));
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
103 public MultiWindowTextGUI(
105 WindowManager windowManager
,
106 Component background
) {
108 this(screen
, windowManager
, new WindowShadowRenderer(), background
);
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
121 public MultiWindowTextGUI(
123 WindowManager windowManager
,
124 WindowPostRenderer postRenderer
,
125 Component background
) {
127 this(new SameTextGUIThread
.Factory(), screen
, windowManager
, postRenderer
, background
);
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
141 public MultiWindowTextGUI(
142 TextGUIThreadFactory guiThreadFactory
,
144 WindowManager windowManager
,
145 WindowPostRenderer postRenderer
,
146 Component background
) {
148 this(guiThreadFactory
, new VirtualScreen(screen
), windowManager
, postRenderer
, background
);
151 private MultiWindowTextGUI(
152 TextGUIThreadFactory guiThreadFactory
,
153 VirtualScreen screen
,
154 WindowManager windowManager
,
155 WindowPostRenderer postRenderer
,
156 Component background
) {
158 super(guiThreadFactory
, screen
);
159 if(windowManager
== null) {
160 throw new IllegalArgumentException("Creating a window-based TextGUI requires a WindowManager");
162 if(background
== null) {
163 //Use a sensible default instead of throwing
164 background
= new EmptySpace(TextColor
.ANSI
.BLUE
);
166 this.virtualScreen
= screen
;
167 this.windowManager
= windowManager
;
168 this.backgroundPane
= new AbstractBasePane() {
170 public TextGUI
getTextGUI() {
171 return MultiWindowTextGUI
.this;
175 public TerminalPosition
toGlobal(TerminalPosition localPosition
) {
176 return localPosition
;
179 public TerminalPosition
fromGlobal(TerminalPosition globalPosition
) {
180 return globalPosition
;
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;
191 public synchronized boolean isPendingUpdate() {
192 for(Window window
: windows
) {
193 if(window
.isInvalid()) {
197 return super.isPendingUpdate() || backgroundPane
.isInvalid() || windowManager
.isInvalid();
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
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)));
219 virtualScreen
.setMinimumSize(minimumTerminalSize
);
220 super.updateScreen();
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
);
229 else if(keyStroke
!= null) {
233 return super.readKeyStroke();
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
);
250 TextGUIGraphics windowGraphics
= new TextGUIGraphics(this, textImage
.newTextGraphics(), graphics
.getTheme());
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
);
259 window
.draw(windowGraphics
);
260 window
.setContentOffset(contentOffset
);
261 Borders
.joinLinesWithFrame(windowGraphics
);
263 graphics
.drawImage(window
.getPosition(), textImage
);
265 if (postRenderer
!= null && !window
.getHints().contains(Window
.Hint
.NO_POST_RENDERING
)) {
266 postRenderer
.postRender(graphics
, this, window
);
271 // Purge the render buffer cache from windows that have been removed
272 windowRenderBufferCache
.keySet().retainAll(windows
);
276 public synchronized TerminalPosition
getCursorPosition() {
277 Window activeWindow
= getActiveWindow();
278 if(activeWindow
!= null) {
279 return activeWindow
.toGlobal(activeWindow
.getCursorPosition());
282 return backgroundPane
.getCursorPosition();
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
290 * @param eofWhenNoWindows Should the GUI return EOF when there are no windows left
292 public void setEOFWhenNoWindows(boolean eofWhenNoWindows
) {
293 this.eofWhenNoWindows
= eofWhenNoWindows
;
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
300 * @return Should the GUI return EOF when there are no windows left
302 public boolean isEOFWhenNoWindows() {
303 return eofWhenNoWindows
;
307 public synchronized Interactable
getFocusedInteractable() {
308 Window activeWindow
= getActiveWindow();
309 if(activeWindow
!= null) {
310 return activeWindow
.getFocusedInteractable();
313 return backgroundPane
.getFocusedInteractable();
318 public synchronized boolean handleInput(KeyStroke keyStroke
) {
319 Window activeWindow
= getActiveWindow();
320 if(activeWindow
!= null) {
321 return activeWindow
.handleInput(keyStroke
);
324 return backgroundPane
.handleInput(keyStroke
);
329 public WindowManager
getWindowManager() {
330 return windowManager
;
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
));
340 if(window
.getTextGUI() != null) {
341 window
.getTextGUI().removeWindow(window
);
343 window
.setTextGUI(this);
344 windowManager
.onAdded(this, window
, windows
);
345 if(!windows
.contains(window
)) {
348 if(!window
.getHints().contains(Window
.Hint
.NO_FOCUS
)) {
349 setActiveWindow(window
);
356 public WindowBasedTextGUI
addWindowAndWait(Window window
) {
358 window
.waitUntilClosed();
363 public synchronized WindowBasedTextGUI
removeWindow(Window window
) {
364 if(!windows
.remove(window
)) {
365 //Didn't contain this window
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
);
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()) {
391 sleep
= !guiThread
.processEventsAndUpdate();
393 catch(EOFException ignore
) {
394 //The GUI has closed so allow exit
397 catch(IOException e
) {
398 throw new RuntimeException("Unexpected IOException while waiting for window to close", e
);
405 catch(InterruptedException ignore
) {}
411 public synchronized Collection
<Window
> getWindows() {
412 return Collections
.unmodifiableList(new ArrayList
<Window
>(windows
));
416 public synchronized MultiWindowTextGUI
setActiveWindow(Window activeWindow
) {
417 this.activeWindow
= activeWindow
;
422 public synchronized Window
getActiveWindow() {
427 public BasePane
getBackgroundPane() {
428 return backgroundPane
;
432 public Screen
getScreen() {
433 return virtualScreen
;
437 public WindowPostRenderer
getWindowPostRenderer() {
442 public synchronized WindowBasedTextGUI
moveToTop(Window window
) {
443 if(!windows
.contains(window
)) {
444 throw new IllegalArgumentException("Window " + window
+ " isn't in MultiWindowTextGUI " + this);
446 windows
.remove(window
);
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
460 public synchronized WindowBasedTextGUI
cycleActiveWindow(boolean reverse
) {
461 if(windows
.isEmpty() || windows
.size() == 1 || activeWindow
.getHints().contains(Window
.Hint
.MODAL
)) {
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
) {
474 moveToTop(nextWindow
);
477 windows
.remove(originalActiveWindow
);
478 windows
.add(0, originalActiveWindow
);
480 setActiveWindow(nextWindow
);
484 private Window
getNextWindow(boolean reverse
, Window window
) {
485 int index
= windows
.indexOf(window
);
487 if(++index
>= windows
.size()) {
493 index
= windows
.size() - 1;
496 return windows
.get(index
);