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.TerminalSize; | |
23 | ||
24 | import java.util.*; | |
25 | ||
26 | /** | |
27 | * This class is used to keep a 'map' of the usable area and note where all the interact:ables are. It can then be used | |
28 | * to find the next interactable in any direction. It is used inside the GUI system to drive arrow key navigation. | |
29 | * @author Martin | |
30 | */ | |
31 | public class InteractableLookupMap { | |
32 | private final int[][] lookupMap; | |
33 | private final List<Interactable> interactables; | |
34 | ||
35 | InteractableLookupMap(TerminalSize size) { | |
36 | lookupMap = new int[size.getRows()][size.getColumns()]; | |
37 | interactables = new ArrayList<Interactable>(); | |
38 | for (int[] aLookupMap : lookupMap) { | |
39 | Arrays.fill(aLookupMap, -1); | |
40 | } | |
41 | } | |
42 | ||
43 | void reset() { | |
44 | interactables.clear(); | |
45 | for (int[] aLookupMap : lookupMap) { | |
46 | Arrays.fill(aLookupMap, -1); | |
47 | } | |
48 | } | |
49 | ||
50 | TerminalSize getSize() { | |
51 | if (lookupMap.length==0) { return TerminalSize.ZERO; } | |
52 | return new TerminalSize(lookupMap[0].length, lookupMap.length); | |
53 | } | |
54 | ||
55 | /** | |
56 | * Adds an interactable component to the lookup map | |
57 | * @param interactable Interactable to add to the lookup map | |
58 | */ | |
59 | @SuppressWarnings("ConstantConditions") | |
60 | public synchronized void add(Interactable interactable) { | |
61 | TerminalPosition topLeft = interactable.toBasePane(TerminalPosition.TOP_LEFT_CORNER); | |
62 | TerminalSize size = interactable.getSize(); | |
63 | interactables.add(interactable); | |
64 | int index = interactables.size() - 1; | |
65 | for(int y = topLeft.getRow(); y < topLeft.getRow() + size.getRows(); y++) { | |
66 | for(int x = topLeft.getColumn(); x < topLeft.getColumn() + size.getColumns(); x++) { | |
67 | //Make sure it's not outside the map | |
68 | if(y >= 0 && y < lookupMap.length && | |
69 | x >= 0 && x < lookupMap[y].length) { | |
70 | lookupMap[y][x] = index; | |
71 | } | |
72 | } | |
73 | } | |
74 | } | |
75 | ||
76 | /** | |
77 | * Looks up what interactable component is as a particular location in the map | |
78 | * @param position Position to look up | |
79 | * @return The {@code Interactable} component at the specified location or {@code null} if there's nothing there | |
80 | */ | |
81 | public synchronized Interactable getInteractableAt(TerminalPosition position) { | |
82 | if(position.getRow() >= lookupMap.length) { | |
83 | return null; | |
84 | } | |
85 | else if(position.getColumn() >= lookupMap[0].length) { | |
86 | return null; | |
87 | } | |
88 | else if(lookupMap[position.getRow()][position.getColumn()] == -1) { | |
89 | return null; | |
90 | } | |
91 | return interactables.get(lookupMap[position.getRow()][position.getColumn()]); | |
92 | } | |
93 | ||
94 | /** | |
95 | * Starting from a particular {@code Interactable} and going up, which is the next interactable? | |
96 | * @param interactable What {@code Interactable} to start searching from | |
97 | * @return The next {@code Interactable} above the one specified or {@code null} if there are no more | |
98 | * {@code Interactable}:s above it | |
99 | */ | |
100 | public synchronized Interactable findNextUp(Interactable interactable) { | |
101 | return findNextUpOrDown(interactable, false); | |
102 | } | |
103 | ||
104 | /** | |
105 | * Starting from a particular {@code Interactable} and going down, which is the next interactable? | |
106 | * @param interactable What {@code Interactable} to start searching from | |
107 | * @return The next {@code Interactable} below the one specified or {@code null} if there are no more | |
108 | * {@code Interactable}:s below it | |
109 | */ | |
110 | public synchronized Interactable findNextDown(Interactable interactable) { | |
111 | return findNextUpOrDown(interactable, true); | |
112 | } | |
113 | ||
114 | //Avoid code duplication in above two methods | |
115 | private Interactable findNextUpOrDown(Interactable interactable, boolean isDown) { | |
116 | int directionTerm = isDown ? 1 : -1; | |
117 | TerminalPosition startPosition = interactable.getCursorLocation(); | |
118 | if (startPosition == null) { | |
119 | // If the currently active interactable component is not showing the cursor, use the top-left position | |
120 | // instead if we're going up, or the bottom-left position if we're going down | |
121 | if(isDown) { | |
122 | startPosition = new TerminalPosition(0, interactable.getSize().getRows() - 1); | |
123 | } | |
124 | else { | |
125 | startPosition = TerminalPosition.TOP_LEFT_CORNER; | |
126 | } | |
127 | } | |
128 | else { | |
129 | //Adjust position so that it's at the bottom of the component if we're going down or at the top of the | |
130 | //component if we're going right. Otherwise the lookup might product odd results in certain cases. | |
131 | if(isDown) { | |
132 | startPosition = startPosition.withRow(interactable.getSize().getRows() - 1); | |
133 | } | |
134 | else { | |
135 | startPosition = startPosition.withRow(0); | |
136 | } | |
137 | } | |
138 | startPosition = interactable.toBasePane(startPosition); | |
139 | Set<Interactable> disqualified = getDisqualifiedInteractables(startPosition, true); | |
140 | TerminalSize size = getSize(); | |
141 | int maxShiftLeft = interactable.toBasePane(TerminalPosition.TOP_LEFT_CORNER).getColumn(); | |
142 | maxShiftLeft = Math.max(maxShiftLeft, 0); | |
143 | int maxShiftRight = interactable.toBasePane(new TerminalPosition(interactable.getSize().getColumns() - 1, 0)).getColumn(); | |
144 | maxShiftRight = Math.min(maxShiftRight, size.getColumns() - 1); | |
145 | int maxShift = Math.max(startPosition.getColumn() - maxShiftLeft, maxShiftRight - startPosition.getRow()); | |
146 | for (int searchRow = startPosition.getRow() + directionTerm; | |
147 | searchRow >= 0 && searchRow < size.getRows(); | |
148 | searchRow += directionTerm) { | |
149 | ||
150 | for (int xShift = 0; xShift <= maxShift; xShift++) { | |
151 | for (int modifier : new int[]{1, -1}) { | |
152 | if (xShift == 0 && modifier == -1) { | |
153 | break; | |
154 | } | |
155 | int searchColumn = startPosition.getColumn() + (xShift * modifier); | |
156 | if (searchColumn < maxShiftLeft || searchColumn > maxShiftRight) { | |
157 | continue; | |
158 | } | |
159 | ||
160 | int index = lookupMap[searchRow][searchColumn]; | |
161 | if (index != -1 && !disqualified.contains(interactables.get(index))) { | |
162 | return interactables.get(index); | |
163 | } | |
164 | } | |
165 | } | |
166 | } | |
167 | return null; | |
168 | } | |
169 | ||
170 | /** | |
171 | * Starting from a particular {@code Interactable} and going left, which is the next interactable? | |
172 | * @param interactable What {@code Interactable} to start searching from | |
173 | * @return The next {@code Interactable} left of the one specified or {@code null} if there are no more | |
174 | * {@code Interactable}:s left of it | |
175 | */ | |
176 | public synchronized Interactable findNextLeft(Interactable interactable) { | |
177 | return findNextLeftOrRight(interactable, false); | |
178 | } | |
179 | ||
180 | /** | |
181 | * Starting from a particular {@code Interactable} and going right, which is the next interactable? | |
182 | * @param interactable What {@code Interactable} to start searching from | |
183 | * @return The next {@code Interactable} right of the one specified or {@code null} if there are no more | |
184 | * {@code Interactable}:s right of it | |
185 | */ | |
186 | public synchronized Interactable findNextRight(Interactable interactable) { | |
187 | return findNextLeftOrRight(interactable, true); | |
188 | } | |
189 | ||
190 | //Avoid code duplication in above two methods | |
191 | private Interactable findNextLeftOrRight(Interactable interactable, boolean isRight) { | |
192 | int directionTerm = isRight ? 1 : -1; | |
193 | TerminalPosition startPosition = interactable.getCursorLocation(); | |
194 | if(startPosition == null) { | |
195 | // If the currently active interactable component is not showing the cursor, use the top-left position | |
196 | // instead if we're going left, or the top-right position if we're going right | |
197 | if(isRight) { | |
198 | startPosition = new TerminalPosition(interactable.getSize().getColumns() - 1, 0); | |
199 | } | |
200 | else { | |
201 | startPosition = TerminalPosition.TOP_LEFT_CORNER; | |
202 | } | |
203 | } | |
204 | else { | |
205 | //Adjust position so that it's on the left-most side if we're going left or right-most side if we're going | |
206 | //right. Otherwise the lookup might product odd results in certain cases | |
207 | if(isRight) { | |
208 | startPosition = startPosition.withColumn(interactable.getSize().getColumns() - 1); | |
209 | } | |
210 | else { | |
211 | startPosition = startPosition.withColumn(0); | |
212 | } | |
213 | } | |
214 | startPosition = interactable.toBasePane(startPosition); | |
215 | Set<Interactable> disqualified = getDisqualifiedInteractables(startPosition, false); | |
216 | TerminalSize size = getSize(); | |
217 | int maxShiftUp = interactable.toBasePane(TerminalPosition.TOP_LEFT_CORNER).getRow(); | |
218 | maxShiftUp = Math.max(maxShiftUp, 0); | |
219 | int maxShiftDown = interactable.toBasePane(new TerminalPosition(0, interactable.getSize().getRows() - 1)).getRow(); | |
220 | maxShiftDown = Math.min(maxShiftDown, size.getRows() - 1); | |
221 | int maxShift = Math.max(startPosition.getRow() - maxShiftUp, maxShiftDown - startPosition.getRow()); | |
222 | for(int searchColumn = startPosition.getColumn() + directionTerm; | |
223 | searchColumn >= 0 && searchColumn < size.getColumns(); | |
224 | searchColumn += directionTerm) { | |
225 | ||
226 | for(int yShift = 0; yShift <= maxShift; yShift++) { | |
227 | for(int modifier: new int[] { 1, -1 }) { | |
228 | if(yShift == 0 && modifier == -1) { | |
229 | break; | |
230 | } | |
231 | int searchRow = startPosition.getRow() + (yShift * modifier); | |
232 | if(searchRow < maxShiftUp || searchRow > maxShiftDown) { | |
233 | continue; | |
234 | } | |
235 | int index = lookupMap[searchRow][searchColumn]; | |
236 | if (index != -1 && !disqualified.contains(interactables.get(index))) { | |
237 | return interactables.get(index); | |
238 | } | |
239 | } | |
240 | } | |
241 | } | |
242 | return null; | |
243 | } | |
244 | ||
245 | private Set<Interactable> getDisqualifiedInteractables(TerminalPosition startPosition, boolean scanHorizontally) { | |
246 | Set<Interactable> disqualified = new HashSet<Interactable>(); | |
247 | if (lookupMap.length == 0) { return disqualified; } // safeguard | |
248 | ||
249 | TerminalSize size = getSize(); | |
250 | ||
251 | //Adjust start position if necessary | |
252 | if(startPosition.getRow() < 0) { | |
253 | startPosition = startPosition.withRow(0); | |
254 | } | |
255 | else if(startPosition.getRow() >= lookupMap.length) { | |
256 | startPosition = startPosition.withRow(lookupMap.length - 1); | |
257 | } | |
258 | if(startPosition.getColumn() < 0) { | |
259 | startPosition = startPosition.withColumn(0); | |
260 | } | |
261 | else if(startPosition.getColumn() >= lookupMap[startPosition.getRow()].length) { | |
262 | startPosition = startPosition.withColumn(lookupMap[startPosition.getRow()].length - 1); | |
263 | } | |
264 | ||
265 | if(scanHorizontally) { | |
266 | for(int column = 0; column < size.getColumns(); column++) { | |
267 | int index = lookupMap[startPosition.getRow()][column]; | |
268 | if(index != -1) { | |
269 | disqualified.add(interactables.get(index)); | |
270 | } | |
271 | } | |
272 | } | |
273 | else { | |
274 | for(int row = 0; row < size.getRows(); row++) { | |
275 | int index = lookupMap[row][startPosition.getColumn()]; | |
276 | if(index != -1) { | |
277 | disqualified.add(interactables.get(index)); | |
278 | } | |
279 | } | |
280 | } | |
281 | return disqualified; | |
282 | } | |
283 | ||
284 | void debug() { | |
285 | for(int[] row: lookupMap) { | |
286 | for(int value: row) { | |
287 | if(value >= 0) { | |
288 | System.out.print(" "); | |
289 | } | |
290 | System.out.print(value); | |
291 | } | |
292 | System.out.println(); | |
293 | } | |
294 | System.out.println(); | |
295 | } | |
296 | } |