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
.input
;
21 import com
.googlecode
.lanterna
.input
.CharacterPattern
.Matching
;
23 import java
.io
.BufferedReader
;
24 import java
.io
.IOException
;
25 import java
.io
.Reader
;
29 * Used to read the input stream character by character and generate {@code Key} objects to be put in the input queue.
31 * @author Martin, Andreas
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
;
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
44 public InputDecoder(final Reader source
) {
45 this.source
= new BufferedReader(source
);
46 this.bytePatterns
= new ArrayList
<CharacterPattern
>();
47 this.currentMatching
= new ArrayList
<Character
>();
49 this.timeoutUnits
= 0; // default is no wait at all
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
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
);
68 * Returns a collection of all patterns registered in this InputDecoder.
69 * @return Collection of patterns in the InputDecoder
71 public synchronized Collection
<CharacterPattern
> getPatterns() {
72 synchronized(bytePatterns
) {
73 return new ArrayList
<CharacterPattern
>(bytePatterns
);
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}
82 public boolean removePattern(CharacterPattern pattern
) {
83 synchronized(bytePatterns
) {
84 return bytePatterns
.remove(pattern
);
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.
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).
95 public void setTimeoutUnits(int units
) {
96 timeoutUnits
= (units
< 0) ?
0 :
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
104 public int getTimeoutUnits() {
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
113 public synchronized KeyStroke
getNextCharacter(boolean blockingIO
) throws IOException
{
115 KeyStroke bestMatch
= null;
121 if ( curLen
< currentMatching
.size() ) {
122 // (re-)consume characters previously read:
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() ) {
134 timeout
--; Thread
.sleep(250);
135 } catch (InterruptedException e
) { timeout
= 0; }
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) {
145 if(currentMatching
.isEmpty()) {
146 return new KeyStroke(KeyType
.EOF
);
150 currentMatching
.add( (char)readChar
);
152 } else { // no more available input at this time.
153 // already found something:
154 if (bestMatch
!= null) {
155 break; // it's something...
157 // otherwise: no KeyStroke yet
162 List
<Character
> curSub
= currentMatching
.subList(0, curLen
);
163 Matching matching
= getBestMatch( curSub
);
165 // fullMatch found...
166 if (matching
.fullMatch
!= null) {
167 bestMatch
= matching
.fullMatch
;
170 if (! matching
.partialMatch
) {
171 // that match and no more
174 // that match, but maybe more
178 // No match found yet, but there's still potential...
179 else if ( matching
.partialMatch
) {
182 // no longer match possible at this point:
184 if (bestMatch
!= null ) {
185 // there was already a previous full-match, use it:
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);
196 //Did we find anything? Otherwise return null
197 if(bestMatch
== null) {
199 currentMatching
.clear();
200 return new KeyStroke(KeyType
.EOF
);
205 List
<Character
> bestSub
= currentMatching
.subList(0, bestLen
);
206 bestSub
.clear(); // remove matched characters from input
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
);
217 if (res
.partialMatch
) { partialMatch
= true; }
218 if (res
.fullMatch
!= null) { bestMatch
= res
.fullMatch
; }
222 return new Matching(partialMatch
, bestMatch
);