Merge branch 'subtree'
[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.BufferedReader;
32 import java.io.BufferedWriter;
33 import java.io.File;
34 import java.io.FileReader;
35 import java.io.FileWriter;
36 import java.io.IOException;
37 import java.util.ArrayList;
38 import java.util.List;
39
40 import jexer.bits.CellAttributes;
41 import jexer.bits.StringUtils;
42 import jexer.event.TKeypressEvent;
43 import jexer.event.TMouseEvent;
44 import jexer.event.TResizeEvent;
45 import static jexer.TKeypress.*;
46
47 /**
48 * TTableWidget is used to display and edit regular two-dimensional tables of
49 * cells.
50 *
51 * This class was inspired by a TTable implementation originally developed by
52 * David "Niki" ROULET [niki@nikiroo.be], made available under MIT at
53 * https://github.com/nikiroo/jexer/tree/ttable_pull.
54 */
55 public class TTableWidget extends TWidget {
56
57 // ------------------------------------------------------------------------
58 // Constants --------------------------------------------------------------
59 // ------------------------------------------------------------------------
60
61 /**
62 * Available borders for cells.
63 */
64 public enum Border {
65 /**
66 * No border.
67 */
68 NONE,
69
70 /**
71 * Single bar: \u2502 (vertical) and \u2500 (horizontal).
72 */
73 SINGLE,
74
75 /**
76 * Double bar: \u2551 (vertical) and \u2550 (horizontal).
77 */
78 DOUBLE,
79
80 /**
81 * Thick bar: \u2503 (vertical heavy) and \u2501 (horizontal heavy).
82 */
83 THICK,
84 }
85
86 /**
87 * If true, put a grid of numbers in the cells.
88 */
89 private static final boolean DEBUG = false;
90
91 /**
92 * Row label width.
93 */
94 private static final int ROW_LABEL_WIDTH = 8;
95
96 /**
97 * Column label height.
98 */
99 private static final int COLUMN_LABEL_HEIGHT = 1;
100
101 /**
102 * Column default width.
103 */
104 private static final int COLUMN_DEFAULT_WIDTH = 8;
105
106 /**
107 * Extra rows to add.
108 */
109 private static final int EXTRA_ROWS = (DEBUG ? 10 : 0);
110
111 /**
112 * Extra columns to add.
113 */
114 private static final int EXTRA_COLUMNS = (DEBUG ? 3 : 0);
115
116 // ------------------------------------------------------------------------
117 // Variables --------------------------------------------------------------
118 // ------------------------------------------------------------------------
119
120 /**
121 * The underlying data, organized as columns.
122 */
123 private ArrayList<Column> columns = new ArrayList<Column>();
124
125 /**
126 * The underlying data, organized as rows.
127 */
128 private ArrayList<Row> rows = new ArrayList<Row>();
129
130 /**
131 * The row in model corresponding to the top-left visible cell.
132 */
133 private int top = 0;
134
135 /**
136 * The column in model corresponding to the top-left visible cell.
137 */
138 private int left = 0;
139
140 /**
141 * The row in model corresponding to the currently selected cell.
142 */
143 private int selectedRow = 0;
144
145 /**
146 * The column in model corresponding to the currently selected cell.
147 */
148 private int selectedColumn = 0;
149
150 /**
151 * If true, highlight the entire row of the currently-selected cell.
152 */
153 private boolean highlightRow = false;
154
155 /**
156 * If true, highlight the entire column of the currently-selected cell.
157 */
158 private boolean highlightColumn = false;
159
160 /**
161 * If true, show the row labels as the first column.
162 */
163 private boolean showRowLabels = true;
164
165 /**
166 * If true, show the column labels as the first row.
167 */
168 private boolean showColumnLabels = true;
169
170 /**
171 * The top border for the first row.
172 */
173 private Border topBorder = Border.NONE;
174
175 /**
176 * The left border for the first column.
177 */
178 private Border leftBorder = Border.NONE;
179
180 /**
181 * Column represents a column of cells.
182 */
183 public class Column {
184
185 /**
186 * X position of this column.
187 */
188 private int x = 0;
189
190 /**
191 * Width of column.
192 */
193 private int width = COLUMN_DEFAULT_WIDTH;
194
195 /**
196 * The cells of this column.
197 */
198 private ArrayList<Cell> cells = new ArrayList<Cell>();
199
200 /**
201 * Column label.
202 */
203 private String label = "";
204
205 /**
206 * The right border for this column.
207 */
208 private Border rightBorder = Border.NONE;
209
210 /**
211 * Constructor sets label to lettered column.
212 *
213 * @param col column number to use for this column. Column 0 will be
214 * "A", column 1 will be "B", column 26 will be "AA", and so on.
215 */
216 Column(int col) {
217 label = makeColumnLabel(col);
218 }
219
220 /**
221 * Add an entry to this column.
222 *
223 * @param cell the cell to add
224 */
225 public void add(final Cell cell) {
226 cells.add(cell);
227 }
228
229 /**
230 * Get an entry from this column.
231 *
232 * @param row the entry index to get
233 * @return the cell at row
234 */
235 public Cell get(final int row) {
236 return cells.get(row);
237 }
238
239 /**
240 * Get the X position of the cells in this column.
241 *
242 * @return the position
243 */
244 public int getX() {
245 return x;
246 }
247
248 /**
249 * Set the X position of the cells in this column.
250 *
251 * @param x the position
252 */
253 public void setX(final int x) {
254 for (Cell cell: cells) {
255 cell.setX(x);
256 }
257 this.x = x;
258 }
259
260 }
261
262 /**
263 * Row represents a row of cells.
264 */
265 public class Row {
266
267 /**
268 * Y position of this row.
269 */
270 private int y = 0;
271
272 /**
273 * Height of row.
274 */
275 private int height = 1;
276
277 /**
278 * The cells of this row.
279 */
280 private ArrayList<Cell> cells = new ArrayList<Cell>();
281
282 /**
283 * Row label.
284 */
285 private String label = "";
286
287 /**
288 * The bottom border for this row.
289 */
290 private Border bottomBorder = Border.NONE;
291
292 /**
293 * Constructor sets label to numbered row.
294 *
295 * @param row row number to use for this row
296 */
297 Row(final int row) {
298 label = Integer.toString(row);
299 }
300
301 /**
302 * Add an entry to this column.
303 *
304 * @param cell the cell to add
305 */
306 public void add(final Cell cell) {
307 cells.add(cell);
308 }
309
310 /**
311 * Get an entry from this row.
312 *
313 * @param column the entry index to get
314 * @return the cell at column
315 */
316 public Cell get(final int column) {
317 return cells.get(column);
318 }
319 /**
320 * Get the Y position of the cells in this column.
321 *
322 * @return the position
323 */
324 public int getY() {
325 return y;
326 }
327
328 /**
329 * Set the Y position of the cells in this column.
330 *
331 * @param y the position
332 */
333 public void setY(final int y) {
334 for (Cell cell: cells) {
335 cell.setY(y);
336 }
337 this.y = y;
338 }
339
340 }
341
342 /**
343 * Cell represents an editable cell in the table. Normally, navigation
344 * to a cell only highlights it; pressing Enter or F2 will switch to
345 * editing mode.
346 */
347 public class Cell extends TWidget {
348
349 // --------------------------------------------------------------------
350 // Variables ----------------------------------------------------------
351 // --------------------------------------------------------------------
352
353 /**
354 * The field containing the cell's data.
355 */
356 private TField field;
357
358 /**
359 * The column of this cell.
360 */
361 private int column;
362
363 /**
364 * The row of this cell.
365 */
366 private int row;
367
368 /**
369 * If true, the cell is being edited.
370 */
371 private boolean isEditing = false;
372
373 /**
374 * If true, the cell is read-only (non-editable).
375 */
376 private boolean readOnly = false;
377
378 /**
379 * Text of field before editing.
380 */
381 private String fieldText;
382
383 // --------------------------------------------------------------------
384 // Constructors -------------------------------------------------------
385 // --------------------------------------------------------------------
386
387 /**
388 * Public constructor.
389 *
390 * @param parent parent widget
391 * @param x column relative to parent
392 * @param y row relative to parent
393 * @param width width of widget
394 * @param height height of widget
395 * @param column column index of this cell
396 * @param row row index of this cell
397 */
398 public Cell(final TTableWidget parent, final int x, final int y,
399 final int width, final int height, final int column,
400 final int row) {
401
402 super(parent, x, y, width, height);
403 this.column = column;
404 this.row = row;
405
406 field = addField(0, 0, width, false);
407 field.setEnabled(false);
408 field.setBackgroundChar(' ');
409 }
410
411 // --------------------------------------------------------------------
412 // Event handlers -----------------------------------------------------
413 // --------------------------------------------------------------------
414
415 /**
416 * Handle mouse double-click events.
417 *
418 * @param mouse mouse double-click event
419 */
420 @Override
421 public void onMouseDoubleClick(final TMouseEvent mouse) {
422 // Use TWidget's code to pass the event to the children.
423 super.onMouseDown(mouse);
424
425 // Double-click means to start editing.
426 fieldText = field.getText();
427 isEditing = true;
428 field.setEnabled(true);
429 activate(field);
430
431 if (isActive()) {
432 // Let the table know that I was activated.
433 ((TTableWidget) getParent()).selectedRow = row;
434 ((TTableWidget) getParent()).selectedColumn = column;
435 ((TTableWidget) getParent()).alignGrid();
436 }
437 }
438
439 /**
440 * Handle mouse press events.
441 *
442 * @param mouse mouse button press event
443 */
444 @Override
445 public void onMouseDown(final TMouseEvent mouse) {
446 // Use TWidget's code to pass the event to the children.
447 super.onMouseDown(mouse);
448
449 if (isActive()) {
450 // Let the table know that I was activated.
451 ((TTableWidget) getParent()).selectedRow = row;
452 ((TTableWidget) getParent()).selectedColumn = column;
453 ((TTableWidget) getParent()).alignGrid();
454 }
455 }
456
457 /**
458 * Handle mouse release events.
459 *
460 * @param mouse mouse button release event
461 */
462 @Override
463 public void onMouseUp(final TMouseEvent mouse) {
464 // Use TWidget's code to pass the event to the children.
465 super.onMouseDown(mouse);
466
467 if (isActive()) {
468 // Let the table know that I was activated.
469 ((TTableWidget) getParent()).selectedRow = row;
470 ((TTableWidget) getParent()).selectedColumn = column;
471 ((TTableWidget) getParent()).alignGrid();
472 }
473 }
474
475 /**
476 * Handle keystrokes.
477 *
478 * @param keypress keystroke event
479 */
480 @Override
481 public void onKeypress(final TKeypressEvent keypress) {
482 // System.err.println("Cell onKeypress: " + keypress);
483
484 if (readOnly) {
485 // Read only: do nothing.
486 return;
487 }
488
489 if (isEditing) {
490 if (keypress.equals(kbEsc)) {
491 // ESC cancels the edit.
492 cancelEdit();
493 return;
494 }
495 if (keypress.equals(kbEnter)) {
496 // Enter ends editing.
497
498 // Pass down to field first so that it can execute
499 // enterAction if specified.
500 super.onKeypress(keypress);
501
502 fieldText = field.getText();
503 isEditing = false;
504 field.setEnabled(false);
505 return;
506 }
507 // Pass down to field.
508 super.onKeypress(keypress);
509 }
510
511 if (keypress.equals(kbEnter) || keypress.equals(kbF2)) {
512 // Enter or F2 starts editing.
513 fieldText = field.getText();
514 isEditing = true;
515 field.setEnabled(true);
516 activate(field);
517 return;
518 }
519 }
520
521 // --------------------------------------------------------------------
522 // TWidget ------------------------------------------------------------
523 // --------------------------------------------------------------------
524
525 /**
526 * Draw this cell.
527 */
528 @Override
529 public void draw() {
530 TTableWidget table = (TTableWidget) getParent();
531
532 if (isAbsoluteActive()) {
533 if (isEditing) {
534 field.setActiveColorKey("tfield.active");
535 field.setInactiveColorKey("tfield.inactive");
536 } else {
537 field.setActiveColorKey("ttable.selected");
538 field.setInactiveColorKey("ttable.selected");
539 }
540 } else if (((table.selectedColumn == column)
541 && ((table.selectedRow == row)
542 || (table.highlightColumn == true)))
543 || ((table.selectedRow == row)
544 && ((table.selectedColumn == column)
545 || (table.highlightRow == true)))
546 ) {
547 field.setActiveColorKey("ttable.active");
548 field.setInactiveColorKey("ttable.active");
549 } else {
550 field.setActiveColorKey("ttable.active");
551 field.setInactiveColorKey("ttable.inactive");
552 }
553
554 assert (isVisible() == true);
555
556 super.draw();
557 }
558
559 // --------------------------------------------------------------------
560 // TTable.Cell --------------------------------------------------------
561 // --------------------------------------------------------------------
562
563 /**
564 * Get field text.
565 *
566 * @return field text
567 */
568 public final String getText() {
569 return field.getText();
570 }
571
572 /**
573 * Set field text.
574 *
575 * @param text the new field text
576 */
577 public void setText(final String text) {
578 field.setText(text);
579 }
580
581 /**
582 * Cancel any pending edit.
583 */
584 public void cancelEdit() {
585 // Cancel any pending edit.
586 if (fieldText != null) {
587 field.setText(fieldText);
588 }
589 isEditing = false;
590 field.setEnabled(false);
591 }
592
593 /**
594 * Set an entire column of cells read-only (non-editable) or not.
595 *
596 * @param readOnly if true, the cells will be non-editable
597 */
598 public void setReadOnly(final boolean readOnly) {
599 cancelEdit();
600 this.readOnly = readOnly;
601 }
602
603 }
604
605 // ------------------------------------------------------------------------
606 // Constructors -----------------------------------------------------------
607 // ------------------------------------------------------------------------
608
609 /**
610 * Public constructor.
611 *
612 * @param parent parent widget
613 * @param x column relative to parent
614 * @param y row relative to parent
615 * @param width width of widget
616 * @param height height of widget
617 * @param gridColumns number of columns in grid
618 * @param gridRows number of rows in grid
619 */
620 public TTableWidget(final TWidget parent, final int x, final int y,
621 final int width, final int height, final int gridColumns,
622 final int gridRows) {
623
624 super(parent, x, y, width, height);
625
626 /*
627 System.err.println("gridColumns " + gridColumns +
628 " gridRows " + gridRows);
629 */
630
631 if (gridColumns < 1) {
632 throw new IllegalArgumentException("Column count cannot be less " +
633 "than 1");
634 }
635 if (gridRows < 1) {
636 throw new IllegalArgumentException("Row count cannot be less " +
637 "than 1");
638 }
639
640 // Initialize the starting row and column.
641 rows.add(new Row(0));
642 columns.add(new Column(0));
643 assert (rows.get(0).height == 1);
644
645 // Place a grid of cells that fit in this space.
646 for (int row = 0; row < gridRows; row++) {
647 for (int column = 0; column < gridColumns; column++) {
648 Cell cell = new Cell(this, 0, 0, COLUMN_DEFAULT_WIDTH, 1,
649 column, row);
650
651 if (DEBUG) {
652 // For debugging: set a grid of cell index labels.
653 cell.setText("" + row + " " + column);
654 }
655 rows.get(row).add(cell);
656 columns.get(column).add(cell);
657
658 if (columns.size() < gridColumns) {
659 columns.add(new Column(column + 1));
660 }
661 }
662 if (row < gridRows - 1) {
663 rows.add(new Row(row + 1));
664 }
665 }
666 for (int i = 0; i < rows.size(); i++) {
667 rows.get(i).setY(i + (showColumnLabels ? COLUMN_LABEL_HEIGHT : 0));
668 }
669 for (int j = 0; j < columns.size(); j++) {
670 columns.get(j).setX((j * (COLUMN_DEFAULT_WIDTH + 1)) +
671 (showRowLabels ? ROW_LABEL_WIDTH : 0));
672 }
673 activate(columns.get(selectedColumn).get(selectedRow));
674
675 alignGrid();
676 }
677
678 /**
679 * Public constructor.
680 *
681 * @param parent parent widget
682 * @param x column relative to parent
683 * @param y row relative to parent
684 * @param width width of widget
685 * @param height height of widget
686 */
687 public TTableWidget(final TWidget parent, final int x, final int y,
688 final int width, final int height) {
689
690 this(parent, x, y, width, height,
691 width / (COLUMN_DEFAULT_WIDTH + 1) + EXTRA_COLUMNS,
692 height + EXTRA_ROWS);
693 }
694
695 // ------------------------------------------------------------------------
696 // Event handlers ---------------------------------------------------------
697 // ------------------------------------------------------------------------
698
699 /**
700 * Handle mouse press events.
701 *
702 * @param mouse mouse button press event
703 */
704 @Override
705 public void onMouseDown(final TMouseEvent mouse) {
706 if (mouse.isMouseWheelUp() || mouse.isMouseWheelDown()) {
707 // Treat wheel up/down as 3 up/down
708 TKeypressEvent keyEvent;
709 if (mouse.isMouseWheelUp()) {
710 keyEvent = new TKeypressEvent(kbUp);
711 } else {
712 keyEvent = new TKeypressEvent(kbDown);
713 }
714 for (int i = 0; i < 3; i++) {
715 onKeypress(keyEvent);
716 }
717 return;
718 }
719
720 // Use TWidget's code to pass the event to the children.
721 super.onMouseDown(mouse);
722 }
723
724 /**
725 * Handle keystrokes.
726 *
727 * @param keypress keystroke event
728 */
729 @Override
730 public void onKeypress(final TKeypressEvent keypress) {
731 if (keypress.equals(kbTab)
732 || keypress.equals(kbShiftTab)
733 ) {
734 // Squash tab and back-tab. They don't make sense in the TTable
735 // grid context.
736 return;
737 }
738
739 // If editing, pass to that cell and do nothing else.
740 if (getSelectedCell().isEditing) {
741 super.onKeypress(keypress);
742 return;
743 }
744
745 if (keypress.equals(kbLeft)) {
746 // Left
747 if (selectedColumn > 0) {
748 selectedColumn--;
749 }
750 activate(columns.get(selectedColumn).get(selectedRow));
751 } else if (keypress.equals(kbRight)) {
752 // Right
753 if (selectedColumn < columns.size() - 1) {
754 selectedColumn++;
755 }
756 activate(columns.get(selectedColumn).get(selectedRow));
757 } else if (keypress.equals(kbUp)) {
758 // Up
759 if (selectedRow > 0) {
760 selectedRow--;
761 }
762 activate(columns.get(selectedColumn).get(selectedRow));
763 } else if (keypress.equals(kbDown)) {
764 // Down
765 if (selectedRow < rows.size() - 1) {
766 selectedRow++;
767 }
768 activate(columns.get(selectedColumn).get(selectedRow));
769 } else if (keypress.equals(kbHome)) {
770 // Home - leftmost column
771 selectedColumn = 0;
772 activate(columns.get(selectedColumn).get(selectedRow));
773 } else if (keypress.equals(kbEnd)) {
774 // End - rightmost column
775 selectedColumn = columns.size() - 1;
776 activate(columns.get(selectedColumn).get(selectedRow));
777 } else if (keypress.equals(kbPgUp)) {
778 // PgUp - Treat like multiple up
779 for (int i = 0; i < getHeight() - 2; i++) {
780 if (selectedRow > 0) {
781 selectedRow--;
782 }
783 }
784 activate(columns.get(selectedColumn).get(selectedRow));
785 } else if (keypress.equals(kbPgDn)) {
786 // PgDn - Treat like multiple up
787 for (int i = 0; i < getHeight() - 2; i++) {
788 if (selectedRow < rows.size() - 1) {
789 selectedRow++;
790 }
791 }
792 activate(columns.get(selectedColumn).get(selectedRow));
793 } else if (keypress.equals(kbCtrlHome)) {
794 // Ctrl-Home - go to top-left
795 selectedRow = 0;
796 selectedColumn = 0;
797 activate(columns.get(selectedColumn).get(selectedRow));
798 activate(columns.get(selectedColumn).get(selectedRow));
799 } else if (keypress.equals(kbCtrlEnd)) {
800 // Ctrl-End - go to bottom-right
801 selectedRow = rows.size() - 1;
802 selectedColumn = columns.size() - 1;
803 activate(columns.get(selectedColumn).get(selectedRow));
804 activate(columns.get(selectedColumn).get(selectedRow));
805 } else {
806 // Pass to the Cell.
807 super.onKeypress(keypress);
808 }
809
810 // We may have scrolled off screen. Reset positions as needed to
811 // make the newly selected cell visible.
812 alignGrid();
813 }
814
815 /**
816 * Handle widget resize events.
817 *
818 * @param event resize event
819 */
820 @Override
821 public void onResize(final TResizeEvent event) {
822 super.onResize(event);
823
824 bottomRightCorner();
825 }
826
827 // ------------------------------------------------------------------------
828 // TWidget ----------------------------------------------------------------
829 // ------------------------------------------------------------------------
830
831 /**
832 * Draw the table row/column labels, and borders.
833 */
834 @Override
835 public void draw() {
836 CellAttributes labelColor = getTheme().getColor("ttable.label");
837 CellAttributes labelColorSelected = getTheme().getColor("ttable.label.selected");
838 CellAttributes borderColor = getTheme().getColor("ttable.border");
839
840 // Column labels.
841 if (showColumnLabels == true) {
842 for (int i = left; i < columns.size(); i++) {
843 if (columns.get(i).get(top).isVisible() == false) {
844 break;
845 }
846 putStringXY(columns.get(i).get(top).getX(), 0,
847 String.format(" %-" +
848 (columns.get(i).width - 2)
849 + "s ", columns.get(i).label),
850 (i == selectedColumn ? labelColorSelected : labelColor));
851 }
852 }
853
854 // Row labels.
855 if (showRowLabels == true) {
856 for (int i = top; i < rows.size(); i++) {
857 if (rows.get(i).get(left).isVisible() == false) {
858 break;
859 }
860 putStringXY(0, rows.get(i).get(left).getY(),
861 String.format(" %-6s ", rows.get(i).label),
862 (i == selectedRow ? labelColorSelected : labelColor));
863 }
864 }
865
866 // Draw vertical borders.
867 if (leftBorder == Border.SINGLE) {
868 vLineXY((showRowLabels ? ROW_LABEL_WIDTH : 0),
869 (topBorder == Border.NONE ? 0 : 1) +
870 (showColumnLabels ? COLUMN_LABEL_HEIGHT : 0),
871 getHeight(), '\u2502', borderColor);
872 }
873 for (int i = left; i < columns.size(); i++) {
874 if (columns.get(i).get(top).isVisible() == false) {
875 break;
876 }
877 if (columns.get(i).rightBorder == Border.SINGLE) {
878 vLineXY(columns.get(i).getX() + columns.get(i).width,
879 (topBorder == Border.NONE ? 0 : 1) +
880 (showColumnLabels ? COLUMN_LABEL_HEIGHT : 0),
881 getHeight(), '\u2502', borderColor);
882 }
883 }
884
885 // Draw horizontal borders.
886 if (topBorder == Border.SINGLE) {
887 hLineXY((showRowLabels ? ROW_LABEL_WIDTH : 0),
888 (showColumnLabels ? COLUMN_LABEL_HEIGHT : 0),
889 getWidth(), '\u2500', borderColor);
890 }
891 for (int i = top; i < rows.size(); i++) {
892 if (rows.get(i).get(left).isVisible() == false) {
893 break;
894 }
895 if (rows.get(i).bottomBorder == Border.SINGLE) {
896 hLineXY((leftBorder == Border.NONE ? 0 : 1) +
897 (showRowLabels ? ROW_LABEL_WIDTH : 0),
898 rows.get(i).getY() + rows.get(i).height - 1,
899 getWidth(), '\u2500', borderColor);
900 } else if (rows.get(i).bottomBorder == Border.DOUBLE) {
901 hLineXY((leftBorder == Border.NONE ? 0 : 1) +
902 (showRowLabels ? ROW_LABEL_WIDTH : 0),
903 rows.get(i).getY() + rows.get(i).height - 1,
904 getWidth(), '\u2550', borderColor);
905 } else if (rows.get(i).bottomBorder == Border.THICK) {
906 hLineXY((leftBorder == Border.NONE ? 0 : 1) +
907 (showRowLabels ? ROW_LABEL_WIDTH : 0),
908 rows.get(i).getY() + rows.get(i).height - 1,
909 getWidth(), '\u2501', borderColor);
910 }
911 }
912 // Top-left corner if needed
913 if ((topBorder == Border.SINGLE) && (leftBorder == Border.SINGLE)) {
914 putCharXY((showRowLabels ? ROW_LABEL_WIDTH : 0),
915 (showColumnLabels ? COLUMN_LABEL_HEIGHT : 0),
916 '\u250c', borderColor);
917 }
918
919 // Now draw the correct corners
920 for (int i = top; i < rows.size(); i++) {
921 if (rows.get(i).get(left).isVisible() == false) {
922 break;
923 }
924 for (int j = left; j < columns.size(); j++) {
925 if (columns.get(j).get(i).isVisible() == false) {
926 break;
927 }
928 if ((i == top) && (topBorder == Border.SINGLE)
929 && (columns.get(j).rightBorder == Border.SINGLE)
930 ) {
931 // Top tee
932 putCharXY(columns.get(j).getX() + columns.get(j).width,
933 (showColumnLabels ? COLUMN_LABEL_HEIGHT : 0),
934 '\u252c', borderColor);
935 }
936 if ((j == left) && (leftBorder == Border.SINGLE)
937 && (rows.get(i).bottomBorder == Border.SINGLE)
938 ) {
939 // Left tee
940 putCharXY((showRowLabels ? ROW_LABEL_WIDTH : 0),
941 rows.get(i).getY() + rows.get(i).height - 1,
942 '\u251c', borderColor);
943 }
944 if ((columns.get(j).rightBorder == Border.SINGLE)
945 && (rows.get(i).bottomBorder == Border.SINGLE)
946 ) {
947 // Intersection of single bars
948 putCharXY(columns.get(j).getX() + columns.get(j).width,
949 rows.get(i).getY() + rows.get(i).height - 1,
950 '\u253c', borderColor);
951 }
952 if ((j == left) && (leftBorder == Border.SINGLE)
953 && (rows.get(i).bottomBorder == Border.DOUBLE)
954 ) {
955 // Left tee: single bar vertical, double bar horizontal
956 putCharXY((showRowLabels ? ROW_LABEL_WIDTH : 0),
957 rows.get(i).getY() + rows.get(i).height - 1,
958 '\u255e', borderColor);
959 }
960 if ((j == left) && (leftBorder == Border.SINGLE)
961 && (rows.get(i).bottomBorder == Border.THICK)
962 ) {
963 // Left tee: single bar vertical, thick bar horizontal
964 putCharXY((showRowLabels ? ROW_LABEL_WIDTH : 0),
965 rows.get(i).getY() + rows.get(i).height - 1,
966 '\u251d', borderColor);
967 }
968 if ((columns.get(j).rightBorder == Border.SINGLE)
969 && (rows.get(i).bottomBorder == Border.DOUBLE)
970 ) {
971 // Intersection: single bar vertical, double bar
972 // horizontal
973 putCharXY(columns.get(j).getX() + columns.get(j).width,
974 rows.get(i).getY() + rows.get(i).height - 1,
975 '\u256a', borderColor);
976 }
977 if ((columns.get(j).rightBorder == Border.SINGLE)
978 && (rows.get(i).bottomBorder == Border.THICK)
979 ) {
980 // Intersection: single bar vertical, thick bar
981 // horizontal
982 putCharXY(columns.get(j).getX() + columns.get(j).width,
983 rows.get(i).getY() + rows.get(i).height - 1,
984 '\u253f', borderColor);
985 }
986 }
987 }
988
989 // Now draw the window borders.
990 super.draw();
991 }
992
993 // ------------------------------------------------------------------------
994 // TTable -----------------------------------------------------------------
995 // ------------------------------------------------------------------------
996
997 /**
998 * Generate the default letter name for a column number.
999 *
1000 * @param col column number to use for this column. Column 0 will be
1001 * "A", column 1 will be "B", column 26 will be "AA", and so on.
1002 */
1003 private String makeColumnLabel(int col) {
1004 StringBuilder sb = new StringBuilder();
1005 for (;;) {
1006 sb.append((char) ('A' + (col % 26)));
1007 if (col < 26) {
1008 break;
1009 }
1010 col /= 26;
1011 }
1012 return sb.reverse().toString();
1013 }
1014
1015 /**
1016 * Get the currently-selected cell.
1017 *
1018 * @return the selected cell
1019 */
1020 public Cell getSelectedCell() {
1021 assert (rows.get(selectedRow) != null);
1022 assert (rows.get(selectedRow).get(selectedColumn) != null);
1023 assert (columns.get(selectedColumn) != null);
1024 assert (columns.get(selectedColumn).get(selectedRow) != null);
1025 assert (rows.get(selectedRow).get(selectedColumn) ==
1026 columns.get(selectedColumn).get(selectedRow));
1027
1028 return (columns.get(selectedColumn).get(selectedRow));
1029 }
1030
1031 /**
1032 * Get the currently-selected column.
1033 *
1034 * @return the selected column
1035 */
1036 public Column getSelectedColumn() {
1037 assert (selectedColumn >= 0);
1038 assert (columns.size() > selectedColumn);
1039 assert (columns.get(selectedColumn) != null);
1040 return columns.get(selectedColumn);
1041 }
1042
1043 /**
1044 * Get the currently-selected row.
1045 *
1046 * @return the selected row
1047 */
1048 public Row getSelectedRow() {
1049 assert (selectedRow >= 0);
1050 assert (rows.size() > selectedRow);
1051 assert (rows.get(selectedRow) != null);
1052 return rows.get(selectedRow);
1053 }
1054
1055 /**
1056 * Get the currently-selected column number. 0 is the left-most column.
1057 *
1058 * @return the selected column number
1059 */
1060 public int getSelectedColumnNumber() {
1061 return selectedColumn;
1062 }
1063
1064 /**
1065 * Set the currently-selected column number. 0 is the left-most column.
1066 *
1067 * @param column the column number to select
1068 */
1069 public void setSelectedColumnNumber(final int column) {
1070 if ((column < 0) || (column > columns.size() - 1)) {
1071 throw new IndexOutOfBoundsException("Column count is " +
1072 columns.size() + ", requested index " + column);
1073 }
1074 selectedColumn = column;
1075 activate(columns.get(selectedColumn).get(selectedRow));
1076 alignGrid();
1077 }
1078
1079 /**
1080 * Get the currently-selected row number. 0 is the top-most row.
1081 *
1082 * @return the selected row number
1083 */
1084 public int getSelectedRowNumber() {
1085 return selectedRow;
1086 }
1087
1088 /**
1089 * Set the currently-selected row number. 0 is the left-most column.
1090 *
1091 * @param row the row number to select
1092 */
1093 public void setSelectedRowNumber(final int row) {
1094 if ((row < 0) || (row > rows.size() - 1)) {
1095 throw new IndexOutOfBoundsException("Row count is " +
1096 rows.size() + ", requested index " + row);
1097 }
1098 selectedRow = row;
1099 activate(columns.get(selectedColumn).get(selectedRow));
1100 alignGrid();
1101 }
1102
1103 /**
1104 * Get the highlight row flag.
1105 *
1106 * @return true if the selected row is highlighted
1107 */
1108 public boolean getHighlightRow() {
1109 return highlightRow;
1110 }
1111
1112 /**
1113 * Set the highlight row flag.
1114 *
1115 * @param highlightRow if true, the selected row will be highlighted
1116 */
1117 public void setHighlightRow(final boolean highlightRow) {
1118 this.highlightRow = highlightRow;
1119 }
1120
1121 /**
1122 * Get the highlight column flag.
1123 *
1124 * @return true if the selected column is highlighted
1125 */
1126 public boolean getHighlightColumn() {
1127 return highlightColumn;
1128 }
1129
1130 /**
1131 * Set the highlight column flag.
1132 *
1133 * @param highlightColumn if true, the selected column will be highlighted
1134 */
1135 public void setHighlightColumn(final boolean highlightColumn) {
1136 this.highlightColumn = highlightColumn;
1137 }
1138
1139 /**
1140 * Get the show row labels flag.
1141 *
1142 * @return true if row labels are shown
1143 */
1144 public boolean getShowRowLabels() {
1145 return showRowLabels;
1146 }
1147
1148 /**
1149 * Set the show row labels flag.
1150 *
1151 * @param showRowLabels if true, the row labels will be shown
1152 */
1153 public void setShowRowLabels(final boolean showRowLabels) {
1154 this.showRowLabels = showRowLabels;
1155 }
1156
1157 /**
1158 * Get the show column labels flag.
1159 *
1160 * @return true if column labels are shown
1161 */
1162 public boolean getShowColumnLabels() {
1163 return showColumnLabels;
1164 }
1165
1166 /**
1167 * Set the show column labels flag.
1168 *
1169 * @param showColumnLabels if true, the column labels will be shown
1170 */
1171 public void setShowColumnLabels(final boolean showColumnLabels) {
1172 this.showColumnLabels = showColumnLabels;
1173 }
1174
1175 /**
1176 * Get the number of columns.
1177 *
1178 * @return the number of columns
1179 */
1180 public int getColumnCount() {
1181 return columns.size();
1182 }
1183
1184 /**
1185 * Get the number of rows.
1186 *
1187 * @return the number of rows
1188 */
1189 public int getRowCount() {
1190 return rows.size();
1191 }
1192
1193
1194 /**
1195 * Push top and left to the bottom-most right corner of the available
1196 * grid.
1197 */
1198 private void bottomRightCorner() {
1199 int viewColumns = getWidth();
1200 if (showRowLabels == true) {
1201 viewColumns -= ROW_LABEL_WIDTH;
1202 }
1203
1204 // Set left and top such that the table stays on screen if possible.
1205 top = rows.size() - getHeight();
1206 left = columns.size() - (getWidth() / (viewColumns / (COLUMN_DEFAULT_WIDTH + 1)));
1207 // Now ensure the selection is visible.
1208 alignGrid();
1209 }
1210
1211 /**
1212 * Align the grid so that the selected cell is fully visible.
1213 */
1214 private void alignGrid() {
1215
1216 /*
1217 System.err.println("alignGrid() # columns " + columns.size() +
1218 " # rows " + rows.size());
1219 */
1220
1221 int viewColumns = getWidth();
1222 if (showRowLabels == true) {
1223 viewColumns -= ROW_LABEL_WIDTH;
1224 }
1225 if (leftBorder != Border.NONE) {
1226 viewColumns--;
1227 }
1228 int viewRows = getHeight();
1229 if (showColumnLabels == true) {
1230 viewRows -= COLUMN_LABEL_HEIGHT;
1231 }
1232 if (topBorder != Border.NONE) {
1233 viewRows--;
1234 }
1235
1236 // If we pushed left or right, adjust the box to include the new
1237 // selected cell.
1238 if (selectedColumn < left) {
1239 left = selectedColumn - 1;
1240 }
1241 if (left < 0) {
1242 left = 0;
1243 }
1244 if (selectedRow < top) {
1245 top = selectedRow - 1;
1246 }
1247 if (top < 0) {
1248 top = 0;
1249 }
1250
1251 /*
1252 * viewColumns and viewRows now contain the available columns and
1253 * rows available to view the selected cell. We adjust left and top
1254 * to ensure the selected cell is within view, and then make all
1255 * cells outside the box between (left, top) and (right, bottom)
1256 * invisible.
1257 *
1258 * We need to calculate right and bottom now.
1259 */
1260 int right = left;
1261
1262 boolean done = false;
1263 while (!done) {
1264 int rightCellX = (showRowLabels ? ROW_LABEL_WIDTH : 0);
1265 if (leftBorder != Border.NONE) {
1266 rightCellX++;
1267 }
1268 int maxCellX = rightCellX + viewColumns;
1269 right = left;
1270 boolean selectedIsVisible = false;
1271 int selectedX = 0;
1272 for (int x = left; x < columns.size(); x++) {
1273 if (x == selectedColumn) {
1274 selectedX = rightCellX;
1275 if (selectedX + columns.get(x).width + 1 <= maxCellX) {
1276 selectedIsVisible = true;
1277 }
1278 }
1279 rightCellX += columns.get(x).width + 1;
1280 if (rightCellX >= maxCellX) {
1281 break;
1282 }
1283 right++;
1284 }
1285 if (right < selectedColumn) {
1286 // selectedColumn is outside the view range. Push left over,
1287 // and calculate again.
1288 left++;
1289 } else if (left == selectedColumn) {
1290 // selectedColumn doesn't fit inside the view range, but we
1291 // can't go over any further either. Bail out.
1292 done = true;
1293 } else if (selectedIsVisible == false) {
1294 // selectedColumn doesn't fit inside the view range, continue
1295 // on.
1296 left++;
1297 } else {
1298 // selectedColumn is fully visible, all done.
1299 assert (selectedIsVisible == true);
1300 done = true;
1301 }
1302
1303 } // while (!done)
1304
1305 // We have the left/right range correct, set cell visibility and
1306 // column X positions.
1307 int leftCellX = showRowLabels ? ROW_LABEL_WIDTH : 0;
1308 if (leftBorder != Border.NONE) {
1309 leftCellX++;
1310 }
1311 for (int x = 0; x < columns.size(); x++) {
1312 if ((x < left) || (x > right)) {
1313 for (int i = 0; i < rows.size(); i++) {
1314 columns.get(x).get(i).setVisible(false);
1315 columns.get(x).setX(getWidth() + 1);
1316 }
1317 continue;
1318 }
1319 for (int i = 0; i < rows.size(); i++) {
1320 columns.get(x).get(i).setVisible(true);
1321 }
1322 columns.get(x).setX(leftCellX);
1323 leftCellX += columns.get(x).width + 1;
1324 }
1325
1326 int bottom = top;
1327
1328 done = false;
1329 while (!done) {
1330 int bottomCellY = (showColumnLabels ? COLUMN_LABEL_HEIGHT : 0);
1331 if (topBorder != Border.NONE) {
1332 bottomCellY++;
1333 }
1334 int maxCellY = bottomCellY + viewRows;
1335 bottom = top;
1336 for (int y = top; y < rows.size(); y++) {
1337 bottomCellY += rows.get(y).height;
1338 if (bottomCellY >= maxCellY) {
1339 break;
1340 }
1341 bottom++;
1342 }
1343 if (bottom < selectedRow) {
1344 // selectedRow is outside the view range. Push top down, and
1345 // calculate again.
1346 top++;
1347 } else {
1348 // selectedRow is inside the view range, done.
1349 done = true;
1350 }
1351 } // while (!done)
1352
1353 // We have the top/bottom range correct, set cell visibility and
1354 // row Y positions.
1355 int topCellY = showColumnLabels ? COLUMN_LABEL_HEIGHT : 0;
1356 if (topBorder != Border.NONE) {
1357 topCellY++;
1358 }
1359 for (int y = 0; y < rows.size(); y++) {
1360 if ((y < top) || (y > bottom)) {
1361 for (int i = 0; i < columns.size(); i++) {
1362 rows.get(y).get(i).setVisible(false);
1363 }
1364 rows.get(y).setY(getHeight() + 1);
1365 continue;
1366 }
1367 for (int i = 0; i < columns.size(); i++) {
1368 rows.get(y).get(i).setVisible(true);
1369 }
1370 rows.get(y).setY(topCellY);
1371 topCellY += rows.get(y).height;
1372 }
1373
1374 // Last thing: cancel any edits that are not the selected cell.
1375 for (int y = 0; y < rows.size(); y++) {
1376 for (int x = 0; x < columns.size(); x++) {
1377 if ((x == selectedColumn) && (y == selectedRow)) {
1378 continue;
1379 }
1380 rows.get(y).get(x).cancelEdit();
1381 }
1382 }
1383 }
1384
1385 /**
1386 * Load contents from file in CSV format.
1387 *
1388 * @param csvFile a File referencing the CSV data
1389 * @throws IOException if a java.io operation throws
1390 */
1391 public void loadCsvFile(final File csvFile) throws IOException {
1392 BufferedReader reader = null;
1393
1394 try {
1395 reader = new BufferedReader(new FileReader(csvFile));
1396
1397 String line = null;
1398 boolean first = true;
1399 for (line = reader.readLine(); line != null;
1400 line = reader.readLine()) {
1401
1402 List<String> list = StringUtils.fromCsv(line);
1403 if (list.size() == 0) {
1404 continue;
1405 }
1406
1407 if (list.size() > columns.size()) {
1408 int n = list.size() - columns.size();
1409 for (int i = 0; i < n; i++) {
1410 selectedColumn = columns.size() - 1;
1411 insertColumnRight(selectedColumn);
1412 }
1413 }
1414 assert (list.size() == columns.size());
1415
1416 if (first) {
1417 // First row: just replace what is here.
1418 selectedRow = 0;
1419 first = false;
1420 } else {
1421 // All other rows: append to the end.
1422 selectedRow = rows.size() - 1;
1423 insertRowBelow(selectedRow);
1424 selectedRow = rows.size() - 1;
1425 }
1426 for (int i = 0; i < list.size(); i++) {
1427 rows.get(selectedRow).get(i).setText(list.get(i));
1428 }
1429 }
1430 } finally {
1431 if (reader != null) {
1432 reader.close();
1433 }
1434 }
1435
1436 left = 0;
1437 top = 0;
1438 selectedRow = 0;
1439 selectedColumn = 0;
1440 alignGrid();
1441 activate(columns.get(selectedColumn).get(selectedRow));
1442 }
1443
1444 /**
1445 * Save contents to file in CSV format.
1446 *
1447 * @param filename file to save to
1448 * @throws IOException if a java.io operation throws
1449 */
1450 public void saveToCsvFilename(final String filename) throws IOException {
1451 BufferedWriter writer = null;
1452
1453 try {
1454 writer = new BufferedWriter(new FileWriter(filename));
1455 for (Row row: rows) {
1456 List<String> list = new ArrayList<String>(row.cells.size());
1457 for (Cell cell: row.cells) {
1458 list.add(cell.getText());
1459 }
1460 writer.write(StringUtils.toCsv(list));
1461 writer.write("\n");
1462 }
1463 } finally {
1464 if (writer != null) {
1465 writer.close();
1466 }
1467 }
1468 }
1469
1470 /**
1471 * Save contents to file in text format with lines.
1472 *
1473 * @param filename file to save to
1474 * @throws IOException if a java.io operation throws
1475 */
1476 public void saveToTextFilename(final String filename) throws IOException {
1477 BufferedWriter writer = null;
1478
1479 try {
1480 writer = new BufferedWriter(new FileWriter(filename));
1481
1482 if ((topBorder == Border.SINGLE) && (leftBorder == Border.SINGLE)) {
1483 // Emit top-left corner.
1484 writer.write("\u250c");
1485 }
1486
1487 if (topBorder == Border.SINGLE) {
1488 int cellI = 0;
1489 for (Cell cell: rows.get(0).cells) {
1490 for (int i = 0; i < columns.get(cellI).width; i++) {
1491 writer.write("\u2500");
1492 }
1493
1494 if (columns.get(cellI).rightBorder == Border.SINGLE) {
1495 if (cellI < columns.size() - 1) {
1496 // Emit top tee.
1497 writer.write("\u252c");
1498 } else {
1499 // Emit top-right corner.
1500 writer.write("\u2510");
1501 }
1502 }
1503 cellI++;
1504 }
1505 }
1506 writer.write("\n");
1507
1508 int rowI = 0;
1509 for (Row row: rows) {
1510
1511 if (leftBorder == Border.SINGLE) {
1512 // Emit left border.
1513 writer.write("\u2502");
1514 }
1515
1516 int cellI = 0;
1517 for (Cell cell: row.cells) {
1518 writer.write(String.format("%" +
1519 columns.get(cellI).width + "s", cell.getText()));
1520
1521 if (columns.get(cellI).rightBorder == Border.SINGLE) {
1522 // Emit right border.
1523 writer.write("\u2502");
1524 }
1525 cellI++;
1526 }
1527 writer.write("\n");
1528
1529 if (row.bottomBorder == Border.NONE) {
1530 // All done, move on to the next row.
1531 continue;
1532 }
1533
1534 // Emit the bottom borders and intersections.
1535 if ((leftBorder == Border.SINGLE)
1536 && (row.bottomBorder != Border.NONE)
1537 ) {
1538 if (rowI < rows.size() - 1) {
1539 if (row.bottomBorder == Border.SINGLE) {
1540 // Emit left tee.
1541 writer.write("\u251c");
1542 } else if (row.bottomBorder == Border.DOUBLE) {
1543 // Emit left tee (double).
1544 writer.write("\u255e");
1545 } else if (row.bottomBorder == Border.THICK) {
1546 // Emit left tee (thick).
1547 writer.write("\u251d");
1548 }
1549 }
1550
1551 if (rowI == rows.size() - 1) {
1552 if (row.bottomBorder == Border.SINGLE) {
1553 // Emit left bottom corner.
1554 writer.write("\u2514");
1555 } else if (row.bottomBorder == Border.DOUBLE) {
1556 // Emit left bottom corner (double).
1557 writer.write("\u2558");
1558 } else if (row.bottomBorder == Border.THICK) {
1559 // Emit left bottom corner (thick).
1560 writer.write("\u2515");
1561 }
1562 }
1563 }
1564
1565 cellI = 0;
1566 for (Cell cell: row.cells) {
1567
1568 for (int i = 0; i < columns.get(cellI).width; i++) {
1569 if (row.bottomBorder == Border.SINGLE) {
1570 writer.write("\u2500");
1571 }
1572 if (row.bottomBorder == Border.DOUBLE) {
1573 writer.write("\u2550");
1574 }
1575 if (row.bottomBorder == Border.THICK) {
1576 writer.write("\u2501");
1577 }
1578 }
1579
1580 if ((rowI < rows.size() - 1)
1581 && (cellI == columns.size() - 1)
1582 && (row.bottomBorder == Border.SINGLE)
1583 && (columns.get(cellI).rightBorder == Border.SINGLE)
1584 ) {
1585 // Emit right tee.
1586 writer.write("\u2524");
1587 }
1588 if ((rowI < rows.size() - 1)
1589 && (cellI == columns.size() - 1)
1590 && (row.bottomBorder == Border.DOUBLE)
1591 && (columns.get(cellI).rightBorder == Border.SINGLE)
1592 ) {
1593 // Emit right tee (double).
1594 writer.write("\u2561");
1595 }
1596 if ((rowI < rows.size() - 1)
1597 && (cellI == columns.size() - 1)
1598 && (row.bottomBorder == Border.THICK)
1599 && (columns.get(cellI).rightBorder == Border.SINGLE)
1600 ) {
1601 // Emit right tee (thick).
1602 writer.write("\u2525");
1603 }
1604 if ((rowI == rows.size() - 1)
1605 && (cellI == columns.size() - 1)
1606 && (row.bottomBorder == Border.SINGLE)
1607 && (columns.get(cellI).rightBorder == Border.SINGLE)
1608 ) {
1609 // Emit right bottom corner.
1610 writer.write("\u2518");
1611 }
1612 if ((rowI == rows.size() - 1)
1613 && (cellI == columns.size() - 1)
1614 && (row.bottomBorder == Border.DOUBLE)
1615 && (columns.get(cellI).rightBorder == Border.SINGLE)
1616 ) {
1617 // Emit right bottom corner (double).
1618 writer.write("\u255b");
1619 }
1620 if ((rowI == rows.size() - 1)
1621 && (cellI == columns.size() - 1)
1622 && (row.bottomBorder == Border.THICK)
1623 && (columns.get(cellI).rightBorder == Border.SINGLE)
1624 ) {
1625 // Emit right bottom corner (thick).
1626 writer.write("\u2519");
1627 }
1628 if ((rowI < rows.size() - 1)
1629 && (cellI < columns.size() - 1)
1630 && (row.bottomBorder == Border.SINGLE)
1631 && (columns.get(cellI).rightBorder == Border.SINGLE)
1632 ) {
1633 // Emit intersection.
1634 writer.write("\u253c");
1635 }
1636 if ((rowI < rows.size() - 1)
1637 && (cellI < columns.size() - 1)
1638 && (row.bottomBorder == Border.DOUBLE)
1639 && (columns.get(cellI).rightBorder == Border.SINGLE)
1640 ) {
1641 // Emit intersection (double).
1642 writer.write("\u256a");
1643 }
1644 if ((rowI < rows.size() - 1)
1645 && (cellI < columns.size() - 1)
1646 && (row.bottomBorder == Border.THICK)
1647 && (columns.get(cellI).rightBorder == Border.SINGLE)
1648 ) {
1649 // Emit intersection (thick).
1650 writer.write("\u253f");
1651 }
1652 if ((rowI == rows.size() - 1)
1653 && (cellI < columns.size() - 1)
1654 && (row.bottomBorder == Border.SINGLE)
1655 && (columns.get(cellI).rightBorder == Border.SINGLE)
1656 ) {
1657 // Emit bottom tee.
1658 writer.write("\u2534");
1659 }
1660 if ((rowI == rows.size() - 1)
1661 && (cellI < columns.size() - 1)
1662 && (row.bottomBorder == Border.DOUBLE)
1663 && (columns.get(cellI).rightBorder == Border.SINGLE)
1664 ) {
1665 // Emit bottom tee (double).
1666 writer.write("\u2567");
1667 }
1668 if ((rowI == rows.size() - 1)
1669 && (cellI < columns.size() - 1)
1670 && (row.bottomBorder == Border.THICK)
1671 && (columns.get(cellI).rightBorder == Border.SINGLE)
1672 ) {
1673 // Emit bottom tee (thick).
1674 writer.write("\u2537");
1675 }
1676
1677 cellI++;
1678 }
1679
1680 writer.write("\n");
1681 rowI++;
1682 }
1683 } finally {
1684 if (writer != null) {
1685 writer.close();
1686 }
1687 }
1688 }
1689
1690 /**
1691 * Set the selected cell location.
1692 *
1693 * @param column the selected cell location column
1694 * @param row the selected cell location row
1695 */
1696 public void setSelectedCell(final int column, final int row) {
1697 if ((column < 0) || (column > columns.size() - 1)) {
1698 throw new IndexOutOfBoundsException("Column count is " +
1699 columns.size() + ", requested index " + column);
1700 }
1701 if ((row < 0) || (row > rows.size() - 1)) {
1702 throw new IndexOutOfBoundsException("Row count is " +
1703 rows.size() + ", requested index " + row);
1704 }
1705 selectedColumn = column;
1706 selectedRow = row;
1707 alignGrid();
1708 }
1709
1710 /**
1711 * Get a particular cell.
1712 *
1713 * @param column the cell column
1714 * @param row the cell row
1715 * @return the cell
1716 */
1717 public Cell getCell(final int column, final int row) {
1718 if ((column < 0) || (column > columns.size() - 1)) {
1719 throw new IndexOutOfBoundsException("Column count is " +
1720 columns.size() + ", requested index " + column);
1721 }
1722 if ((row < 0) || (row > rows.size() - 1)) {
1723 throw new IndexOutOfBoundsException("Row count is " +
1724 rows.size() + ", requested index " + row);
1725 }
1726 return rows.get(row).get(column);
1727 }
1728
1729 /**
1730 * Get the text of a particular cell.
1731 *
1732 * @param column the cell column
1733 * @param row the cell row
1734 * @return the text in the cell
1735 */
1736 public String getCellText(final int column, final int row) {
1737 if ((column < 0) || (column > columns.size() - 1)) {
1738 throw new IndexOutOfBoundsException("Column count is " +
1739 columns.size() + ", requested index " + column);
1740 }
1741 if ((row < 0) || (row > rows.size() - 1)) {
1742 throw new IndexOutOfBoundsException("Row count is " +
1743 rows.size() + ", requested index " + row);
1744 }
1745 return rows.get(row).get(column).getText();
1746 }
1747
1748 /**
1749 * Set the text of a particular cell.
1750 *
1751 * @param column the cell column
1752 * @param row the cell row
1753 * @param text the text to put into the cell
1754 */
1755 public void setCellText(final int column, final int row,
1756 final String text) {
1757
1758 if ((column < 0) || (column > columns.size() - 1)) {
1759 throw new IndexOutOfBoundsException("Column count is " +
1760 columns.size() + ", requested index " + column);
1761 }
1762 if ((row < 0) || (row > rows.size() - 1)) {
1763 throw new IndexOutOfBoundsException("Row count is " +
1764 rows.size() + ", requested index " + row);
1765 }
1766 rows.get(row).get(column).setText(text);
1767 }
1768
1769 /**
1770 * Set the action to perform when the user presses enter on a particular
1771 * cell.
1772 *
1773 * @param column the cell column
1774 * @param row the cell row
1775 * @param action the action to perform when the user presses enter on the
1776 * cell
1777 */
1778 public void setCellEnterAction(final int column, final int row,
1779 final TAction action) {
1780
1781 if ((column < 0) || (column > columns.size() - 1)) {
1782 throw new IndexOutOfBoundsException("Column count is " +
1783 columns.size() + ", requested index " + column);
1784 }
1785 if ((row < 0) || (row > rows.size() - 1)) {
1786 throw new IndexOutOfBoundsException("Row count is " +
1787 rows.size() + ", requested index " + row);
1788 }
1789 rows.get(row).get(column).field.setEnterAction(action);
1790 }
1791
1792 /**
1793 * Set the action to perform when the user updates a particular cell.
1794 *
1795 * @param column the cell column
1796 * @param row the cell row
1797 * @param action the action to perform when the user updates the cell
1798 */
1799 public void setCellUpdateAction(final int column, final int row,
1800 final TAction action) {
1801
1802 if ((column < 0) || (column > columns.size() - 1)) {
1803 throw new IndexOutOfBoundsException("Column count is " +
1804 columns.size() + ", requested index " + column);
1805 }
1806 if ((row < 0) || (row > rows.size() - 1)) {
1807 throw new IndexOutOfBoundsException("Row count is " +
1808 rows.size() + ", requested index " + row);
1809 }
1810 rows.get(row).get(column).field.setUpdateAction(action);
1811 }
1812
1813 /**
1814 * Get the width of a column.
1815 *
1816 * @param column the column number
1817 * @return the width of the column
1818 */
1819 public int getColumnWidth(final int column) {
1820 if ((column < 0) || (column > columns.size() - 1)) {
1821 throw new IndexOutOfBoundsException("Column count is " +
1822 columns.size() + ", requested index " + column);
1823 }
1824 return columns.get(column).width;
1825 }
1826
1827 /**
1828 * Set the width of a column.
1829 *
1830 * @param column the column number
1831 * @param width the new width of the column
1832 */
1833 public void setColumnWidth(final int column, final int width) {
1834 if ((column < 0) || (column > columns.size() - 1)) {
1835 throw new IndexOutOfBoundsException("Column count is " +
1836 columns.size() + ", requested index " + column);
1837 }
1838
1839 if (width < 4) {
1840 // Columns may not be smaller than 4 cells wide.
1841 return;
1842 }
1843
1844 int delta = width - columns.get(column).width;
1845 columns.get(column).width = width;
1846 for (Cell cell: columns.get(column).cells) {
1847 cell.setWidth(columns.get(column).width);
1848 cell.field.setWidth(columns.get(column).width);
1849 }
1850 for (int i = column + 1; i < columns.size(); i++) {
1851 columns.get(i).setX(columns.get(i).getX() + delta);
1852 }
1853 if (column == columns.size() - 1) {
1854 bottomRightCorner();
1855 } else {
1856 alignGrid();
1857 }
1858 }
1859
1860 /**
1861 * Get the label of a column.
1862 *
1863 * @param column the column number
1864 * @return the label of the column
1865 */
1866 public String getColumnLabel(final int column) {
1867 if ((column < 0) || (column > columns.size() - 1)) {
1868 throw new IndexOutOfBoundsException("Column count is " +
1869 columns.size() + ", requested index " + column);
1870 }
1871 return columns.get(column).label;
1872 }
1873
1874 /**
1875 * Set the label of a column.
1876 *
1877 * @param column the column number
1878 * @param label the new label of the column
1879 */
1880 public void setColumnLabel(final int column, final String label) {
1881 if ((column < 0) || (column > columns.size() - 1)) {
1882 throw new IndexOutOfBoundsException("Column count is " +
1883 columns.size() + ", requested index " + column);
1884 }
1885 columns.get(column).label = label;
1886 }
1887
1888 /**
1889 * Get the label of a row.
1890 *
1891 * @param row the row number
1892 * @return the label of the row
1893 */
1894 public String getRowLabel(final int row) {
1895 if ((row < 0) || (row > rows.size() - 1)) {
1896 throw new IndexOutOfBoundsException("Row count is " +
1897 rows.size() + ", requested index " + row);
1898 }
1899 return rows.get(row).label;
1900 }
1901
1902 /**
1903 * Set the label of a row.
1904 *
1905 * @param row the row number
1906 * @param label the new label of the row
1907 */
1908 public void setRowLabel(final int row, final String label) {
1909 if ((row < 0) || (row > rows.size() - 1)) {
1910 throw new IndexOutOfBoundsException("Row count is " +
1911 rows.size() + ", requested index " + row);
1912 }
1913 rows.get(row).label = label;
1914 }
1915
1916 /**
1917 * Insert one row at a particular index.
1918 *
1919 * @param idx the row number
1920 */
1921 private void insertRowAt(final int idx) {
1922 Row newRow = new Row(idx);
1923 for (int i = 0; i < columns.size(); i++) {
1924 Cell cell = new Cell(this, columns.get(i).getX(),
1925 rows.get(idx).getY(), COLUMN_DEFAULT_WIDTH, 1, i, idx);
1926 newRow.add(cell);
1927 columns.get(i).cells.add(idx, cell);
1928 }
1929 rows.add(idx, newRow);
1930
1931 for (int x = 0; x < columns.size(); x++) {
1932 for (int y = idx; y < rows.size(); y++) {
1933 columns.get(x).get(y).row = y;
1934 columns.get(x).get(y).column = x;
1935 }
1936 }
1937 for (int i = idx + 1; i < rows.size(); i++) {
1938 String oldRowLabel = Integer.toString(i - 1);
1939 if (rows.get(i).label.equals(oldRowLabel)) {
1940 rows.get(i).label = Integer.toString(i);
1941 }
1942 }
1943 alignGrid();
1944 }
1945
1946 /**
1947 * Insert one row above a particular row.
1948 *
1949 * @param row the row number
1950 */
1951 public void insertRowAbove(final int row) {
1952 if ((row < 0) || (row > rows.size() - 1)) {
1953 throw new IndexOutOfBoundsException("Row count is " +
1954 rows.size() + ", requested index " + row);
1955 }
1956 insertRowAt(row);
1957 selectedRow++;
1958 activate(columns.get(selectedColumn).get(selectedRow));
1959 }
1960
1961 /**
1962 * Insert one row below a particular row.
1963 *
1964 * @param row the row number
1965 */
1966 public void insertRowBelow(final int row) {
1967 if ((row < 0) || (row > rows.size() - 1)) {
1968 throw new IndexOutOfBoundsException("Row count is " +
1969 rows.size() + ", requested index " + row);
1970 }
1971 int idx = row + 1;
1972 if (idx < rows.size()) {
1973 insertRowAt(idx);
1974 activate(columns.get(selectedColumn).get(selectedRow));
1975 return;
1976 }
1977
1978 // row is the last row, we need to perform an append.
1979 Row newRow = new Row(idx);
1980 for (int i = 0; i < columns.size(); i++) {
1981 Cell cell = new Cell(this, columns.get(i).getX(),
1982 rows.get(row).getY(), COLUMN_DEFAULT_WIDTH, 1, i, idx);
1983 newRow.add(cell);
1984 columns.get(i).cells.add(cell);
1985 }
1986 rows.add(newRow);
1987 alignGrid();
1988 activate(columns.get(selectedColumn).get(selectedRow));
1989 }
1990
1991 /**
1992 * Delete a particular row.
1993 *
1994 * @param row the row number
1995 */
1996 public void deleteRow(final int row) {
1997 if ((row < 0) || (row > rows.size() - 1)) {
1998 throw new IndexOutOfBoundsException("Row count is " +
1999 rows.size() + ", requested index " + row);
2000 }
2001 if (rows.size() == 1) {
2002 // Don't delete the last row.
2003 return;
2004 }
2005 for (int i = 0; i < columns.size(); i++) {
2006 Cell cell = columns.get(i).cells.remove(row);
2007 getChildren().remove(cell);
2008 }
2009 rows.remove(row);
2010
2011 for (int x = 0; x < columns.size(); x++) {
2012 for (int y = row; y < rows.size(); y++) {
2013 columns.get(x).get(y).row = y;
2014 columns.get(x).get(y).column = x;
2015 }
2016 }
2017 for (int i = row; i < rows.size(); i++) {
2018 String oldRowLabel = Integer.toString(i + 1);
2019 if (rows.get(i).label.equals(oldRowLabel)) {
2020 rows.get(i).label = Integer.toString(i);
2021 }
2022 }
2023 if (selectedRow == rows.size()) {
2024 selectedRow--;
2025 }
2026 activate(columns.get(selectedColumn).get(selectedRow));
2027 bottomRightCorner();
2028 }
2029
2030 /**
2031 * Insert one column at a particular index.
2032 *
2033 * @param idx the column number
2034 */
2035 private void insertColumnAt(final int idx) {
2036 Column newColumn = new Column(idx);
2037 for (int i = 0; i < rows.size(); i++) {
2038 Cell cell = new Cell(this, columns.get(idx).getX(),
2039 rows.get(i).getY(), COLUMN_DEFAULT_WIDTH, 1, idx, i);
2040 newColumn.add(cell);
2041 rows.get(i).cells.add(idx, cell);
2042 }
2043 columns.add(idx, newColumn);
2044
2045 for (int x = idx; x < columns.size(); x++) {
2046 for (int y = 0; y < rows.size(); y++) {
2047 columns.get(x).get(y).row = y;
2048 columns.get(x).get(y).column = x;
2049 }
2050 }
2051 for (int i = idx + 1; i < columns.size(); i++) {
2052 String oldColumnLabel = makeColumnLabel(i - 1);
2053 if (columns.get(i).label.equals(oldColumnLabel)) {
2054 columns.get(i).label = makeColumnLabel(i);
2055 }
2056 }
2057 alignGrid();
2058 }
2059
2060 /**
2061 * Insert one column to the left of a particular column.
2062 *
2063 * @param column the column number
2064 */
2065 public void insertColumnLeft(final int column) {
2066 if ((column < 0) || (column > columns.size() - 1)) {
2067 throw new IndexOutOfBoundsException("Column count is " +
2068 columns.size() + ", requested index " + column);
2069 }
2070 insertColumnAt(column);
2071 selectedColumn++;
2072 activate(columns.get(selectedColumn).get(selectedRow));
2073 }
2074
2075 /**
2076 * Insert one column to the right of a particular column.
2077 *
2078 * @param column the column number
2079 */
2080 public void insertColumnRight(final int column) {
2081 if ((column < 0) || (column > columns.size() - 1)) {
2082 throw new IndexOutOfBoundsException("Column count is " +
2083 columns.size() + ", requested index " + column);
2084 }
2085 int idx = column + 1;
2086 if (idx < columns.size()) {
2087 insertColumnAt(idx);
2088 activate(columns.get(selectedColumn).get(selectedRow));
2089 return;
2090 }
2091
2092 // column is the last column, we need to perform an append.
2093 Column newColumn = new Column(idx);
2094 for (int i = 0; i < rows.size(); i++) {
2095 Cell cell = new Cell(this, columns.get(column).getX(),
2096 rows.get(i).getY(), COLUMN_DEFAULT_WIDTH, 1, idx, i);
2097 newColumn.add(cell);
2098 rows.get(i).cells.add(cell);
2099 }
2100 columns.add(newColumn);
2101 alignGrid();
2102 activate(columns.get(selectedColumn).get(selectedRow));
2103 }
2104
2105 /**
2106 * Delete a particular column.
2107 *
2108 * @param column the column number
2109 */
2110 public void deleteColumn(final int column) {
2111 if ((column < 0) || (column > columns.size() - 1)) {
2112 throw new IndexOutOfBoundsException("Column count is " +
2113 columns.size() + ", requested index " + column);
2114 }
2115 if (columns.size() == 1) {
2116 // Don't delete the last column.
2117 return;
2118 }
2119 for (int i = 0; i < rows.size(); i++) {
2120 Cell cell = rows.get(i).cells.remove(column);
2121 getChildren().remove(cell);
2122 }
2123 columns.remove(column);
2124
2125 for (int x = column; x < columns.size(); x++) {
2126 for (int y = 0; y < rows.size(); y++) {
2127 columns.get(x).get(y).row = y;
2128 columns.get(x).get(y).column = x;
2129 }
2130 }
2131 for (int i = column; i < columns.size(); i++) {
2132 String oldColumnLabel = makeColumnLabel(i + 1);
2133 if (columns.get(i).label.equals(oldColumnLabel)) {
2134 columns.get(i).label = makeColumnLabel(i);
2135 }
2136 }
2137 if (selectedColumn == columns.size()) {
2138 selectedColumn--;
2139 }
2140 activate(columns.get(selectedColumn).get(selectedRow));
2141 bottomRightCorner();
2142 }
2143
2144 /**
2145 * Delete the selected cell, shifting cells over to the left.
2146 */
2147 public void deleteCellShiftLeft() {
2148 // All we do is copy the text from every cell in this row over.
2149 for (int i = selectedColumn + 1; i < columns.size(); i++) {
2150 setCellText(i - 1, selectedRow, getCellText(i, selectedRow));
2151 }
2152 setCellText(columns.size() - 1, selectedRow, "");
2153 }
2154
2155 /**
2156 * Delete the selected cell, shifting cells from below up.
2157 */
2158 public void deleteCellShiftUp() {
2159 // All we do is copy the text from every cell in this column up.
2160 for (int i = selectedRow + 1; i < rows.size(); i++) {
2161 setCellText(selectedColumn, i - 1, getCellText(selectedColumn, i));
2162 }
2163 setCellText(selectedColumn, rows.size() - 1, "");
2164 }
2165
2166 /**
2167 * Set a particular cell read-only (non-editable) or not.
2168 *
2169 * @param column the cell column
2170 * @param row the cell row
2171 * @param readOnly if true, the cell will be non-editable
2172 */
2173 public void setCellReadOnly(final int column, final int row,
2174 final boolean readOnly) {
2175
2176 if ((column < 0) || (column > columns.size() - 1)) {
2177 throw new IndexOutOfBoundsException("Column count is " +
2178 columns.size() + ", requested index " + column);
2179 }
2180 if ((row < 0) || (row > rows.size() - 1)) {
2181 throw new IndexOutOfBoundsException("Row count is " +
2182 rows.size() + ", requested index " + row);
2183 }
2184 rows.get(row).get(column).setReadOnly(readOnly);
2185 }
2186
2187 /**
2188 * Set an entire row of cells read-only (non-editable) or not.
2189 *
2190 * @param row the row number
2191 * @param readOnly if true, the cells will be non-editable
2192 */
2193 public void setRowReadOnly(final int row, final boolean readOnly) {
2194 if ((row < 0) || (row > rows.size() - 1)) {
2195 throw new IndexOutOfBoundsException("Row count is " +
2196 rows.size() + ", requested index " + row);
2197 }
2198 for (Cell cell: rows.get(row).cells) {
2199 cell.setReadOnly(readOnly);
2200 }
2201 }
2202
2203 /**
2204 * Set an entire column of cells read-only (non-editable) or not.
2205 *
2206 * @param column the column number
2207 * @param readOnly if true, the cells will be non-editable
2208 */
2209 public void setColumnReadOnly(final int column, final boolean readOnly) {
2210 if ((column < 0) || (column > columns.size() - 1)) {
2211 throw new IndexOutOfBoundsException("Column count is " +
2212 columns.size() + ", requested index " + column);
2213 }
2214 for (Cell cell: columns.get(column).cells) {
2215 cell.setReadOnly(readOnly);
2216 }
2217 }
2218
2219 /**
2220 * Set all borders across the entire table to Border.NONE.
2221 */
2222 public void setBorderAllNone() {
2223 topBorder = Border.NONE;
2224 leftBorder = Border.NONE;
2225 for (int i = 0; i < columns.size(); i++) {
2226 columns.get(i).rightBorder = Border.NONE;
2227 }
2228 for (int i = 0; i < rows.size(); i++) {
2229 rows.get(i).bottomBorder = Border.NONE;
2230 rows.get(i).height = 1;
2231 }
2232 bottomRightCorner();
2233 }
2234
2235 /**
2236 * Set all borders across the entire table to Border.SINGLE.
2237 */
2238 public void setBorderAllSingle() {
2239 topBorder = Border.SINGLE;
2240 leftBorder = Border.SINGLE;
2241 for (int i = 0; i < columns.size(); i++) {
2242 columns.get(i).rightBorder = Border.SINGLE;
2243 }
2244 for (int i = 0; i < rows.size(); i++) {
2245 rows.get(i).bottomBorder = Border.SINGLE;
2246 rows.get(i).height = 2;
2247 }
2248 alignGrid();
2249 }
2250
2251 /**
2252 * Set all borders around the selected cell to Border.NONE.
2253 */
2254 public void setBorderCellNone() {
2255 if (selectedRow == 0) {
2256 topBorder = Border.NONE;
2257 }
2258 if (selectedColumn == 0) {
2259 leftBorder = Border.NONE;
2260 }
2261 if (selectedColumn > 0) {
2262 columns.get(selectedColumn - 1).rightBorder = Border.NONE;
2263 }
2264 columns.get(selectedColumn).rightBorder = Border.NONE;
2265 if (selectedRow > 0) {
2266 rows.get(selectedRow - 1).bottomBorder = Border.NONE;
2267 rows.get(selectedRow - 1).height = 1;
2268 }
2269 rows.get(selectedRow).bottomBorder = Border.NONE;
2270 rows.get(selectedRow).height = 1;
2271 bottomRightCorner();
2272 }
2273
2274 /**
2275 * Set all borders around the selected cell to Border.SINGLE.
2276 */
2277 public void setBorderCellSingle() {
2278 if (selectedRow == 0) {
2279 topBorder = Border.SINGLE;
2280 }
2281 if (selectedColumn == 0) {
2282 leftBorder = Border.SINGLE;
2283 }
2284 if (selectedColumn > 0) {
2285 columns.get(selectedColumn - 1).rightBorder = Border.SINGLE;
2286 }
2287 columns.get(selectedColumn).rightBorder = Border.SINGLE;
2288 if (selectedRow > 0) {
2289 rows.get(selectedRow - 1).bottomBorder = Border.SINGLE;
2290 rows.get(selectedRow - 1).height = 2;
2291 }
2292 rows.get(selectedRow).bottomBorder = Border.SINGLE;
2293 rows.get(selectedRow).height = 2;
2294 alignGrid();
2295 }
2296
2297 /**
2298 * Set the column border to the right of the selected cell to
2299 * Border.SINGLE.
2300 */
2301 public void setBorderColumnRightSingle() {
2302 columns.get(selectedColumn).rightBorder = Border.SINGLE;
2303 alignGrid();
2304 }
2305
2306 /**
2307 * Set the column border to the right of the selected cell to
2308 * Border.SINGLE.
2309 */
2310 public void setBorderColumnLeftSingle() {
2311 if (selectedColumn == 0) {
2312 leftBorder = Border.SINGLE;
2313 } else {
2314 columns.get(selectedColumn - 1).rightBorder = Border.SINGLE;
2315 }
2316 alignGrid();
2317 }
2318
2319 /**
2320 * Set the row border above the selected cell to Border.SINGLE.
2321 */
2322 public void setBorderRowAboveSingle() {
2323 if (selectedRow == 0) {
2324 topBorder = Border.SINGLE;
2325 } else {
2326 rows.get(selectedRow - 1).bottomBorder = Border.SINGLE;
2327 rows.get(selectedRow - 1).height = 2;
2328 }
2329 alignGrid();
2330 }
2331
2332 /**
2333 * Set the row border below the selected cell to Border.SINGLE.
2334 */
2335 public void setBorderRowBelowSingle() {
2336 rows.get(selectedRow).bottomBorder = Border.SINGLE;
2337 rows.get(selectedRow).height = 2;
2338 alignGrid();
2339 }
2340
2341 /**
2342 * Set the row border below the selected cell to Border.DOUBLE.
2343 */
2344 public void setBorderRowBelowDouble() {
2345 rows.get(selectedRow).bottomBorder = Border.DOUBLE;
2346 rows.get(selectedRow).height = 2;
2347 alignGrid();
2348 }
2349
2350 /**
2351 * Set the row border below the selected cell to Border.THICK.
2352 */
2353 public void setBorderRowBelowThick() {
2354 rows.get(selectedRow).bottomBorder = Border.THICK;
2355 rows.get(selectedRow).height = 2;
2356 alignGrid();
2357 }
2358
2359 }