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