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
.bits
.CellAttributes
;
35 import jexer
.event
.TKeypressEvent
;
36 import jexer
.event
.TMenuEvent
;
37 import jexer
.menu
.TMenu
;
38 import static jexer
.TKeypress
.*;
41 * TTableWidget is used to display and edit regular two-dimensional tables of
44 * This class was inspired by a TTable implementation originally developed by
45 * David "Niki" ROULET [niki@nikiroo.be], made available under MIT at
46 * https://github.com/nikiroo/jexer/tree/ttable_pull.
48 public class TTableWidget
extends TWidget
{
50 // ------------------------------------------------------------------------
51 // Constants --------------------------------------------------------------
52 // ------------------------------------------------------------------------
55 * Available borders for cells.
64 * Single bar: \u2502 (vertical) and \u2500 (horizontal).
69 * Double bar: \u2551 (vertical) and \u2550 (horizontal).
74 * Thick bar: \u258C (vertical, left half block) and \u2580
75 * (horizontal, upper block).
80 // ------------------------------------------------------------------------
81 // Variables --------------------------------------------------------------
82 // ------------------------------------------------------------------------
85 * The underlying data, organized as columns.
87 private ArrayList
<Column
> columns
= new ArrayList
<Column
>();
90 * The underlying data, organized as rows.
92 private ArrayList
<Row
> rows
= new ArrayList
<Row
>();
95 * The row in model corresponding to the top-left visible cell.
100 * The column in model corresponding to the top-left visible cell.
102 private int left
= 0;
105 * The row in model corresponding to the currently selected cell.
107 private int selectedRow
= 0;
110 * The column in model corresponding to the currently selected cell.
112 private int selectedColumn
= 0;
115 * If true, highlight the entire row of the currently-selected cell.
117 private boolean highlightRow
= true;
120 * If true, highlight the entire column of the currently-selected cell.
122 private boolean highlightColumn
= true;
125 * If true, show the row labels as the first column.
127 private boolean showRowLabels
= true;
130 * If true, show the column labels as the first row.
132 private boolean showColumnLabels
= true;
135 * Column represents a column of cells.
137 public class Column
{
142 private int width
= 8;
145 * The cells of this column.
147 private ArrayList
<Cell
> cells
= new ArrayList
<Cell
>();
152 private String label
= "";
155 * The border for this column.
157 private Border border
= Border
.NONE
;
160 * Constructor sets label to lettered column.
162 * @param col column number to use for this column. Column 0 will be
163 * "A", column 1 will be "B", column 26 will be "AA", and so on.
166 StringBuilder sb
= new StringBuilder();
168 sb
.append((char) ('A' + (col
% 26)));
174 label
= sb
.reverse().toString();
178 * Add an entry to this column.
180 * @param cell the cell to add
182 public void add(final Cell cell
) {
187 * Get an entry from this column.
189 * @param row the entry index to get
190 * @return the cell at row
192 public Cell
get(final int row
) {
193 return cells
.get(row
);
198 * Row represents a row of cells.
205 private int height
= 1;
208 * The cells of this row.
210 private ArrayList
<Cell
> cells
= new ArrayList
<Cell
>();
215 private String label
= "";
218 * The border for this row.
220 private Border border
= Border
.NONE
;
223 * Constructor sets label to numbered row.
225 * @param row row number to use for this row
228 label
= Integer
.toString(row
);
232 * Add an entry to this column.
234 * @param cell the cell to add
236 public void add(final Cell cell
) {
241 * Get an entry from this row.
243 * @param column the entry index to get
244 * @return the cell at column
246 public Cell
get(final int column
) {
247 return cells
.get(column
);
253 * Cell represents an editable cell in the table. Normally, navigation
254 * to a cell only highlights it; pressing Enter or F2 will switch to
257 public class Cell
extends TWidget
{
259 // --------------------------------------------------------------------
260 // Variables ----------------------------------------------------------
261 // --------------------------------------------------------------------
264 * The field containing the cell's data.
266 private TField field
;
269 * The column of this cell.
274 * The row of this cell.
279 * If true, the cell is being edited.
281 private boolean isEditing
= false;
284 * Text of field before editing.
286 private String fieldText
;
288 // --------------------------------------------------------------------
289 // Constructors -------------------------------------------------------
290 // --------------------------------------------------------------------
293 * Public constructor.
295 * @param parent parent widget
296 * @param x column relative to parent
297 * @param y row relative to parent
298 * @param width width of widget
299 * @param height height of widget
300 * @param column column index of this cell
301 * @param row row index of this cell
303 public Cell(final TTableWidget parent
, final int x
, final int y
,
304 final int width
, final int height
, final int column
,
307 super(parent
, x
, y
, width
, height
);
308 this.column
= column
;
311 field
= addField(0, 0, width
- 1, false);
312 field
.setEnabled(false);
313 field
.setBackgroundChar(' ');
316 // --------------------------------------------------------------------
317 // Event handlers -----------------------------------------------------
318 // --------------------------------------------------------------------
323 * @param keypress keystroke event
326 public void onKeypress(final TKeypressEvent keypress
) {
327 // System.err.println("Cell onKeypress: " + keypress);
330 if (keypress
.equals(kbEsc
)) {
331 // ESC cancels the edit.
332 field
.setText(fieldText
);
334 field
.setEnabled(false);
337 if (keypress
.equals(kbEnter
)) {
338 // Enter ends editing.
339 fieldText
= field
.getText();
341 field
.setEnabled(false);
344 // Pass down to field.
345 super.onKeypress(keypress
);
348 if (keypress
.equals(kbEnter
) || keypress
.equals(kbF2
)) {
349 // Enter or F2 starts editing.
350 fieldText
= field
.getText();
352 field
.setEnabled(true);
358 // --------------------------------------------------------------------
359 // TWidget ------------------------------------------------------------
360 // --------------------------------------------------------------------
367 TTableWidget table
= (TTableWidget
) getParent();
369 if (isAbsoluteActive()) {
371 field
.setActiveColorKey("tfield.active");
372 field
.setInactiveColorKey("tfield.inactive");
374 field
.setActiveColorKey("ttable.selected");
375 field
.setInactiveColorKey("ttable.selected");
377 } else if (((table
.selectedColumn
== column
)
378 && ((table
.selectedRow
== row
)
379 || (table
.highlightColumn
== true)))
380 || ((table
.selectedRow
== row
)
381 && ((table
.selectedColumn
== column
)
382 || (table
.highlightRow
== true)))
384 field
.setActiveColorKey("ttable.active");
385 field
.setInactiveColorKey("ttable.active");
387 field
.setActiveColorKey("ttable.active");
388 field
.setInactiveColorKey("ttable.inactive");
394 // --------------------------------------------------------------------
395 // TTable.Cell --------------------------------------------------------
396 // --------------------------------------------------------------------
403 public final String
getText() {
404 return field
.getText();
410 * @param text the new field text
412 public void setText(final String text
) {
418 // ------------------------------------------------------------------------
419 // Constructors -----------------------------------------------------------
420 // ------------------------------------------------------------------------
423 * Public constructor.
425 * @param parent parent widget
426 * @param x column relative to parent
427 * @param y row relative to parent
428 * @param width width of widget
429 * @param height height of widget
431 public TTableWidget(final TWidget parent
, final int x
, final int y
,
432 final int width
, final int height
) {
434 super(parent
, x
, y
, width
, height
);
436 // Initialize the starting row and column.
437 rows
.add(new Row(0));
438 columns
.add(new Column(0));
440 // Place a grid of cells that fit in this space.
442 for (int i
= 0; i
< height
; i
+= rows
.get(0).height
) {
444 for (int j
= 0; j
< width
; j
+= columns
.get(0).width
) {
445 Cell cell
= new Cell(this, j
, i
, columns
.get(0).width
,
446 rows
.get(0).height
, column
, row
);
448 cell
.setText("" + row
+ " " + column
);
449 rows
.get(row
).add(cell
);
450 columns
.get(column
).add(cell
);
451 if ((i
== 0) && (j
+ columns
.get(0).width
< width
)) {
452 columns
.add(new Column(column
+ 1));
456 if (i
+ rows
.get(0).height
< height
) {
457 rows
.add(new Row(row
+ 1));
461 activate(columns
.get(selectedColumn
).get(selectedRow
));
466 // ------------------------------------------------------------------------
467 // Event handlers ---------------------------------------------------------
468 // ------------------------------------------------------------------------
473 * @param keypress keystroke event
476 public void onKeypress(final TKeypressEvent keypress
) {
477 if (keypress
.equals(kbTab
)
478 || keypress
.equals(kbShiftTab
)
480 // Squash tab and back-tab. They don't make sense in the TTable
485 // If editing, pass to that cell and do nothing else.
486 if (getSelectedCell().isEditing
) {
487 super.onKeypress(keypress
);
491 if (keypress
.equals(kbLeft
)) {
492 if (selectedColumn
> 0) {
495 activate(columns
.get(selectedColumn
).get(selectedRow
));
496 } else if (keypress
.equals(kbRight
)) {
497 if (selectedColumn
< columns
.size() - 1) {
500 activate(columns
.get(selectedColumn
).get(selectedRow
));
501 } else if (keypress
.equals(kbUp
)) {
502 if (selectedRow
> 0) {
505 activate(columns
.get(selectedColumn
).get(selectedRow
));
506 } else if (keypress
.equals(kbDown
)) {
507 if (selectedRow
< rows
.size() - 1) {
510 activate(columns
.get(selectedColumn
).get(selectedRow
));
511 } else if (keypress
.equals(kbHome
)) {
513 activate(columns
.get(selectedColumn
).get(selectedRow
));
514 } else if (keypress
.equals(kbEnd
)) {
515 selectedColumn
= columns
.size() - 1;
516 activate(columns
.get(selectedColumn
).get(selectedRow
));
517 } else if (keypress
.equals(kbPgUp
)) {
519 } else if (keypress
.equals(kbPgDn
)) {
521 } else if (keypress
.equals(kbCtrlHome
)) {
523 } else if (keypress
.equals(kbCtrlEnd
)) {
527 super.onKeypress(keypress
);
530 // We may have scrolled off screen. Reset positions as needed to
531 // make the newly selected cell visible.
536 * Handle posted menu events.
538 * @param menu menu event
541 public void onMenu(final TMenuEvent menu
) {
542 switch (menu
.getId()) {
543 case TMenu
.MID_TABLE_BORDER_NONE
:
544 case TMenu
.MID_TABLE_BORDER_ALL
:
545 case TMenu
.MID_TABLE_BORDER_RIGHT
:
546 case TMenu
.MID_TABLE_BORDER_LEFT
:
547 case TMenu
.MID_TABLE_BORDER_TOP
:
548 case TMenu
.MID_TABLE_BORDER_BOTTOM
:
549 case TMenu
.MID_TABLE_BORDER_DOUBLE_BOTTOM
:
550 case TMenu
.MID_TABLE_BORDER_THICK_BOTTOM
:
551 case TMenu
.MID_TABLE_DELETE_LEFT
:
552 case TMenu
.MID_TABLE_DELETE_UP
:
553 case TMenu
.MID_TABLE_DELETE_ROW
:
554 case TMenu
.MID_TABLE_DELETE_COLUMN
:
555 case TMenu
.MID_TABLE_INSERT_LEFT
:
556 case TMenu
.MID_TABLE_INSERT_RIGHT
:
557 case TMenu
.MID_TABLE_INSERT_ABOVE
:
558 case TMenu
.MID_TABLE_INSERT_BELOW
:
560 case TMenu
.MID_TABLE_COLUMN_NARROW
:
561 columns
.get(selectedColumn
).width
--;
562 for (Cell cell
: getSelectedColumn().cells
) {
563 cell
.setWidth(columns
.get(selectedColumn
).width
);
564 cell
.field
.setWidth(columns
.get(selectedColumn
).width
- 1);
566 for (int i
= selectedColumn
+ 1; i
< columns
.size(); i
++) {
567 for (Cell cell
: columns
.get(i
).cells
) {
568 cell
.setX(cell
.getX() - 1);
573 case TMenu
.MID_TABLE_COLUMN_WIDEN
:
574 columns
.get(selectedColumn
).width
++;
575 for (Cell cell
: getSelectedColumn().cells
) {
576 cell
.setWidth(columns
.get(selectedColumn
).width
);
577 cell
.field
.setWidth(columns
.get(selectedColumn
).width
- 1);
579 for (int i
= selectedColumn
+ 1; i
< columns
.size(); i
++) {
580 for (Cell cell
: columns
.get(i
).cells
) {
581 cell
.setX(cell
.getX() + 1);
586 case TMenu
.MID_TABLE_FILE_SAVE_CSV
:
587 case TMenu
.MID_TABLE_FILE_SAVE_TEXT
:
594 // ------------------------------------------------------------------------
595 // TWidget ----------------------------------------------------------------
596 // ------------------------------------------------------------------------
599 * Draw the table row/column headings, and borders.
603 CellAttributes headingColor
= getTheme().getColor("ttable.heading");
604 CellAttributes borderColor
= getTheme().getColor("ttable.border");
607 if (showColumnLabels
== true) {
609 if (showRowLabels
== true) {
612 for (int i
= left
; i
< columns
.size(); i
++) {
613 putStringXY(x
+ (i
* 8), 0, String
.format(" %-6s ",
614 columns
.get(i
).label
), headingColor
);
619 if (showRowLabels
== true) {
621 if (showColumnLabels
== true) {
624 for (int i
= top
; i
< rows
.size(); i
++) {
625 putStringXY(0, y
+ i
, String
.format(" %-6s ",
626 rows
.get(i
).label
), headingColor
);
630 // Now draw the window borders.
634 // ------------------------------------------------------------------------
635 // TTable -----------------------------------------------------------------
636 // ------------------------------------------------------------------------
639 * Get the currently-selected cell.
641 * @return the selected cell
643 public Cell
getSelectedCell() {
644 assert (rows
.get(selectedRow
) != null);
645 assert (rows
.get(selectedRow
).get(selectedColumn
) != null);
646 assert (columns
.get(selectedColumn
) != null);
647 assert (columns
.get(selectedColumn
).get(selectedRow
) != null);
648 assert (rows
.get(selectedRow
).get(selectedColumn
) ==
649 columns
.get(selectedColumn
).get(selectedRow
));
651 return (columns
.get(selectedColumn
).get(selectedRow
));
655 * Get the currently-selected column.
657 * @return the selected column
659 public Column
getSelectedColumn() {
660 assert (selectedColumn
>= 0);
661 assert (columns
.size() > selectedColumn
);
662 assert (columns
.get(selectedColumn
) != null);
663 return columns
.get(selectedColumn
);
667 * Get the currently-selected row.
669 * @return the selected row
671 public Row
getSelectedRow() {
672 assert (selectedRow
>= 0);
673 assert (rows
.size() > selectedRow
);
674 assert (rows
.get(selectedRow
) != null);
675 return rows
.get(selectedRow
);
679 * Get the full horizontal width of this table.
681 * @return the width required to render the entire table
683 public int getMaximumWidth() {
685 if (showRowLabels
== true) {
686 // For now, all row labels are 8 cells wide. TODO: make this
690 for (Cell cell
: getSelectedRow().cells
) {
691 totalWidth
+= cell
.getWidth() + 1;
697 * Get the full vertical height of this table.
699 * @return the height required to render the entire table
701 public int getMaximumHeight() {
703 if (showColumnLabels
== true) {
704 // For now, all column labels are 1 cell tall. TODO: make this
708 for (Cell cell
: getSelectedColumn().cells
) {
709 totalHeight
+= cell
.getHeight();
710 // TODO: handle top/bottom borders.
716 * Align the grid so that the selected cell is fully visible.
718 private void alignGrid() {
720 // Adjust X locations to be visible -----------------------------------
722 // Determine if we need to shift left or right.
723 int width
= getMaximumWidth();
725 if (showRowLabels
== true) {
726 // For now, all row labels are 8 cells wide. TODO: make this
730 Row row
= getSelectedRow();
731 Cell selectedColumnCell
= null;
732 for (int i
= 0; i
< row
.cells
.size(); i
++) {
733 if (i
== selectedColumn
) {
734 selectedColumnCell
= row
.cells
.get(i
);
737 leftCellX
+= row
.cells
.get(i
).getWidth() + 1;
739 // There should always be a selected column.
740 assert (selectedColumnCell
!= null);
742 while (leftCellX
+ selectedColumnCell
.getWidth() + 1 > getWidth()) {
743 leftCellX
-= (getWidth() - selectedColumnCell
.getWidth() - 1);
750 * leftCellX now contains the basic left offset necessary to draw the
751 * cells such that the selected cell (column) is fully visible within
752 * this widget's given width. Or, if the widget is too narrow to
753 * display the full cell, leftCellX is 0.
755 * Now reset all of the X positions of the other cells so that the
756 * selected cell X is leftCellX.
758 for (int y
= 0; y
< rows
.size(); y
++) {
759 // All cells to the left of selected cell.
760 int newCellX
= leftCellX
;
761 left
= selectedColumn
;
762 for (int x
= selectedColumn
- 1; x
>= 0; x
--) {
763 newCellX
-= rows
.get(y
).cells
.get(x
).getWidth() - 1;
764 if (newCellX
- rows
.get(y
).cells
.get(x
).getWidth() - 1 > 0) {
765 rows
.get(y
).cells
.get(x
).setVisible(true);
766 rows
.get(y
).cells
.get(x
).setX(newCellX
);
769 // This cell won't be visible.
770 rows
.get(y
).cells
.get(x
).setVisible(false);
775 rows
.get(y
).cells
.get(selectedColumn
).setX(leftCellX
);
777 // All cells to the right of selected cell.
778 newCellX
= leftCellX
+ selectedColumnCell
.getWidth() + 1;
779 for (int x
= selectedColumn
+ 1; x
< columns
.size(); x
++) {
780 if (newCellX
< getWidth()) {
781 rows
.get(y
).cells
.get(x
).setVisible(true);
782 rows
.get(y
).cells
.get(x
).setX(newCellX
);
784 // This cell won't be visible.
785 rows
.get(y
).cells
.get(x
).setVisible(false);
787 newCellX
+= rows
.get(y
).cells
.get(x
).getWidth() + 1;
791 // Adjust Y locations to be visible -----------------------------------
792 // The same logic as above, but applied to the column Y.
794 // Determine if we need to shift up or down.
795 int height
= getMaximumHeight();
797 if (showColumnLabels
== true) {
798 // For now, all column labels are 1 cell high. TODO: make this
802 Column column
= getSelectedColumn();
803 Cell selectedRowCell
= null;
804 for (int i
= 0; i
< column
.cells
.size(); i
++) {
805 if (i
== selectedRow
) {
806 selectedRowCell
= column
.cells
.get(i
);
809 topCellY
+= column
.cells
.get(i
).getHeight();
810 // TODO: if a border is selected, add 1 to topCellY.
812 // There should always be a selected row.
813 assert (selectedRowCell
!= null);
815 while (topCellY
+ selectedRowCell
.getHeight() > getHeight()) {
816 topCellY
-= (getHeight() - selectedRowCell
.getHeight());
823 * topCellY now contains the basic top offset necessary to draw the
824 * cells such that the selected cell (row) is fully visible within
825 * this widget's given height. Or, if the widget is too short to
826 * display the full cell, topCellY is 0.
828 * Now reset all of the Y positions of the other cells so that the
829 * selected cell Y is topCellY.
831 for (int x
= 0; x
< columns
.size(); x
++) {
832 // All cells above the selected cell.
833 int newCellY
= topCellY
;
835 for (int y
= selectedRow
- 1; y
>= 0; y
--) {
836 newCellY
-= rows
.get(y
).cells
.get(x
).getHeight();
837 if (newCellY
- rows
.get(y
).cells
.get(x
).getHeight() > 0) {
838 rows
.get(y
).cells
.get(x
).setVisible(true);
839 rows
.get(y
).cells
.get(x
).setY(newCellY
);
842 // This cell won't be visible.
843 rows
.get(y
).cells
.get(x
).setVisible(false);
848 columns
.get(x
).cells
.get(selectedColumn
).setY(topCellY
);
850 // All cells below of selected cell.
851 newCellY
= topCellY
+ selectedRowCell
.getHeight();
852 for (int y
= selectedRow
+ 1; y
< rows
.size(); y
++) {
853 if (newCellY
< getHeight()) {
854 rows
.get(y
).cells
.get(x
).setVisible(true);
855 rows
.get(y
).cells
.get(x
).setY(newCellY
);
857 // This cell won't be visible.
858 rows
.get(y
).cells
.get(x
).setVisible(false);
860 newCellY
+= rows
.get(y
).cells
.get(x
).getHeight();