Commit | Line | Data |
---|---|---|
daa4106c | 1 | /* |
7668cb45 KL |
2 | * Jexer - Java Text User Interface |
3 | * | |
e16dda65 | 4 | * The MIT License (MIT) |
7668cb45 | 5 | * |
a2018e99 | 6 | * Copyright (C) 2017 Kevin Lamonte |
7668cb45 | 7 | * |
e16dda65 KL |
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: | |
7668cb45 | 14 | * |
e16dda65 KL |
15 | * The above copyright notice and this permission notice shall be included in |
16 | * all copies or substantial portions of the Software. | |
7668cb45 | 17 | * |
e16dda65 KL |
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. | |
7668cb45 KL |
25 | * |
26 | * @author Kevin Lamonte [kevin.lamonte@gmail.com] | |
27 | * @version 1 | |
28 | */ | |
29 | package jexer; | |
30 | ||
7668cb45 KL |
31 | import jexer.event.TKeypressEvent; |
32 | import jexer.event.TMouseEvent; | |
33 | import static jexer.TKeypress.*; | |
34 | ||
35 | /** | |
36 | * TTreeView implements a simple tree view. | |
37 | */ | |
38 | public class TTreeView extends TWidget { | |
39 | ||
40 | /** | |
41 | * Vertical scrollbar. | |
42 | */ | |
43 | private TVScroller vScroller; | |
44 | ||
45 | /** | |
0d47c546 | 46 | * Horizontal scrollbar. |
7668cb45 | 47 | */ |
0d47c546 KL |
48 | private THScroller hScroller; |
49 | ||
50 | /** | |
51 | * Get the horizontal scrollbar. This is used by TTreeItem.draw(), and | |
52 | * potentially subclasses. | |
329fd62e KL |
53 | * |
54 | * @return the horizontal scrollbar | |
0d47c546 KL |
55 | */ |
56 | public final THScroller getHScroller() { | |
57 | return hScroller; | |
58 | } | |
7668cb45 KL |
59 | |
60 | /** | |
61 | * Root of the tree. | |
62 | */ | |
63 | private TTreeItem treeRoot; | |
64 | ||
65 | /** | |
66 | * Get the root of the tree. | |
67 | * | |
68 | * @return the root of the tree | |
69 | */ | |
70 | public final TTreeItem getTreeRoot() { | |
71 | return treeRoot; | |
72 | } | |
73 | ||
74 | /** | |
75 | * Set the root of the tree. | |
76 | * | |
77 | * @param treeRoot the new root of the tree | |
78 | */ | |
79 | public final void setTreeRoot(final TTreeItem treeRoot) { | |
80 | this.treeRoot = treeRoot; | |
81 | } | |
82 | ||
83 | /** | |
84 | * Maximum width of a single line. | |
85 | */ | |
86 | private int maxLineWidth; | |
87 | ||
88 | /** | |
89 | * Only one of my children can be selected. | |
90 | */ | |
91 | private TTreeItem selectedItem = null; | |
92 | ||
93 | /** | |
94 | * If true, move the window to put the selected item in view. This | |
95 | * normally only happens once after setting treeRoot. | |
96 | */ | |
329fd62e | 97 | private boolean centerWindow = false; |
7668cb45 KL |
98 | |
99 | /** | |
100 | * The action to perform when the user selects an item. | |
101 | */ | |
102 | private TAction action = null; | |
103 | ||
104 | /** | |
105 | * Set treeRoot. | |
106 | * | |
107 | * @param treeRoot ultimate root of tree | |
108 | * @param centerWindow if true, move the window to put the root in view | |
109 | */ | |
329fd62e KL |
110 | public void setTreeRoot(final TTreeItem treeRoot, |
111 | final boolean centerWindow) { | |
112 | ||
7668cb45 KL |
113 | this.treeRoot = treeRoot; |
114 | this.centerWindow = centerWindow; | |
115 | } | |
116 | ||
117 | /** | |
118 | * Public constructor. | |
119 | * | |
120 | * @param parent parent widget | |
121 | * @param x column relative to parent | |
122 | * @param y row relative to parent | |
123 | * @param width width of tree view | |
124 | * @param height height of tree view | |
125 | */ | |
126 | public TTreeView(final TWidget parent, final int x, final int y, | |
127 | final int width, final int height) { | |
128 | ||
129 | this(parent, x, y, width, height, null); | |
130 | } | |
131 | ||
132 | /** | |
133 | * Public constructor. | |
134 | * | |
135 | * @param parent parent widget | |
136 | * @param x column relative to parent | |
137 | * @param y row relative to parent | |
138 | * @param width width of tree view | |
139 | * @param height height of tree view | |
140 | * @param action action to perform when an item is selected | |
141 | */ | |
142 | public TTreeView(final TWidget parent, final int x, final int y, | |
143 | final int width, final int height, final TAction action) { | |
144 | ||
145 | super(parent, x, y, width, height); | |
146 | this.action = action; | |
147 | } | |
148 | ||
149 | /** | |
150 | * Get the tree view item that was selected. | |
151 | * | |
152 | * @return the selected item, or null if no item is selected | |
153 | */ | |
154 | public final TTreeItem getSelected() { | |
155 | return selectedItem; | |
156 | } | |
157 | ||
158 | /** | |
0d47c546 | 159 | * Set the new selected tree view item. |
7668cb45 KL |
160 | * |
161 | * @param item new item that became selected | |
162 | */ | |
0d47c546 | 163 | public void setSelected(final TTreeItem item) { |
7668cb45 KL |
164 | if (item != null) { |
165 | item.setSelected(true); | |
166 | } | |
167 | if ((selectedItem != null) && (selectedItem != item)) { | |
168 | selectedItem.setSelected(false); | |
169 | } | |
170 | selectedItem = item; | |
171 | } | |
172 | ||
173 | /** | |
0d47c546 | 174 | * Perform user selection action. |
7668cb45 | 175 | */ |
0d47c546 | 176 | public void dispatch() { |
7668cb45 KL |
177 | if (action != null) { |
178 | action.DO(); | |
179 | } | |
180 | } | |
181 | ||
182 | /** | |
183 | * Update (or instantiate) vScroller and hScroller. | |
184 | */ | |
185 | private void updateScrollers() { | |
186 | // Setup vertical scroller | |
187 | if (vScroller == null) { | |
188 | vScroller = new TVScroller(this, getWidth() - 1, 0, | |
189 | getHeight() - 1); | |
190 | vScroller.setValue(0); | |
191 | vScroller.setTopValue(0); | |
192 | } | |
193 | vScroller.setX(getWidth() - 1); | |
194 | vScroller.setHeight(getHeight() - 1); | |
195 | vScroller.setBigChange(getHeight() - 1); | |
196 | ||
197 | // Setup horizontal scroller | |
198 | if (hScroller == null) { | |
199 | hScroller = new THScroller(this, 0, getHeight() - 1, | |
200 | getWidth() - 1); | |
201 | hScroller.setValue(0); | |
202 | hScroller.setLeftValue(0); | |
203 | } | |
204 | hScroller.setY(getHeight() - 1); | |
205 | hScroller.setWidth(getWidth() - 1); | |
206 | hScroller.setBigChange(getWidth() - 1); | |
207 | } | |
208 | ||
209 | /** | |
210 | * Resize text and scrollbars for a new width/height. | |
211 | */ | |
212 | public void reflow() { | |
213 | int selectedRow = 0; | |
214 | boolean foundSelectedRow = false; | |
215 | ||
216 | updateScrollers(); | |
217 | if (treeRoot == null) { | |
218 | return; | |
219 | } | |
220 | ||
221 | // Make each child invisible/inactive to start, expandTree() will | |
222 | // reactivate the visible ones. | |
223 | for (TWidget widget: getChildren()) { | |
224 | if (widget instanceof TTreeItem) { | |
225 | TTreeItem item = (TTreeItem) widget; | |
226 | item.setInvisible(true); | |
227 | item.setEnabled(false); | |
0d47c546 KL |
228 | item.keyboardPrevious = null; |
229 | item.keyboardNext = null; | |
7668cb45 KL |
230 | } |
231 | } | |
232 | ||
233 | // Expand the tree into a linear list | |
234 | getChildren().clear(); | |
235 | getChildren().addAll(treeRoot.expandTree("", true)); | |
0d47c546 KL |
236 | |
237 | // Locate the selected row and maximum line width | |
7668cb45 KL |
238 | for (TWidget widget: getChildren()) { |
239 | TTreeItem item = (TTreeItem) widget; | |
240 | ||
241 | if (item == selectedItem) { | |
242 | foundSelectedRow = true; | |
243 | } | |
329fd62e | 244 | if (!foundSelectedRow) { |
7668cb45 KL |
245 | selectedRow++; |
246 | } | |
247 | ||
248 | int lineWidth = item.getText().length() | |
329fd62e | 249 | + item.getPrefix().length() + 4; |
7668cb45 KL |
250 | if (lineWidth > maxLineWidth) { |
251 | maxLineWidth = lineWidth; | |
252 | } | |
253 | } | |
0d47c546 | 254 | |
7668cb45 KL |
255 | if ((centerWindow) && (foundSelectedRow)) { |
256 | if ((selectedRow < vScroller.getValue()) | |
257 | || (selectedRow > vScroller.getValue() + getHeight() - 2) | |
258 | ) { | |
259 | vScroller.setValue(selectedRow); | |
260 | centerWindow = false; | |
261 | } | |
262 | } | |
263 | updatePositions(); | |
264 | ||
265 | // Rescale the scroll bars | |
266 | vScroller.setBottomValue(getChildren().size() - getHeight() + 1); | |
267 | if (vScroller.getBottomValue() < 0) { | |
268 | vScroller.setBottomValue(0); | |
269 | } | |
270 | /* | |
271 | if (vScroller.getValue() > vScroller.getBottomValue()) { | |
272 | vScroller.setValue(vScroller.getBottomValue()); | |
273 | } | |
274 | */ | |
275 | hScroller.setRightValue(maxLineWidth - getWidth() + 3); | |
276 | if (hScroller.getRightValue() < 0) { | |
277 | hScroller.setRightValue(0); | |
278 | } | |
279 | /* | |
280 | if (hScroller.getValue() > hScroller.getRightValue()) { | |
281 | hScroller.setValue(hScroller.getRightValue()); | |
282 | } | |
283 | */ | |
284 | getChildren().add(hScroller); | |
285 | getChildren().add(vScroller); | |
286 | } | |
287 | ||
288 | /** | |
289 | * Update the Y positions of all the children items. | |
290 | */ | |
291 | private void updatePositions() { | |
292 | if (treeRoot == null) { | |
293 | return; | |
294 | } | |
295 | ||
296 | int begin = vScroller.getValue(); | |
297 | int topY = 0; | |
0d47c546 KL |
298 | |
299 | // As we walk the list we also adjust next/previous pointers, | |
300 | // resulting in a doubly-linked list but only of the expanded items. | |
301 | TTreeItem p = null; | |
302 | ||
7668cb45 KL |
303 | for (int i = 0; i < getChildren().size(); i++) { |
304 | if (!(getChildren().get(i) instanceof TTreeItem)) { | |
0d47c546 | 305 | // Skip the scrollbars |
7668cb45 KL |
306 | continue; |
307 | } | |
308 | TTreeItem item = (TTreeItem) getChildren().get(i); | |
309 | ||
0d47c546 KL |
310 | if (p != null) { |
311 | item.keyboardPrevious = p; | |
312 | p.keyboardNext = item; | |
313 | } | |
314 | p = item; | |
315 | ||
7668cb45 KL |
316 | if (i < begin) { |
317 | // Render invisible | |
318 | item.setEnabled(false); | |
319 | item.setInvisible(true); | |
320 | continue; | |
321 | } | |
322 | ||
323 | if (topY >= getHeight() - 1) { | |
324 | // Render invisible | |
325 | item.setEnabled(false); | |
326 | item.setInvisible(true); | |
327 | continue; | |
328 | } | |
329 | ||
330 | item.setY(topY); | |
331 | item.setEnabled(true); | |
332 | item.setInvisible(false); | |
333 | item.setWidth(getWidth() - 1); | |
334 | topY++; | |
335 | } | |
0d47c546 | 336 | |
7668cb45 KL |
337 | } |
338 | ||
339 | /** | |
340 | * Handle mouse press events. | |
341 | * | |
342 | * @param mouse mouse button press event | |
343 | */ | |
344 | @Override | |
345 | public void onMouseDown(final TMouseEvent mouse) { | |
346 | if (mouse.isMouseWheelUp()) { | |
347 | vScroller.decrement(); | |
348 | } else if (mouse.isMouseWheelDown()) { | |
349 | vScroller.increment(); | |
350 | } else { | |
351 | // Pass to children | |
352 | super.onMouseDown(mouse); | |
353 | } | |
354 | ||
355 | // Update the screen after the scrollbars have moved | |
356 | reflow(); | |
357 | } | |
358 | ||
359 | /** | |
360 | * Handle mouse release events. | |
361 | * | |
362 | * @param mouse mouse button release event | |
363 | */ | |
364 | @Override | |
329fd62e | 365 | public void onMouseUp(final TMouseEvent mouse) { |
7668cb45 KL |
366 | // Pass to children |
367 | super.onMouseDown(mouse); | |
368 | ||
369 | // Update the screen after any thing has expanded/contracted | |
370 | reflow(); | |
371 | } | |
372 | ||
373 | /** | |
374 | * Handle keystrokes. | |
375 | * | |
376 | * @param keypress keystroke event | |
377 | */ | |
378 | @Override | |
379 | public void onKeypress(final TKeypressEvent keypress) { | |
0d47c546 KL |
380 | if (keypress.equals(kbShiftLeft) |
381 | || keypress.equals(kbCtrlLeft) | |
382 | || keypress.equals(kbAltLeft) | |
383 | ) { | |
7668cb45 | 384 | hScroller.decrement(); |
0d47c546 KL |
385 | } else if (keypress.equals(kbShiftRight) |
386 | || keypress.equals(kbCtrlRight) | |
387 | || keypress.equals(kbAltRight) | |
388 | ) { | |
7668cb45 | 389 | hScroller.increment(); |
0d47c546 KL |
390 | } else if (keypress.equals(kbShiftUp) |
391 | || keypress.equals(kbCtrlUp) | |
392 | || keypress.equals(kbAltUp) | |
393 | ) { | |
7668cb45 | 394 | vScroller.decrement(); |
0d47c546 KL |
395 | } else if (keypress.equals(kbShiftDown) |
396 | || keypress.equals(kbCtrlDown) | |
397 | || keypress.equals(kbAltDown) | |
398 | ) { | |
7668cb45 | 399 | vScroller.increment(); |
0d47c546 KL |
400 | } else if (keypress.equals(kbShiftPgUp) |
401 | || keypress.equals(kbCtrlPgUp) | |
402 | || keypress.equals(kbAltPgUp) | |
403 | ) { | |
7668cb45 | 404 | vScroller.bigDecrement(); |
0d47c546 KL |
405 | } else if (keypress.equals(kbShiftPgDn) |
406 | || keypress.equals(kbCtrlPgDn) | |
407 | || keypress.equals(kbAltPgDn) | |
408 | ) { | |
7668cb45 KL |
409 | vScroller.bigIncrement(); |
410 | } else if (keypress.equals(kbHome)) { | |
411 | vScroller.toTop(); | |
412 | } else if (keypress.equals(kbEnd)) { | |
413 | vScroller.toBottom(); | |
414 | } else if (keypress.equals(kbEnter)) { | |
415 | if (selectedItem != null) { | |
416 | dispatch(); | |
417 | } | |
0d47c546 KL |
418 | } else if (keypress.equals(kbUp)) { |
419 | // Select the previous item | |
420 | if (selectedItem != null) { | |
421 | TTreeItem oldItem = selectedItem; | |
422 | if (selectedItem.keyboardPrevious != null) { | |
423 | setSelected(selectedItem.keyboardPrevious); | |
424 | if (oldItem.getY() == 0) { | |
425 | vScroller.decrement(); | |
426 | } | |
427 | } | |
428 | } | |
429 | } else if (keypress.equals(kbDown)) { | |
430 | // Select the next item | |
431 | if (selectedItem != null) { | |
432 | TTreeItem oldItem = selectedItem; | |
433 | if (selectedItem.keyboardNext != null) { | |
434 | setSelected(selectedItem.keyboardNext); | |
435 | if (oldItem.getY() == getHeight() - 2) { | |
436 | vScroller.increment(); | |
437 | } | |
438 | } | |
439 | } | |
a043164f KL |
440 | } else if (keypress.equals(kbTab)) { |
441 | getParent().switchWidget(true); | |
442 | return; | |
443 | } else if (keypress.equals(kbShiftTab) | |
444 | || keypress.equals(kbBackTab)) { | |
445 | getParent().switchWidget(false); | |
446 | return; | |
0d47c546 KL |
447 | } else if (selectedItem != null) { |
448 | // Give the TTreeItem a chance to handle arrow keys | |
449 | selectedItem.onKeypress(keypress); | |
7668cb45 | 450 | } else { |
0d47c546 | 451 | // Pass other keys (tab etc.) on to TWidget's handler. |
7668cb45 | 452 | super.onKeypress(keypress); |
a043164f | 453 | return; |
7668cb45 KL |
454 | } |
455 | ||
456 | // Update the screen after any thing has expanded/contracted | |
457 | reflow(); | |
458 | } | |
459 | ||
460 | } |