Commit | Line | Data |
---|---|---|
a3b510ab NR |
1 | package com.googlecode.lanterna.gui2.table; |
2 | ||
3 | import com.googlecode.lanterna.gui2.*; | |
4 | import com.googlecode.lanterna.input.KeyStroke; | |
5 | ||
6 | /** | |
7 | * The table class is an interactable component that displays a grid of cells containing data along with a header of | |
8 | * labels. It supports scrolling when the number of rows and/or columns gets too large to fit and also supports | |
9 | * user selection which is either row-based or cell-based. User will move the current selection by using the arrow keys | |
10 | * on the keyboard. | |
11 | * @param <V> Type of data to store in the table cells, presented through {@code toString()} | |
12 | * @author Martin | |
13 | */ | |
14 | public class Table<V> extends AbstractInteractableComponent<Table<V>> { | |
15 | private TableModel<V> tableModel; | |
16 | private TableHeaderRenderer<V> tableHeaderRenderer; | |
17 | private TableCellRenderer<V> tableCellRenderer; | |
18 | private Runnable selectAction; | |
19 | private boolean cellSelection; | |
20 | private int visibleRows; | |
21 | private int visibleColumns; | |
22 | private int viewTopRow; | |
23 | private int viewLeftColumn; | |
24 | private int selectedRow; | |
25 | private int selectedColumn; | |
26 | private boolean escapeByArrowKey; | |
27 | ||
28 | /** | |
29 | * Creates a new {@code Table} with the number of columns as specified by the array of labels | |
30 | * @param columnLabels Creates one column per label in the array, must be more than one | |
31 | */ | |
32 | public Table(String... columnLabels) { | |
33 | if(columnLabels.length == 0) { | |
34 | throw new IllegalArgumentException("Table needs at least one column"); | |
35 | } | |
36 | this.tableHeaderRenderer = new DefaultTableHeaderRenderer<V>(); | |
37 | this.tableCellRenderer = new DefaultTableCellRenderer<V>(); | |
38 | this.tableModel = new TableModel<V>(columnLabels); | |
39 | this.selectAction = null; | |
40 | this.visibleColumns = 0; | |
41 | this.visibleRows = 0; | |
42 | this.viewTopRow = 0; | |
43 | this.viewLeftColumn = 0; | |
44 | this.cellSelection = false; | |
45 | this.selectedRow = 0; | |
46 | this.selectedColumn = -1; | |
47 | this.escapeByArrowKey = true; | |
48 | } | |
49 | ||
50 | /** | |
51 | * Returns the underlying table model | |
52 | * @return Underlying table model | |
53 | */ | |
54 | public TableModel<V> getTableModel() { | |
55 | return tableModel; | |
56 | } | |
57 | ||
58 | /** | |
59 | * Updates the table with a new table model, effectively replacing the content of the table completely | |
60 | * @param tableModel New table model | |
61 | * @return Itself | |
62 | */ | |
63 | public synchronized Table<V> setTableModel(TableModel<V> tableModel) { | |
64 | if(tableModel == null) { | |
65 | throw new IllegalArgumentException("Cannot assign a null TableModel"); | |
66 | } | |
67 | this.tableModel = tableModel; | |
68 | invalidate(); | |
69 | return this; | |
70 | } | |
71 | ||
72 | /** | |
73 | * Returns the {@code TableCellRenderer} used by this table when drawing cells | |
74 | * @return {@code TableCellRenderer} used by this table when drawing cells | |
75 | */ | |
76 | public TableCellRenderer<V> getTableCellRenderer() { | |
77 | return tableCellRenderer; | |
78 | } | |
79 | ||
80 | /** | |
81 | * Replaces the {@code TableCellRenderer} used by this table when drawing cells | |
82 | * @param tableCellRenderer New {@code TableCellRenderer} to use | |
83 | * @return Itself | |
84 | */ | |
85 | public synchronized Table<V> setTableCellRenderer(TableCellRenderer<V> tableCellRenderer) { | |
86 | this.tableCellRenderer = tableCellRenderer; | |
87 | invalidate(); | |
88 | return this; | |
89 | } | |
90 | ||
91 | /** | |
92 | * Returns the {@code TableHeaderRenderer} used by this table when drawing the table's header | |
93 | * @return {@code TableHeaderRenderer} used by this table when drawing the table's header | |
94 | */ | |
95 | public TableHeaderRenderer<V> getTableHeaderRenderer() { | |
96 | return tableHeaderRenderer; | |
97 | } | |
98 | ||
99 | /** | |
100 | * Replaces the {@code TableHeaderRenderer} used by this table when drawing the table's header | |
101 | * @param tableHeaderRenderer New {@code TableHeaderRenderer} to use | |
102 | * @return Itself | |
103 | */ | |
104 | public synchronized Table<V> setTableHeaderRenderer(TableHeaderRenderer<V> tableHeaderRenderer) { | |
105 | this.tableHeaderRenderer = tableHeaderRenderer; | |
106 | invalidate(); | |
107 | return this; | |
108 | } | |
109 | ||
110 | /** | |
111 | * Sets the number of columns this table should show. If there are more columns in the table model, a scrollbar will | |
112 | * be used to allow the user to scroll left and right and view all columns. | |
113 | * @param visibleColumns Number of columns to display at once | |
114 | */ | |
115 | public synchronized void setVisibleColumns(int visibleColumns) { | |
116 | this.visibleColumns = visibleColumns; | |
117 | invalidate(); | |
118 | } | |
119 | ||
120 | /** | |
121 | * Returns the number of columns this table will show. If there are more columns in the table model, a scrollbar | |
122 | * will be used to allow the user to scroll left and right and view all columns. | |
123 | * @return Number of visible columns for this table | |
124 | */ | |
125 | public int getVisibleColumns() { | |
126 | return visibleColumns; | |
127 | } | |
128 | ||
129 | /** | |
130 | * Sets the number of rows this table will show. If there are more rows in the table model, a scrollbar will be used | |
131 | * to allow the user to scroll up and down and view all rows. | |
132 | * @param visibleRows Number of rows to display at once | |
133 | */ | |
134 | public synchronized void setVisibleRows(int visibleRows) { | |
135 | this.visibleRows = visibleRows; | |
136 | invalidate(); | |
137 | } | |
138 | ||
139 | /** | |
140 | * Returns the number of rows this table will show. If there are more rows in the table model, a scrollbar will be | |
141 | * used to allow the user to scroll up and down and view all rows. | |
142 | * @return Number of rows to display at once | |
143 | */ | |
144 | public int getVisibleRows() { | |
145 | return visibleRows; | |
146 | } | |
147 | ||
148 | /** | |
149 | * Returns the index of the row that is currently the first row visible. This is always 0 unless scrolling has been | |
150 | * enabled and either the user or the software (through {@code setViewTopRow(..)}) has scrolled down. | |
151 | * @return Index of the row that is currently the first row visible | |
152 | */ | |
153 | public int getViewTopRow() { | |
154 | return viewTopRow; | |
155 | } | |
156 | ||
157 | /** | |
158 | * Sets the view row offset for the first row to display in the table. Calling this with 0 will make the first row | |
159 | * in the model be the first visible row in the table. | |
160 | * | |
161 | * @param viewTopRow Index of the row that is currently the first row visible | |
162 | * @return Itself | |
163 | */ | |
164 | public synchronized Table<V> setViewTopRow(int viewTopRow) { | |
165 | this.viewTopRow = viewTopRow; | |
166 | return this; | |
167 | } | |
168 | ||
169 | /** | |
170 | * Returns the index of the column that is currently the first column visible. This is always 0 unless scrolling has | |
171 | * been enabled and either the user or the software (through {@code setViewLeftColumn(..)}) has scrolled to the | |
172 | * right. | |
173 | * @return Index of the column that is currently the first column visible | |
174 | */ | |
175 | public int getViewLeftColumn() { | |
176 | return viewLeftColumn; | |
177 | } | |
178 | ||
179 | /** | |
180 | * Sets the view column offset for the first column to display in the table. Calling this with 0 will make the first | |
181 | * column in the model be the first visible column in the table. | |
182 | * | |
183 | * @param viewLeftColumn Index of the column that is currently the first column visible | |
184 | * @return Itself | |
185 | */ | |
186 | public synchronized Table<V> setViewLeftColumn(int viewLeftColumn) { | |
187 | this.viewLeftColumn = viewLeftColumn; | |
188 | return this; | |
189 | } | |
190 | ||
191 | /** | |
192 | * Returns the currently selection column index, if in cell-selection mode. Otherwise it returns -1. | |
193 | * @return In cell-selection mode returns the index of the selected column, otherwise -1 | |
194 | */ | |
195 | public int getSelectedColumn() { | |
196 | return selectedColumn; | |
197 | } | |
198 | ||
199 | /** | |
200 | * If in cell selection mode, updates which column is selected and ensures the selected column is visible in the | |
201 | * view. If not in cell selection mode, does nothing. | |
202 | * @param selectedColumn Index of the column that should be selected | |
203 | * @return Itself | |
204 | */ | |
205 | public synchronized Table<V> setSelectedColumn(int selectedColumn) { | |
206 | if(cellSelection) { | |
207 | this.selectedColumn = selectedColumn; | |
208 | ensureSelectedItemIsVisible(); | |
209 | } | |
210 | return this; | |
211 | } | |
212 | ||
213 | /** | |
214 | * Returns the index of the currently selected row | |
215 | * @return Index of the currently selected row | |
216 | */ | |
217 | public int getSelectedRow() { | |
218 | return selectedRow; | |
219 | } | |
220 | ||
221 | /** | |
222 | * Sets the index of the selected row and ensures the selected row is visible in the view | |
223 | * @param selectedRow Index of the row to select | |
224 | * @return Itself | |
225 | */ | |
226 | public synchronized Table<V> setSelectedRow(int selectedRow) { | |
227 | this.selectedRow = selectedRow; | |
228 | ensureSelectedItemIsVisible(); | |
229 | return this; | |
230 | } | |
231 | ||
232 | /** | |
233 | * If {@code true}, the user will be able to select and navigate individual cells, otherwise the user can only | |
234 | * select full rows. | |
235 | * @param cellSelection {@code true} if cell selection should be enabled, {@code false} for row selection | |
236 | * @return Itself | |
237 | */ | |
238 | public synchronized Table<V> setCellSelection(boolean cellSelection) { | |
239 | this.cellSelection = cellSelection; | |
240 | if(cellSelection && selectedColumn == -1) { | |
241 | selectedColumn = 0; | |
242 | } | |
243 | else if(!cellSelection) { | |
244 | selectedColumn = -1; | |
245 | } | |
246 | return this; | |
247 | } | |
248 | ||
249 | /** | |
250 | * Returns {@code true} if this table is in cell-selection mode, otherwise {@code false} | |
251 | * @return {@code true} if this table is in cell-selection mode, otherwise {@code false} | |
252 | */ | |
253 | public boolean isCellSelection() { | |
254 | return cellSelection; | |
255 | } | |
256 | ||
257 | /** | |
258 | * Assigns an action to run whenever the user presses the enter key while focused on the table. If called with | |
259 | * {@code null}, no action will be run. | |
260 | * @param selectAction Action to perform when user presses the enter key | |
261 | * @return Itself | |
262 | */ | |
263 | public synchronized Table<V> setSelectAction(Runnable selectAction) { | |
264 | this.selectAction = selectAction; | |
265 | return this; | |
266 | } | |
267 | ||
268 | /** | |
269 | * Returns {@code true} if this table can be navigated away from when the selected row is at one of the extremes and | |
270 | * the user presses the array key to continue in that direction. With {@code escapeByArrowKey} set to {@code true}, | |
271 | * this will move focus away from the table in the direction the user pressed, if {@code false} then nothing will | |
272 | * happen. | |
273 | * @return {@code true} if user can switch focus away from the table using arrow keys, {@code false} otherwise | |
274 | */ | |
275 | public boolean isEscapeByArrowKey() { | |
276 | return escapeByArrowKey; | |
277 | } | |
278 | ||
279 | /** | |
280 | * Sets the flag for if this table can be navigated away from when the selected row is at one of the extremes and | |
281 | * the user presses the array key to continue in that direction. With {@code escapeByArrowKey} set to {@code true}, | |
282 | * this will move focus away from the table in the direction the user pressed, if {@code false} then nothing will | |
283 | * happen. | |
284 | * @param escapeByArrowKey {@code true} if user can switch focus away from the table using arrow keys, {@code false} otherwise | |
285 | * @return Itself | |
286 | */ | |
287 | public synchronized Table<V> setEscapeByArrowKey(boolean escapeByArrowKey) { | |
288 | this.escapeByArrowKey = escapeByArrowKey; | |
289 | return this; | |
290 | } | |
291 | ||
292 | @Override | |
293 | protected TableRenderer<V> createDefaultRenderer() { | |
294 | return new DefaultTableRenderer<V>(); | |
295 | } | |
296 | ||
297 | @Override | |
298 | public TableRenderer<V> getRenderer() { | |
299 | return (TableRenderer<V>)super.getRenderer(); | |
300 | } | |
301 | ||
302 | @Override | |
303 | public Result handleKeyStroke(KeyStroke keyStroke) { | |
304 | switch(keyStroke.getKeyType()) { | |
305 | case ArrowUp: | |
306 | if(selectedRow > 0) { | |
307 | selectedRow--; | |
308 | } | |
309 | else if(escapeByArrowKey) { | |
310 | return Result.MOVE_FOCUS_UP; | |
311 | } | |
312 | break; | |
313 | case ArrowDown: | |
314 | if(selectedRow < tableModel.getRowCount() - 1) { | |
315 | selectedRow++; | |
316 | } | |
317 | else if(escapeByArrowKey) { | |
318 | return Result.MOVE_FOCUS_DOWN; | |
319 | } | |
320 | break; | |
321 | case ArrowLeft: | |
322 | if(cellSelection && selectedColumn > 0) { | |
323 | selectedColumn--; | |
324 | } | |
325 | else if(escapeByArrowKey) { | |
326 | return Result.MOVE_FOCUS_LEFT; | |
327 | } | |
328 | break; | |
329 | case ArrowRight: | |
330 | if(cellSelection && selectedColumn < tableModel.getColumnCount() - 1) { | |
331 | selectedColumn++; | |
332 | } | |
333 | else if(escapeByArrowKey) { | |
334 | return Result.MOVE_FOCUS_RIGHT; | |
335 | } | |
336 | break; | |
337 | case Enter: | |
338 | Runnable runnable = selectAction; //To avoid synchronizing | |
339 | if(runnable != null) { | |
340 | runnable.run(); | |
341 | } | |
342 | else { | |
343 | return Result.MOVE_FOCUS_NEXT; | |
344 | } | |
345 | break; | |
346 | default: | |
347 | return super.handleKeyStroke(keyStroke); | |
348 | } | |
349 | ensureSelectedItemIsVisible(); | |
350 | invalidate(); | |
351 | return Result.HANDLED; | |
352 | } | |
353 | ||
354 | private void ensureSelectedItemIsVisible() { | |
355 | if(visibleRows > 0 && selectedRow < viewTopRow) { | |
356 | viewTopRow = selectedRow; | |
357 | } | |
358 | else if(visibleRows > 0 && selectedRow >= viewTopRow + visibleRows) { | |
359 | viewTopRow = Math.max(0, selectedRow - visibleRows + 1); | |
360 | } | |
361 | if(selectedColumn != -1) { | |
362 | if(visibleColumns > 0 && selectedColumn < viewLeftColumn) { | |
363 | viewLeftColumn = selectedColumn; | |
364 | } | |
365 | else if(visibleColumns > 0 && selectedColumn >= viewLeftColumn + visibleColumns) { | |
366 | viewLeftColumn = Math.max(0, selectedColumn - visibleColumns + 1); | |
367 | } | |
368 | } | |
369 | } | |
370 | } |