weblib: fixes
[fanfix.git] / src / jexer / TComboBox.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.GraphicsChars;
36 import jexer.event.TKeypressEvent;
37 import jexer.event.TMouseEvent;
38 import jexer.event.TResizeEvent;
39 import jexer.event.TResizeEvent.Type;
40 import static jexer.TKeypress.*;
41
42 /**
43 * TComboBox implements a combobox containing a drop-down list and edit
44 * field. Alt-Down can be used to show the drop-down.
45 */
46 public class TComboBox extends TWidget {
47
48 // ------------------------------------------------------------------------
49 // Variables --------------------------------------------------------------
50 // ------------------------------------------------------------------------
51
52 /**
53 * The list of items in the drop-down.
54 */
55 private TList list;
56
57 /**
58 * The edit field containing the value to return.
59 */
60 private TField field;
61
62 /**
63 * The action to perform when the user selects an item (clicks or enter).
64 */
65 private TAction updateAction = null;
66
67 /**
68 * If true, the field cannot be updated to a value not on the list.
69 */
70 private boolean limitToListValue = true;
71
72 /**
73 * The height of the list of values when it is shown, or -1 to use the
74 * number of values in the list as the height.
75 */
76 private int valuesHeight = -1;
77
78 /**
79 * The values shown by the drop-down list.
80 */
81 private List<String> values = new ArrayList<String>();
82
83 /**
84 * When looking for a link between the displayed text and the list
85 * of values, do a case sensitive search.
86 */
87 private boolean caseSensitive = true;
88
89 /**
90 * The maximum height of the values drop-down when it is visible.
91 */
92 private int maxValuesHeight = 3;
93
94 // ------------------------------------------------------------------------
95 // Constructors -----------------------------------------------------------
96 // ------------------------------------------------------------------------
97
98 /**
99 * Public constructor.
100 *
101 * @param parent parent widget
102 * @param x column relative to parent
103 * @param y row relative to parent
104 * @param width visible combobox width, including the down-arrow
105 * @param values the possible values for the box, shown in the drop-down
106 * @param valuesIndex the initial index in values, or -1 for no default
107 * value
108 * @param valuesHeight the height of the values drop-down when it is
109 * visible, or -1 to use the number of values as the height of the list
110 * @param updateAction action to call when a new value is selected from
111 * the list or enter is pressed in the edit field
112 */
113 public TComboBox(final TWidget parent, final int x, final int y,
114 final int width, final List<String> values, final int valuesIndex,
115 final int valuesHeight, final TAction updateAction) {
116
117 // Set parent and window
118 super(parent, x, y, width, 1);
119
120 assert (values != null);
121
122 this.updateAction = updateAction;
123 this.values = values;
124 this.valuesHeight = valuesHeight;
125
126 field = new TField(this, 0, 0, Math.max(0, width - 3), false, "",
127 updateAction, null);
128 if (valuesIndex >= 0) {
129 field.setText(values.get(valuesIndex));
130 }
131
132 setHeight(1);
133 if (limitToListValue) {
134 field.setEnabled(false);
135 } else {
136 activate(field);
137 }
138 }
139
140 // ------------------------------------------------------------------------
141 // Event handlers ---------------------------------------------------------
142 // ------------------------------------------------------------------------
143
144 /**
145 * Returns true if the mouse is currently on the down arrow.
146 *
147 * @param mouse mouse event
148 * @return true if the mouse is currently on the down arrow
149 */
150 private boolean mouseOnArrow(final TMouseEvent mouse) {
151 if ((mouse.getY() == 0)
152 && (mouse.getX() >= getWidth() - 3)
153 && (mouse.getX() <= getWidth() - 1)
154 ) {
155 return true;
156 }
157 return false;
158 }
159
160 /**
161 * Handle mouse down clicks.
162 *
163 * @param mouse mouse button down event
164 */
165 @Override
166 public void onMouseDown(final TMouseEvent mouse) {
167 if ((mouseOnArrow(mouse)) && (mouse.isMouse1())) {
168 // Make the list visible or not.
169 if (list != null) {
170 hideDropdown();
171 } else {
172 displayDropdown();
173 }
174 }
175
176 // Pass to parent for the things we don't care about.
177 super.onMouseDown(mouse);
178 }
179
180 /**
181 * Handle keystrokes.
182 *
183 * @param keypress keystroke event
184 */
185 @Override
186 public void onKeypress(final TKeypressEvent keypress) {
187 if (keypress.equals(kbEsc)) {
188 if (list != null) {
189 hideDropdown();
190 return;
191 }
192 }
193
194 if (keypress.equals(kbAltDown)) {
195 displayDropdown();
196 return;
197 }
198
199 if (keypress.equals(kbTab)
200 || (keypress.equals(kbShiftTab))
201 || (keypress.equals(kbBackTab))
202 ) {
203 if (list != null) {
204 hideDropdown();
205 return;
206 }
207 }
208
209 // Pass to parent for the things we don't care about.
210 super.onKeypress(keypress);
211 }
212
213 // ------------------------------------------------------------------------
214 // TWidget ----------------------------------------------------------------
215 // ------------------------------------------------------------------------
216
217 /**
218 * Override TWidget's width: we need to set child widget widths.
219 *
220 * @param width new widget width
221 */
222 @Override
223 public void setWidth(final int width) {
224 if (field != null) {
225 field.setWidth(width - 3);
226 }
227 if (list != null) {
228 list.setWidth(width);
229 }
230 super.setWidth(width);
231 }
232
233 /**
234 * Override TWidget's height: we can only set height at construction
235 * time.
236 *
237 * @param height new widget height (ignored)
238 */
239 @Override
240 public void setHeight(final int height) {
241 // Do nothing
242 }
243
244 /**
245 * Draw the combobox down arrow.
246 */
247 @Override
248 public void draw() {
249 CellAttributes comboBoxColor;
250
251 if (!isAbsoluteActive()) {
252 // We lost focus, turn off the list.
253 hideDropdown();
254 }
255
256 if (isAbsoluteActive()) {
257 comboBoxColor = getTheme().getColor("tcombobox.active");
258 } else {
259 comboBoxColor = getTheme().getColor("tcombobox.inactive");
260 }
261
262 putCharXY(getWidth() - 3, 0, GraphicsChars.DOWNARROWLEFT,
263 comboBoxColor);
264 putCharXY(getWidth() - 2, 0, GraphicsChars.DOWNARROW,
265 comboBoxColor);
266 putCharXY(getWidth() - 1, 0, GraphicsChars.DOWNARROWRIGHT,
267 comboBoxColor);
268 }
269
270 // ------------------------------------------------------------------------
271 // TComboBox --------------------------------------------------------------
272 // ------------------------------------------------------------------------
273
274 /**
275 * Hide the drop-down list.
276 */
277 public void hideList() {
278 list.setEnabled(false);
279 list.setVisible(false);
280 super.setHeight(1);
281 if (limitToListValue == false) {
282 activate(field);
283 }
284 }
285
286 /**
287 * Show the drop-down list.
288 */
289 public void showList() {
290 list.setEnabled(true);
291 list.setVisible(true);
292 super.setHeight(list.getHeight() + 1);
293 activate(list);
294 }
295
296 /**
297 * Get combobox text value.
298 *
299 * @return text in the edit field
300 */
301 public String getText() {
302 return field.getText();
303 }
304
305 /**
306 * Set combobox text value.
307 *
308 * @param text the new text in the edit field
309 */
310 public void setText(final String text) {
311 setText(text, true);
312 }
313
314 /**
315 * Set combobox text value.
316 *
317 * @param text the new text in the edit field
318 * @param caseSensitive if true, perform a case-sensitive search for the
319 * list item
320 */
321 public void setText(final String text, final boolean caseSensitive) {
322 this.caseSensitive = caseSensitive;
323 field.setText(text);
324 if (list != null) {
325 displayDropdown();
326 }
327 }
328
329 /**
330 * Set combobox text to one of the list values.
331 *
332 * @param index the index in the list
333 */
334 public void setIndex(final int index) {
335 list.setSelectedIndex(index);
336 field.setText(list.getSelected());
337 }
338
339 /**
340 * Get a copy of the list of strings to display.
341 *
342 * @return the list of strings
343 */
344 public final List<String> getList() {
345 return list.getList();
346 }
347
348 /**
349 * Set the new list of strings to display.
350 *
351 * @param list new list of strings
352 */
353 public final void setList(final List<String> list) {
354 this.list.setList(list);
355 this.list.setHeight(Math.max(3, Math.min(list.size() + 1,
356 maxValuesHeight)));
357 field.setText("");
358 }
359
360 /**
361 * Make sure the widget displays all its elements correctly according to
362 * the current size and content.
363 */
364 public void reflowData() {
365 // TODO: why setW/setH/reflow not enough for the scrollbars?
366 TList list = this.list;
367 if (list != null) {
368 int valuesHeight = this.valuesHeight;
369 if (valuesHeight < 0) {
370 valuesHeight = values == null ? 0 : values.size() + 1;
371 }
372
373 list.onResize(new TResizeEvent(Type.WIDGET, getWidth(),
374 valuesHeight));
375 setHeight(valuesHeight + 1);
376 }
377
378 field.onResize(new TResizeEvent(Type.WIDGET, getWidth(),
379 field.getHeight()));
380 }
381
382 @Override
383 public void onResize(TResizeEvent resize) {
384 super.onResize(resize);
385 reflowData();
386 }
387
388 /**
389 * Display the drop-down menu represented by {@link TComboBox#list}.
390 */
391 private void displayDropdown() {
392 if (this.list != null) {
393 hideDropdown();
394 }
395
396 int valuesHeight = this.valuesHeight;
397 if (valuesHeight < 0) {
398 valuesHeight = values == null ? 0 : values.size() + 1;
399 }
400
401 TList list = new TList(this, values, 0, 1, getWidth(), valuesHeight,
402 new TAction() {
403 @Override
404 public void DO() {
405 TList list = TComboBox.this.list;
406 if (list == null) {
407 return;
408 }
409
410 field.setText(list.getSelected());
411 hideDropdown();
412
413 if (updateAction != null) {
414 updateAction.DO();
415 }
416 }
417 }
418 );
419
420 int i = -1;
421 if (values != null) {
422 String current = field.getText();
423 for (i = 0 ; i < values.size() ; i++) {
424 String value = values.get(i);
425 if ((caseSensitive && current.equals(value))
426 || (!caseSensitive && current.equalsIgnoreCase(value))) {
427 break;
428 }
429 }
430
431 if (i >= values.size()) {
432 i = -1;
433 }
434 }
435 list.setSelectedIndex(i);
436
437 list.setEnabled(true);
438 list.setVisible(true);
439
440 this.list = list;
441
442 reflowData();
443 activate(list);
444 }
445
446 /**
447 * Hide the drop-down menu represented by {@link TComboBox#list}.
448 */
449 private void hideDropdown() {
450 TList list = this.list;
451
452 if (list != null) {
453 list.setEnabled(false);
454 list.setVisible(false);
455 removeChild(list);
456
457 setHeight(1);
458 if (limitToListValue == false) {
459 activate(field);
460 }
461
462 this.list = null;
463 }
464 }
465 }