retrofit from binarytelnet
[fanfix.git] / src / jexer / TTableWidget.java
CommitLineData
1dac6b8d
KL
1/*
2 * Jexer - Java Text User Interface
3 *
4 * The MIT License (MIT)
5 *
6 * Copyright (C) 2019 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 */
29package jexer;
30
31import java.util.ArrayList;
32import java.util.List;
33
34import jexer.event.TKeypressEvent;
35import jexer.event.TMenuEvent;
36import jexer.menu.TMenu;
37import static jexer.TKeypress.*;
38
39/**
40 * TTableWidget is used to display and edit regular two-dimensional tables of
41 * cells.
42 *
43 * This class was inspired by a TTable implementation originally developed by
44 * David "Niki" ROULET [niki@nikiroo.be], made available under MIT at
45 * https://github.com/nikiroo/jexer/tree/ttable_pull.
46 */
47public class TTableWidget extends TWidget {
48
49 // ------------------------------------------------------------------------
50 // Constants --------------------------------------------------------------
51 // ------------------------------------------------------------------------
52
53 /**
54 * Available borders for cells.
55 */
56 public enum Border {
57 /**
58 * No border.
59 */
60 NONE,
61
62 /**
63 * Single bar: \u2502 (vertical) and \u2500 (horizontal).
64 */
65 SINGLE,
66
67 /**
68 * Double bar: \u2551 (vertical) and \u2550 (horizontal).
69 */
70 DOUBLE,
71
72 /**
73 * Thick bar: \u258C (vertical, left half block) and \u2580
74 * (horizontal, upper block).
75 */
76 THICK,
77 }
78
79 // ------------------------------------------------------------------------
80 // Variables --------------------------------------------------------------
81 // ------------------------------------------------------------------------
82
83 /**
84 * The underlying data, organized as columns.
85 */
86 private ArrayList<Column> columns = new ArrayList<Column>();
87
88 /**
89 * The underlying data, organized as rows.
90 */
91 private ArrayList<Row> rows = new ArrayList<Row>();
92
93 /**
94 * The row in model corresponding to the top-left visible cell.
95 */
96 private int top = 0;
97
98 /**
99 * The column in model corresponding to the top-left visible cell.
100 */
101 private int left = 0;
102
103 /**
104 * The row in model corresponding to the currently selected cell.
105 */
106 private int selectedRow = 0;
107
108 /**
109 * The column in model corresponding to the currently selected cell.
110 */
111 private int selectedColumn = 0;
112
113 /**
114 * If true, highlight the entire row of the currently-selected cell.
115 */
9c172016 116 private boolean highlightRow = true;
1dac6b8d
KL
117
118 /**
119 * If true, highlight the entire column of the currently-selected cell.
120 */
9c172016
KL
121 private boolean highlightColumn = true;
122
123 /**
124 * If true, show the row labels as the first column.
125 */
126 private boolean showRowLabels = true;
1dac6b8d
KL
127
128 /**
129 * Column represents a column of cells.
130 */
131 public class Column {
132
133 /**
134 * Width of column.
135 */
136 private int width = 8;
137
138 /**
139 * The cells of this column.
140 */
141 private ArrayList<Cell> cells = new ArrayList<Cell>();
142
143 /**
144 * Column label.
145 */
9c172016 146 private String label;
1dac6b8d
KL
147
148 /**
149 * The border for this column.
150 */
151 private Border border = Border.NONE;
152
9c172016
KL
153 /**
154 * Constructor sets label to lettered column.
155 *
156 * @param col column number to use for this column. Column 0 will be
157 * "A", column 1 will be "B", column 26 will be "AA", and so on.
158 */
159 Column(int col) {
160 StringBuilder sb = new StringBuilder();
161 for (;;) {
162 sb.append((char) ('A' + (col % 26)));
163 if (col < 26) {
164 break;
165 }
166 col /= 26;
167 }
168 label = sb.reverse().toString();
169 }
170
1dac6b8d
KL
171 /**
172 * Add an entry to this column.
173 *
174 * @param cell the cell to add
175 */
176 public void add(final Cell cell) {
177 cells.add(cell);
178 }
179
180 /**
181 * Get an entry from this column.
182 *
183 * @param row the entry index to get
184 * @return the cell at row
185 */
186 public Cell get(final int row) {
187 return cells.get(row);
188 }
189 }
190
191 /**
192 * Row represents a row of cells.
193 */
194 public class Row {
195
196 /**
197 * Height of row.
198 */
199 private int height = 1;
200
201 /**
202 * The cells of this row.
203 */
204 private ArrayList<Cell> cells = new ArrayList<Cell>();
205
206 /**
207 * Row label.
208 */
209 private String label = "";
210
211 /**
212 * The border for this row.
213 */
214 private Border border = Border.NONE;
215
9c172016
KL
216 /**
217 * Constructor sets label to numbered row.
218 *
219 * @param row row number to use for this row
220 */
221 Row(final int row) {
222 label = Integer.toString(row);
223 }
224
1dac6b8d
KL
225 /**
226 * Add an entry to this column.
227 *
228 * @param cell the cell to add
229 */
230 public void add(final Cell cell) {
231 cells.add(cell);
232 }
233
234 /**
235 * Get an entry from this row.
236 *
237 * @param column the entry index to get
238 * @return the cell at column
239 */
240 public Cell get(final int column) {
241 return cells.get(column);
242 }
243
244 }
245
246 /**
247 * Cell represents an editable cell in the table. Normally, navigation
248 * to a cell only highlights it; pressing Enter or F2 will switch to
249 * editing mode.
250 */
251 public class Cell extends TWidget {
252
253 // --------------------------------------------------------------------
254 // Variables ----------------------------------------------------------
255 // --------------------------------------------------------------------
256
257 /**
258 * The field containing the cell's data.
259 */
260 private TField field;
261
262 /**
263 * The column of this cell.
264 */
265 private int column;
266
267 /**
268 * The row of this cell.
269 */
270 private int row;
271
272 /**
273 * If true, the cell is being edited.
274 */
275 private boolean isEditing = false;
276
277 /**
278 * Text of field before editing.
279 */
280 private String fieldText;
281
282 // --------------------------------------------------------------------
283 // Constructors -------------------------------------------------------
284 // --------------------------------------------------------------------
285
286 /**
287 * Public constructor.
288 *
289 * @param parent parent widget
290 * @param x column relative to parent
291 * @param y row relative to parent
292 * @param width width of widget
293 * @param height height of widget
294 * @param column column index of this cell
295 * @param row row index of this cell
296 */
297 public Cell(final TTableWidget parent, final int x, final int y,
298 final int width, final int height, final int column,
299 final int row) {
300
301 super(parent, x, y, width, height);
302 this.column = column;
303 this.row = row;
304
305 field = addField(0, 0, width - 1, false);
306 field.setEnabled(false);
1dac6b8d
KL
307 field.setBackgroundChar(' ');
308 }
309
310 // --------------------------------------------------------------------
311 // Event handlers -----------------------------------------------------
312 // --------------------------------------------------------------------
313
314 /**
315 * Handle keystrokes.
316 *
317 * @param keypress keystroke event
318 */
319 @Override
320 public void onKeypress(final TKeypressEvent keypress) {
321 // System.err.println("Cell onKeypress: " + keypress);
322
323 if (isEditing) {
324 if (keypress.equals(kbEsc)) {
325 // ESC cancels the edit.
326 field.setText(fieldText);
327 isEditing = false;
328 field.setEnabled(false);
329 return;
330 }
331 if (keypress.equals(kbEnter)) {
332 // Enter ends editing.
333 fieldText = field.getText();
334 isEditing = false;
335 field.setEnabled(false);
336 return;
337 }
338 // Pass down to field.
339 super.onKeypress(keypress);
340 }
341
342 if (keypress.equals(kbEnter) || keypress.equals(kbF2)) {
343 // Enter or F2 starts editing.
344 fieldText = field.getText();
345 isEditing = true;
346 field.setEnabled(true);
347 activate(field);
348 return;
349 }
350 }
351
352 // --------------------------------------------------------------------
353 // TWidget ------------------------------------------------------------
354 // --------------------------------------------------------------------
355
356 /**
357 * Draw this cell.
358 */
359 @Override
360 public void draw() {
361 TTableWidget table = (TTableWidget) getParent();
362
1dac6b8d 363 if (isAbsoluteActive()) {
9c172016
KL
364 if (isEditing) {
365 field.setActiveColorKey("tfield.active");
366 field.setInactiveColorKey("tfield.inactive");
367 } else {
368 field.setActiveColorKey("ttable.selected");
369 field.setInactiveColorKey("ttable.selected");
1dac6b8d 370 }
9c172016
KL
371 } else if (((table.selectedColumn == column)
372 && ((table.selectedRow == row)
373 || (table.highlightColumn == true)))
374 || ((table.selectedRow == row)
375 && ((table.selectedColumn == column)
376 || (table.highlightRow == true)))
377 ) {
378 field.setActiveColorKey("ttable.active");
379 field.setInactiveColorKey("ttable.active");
380 } else {
381 field.setActiveColorKey("ttable.active");
382 field.setInactiveColorKey("ttable.inactive");
1dac6b8d
KL
383 }
384
385 super.draw();
386 }
387
388 // --------------------------------------------------------------------
389 // TTable.Cell --------------------------------------------------------
390 // --------------------------------------------------------------------
391
392 /**
393 * Get field text.
394 *
395 * @return field text
396 */
397 public final String getText() {
398 return field.getText();
399 }
400
401 /**
402 * Set field text.
403 *
404 * @param text the new field text
405 */
406 public void setText(final String text) {
407 field.setText(text);
408 }
409
410 }
411
412 // ------------------------------------------------------------------------
413 // Constructors -----------------------------------------------------------
414 // ------------------------------------------------------------------------
415
416 /**
417 * Public constructor.
418 *
419 * @param parent parent widget
420 * @param x column relative to parent
421 * @param y row relative to parent
422 * @param width width of widget
423 * @param height height of widget
424 */
425 public TTableWidget(final TWidget parent, final int x, final int y,
426 final int width, final int height) {
427
428 super(parent, x, y, width, height);
429
430 // Initialize the starting row and column.
9c172016
KL
431 rows.add(new Row(0));
432 columns.add(new Column(0));
1dac6b8d
KL
433
434 // Place a grid of cells that fit in this space.
435 int row = 0;
436 for (int i = 0; i < height; i += rows.get(0).height) {
437 int column = 0;
438 for (int j = 0; j < width; j += columns.get(0).width) {
439 Cell cell = new Cell(this, j, i, columns.get(0).width,
440 rows.get(0).height, column, row);
441
442 cell.setText("" + row + " " + column);
443 rows.get(row).add(cell);
444 columns.get(column).add(cell);
445 if ((i == 0) && (j + columns.get(0).width < width)) {
9c172016 446 columns.add(new Column(column + 1));
1dac6b8d
KL
447 }
448 column++;
449 }
450 if (i + rows.get(0).height < height) {
9c172016 451 rows.add(new Row(row + 1));
1dac6b8d
KL
452 }
453 row++;
454 }
455 activate(columns.get(selectedColumn).get(selectedRow));
9c172016
KL
456
457 alignGrid();
1dac6b8d
KL
458 }
459
460 // ------------------------------------------------------------------------
461 // Event handlers ---------------------------------------------------------
462 // ------------------------------------------------------------------------
463
464 /**
465 * Handle keystrokes.
466 *
467 * @param keypress keystroke event
468 */
469 @Override
470 public void onKeypress(final TKeypressEvent keypress) {
471 if (keypress.equals(kbTab)
472 || keypress.equals(kbShiftTab)
473 ) {
474 // Squash tab and back-tab. They don't make sense in the TTable
475 // grid context.
476 return;
477 }
9c172016
KL
478
479 // If editing, pass to that cell and do nothing else.
1dac6b8d
KL
480 if (getSelectedCell().isEditing) {
481 super.onKeypress(keypress);
482 return;
483 }
484
485 if (keypress.equals(kbLeft)) {
486 if (selectedColumn > 0) {
487 selectedColumn--;
488 }
489 activate(columns.get(selectedColumn).get(selectedRow));
490 } else if (keypress.equals(kbRight)) {
491 if (selectedColumn < columns.size() - 1) {
492 selectedColumn++;
493 }
494 activate(columns.get(selectedColumn).get(selectedRow));
495 } else if (keypress.equals(kbUp)) {
496 if (selectedRow > 0) {
497 selectedRow--;
498 }
499 activate(columns.get(selectedColumn).get(selectedRow));
500 } else if (keypress.equals(kbDown)) {
501 if (selectedRow < rows.size() - 1) {
502 selectedRow++;
503 }
504 activate(columns.get(selectedColumn).get(selectedRow));
505 } else if (keypress.equals(kbHome)) {
506 selectedColumn = 0;
507 activate(columns.get(selectedColumn).get(selectedRow));
508 } else if (keypress.equals(kbEnd)) {
509 selectedColumn = columns.size() - 1;
510 activate(columns.get(selectedColumn).get(selectedRow));
511 } else if (keypress.equals(kbPgUp)) {
512 // TODO
513 } else if (keypress.equals(kbPgDn)) {
514 // TODO
515 } else if (keypress.equals(kbCtrlHome)) {
516 // TODO
517 } else if (keypress.equals(kbCtrlEnd)) {
518 // TODO
519 } else {
520 // Pass to the Cell.
521 super.onKeypress(keypress);
522 }
9c172016
KL
523
524 // We may have scrolled off screen. Reset positions as needed to
525 // make the newly selected cell visible.
526 alignGrid();
1dac6b8d
KL
527 }
528
529 /**
530 * Handle posted menu events.
531 *
532 * @param menu menu event
533 */
534 @Override
535 public void onMenu(final TMenuEvent menu) {
536 switch (menu.getId()) {
537 case TMenu.MID_TABLE_BORDER_NONE:
538 case TMenu.MID_TABLE_BORDER_ALL:
539 case TMenu.MID_TABLE_BORDER_RIGHT:
540 case TMenu.MID_TABLE_BORDER_LEFT:
541 case TMenu.MID_TABLE_BORDER_TOP:
542 case TMenu.MID_TABLE_BORDER_BOTTOM:
543 case TMenu.MID_TABLE_BORDER_DOUBLE_BOTTOM:
544 case TMenu.MID_TABLE_BORDER_THICK_BOTTOM:
545 case TMenu.MID_TABLE_DELETE_LEFT:
546 case TMenu.MID_TABLE_DELETE_UP:
547 case TMenu.MID_TABLE_DELETE_ROW:
548 case TMenu.MID_TABLE_DELETE_COLUMN:
549 case TMenu.MID_TABLE_INSERT_LEFT:
550 case TMenu.MID_TABLE_INSERT_RIGHT:
551 case TMenu.MID_TABLE_INSERT_ABOVE:
552 case TMenu.MID_TABLE_INSERT_BELOW:
553 break;
554 case TMenu.MID_TABLE_COLUMN_NARROW:
555 columns.get(selectedColumn).width--;
556 for (Cell cell: getSelectedColumn().cells) {
557 cell.setWidth(columns.get(selectedColumn).width);
558 cell.field.setWidth(columns.get(selectedColumn).width - 1);
559 }
9c172016
KL
560 for (int i = selectedColumn + 1; i < columns.size(); i++) {
561 for (Cell cell: columns.get(i).cells) {
562 cell.setX(cell.getX() - 1);
563 }
564 }
565 alignGrid();
1dac6b8d
KL
566 break;
567 case TMenu.MID_TABLE_COLUMN_WIDEN:
568 columns.get(selectedColumn).width++;
569 for (Cell cell: getSelectedColumn().cells) {
570 cell.setWidth(columns.get(selectedColumn).width);
571 cell.field.setWidth(columns.get(selectedColumn).width - 1);
572 }
9c172016
KL
573 for (int i = selectedColumn + 1; i < columns.size(); i++) {
574 for (Cell cell: columns.get(i).cells) {
575 cell.setX(cell.getX() + 1);
576 }
577 }
578 alignGrid();
1dac6b8d
KL
579 break;
580 case TMenu.MID_TABLE_FILE_SAVE_CSV:
581 case TMenu.MID_TABLE_FILE_SAVE_TEXT:
582 break;
583 default:
584 super.onMenu(menu);
585 }
586 }
587
588 // ------------------------------------------------------------------------
589 // TWidget ----------------------------------------------------------------
590 // ------------------------------------------------------------------------
591
592 // ------------------------------------------------------------------------
593 // TTable -----------------------------------------------------------------
594 // ------------------------------------------------------------------------
595
596 /**
597 * Get the currently-selected cell.
598 *
599 * @return the selected cell
600 */
601 public Cell getSelectedCell() {
602 assert (rows.get(selectedRow) != null);
603 assert (rows.get(selectedRow).get(selectedColumn) != null);
604 assert (columns.get(selectedColumn) != null);
605 assert (columns.get(selectedColumn).get(selectedRow) != null);
606 assert (rows.get(selectedRow).get(selectedColumn) ==
607 columns.get(selectedColumn).get(selectedRow));
608
609 return (columns.get(selectedColumn).get(selectedRow));
610 }
611
612 /**
613 * Get the currently-selected column.
614 *
615 * @return the selected column
616 */
617 public Column getSelectedColumn() {
618 assert (selectedColumn >= 0);
619 assert (columns.size() > selectedColumn);
620 assert (columns.get(selectedColumn) != null);
621 return columns.get(selectedColumn);
622 }
623
624 /**
625 * Get the currently-selected row.
626 *
627 * @return the selected row
628 */
629 public Row getSelectedRow() {
630 assert (selectedRow >= 0);
631 assert (rows.size() > selectedRow);
632 assert (rows.get(selectedRow) != null);
633 return rows.get(selectedRow);
634 }
635
9c172016
KL
636 /**
637 * Get the full horizontal width of this table.
638 *
639 * @return the width required to render the entire table
640 */
641 public int getMaximumWidth() {
642 int totalWidth = 0;
643 if (showRowLabels == true) {
644 // For now, all row labels are 8 cells wide. TODO: make this
645 // adjustable.
646 totalWidth += 8;
647 }
648 for (Cell cell: getSelectedRow().cells) {
649 totalWidth += cell.getWidth() + 1;
650 }
651 return totalWidth;
652 }
653
654 /**
655 * Align the grid so that the selected cell is fully visible.
656 */
657 private void alignGrid() {
658 // Determine if we need to shift left or right.
659 int width = getMaximumWidth();
660 int leftCellX = 0;
661 if (showRowLabels == true) {
662 // For now, all row labels are 8 cells wide. TODO: make this
663 // adjustable.
664 leftCellX += 8;
665 }
666 // TODO
667
668
669 // TODO: determine shift up/down
670
671
672 }
673
1dac6b8d 674}