Merge branch 'subtree'
[nikiroo-utils.git] / src / jexer / TList.java
1 /*
2 * Jexer - Java Text User Interface
3 *
4 * The MIT License (MIT)
5 *
6 * Copyright (C) 2019 Kevin Lamonte
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 Kevin Lamonte [kevin.lamonte@gmail.com]
27 * @version 1
28 */
29 package jexer;
30
31 import java.util.ArrayList;
32 import java.util.List;
33
34 import jexer.bits.CellAttributes;
35 import jexer.bits.StringUtils;
36 import jexer.event.TKeypressEvent;
37 import jexer.event.TMouseEvent;
38 import static jexer.TKeypress.*;
39
40 /**
41 * TList shows a list of strings, and lets the user select one.
42 */
43 public class TList extends TScrollableWidget {
44
45 // ------------------------------------------------------------------------
46 // Variables --------------------------------------------------------------
47 // ------------------------------------------------------------------------
48
49 /**
50 * The list of strings to display.
51 */
52 private List<String> strings;
53
54 /**
55 * Selected string.
56 */
57 private int selectedString = -1;
58
59 /**
60 * Maximum width of a single line.
61 */
62 private int maxLineWidth;
63
64 /**
65 * The action to perform when the user selects an item (double-clicks or
66 * enter).
67 */
68 protected TAction enterAction = null;
69
70 /**
71 * The action to perform when the user selects an item (single-click).
72 */
73 protected TAction singleClickAction = null;
74
75 /**
76 * The action to perform when the user navigates with keyboard.
77 */
78 protected TAction moveAction = null;
79
80 // ------------------------------------------------------------------------
81 // Constructors -----------------------------------------------------------
82 // ------------------------------------------------------------------------
83
84 /**
85 * Public constructor.
86 *
87 * @param parent parent widget
88 * @param strings list of strings to show
89 * @param x column relative to parent
90 * @param y row relative to parent
91 * @param width width of text area
92 * @param height height of text area
93 */
94 public TList(final TWidget parent, final List<String> strings, final int x,
95 final int y, final int width, final int height) {
96
97 this(parent, strings, x, y, width, height, null, null, null);
98 }
99
100 /**
101 * Public constructor.
102 *
103 * @param parent parent widget
104 * @param strings list of strings to show. This is allowed to be null
105 * and set later with setList() or by subclasses.
106 * @param x column relative to parent
107 * @param y row relative to parent
108 * @param width width of text area
109 * @param height height of text area
110 * @param enterAction action to perform when an item is selected
111 */
112 public TList(final TWidget parent, final List<String> strings, final int x,
113 final int y, final int width, final int height,
114 final TAction enterAction) {
115
116 this(parent, strings, x, y, width, height, enterAction, null, null);
117 }
118
119 /**
120 * Public constructor.
121 *
122 * @param parent parent widget
123 * @param strings list of strings to show. This is allowed to be null
124 * and set later with setList() or by subclasses.
125 * @param x column relative to parent
126 * @param y row relative to parent
127 * @param width width of text area
128 * @param height height of text area
129 * @param enterAction action to perform when an item is selected
130 * @param moveAction action to perform when the user navigates to a new
131 * item with arrow/page keys
132 */
133 public TList(final TWidget parent, final List<String> strings, final int x,
134 final int y, final int width, final int height,
135 final TAction enterAction, final TAction moveAction) {
136
137 this(parent, strings, x, y, width, height, enterAction, moveAction,
138 null);
139 }
140
141 /**
142 * Public constructor.
143 *
144 * @param parent parent widget
145 * @param strings list of strings to show. This is allowed to be null
146 * and set later with setList() or by subclasses.
147 * @param x column relative to parent
148 * @param y row relative to parent
149 * @param width width of text area
150 * @param height height of text area
151 * @param enterAction action to perform when an item is selected
152 * @param moveAction action to perform when the user navigates to a new
153 * item with arrow/page keys
154 * @param singleClickAction action to perform when the user clicks on an
155 * item
156 */
157 public TList(final TWidget parent, final List<String> strings, final int x,
158 final int y, final int width, final int height,
159 final TAction enterAction, final TAction moveAction,
160 final TAction singleClickAction) {
161
162 super(parent, x, y, width, height);
163 this.enterAction = enterAction;
164 this.moveAction = moveAction;
165 this.singleClickAction = singleClickAction;
166 this.strings = new ArrayList<String>();
167 if (strings != null) {
168 this.strings.addAll(strings);
169 }
170
171 hScroller = new THScroller(this, 0, getHeight() - 1, getWidth() - 1);
172 vScroller = new TVScroller(this, getWidth() - 1, 0, getHeight() - 1);
173 reflowData();
174 }
175
176 // ------------------------------------------------------------------------
177 // Event handlers ---------------------------------------------------------
178 // ------------------------------------------------------------------------
179
180 /**
181 * Handle mouse press events.
182 *
183 * @param mouse mouse button press event
184 */
185 @Override
186 public void onMouseDown(final TMouseEvent mouse) {
187 if (mouse.isMouseWheelUp()) {
188 verticalDecrement();
189 return;
190 }
191 if (mouse.isMouseWheelDown()) {
192 verticalIncrement();
193 return;
194 }
195
196 if ((mouse.getX() < getWidth() - 1)
197 && (mouse.getY() < getHeight() - 1)
198 ) {
199 if (getVerticalValue() + mouse.getY() < strings.size()) {
200 selectedString = getVerticalValue() + mouse.getY();
201 dispatchSingleClick();
202 }
203 return;
204 }
205
206 // Pass to children
207 super.onMouseDown(mouse);
208 }
209
210 /**
211 * Handle mouse double click.
212 *
213 * @param mouse mouse double click event
214 */
215 @Override
216 public void onMouseDoubleClick(final TMouseEvent mouse) {
217 if ((mouse.getX() < getWidth() - 1)
218 && (mouse.getY() < getHeight() - 1)
219 ) {
220 if (getVerticalValue() + mouse.getY() < strings.size()) {
221 selectedString = getVerticalValue() + mouse.getY();
222 dispatchEnter();
223 }
224 return;
225 }
226
227 // Pass to children
228 super.onMouseDoubleClick(mouse);
229 }
230
231 /**
232 * Handle keystrokes.
233 *
234 * @param keypress keystroke event
235 */
236 @Override
237 public void onKeypress(final TKeypressEvent keypress) {
238 if (keypress.equals(kbLeft)) {
239 horizontalDecrement();
240 } else if (keypress.equals(kbRight)) {
241 horizontalIncrement();
242 } else if (keypress.equals(kbUp)) {
243 if (strings.size() > 0) {
244 if (selectedString >= 0) {
245 if (selectedString > 0) {
246 if (selectedString - getVerticalValue() == 0) {
247 verticalDecrement();
248 }
249 selectedString--;
250 }
251 } else {
252 selectedString = strings.size() - 1;
253 }
254 }
255 if (selectedString >= 0) {
256 dispatchMove();
257 }
258 } else if (keypress.equals(kbDown)) {
259 if (strings.size() > 0) {
260 if (selectedString >= 0) {
261 if (selectedString < strings.size() - 1) {
262 selectedString++;
263 if (selectedString - getVerticalValue() == getHeight() - 1) {
264 verticalIncrement();
265 }
266 }
267 } else {
268 selectedString = 0;
269 }
270 }
271 if (selectedString >= 0) {
272 dispatchMove();
273 }
274 } else if (keypress.equals(kbPgUp)) {
275 bigVerticalDecrement();
276 if (selectedString >= 0) {
277 selectedString -= getHeight() - 1;
278 if (selectedString < 0) {
279 selectedString = 0;
280 }
281 }
282 if (selectedString >= 0) {
283 dispatchMove();
284 }
285 } else if (keypress.equals(kbPgDn)) {
286 bigVerticalIncrement();
287 if (selectedString >= 0) {
288 selectedString += getHeight() - 1;
289 if (selectedString > strings.size() - 1) {
290 selectedString = strings.size() - 1;
291 }
292 }
293 if (selectedString >= 0) {
294 dispatchMove();
295 }
296 } else if (keypress.equals(kbHome)) {
297 toTop();
298 if (strings.size() > 0) {
299 selectedString = 0;
300 }
301 if (selectedString >= 0) {
302 dispatchMove();
303 }
304 } else if (keypress.equals(kbEnd)) {
305 toBottom();
306 if (strings.size() > 0) {
307 selectedString = strings.size() - 1;
308 }
309 if (selectedString >= 0) {
310 dispatchMove();
311 }
312 } else if (keypress.equals(kbTab)) {
313 getParent().switchWidget(true);
314 } else if (keypress.equals(kbShiftTab) || keypress.equals(kbBackTab)) {
315 getParent().switchWidget(false);
316 } else if (keypress.equals(kbEnter)) {
317 if (selectedString >= 0) {
318 dispatchEnter();
319 }
320 } else {
321 // Pass other keys (tab etc.) on
322 super.onKeypress(keypress);
323 }
324 }
325
326 // ------------------------------------------------------------------------
327 // TScrollableWidget ------------------------------------------------------
328 // ------------------------------------------------------------------------
329
330 /**
331 * Override TWidget's width: we need to set child widget widths.
332 *
333 * @param width new widget width
334 */
335 @Override
336 public void setWidth(final int width) {
337 super.setWidth(width);
338 if (hScroller != null) {
339 hScroller.setWidth(getWidth() - 1);
340 }
341 if (vScroller != null) {
342 vScroller.setX(getWidth() - 1);
343 }
344 }
345
346 /**
347 * Override TWidget's height: we need to set child widget heights.
348 *
349 * @param height new widget height
350 */
351 @Override
352 public void setHeight(final int height) {
353 super.setHeight(height);
354 if (hScroller != null) {
355 hScroller.setY(getHeight() - 1);
356 }
357 if (vScroller != null) {
358 vScroller.setHeight(getHeight() - 1);
359 }
360 }
361
362 /**
363 * Resize for a new width/height.
364 */
365 @Override
366 public void reflowData() {
367
368 // Reset the lines
369 selectedString = -1;
370 maxLineWidth = 0;
371
372 for (int i = 0; i < strings.size(); i++) {
373 String line = strings.get(i);
374 int lineLength = StringUtils.width(line);
375 if (lineLength > maxLineWidth) {
376 maxLineWidth = lineLength;
377 }
378 }
379
380 setBottomValue(strings.size() - getHeight() + 1);
381 if (getBottomValue() < 0) {
382 setBottomValue(0);
383 }
384
385 setRightValue(maxLineWidth - getWidth() + 1);
386 if (getRightValue() < 0) {
387 setRightValue(0);
388 }
389 }
390
391 /**
392 * Draw the list.
393 */
394 @Override
395 public void draw() {
396 CellAttributes color = null;
397 int begin = getVerticalValue();
398 int topY = 0;
399 for (int i = begin; i < strings.size(); i++) {
400 String line = strings.get(i);
401 if (line == null) {
402 line = "";
403 }
404 if (getHorizontalValue() < line.length()) {
405 line = line.substring(getHorizontalValue());
406 } else {
407 line = "";
408 }
409 if (i == selectedString) {
410 if (isAbsoluteActive()) {
411 color = getTheme().getColor("tlist.selected");
412 } else {
413 color = getTheme().getColor("tlist.selected.inactive");
414 }
415 } else if (isAbsoluteActive()) {
416 color = getTheme().getColor("tlist");
417 } else {
418 color = getTheme().getColor("tlist.inactive");
419 }
420 String formatString = "%-" + Integer.toString(getWidth() - 1) + "s";
421 putStringXY(0, topY, String.format(formatString, line), color);
422 topY++;
423 if (topY >= getHeight() - 1) {
424 break;
425 }
426 }
427
428 if (isAbsoluteActive()) {
429 color = getTheme().getColor("tlist");
430 } else {
431 color = getTheme().getColor("tlist.inactive");
432 }
433
434 // Pad the rest with blank lines
435 for (int i = topY; i < getHeight() - 1; i++) {
436 hLineXY(0, i, getWidth() - 1, ' ', color);
437 }
438 }
439
440 // ------------------------------------------------------------------------
441 // TList ------------------------------------------------------------------
442 // ------------------------------------------------------------------------
443
444 /**
445 * Get the selection index.
446 *
447 * @return -1 if nothing is selected, otherwise the index into the list
448 */
449 public final int getSelectedIndex() {
450 return selectedString;
451 }
452
453 /**
454 * Set the selected string index.
455 *
456 * @param index -1 to unselect, otherwise the index into the list
457 */
458 public final void setSelectedIndex(final int index) {
459 selectedString = index;
460 }
461
462 /**
463 * Get a selectable string by index.
464 *
465 * @param idx index into list
466 * @return the string at idx in the list
467 */
468 public final String getListItem(final int idx) {
469 return strings.get(idx);
470 }
471
472 /**
473 * Get the selected string.
474 *
475 * @return the selected string, or null of nothing is selected yet
476 */
477 public final String getSelected() {
478 if ((selectedString >= 0) && (selectedString <= strings.size() - 1)) {
479 return strings.get(selectedString);
480 }
481 return null;
482 }
483
484 /**
485 * Get the maximum selection index value.
486 *
487 * @return -1 if the list is empty
488 */
489 public final int getMaxSelectedIndex() {
490 return strings.size() - 1;
491 }
492
493 /**
494 * Get a copy of the list of strings to display.
495 *
496 * @return the list of strings
497 */
498 public final List<String> getList() {
499 return new ArrayList<String>(strings);
500 }
501
502 /**
503 * Set the new list of strings to display.
504 *
505 * @param list new list of strings
506 */
507 public final void setList(final List<String> list) {
508 strings.clear();
509 strings.addAll(list);
510 reflowData();
511 }
512
513 /**
514 * Perform user selection action.
515 */
516 public void dispatchEnter() {
517 assert (selectedString >= 0);
518 assert (selectedString < strings.size());
519 if (enterAction != null) {
520 enterAction.DO(this);
521 }
522 }
523
524 /**
525 * Perform list movement action.
526 */
527 public void dispatchMove() {
528 assert (selectedString >= 0);
529 assert (selectedString < strings.size());
530 if (moveAction != null) {
531 moveAction.DO(this);
532 }
533 }
534
535 /**
536 * Perform single-click action.
537 */
538 public void dispatchSingleClick() {
539 assert (selectedString >= 0);
540 assert (selectedString < strings.size());
541 if (singleClickAction != null) {
542 singleClickAction.DO(this);
543 }
544 }
545
546 }