Initial commit
[jvcard.git] / src / com / googlecode / lanterna / gui2 / InteractableLookupMap.java
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 }