Merge commit '929409950e82914aa3cee323cfa7c5007585d2ea'
[nikiroo-utils.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 // TODO: detect header line
1431 }
1432 } finally {
1433 if (reader != null) {
1434 reader.close();
1435 }
1436 }
1437
1438 left = 0;
1439 top = 0;
1440 selectedRow = 0;
1441 selectedColumn = 0;
1442 alignGrid();
1443 activate(columns.get(selectedColumn).get(selectedRow));
1444 }
1445
1446 /**
1447 * Save contents to file in CSV format.
1448 *
1449 * @param filename file to save to
1450 * @throws IOException if a java.io operation throws
1451 */
1452 public void saveToCsvFilename(final String filename) throws IOException {
1453 BufferedWriter writer = null;
1454
1455 try {
1456 writer = new BufferedWriter(new FileWriter(filename));
1457 for (Row row: rows) {
1458 List<String> list = new ArrayList<String>(row.cells.size());
1459 for (Cell cell: row.cells) {
1460 list.add(cell.getText());
1461 }
1462 writer.write(StringUtils.toCsv(list));
1463 writer.write("\n");
1464 }
1465 } finally {
1466 if (writer != null) {
1467 writer.close();
1468 }
1469 }
1470 }
1471
1472 /**
1473 * Save contents to file in text format with lines.
1474 *
1475 * @param filename file to save to
1476 * @throws IOException if a java.io operation throws
1477 */
1478 public void saveToTextFilename(final String filename) throws IOException {
1479 BufferedWriter writer = null;
1480
1481 try {
1482 writer = new BufferedWriter(new FileWriter(filename));
1483
1484 if ((topBorder == Border.SINGLE) && (leftBorder == Border.SINGLE)) {
1485 // Emit top-left corner.
1486 writer.write("\u250c");
1487 }
1488
1489 if (topBorder == Border.SINGLE) {
1490 int cellI = 0;
1491 for (Cell cell: rows.get(0).cells) {
1492 for (int i = 0; i < columns.get(cellI).width; i++) {
1493 writer.write("\u2500");
1494 }
1495
1496 if (columns.get(cellI).rightBorder == Border.SINGLE) {
1497 if (cellI < columns.size() - 1) {
1498 // Emit top tee.
1499 writer.write("\u252c");
1500 } else {
1501 // Emit top-right corner.
1502 writer.write("\u2510");
1503 }
1504 }
1505 cellI++;
1506 }
1507 }
1508 writer.write("\n");
1509
1510 int rowI = 0;
1511 for (Row row: rows) {
1512
1513 if (leftBorder == Border.SINGLE) {
1514 // Emit left border.
1515 writer.write("\u2502");
1516 }
1517
1518 int cellI = 0;
1519 for (Cell cell: row.cells) {
1520 writer.write(String.format("%" +
1521 columns.get(cellI).width + "s", cell.getText()));
1522
1523 if (columns.get(cellI).rightBorder == Border.SINGLE) {
1524 // Emit right border.
1525 writer.write("\u2502");
1526 }
1527 cellI++;
1528 }
1529 writer.write("\n");
1530
1531 if (row.bottomBorder == Border.NONE) {
1532 // All done, move on to the next row.
1533 continue;
1534 }
1535
1536 // Emit the bottom borders and intersections.
1537 if ((leftBorder == Border.SINGLE)
1538 && (row.bottomBorder != Border.NONE)
1539 ) {
1540 if (rowI < rows.size() - 1) {
1541 if (row.bottomBorder == Border.SINGLE) {
1542 // Emit left tee.
1543 writer.write("\u251c");
1544 } else if (row.bottomBorder == Border.DOUBLE) {
1545 // Emit left tee (double).
1546 writer.write("\u255e");
1547 } else if (row.bottomBorder == Border.THICK) {
1548 // Emit left tee (thick).
1549 writer.write("\u251d");
1550 }
1551 }
1552
1553 if (rowI == rows.size() - 1) {
1554 if (row.bottomBorder == Border.SINGLE) {
1555 // Emit left bottom corner.
1556 writer.write("\u2514");
1557 } else if (row.bottomBorder == Border.DOUBLE) {
1558 // Emit left bottom corner (double).
1559 writer.write("\u2558");
1560 } else if (row.bottomBorder == Border.THICK) {
1561 // Emit left bottom corner (thick).
1562 writer.write("\u2515");
1563 }
1564 }
1565 }
1566
1567 cellI = 0;
1568 for (Cell cell: row.cells) {
1569
1570 for (int i = 0; i < columns.get(cellI).width; i++) {
1571 if (row.bottomBorder == Border.SINGLE) {
1572 writer.write("\u2500");
1573 }
1574 if (row.bottomBorder == Border.DOUBLE) {
1575 writer.write("\u2550");
1576 }
1577 if (row.bottomBorder == Border.THICK) {
1578 writer.write("\u2501");
1579 }
1580 }
1581
1582 if ((rowI < rows.size() - 1)
1583 && (cellI == columns.size() - 1)
1584 && (row.bottomBorder == Border.SINGLE)
1585 && (columns.get(cellI).rightBorder == Border.SINGLE)
1586 ) {
1587 // Emit right tee.
1588 writer.write("\u2524");
1589 }
1590 if ((rowI < rows.size() - 1)
1591 && (cellI == columns.size() - 1)
1592 && (row.bottomBorder == Border.DOUBLE)
1593 && (columns.get(cellI).rightBorder == Border.SINGLE)
1594 ) {
1595 // Emit right tee (double).
1596 writer.write("\u2561");
1597 }
1598 if ((rowI < rows.size() - 1)
1599 && (cellI == columns.size() - 1)
1600 && (row.bottomBorder == Border.THICK)
1601 && (columns.get(cellI).rightBorder == Border.SINGLE)
1602 ) {
1603 // Emit right tee (thick).
1604 writer.write("\u2525");
1605 }
1606 if ((rowI == rows.size() - 1)
1607 && (cellI == columns.size() - 1)
1608 && (row.bottomBorder == Border.SINGLE)
1609 && (columns.get(cellI).rightBorder == Border.SINGLE)
1610 ) {
1611 // Emit right bottom corner.
1612 writer.write("\u2518");
1613 }
1614 if ((rowI == rows.size() - 1)
1615 && (cellI == columns.size() - 1)
1616 && (row.bottomBorder == Border.DOUBLE)
1617 && (columns.get(cellI).rightBorder == Border.SINGLE)
1618 ) {
1619 // Emit right bottom corner (double).
1620 writer.write("\u255b");
1621 }
1622 if ((rowI == rows.size() - 1)
1623 && (cellI == columns.size() - 1)
1624 && (row.bottomBorder == Border.THICK)
1625 && (columns.get(cellI).rightBorder == Border.SINGLE)
1626 ) {
1627 // Emit right bottom corner (thick).
1628 writer.write("\u2519");
1629 }
1630 if ((rowI < rows.size() - 1)
1631 && (cellI < columns.size() - 1)
1632 && (row.bottomBorder == Border.SINGLE)
1633 && (columns.get(cellI).rightBorder == Border.SINGLE)
1634 ) {
1635 // Emit intersection.
1636 writer.write("\u253c");
1637 }
1638 if ((rowI < rows.size() - 1)
1639 && (cellI < columns.size() - 1)
1640 && (row.bottomBorder == Border.DOUBLE)
1641 && (columns.get(cellI).rightBorder == Border.SINGLE)
1642 ) {
1643 // Emit intersection (double).
1644 writer.write("\u256a");
1645 }
1646 if ((rowI < rows.size() - 1)
1647 && (cellI < columns.size() - 1)
1648 && (row.bottomBorder == Border.THICK)
1649 && (columns.get(cellI).rightBorder == Border.SINGLE)
1650 ) {
1651 // Emit intersection (thick).
1652 writer.write("\u253f");
1653 }
1654 if ((rowI == rows.size() - 1)
1655 && (cellI < columns.size() - 1)
1656 && (row.bottomBorder == Border.SINGLE)
1657 && (columns.get(cellI).rightBorder == Border.SINGLE)
1658 ) {
1659 // Emit bottom tee.
1660 writer.write("\u2534");
1661 }
1662 if ((rowI == rows.size() - 1)
1663 && (cellI < columns.size() - 1)
1664 && (row.bottomBorder == Border.DOUBLE)
1665 && (columns.get(cellI).rightBorder == Border.SINGLE)
1666 ) {
1667 // Emit bottom tee (double).
1668 writer.write("\u2567");
1669 }
1670 if ((rowI == rows.size() - 1)
1671 && (cellI < columns.size() - 1)
1672 && (row.bottomBorder == Border.THICK)
1673 && (columns.get(cellI).rightBorder == Border.SINGLE)
1674 ) {
1675 // Emit bottom tee (thick).
1676 writer.write("\u2537");
1677 }
1678
1679 cellI++;
1680 }
1681
1682 writer.write("\n");
1683 rowI++;
1684 }
1685 } finally {
1686 if (writer != null) {
1687 writer.close();
1688 }
1689 }
1690 }
1691
1692 /**
1693 * Set the selected cell location.
1694 *
1695 * @param column the selected cell location column
1696 * @param row the selected cell location row
1697 */
1698 public void setSelectedCell(final int column, final int row) {
1699 if ((column < 0) || (column > columns.size() - 1)) {
1700 throw new IndexOutOfBoundsException("Column count is " +
1701 columns.size() + ", requested index " + column);
1702 }
1703 if ((row < 0) || (row > rows.size() - 1)) {
1704 throw new IndexOutOfBoundsException("Row count is " +
1705 rows.size() + ", requested index " + row);
1706 }
1707 selectedColumn = column;
1708 selectedRow = row;
1709 alignGrid();
1710 }
1711
1712 /**
1713 * Get a particular cell.
1714 *
1715 * @param column the cell column
1716 * @param row the cell row
1717 * @return the cell
1718 */
1719 public Cell getCell(final int column, final int row) {
1720 if ((column < 0) || (column > columns.size() - 1)) {
1721 throw new IndexOutOfBoundsException("Column count is " +
1722 columns.size() + ", requested index " + column);
1723 }
1724 if ((row < 0) || (row > rows.size() - 1)) {
1725 throw new IndexOutOfBoundsException("Row count is " +
1726 rows.size() + ", requested index " + row);
1727 }
1728 return rows.get(row).get(column);
1729 }
1730
1731 /**
1732 * Get the text of a particular cell.
1733 *
1734 * @param column the cell column
1735 * @param row the cell row
1736 * @return the text in the cell
1737 */
1738 public String getCellText(final int column, final int row) {
1739 if ((column < 0) || (column > columns.size() - 1)) {
1740 throw new IndexOutOfBoundsException("Column count is " +
1741 columns.size() + ", requested index " + column);
1742 }
1743 if ((row < 0) || (row > rows.size() - 1)) {
1744 throw new IndexOutOfBoundsException("Row count is " +
1745 rows.size() + ", requested index " + row);
1746 }
1747 return rows.get(row).get(column).getText();
1748 }
1749
1750 /**
1751 * Set the text of a particular cell.
1752 *
1753 * @param column the cell column
1754 * @param row the cell row
1755 * @param text the text to put into the cell
1756 */
1757 public void setCellText(final int column, final int row,
1758 final String text) {
1759
1760 if ((column < 0) || (column > columns.size() - 1)) {
1761 throw new IndexOutOfBoundsException("Column count is " +
1762 columns.size() + ", requested index " + column);
1763 }
1764 if ((row < 0) || (row > rows.size() - 1)) {
1765 throw new IndexOutOfBoundsException("Row count is " +
1766 rows.size() + ", requested index " + row);
1767 }
1768 rows.get(row).get(column).setText(text);
1769 }
1770
1771 /**
1772 * Set the action to perform when the user presses enter on a particular
1773 * cell.
1774 *
1775 * @param column the cell column
1776 * @param row the cell row
1777 * @param action the action to perform when the user presses enter on the
1778 * cell
1779 */
1780 public void setCellEnterAction(final int column, final int row,
1781 final TAction action) {
1782
1783 if ((column < 0) || (column > columns.size() - 1)) {
1784 throw new IndexOutOfBoundsException("Column count is " +
1785 columns.size() + ", requested index " + column);
1786 }
1787 if ((row < 0) || (row > rows.size() - 1)) {
1788 throw new IndexOutOfBoundsException("Row count is " +
1789 rows.size() + ", requested index " + row);
1790 }
1791 rows.get(row).get(column).field.setEnterAction(action);
1792 }
1793
1794 /**
1795 * Set the action to perform when the user updates a particular cell.
1796 *
1797 * @param column the cell column
1798 * @param row the cell row
1799 * @param action the action to perform when the user updates the cell
1800 */
1801 public void setCellUpdateAction(final int column, final int row,
1802 final TAction action) {
1803
1804 if ((column < 0) || (column > columns.size() - 1)) {
1805 throw new IndexOutOfBoundsException("Column count is " +
1806 columns.size() + ", requested index " + column);
1807 }
1808 if ((row < 0) || (row > rows.size() - 1)) {
1809 throw new IndexOutOfBoundsException("Row count is " +
1810 rows.size() + ", requested index " + row);
1811 }
1812 rows.get(row).get(column).field.setUpdateAction(action);
1813 }
1814
1815 /**
1816 * Get the width of a column.
1817 *
1818 * @param column the column number
1819 * @return the width of the column
1820 */
1821 public int getColumnWidth(final int column) {
1822 if ((column < 0) || (column > columns.size() - 1)) {
1823 throw new IndexOutOfBoundsException("Column count is " +
1824 columns.size() + ", requested index " + column);
1825 }
1826 return columns.get(column).width;
1827 }
1828
1829 /**
1830 * Set the width of a column.
1831 *
1832 * @param column the column number
1833 * @param width the new width of the column
1834 */
1835 public void setColumnWidth(final int column, final int width) {
1836 if ((column < 0) || (column > columns.size() - 1)) {
1837 throw new IndexOutOfBoundsException("Column count is " +
1838 columns.size() + ", requested index " + column);
1839 }
1840
1841 if (width < 4) {
1842 // Columns may not be smaller than 4 cells wide.
1843 return;
1844 }
1845
1846 int delta = width - columns.get(column).width;
1847 columns.get(column).width = width;
1848 for (Cell cell: columns.get(column).cells) {
1849 cell.setWidth(columns.get(column).width);
1850 cell.field.setWidth(columns.get(column).width);
1851 }
1852 for (int i = column + 1; i < columns.size(); i++) {
1853 columns.get(i).setX(columns.get(i).getX() + delta);
1854 }
1855 if (column == columns.size() - 1) {
1856 bottomRightCorner();
1857 } else {
1858 alignGrid();
1859 }
1860 }
1861
1862 /**
1863 * Get the label of a column.
1864 *
1865 * @param column the column number
1866 * @return the label of the column
1867 */
1868 public String getColumnLabel(final int column) {
1869 if ((column < 0) || (column > columns.size() - 1)) {
1870 throw new IndexOutOfBoundsException("Column count is " +
1871 columns.size() + ", requested index " + column);
1872 }
1873 return columns.get(column).label;
1874 }
1875
1876 /**
1877 * Set the label of a column.
1878 *
1879 * @param column the column number
1880 * @param label the new label of the column
1881 */
1882 public void setColumnLabel(final int column, final String label) {
1883 if ((column < 0) || (column > columns.size() - 1)) {
1884 throw new IndexOutOfBoundsException("Column count is " +
1885 columns.size() + ", requested index " + column);
1886 }
1887 columns.get(column).label = label;
1888 }
1889
1890 /**
1891 * Get the label of a row.
1892 *
1893 * @param row the row number
1894 * @return the label of the row
1895 */
1896 public String getRowLabel(final int row) {
1897 if ((row < 0) || (row > rows.size() - 1)) {
1898 throw new IndexOutOfBoundsException("Row count is " +
1899 rows.size() + ", requested index " + row);
1900 }
1901 return rows.get(row).label;
1902 }
1903
1904 /**
1905 * Set the label of a row.
1906 *
1907 * @param row the row number
1908 * @param label the new label of the row
1909 */
1910 public void setRowLabel(final int row, final String label) {
1911 if ((row < 0) || (row > rows.size() - 1)) {
1912 throw new IndexOutOfBoundsException("Row count is " +
1913 rows.size() + ", requested index " + row);
1914 }
1915 rows.get(row).label = label;
1916 }
1917
1918 /**
1919 * Insert one row at a particular index.
1920 *
1921 * @param idx the row number
1922 */
1923 private void insertRowAt(final int idx) {
1924 Row newRow = new Row(idx);
1925 for (int i = 0; i < columns.size(); i++) {
1926 Cell cell = new Cell(this, columns.get(i).getX(),
1927 rows.get(idx).getY(), COLUMN_DEFAULT_WIDTH, 1, i, idx);
1928 newRow.add(cell);
1929 columns.get(i).cells.add(idx, cell);
1930 }
1931 rows.add(idx, newRow);
1932
1933 for (int x = 0; x < columns.size(); x++) {
1934 for (int y = idx; y < rows.size(); y++) {
1935 columns.get(x).get(y).row = y;
1936 columns.get(x).get(y).column = x;
1937 }
1938 }
1939 for (int i = idx + 1; i < rows.size(); i++) {
1940 String oldRowLabel = Integer.toString(i - 1);
1941 if (rows.get(i).label.equals(oldRowLabel)) {
1942 rows.get(i).label = Integer.toString(i);
1943 }
1944 }
1945 alignGrid();
1946 }
1947
1948 /**
1949 * Insert one row above a particular row.
1950 *
1951 * @param row the row number
1952 */
1953 public void insertRowAbove(final int row) {
1954 if ((row < 0) || (row > rows.size() - 1)) {
1955 throw new IndexOutOfBoundsException("Row count is " +
1956 rows.size() + ", requested index " + row);
1957 }
1958 insertRowAt(row);
1959 selectedRow++;
1960 activate(columns.get(selectedColumn).get(selectedRow));
1961 }
1962
1963 /**
1964 * Insert one row below a particular row.
1965 *
1966 * @param row the row number
1967 */
1968 public void insertRowBelow(final int row) {
1969 if ((row < 0) || (row > rows.size() - 1)) {
1970 throw new IndexOutOfBoundsException("Row count is " +
1971 rows.size() + ", requested index " + row);
1972 }
1973 int idx = row + 1;
1974 if (idx < rows.size()) {
1975 insertRowAt(idx);
1976 activate(columns.get(selectedColumn).get(selectedRow));
1977 return;
1978 }
1979
1980 // row is the last row, we need to perform an append.
1981 Row newRow = new Row(idx);
1982 for (int i = 0; i < columns.size(); i++) {
1983 Cell cell = new Cell(this, columns.get(i).getX(),
1984 rows.get(row).getY(), COLUMN_DEFAULT_WIDTH, 1, i, idx);
1985 newRow.add(cell);
1986 columns.get(i).cells.add(cell);
1987 }
1988 rows.add(newRow);
1989 alignGrid();
1990 activate(columns.get(selectedColumn).get(selectedRow));
1991 }
1992
1993 /**
1994 * Delete a particular row.
1995 *
1996 * @param row the row number
1997 */
1998 public void deleteRow(final int row) {
1999 if ((row < 0) || (row > rows.size() - 1)) {
2000 throw new IndexOutOfBoundsException("Row count is " +
2001 rows.size() + ", requested index " + row);
2002 }
2003 if (rows.size() == 1) {
2004 // Don't delete the last row.
2005 return;
2006 }
2007 for (int i = 0; i < columns.size(); i++) {
2008 Cell cell = columns.get(i).cells.remove(row);
2009 getChildren().remove(cell);
2010 }
2011 rows.remove(row);
2012
2013 for (int x = 0; x < columns.size(); x++) {
2014 for (int y = row; y < rows.size(); y++) {
2015 columns.get(x).get(y).row = y;
2016 columns.get(x).get(y).column = x;
2017 }
2018 }
2019 for (int i = row; i < rows.size(); i++) {
2020 String oldRowLabel = Integer.toString(i + 1);
2021 if (rows.get(i).label.equals(oldRowLabel)) {
2022 rows.get(i).label = Integer.toString(i);
2023 }
2024 }
2025 if (selectedRow == rows.size()) {
2026 selectedRow--;
2027 }
2028 activate(columns.get(selectedColumn).get(selectedRow));
2029 bottomRightCorner();
2030 }
2031
2032 /**
2033 * Insert one column at a particular index.
2034 *
2035 * @param idx the column number
2036 */
2037 private void insertColumnAt(final int idx) {
2038 Column newColumn = new Column(idx);
2039 for (int i = 0; i < rows.size(); i++) {
2040 Cell cell = new Cell(this, columns.get(idx).getX(),
2041 rows.get(i).getY(), COLUMN_DEFAULT_WIDTH, 1, idx, i);
2042 newColumn.add(cell);
2043 rows.get(i).cells.add(idx, cell);
2044 }
2045 columns.add(idx, newColumn);
2046
2047 for (int x = idx; x < columns.size(); x++) {
2048 for (int y = 0; y < rows.size(); y++) {
2049 columns.get(x).get(y).row = y;
2050 columns.get(x).get(y).column = x;
2051 }
2052 }
2053 for (int i = idx + 1; i < columns.size(); i++) {
2054 String oldColumnLabel = makeColumnLabel(i - 1);
2055 if (columns.get(i).label.equals(oldColumnLabel)) {
2056 columns.get(i).label = makeColumnLabel(i);
2057 }
2058 }
2059 alignGrid();
2060 }
2061
2062 /**
2063 * Insert one column to the left of a particular column.
2064 *
2065 * @param column the column number
2066 */
2067 public void insertColumnLeft(final int column) {
2068 if ((column < 0) || (column > columns.size() - 1)) {
2069 throw new IndexOutOfBoundsException("Column count is " +
2070 columns.size() + ", requested index " + column);
2071 }
2072 insertColumnAt(column);
2073 selectedColumn++;
2074 activate(columns.get(selectedColumn).get(selectedRow));
2075 }
2076
2077 /**
2078 * Insert one column to the right of a particular column.
2079 *
2080 * @param column the column number
2081 */
2082 public void insertColumnRight(final int column) {
2083 if ((column < 0) || (column > columns.size() - 1)) {
2084 throw new IndexOutOfBoundsException("Column count is " +
2085 columns.size() + ", requested index " + column);
2086 }
2087 int idx = column + 1;
2088 if (idx < columns.size()) {
2089 insertColumnAt(idx);
2090 activate(columns.get(selectedColumn).get(selectedRow));
2091 return;
2092 }
2093
2094 // column is the last column, we need to perform an append.
2095 Column newColumn = new Column(idx);
2096 for (int i = 0; i < rows.size(); i++) {
2097 Cell cell = new Cell(this, columns.get(column).getX(),
2098 rows.get(i).getY(), COLUMN_DEFAULT_WIDTH, 1, idx, i);
2099 newColumn.add(cell);
2100 rows.get(i).cells.add(cell);
2101 }
2102 columns.add(newColumn);
2103 alignGrid();
2104 activate(columns.get(selectedColumn).get(selectedRow));
2105 }
2106
2107 /**
2108 * Delete a particular column.
2109 *
2110 * @param column the column number
2111 */
2112 public void deleteColumn(final int column) {
2113 if ((column < 0) || (column > columns.size() - 1)) {
2114 throw new IndexOutOfBoundsException("Column count is " +
2115 columns.size() + ", requested index " + column);
2116 }
2117 if (columns.size() == 1) {
2118 // Don't delete the last column.
2119 return;
2120 }
2121 for (int i = 0; i < rows.size(); i++) {
2122 Cell cell = rows.get(i).cells.remove(column);
2123 getChildren().remove(cell);
2124 }
2125 columns.remove(column);
2126
2127 for (int x = column; x < columns.size(); x++) {
2128 for (int y = 0; y < rows.size(); y++) {
2129 columns.get(x).get(y).row = y;
2130 columns.get(x).get(y).column = x;
2131 }
2132 }
2133 for (int i = column; i < columns.size(); i++) {
2134 String oldColumnLabel = makeColumnLabel(i + 1);
2135 if (columns.get(i).label.equals(oldColumnLabel)) {
2136 columns.get(i).label = makeColumnLabel(i);
2137 }
2138 }
2139 if (selectedColumn == columns.size()) {
2140 selectedColumn--;
2141 }
2142 activate(columns.get(selectedColumn).get(selectedRow));
2143 bottomRightCorner();
2144 }
2145
2146 /**
2147 * Delete the selected cell, shifting cells over to the left.
2148 */
2149 public void deleteCellShiftLeft() {
2150 // All we do is copy the text from every cell in this row over.
2151 for (int i = selectedColumn + 1; i < columns.size(); i++) {
2152 setCellText(i - 1, selectedRow, getCellText(i, selectedRow));
2153 }
2154 setCellText(columns.size() - 1, selectedRow, "");
2155 }
2156
2157 /**
2158 * Delete the selected cell, shifting cells from below up.
2159 */
2160 public void deleteCellShiftUp() {
2161 // All we do is copy the text from every cell in this column up.
2162 for (int i = selectedRow + 1; i < rows.size(); i++) {
2163 setCellText(selectedColumn, i - 1, getCellText(selectedColumn, i));
2164 }
2165 setCellText(selectedColumn, rows.size() - 1, "");
2166 }
2167
2168 /**
2169 * Set a particular cell read-only (non-editable) or not.
2170 *
2171 * @param column the cell column
2172 * @param row the cell row
2173 * @param readOnly if true, the cell will be non-editable
2174 */
2175 public void setCellReadOnly(final int column, final int row,
2176 final boolean readOnly) {
2177
2178 if ((column < 0) || (column > columns.size() - 1)) {
2179 throw new IndexOutOfBoundsException("Column count is " +
2180 columns.size() + ", requested index " + column);
2181 }
2182 if ((row < 0) || (row > rows.size() - 1)) {
2183 throw new IndexOutOfBoundsException("Row count is " +
2184 rows.size() + ", requested index " + row);
2185 }
2186 rows.get(row).get(column).setReadOnly(readOnly);
2187 }
2188
2189 /**
2190 * Set an entire row of cells read-only (non-editable) or not.
2191 *
2192 * @param row the row number
2193 * @param readOnly if true, the cells will be non-editable
2194 */
2195 public void setRowReadOnly(final int row, final boolean readOnly) {
2196 if ((row < 0) || (row > rows.size() - 1)) {
2197 throw new IndexOutOfBoundsException("Row count is " +
2198 rows.size() + ", requested index " + row);
2199 }
2200 for (Cell cell: rows.get(row).cells) {
2201 cell.setReadOnly(readOnly);
2202 }
2203 }
2204
2205 /**
2206 * Set an entire column of cells read-only (non-editable) or not.
2207 *
2208 * @param column the column number
2209 * @param readOnly if true, the cells will be non-editable
2210 */
2211 public void setColumnReadOnly(final int column, final boolean readOnly) {
2212 if ((column < 0) || (column > columns.size() - 1)) {
2213 throw new IndexOutOfBoundsException("Column count is " +
2214 columns.size() + ", requested index " + column);
2215 }
2216 for (Cell cell: columns.get(column).cells) {
2217 cell.setReadOnly(readOnly);
2218 }
2219 }
2220
2221 /**
2222 * Set all borders across the entire table to Border.NONE.
2223 */
2224 public void setBorderAllNone() {
2225 topBorder = Border.NONE;
2226 leftBorder = Border.NONE;
2227 for (int i = 0; i < columns.size(); i++) {
2228 columns.get(i).rightBorder = Border.NONE;
2229 }
2230 for (int i = 0; i < rows.size(); i++) {
2231 rows.get(i).bottomBorder = Border.NONE;
2232 rows.get(i).height = 1;
2233 }
2234 bottomRightCorner();
2235 }
2236
2237 /**
2238 * Set all borders across the entire table to Border.SINGLE.
2239 */
2240 public void setBorderAllSingle() {
2241 topBorder = Border.SINGLE;
2242 leftBorder = Border.SINGLE;
2243 for (int i = 0; i < columns.size(); i++) {
2244 columns.get(i).rightBorder = Border.SINGLE;
2245 }
2246 for (int i = 0; i < rows.size(); i++) {
2247 rows.get(i).bottomBorder = Border.SINGLE;
2248 rows.get(i).height = 2;
2249 }
2250 alignGrid();
2251 }
2252
2253 /**
2254 * Set all borders around the selected cell to Border.NONE.
2255 */
2256 public void setBorderCellNone() {
2257 if (selectedRow == 0) {
2258 topBorder = Border.NONE;
2259 }
2260 if (selectedColumn == 0) {
2261 leftBorder = Border.NONE;
2262 }
2263 if (selectedColumn > 0) {
2264 columns.get(selectedColumn - 1).rightBorder = Border.NONE;
2265 }
2266 columns.get(selectedColumn).rightBorder = Border.NONE;
2267 if (selectedRow > 0) {
2268 rows.get(selectedRow - 1).bottomBorder = Border.NONE;
2269 rows.get(selectedRow - 1).height = 1;
2270 }
2271 rows.get(selectedRow).bottomBorder = Border.NONE;
2272 rows.get(selectedRow).height = 1;
2273 bottomRightCorner();
2274 }
2275
2276 /**
2277 * Set all borders around the selected cell to Border.SINGLE.
2278 */
2279 public void setBorderCellSingle() {
2280 if (selectedRow == 0) {
2281 topBorder = Border.SINGLE;
2282 }
2283 if (selectedColumn == 0) {
2284 leftBorder = Border.SINGLE;
2285 }
2286 if (selectedColumn > 0) {
2287 columns.get(selectedColumn - 1).rightBorder = Border.SINGLE;
2288 }
2289 columns.get(selectedColumn).rightBorder = Border.SINGLE;
2290 if (selectedRow > 0) {
2291 rows.get(selectedRow - 1).bottomBorder = Border.SINGLE;
2292 rows.get(selectedRow - 1).height = 2;
2293 }
2294 rows.get(selectedRow).bottomBorder = Border.SINGLE;
2295 rows.get(selectedRow).height = 2;
2296 alignGrid();
2297 }
2298
2299 /**
2300 * Set the column border to the right of the selected cell to
2301 * Border.SINGLE.
2302 */
2303 public void setBorderColumnRightSingle() {
2304 columns.get(selectedColumn).rightBorder = Border.SINGLE;
2305 alignGrid();
2306 }
2307
2308 /**
2309 * Set the column border to the right of the selected cell to
2310 * Border.SINGLE.
2311 */
2312 public void setBorderColumnLeftSingle() {
2313 if (selectedColumn == 0) {
2314 leftBorder = Border.SINGLE;
2315 } else {
2316 columns.get(selectedColumn - 1).rightBorder = Border.SINGLE;
2317 }
2318 alignGrid();
2319 }
2320
2321 /**
2322 * Set the row border above the selected cell to Border.SINGLE.
2323 */
2324 public void setBorderRowAboveSingle() {
2325 if (selectedRow == 0) {
2326 topBorder = Border.SINGLE;
2327 } else {
2328 rows.get(selectedRow - 1).bottomBorder = Border.SINGLE;
2329 rows.get(selectedRow - 1).height = 2;
2330 }
2331 alignGrid();
2332 }
2333
2334 /**
2335 * Set the row border below the selected cell to Border.SINGLE.
2336 */
2337 public void setBorderRowBelowSingle() {
2338 rows.get(selectedRow).bottomBorder = Border.SINGLE;
2339 rows.get(selectedRow).height = 2;
2340 alignGrid();
2341 }
2342
2343 /**
2344 * Set the row border below the selected cell to Border.DOUBLE.
2345 */
2346 public void setBorderRowBelowDouble() {
2347 rows.get(selectedRow).bottomBorder = Border.DOUBLE;
2348 rows.get(selectedRow).height = 2;
2349 alignGrid();
2350 }
2351
2352 /**
2353 * Set the row border below the selected cell to Border.THICK.
2354 */
2355 public void setBorderRowBelowThick() {
2356 rows.get(selectedRow).bottomBorder = Border.THICK;
2357 rows.get(selectedRow).height = 2;
2358 alignGrid();
2359 }
2360
2361 }