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.input; | |
20 | ||
21 | import com.googlecode.lanterna.input.CharacterPattern.Matching; | |
22 | ||
23 | import java.io.BufferedReader; | |
24 | import java.io.IOException; | |
25 | import java.io.Reader; | |
26 | import java.util.*; | |
27 | ||
28 | /** | |
29 | * Used to read the input stream character by character and generate {@code Key} objects to be put in the input queue. | |
30 | * | |
31 | * @author Martin, Andreas | |
32 | */ | |
33 | public class InputDecoder { | |
34 | private final Reader source; | |
35 | private final List<CharacterPattern> bytePatterns; | |
36 | private final List<Character> currentMatching; | |
37 | private boolean seenEOF; | |
38 | private int timeoutUnits; | |
39 | ||
40 | /** | |
41 | * Creates a new input decoder using a specified Reader as the source to read characters from | |
42 | * @param source Reader to read characters from, will be wrapped by a BufferedReader | |
43 | */ | |
44 | public InputDecoder(final Reader source) { | |
45 | this.source = new BufferedReader(source); | |
46 | this.bytePatterns = new ArrayList<CharacterPattern>(); | |
47 | this.currentMatching = new ArrayList<Character>(); | |
48 | this.seenEOF = false; | |
49 | this.timeoutUnits = 0; // default is no wait at all | |
50 | } | |
51 | ||
52 | /** | |
53 | * Adds another key decoding profile to this InputDecoder, which means all patterns from the profile will be used | |
54 | * when decoding input. | |
55 | * @param profile Profile to add | |
56 | */ | |
57 | public void addProfile(KeyDecodingProfile profile) { | |
58 | for (CharacterPattern pattern : profile.getPatterns()) { | |
59 | synchronized(bytePatterns) { | |
60 | //If an equivalent pattern already exists, remove it first | |
61 | bytePatterns.remove(pattern); | |
62 | bytePatterns.add(pattern); | |
63 | } | |
64 | } | |
65 | } | |
66 | ||
67 | /** | |
68 | * Returns a collection of all patterns registered in this InputDecoder. | |
69 | * @return Collection of patterns in the InputDecoder | |
70 | */ | |
71 | public synchronized Collection<CharacterPattern> getPatterns() { | |
72 | synchronized(bytePatterns) { | |
73 | return new ArrayList<CharacterPattern>(bytePatterns); | |
74 | } | |
75 | } | |
76 | ||
77 | /** | |
78 | * Removes one pattern from the list of patterns in this InputDecoder | |
79 | * @param pattern Pattern to remove | |
80 | * @return {@code true} if the supplied pattern was found and was removed, otherwise {@code false} | |
81 | */ | |
82 | public boolean removePattern(CharacterPattern pattern) { | |
83 | synchronized(bytePatterns) { | |
84 | return bytePatterns.remove(pattern); | |
85 | } | |
86 | } | |
87 | ||
88 | /** | |
89 | * Sets the number of 1/4-second units for how long to try to get further input | |
90 | * to complete an escape-sequence for a special Key. | |
91 | * | |
92 | * Negative numbers are mapped to 0 (no wait at all), and unreasonably high | |
93 | * values are mapped to a maximum of 240 (1 minute). | |
94 | */ | |
95 | public void setTimeoutUnits(int units) { | |
96 | timeoutUnits = (units < 0) ? 0 : | |
97 | (units > 240) ? 240 : | |
98 | units; | |
99 | } | |
100 | /** | |
101 | * queries the current timeoutUnits value. One unit is 1/4 second. | |
102 | * @return The timeout this InputDecoder will use when waiting for additional input, in units of 1/4 seconds | |
103 | */ | |
104 | public int getTimeoutUnits() { | |
105 | return timeoutUnits; | |
106 | } | |
107 | ||
108 | /** | |
109 | * Reads and decodes the next key stroke from the input stream | |
110 | * @return Key stroke read from the input stream, or {@code null} if none | |
111 | * @throws IOException If there was an I/O error when reading from the input stream | |
112 | */ | |
113 | public synchronized KeyStroke getNextCharacter(boolean blockingIO) throws IOException { | |
114 | ||
115 | KeyStroke bestMatch = null; | |
116 | int bestLen = 0; | |
117 | int curLen = 0; | |
118 | ||
119 | while(true) { | |
120 | ||
121 | if ( curLen < currentMatching.size() ) { | |
122 | // (re-)consume characters previously read: | |
123 | curLen++; | |
124 | } | |
125 | else { | |
126 | // If we already have a bestMatch but a chance for a longer match | |
127 | // then we poll for the configured number of timeout units: | |
128 | // It would be much better, if we could just read with a timeout, | |
129 | // but lacking that, we wait 1/4s units and check for readiness. | |
130 | if (bestMatch != null) { | |
131 | int timeout = getTimeoutUnits(); | |
132 | while (timeout > 0 && ! source.ready() ) { | |
133 | try { | |
134 | timeout--; Thread.sleep(250); | |
135 | } catch (InterruptedException e) { timeout = 0; } | |
136 | } | |
137 | } | |
138 | // if input is available, we can just read a char without waiting, | |
139 | // otherwise, for readInput() with no bestMatch found yet, | |
140 | // we have to wait blocking for more input: | |
141 | if ( source.ready() || ( blockingIO && bestMatch == null ) ) { | |
142 | int readChar = source.read(); | |
143 | if (readChar == -1) { | |
144 | seenEOF = true; | |
145 | if(currentMatching.isEmpty()) { | |
146 | return new KeyStroke(KeyType.EOF); | |
147 | } | |
148 | break; | |
149 | } | |
150 | currentMatching.add( (char)readChar ); | |
151 | curLen++; | |
152 | } else { // no more available input at this time. | |
153 | // already found something: | |
154 | if (bestMatch != null) { | |
155 | break; // it's something... | |
156 | } | |
157 | // otherwise: no KeyStroke yet | |
158 | return null; | |
159 | } | |
160 | } | |
161 | ||
162 | List<Character> curSub = currentMatching.subList(0, curLen); | |
163 | Matching matching = getBestMatch( curSub ); | |
164 | ||
165 | // fullMatch found... | |
166 | if (matching.fullMatch != null) { | |
167 | bestMatch = matching.fullMatch; | |
168 | bestLen = curLen; | |
169 | ||
170 | if (! matching.partialMatch) { | |
171 | // that match and no more | |
172 | break; | |
173 | } else { | |
174 | // that match, but maybe more | |
175 | continue; | |
176 | } | |
177 | } | |
178 | // No match found yet, but there's still potential... | |
179 | else if ( matching.partialMatch ) { | |
180 | continue; | |
181 | } | |
182 | // no longer match possible at this point: | |
183 | else { | |
184 | if (bestMatch != null ) { | |
185 | // there was already a previous full-match, use it: | |
186 | break; | |
187 | } else { // invalid input! | |
188 | // remove the whole fail and re-try finding a KeyStroke... | |
189 | curSub.clear(); // or just 1 char? currentMatching.remove(0); | |
190 | curLen = 0; | |
191 | continue; | |
192 | } | |
193 | } | |
194 | } | |
195 | ||
196 | //Did we find anything? Otherwise return null | |
197 | if(bestMatch == null) { | |
198 | if(seenEOF) { | |
199 | currentMatching.clear(); | |
200 | return new KeyStroke(KeyType.EOF); | |
201 | } | |
202 | return null; | |
203 | } | |
204 | ||
205 | List<Character> bestSub = currentMatching.subList(0, bestLen ); | |
206 | bestSub.clear(); // remove matched characters from input | |
207 | return bestMatch; | |
208 | } | |
209 | ||
210 | private Matching getBestMatch(List<Character> characterSequence) { | |
211 | boolean partialMatch = false; | |
212 | KeyStroke bestMatch = null; | |
213 | synchronized(bytePatterns) { | |
214 | for(CharacterPattern pattern : bytePatterns) { | |
215 | Matching res = pattern.match(characterSequence); | |
216 | if (res != null) { | |
217 | if (res.partialMatch) { partialMatch = true; } | |
218 | if (res.fullMatch != null) { bestMatch = res.fullMatch; } | |
219 | } | |
220 | } | |
221 | } | |
222 | return new Matching(partialMatch, bestMatch); | |
223 | } | |
224 | } |