Merge branch 'subtree'
[fanfix.git] / src / be / nikiroo / jexer / TTable.java
1 /*
2 * Jexer - Java Text User Interface
3 *
4 * The MIT License (MIT)
5 *
6 * Copyright (C) 2019 David "Niki" ROULET
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 David ROULET [niki@nikiroo.be]
27 * @version 1
28 */
29 package be.nikiroo.jexer;
30
31 import java.util.ArrayList;
32 import java.util.Collection;
33 import java.util.List;
34
35 import javax.swing.table.TableModel;
36
37 import be.nikiroo.jexer.TTableCellRenderer.CellRendererMode;
38 import jexer.TAction;
39 import jexer.TWidget;
40 import jexer.bits.CellAttributes;
41
42 /**
43 * A table widget to display and browse through tabular data.
44 * <p>
45 * Currently, you can only select a line (a row) at a time, but the data you
46 * present is still tabular. You also access the data in a tabular way (by
47 * <tt>(raw,column)</tt>).
48 *
49 * @author niki
50 */
51 public class TTable extends TBrowsableWidget {
52 // Default renderers use text mode
53 static private TTableCellRenderer defaultSeparatorRenderer = new TTableCellRendererText(
54 CellRendererMode.SEPARATOR);
55 static private TTableCellRenderer defaultHeaderRenderer = new TTableCellRendererText(
56 CellRendererMode.HEADER);
57 static private TTableCellRenderer defaultHeaderSeparatorRenderer = new TTableCellRendererText(
58 CellRendererMode.HEADER_SEPARATOR);
59
60 private boolean showHeader;
61
62 private List<TTableColumn> columns = new ArrayList<TTableColumn>();
63 private TableModel model;
64
65 private int selectedColumn;
66
67 private TTableCellRenderer separatorRenderer;
68 private TTableCellRenderer headerRenderer;
69 private TTableCellRenderer headerSeparatorRenderer;
70
71 /**
72 * The action to perform when the user selects an item (clicks or enter).
73 */
74 private TAction enterAction = null;
75
76 /**
77 * The action to perform when the user navigates with keyboard.
78 */
79 private TAction moveAction = null;
80
81 /**
82 * Create a new {@link TTable}.
83 *
84 * @param parent
85 * the parent widget
86 * @param x
87 * the X position
88 * @param y
89 * the Y position
90 * @param width
91 * the width of the {@link TTable}
92 * @param height
93 * the height of the {@link TTable}
94 * @param enterAction
95 * an action to call when a cell is selected
96 * @param moveAction
97 * an action to call when the currently active cell is changed
98 */
99 public TTable(TWidget parent, int x, int y, int width, int height,
100 final TAction enterAction, final TAction moveAction) {
101 this(parent, x, y, width, height, enterAction, moveAction, null, false);
102 }
103
104 /**
105 * Create a new {@link TTable}.
106 *
107 * @param parent
108 * the parent widget
109 * @param x
110 * the X position
111 * @param y
112 * the Y position
113 * @param width
114 * the width of the {@link TTable}
115 * @param height
116 * the height of the {@link TTable}
117 * @param enterAction
118 * an action to call when a cell is selected
119 * @param moveAction
120 * an action to call when the currently active cell is changed
121 * @param headers
122 * the headers of the {@link TTable}
123 * @param showHeaders
124 * TRUE to show the headers on screen
125 */
126 public TTable(TWidget parent, int x, int y, int width, int height,
127 final TAction enterAction, final TAction moveAction,
128 List<? extends Object> headers, boolean showHeaders) {
129 super(parent, x, y, width, height);
130
131 this.model = new TTableModel(new Object[][] {});
132 setSelectedRow(-1);
133 this.selectedColumn = -1;
134
135 setHeaders(headers, showHeaders);
136
137 this.enterAction = enterAction;
138 this.moveAction = moveAction;
139
140 reflowData();
141 }
142
143 /**
144 * The data model (containing the actual data) used by this {@link TTable},
145 * as with the usual Swing tables.
146 *
147 * @return the model
148 */
149 public TableModel getModel() {
150 return model;
151 }
152
153 /**
154 * The data model (containing the actual data) used by this {@link TTable},
155 * as with the usual Swing tables.
156 * <p>
157 * Will reset all the rendering cells.
158 *
159 * @param model
160 * the new model
161 */
162 public void setModel(TableModel model) {
163 this.model = model;
164 reflowData();
165 }
166
167 /**
168 * The columns used by this {@link TTable} (you need to access them if you
169 * want to change the way they are rendered, for instance, or their size).
170 *
171 * @return the columns
172 */
173 public List<TTableColumn> getColumns() {
174 return columns;
175 }
176
177 /**
178 * The {@link TTableCellRenderer} used by the separators (one separator
179 * between two data columns).
180 *
181 * @return the renderer, or the default one if none is set (never NULL)
182 */
183 public TTableCellRenderer getSeparatorRenderer() {
184 return separatorRenderer != null ? separatorRenderer
185 : defaultSeparatorRenderer;
186 }
187
188 /**
189 * The {@link TTableCellRenderer} used by the separators (one separator
190 * between two data columns).
191 *
192 * @param separatorRenderer
193 * the new renderer, or NULL to use the default renderer
194 */
195 public void setSeparatorRenderer(TTableCellRenderer separatorRenderer) {
196 this.separatorRenderer = separatorRenderer;
197 }
198
199 /**
200 * The {@link TTableCellRenderer} used by the headers (if
201 * {@link TTable#isShowHeader()} is enabled, the first line represents the
202 * headers with the column names).
203 *
204 * @return the renderer, or the default one if none is set (never NULL)
205 */
206 public TTableCellRenderer getHeaderRenderer() {
207 return headerRenderer != null ? headerRenderer : defaultHeaderRenderer;
208 }
209
210 /**
211 * The {@link TTableCellRenderer} used by the headers (if
212 * {@link TTable#isShowHeader()} is enabled, the first line represents the
213 * headers with the column names).
214 *
215 * @param headerRenderer
216 * the new renderer, or NULL to use the default renderer
217 */
218 public void setHeaderRenderer(TTableCellRenderer headerRenderer) {
219 this.headerRenderer = headerRenderer;
220 }
221
222 /**
223 * The {@link TTableCellRenderer} to use on separators in header lines (see
224 * the related methods to understand what each of them is).
225 *
226 * @return the renderer, or the default one if none is set (never NULL)
227 */
228 public TTableCellRenderer getHeaderSeparatorRenderer() {
229 return headerSeparatorRenderer != null ? headerSeparatorRenderer
230 : defaultHeaderSeparatorRenderer;
231 }
232
233 /**
234 * The {@link TTableCellRenderer} to use on separators in header lines (see
235 * the related methods to understand what each of them is).
236 *
237 * @param headerSeparatorRenderer
238 * the new renderer, or NULL to use the default renderer
239 */
240 public void setHeaderSeparatorRenderer(
241 TTableCellRenderer headerSeparatorRenderer) {
242 this.headerSeparatorRenderer = headerSeparatorRenderer;
243 }
244
245 /**
246 * Show the header row on this {@link TTable}.
247 *
248 * @return TRUE if we show them
249 */
250 public boolean isShowHeader() {
251 return showHeader;
252 }
253
254 /**
255 * Show the header row on this {@link TTable}.
256 *
257 * @param showHeader
258 * TRUE to show them
259 */
260 public void setShowHeader(boolean showHeader) {
261 this.showHeader = showHeader;
262 setYOffset(showHeader ? 2 : 0);
263 reflowData();
264 }
265
266 /**
267 * Change the headers of the table.
268 * <p>
269 * Note that this method is a convenience method that will create columns of
270 * the corresponding names and set them. As such, the previous columns if
271 * any will be replaced.
272 *
273 * @param headers
274 * the new headers
275 */
276 public void setHeaders(List<? extends Object> headers) {
277 setHeaders(headers, showHeader);
278 }
279
280 /**
281 * Change the headers of the table.
282 * <p>
283 * Note that this method is a convenience method that will create columns of
284 * the corresponding names and set them in the same order. As such, the
285 * previous columns if any will be replaced.
286 *
287 * @param headers
288 * the new headers
289 * @param showHeader
290 * TRUE to show them on screen
291 */
292 public void setHeaders(List<? extends Object> headers, boolean showHeader) {
293 if (headers == null) {
294 headers = new ArrayList<Object>();
295 }
296
297 int i = 0;
298 this.columns = new ArrayList<TTableColumn>();
299 for (Object header : headers) {
300 this.columns.add(new TTableColumn(i++, header, getModel()));
301 }
302
303 setShowHeader(showHeader);
304 }
305
306 /**
307 * Set the data and create a new {@link TTableModel} for them.
308 *
309 * @param data
310 * the data to set into this table, as an array of rows, that is,
311 * an array of arrays of values
312 */
313
314 public void setRowData(Object[][] data) {
315 setRowData(TTableModel.convert(data));
316 }
317
318 /**
319 * Set the data and create a new {@link TTableModel} for them.
320 *
321 * @param data
322 * the data to set into this table, as a collection of rows, that
323 * is, a collection of collections of values
324 */
325 public void setRowData(
326 final Collection<? extends Collection<? extends Object>> data) {
327 setModel(new TTableModel(data));
328 }
329
330 /**
331 * The currently selected cell.
332 *
333 * @return the cell
334 */
335 public Object getSelectedCell() {
336 int selectedRow = getSelectedRow();
337 if (selectedRow >= 0 && selectedColumn >= 0) {
338 return model.getValueAt(selectedRow, selectedColumn);
339 }
340
341 return null;
342 }
343
344 @Override
345 public int getRowCount() {
346 if (model == null) {
347 return 0;
348 }
349 return model.getRowCount();
350 }
351
352 @Override
353 public int getColumnCount() {
354 if (model == null) {
355 return 0;
356 }
357 return model.getColumnCount();
358 }
359
360 @Override
361 public void dispatchEnter(int selectedRow) {
362 super.dispatchEnter(selectedRow);
363 if (enterAction != null) {
364 enterAction.DO();
365 }
366 }
367
368 @Override
369 public void dispatchMove(int fromRow, int toRow) {
370 super.dispatchMove(fromRow, toRow);
371 if (moveAction != null) {
372 moveAction.DO();
373 }
374 }
375
376 /**
377 * Clear the content of the {@link TTable}.
378 * <p>
379 * It will not affect the headers.
380 * <p>
381 * You may want to call {@link TTable#reflowData()} when done to see the
382 * changes.
383 */
384 public void clear() {
385 setSelectedRow(-1);
386 selectedColumn = -1;
387 setModel(new TTableModel(new Object[][] {}));
388 }
389
390 @Override
391 public void reflowData() {
392 super.reflowData();
393
394 int lastAutoColumn = -1;
395 int rowWidth = 0;
396
397 int i = 0;
398 for (TTableColumn tcol : columns) {
399 tcol.reflowData();
400
401 if (!tcol.isForcedWidth()) {
402 lastAutoColumn = i;
403 }
404
405 rowWidth += tcol.getWidth();
406
407 i++;
408 }
409
410 if (!columns.isEmpty()) {
411 rowWidth += (i - 1) * getSeparatorRenderer().getWidthOf(null);
412
413 int extraWidth = getWidth() - rowWidth;
414 if (extraWidth > 0) {
415 if (lastAutoColumn < 0) {
416 lastAutoColumn = columns.size() - 1;
417 }
418 TTableColumn tcol = columns.get(lastAutoColumn);
419 tcol.expandWidthTo(tcol.getWidth() + extraWidth);
420 rowWidth += extraWidth;
421 }
422 }
423 }
424
425 @Override
426 public void draw() {
427 int begin = vScroller.getValue();
428 int y = this.showHeader ? 2 : 0;
429
430 if (showHeader) {
431 CellAttributes colorHeaders = getHeaderRenderer()
432 .getCellAttributes(getTheme(), false, isAbsoluteActive());
433 drawRow(-1, 0);
434 String formatString = "%-" + Integer.toString(getWidth()) + "s";
435 String data = String.format(formatString, "");
436 getScreen().putStringXY(0, 1, data, colorHeaders);
437 }
438
439 // draw the actual rows until no more,
440 // then pad the rest with blank rows
441 for (int i = begin; i < getRowCount(); i++) {
442 drawRow(i, y);
443 y++;
444
445 // -2: window borders
446 if (y >= getHeight() - 2 - getHorizontalScroller().getHeight()) {
447 break;
448 }
449 }
450
451 CellAttributes emptyRowColor = getSeparatorRenderer()
452 .getCellAttributes(getTheme(), false, isAbsoluteActive());
453 for (int i = getRowCount(); i < getHeight(); i++) {
454 getScreen().hLineXY(0, y, getWidth() - 1, ' ', emptyRowColor);
455 y++;
456 }
457 }
458
459 @Override
460 protected int getVirtualWidth() {
461 int width = 0;
462
463 if (getColumns() != null) {
464 for (TTableColumn tcol : getColumns()) {
465 width += tcol.getWidth();
466 }
467
468 if (getColumnCount() > 0) {
469 width += (getColumnCount() - 1)
470 * getSeparatorRenderer().getWidthOf(null);
471 }
472 }
473
474 return width;
475 }
476
477 @Override
478 protected int getVirtualHeight() {
479 // TODO: allow changing the height of one row
480 return (showHeader ? 2 : 0) + (getRowCount() * 1);
481 }
482
483 /**
484 * Draw the given row (it <b>MUST</b> exist) at the specified index and
485 * offset.
486 *
487 * @param rowIndex
488 * the index of the row to draw or -1 for the headers
489 * @param y
490 * the Y position
491 */
492 private void drawRow(int rowIndex, int y) {
493 for (int i = 0; i < getColumnCount(); i++) {
494 TTableColumn tcol = columns.get(i);
495 Object value;
496 if (rowIndex < 0) {
497 value = tcol.getHeaderValue();
498 } else {
499 value = model.getValueAt(rowIndex, tcol.getModelIndex());
500 }
501
502 if (i > 0) {
503 TTableCellRenderer sep = rowIndex < 0 ? getHeaderSeparatorRenderer()
504 : getSeparatorRenderer();
505 sep.renderTableCell(this, null, rowIndex, i - 1, y);
506 }
507
508 if (rowIndex < 0) {
509 getHeaderRenderer()
510 .renderTableCell(this, value, rowIndex, i, y);
511 } else {
512 tcol.getRenderer().renderTableCell(this, value, rowIndex, i, y);
513 }
514 }
515 }
516 }