Commit | Line | Data |
---|---|---|
8f34a795 NR |
1 | /* |
2 | * Jexer - Java Text User Interface | |
3 | * | |
4 | * The MIT License (MIT) | |
5 | * | |
6 | * Copyright (C) 2019 David "Niki" ROULET | |
7 | * | |
8 | * Permission is hereby granted, free of charge, to any person obtaining a | |
9 | * copy of this software and associated documentation files (the "Software"), | |
10 | * to deal in the Software without restriction, including without limitation | |
11 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, | |
12 | * and/or sell copies of the Software, and to permit persons to whom the | |
13 | * Software is furnished to do so, subject to the following conditions: | |
14 | * | |
15 | * The above copyright notice and this permission notice shall be included in | |
16 | * all copies or substantial portions of the Software. | |
17 | * | |
18 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL | |
21 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | |
23 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER | |
24 | * DEALINGS IN THE SOFTWARE. | |
25 | * | |
26 | * @author David ROULET [niki@nikiroo.be] | |
0e38ba53 | 27 | * @version 2 |
8f34a795 NR |
28 | */ |
29 | package be.nikiroo.jexer; | |
30 | ||
31 | import static jexer.TKeypress.kbBackTab; | |
32 | import static jexer.TKeypress.kbDown; | |
33 | import static jexer.TKeypress.kbEnd; | |
34 | import static jexer.TKeypress.kbEnter; | |
35 | import static jexer.TKeypress.kbHome; | |
36 | import static jexer.TKeypress.kbLeft; | |
37 | import static jexer.TKeypress.kbPgDn; | |
38 | import static jexer.TKeypress.kbPgUp; | |
39 | import static jexer.TKeypress.kbRight; | |
40 | import static jexer.TKeypress.kbShiftTab; | |
41 | import static jexer.TKeypress.kbTab; | |
42 | import static jexer.TKeypress.kbUp; | |
43 | import jexer.THScroller; | |
44 | import jexer.TScrollableWidget; | |
45 | import jexer.TVScroller; | |
46 | import jexer.TWidget; | |
47 | import jexer.event.TKeypressEvent; | |
48 | import jexer.event.TMouseEvent; | |
49 | import jexer.event.TResizeEvent; | |
50 | ||
51 | /** | |
52 | * This class represents a browsable {@link TWidget}, that is, a {@link TWidget} | |
53 | * where you can use the keyboard or mouse to browse to one line to the next, or | |
54 | * from left t right. | |
55 | * | |
56 | * @author niki | |
57 | */ | |
58 | abstract public class TBrowsableWidget extends TScrollableWidget { | |
59 | private int selectedRow; | |
60 | private int selectedColumn; | |
61 | private int yOffset; | |
62 | ||
63 | /** | |
64 | * The number of rows in this {@link TWidget}. | |
65 | * | |
66 | * @return the number of rows | |
67 | */ | |
68 | abstract protected int getRowCount(); | |
69 | ||
70 | /** | |
71 | * The number of columns in this {@link TWidget}. | |
72 | * | |
73 | * @return the number of columns | |
74 | */ | |
75 | abstract protected int getColumnCount(); | |
76 | ||
77 | /** | |
78 | * The virtual width of this {@link TWidget}, that is, the total width it | |
79 | * can take to display all the data. | |
80 | * | |
81 | * @return the width | |
82 | */ | |
83 | abstract int getVirtualWidth(); | |
84 | ||
85 | /** | |
86 | * The virtual height of this {@link TWidget}, that is, the total width it | |
87 | * can take to display all the data. | |
88 | * | |
89 | * @return the height | |
90 | */ | |
91 | abstract int getVirtualHeight(); | |
92 | ||
93 | /** | |
94 | * Basic setup of this class (called by all constructors) | |
95 | */ | |
96 | private void setup() { | |
97 | vScroller = new TVScroller(this, 0, 0, 1); | |
98 | hScroller = new THScroller(this, 0, 0, 1); | |
99 | fixScrollers(); | |
100 | } | |
101 | ||
102 | /** | |
103 | * Create a new {@link TBrowsableWidget} linked to the given {@link TWidget} | |
104 | * parent. | |
105 | * | |
106 | * @param parent | |
107 | * parent widget | |
108 | */ | |
109 | protected TBrowsableWidget(final TWidget parent) { | |
110 | super(parent); | |
111 | setup(); | |
112 | } | |
113 | ||
114 | /** | |
115 | * Create a new {@link TBrowsableWidget} linked to the given {@link TWidget} | |
116 | * parent. | |
117 | * | |
118 | * @param parent | |
119 | * parent widget | |
120 | * @param x | |
121 | * column relative to parent | |
122 | * @param y | |
123 | * row relative to parent | |
124 | * @param width | |
125 | * width of widget | |
126 | * @param height | |
127 | * height of widget | |
128 | */ | |
129 | protected TBrowsableWidget(final TWidget parent, final int x, final int y, | |
130 | final int width, final int height) { | |
131 | super(parent, x, y, width, height); | |
132 | setup(); | |
133 | } | |
134 | ||
135 | /** | |
136 | * Create a new {@link TBrowsableWidget} linked to the given {@link TWidget} | |
137 | * parent. | |
138 | * | |
139 | * @param parent | |
140 | * parent widget | |
141 | * @param enabled | |
142 | * if true assume enabled | |
143 | */ | |
144 | protected TBrowsableWidget(final TWidget parent, final boolean enabled) { | |
145 | super(parent, enabled); | |
146 | setup(); | |
147 | } | |
148 | ||
149 | /** | |
150 | * Create a new {@link TBrowsableWidget} linked to the given {@link TWidget} | |
151 | * parent. | |
152 | * | |
153 | * @param parent | |
154 | * parent widget | |
155 | * @param enabled | |
156 | * if true assume enabled | |
157 | * @param x | |
158 | * column relative to parent | |
159 | * @param y | |
160 | * row relative to parent | |
161 | * @param width | |
162 | * width of widget | |
163 | * @param height | |
164 | * height of widget | |
165 | */ | |
166 | protected TBrowsableWidget(final TWidget parent, final boolean enabled, | |
167 | final int x, final int y, final int width, final int height) { | |
168 | super(parent, enabled, x, y, width, height); | |
169 | setup(); | |
170 | } | |
171 | ||
172 | /** | |
173 | * The currently selected row (or -1 if no row is selected). | |
174 | * | |
175 | * @return the selected row | |
176 | */ | |
177 | public int getSelectedRow() { | |
178 | return selectedRow; | |
179 | } | |
180 | ||
181 | /** | |
182 | * The currently selected row (or -1 if no row is selected). | |
183 | * <p> | |
184 | * You may want to call {@link TBrowsableWidget#reflowData()} when done to | |
185 | * see the changes. | |
186 | * | |
187 | * @param selectedRow | |
188 | * the new selected row | |
189 | * | |
190 | * @throws IndexOutOfBoundsException | |
191 | * when the index is out of bounds | |
192 | */ | |
193 | public void setSelectedRow(int selectedRow) { | |
194 | if (selectedRow < -1 || selectedRow >= getRowCount()) { | |
195 | throw new IndexOutOfBoundsException(String.format( | |
196 | "Cannot set row %d on a table with %d rows", selectedRow, | |
197 | getRowCount())); | |
198 | } | |
199 | ||
200 | this.selectedRow = selectedRow; | |
201 | } | |
202 | ||
203 | /** | |
204 | * The currently selected column (or -1 if no column is selected). | |
205 | * | |
206 | * @return the new selected column | |
207 | */ | |
208 | public int getSelectedColumn() { | |
209 | return selectedColumn; | |
210 | } | |
211 | ||
212 | /** | |
213 | * The currently selected column (or -1 if no column is selected). | |
214 | * <p> | |
215 | * You may want to call {@link TBrowsableWidget#reflowData()} when done to | |
216 | * see the changes. | |
217 | * | |
218 | * @param selectedColumn | |
219 | * the new selected column | |
220 | * | |
221 | * @throws IndexOutOfBoundsException | |
222 | * when the index is out of bounds | |
223 | */ | |
224 | public void setSelectedColumn(int selectedColumn) { | |
225 | if (selectedColumn < -1 || selectedColumn >= getColumnCount()) { | |
226 | throw new IndexOutOfBoundsException(String.format( | |
227 | "Cannot set column %d on a table with %d columns", | |
228 | selectedColumn, getColumnCount())); | |
229 | } | |
230 | ||
231 | this.selectedColumn = selectedColumn; | |
232 | } | |
233 | ||
234 | /** | |
235 | * An offset on the Y position of the table, i.e., the number of rows to | |
236 | * skip so the control can draw that many rows always on top. | |
237 | * | |
238 | * @return the offset | |
239 | */ | |
240 | public int getYOffset() { | |
241 | return yOffset; | |
242 | } | |
243 | ||
244 | /** | |
245 | * An offset on the Y position of the table, i.e., the number of rows that | |
246 | * should always stay on top. | |
247 | * | |
248 | * @param yOffset | |
249 | * the new offset | |
250 | */ | |
251 | public void setYOffset(int yOffset) { | |
252 | this.yOffset = yOffset; | |
253 | } | |
254 | ||
255 | @SuppressWarnings("unused") | |
256 | public void dispatchMove(int fromRow, int toRow) { | |
257 | reflowData(); | |
258 | } | |
259 | ||
260 | @SuppressWarnings("unused") | |
261 | public void dispatchEnter(int selectedRow) { | |
262 | reflowData(); | |
263 | } | |
264 | ||
265 | @Override | |
266 | public void onMouseDown(final TMouseEvent mouse) { | |
267 | if (mouse.isMouseWheelUp()) { | |
268 | vScroller.decrement(); | |
269 | return; | |
270 | } | |
271 | if (mouse.isMouseWheelDown()) { | |
272 | vScroller.increment(); | |
273 | return; | |
274 | } | |
275 | ||
276 | if ((mouse.getX() < getWidth() - 1) && (mouse.getY() < getHeight() - 1)) { | |
277 | if (vScroller.getValue() + mouse.getY() < getRowCount()) { | |
278 | selectedRow = vScroller.getValue() + mouse.getY() | |
279 | - getYOffset(); | |
280 | } | |
281 | dispatchEnter(selectedRow); | |
282 | return; | |
283 | } | |
284 | ||
285 | // Pass to children | |
286 | super.onMouseDown(mouse); | |
287 | } | |
288 | ||
289 | @Override | |
290 | public void onKeypress(final TKeypressEvent keypress) { | |
291 | int maxX = getRowCount(); | |
292 | int prevSelectedRow = selectedRow; | |
293 | ||
294 | int firstLineIndex = vScroller.getValue() - getYOffset() + 2; | |
295 | int lastLineIndex = firstLineIndex - hScroller.getHeight() | |
296 | + getHeight() - 2 - 2; | |
297 | ||
298 | if (keypress.equals(kbLeft)) { | |
299 | hScroller.decrement(); | |
300 | } else if (keypress.equals(kbRight)) { | |
301 | hScroller.increment(); | |
302 | } else if (keypress.equals(kbUp)) { | |
303 | if (maxX > 0 && selectedRow < maxX) { | |
304 | if (selectedRow > 0) { | |
305 | if (selectedRow <= firstLineIndex) { | |
306 | vScroller.decrement(); | |
307 | } | |
308 | selectedRow--; | |
309 | } else { | |
310 | selectedRow = 0; | |
311 | } | |
312 | ||
313 | dispatchMove(prevSelectedRow, selectedRow); | |
314 | } | |
315 | } else if (keypress.equals(kbDown)) { | |
316 | if (maxX > 0) { | |
317 | if (selectedRow >= 0) { | |
318 | if (selectedRow < maxX - 1) { | |
319 | selectedRow++; | |
320 | if (selectedRow >= lastLineIndex) { | |
321 | vScroller.increment(); | |
322 | } | |
323 | } | |
324 | } else { | |
325 | selectedRow = 0; | |
326 | } | |
327 | ||
328 | dispatchMove(prevSelectedRow, selectedRow); | |
329 | } | |
330 | } else if (keypress.equals(kbPgUp)) { | |
331 | if (selectedRow >= 0) { | |
332 | vScroller.bigDecrement(); | |
333 | selectedRow -= getHeight() - 1; | |
334 | if (selectedRow < 0) { | |
335 | selectedRow = 0; | |
336 | } | |
337 | ||
338 | dispatchMove(prevSelectedRow, selectedRow); | |
339 | } | |
340 | } else if (keypress.equals(kbPgDn)) { | |
341 | if (selectedRow >= 0) { | |
342 | vScroller.bigIncrement(); | |
343 | selectedRow += getHeight() - 1; | |
344 | if (selectedRow > getRowCount() - 1) { | |
345 | selectedRow = getRowCount() - 1; | |
346 | } | |
347 | ||
348 | dispatchMove(prevSelectedRow, selectedRow); | |
349 | } | |
350 | } else if (keypress.equals(kbHome)) { | |
351 | if (getRowCount() > 0) { | |
352 | vScroller.toTop(); | |
353 | selectedRow = 0; | |
354 | dispatchMove(prevSelectedRow, selectedRow); | |
355 | } | |
356 | } else if (keypress.equals(kbEnd)) { | |
357 | if (getRowCount() > 0) { | |
358 | vScroller.toBottom(); | |
359 | selectedRow = getRowCount() - 1; | |
360 | dispatchMove(prevSelectedRow, selectedRow); | |
361 | } | |
362 | } else if (keypress.equals(kbTab)) { | |
363 | getParent().switchWidget(true); | |
364 | } else if (keypress.equals(kbShiftTab) || keypress.equals(kbBackTab)) { | |
365 | getParent().switchWidget(false); | |
366 | } else if (keypress.equals(kbEnter)) { | |
367 | if (selectedRow >= 0) { | |
368 | dispatchEnter(selectedRow); | |
369 | } | |
370 | } else { | |
371 | // Pass other keys (tab etc.) on | |
372 | super.onKeypress(keypress); | |
373 | } | |
374 | } | |
375 | ||
376 | @Override | |
377 | public void onResize(TResizeEvent event) { | |
378 | super.onResize(event); | |
379 | reflowData(); | |
380 | } | |
381 | ||
382 | @Override | |
383 | public void reflowData() { | |
384 | super.reflowData(); | |
385 | fixScrollers(); | |
386 | } | |
387 | ||
388 | private void fixScrollers() { | |
89993676 NR |
389 | int width = getWidth() - 1; // vertical prio |
390 | int height = getHeight(); | |
391 | ||
392 | // TODO: why did we do that before? | |
393 | if (false) { | |
394 | width -= 2; | |
395 | height = -1; | |
396 | } | |
397 | ||
398 | int x = Math.max(0, width); | |
399 | int y = Math.max(0, height - 1); | |
400 | ||
401 | vScroller.setX(x); | |
402 | vScroller.setHeight(height); | |
403 | hScroller.setY(y); | |
404 | hScroller.setWidth(width); | |
8f34a795 | 405 | |
0e38ba53 NR |
406 | // TODO why did we use to add 2? |
407 | // + 2 (for the border of the window) | |
408 | ||
8f34a795 | 409 | // virtual_size |
0e38ba53 | 410 | // + the other scroll bar size |
8f34a795 NR |
411 | vScroller.setTopValue(0); |
412 | vScroller.setBottomValue(Math.max(0, getVirtualHeight() - getHeight() | |
0e38ba53 | 413 | + hScroller.getHeight())); |
8f34a795 NR |
414 | hScroller.setLeftValue(0); |
415 | hScroller.setRightValue(Math.max(0, getVirtualWidth() - getWidth() | |
0e38ba53 | 416 | + vScroller.getWidth())); |
8f34a795 NR |
417 | } |
418 | } |