box drawing intersections
[fanfix.git] / src / jexer / TTableWidget.java
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 */
29 package jexer;
30
31 import java.io.IOException;
32 import java.util.ArrayList;
33 import java.util.List;
34
35 import jexer.bits.CellAttributes;
36 import jexer.event.TKeypressEvent;
37 import jexer.event.TMenuEvent;
38 import jexer.event.TMouseEvent;
39 import jexer.event.TResizeEvent;
40 import jexer.menu.TMenu;
41 import static jexer.TKeypress.*;
42
43 /**
44 * TTableWidget is used to display and edit regular two-dimensional tables of
45 * cells.
46 *
47 * This class was inspired by a TTable implementation originally developed by
48 * David "Niki" ROULET [niki@nikiroo.be], made available under MIT at
49 * https://github.com/nikiroo/jexer/tree/ttable_pull.
50 */
51 public class TTableWidget extends TWidget {
52
53 // ------------------------------------------------------------------------
54 // Constants --------------------------------------------------------------
55 // ------------------------------------------------------------------------
56
57 /**
58 * Available borders for cells.
59 */
60 public enum Border {
61 /**
62 * No border.
63 */
64 NONE,
65
66 /**
67 * Single bar: \u2502 (vertical) and \u2500 (horizontal).
68 */
69 SINGLE,
70
71 /**
72 * Double bar: \u2551 (vertical) and \u2550 (horizontal).
73 */
74 DOUBLE,
75
76 /**
77 * Thick bar: \u2503 (vertical heavy) and \u2501 (horizontal heavy).
78 */
79 THICK,
80 }
81
82 /**
83 * Row label width.
84 */
85 private static final int ROW_LABEL_WIDTH = 8;
86
87 /**
88 * Column label height.
89 */
90 private static final int COLUMN_LABEL_HEIGHT = 1;
91
92 /**
93 * Column default width.
94 */
95 private static final int COLUMN_DEFAULT_WIDTH = 8;
96
97 /**
98 * Extra rows to add.
99 */
100 private static final int EXTRA_ROWS = 10;
101
102 /**
103 * Extra columns to add.
104 */
105 private static final int EXTRA_COLUMNS = 10 * (8 + 1);
106
107 // ------------------------------------------------------------------------
108 // Variables --------------------------------------------------------------
109 // ------------------------------------------------------------------------
110
111 /**
112 * The underlying data, organized as columns.
113 */
114 private ArrayList<Column> columns = new ArrayList<Column>();
115
116 /**
117 * The underlying data, organized as rows.
118 */
119 private ArrayList<Row> rows = new ArrayList<Row>();
120
121 /**
122 * The row in model corresponding to the top-left visible cell.
123 */
124 private int top = 0;
125
126 /**
127 * The column in model corresponding to the top-left visible cell.
128 */
129 private int left = 0;
130
131 /**
132 * The row in model corresponding to the currently selected cell.
133 */
134 private int selectedRow = 0;
135
136 /**
137 * The column in model corresponding to the currently selected cell.
138 */
139 private int selectedColumn = 0;
140
141 /**
142 * If true, highlight the entire row of the currently-selected cell.
143 */
144 private boolean highlightRow = false;
145
146 /**
147 * If true, highlight the entire column of the currently-selected cell.
148 */
149 private boolean highlightColumn = false;
150
151 /**
152 * If true, show the row labels as the first column.
153 */
154 private boolean showRowLabels = true;
155
156 /**
157 * If true, show the column labels as the first row.
158 */
159 private boolean showColumnLabels = true;
160
161 /**
162 * The top border for the first row.
163 */
164 private Border topBorder = Border.NONE;
165
166 /**
167 * The left border for the first column.
168 */
169 private Border leftBorder = Border.NONE;
170
171 /**
172 * Column represents a column of cells.
173 */
174 public class Column {
175
176 /**
177 * X position of this column.
178 */
179 private int x = 0;
180
181 /**
182 * Width of column.
183 */
184 private int width = COLUMN_DEFAULT_WIDTH;
185
186 /**
187 * The cells of this column.
188 */
189 private ArrayList<Cell> cells = new ArrayList<Cell>();
190
191 /**
192 * Column label.
193 */
194 private String label = "";
195
196 /**
197 * The right border for this column.
198 */
199 private Border rightBorder = Border.NONE;
200
201 /**
202 * Constructor sets label to lettered column.
203 *
204 * @param col column number to use for this column. Column 0 will be
205 * "A", column 1 will be "B", column 26 will be "AA", and so on.
206 */
207 Column(int col) {
208 StringBuilder sb = new StringBuilder();
209 for (;;) {
210 sb.append((char) ('A' + (col % 26)));
211 if (col < 26) {
212 break;
213 }
214 col /= 26;
215 }
216 label = sb.reverse().toString();
217 }
218
219 /**
220 * Add an entry to this column.
221 *
222 * @param cell the cell to add
223 */
224 public void add(final Cell cell) {
225 cells.add(cell);
226 }
227
228 /**
229 * Get an entry from this column.
230 *
231 * @param row the entry index to get
232 * @return the cell at row
233 */
234 public Cell get(final int row) {
235 return cells.get(row);
236 }
237
238 /**
239 * Get the X position of the cells in this column.
240 *
241 * @return the position
242 */
243 public int getX() {
244 return x;
245 }
246
247 /**
248 * Set the X position of the cells in this column.
249 *
250 * @param x the position
251 */
252 public void setX(final int x) {
253 for (Cell cell: cells) {
254 cell.setX(x);
255 }
256 this.x = x;
257 }
258
259 }
260
261 /**
262 * Row represents a row of cells.
263 */
264 public class Row {
265
266 /**
267 * Y position of this row.
268 */
269 private int y = 0;
270
271 /**
272 * Height of row.
273 */
274 private int height = 1;
275
276 /**
277 * The cells of this row.
278 */
279 private ArrayList<Cell> cells = new ArrayList<Cell>();
280
281 /**
282 * Row label.
283 */
284 private String label = "";
285
286 /**
287 * The bottom border for this row.
288 */
289 private Border bottomBorder = Border.NONE;
290
291 /**
292 * Constructor sets label to numbered row.
293 *
294 * @param row row number to use for this row
295 */
296 Row(final int row) {
297 label = Integer.toString(row);
298 }
299
300 /**
301 * Add an entry to this column.
302 *
303 * @param cell the cell to add
304 */
305 public void add(final Cell cell) {
306 cells.add(cell);
307 }
308
309 /**
310 * Get an entry from this row.
311 *
312 * @param column the entry index to get
313 * @return the cell at column
314 */
315 public Cell get(final int column) {
316 return cells.get(column);
317 }
318 /**
319 * Get the Y position of the cells in this column.
320 *
321 * @return the position
322 */
323 public int getY() {
324 return y;
325 }
326
327 /**
328 * Set the Y position of the cells in this column.
329 *
330 * @param y the position
331 */
332 public void setY(final int y) {
333 for (Cell cell: cells) {
334 cell.setY(y);
335 }
336 this.y = y;
337 }
338
339 }
340
341 /**
342 * Cell represents an editable cell in the table. Normally, navigation
343 * to a cell only highlights it; pressing Enter or F2 will switch to
344 * editing mode.
345 */
346 public class Cell extends TWidget {
347
348 // --------------------------------------------------------------------
349 // Variables ----------------------------------------------------------
350 // --------------------------------------------------------------------
351
352 /**
353 * The field containing the cell's data.
354 */
355 private TField field;
356
357 /**
358 * The column of this cell.
359 */
360 private int column;
361
362 /**
363 * The row of this cell.
364 */
365 private int row;
366
367 /**
368 * If true, the cell is being edited.
369 */
370 private boolean isEditing = false;
371
372 /**
373 * Text of field before editing.
374 */
375 private String fieldText;
376
377 // --------------------------------------------------------------------
378 // Constructors -------------------------------------------------------
379 // --------------------------------------------------------------------
380
381 /**
382 * Public constructor.
383 *
384 * @param parent parent widget
385 * @param x column relative to parent
386 * @param y row relative to parent
387 * @param width width of widget
388 * @param height height of widget
389 * @param column column index of this cell
390 * @param row row index of this cell
391 */
392 public Cell(final TTableWidget parent, final int x, final int y,
393 final int width, final int height, final int column,
394 final int row) {
395
396 super(parent, x, y, width, height);
397 this.column = column;
398 this.row = row;
399
400 field = addField(0, 0, width, false);
401 field.setEnabled(false);
402 field.setBackgroundChar(' ');
403 }
404
405 // --------------------------------------------------------------------
406 // Event handlers -----------------------------------------------------
407 // --------------------------------------------------------------------
408
409 /**
410 * Handle mouse double-click events.
411 *
412 * @param mouse mouse double-click event
413 */
414 @Override
415 public void onMouseDoubleClick(final TMouseEvent mouse) {
416 // Use TWidget's code to pass the event to the children.
417 super.onMouseDown(mouse);
418
419 // Double-click means to start editing.
420 fieldText = field.getText();
421 isEditing = true;
422 field.setEnabled(true);
423 activate(field);
424
425 if (isActive()) {
426 // Let the table know that I was activated.
427 ((TTableWidget) getParent()).selectedRow = row;
428 ((TTableWidget) getParent()).selectedColumn = column;
429 ((TTableWidget) getParent()).alignGrid();
430 }
431 }
432
433 /**
434 * Handle mouse press events.
435 *
436 * @param mouse mouse button press event
437 */
438 @Override
439 public void onMouseDown(final TMouseEvent mouse) {
440 // Use TWidget's code to pass the event to the children.
441 super.onMouseDown(mouse);
442
443 if (isActive()) {
444 // Let the table know that I was activated.
445 ((TTableWidget) getParent()).selectedRow = row;
446 ((TTableWidget) getParent()).selectedColumn = column;
447 ((TTableWidget) getParent()).alignGrid();
448 }
449 }
450
451 /**
452 * Handle mouse release events.
453 *
454 * @param mouse mouse button release event
455 */
456 @Override
457 public void onMouseUp(final TMouseEvent mouse) {
458 // Use TWidget's code to pass the event to the children.
459 super.onMouseDown(mouse);
460
461 if (isActive()) {
462 // Let the table know that I was activated.
463 ((TTableWidget) getParent()).selectedRow = row;
464 ((TTableWidget) getParent()).selectedColumn = column;
465 ((TTableWidget) getParent()).alignGrid();
466 }
467 }
468
469 /**
470 * Handle keystrokes.
471 *
472 * @param keypress keystroke event
473 */
474 @Override
475 public void onKeypress(final TKeypressEvent keypress) {
476 // System.err.println("Cell onKeypress: " + keypress);
477
478 if (isEditing) {
479 if (keypress.equals(kbEsc)) {
480 // ESC cancels the edit.
481 field.setText(fieldText);
482 isEditing = false;
483 field.setEnabled(false);
484 return;
485 }
486 if (keypress.equals(kbEnter)) {
487 // Enter ends editing.
488 fieldText = field.getText();
489 isEditing = false;
490 field.setEnabled(false);
491 return;
492 }
493 // Pass down to field.
494 super.onKeypress(keypress);
495 }
496
497 if (keypress.equals(kbEnter) || keypress.equals(kbF2)) {
498 // Enter or F2 starts editing.
499 fieldText = field.getText();
500 isEditing = true;
501 field.setEnabled(true);
502 activate(field);
503 return;
504 }
505 }
506
507 // --------------------------------------------------------------------
508 // TWidget ------------------------------------------------------------
509 // --------------------------------------------------------------------
510
511 /**
512 * Draw this cell.
513 */
514 @Override
515 public void draw() {
516 TTableWidget table = (TTableWidget) getParent();
517
518 if (isAbsoluteActive()) {
519 if (isEditing) {
520 field.setActiveColorKey("tfield.active");
521 field.setInactiveColorKey("tfield.inactive");
522 } else {
523 field.setActiveColorKey("ttable.selected");
524 field.setInactiveColorKey("ttable.selected");
525 }
526 } else if (((table.selectedColumn == column)
527 && ((table.selectedRow == row)
528 || (table.highlightColumn == true)))
529 || ((table.selectedRow == row)
530 && ((table.selectedColumn == column)
531 || (table.highlightRow == true)))
532 ) {
533 field.setActiveColorKey("ttable.active");
534 field.setInactiveColorKey("ttable.active");
535 } else {
536 field.setActiveColorKey("ttable.active");
537 field.setInactiveColorKey("ttable.inactive");
538 }
539
540 assert (isVisible() == true);
541
542 super.draw();
543 }
544
545 // --------------------------------------------------------------------
546 // TTable.Cell --------------------------------------------------------
547 // --------------------------------------------------------------------
548
549 /**
550 * Get field text.
551 *
552 * @return field text
553 */
554 public final String getText() {
555 return field.getText();
556 }
557
558 /**
559 * Set field text.
560 *
561 * @param text the new field text
562 */
563 public void setText(final String text) {
564 field.setText(text);
565 }
566
567 }
568
569 // ------------------------------------------------------------------------
570 // Constructors -----------------------------------------------------------
571 // ------------------------------------------------------------------------
572
573 /**
574 * Public constructor.
575 *
576 * @param parent parent widget
577 * @param x column relative to parent
578 * @param y row relative to parent
579 * @param width width of widget
580 * @param height height of widget
581 */
582 public TTableWidget(final TWidget parent, final int x, final int y,
583 final int width, final int height) {
584
585 super(parent, x, y, width, height);
586
587 // Initialize the starting row and column.
588 rows.add(new Row(0));
589 columns.add(new Column(0));
590
591 // Place a grid of cells that fit in this space.
592 int row = 0;
593 for (int i = 0; i < height + EXTRA_ROWS; i += rows.get(0).height) {
594 int column = 0;
595 for (int j = 0; j < width + EXTRA_COLUMNS;
596 j += columns.get(0).width) {
597
598 Cell cell = new Cell(this, 0, 0, /* j, i, */ columns.get(0).width,
599 rows.get(0).height, column, row);
600
601 // DEBUG: set a grid of cell index labels
602 // TODO: remove this
603 cell.setText("" + row + " " + column);
604 rows.get(row).add(cell);
605 columns.get(column).add(cell);
606 if ((i == 0) &&
607 (j + columns.get(0).width < width + EXTRA_COLUMNS)
608 ) {
609 columns.add(new Column(column + 1));
610 }
611 column++;
612 }
613 if (i + rows.get(0).height < height + EXTRA_ROWS) {
614 rows.add(new Row(row + 1));
615 }
616 row++;
617 }
618 for (int i = 0; i < rows.size(); i++) {
619 rows.get(i).setY(i + (showColumnLabels ? COLUMN_LABEL_HEIGHT : 0));
620 }
621 for (int j = 0; j < columns.size(); j++) {
622 columns.get(j).setX((j * COLUMN_DEFAULT_WIDTH) +
623 (showRowLabels ? ROW_LABEL_WIDTH : 0));
624 }
625 activate(columns.get(selectedColumn).get(selectedRow));
626
627 alignGrid();
628
629 // Set the menu to match the flags.
630 getApplication().getMenuItem(TMenu.MID_TABLE_VIEW_ROW_LABELS).
631 setChecked(showRowLabels);
632 getApplication().getMenuItem(TMenu.MID_TABLE_VIEW_COLUMN_LABELS).
633 setChecked(showColumnLabels);
634 getApplication().getMenuItem(TMenu.MID_TABLE_VIEW_HIGHLIGHT_ROW).
635 setChecked(highlightRow);
636 getApplication().getMenuItem(TMenu.MID_TABLE_VIEW_HIGHLIGHT_COLUMN).
637 setChecked(highlightColumn);
638
639
640 }
641
642 // ------------------------------------------------------------------------
643 // Event handlers ---------------------------------------------------------
644 // ------------------------------------------------------------------------
645
646 /**
647 * Handle mouse press events.
648 *
649 * @param mouse mouse button press event
650 */
651 @Override
652 public void onMouseDown(final TMouseEvent mouse) {
653 if (mouse.isMouseWheelUp() || mouse.isMouseWheelDown()) {
654 // Treat wheel up/down as 3 up/down
655 TKeypressEvent keyEvent;
656 if (mouse.isMouseWheelUp()) {
657 keyEvent = new TKeypressEvent(kbUp);
658 } else {
659 keyEvent = new TKeypressEvent(kbDown);
660 }
661 for (int i = 0; i < 3; i++) {
662 onKeypress(keyEvent);
663 }
664 return;
665 }
666
667 // Use TWidget's code to pass the event to the children.
668 super.onMouseDown(mouse);
669 }
670
671 /**
672 * Handle keystrokes.
673 *
674 * @param keypress keystroke event
675 */
676 @Override
677 public void onKeypress(final TKeypressEvent keypress) {
678 if (keypress.equals(kbTab)
679 || keypress.equals(kbShiftTab)
680 ) {
681 // Squash tab and back-tab. They don't make sense in the TTable
682 // grid context.
683 return;
684 }
685
686 // If editing, pass to that cell and do nothing else.
687 if (getSelectedCell().isEditing) {
688 super.onKeypress(keypress);
689 return;
690 }
691
692 if (keypress.equals(kbLeft)) {
693 // Left
694 if (selectedColumn > 0) {
695 selectedColumn--;
696 }
697 activate(columns.get(selectedColumn).get(selectedRow));
698 } else if (keypress.equals(kbRight)) {
699 // Right
700 if (selectedColumn < columns.size() - 1) {
701 selectedColumn++;
702 }
703 activate(columns.get(selectedColumn).get(selectedRow));
704 } else if (keypress.equals(kbUp)) {
705 // Up
706 if (selectedRow > 0) {
707 selectedRow--;
708 }
709 activate(columns.get(selectedColumn).get(selectedRow));
710 } else if (keypress.equals(kbDown)) {
711 // Down
712 if (selectedRow < rows.size() - 1) {
713 selectedRow++;
714 }
715 activate(columns.get(selectedColumn).get(selectedRow));
716 } else if (keypress.equals(kbHome)) {
717 // Home - leftmost column
718 selectedColumn = 0;
719 activate(columns.get(selectedColumn).get(selectedRow));
720 } else if (keypress.equals(kbEnd)) {
721 // End - rightmost column
722 selectedColumn = columns.size() - 1;
723 activate(columns.get(selectedColumn).get(selectedRow));
724 } else if (keypress.equals(kbPgUp)) {
725 // PgUp - Treat like multiple up
726 for (int i = 0; i < getHeight() - 2; i++) {
727 if (selectedRow > 0) {
728 selectedRow--;
729 }
730 }
731 activate(columns.get(selectedColumn).get(selectedRow));
732 } else if (keypress.equals(kbPgDn)) {
733 // PgDn - Treat like multiple up
734 for (int i = 0; i < getHeight() - 2; i++) {
735 if (selectedRow < rows.size() - 1) {
736 selectedRow++;
737 }
738 }
739 activate(columns.get(selectedColumn).get(selectedRow));
740 } else if (keypress.equals(kbCtrlHome)) {
741 // Ctrl-Home - go to top-left
742 selectedRow = 0;
743 selectedColumn = 0;
744 activate(columns.get(selectedColumn).get(selectedRow));
745 activate(columns.get(selectedColumn).get(selectedRow));
746 } else if (keypress.equals(kbCtrlEnd)) {
747 // Ctrl-End - go to bottom-right
748 selectedRow = rows.size() - 1;
749 selectedColumn = columns.size() - 1;
750 activate(columns.get(selectedColumn).get(selectedRow));
751 activate(columns.get(selectedColumn).get(selectedRow));
752 } else {
753 // Pass to the Cell.
754 super.onKeypress(keypress);
755 }
756
757 // We may have scrolled off screen. Reset positions as needed to
758 // make the newly selected cell visible.
759 alignGrid();
760 }
761
762 /**
763 * Handle widget resize events.
764 *
765 * @param event resize event
766 */
767 @Override
768 public void onResize(final TResizeEvent event) {
769 super.onResize(event);
770
771 alignGrid();
772 }
773
774 /**
775 * Handle posted menu events.
776 *
777 * @param menu menu event
778 */
779 @Override
780 public void onMenu(final TMenuEvent menu) {
781 switch (menu.getId()) {
782 case TMenu.MID_TABLE_VIEW_ROW_LABELS:
783 showRowLabels = getApplication().getMenuItem(menu.getId()).getChecked();
784 break;
785 case TMenu.MID_TABLE_VIEW_COLUMN_LABELS:
786 showColumnLabels = getApplication().getMenuItem(menu.getId()).getChecked();
787 break;
788 case TMenu.MID_TABLE_VIEW_HIGHLIGHT_ROW:
789 highlightRow = getApplication().getMenuItem(menu.getId()).getChecked();
790 break;
791 case TMenu.MID_TABLE_VIEW_HIGHLIGHT_COLUMN:
792 highlightColumn = getApplication().getMenuItem(menu.getId()).getChecked();
793 break;
794 case TMenu.MID_TABLE_BORDER_NONE:
795 topBorder = Border.NONE;
796 leftBorder = Border.NONE;
797 for (int i = 0; i < columns.size(); i++) {
798 columns.get(i).rightBorder = Border.NONE;
799 }
800 for (int i = 0; i < rows.size(); i++) {
801 rows.get(i).bottomBorder = Border.NONE;
802 rows.get(i).height = 1;
803 }
804 break;
805 case TMenu.MID_TABLE_BORDER_ALL:
806 topBorder = Border.SINGLE;
807 leftBorder = Border.SINGLE;
808 for (int i = 0; i < columns.size(); i++) {
809 columns.get(i).rightBorder = Border.SINGLE;
810 }
811 for (int i = 0; i < rows.size(); i++) {
812 rows.get(i).bottomBorder = Border.SINGLE;
813 rows.get(i).height = 2;
814 }
815 break;
816 case TMenu.MID_TABLE_BORDER_CELL_NONE:
817 if (selectedRow == 0) {
818 topBorder = Border.NONE;
819 }
820 if (selectedColumn == 0) {
821 leftBorder = Border.NONE;
822 }
823 columns.get(selectedColumn).rightBorder = Border.NONE;
824 rows.get(selectedRow).bottomBorder = Border.NONE;
825 rows.get(selectedRow).height = 1;
826 break;
827 case TMenu.MID_TABLE_BORDER_CELL_ALL:
828 if (selectedRow == 0) {
829 topBorder = Border.SINGLE;
830 }
831 if (selectedColumn == 0) {
832 leftBorder = Border.SINGLE;
833 }
834 columns.get(selectedColumn).rightBorder = Border.SINGLE;
835 rows.get(selectedRow).bottomBorder = Border.SINGLE;
836 rows.get(selectedRow).height = 2;
837 break;
838 case TMenu.MID_TABLE_BORDER_RIGHT:
839 columns.get(selectedColumn).rightBorder = Border.SINGLE;
840 break;
841 case TMenu.MID_TABLE_BORDER_LEFT:
842 if (selectedColumn == 0) {
843 leftBorder = Border.SINGLE;
844 } else {
845 columns.get(selectedColumn - 1).rightBorder = Border.SINGLE;
846 }
847 break;
848 case TMenu.MID_TABLE_BORDER_TOP:
849 if (selectedRow == 0) {
850 topBorder = Border.SINGLE;
851 } else {
852 rows.get(selectedRow - 1).bottomBorder = Border.SINGLE;
853 rows.get(selectedRow - 1).height = 2;
854 }
855 break;
856 case TMenu.MID_TABLE_BORDER_BOTTOM:
857 rows.get(selectedRow).bottomBorder = Border.SINGLE;
858 rows.get(selectedRow).height = 2;
859 break;
860 case TMenu.MID_TABLE_BORDER_DOUBLE_BOTTOM:
861 rows.get(selectedRow).bottomBorder = Border.DOUBLE;
862 rows.get(selectedRow).height = 2;
863 break;
864 case TMenu.MID_TABLE_BORDER_THICK_BOTTOM:
865 rows.get(selectedRow).bottomBorder = Border.THICK;
866 rows.get(selectedRow).height = 2;
867 break;
868 case TMenu.MID_TABLE_DELETE_LEFT:
869 case TMenu.MID_TABLE_DELETE_UP:
870 case TMenu.MID_TABLE_DELETE_ROW:
871 case TMenu.MID_TABLE_DELETE_COLUMN:
872 case TMenu.MID_TABLE_INSERT_LEFT:
873 case TMenu.MID_TABLE_INSERT_RIGHT:
874 case TMenu.MID_TABLE_INSERT_ABOVE:
875 case TMenu.MID_TABLE_INSERT_BELOW:
876 break;
877 case TMenu.MID_TABLE_COLUMN_NARROW:
878 columns.get(selectedColumn).width--;
879 for (Cell cell: getSelectedColumn().cells) {
880 cell.setWidth(columns.get(selectedColumn).width);
881 cell.field.setWidth(columns.get(selectedColumn).width);
882 }
883 for (int i = selectedColumn + 1; i < columns.size(); i++) {
884 columns.get(i).setX(columns.get(i).getX() - 1);
885 }
886 break;
887 case TMenu.MID_TABLE_COLUMN_WIDEN:
888 columns.get(selectedColumn).width++;
889 for (Cell cell: getSelectedColumn().cells) {
890 cell.setWidth(columns.get(selectedColumn).width);
891 cell.field.setWidth(columns.get(selectedColumn).width);
892 }
893 for (int i = selectedColumn + 1; i < columns.size(); i++) {
894 columns.get(i).setX(columns.get(i).getX() + 1);
895 }
896 break;
897 case TMenu.MID_TABLE_FILE_SAVE_CSV:
898 // TODO
899 break;
900 case TMenu.MID_TABLE_FILE_SAVE_TEXT:
901 // TODO
902 break;
903 default:
904 super.onMenu(menu);
905 }
906
907 // Fix/redraw the display.
908 alignGrid();
909 }
910
911 // ------------------------------------------------------------------------
912 // TWidget ----------------------------------------------------------------
913 // ------------------------------------------------------------------------
914
915 /**
916 * Draw the table row/column labels, and borders.
917 */
918 @Override
919 public void draw() {
920 CellAttributes labelColor = getTheme().getColor("ttable.label");
921 CellAttributes labelColorSelected = getTheme().getColor("ttable.label.selected");
922 CellAttributes borderColor = getTheme().getColor("ttable.border");
923
924 // Column labels.
925 if (showColumnLabels == true) {
926 for (int i = left; i < columns.size(); i++) {
927 if (columns.get(i).get(top).isVisible() == false) {
928 break;
929 }
930 putStringXY(columns.get(i).get(top).getX(), 0,
931 String.format(" %-" +
932 (columns.get(i).width - 2)
933 + "s ", columns.get(i).label),
934 (i == selectedColumn ? labelColorSelected : labelColor));
935 }
936 }
937
938 // Row labels.
939 if (showRowLabels == true) {
940 for (int i = top; i < rows.size(); i++) {
941 if (rows.get(i).get(left).isVisible() == false) {
942 break;
943 }
944 putStringXY(0, rows.get(i).get(left).getY(),
945 String.format(" %-6s ", rows.get(i).label),
946 (i == selectedRow ? labelColorSelected : labelColor));
947 }
948 }
949
950 // Draw vertical borders.
951 if (leftBorder == Border.SINGLE) {
952 vLineXY((showRowLabels ? ROW_LABEL_WIDTH : 0),
953 (topBorder == Border.NONE ? 0 : 1) +
954 (showColumnLabels ? COLUMN_LABEL_HEIGHT : 0),
955 getHeight(), '\u2502', borderColor);
956 }
957 for (int i = left; i < columns.size(); i++) {
958 if (columns.get(i).get(top).isVisible() == false) {
959 break;
960 }
961 if (columns.get(i).rightBorder == Border.SINGLE) {
962 vLineXY(columns.get(i).getX() + columns.get(i).width,
963 (topBorder == Border.NONE ? 0 : 1) +
964 (showColumnLabels ? COLUMN_LABEL_HEIGHT : 0),
965 getHeight(), '\u2502', borderColor);
966 }
967 }
968
969 // Draw horizontal borders.
970 if (topBorder == Border.SINGLE) {
971 hLineXY((showRowLabels ? ROW_LABEL_WIDTH : 0),
972 (showColumnLabels ? COLUMN_LABEL_HEIGHT : 0),
973 getWidth(), '\u2500', borderColor);
974 }
975 for (int i = top; i < rows.size(); i++) {
976 if (rows.get(i).get(left).isVisible() == false) {
977 break;
978 }
979 if (rows.get(i).bottomBorder == Border.SINGLE) {
980 hLineXY((leftBorder == Border.NONE ? 0 : 1) +
981 (showRowLabels ? ROW_LABEL_WIDTH : 0),
982 rows.get(i).getY() + rows.get(i).height - 1,
983 getWidth(), '\u2500', borderColor);
984 } else if (rows.get(i).bottomBorder == Border.DOUBLE) {
985 hLineXY((leftBorder == Border.NONE ? 0 : 1) +
986 (showRowLabels ? ROW_LABEL_WIDTH : 0),
987 rows.get(i).getY() + rows.get(i).height - 1,
988 getWidth(), '\u2550', borderColor);
989 } else if (rows.get(i).bottomBorder == Border.THICK) {
990 hLineXY((leftBorder == Border.NONE ? 0 : 1) +
991 (showRowLabels ? ROW_LABEL_WIDTH : 0),
992 rows.get(i).getY() + rows.get(i).height - 1,
993 getWidth(), '\u2501', borderColor);
994 }
995 }
996 // Top-left corner if needed
997 if ((topBorder == Border.SINGLE) && (leftBorder == Border.SINGLE)) {
998 putCharXY((showRowLabels ? ROW_LABEL_WIDTH : 0),
999 (showColumnLabels ? COLUMN_LABEL_HEIGHT : 0),
1000 '\u250c', borderColor);
1001 }
1002
1003 // Now draw the correct corners
1004 for (int i = top; i < rows.size(); i++) {
1005 if (rows.get(i).get(left).isVisible() == false) {
1006 break;
1007 }
1008 for (int j = left; j < columns.size(); j++) {
1009 if (columns.get(j).get(i).isVisible() == false) {
1010 break;
1011 }
1012 if ((i == top) && (topBorder == Border.SINGLE)
1013 && (columns.get(j).rightBorder == Border.SINGLE)
1014 ) {
1015 // Top tee
1016 putCharXY(columns.get(j).getX() + columns.get(j).width,
1017 (showColumnLabels ? COLUMN_LABEL_HEIGHT : 0),
1018 '\u252c', borderColor);
1019 }
1020 if ((j == left) && (leftBorder == Border.SINGLE)
1021 && (rows.get(i).bottomBorder == Border.SINGLE)
1022 ) {
1023 // Left tee
1024 putCharXY((showRowLabels ? ROW_LABEL_WIDTH : 0),
1025 rows.get(i).getY() + rows.get(i).height - 1,
1026 '\u251c', borderColor);
1027 }
1028 if ((columns.get(j).rightBorder == Border.SINGLE)
1029 && (rows.get(i).bottomBorder == Border.SINGLE)
1030 ) {
1031 // Intersection of single bars
1032 putCharXY(columns.get(j).getX() + columns.get(j).width,
1033 rows.get(i).getY() + rows.get(i).height - 1,
1034 '\u253c', borderColor);
1035 }
1036 if ((j == left) && (leftBorder == Border.SINGLE)
1037 && (rows.get(i).bottomBorder == Border.DOUBLE)
1038 ) {
1039 // Left tee: single bar vertical, double bar horizontal
1040 putCharXY((showRowLabels ? ROW_LABEL_WIDTH : 0),
1041 rows.get(i).getY() + rows.get(i).height - 1,
1042 '\u255e', borderColor);
1043 }
1044 if ((j == left) && (leftBorder == Border.SINGLE)
1045 && (rows.get(i).bottomBorder == Border.THICK)
1046 ) {
1047 // Left tee: single bar vertical, thick bar horizontal
1048 putCharXY((showRowLabels ? ROW_LABEL_WIDTH : 0),
1049 rows.get(i).getY() + rows.get(i).height - 1,
1050 '\u251d', borderColor);
1051 }
1052 if ((columns.get(j).rightBorder == Border.SINGLE)
1053 && (rows.get(i).bottomBorder == Border.DOUBLE)
1054 ) {
1055 // Intersection: single bar vertical, double bar
1056 // horizontal
1057 putCharXY(columns.get(j).getX() + columns.get(j).width,
1058 rows.get(i).getY() + rows.get(i).height - 1,
1059 '\u256a', borderColor);
1060 }
1061 if ((columns.get(j).rightBorder == Border.SINGLE)
1062 && (rows.get(i).bottomBorder == Border.THICK)
1063 ) {
1064 // Intersection: single bar vertical, thick bar
1065 // horizontal
1066 putCharXY(columns.get(j).getX() + columns.get(j).width,
1067 rows.get(i).getY() + rows.get(i).height - 1,
1068 '\u253f', borderColor);
1069 }
1070 }
1071 }
1072
1073 // Now draw the window borders.
1074 super.draw();
1075 }
1076
1077 // ------------------------------------------------------------------------
1078 // TTable -----------------------------------------------------------------
1079 // ------------------------------------------------------------------------
1080
1081 /**
1082 * Get the currently-selected cell.
1083 *
1084 * @return the selected cell
1085 */
1086 public Cell getSelectedCell() {
1087 assert (rows.get(selectedRow) != null);
1088 assert (rows.get(selectedRow).get(selectedColumn) != null);
1089 assert (columns.get(selectedColumn) != null);
1090 assert (columns.get(selectedColumn).get(selectedRow) != null);
1091 assert (rows.get(selectedRow).get(selectedColumn) ==
1092 columns.get(selectedColumn).get(selectedRow));
1093
1094 return (columns.get(selectedColumn).get(selectedRow));
1095 }
1096
1097 /**
1098 * Get the currently-selected column.
1099 *
1100 * @return the selected column
1101 */
1102 public Column getSelectedColumn() {
1103 assert (selectedColumn >= 0);
1104 assert (columns.size() > selectedColumn);
1105 assert (columns.get(selectedColumn) != null);
1106 return columns.get(selectedColumn);
1107 }
1108
1109 /**
1110 * Get the currently-selected row.
1111 *
1112 * @return the selected row
1113 */
1114 public Row getSelectedRow() {
1115 assert (selectedRow >= 0);
1116 assert (rows.size() > selectedRow);
1117 assert (rows.get(selectedRow) != null);
1118 return rows.get(selectedRow);
1119 }
1120
1121 /**
1122 * Get the currently-selected column number. 0 is the left-most column.
1123 *
1124 * @return the selected column number
1125 */
1126 public int getSelectedColumnNumber() {
1127 return selectedColumn;
1128 }
1129
1130 /**
1131 * Set the currently-selected column number. 0 is the left-most column.
1132 *
1133 * @param column the column number to select
1134 */
1135 public void setSelectedColumnNumber(final int column) {
1136 if ((column < 0) || (column > columns.size() - 1)) {
1137 throw new IndexOutOfBoundsException("Column count is " +
1138 columns.size() + ", requested index " + column);
1139 }
1140 selectedColumn = column;
1141 activate(columns.get(selectedColumn).get(selectedRow));
1142 alignGrid();
1143 }
1144
1145 /**
1146 * Get the currently-selected row number. 0 is the top-most row.
1147 *
1148 * @return the selected row number
1149 */
1150 public int getSelectedRowNumber() {
1151 return selectedRow;
1152 }
1153
1154 /**
1155 * Set the currently-selected row number. 0 is the left-most column.
1156 *
1157 * @param row the row number to select
1158 */
1159 public void setSelectedRowNumber(final int row) {
1160 if ((row < 0) || (row > rows.size() - 1)) {
1161 throw new IndexOutOfBoundsException("Row count is " +
1162 rows.size() + ", requested index " + row);
1163 }
1164 selectedRow = row;
1165 activate(columns.get(selectedColumn).get(selectedRow));
1166 alignGrid();
1167 }
1168
1169 /**
1170 * Get the number of columns.
1171 *
1172 * @return the number of columns
1173 */
1174 public int getColumnCount() {
1175 return columns.size();
1176 }
1177
1178 /**
1179 * Get the number of rows.
1180 *
1181 * @return the number of rows
1182 */
1183 public int getRowCount() {
1184 return rows.size();
1185 }
1186
1187 /**
1188 * Align the grid so that the selected cell is fully visible.
1189 */
1190 private void alignGrid() {
1191 int viewColumns = getWidth();
1192 if (showRowLabels == true) {
1193 viewColumns -= ROW_LABEL_WIDTH;
1194 }
1195 if (leftBorder != Border.NONE) {
1196 viewColumns--;
1197 }
1198 int viewRows = getHeight();
1199 if (showColumnLabels == true) {
1200 viewRows -= COLUMN_LABEL_HEIGHT;
1201 }
1202 if (topBorder != Border.NONE) {
1203 viewRows--;
1204 }
1205
1206 // If we pushed left or right, adjust the box to include the new
1207 // selected cell.
1208 if (selectedColumn < left) {
1209 left = selectedColumn - 1;
1210 }
1211 if (left < 0) {
1212 left = 0;
1213 }
1214 if (selectedRow < top) {
1215 top = selectedRow - 1;
1216 }
1217 if (top < 0) {
1218 top = 0;
1219 }
1220
1221 /*
1222 * viewColumns and viewRows now contain the available columns and
1223 * rows available to view the selected cell. We adjust left and top
1224 * to ensure the selected cell is within view, and then make all
1225 * cells outside the box between (left, top) and (right, bottom)
1226 * invisible.
1227 *
1228 * We need to calculate right and bottom now.
1229 */
1230 int right = left;
1231
1232 boolean done = false;
1233 while (!done) {
1234 int rightCellX = (showRowLabels ? ROW_LABEL_WIDTH : 0);
1235 if (leftBorder != Border.NONE) {
1236 rightCellX++;
1237 }
1238 int maxCellX = rightCellX + viewColumns;
1239 right = left;
1240 boolean selectedIsVisible = false;
1241 int selectedX = 0;
1242 for (int x = left; x < columns.size(); x++) {
1243 if (x == selectedColumn) {
1244 selectedX = rightCellX;
1245 if (selectedX + columns.get(x).width + 1 <= maxCellX) {
1246 selectedIsVisible = true;
1247 }
1248 }
1249 rightCellX += columns.get(x).width + 1;
1250 if (rightCellX >= maxCellX) {
1251 break;
1252 }
1253 right++;
1254 }
1255 if (right < selectedColumn) {
1256 // selectedColumn is outside the view range. Push left over,
1257 // and calculate again.
1258 left++;
1259 } else if (left == selectedColumn) {
1260 // selectedColumn doesn't fit inside the view range, but we
1261 // can't go over any further either. Bail out.
1262 done = true;
1263 } else if (selectedIsVisible == false) {
1264 // selectedColumn doesn't fit inside the view range, continue
1265 // on.
1266 left++;
1267 } else {
1268 // selectedColumn is fully visible, all done.
1269 assert (selectedIsVisible == true);
1270 done = true;
1271 }
1272
1273 } // while (!done)
1274
1275 // We have the left/right range correct, set cell visibility and
1276 // column X positions.
1277 int leftCellX = showRowLabels ? ROW_LABEL_WIDTH : 0;
1278 if (leftBorder != Border.NONE) {
1279 leftCellX++;
1280 }
1281 for (int x = 0; x < columns.size(); x++) {
1282 if ((x < left) || (x > right)) {
1283 for (int i = 0; i < rows.size(); i++) {
1284 columns.get(x).get(i).setVisible(false);
1285 columns.get(x).setX(getWidth() + 1);
1286 }
1287 continue;
1288 }
1289 for (int i = 0; i < rows.size(); i++) {
1290 columns.get(x).get(i).setVisible(true);
1291 }
1292 columns.get(x).setX(leftCellX);
1293 leftCellX += columns.get(x).width + 1;
1294 }
1295
1296 int bottom = top;
1297
1298 done = false;
1299 while (!done) {
1300 int bottomCellY = (showColumnLabels ? COLUMN_LABEL_HEIGHT : 0);
1301 if (topBorder != Border.NONE) {
1302 bottomCellY++;
1303 }
1304 int maxCellY = bottomCellY + viewRows;
1305 bottom = top;
1306 for (int y = top; y < rows.size(); y++) {
1307 bottomCellY += rows.get(y).height;
1308 if (bottomCellY >= maxCellY) {
1309 break;
1310 }
1311 bottom++;
1312 }
1313 if (bottom < selectedRow) {
1314 // selectedRow is outside the view range. Push top down, and
1315 // calculate again.
1316 top++;
1317 } else {
1318 // selectedRow is inside the view range, done.
1319 done = true;
1320 }
1321 } // while (!done)
1322
1323 // We have the top/bottom range correct, set cell visibility and
1324 // row Y positions.
1325 int topCellY = showColumnLabels ? COLUMN_LABEL_HEIGHT : 0;
1326 if (topBorder != Border.NONE) {
1327 topCellY++;
1328 }
1329 for (int y = 0; y < rows.size(); y++) {
1330 if ((y < top) || (y > bottom)) {
1331 for (int i = 0; i < columns.size(); i++) {
1332 rows.get(y).get(i).setVisible(false);
1333 }
1334 rows.get(y).setY(getHeight() + 1);
1335 continue;
1336 }
1337 for (int i = 0; i < columns.size(); i++) {
1338 rows.get(y).get(i).setVisible(true);
1339 }
1340 rows.get(y).setY(topCellY);
1341 topCellY += rows.get(y).height;
1342 }
1343
1344 }
1345
1346 /**
1347 * Save contents to file.
1348 *
1349 * @param filename file to save to
1350 * @throws IOException if a java.io operation throws
1351 */
1352 public void saveToFilename(final String filename) throws IOException {
1353 // TODO
1354 }
1355
1356 }