2 * Jexer - Java Text User Interface
4 * The MIT License (MIT)
6 * Copyright (C) 2019 Kevin Lamonte
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:
15 * The above copyright notice and this permission notice shall be included in
16 * all copies or substantial portions of the Software.
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.
26 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
31 import java
.util
.ArrayList
;
32 import java
.util
.List
;
34 import jexer
.event
.TKeypressEvent
;
35 import jexer
.event
.TMenuEvent
;
36 import jexer
.menu
.TMenu
;
37 import static jexer
.TKeypress
.*;
40 * TTableWidget is used to display and edit regular two-dimensional tables of
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.
47 public class TTableWidget
extends TWidget
{
49 // ------------------------------------------------------------------------
50 // Constants --------------------------------------------------------------
51 // ------------------------------------------------------------------------
54 * Available borders for cells.
63 * Single bar: \u2502 (vertical) and \u2500 (horizontal).
68 * Double bar: \u2551 (vertical) and \u2550 (horizontal).
73 * Thick bar: \u258C (vertical, left half block) and \u2580
74 * (horizontal, upper block).
79 // ------------------------------------------------------------------------
80 // Variables --------------------------------------------------------------
81 // ------------------------------------------------------------------------
84 * The underlying data, organized as columns.
86 private ArrayList
<Column
> columns
= new ArrayList
<Column
>();
89 * The underlying data, organized as rows.
91 private ArrayList
<Row
> rows
= new ArrayList
<Row
>();
94 * The row in model corresponding to the top-left visible cell.
99 * The column in model corresponding to the top-left visible cell.
101 private int left
= 0;
104 * The row in model corresponding to the currently selected cell.
106 private int selectedRow
= 0;
109 * The column in model corresponding to the currently selected cell.
111 private int selectedColumn
= 0;
114 * If true, highlight the entire row of the currently-selected cell.
116 private boolean highlightRow
= true;
119 * If true, highlight the entire column of the currently-selected cell.
121 private boolean highlightColumn
= true;
124 * If true, show the row labels as the first column.
126 private boolean showRowLabels
= true;
129 * Column represents a column of cells.
131 public class Column
{
136 private int width
= 8;
139 * The cells of this column.
141 private ArrayList
<Cell
> cells
= new ArrayList
<Cell
>();
146 private String label
;
149 * The border for this column.
151 private Border border
= Border
.NONE
;
154 * Constructor sets label to lettered column.
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.
160 StringBuilder sb
= new StringBuilder();
162 sb
.append((char) ('A' + (col
% 26)));
168 label
= sb
.reverse().toString();
172 * Add an entry to this column.
174 * @param cell the cell to add
176 public void add(final Cell cell
) {
181 * Get an entry from this column.
183 * @param row the entry index to get
184 * @return the cell at row
186 public Cell
get(final int row
) {
187 return cells
.get(row
);
192 * Row represents a row of cells.
199 private int height
= 1;
202 * The cells of this row.
204 private ArrayList
<Cell
> cells
= new ArrayList
<Cell
>();
209 private String label
= "";
212 * The border for this row.
214 private Border border
= Border
.NONE
;
217 * Constructor sets label to numbered row.
219 * @param row row number to use for this row
222 label
= Integer
.toString(row
);
226 * Add an entry to this column.
228 * @param cell the cell to add
230 public void add(final Cell cell
) {
235 * Get an entry from this row.
237 * @param column the entry index to get
238 * @return the cell at column
240 public Cell
get(final int column
) {
241 return cells
.get(column
);
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
251 public class Cell
extends TWidget
{
253 // --------------------------------------------------------------------
254 // Variables ----------------------------------------------------------
255 // --------------------------------------------------------------------
258 * The field containing the cell's data.
260 private TField field
;
263 * The column of this cell.
268 * The row of this cell.
273 * If true, the cell is being edited.
275 private boolean isEditing
= false;
278 * Text of field before editing.
280 private String fieldText
;
282 // --------------------------------------------------------------------
283 // Constructors -------------------------------------------------------
284 // --------------------------------------------------------------------
287 * Public constructor.
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
297 public Cell(final TTableWidget parent
, final int x
, final int y
,
298 final int width
, final int height
, final int column
,
301 super(parent
, x
, y
, width
, height
);
302 this.column
= column
;
305 field
= addField(0, 0, width
- 1, false);
306 field
.setEnabled(false);
307 field
.setBackgroundChar(' ');
310 // --------------------------------------------------------------------
311 // Event handlers -----------------------------------------------------
312 // --------------------------------------------------------------------
317 * @param keypress keystroke event
320 public void onKeypress(final TKeypressEvent keypress
) {
321 // System.err.println("Cell onKeypress: " + keypress);
324 if (keypress
.equals(kbEsc
)) {
325 // ESC cancels the edit.
326 field
.setText(fieldText
);
328 field
.setEnabled(false);
331 if (keypress
.equals(kbEnter
)) {
332 // Enter ends editing.
333 fieldText
= field
.getText();
335 field
.setEnabled(false);
338 // Pass down to field.
339 super.onKeypress(keypress
);
342 if (keypress
.equals(kbEnter
) || keypress
.equals(kbF2
)) {
343 // Enter or F2 starts editing.
344 fieldText
= field
.getText();
346 field
.setEnabled(true);
352 // --------------------------------------------------------------------
353 // TWidget ------------------------------------------------------------
354 // --------------------------------------------------------------------
361 TTableWidget table
= (TTableWidget
) getParent();
363 if (isAbsoluteActive()) {
365 field
.setActiveColorKey("tfield.active");
366 field
.setInactiveColorKey("tfield.inactive");
368 field
.setActiveColorKey("ttable.selected");
369 field
.setInactiveColorKey("ttable.selected");
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)))
378 field
.setActiveColorKey("ttable.active");
379 field
.setInactiveColorKey("ttable.active");
381 field
.setActiveColorKey("ttable.active");
382 field
.setInactiveColorKey("ttable.inactive");
388 // --------------------------------------------------------------------
389 // TTable.Cell --------------------------------------------------------
390 // --------------------------------------------------------------------
397 public final String
getText() {
398 return field
.getText();
404 * @param text the new field text
406 public void setText(final String text
) {
412 // ------------------------------------------------------------------------
413 // Constructors -----------------------------------------------------------
414 // ------------------------------------------------------------------------
417 * Public constructor.
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
425 public TTableWidget(final TWidget parent
, final int x
, final int y
,
426 final int width
, final int height
) {
428 super(parent
, x
, y
, width
, height
);
430 // Initialize the starting row and column.
431 rows
.add(new Row(0));
432 columns
.add(new Column(0));
434 // Place a grid of cells that fit in this space.
436 for (int i
= 0; i
< height
; i
+= rows
.get(0).height
) {
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
);
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
)) {
446 columns
.add(new Column(column
+ 1));
450 if (i
+ rows
.get(0).height
< height
) {
451 rows
.add(new Row(row
+ 1));
455 activate(columns
.get(selectedColumn
).get(selectedRow
));
460 // ------------------------------------------------------------------------
461 // Event handlers ---------------------------------------------------------
462 // ------------------------------------------------------------------------
467 * @param keypress keystroke event
470 public void onKeypress(final TKeypressEvent keypress
) {
471 if (keypress
.equals(kbTab
)
472 || keypress
.equals(kbShiftTab
)
474 // Squash tab and back-tab. They don't make sense in the TTable
479 // If editing, pass to that cell and do nothing else.
480 if (getSelectedCell().isEditing
) {
481 super.onKeypress(keypress
);
485 if (keypress
.equals(kbLeft
)) {
486 if (selectedColumn
> 0) {
489 activate(columns
.get(selectedColumn
).get(selectedRow
));
490 } else if (keypress
.equals(kbRight
)) {
491 if (selectedColumn
< columns
.size() - 1) {
494 activate(columns
.get(selectedColumn
).get(selectedRow
));
495 } else if (keypress
.equals(kbUp
)) {
496 if (selectedRow
> 0) {
499 activate(columns
.get(selectedColumn
).get(selectedRow
));
500 } else if (keypress
.equals(kbDown
)) {
501 if (selectedRow
< rows
.size() - 1) {
504 activate(columns
.get(selectedColumn
).get(selectedRow
));
505 } else if (keypress
.equals(kbHome
)) {
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
)) {
513 } else if (keypress
.equals(kbPgDn
)) {
515 } else if (keypress
.equals(kbCtrlHome
)) {
517 } else if (keypress
.equals(kbCtrlEnd
)) {
521 super.onKeypress(keypress
);
524 // We may have scrolled off screen. Reset positions as needed to
525 // make the newly selected cell visible.
530 * Handle posted menu events.
532 * @param menu menu event
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
:
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);
560 for (int i
= selectedColumn
+ 1; i
< columns
.size(); i
++) {
561 for (Cell cell
: columns
.get(i
).cells
) {
562 cell
.setX(cell
.getX() - 1);
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);
573 for (int i
= selectedColumn
+ 1; i
< columns
.size(); i
++) {
574 for (Cell cell
: columns
.get(i
).cells
) {
575 cell
.setX(cell
.getX() + 1);
580 case TMenu
.MID_TABLE_FILE_SAVE_CSV
:
581 case TMenu
.MID_TABLE_FILE_SAVE_TEXT
:
588 // ------------------------------------------------------------------------
589 // TWidget ----------------------------------------------------------------
590 // ------------------------------------------------------------------------
592 // ------------------------------------------------------------------------
593 // TTable -----------------------------------------------------------------
594 // ------------------------------------------------------------------------
597 * Get the currently-selected cell.
599 * @return the selected cell
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
));
609 return (columns
.get(selectedColumn
).get(selectedRow
));
613 * Get the currently-selected column.
615 * @return the selected column
617 public Column
getSelectedColumn() {
618 assert (selectedColumn
>= 0);
619 assert (columns
.size() > selectedColumn
);
620 assert (columns
.get(selectedColumn
) != null);
621 return columns
.get(selectedColumn
);
625 * Get the currently-selected row.
627 * @return the selected row
629 public Row
getSelectedRow() {
630 assert (selectedRow
>= 0);
631 assert (rows
.size() > selectedRow
);
632 assert (rows
.get(selectedRow
) != null);
633 return rows
.get(selectedRow
);
637 * Get the full horizontal width of this table.
639 * @return the width required to render the entire table
641 public int getMaximumWidth() {
643 if (showRowLabels
== true) {
644 // For now, all row labels are 8 cells wide. TODO: make this
648 for (Cell cell
: getSelectedRow().cells
) {
649 totalWidth
+= cell
.getWidth() + 1;
655 * Align the grid so that the selected cell is fully visible.
657 private void alignGrid() {
658 // Determine if we need to shift left or right.
659 int width
= getMaximumWidth();
661 if (showRowLabels
== true) {
662 // For now, all row labels are 8 cells wide. TODO: make this
669 // TODO: determine shift up/down