Merge branch 'subtree'
[fanfix.git] / src / be / nikiroo / jexer / TBrowsableWidget.java
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]
27 * @version 2
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() {
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);
405
406 // TODO why did we use to add 2?
407 // + 2 (for the border of the window)
408
409 // virtual_size
410 // + the other scroll bar size
411 vScroller.setTopValue(0);
412 vScroller.setBottomValue(Math.max(0, getVirtualHeight() - getHeight()
413 + hScroller.getHeight()));
414 hScroller.setLeftValue(0);
415 hScroller.setRightValue(Math.max(0, getVirtualWidth() - getWidth()
416 + vScroller.getWidth()));
417 }
418 }