Fix UTF8 bug, create first executable JAR file
[jvcard.git] / src / com / googlecode / lanterna / gui2 / InteractableLookupMap.java
... / ...
CommitLineData
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 */
19package com.googlecode.lanterna.gui2;
20
21import com.googlecode.lanterna.TerminalPosition;
22import com.googlecode.lanterna.TerminalSize;
23
24import 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 */
31public 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 if(startPosition == null) {
140 // The structure has changed, our interactable is no longer inside the base pane!
141 return null;
142 }
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);
219 if(startPosition == null) {
220 // The structure has changed, our interactable is no longer inside the base pane!
221 return null;
222 }
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}