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); | |
b71d1368 NR |
139 | if(startPosition == null) { |
140 | // The structure has changed, our interactable is no longer inside the base pane! | |
141 | return null; | |
142 | } | |
a3b510ab NR |
143 | Set<Interactable> disqualified = getDisqualifiedInteractables(startPosition, true); |
144 | TerminalSize size = getSize(); | |
145 | int maxShiftLeft = interactable.toBasePane(TerminalPosition.TOP_LEFT_CORNER).getColumn(); | |
146 | maxShiftLeft = Math.max(maxShiftLeft, 0); | |
147 | int maxShiftRight = interactable.toBasePane(new TerminalPosition(interactable.getSize().getColumns() - 1, 0)).getColumn(); | |
148 | maxShiftRight = Math.min(maxShiftRight, size.getColumns() - 1); | |
149 | int maxShift = Math.max(startPosition.getColumn() - maxShiftLeft, maxShiftRight - startPosition.getRow()); | |
150 | for (int searchRow = startPosition.getRow() + directionTerm; | |
151 | searchRow >= 0 && searchRow < size.getRows(); | |
152 | searchRow += directionTerm) { | |
153 | ||
154 | for (int xShift = 0; xShift <= maxShift; xShift++) { | |
155 | for (int modifier : new int[]{1, -1}) { | |
156 | if (xShift == 0 && modifier == -1) { | |
157 | break; | |
158 | } | |
159 | int searchColumn = startPosition.getColumn() + (xShift * modifier); | |
160 | if (searchColumn < maxShiftLeft || searchColumn > maxShiftRight) { | |
161 | continue; | |
162 | } | |
163 | ||
164 | int index = lookupMap[searchRow][searchColumn]; | |
165 | if (index != -1 && !disqualified.contains(interactables.get(index))) { | |
166 | return interactables.get(index); | |
167 | } | |
168 | } | |
169 | } | |
170 | } | |
171 | return null; | |
172 | } | |
173 | ||
174 | /** | |
175 | * Starting from a particular {@code Interactable} and going left, which is the next interactable? | |
176 | * @param interactable What {@code Interactable} to start searching from | |
177 | * @return The next {@code Interactable} left of the one specified or {@code null} if there are no more | |
178 | * {@code Interactable}:s left of it | |
179 | */ | |
180 | public synchronized Interactable findNextLeft(Interactable interactable) { | |
181 | return findNextLeftOrRight(interactable, false); | |
182 | } | |
183 | ||
184 | /** | |
185 | * Starting from a particular {@code Interactable} and going right, which is the next interactable? | |
186 | * @param interactable What {@code Interactable} to start searching from | |
187 | * @return The next {@code Interactable} right of the one specified or {@code null} if there are no more | |
188 | * {@code Interactable}:s right of it | |
189 | */ | |
190 | public synchronized Interactable findNextRight(Interactable interactable) { | |
191 | return findNextLeftOrRight(interactable, true); | |
192 | } | |
193 | ||
194 | //Avoid code duplication in above two methods | |
195 | private Interactable findNextLeftOrRight(Interactable interactable, boolean isRight) { | |
196 | int directionTerm = isRight ? 1 : -1; | |
197 | TerminalPosition startPosition = interactable.getCursorLocation(); | |
198 | if(startPosition == null) { | |
199 | // If the currently active interactable component is not showing the cursor, use the top-left position | |
200 | // instead if we're going left, or the top-right position if we're going right | |
201 | if(isRight) { | |
202 | startPosition = new TerminalPosition(interactable.getSize().getColumns() - 1, 0); | |
203 | } | |
204 | else { | |
205 | startPosition = TerminalPosition.TOP_LEFT_CORNER; | |
206 | } | |
207 | } | |
208 | else { | |
209 | //Adjust position so that it's on the left-most side if we're going left or right-most side if we're going | |
210 | //right. Otherwise the lookup might product odd results in certain cases | |
211 | if(isRight) { | |
212 | startPosition = startPosition.withColumn(interactable.getSize().getColumns() - 1); | |
213 | } | |
214 | else { | |
215 | startPosition = startPosition.withColumn(0); | |
216 | } | |
217 | } | |
218 | startPosition = interactable.toBasePane(startPosition); | |
b71d1368 NR |
219 | if(startPosition == null) { |
220 | // The structure has changed, our interactable is no longer inside the base pane! | |
221 | return null; | |
222 | } | |
a3b510ab NR |
223 | Set<Interactable> disqualified = getDisqualifiedInteractables(startPosition, false); |
224 | TerminalSize size = getSize(); | |
225 | int maxShiftUp = interactable.toBasePane(TerminalPosition.TOP_LEFT_CORNER).getRow(); | |
226 | maxShiftUp = Math.max(maxShiftUp, 0); | |
227 | int maxShiftDown = interactable.toBasePane(new TerminalPosition(0, interactable.getSize().getRows() - 1)).getRow(); | |
228 | maxShiftDown = Math.min(maxShiftDown, size.getRows() - 1); | |
229 | int maxShift = Math.max(startPosition.getRow() - maxShiftUp, maxShiftDown - startPosition.getRow()); | |
230 | for(int searchColumn = startPosition.getColumn() + directionTerm; | |
231 | searchColumn >= 0 && searchColumn < size.getColumns(); | |
232 | searchColumn += directionTerm) { | |
233 | ||
234 | for(int yShift = 0; yShift <= maxShift; yShift++) { | |
235 | for(int modifier: new int[] { 1, -1 }) { | |
236 | if(yShift == 0 && modifier == -1) { | |
237 | break; | |
238 | } | |
239 | int searchRow = startPosition.getRow() + (yShift * modifier); | |
240 | if(searchRow < maxShiftUp || searchRow > maxShiftDown) { | |
241 | continue; | |
242 | } | |
243 | int index = lookupMap[searchRow][searchColumn]; | |
244 | if (index != -1 && !disqualified.contains(interactables.get(index))) { | |
245 | return interactables.get(index); | |
246 | } | |
247 | } | |
248 | } | |
249 | } | |
250 | return null; | |
251 | } | |
252 | ||
253 | private Set<Interactable> getDisqualifiedInteractables(TerminalPosition startPosition, boolean scanHorizontally) { | |
254 | Set<Interactable> disqualified = new HashSet<Interactable>(); | |
255 | if (lookupMap.length == 0) { return disqualified; } // safeguard | |
256 | ||
257 | TerminalSize size = getSize(); | |
258 | ||
259 | //Adjust start position if necessary | |
260 | if(startPosition.getRow() < 0) { | |
261 | startPosition = startPosition.withRow(0); | |
262 | } | |
263 | else if(startPosition.getRow() >= lookupMap.length) { | |
264 | startPosition = startPosition.withRow(lookupMap.length - 1); | |
265 | } | |
266 | if(startPosition.getColumn() < 0) { | |
267 | startPosition = startPosition.withColumn(0); | |
268 | } | |
269 | else if(startPosition.getColumn() >= lookupMap[startPosition.getRow()].length) { | |
270 | startPosition = startPosition.withColumn(lookupMap[startPosition.getRow()].length - 1); | |
271 | } | |
272 | ||
273 | if(scanHorizontally) { | |
274 | for(int column = 0; column < size.getColumns(); column++) { | |
275 | int index = lookupMap[startPosition.getRow()][column]; | |
276 | if(index != -1) { | |
277 | disqualified.add(interactables.get(index)); | |
278 | } | |
279 | } | |
280 | } | |
281 | else { | |
282 | for(int row = 0; row < size.getRows(); row++) { | |
283 | int index = lookupMap[row][startPosition.getColumn()]; | |
284 | if(index != -1) { | |
285 | disqualified.add(interactables.get(index)); | |
286 | } | |
287 | } | |
288 | } | |
289 | return disqualified; | |
290 | } | |
291 | ||
292 | void debug() { | |
293 | for(int[] row: lookupMap) { | |
294 | for(int value: row) { | |
295 | if(value >= 0) { | |
296 | System.out.print(" "); | |
297 | } | |
298 | System.out.print(value); | |
299 | } | |
300 | System.out.println(); | |
301 | } | |
302 | System.out.println(); | |
303 | } | |
304 | } |