Commit | Line | Data |
---|---|---|
051e2913 KL |
1 | /* |
2 | * Jexer - Java Text User Interface | |
3 | * | |
4 | * The MIT License (MIT) | |
5 | * | |
a69ed767 | 6 | * Copyright (C) 2019 Kevin Lamonte |
051e2913 KL |
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 | ||
1f1d1146 | 31 | import java.util.ArrayList; |
051e2913 KL |
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; | |
1f1d1146 NR |
38 | import jexer.event.TResizeEvent; |
39 | import jexer.event.TResizeEvent.Type; | |
051e2913 KL |
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 | ||
a69ed767 KL |
67 | /** |
68 | * If true, the field cannot be updated to a value not on the list. | |
69 | */ | |
70 | private boolean limitToListValue = true; | |
1f1d1146 NR |
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; | |
a69ed767 | 88 | |
8ab60a33 KL |
89 | /** |
90 | * The maximum height of the values drop-down when it is visible. | |
91 | */ | |
92 | private int maxValuesHeight = 3; | |
93 | ||
051e2913 KL |
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 | |
1f1d1146 NR |
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 | |
051e2913 KL |
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, | |
1f1d1146 | 115 | final int valuesHeight, final TAction updateAction) { |
051e2913 KL |
116 | |
117 | // Set parent and window | |
118 | super(parent, x, y, width, 1); | |
119 | ||
a69ed767 KL |
120 | assert (values != null); |
121 | ||
051e2913 | 122 | this.updateAction = updateAction; |
1f1d1146 NR |
123 | this.values = values; |
124 | this.valuesHeight = valuesHeight; | |
051e2913 | 125 | |
1f1d1146 NR |
126 | field = new TField(this, 0, 0, Math.max(0, width - 3), false, "", |
127 | updateAction, null); | |
a69ed767 | 128 | if (valuesIndex >= 0) { |
1f1d1146 | 129 | field.setText(values.get(valuesIndex)); |
a69ed767 | 130 | } |
051e2913 | 131 | |
1f1d1146 | 132 | setHeight(1); |
a69ed767 KL |
133 | if (limitToListValue) { |
134 | field.setEnabled(false); | |
135 | } else { | |
136 | activate(field); | |
137 | } | |
051e2913 KL |
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) | |
a69ed767 KL |
152 | && (mouse.getX() >= getWidth() - 3) |
153 | && (mouse.getX() <= getWidth() - 1) | |
051e2913 KL |
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. | |
1f1d1146 NR |
169 | if (list != null) { |
170 | hideDropdown(); | |
051e2913 | 171 | } else { |
1f1d1146 | 172 | displayDropdown(); |
051e2913 KL |
173 | } |
174 | } | |
a69ed767 KL |
175 | |
176 | // Pass to parent for the things we don't care about. | |
177 | super.onMouseDown(mouse); | |
051e2913 KL |
178 | } |
179 | ||
180 | /** | |
181 | * Handle keystrokes. | |
182 | * | |
183 | * @param keypress keystroke event | |
184 | */ | |
185 | @Override | |
186 | public void onKeypress(final TKeypressEvent keypress) { | |
a69ed767 | 187 | if (keypress.equals(kbEsc)) { |
1f1d1146 NR |
188 | if (list != null) { |
189 | hideDropdown(); | |
a69ed767 KL |
190 | return; |
191 | } | |
192 | } | |
193 | ||
051e2913 | 194 | if (keypress.equals(kbAltDown)) { |
1f1d1146 | 195 | displayDropdown(); |
051e2913 KL |
196 | return; |
197 | } | |
198 | ||
199 | if (keypress.equals(kbTab) | |
200 | || (keypress.equals(kbShiftTab)) | |
201 | || (keypress.equals(kbBackTab)) | |
202 | ) { | |
1f1d1146 NR |
203 | if (list != null) { |
204 | hideDropdown(); | |
051e2913 KL |
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 | ||
d8dc8aea | 217 | /** |
8f62f06e | 218 | * Override TWidget's width: we need to set child widget widths. |
d8dc8aea | 219 | * |
8f62f06e | 220 | * @param width new widget width |
d8dc8aea KL |
221 | */ |
222 | @Override | |
223 | public void setWidth(final int width) { | |
fc2af494 KL |
224 | if (field != null) { |
225 | field.setWidth(width - 3); | |
226 | } | |
227 | if (list != null) { | |
228 | list.setWidth(width); | |
229 | } | |
8f62f06e | 230 | super.setWidth(width); |
d8dc8aea KL |
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 | ||
051e2913 KL |
244 | /** |
245 | * Draw the combobox down arrow. | |
246 | */ | |
247 | @Override | |
248 | public void draw() { | |
249 | CellAttributes comboBoxColor; | |
250 | ||
a69ed767 KL |
251 | if (!isAbsoluteActive()) { |
252 | // We lost focus, turn off the list. | |
1f1d1146 | 253 | hideDropdown(); |
a69ed767 KL |
254 | } |
255 | ||
e23ea538 | 256 | if (isAbsoluteActive()) { |
051e2913 KL |
257 | comboBoxColor = getTheme().getColor("tcombobox.active"); |
258 | } else { | |
259 | comboBoxColor = getTheme().getColor("tcombobox.inactive"); | |
260 | } | |
261 | ||
a69ed767 KL |
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, | |
051e2913 KL |
267 | comboBoxColor); |
268 | } | |
269 | ||
270 | // ------------------------------------------------------------------------ | |
271 | // TComboBox -------------------------------------------------------------- | |
272 | // ------------------------------------------------------------------------ | |
273 | ||
8ab60a33 KL |
274 | /** |
275 | * Hide the drop-down list. | |
276 | */ | |
277 | public void hideList() { | |
278 | list.setEnabled(false); | |
279 | list.setVisible(false); | |
d8dc8aea | 280 | super.setHeight(1); |
8ab60a33 KL |
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); | |
d8dc8aea | 292 | super.setHeight(list.getHeight() + 1); |
8ab60a33 KL |
293 | activate(list); |
294 | } | |
295 | ||
051e2913 KL |
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) { | |
af56159c KL |
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) { | |
1f1d1146 NR |
322 | this.caseSensitive = caseSensitive; |
323 | field.setText(text); | |
324 | if (list != null) { | |
325 | displayDropdown(); | |
051e2913 | 326 | } |
051e2913 KL |
327 | } |
328 | ||
a69ed767 KL |
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); | |
8ab60a33 KL |
355 | this.list.setHeight(Math.max(3, Math.min(list.size() + 1, |
356 | maxValuesHeight))); | |
a69ed767 KL |
357 | field.setText(""); |
358 | } | |
1f1d1146 NR |
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 | } | |
a69ed767 | 387 | |
1f1d1146 NR |
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 | } | |
051e2913 | 465 | } |