LICENSE CHANGED TO MIT
[nikiroo-utils.git] / src / jexer / TTreeView.java
CommitLineData
daa4106c 1/*
7668cb45
KL
2 * Jexer - Java Text User Interface
3 *
e16dda65 4 * The MIT License (MIT)
7668cb45 5 *
e16dda65 6 * Copyright (C) 2016 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 */
29package jexer;
30
7668cb45
KL
31import jexer.event.TKeypressEvent;
32import jexer.event.TMouseEvent;
33import static jexer.TKeypress.*;
34
35/**
36 * TTreeView implements a simple tree view.
37 */
38public 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}