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.TerminalPosition; | |
22 | import com.googlecode.lanterna.graphics.PropertiesTheme; | |
23 | import com.googlecode.lanterna.graphics.Theme; | |
24 | import com.googlecode.lanterna.input.KeyStroke; | |
25 | import com.googlecode.lanterna.input.KeyType; | |
26 | import com.googlecode.lanterna.screen.Screen; | |
27 | ||
28 | import java.io.EOFException; | |
29 | import java.io.FileInputStream; | |
30 | ||
31 | import java.io.IOException; | |
32 | import java.io.InputStream; | |
33 | import java.util.List; | |
34 | import java.util.Properties; | |
35 | import java.util.concurrent.CopyOnWriteArrayList; | |
36 | ||
37 | /** | |
38 | * This abstract implementation of TextGUI contains some basic management of the underlying Screen and other common code | |
39 | * that can be shared between different implementations. | |
40 | * @author Martin | |
41 | */ | |
42 | public abstract class AbstractTextGUI implements TextGUI { | |
43 | ||
44 | private final Screen screen; | |
45 | private final List<Listener> listeners; | |
46 | private boolean blockingIO; | |
47 | private boolean dirty; | |
48 | private TextGUIThread textGUIThread; | |
49 | private Theme guiTheme; | |
50 | ||
51 | /** | |
52 | * Constructor for {@code AbstractTextGUI} that requires a {@code Screen} and a factory for creating the GUI thread | |
53 | * @param textGUIThreadFactory Factory class to use for creating the {@code TextGUIThread} class | |
54 | * @param screen What underlying {@code Screen} to use for this text GUI | |
55 | */ | |
56 | protected AbstractTextGUI(TextGUIThreadFactory textGUIThreadFactory, Screen screen) { | |
57 | if(screen == null) { | |
58 | throw new IllegalArgumentException("Creating a TextGUI requires an underlying Screen"); | |
59 | } | |
60 | this.screen = screen; | |
61 | this.listeners = new CopyOnWriteArrayList<Listener>(); | |
62 | this.blockingIO = false; | |
63 | this.dirty = false; | |
64 | this.guiTheme = new PropertiesTheme(loadDefaultThemeProperties()); | |
65 | this.textGUIThread = textGUIThreadFactory.createTextGUIThread(this); | |
66 | } | |
67 | ||
68 | private static Properties loadDefaultThemeProperties() { | |
69 | Properties properties = new Properties(); | |
70 | try { | |
71 | ClassLoader classLoader = AbstractTextGUI.class.getClassLoader(); | |
72 | InputStream resourceAsStream = classLoader.getResourceAsStream("default-theme.properties"); | |
73 | if(resourceAsStream == null) { | |
74 | resourceAsStream = new FileInputStream("src/main/resources/default-theme.properties"); | |
75 | } | |
76 | properties.load(resourceAsStream); | |
77 | resourceAsStream.close(); | |
78 | return properties; | |
79 | } | |
80 | catch(IOException e) { | |
81 | return properties; | |
82 | } | |
83 | } | |
84 | ||
85 | /** | |
86 | * Reads one key from the input queue, blocking or non-blocking depending on if blocking I/O has been enabled. To | |
87 | * enable blocking I/O (disabled by default), use {@code setBlockingIO(true)}. | |
88 | * @return One piece of user input as a {@code KeyStroke} or {@code null} if blocking I/O is disabled and there was | |
89 | * no input waiting | |
90 | * @throws IOException In case of an I/O error while reading input | |
91 | */ | |
92 | protected KeyStroke readKeyStroke() throws IOException { | |
93 | return blockingIO ? screen.readInput() : pollInput(); | |
94 | } | |
95 | ||
96 | /** | |
97 | * Polls the underlying input queue for user input, returning either a {@code KeyStroke} or {@code null} | |
98 | * @return {@code KeyStroke} representing the user input or {@code null} if there was none | |
99 | * @throws IOException In case of an I/O error while reading input | |
100 | */ | |
101 | protected KeyStroke pollInput() throws IOException { | |
102 | return screen.pollInput(); | |
103 | } | |
104 | ||
105 | @Override | |
106 | public synchronized boolean processInput() throws IOException { | |
107 | boolean gotInput = false; | |
108 | KeyStroke keyStroke = readKeyStroke(); | |
109 | if(keyStroke != null) { | |
110 | gotInput = true; | |
111 | do { | |
112 | if (keyStroke.getKeyType() == KeyType.EOF) { | |
113 | throw new EOFException(); | |
114 | } | |
115 | boolean handled = handleInput(keyStroke); | |
116 | if(!handled) { | |
117 | handled = fireUnhandledKeyStroke(keyStroke); | |
118 | } | |
119 | dirty = handled || dirty; | |
120 | keyStroke = pollInput(); | |
121 | } while(keyStroke != null); | |
122 | } | |
123 | return gotInput; | |
124 | } | |
125 | ||
126 | @Override | |
127 | public void setTheme(Theme theme) { | |
128 | this.guiTheme = theme; | |
129 | } | |
130 | ||
131 | @Override | |
132 | public synchronized void updateScreen() throws IOException { | |
133 | screen.doResizeIfNecessary(); | |
134 | drawGUI(new TextGUIGraphics(this, screen.newTextGraphics(), guiTheme)); | |
135 | screen.setCursorPosition(getCursorPosition()); | |
136 | screen.refresh(); | |
137 | dirty = false; | |
138 | } | |
139 | ||
140 | @Override | |
141 | public boolean isPendingUpdate() { | |
142 | return screen.doResizeIfNecessary() != null || dirty; | |
143 | } | |
144 | ||
145 | @Override | |
146 | public TextGUIThread getGUIThread() { | |
147 | return textGUIThread; | |
148 | } | |
149 | ||
150 | @Override | |
151 | public void addListener(Listener listener) { | |
152 | listeners.add(listener); | |
153 | } | |
154 | ||
155 | @Override | |
156 | public void removeListener(Listener listener) { | |
157 | listeners.remove(listener); | |
158 | } | |
159 | ||
160 | /** | |
161 | * Enables blocking I/O, causing calls to {@code readKeyStroke()} to block until there is input available. Notice | |
162 | * that you can still poll for input using {@code pollInput()}. | |
163 | * @param blockingIO Set this to {@code true} if blocking I/O should be enabled, otherwise {@code false} | |
164 | */ | |
165 | public void setBlockingIO(boolean blockingIO) { | |
166 | this.blockingIO = blockingIO; | |
167 | } | |
168 | ||
169 | /** | |
170 | * Checks if blocking I/O is enabled or not | |
171 | * @return {@code true} if blocking I/O is enabled, otherwise {@code false} | |
172 | */ | |
173 | public boolean isBlockingIO() { | |
174 | return blockingIO; | |
175 | } | |
176 | ||
177 | /** | |
178 | * This method should be called when there was user input that wasn't handled by the GUI. It will fire the | |
179 | * {@code onUnhandledKeyStroke(..)} method on any registered listener. | |
180 | * @param keyStroke The {@code KeyStroke} that wasn't handled by the GUI | |
181 | * @return {@code true} if at least one of the listeners handled the key stroke, this will signal to the GUI that it | |
182 | * needs to be redrawn again. | |
183 | */ | |
184 | protected final boolean fireUnhandledKeyStroke(KeyStroke keyStroke) { | |
185 | boolean handled = false; | |
186 | for(Listener listener: listeners) { | |
187 | handled = listener.onUnhandledKeyStroke(this, keyStroke) || handled; | |
188 | } | |
189 | return handled; | |
190 | } | |
191 | ||
192 | /** | |
193 | * Marks the whole text GUI as invalid and that it needs to be redrawn at next opportunity | |
194 | */ | |
195 | protected void invalidate() { | |
196 | dirty = true; | |
197 | } | |
198 | ||
199 | /** | |
200 | * Draws the entire GUI using a {@code TextGUIGraphics} object | |
201 | * @param graphics Graphics object to draw using | |
202 | */ | |
203 | protected abstract void drawGUI(TextGUIGraphics graphics); | |
204 | ||
205 | /** | |
206 | * Top-level method for drilling in to the GUI and figuring out, in global coordinates, where to place the text | |
207 | * cursor on the screen at this time. | |
208 | * @return Where to place the text cursor, or {@code null} if the cursor should be hidden | |
209 | */ | |
210 | protected abstract TerminalPosition getCursorPosition(); | |
211 | ||
212 | /** | |
213 | * This method should take the user input and feed it to the focused component for handling. | |
214 | * @param key {@code KeyStroke} representing the user input | |
215 | * @return {@code true} if the input was recognized and handled by the GUI, indicating that the GUI should be redrawn | |
216 | */ | |
217 | protected abstract boolean handleInput(KeyStroke key); | |
218 | } |