refactored ttable widget to not use menu directly
[fanfix.git] / src / jexer / TTableWidget.java
1 /*
2 * Jexer - Java Text User Interface
3 *
4 * The MIT License (MIT)
5 *
6 * Copyright (C) 2019 Kevin Lamonte
7 *
8 * Permission is hereby granted, free of charge, to any person obtaining a
9 * copy of this software and associated documentation files (the "Software"),
10 * to deal in the Software without restriction, including without limitation
11 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
12 * and/or sell copies of the Software, and to permit persons to whom the
13 * Software is furnished to do so, subject to the following conditions:
14 *
15 * The above copyright notice and this permission notice shall be included in
16 * all copies or substantial portions of the Software.
17 *
18 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
21 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
24 * DEALINGS IN THE SOFTWARE.
25 *
26 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
27 * @version 1
28 */
29 package jexer;
30
31 import java.io.IOException;
32 import java.util.ArrayList;
33 import java.util.List;
34 import java.util.ResourceBundle;
35
36 import jexer.bits.CellAttributes;
37 import jexer.event.TKeypressEvent;
38 import jexer.event.TMouseEvent;
39 import jexer.event.TResizeEvent;
40 import static jexer.TKeypress.*;
41
42 /**
43 * TTableWidget is used to display and edit regular two-dimensional tables of
44 * cells.
45 *
46 * This class was inspired by a TTable implementation originally developed by
47 * David "Niki" ROULET [niki@nikiroo.be], made available under MIT at
48 * https://github.com/nikiroo/jexer/tree/ttable_pull.
49 */
50 public class TTableWidget extends TWidget {
51
52 /**
53 * Translated strings.
54 */
55 private static final ResourceBundle i18n = ResourceBundle.getBundle(TTableWidget.class.getName());
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 ? 10 * (8 + 1) : 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 */
618 public TTableWidget(final TWidget parent, final int x, final int y,
619 final int width, final int height) {
620
621 super(parent, x, y, width, height);
622
623 // Initialize the starting row and column.
624 rows.add(new Row(0));
625 columns.add(new Column(0));
626
627 // Place a grid of cells that fit in this space.
628 int row = 0;
629 for (int i = 0; i < height + EXTRA_ROWS; i += rows.get(0).height) {
630 int column = 0;
631 for (int j = 0; j < width + EXTRA_COLUMNS;
632 j += columns.get(0).width) {
633
634 Cell cell = new Cell(this, 0, 0, columns.get(0).width,
635 rows.get(0).height, column, row);
636
637 if (DEBUG) {
638 // For debugging: set a grid of cell index labels.
639 cell.setText("" + row + " " + column);
640 }
641 rows.get(row).add(cell);
642 columns.get(column).add(cell);
643 if ((i == 0) &&
644 (j + columns.get(0).width < width + EXTRA_COLUMNS)
645 ) {
646 columns.add(new Column(column + 1));
647 }
648 column++;
649 }
650 if (i + rows.get(0).height < height + EXTRA_ROWS) {
651 rows.add(new Row(row + 1));
652 }
653 row++;
654 }
655 for (int i = 0; i < rows.size(); i++) {
656 rows.get(i).setY(i + (showColumnLabels ? COLUMN_LABEL_HEIGHT : 0));
657 }
658 for (int j = 0; j < columns.size(); j++) {
659 columns.get(j).setX((j * COLUMN_DEFAULT_WIDTH) +
660 (showRowLabels ? ROW_LABEL_WIDTH : 0));
661 }
662 activate(columns.get(selectedColumn).get(selectedRow));
663
664 alignGrid();
665 }
666
667 // ------------------------------------------------------------------------
668 // Event handlers ---------------------------------------------------------
669 // ------------------------------------------------------------------------
670
671 /**
672 * Handle mouse press events.
673 *
674 * @param mouse mouse button press event
675 */
676 @Override
677 public void onMouseDown(final TMouseEvent mouse) {
678 if (mouse.isMouseWheelUp() || mouse.isMouseWheelDown()) {
679 // Treat wheel up/down as 3 up/down
680 TKeypressEvent keyEvent;
681 if (mouse.isMouseWheelUp()) {
682 keyEvent = new TKeypressEvent(kbUp);
683 } else {
684 keyEvent = new TKeypressEvent(kbDown);
685 }
686 for (int i = 0; i < 3; i++) {
687 onKeypress(keyEvent);
688 }
689 return;
690 }
691
692 // Use TWidget's code to pass the event to the children.
693 super.onMouseDown(mouse);
694 }
695
696 /**
697 * Handle keystrokes.
698 *
699 * @param keypress keystroke event
700 */
701 @Override
702 public void onKeypress(final TKeypressEvent keypress) {
703 if (keypress.equals(kbTab)
704 || keypress.equals(kbShiftTab)
705 ) {
706 // Squash tab and back-tab. They don't make sense in the TTable
707 // grid context.
708 return;
709 }
710
711 // If editing, pass to that cell and do nothing else.
712 if (getSelectedCell().isEditing) {
713 super.onKeypress(keypress);
714 return;
715 }
716
717 if (keypress.equals(kbLeft)) {
718 // Left
719 if (selectedColumn > 0) {
720 selectedColumn--;
721 }
722 activate(columns.get(selectedColumn).get(selectedRow));
723 } else if (keypress.equals(kbRight)) {
724 // Right
725 if (selectedColumn < columns.size() - 1) {
726 selectedColumn++;
727 }
728 activate(columns.get(selectedColumn).get(selectedRow));
729 } else if (keypress.equals(kbUp)) {
730 // Up
731 if (selectedRow > 0) {
732 selectedRow--;
733 }
734 activate(columns.get(selectedColumn).get(selectedRow));
735 } else if (keypress.equals(kbDown)) {
736 // Down
737 if (selectedRow < rows.size() - 1) {
738 selectedRow++;
739 }
740 activate(columns.get(selectedColumn).get(selectedRow));
741 } else if (keypress.equals(kbHome)) {
742 // Home - leftmost column
743 selectedColumn = 0;
744 activate(columns.get(selectedColumn).get(selectedRow));
745 } else if (keypress.equals(kbEnd)) {
746 // End - rightmost column
747 selectedColumn = columns.size() - 1;
748 activate(columns.get(selectedColumn).get(selectedRow));
749 } else if (keypress.equals(kbPgUp)) {
750 // PgUp - Treat like multiple up
751 for (int i = 0; i < getHeight() - 2; i++) {
752 if (selectedRow > 0) {
753 selectedRow--;
754 }
755 }
756 activate(columns.get(selectedColumn).get(selectedRow));
757 } else if (keypress.equals(kbPgDn)) {
758 // PgDn - Treat like multiple up
759 for (int i = 0; i < getHeight() - 2; i++) {
760 if (selectedRow < rows.size() - 1) {
761 selectedRow++;
762 }
763 }
764 activate(columns.get(selectedColumn).get(selectedRow));
765 } else if (keypress.equals(kbCtrlHome)) {
766 // Ctrl-Home - go to top-left
767 selectedRow = 0;
768 selectedColumn = 0;
769 activate(columns.get(selectedColumn).get(selectedRow));
770 activate(columns.get(selectedColumn).get(selectedRow));
771 } else if (keypress.equals(kbCtrlEnd)) {
772 // Ctrl-End - go to bottom-right
773 selectedRow = rows.size() - 1;
774 selectedColumn = columns.size() - 1;
775 activate(columns.get(selectedColumn).get(selectedRow));
776 activate(columns.get(selectedColumn).get(selectedRow));
777 } else {
778 // Pass to the Cell.
779 super.onKeypress(keypress);
780 }
781
782 // We may have scrolled off screen. Reset positions as needed to
783 // make the newly selected cell visible.
784 alignGrid();
785 }
786
787 /**
788 * Handle widget resize events.
789 *
790 * @param event resize event
791 */
792 @Override
793 public void onResize(final TResizeEvent event) {
794 super.onResize(event);
795
796 bottomRightCorner();
797 }
798
799 // ------------------------------------------------------------------------
800 // TWidget ----------------------------------------------------------------
801 // ------------------------------------------------------------------------
802
803 /**
804 * Draw the table row/column labels, and borders.
805 */
806 @Override
807 public void draw() {
808 CellAttributes labelColor = getTheme().getColor("ttable.label");
809 CellAttributes labelColorSelected = getTheme().getColor("ttable.label.selected");
810 CellAttributes borderColor = getTheme().getColor("ttable.border");
811
812 // Column labels.
813 if (showColumnLabels == true) {
814 for (int i = left; i < columns.size(); i++) {
815 if (columns.get(i).get(top).isVisible() == false) {
816 break;
817 }
818 putStringXY(columns.get(i).get(top).getX(), 0,
819 String.format(" %-" +
820 (columns.get(i).width - 2)
821 + "s ", columns.get(i).label),
822 (i == selectedColumn ? labelColorSelected : labelColor));
823 }
824 }
825
826 // Row labels.
827 if (showRowLabels == true) {
828 for (int i = top; i < rows.size(); i++) {
829 if (rows.get(i).get(left).isVisible() == false) {
830 break;
831 }
832 putStringXY(0, rows.get(i).get(left).getY(),
833 String.format(" %-6s ", rows.get(i).label),
834 (i == selectedRow ? labelColorSelected : labelColor));
835 }
836 }
837
838 // Draw vertical borders.
839 if (leftBorder == Border.SINGLE) {
840 vLineXY((showRowLabels ? ROW_LABEL_WIDTH : 0),
841 (topBorder == Border.NONE ? 0 : 1) +
842 (showColumnLabels ? COLUMN_LABEL_HEIGHT : 0),
843 getHeight(), '\u2502', borderColor);
844 }
845 for (int i = left; i < columns.size(); i++) {
846 if (columns.get(i).get(top).isVisible() == false) {
847 break;
848 }
849 if (columns.get(i).rightBorder == Border.SINGLE) {
850 vLineXY(columns.get(i).getX() + columns.get(i).width,
851 (topBorder == Border.NONE ? 0 : 1) +
852 (showColumnLabels ? COLUMN_LABEL_HEIGHT : 0),
853 getHeight(), '\u2502', borderColor);
854 }
855 }
856
857 // Draw horizontal borders.
858 if (topBorder == Border.SINGLE) {
859 hLineXY((showRowLabels ? ROW_LABEL_WIDTH : 0),
860 (showColumnLabels ? COLUMN_LABEL_HEIGHT : 0),
861 getWidth(), '\u2500', borderColor);
862 }
863 for (int i = top; i < rows.size(); i++) {
864 if (rows.get(i).get(left).isVisible() == false) {
865 break;
866 }
867 if (rows.get(i).bottomBorder == Border.SINGLE) {
868 hLineXY((leftBorder == Border.NONE ? 0 : 1) +
869 (showRowLabels ? ROW_LABEL_WIDTH : 0),
870 rows.get(i).getY() + rows.get(i).height - 1,
871 getWidth(), '\u2500', borderColor);
872 } else if (rows.get(i).bottomBorder == Border.DOUBLE) {
873 hLineXY((leftBorder == Border.NONE ? 0 : 1) +
874 (showRowLabels ? ROW_LABEL_WIDTH : 0),
875 rows.get(i).getY() + rows.get(i).height - 1,
876 getWidth(), '\u2550', borderColor);
877 } else if (rows.get(i).bottomBorder == Border.THICK) {
878 hLineXY((leftBorder == Border.NONE ? 0 : 1) +
879 (showRowLabels ? ROW_LABEL_WIDTH : 0),
880 rows.get(i).getY() + rows.get(i).height - 1,
881 getWidth(), '\u2501', borderColor);
882 }
883 }
884 // Top-left corner if needed
885 if ((topBorder == Border.SINGLE) && (leftBorder == Border.SINGLE)) {
886 putCharXY((showRowLabels ? ROW_LABEL_WIDTH : 0),
887 (showColumnLabels ? COLUMN_LABEL_HEIGHT : 0),
888 '\u250c', borderColor);
889 }
890
891 // Now draw the correct corners
892 for (int i = top; i < rows.size(); i++) {
893 if (rows.get(i).get(left).isVisible() == false) {
894 break;
895 }
896 for (int j = left; j < columns.size(); j++) {
897 if (columns.get(j).get(i).isVisible() == false) {
898 break;
899 }
900 if ((i == top) && (topBorder == Border.SINGLE)
901 && (columns.get(j).rightBorder == Border.SINGLE)
902 ) {
903 // Top tee
904 putCharXY(columns.get(j).getX() + columns.get(j).width,
905 (showColumnLabels ? COLUMN_LABEL_HEIGHT : 0),
906 '\u252c', borderColor);
907 }
908 if ((j == left) && (leftBorder == Border.SINGLE)
909 && (rows.get(i).bottomBorder == Border.SINGLE)
910 ) {
911 // Left tee
912 putCharXY((showRowLabels ? ROW_LABEL_WIDTH : 0),
913 rows.get(i).getY() + rows.get(i).height - 1,
914 '\u251c', borderColor);
915 }
916 if ((columns.get(j).rightBorder == Border.SINGLE)
917 && (rows.get(i).bottomBorder == Border.SINGLE)
918 ) {
919 // Intersection of single bars
920 putCharXY(columns.get(j).getX() + columns.get(j).width,
921 rows.get(i).getY() + rows.get(i).height - 1,
922 '\u253c', borderColor);
923 }
924 if ((j == left) && (leftBorder == Border.SINGLE)
925 && (rows.get(i).bottomBorder == Border.DOUBLE)
926 ) {
927 // Left tee: single bar vertical, double bar horizontal
928 putCharXY((showRowLabels ? ROW_LABEL_WIDTH : 0),
929 rows.get(i).getY() + rows.get(i).height - 1,
930 '\u255e', borderColor);
931 }
932 if ((j == left) && (leftBorder == Border.SINGLE)
933 && (rows.get(i).bottomBorder == Border.THICK)
934 ) {
935 // Left tee: single bar vertical, thick bar horizontal
936 putCharXY((showRowLabels ? ROW_LABEL_WIDTH : 0),
937 rows.get(i).getY() + rows.get(i).height - 1,
938 '\u251d', borderColor);
939 }
940 if ((columns.get(j).rightBorder == Border.SINGLE)
941 && (rows.get(i).bottomBorder == Border.DOUBLE)
942 ) {
943 // Intersection: single bar vertical, double bar
944 // horizontal
945 putCharXY(columns.get(j).getX() + columns.get(j).width,
946 rows.get(i).getY() + rows.get(i).height - 1,
947 '\u256a', borderColor);
948 }
949 if ((columns.get(j).rightBorder == Border.SINGLE)
950 && (rows.get(i).bottomBorder == Border.THICK)
951 ) {
952 // Intersection: single bar vertical, thick bar
953 // horizontal
954 putCharXY(columns.get(j).getX() + columns.get(j).width,
955 rows.get(i).getY() + rows.get(i).height - 1,
956 '\u253f', borderColor);
957 }
958 }
959 }
960
961 // Now draw the window borders.
962 super.draw();
963 }
964
965 // ------------------------------------------------------------------------
966 // TTable -----------------------------------------------------------------
967 // ------------------------------------------------------------------------
968
969 /**
970 * Generate the default letter name for a column number.
971 *
972 * @param col column number to use for this column. Column 0 will be
973 * "A", column 1 will be "B", column 26 will be "AA", and so on.
974 */
975 private String makeColumnLabel(int col) {
976 StringBuilder sb = new StringBuilder();
977 for (;;) {
978 sb.append((char) ('A' + (col % 26)));
979 if (col < 26) {
980 break;
981 }
982 col /= 26;
983 }
984 return sb.reverse().toString();
985 }
986
987 /**
988 * Get the currently-selected cell.
989 *
990 * @return the selected cell
991 */
992 public Cell getSelectedCell() {
993 assert (rows.get(selectedRow) != null);
994 assert (rows.get(selectedRow).get(selectedColumn) != null);
995 assert (columns.get(selectedColumn) != null);
996 assert (columns.get(selectedColumn).get(selectedRow) != null);
997 assert (rows.get(selectedRow).get(selectedColumn) ==
998 columns.get(selectedColumn).get(selectedRow));
999
1000 return (columns.get(selectedColumn).get(selectedRow));
1001 }
1002
1003 /**
1004 * Get the currently-selected column.
1005 *
1006 * @return the selected column
1007 */
1008 public Column getSelectedColumn() {
1009 assert (selectedColumn >= 0);
1010 assert (columns.size() > selectedColumn);
1011 assert (columns.get(selectedColumn) != null);
1012 return columns.get(selectedColumn);
1013 }
1014
1015 /**
1016 * Get the currently-selected row.
1017 *
1018 * @return the selected row
1019 */
1020 public Row getSelectedRow() {
1021 assert (selectedRow >= 0);
1022 assert (rows.size() > selectedRow);
1023 assert (rows.get(selectedRow) != null);
1024 return rows.get(selectedRow);
1025 }
1026
1027 /**
1028 * Get the currently-selected column number. 0 is the left-most column.
1029 *
1030 * @return the selected column number
1031 */
1032 public int getSelectedColumnNumber() {
1033 return selectedColumn;
1034 }
1035
1036 /**
1037 * Set the currently-selected column number. 0 is the left-most column.
1038 *
1039 * @param column the column number to select
1040 */
1041 public void setSelectedColumnNumber(final int column) {
1042 if ((column < 0) || (column > columns.size() - 1)) {
1043 throw new IndexOutOfBoundsException("Column count is " +
1044 columns.size() + ", requested index " + column);
1045 }
1046 selectedColumn = column;
1047 activate(columns.get(selectedColumn).get(selectedRow));
1048 alignGrid();
1049 }
1050
1051 /**
1052 * Get the currently-selected row number. 0 is the top-most row.
1053 *
1054 * @return the selected row number
1055 */
1056 public int getSelectedRowNumber() {
1057 return selectedRow;
1058 }
1059
1060 /**
1061 * Set the currently-selected row number. 0 is the left-most column.
1062 *
1063 * @param row the row number to select
1064 */
1065 public void setSelectedRowNumber(final int row) {
1066 if ((row < 0) || (row > rows.size() - 1)) {
1067 throw new IndexOutOfBoundsException("Row count is " +
1068 rows.size() + ", requested index " + row);
1069 }
1070 selectedRow = row;
1071 activate(columns.get(selectedColumn).get(selectedRow));
1072 alignGrid();
1073 }
1074
1075 /**
1076 * Get the highlight row flag.
1077 *
1078 * @return true if the selected row is highlighted
1079 */
1080 public boolean getHighlightRow() {
1081 return highlightRow;
1082 }
1083
1084 /**
1085 * Set the highlight row flag.
1086 *
1087 * @param highlightRow if true, the selected row will be highlighted
1088 */
1089 public void setHighlightRow(final boolean highlightRow) {
1090 this.highlightRow = highlightRow;
1091 }
1092
1093 /**
1094 * Get the highlight column flag.
1095 *
1096 * @return true if the selected column is highlighted
1097 */
1098 public boolean getHighlightColumn() {
1099 return highlightColumn;
1100 }
1101
1102 /**
1103 * Set the highlight column flag.
1104 *
1105 * @param highlightColumn if true, the selected column will be highlighted
1106 */
1107 public void setHighlightColumn(final boolean highlightColumn) {
1108 this.highlightColumn = highlightColumn;
1109 }
1110
1111 /**
1112 * Get the show row labels flag.
1113 *
1114 * @return true if row labels are shown
1115 */
1116 public boolean getShowRowLabels() {
1117 return showRowLabels;
1118 }
1119
1120 /**
1121 * Set the show row labels flag.
1122 *
1123 * @param showRowLabels if true, the row labels will be shown
1124 */
1125 public void setShowRowLabels(final boolean showRowLabels) {
1126 this.showRowLabels = showRowLabels;
1127 }
1128
1129 /**
1130 * Get the show column labels flag.
1131 *
1132 * @return true if column labels are shown
1133 */
1134 public boolean getShowColumnLabels() {
1135 return showColumnLabels;
1136 }
1137
1138 /**
1139 * Set the show column labels flag.
1140 *
1141 * @param showColumnLabels if true, the column labels will be shown
1142 */
1143 public void setShowColumnLabels(final boolean showColumnLabels) {
1144 this.showColumnLabels = showColumnLabels;
1145 }
1146
1147 /**
1148 * Get the number of columns.
1149 *
1150 * @return the number of columns
1151 */
1152 public int getColumnCount() {
1153 return columns.size();
1154 }
1155
1156 /**
1157 * Get the number of rows.
1158 *
1159 * @return the number of rows
1160 */
1161 public int getRowCount() {
1162 return rows.size();
1163 }
1164
1165
1166 /**
1167 * Push top and left to the bottom-most right corner of the available
1168 * grid.
1169 */
1170 private void bottomRightCorner() {
1171 int viewColumns = getWidth();
1172 if (showRowLabels == true) {
1173 viewColumns -= ROW_LABEL_WIDTH;
1174 }
1175
1176 // Set left and top such that the table stays on screen if possible.
1177 top = rows.size() - getHeight();
1178 left = columns.size() - (getWidth() / (viewColumns / (COLUMN_DEFAULT_WIDTH + 1)));
1179 // Now ensure the selection is visible.
1180 alignGrid();
1181 }
1182
1183 /**
1184 * Align the grid so that the selected cell is fully visible.
1185 */
1186 private void alignGrid() {
1187 int viewColumns = getWidth();
1188 if (showRowLabels == true) {
1189 viewColumns -= ROW_LABEL_WIDTH;
1190 }
1191 if (leftBorder != Border.NONE) {
1192 viewColumns--;
1193 }
1194 int viewRows = getHeight();
1195 if (showColumnLabels == true) {
1196 viewRows -= COLUMN_LABEL_HEIGHT;
1197 }
1198 if (topBorder != Border.NONE) {
1199 viewRows--;
1200 }
1201
1202 // If we pushed left or right, adjust the box to include the new
1203 // selected cell.
1204 if (selectedColumn < left) {
1205 left = selectedColumn - 1;
1206 }
1207 if (left < 0) {
1208 left = 0;
1209 }
1210 if (selectedRow < top) {
1211 top = selectedRow - 1;
1212 }
1213 if (top < 0) {
1214 top = 0;
1215 }
1216
1217 /*
1218 * viewColumns and viewRows now contain the available columns and
1219 * rows available to view the selected cell. We adjust left and top
1220 * to ensure the selected cell is within view, and then make all
1221 * cells outside the box between (left, top) and (right, bottom)
1222 * invisible.
1223 *
1224 * We need to calculate right and bottom now.
1225 */
1226 int right = left;
1227
1228 boolean done = false;
1229 while (!done) {
1230 int rightCellX = (showRowLabels ? ROW_LABEL_WIDTH : 0);
1231 if (leftBorder != Border.NONE) {
1232 rightCellX++;
1233 }
1234 int maxCellX = rightCellX + viewColumns;
1235 right = left;
1236 boolean selectedIsVisible = false;
1237 int selectedX = 0;
1238 for (int x = left; x < columns.size(); x++) {
1239 if (x == selectedColumn) {
1240 selectedX = rightCellX;
1241 if (selectedX + columns.get(x).width + 1 <= maxCellX) {
1242 selectedIsVisible = true;
1243 }
1244 }
1245 rightCellX += columns.get(x).width + 1;
1246 if (rightCellX >= maxCellX) {
1247 break;
1248 }
1249 right++;
1250 }
1251 if (right < selectedColumn) {
1252 // selectedColumn is outside the view range. Push left over,
1253 // and calculate again.
1254 left++;
1255 } else if (left == selectedColumn) {
1256 // selectedColumn doesn't fit inside the view range, but we
1257 // can't go over any further either. Bail out.
1258 done = true;
1259 } else if (selectedIsVisible == false) {
1260 // selectedColumn doesn't fit inside the view range, continue
1261 // on.
1262 left++;
1263 } else {
1264 // selectedColumn is fully visible, all done.
1265 assert (selectedIsVisible == true);
1266 done = true;
1267 }
1268
1269 } // while (!done)
1270
1271 // We have the left/right range correct, set cell visibility and
1272 // column X positions.
1273 int leftCellX = showRowLabels ? ROW_LABEL_WIDTH : 0;
1274 if (leftBorder != Border.NONE) {
1275 leftCellX++;
1276 }
1277 for (int x = 0; x < columns.size(); x++) {
1278 if ((x < left) || (x > right)) {
1279 for (int i = 0; i < rows.size(); i++) {
1280 columns.get(x).get(i).setVisible(false);
1281 columns.get(x).setX(getWidth() + 1);
1282 }
1283 continue;
1284 }
1285 for (int i = 0; i < rows.size(); i++) {
1286 columns.get(x).get(i).setVisible(true);
1287 }
1288 columns.get(x).setX(leftCellX);
1289 leftCellX += columns.get(x).width + 1;
1290 }
1291
1292 int bottom = top;
1293
1294 done = false;
1295 while (!done) {
1296 int bottomCellY = (showColumnLabels ? COLUMN_LABEL_HEIGHT : 0);
1297 if (topBorder != Border.NONE) {
1298 bottomCellY++;
1299 }
1300 int maxCellY = bottomCellY + viewRows;
1301 bottom = top;
1302 for (int y = top; y < rows.size(); y++) {
1303 bottomCellY += rows.get(y).height;
1304 if (bottomCellY >= maxCellY) {
1305 break;
1306 }
1307 bottom++;
1308 }
1309 if (bottom < selectedRow) {
1310 // selectedRow is outside the view range. Push top down, and
1311 // calculate again.
1312 top++;
1313 } else {
1314 // selectedRow is inside the view range, done.
1315 done = true;
1316 }
1317 } // while (!done)
1318
1319 // We have the top/bottom range correct, set cell visibility and
1320 // row Y positions.
1321 int topCellY = showColumnLabels ? COLUMN_LABEL_HEIGHT : 0;
1322 if (topBorder != Border.NONE) {
1323 topCellY++;
1324 }
1325 for (int y = 0; y < rows.size(); y++) {
1326 if ((y < top) || (y > bottom)) {
1327 for (int i = 0; i < columns.size(); i++) {
1328 rows.get(y).get(i).setVisible(false);
1329 }
1330 rows.get(y).setY(getHeight() + 1);
1331 continue;
1332 }
1333 for (int i = 0; i < columns.size(); i++) {
1334 rows.get(y).get(i).setVisible(true);
1335 }
1336 rows.get(y).setY(topCellY);
1337 topCellY += rows.get(y).height;
1338 }
1339
1340 // Last thing: cancel any edits that are not the selected cell.
1341 for (int y = 0; y < rows.size(); y++) {
1342 for (int x = 0; x < columns.size(); x++) {
1343 if ((x == selectedColumn) && (y == selectedRow)) {
1344 continue;
1345 }
1346 rows.get(y).get(x).cancelEdit();
1347 }
1348 }
1349 }
1350
1351 /**
1352 * Save contents to file in CSV format.
1353 *
1354 * @param filename file to save to
1355 * @throws IOException if a java.io operation throws
1356 */
1357 public void saveToCsvFilename(final String filename) throws IOException {
1358 // TODO
1359 }
1360
1361 /**
1362 * Save contents to file in text format with lines.
1363 *
1364 * @param filename file to save to
1365 * @throws IOException if a java.io operation throws
1366 */
1367 public void saveToTextFilename(final String filename) throws IOException {
1368 // TODO
1369 }
1370
1371 /**
1372 * Set the selected cell location.
1373 *
1374 * @param column the selected cell location column
1375 * @param row the selected cell location row
1376 */
1377 public void setSelectedCell(final int column, final int row) {
1378 if ((column < 0) || (column > columns.size() - 1)) {
1379 throw new IndexOutOfBoundsException("Column count is " +
1380 columns.size() + ", requested index " + column);
1381 }
1382 if ((row < 0) || (row > rows.size() - 1)) {
1383 throw new IndexOutOfBoundsException("Row count is " +
1384 rows.size() + ", requested index " + row);
1385 }
1386 selectedColumn = column;
1387 selectedRow = row;
1388 alignGrid();
1389 }
1390
1391 /**
1392 * Get a particular cell.
1393 *
1394 * @param column the cell column
1395 * @param row the cell row
1396 * @return the cell
1397 */
1398 public Cell getCell(final int column, final int row) {
1399 if ((column < 0) || (column > columns.size() - 1)) {
1400 throw new IndexOutOfBoundsException("Column count is " +
1401 columns.size() + ", requested index " + column);
1402 }
1403 if ((row < 0) || (row > rows.size() - 1)) {
1404 throw new IndexOutOfBoundsException("Row count is " +
1405 rows.size() + ", requested index " + row);
1406 }
1407 return rows.get(row).get(column);
1408 }
1409
1410 /**
1411 * Get the text of a particular cell.
1412 *
1413 * @param column the cell column
1414 * @param row the cell row
1415 * @return the text in the cell
1416 */
1417 public String getCellText(final int column, final int row) {
1418 if ((column < 0) || (column > columns.size() - 1)) {
1419 throw new IndexOutOfBoundsException("Column count is " +
1420 columns.size() + ", requested index " + column);
1421 }
1422 if ((row < 0) || (row > rows.size() - 1)) {
1423 throw new IndexOutOfBoundsException("Row count is " +
1424 rows.size() + ", requested index " + row);
1425 }
1426 return rows.get(row).get(column).getText();
1427 }
1428
1429 /**
1430 * Set the text of a particular cell.
1431 *
1432 * @param column the cell column
1433 * @param row the cell row
1434 * @param text the text to put into the cell
1435 */
1436 public void setCellText(final int column, final int row,
1437 final String text) {
1438
1439 if ((column < 0) || (column > columns.size() - 1)) {
1440 throw new IndexOutOfBoundsException("Column count is " +
1441 columns.size() + ", requested index " + column);
1442 }
1443 if ((row < 0) || (row > rows.size() - 1)) {
1444 throw new IndexOutOfBoundsException("Row count is " +
1445 rows.size() + ", requested index " + row);
1446 }
1447 rows.get(row).get(column).setText(text);
1448 }
1449
1450 /**
1451 * Set the action to perform when the user presses enter on a particular
1452 * cell.
1453 *
1454 * @param column the cell column
1455 * @param row the cell row
1456 * @param action the action to perform when the user presses enter on the
1457 * cell
1458 */
1459 public void setCellEnterAction(final int column, final int row,
1460 final TAction action) {
1461
1462 if ((column < 0) || (column > columns.size() - 1)) {
1463 throw new IndexOutOfBoundsException("Column count is " +
1464 columns.size() + ", requested index " + column);
1465 }
1466 if ((row < 0) || (row > rows.size() - 1)) {
1467 throw new IndexOutOfBoundsException("Row count is " +
1468 rows.size() + ", requested index " + row);
1469 }
1470 rows.get(row).get(column).field.setEnterAction(action);
1471 }
1472
1473 /**
1474 * Set the action to perform when the user updates a particular cell.
1475 *
1476 * @param column the cell column
1477 * @param row the cell row
1478 * @param action the action to perform when the user updates the cell
1479 */
1480 public void setCellUpdateAction(final int column, final int row,
1481 final TAction action) {
1482
1483 if ((column < 0) || (column > columns.size() - 1)) {
1484 throw new IndexOutOfBoundsException("Column count is " +
1485 columns.size() + ", requested index " + column);
1486 }
1487 if ((row < 0) || (row > rows.size() - 1)) {
1488 throw new IndexOutOfBoundsException("Row count is " +
1489 rows.size() + ", requested index " + row);
1490 }
1491 rows.get(row).get(column).field.setUpdateAction(action);
1492 }
1493
1494 /**
1495 * Get the width of a column.
1496 *
1497 * @param column the column number
1498 * @return the width of the column
1499 */
1500 public int getColumnWidth(final int column) {
1501 if ((column < 0) || (column > columns.size() - 1)) {
1502 throw new IndexOutOfBoundsException("Column count is " +
1503 columns.size() + ", requested index " + column);
1504 }
1505 return columns.get(column).width;
1506 }
1507
1508 /**
1509 * Set the width of a column.
1510 *
1511 * @param column the column number
1512 * @param width the new width of the column
1513 */
1514 public void setColumnWidth(final int column, final int width) {
1515 if ((column < 0) || (column > columns.size() - 1)) {
1516 throw new IndexOutOfBoundsException("Column count is " +
1517 columns.size() + ", requested index " + column);
1518 }
1519
1520 if (width < 4) {
1521 // Columns may not be smaller than 4 cells wide.
1522 return;
1523 }
1524
1525 int delta = width - columns.get(column).width;
1526 columns.get(column).width = width;
1527 for (Cell cell: columns.get(column).cells) {
1528 cell.setWidth(columns.get(column).width);
1529 cell.field.setWidth(columns.get(column).width);
1530 }
1531 for (int i = column + 1; i < columns.size(); i++) {
1532 columns.get(i).setX(columns.get(i).getX() + delta);
1533 }
1534 if (column == columns.size() - 1) {
1535 bottomRightCorner();
1536 } else {
1537 alignGrid();
1538 }
1539 }
1540
1541 /**
1542 * Get the label of a column.
1543 *
1544 * @param column the column number
1545 * @return the label of the column
1546 */
1547 public String getColumnLabel(final int column) {
1548 if ((column < 0) || (column > columns.size() - 1)) {
1549 throw new IndexOutOfBoundsException("Column count is " +
1550 columns.size() + ", requested index " + column);
1551 }
1552 return columns.get(column).label;
1553 }
1554
1555 /**
1556 * Set the label of a column.
1557 *
1558 * @param column the column number
1559 * @param label the new label of the column
1560 */
1561 public void setColumnLabel(final int column, final String label) {
1562 if ((column < 0) || (column > columns.size() - 1)) {
1563 throw new IndexOutOfBoundsException("Column count is " +
1564 columns.size() + ", requested index " + column);
1565 }
1566 columns.get(column).label = label;
1567 }
1568
1569 /**
1570 * Get the label of a row.
1571 *
1572 * @param row the row number
1573 * @return the label of the row
1574 */
1575 public String getRowLabel(final int row) {
1576 if ((row < 0) || (row > rows.size() - 1)) {
1577 throw new IndexOutOfBoundsException("Row count is " +
1578 rows.size() + ", requested index " + row);
1579 }
1580 return rows.get(row).label;
1581 }
1582
1583 /**
1584 * Set the label of a row.
1585 *
1586 * @param row the row number
1587 * @param label the new label of the row
1588 */
1589 public void setRowLabel(final int row, final String label) {
1590 if ((row < 0) || (row > rows.size() - 1)) {
1591 throw new IndexOutOfBoundsException("Row count is " +
1592 rows.size() + ", requested index " + row);
1593 }
1594 rows.get(row).label = label;
1595 }
1596
1597 /**
1598 * Insert one row at a particular index.
1599 *
1600 * @param idx the row number
1601 */
1602 private void insertRowAt(final int idx) {
1603 Row newRow = new Row(idx);
1604 for (int i = 0; i < columns.size(); i++) {
1605 Cell cell = new Cell(this, columns.get(i).getX(),
1606 rows.get(selectedRow).getY(), COLUMN_DEFAULT_WIDTH, 1, i, idx);
1607 newRow.add(cell);
1608 columns.get(i).cells.add(idx, cell);
1609 }
1610 rows.add(idx, newRow);
1611
1612 for (int x = 0; x < columns.size(); x++) {
1613 for (int y = idx; y < rows.size(); y++) {
1614 columns.get(x).get(y).row = y;
1615 columns.get(x).get(y).column = x;
1616 }
1617 }
1618 for (int i = idx + 1; i < rows.size(); i++) {
1619 String oldRowLabel = Integer.toString(i - 1);
1620 if (rows.get(i).label.equals(oldRowLabel)) {
1621 rows.get(i).label = Integer.toString(i);
1622 }
1623 }
1624 alignGrid();
1625 }
1626
1627 /**
1628 * Insert one row above a particular row.
1629 *
1630 * @param row the row number
1631 */
1632 public void insertRowAbove(final int row) {
1633 if ((row < 0) || (row > rows.size() - 1)) {
1634 throw new IndexOutOfBoundsException("Row count is " +
1635 rows.size() + ", requested index " + row);
1636 }
1637 insertRowAt(selectedRow);
1638 selectedRow++;
1639 activate(columns.get(selectedColumn).get(selectedRow));
1640 }
1641
1642 /**
1643 * Insert one row below a particular row.
1644 *
1645 * @param row the row number
1646 */
1647 public void insertRowBelow(final int row) {
1648 if ((row < 0) || (row > rows.size() - 1)) {
1649 throw new IndexOutOfBoundsException("Row count is " +
1650 rows.size() + ", requested index " + row);
1651 }
1652 int idx = selectedRow + 1;
1653 if (idx < rows.size()) {
1654 insertRowAt(idx);
1655 activate(columns.get(selectedColumn).get(selectedRow));
1656 return;
1657 }
1658
1659 // selectedRow is the last row, we need to perform an append.
1660 Row newRow = new Row(idx);
1661 for (int i = 0; i < columns.size(); i++) {
1662 Cell cell = new Cell(this, columns.get(i).getX(),
1663 rows.get(selectedRow).getY(), COLUMN_DEFAULT_WIDTH, 1, i, idx);
1664 newRow.add(cell);
1665 columns.get(i).cells.add(cell);
1666 }
1667 rows.add(newRow);
1668 alignGrid();
1669 activate(columns.get(selectedColumn).get(selectedRow));
1670 }
1671
1672 /**
1673 * Delete a particular row.
1674 *
1675 * @param row the row number
1676 */
1677 public void deleteRow(final int row) {
1678 if ((row < 0) || (row > rows.size() - 1)) {
1679 throw new IndexOutOfBoundsException("Row count is " +
1680 rows.size() + ", requested index " + row);
1681 }
1682 if (rows.size() == 1) {
1683 // Don't delete the last row.
1684 return;
1685 }
1686 for (int i = 0; i < columns.size(); i++) {
1687 Cell cell = columns.get(i).cells.remove(row);
1688 getChildren().remove(cell);
1689 }
1690 rows.remove(row);
1691
1692 for (int x = 0; x < columns.size(); x++) {
1693 for (int y = row; y < rows.size(); y++) {
1694 columns.get(x).get(y).row = y;
1695 columns.get(x).get(y).column = x;
1696 }
1697 }
1698 for (int i = row; i < rows.size(); i++) {
1699 String oldRowLabel = Integer.toString(i + 1);
1700 if (rows.get(i).label.equals(oldRowLabel)) {
1701 rows.get(i).label = Integer.toString(i);
1702 }
1703 }
1704 if (selectedRow == rows.size()) {
1705 selectedRow--;
1706 }
1707 activate(columns.get(selectedColumn).get(selectedRow));
1708 bottomRightCorner();
1709 }
1710
1711 /**
1712 * Insert one column at a particular index.
1713 *
1714 * @param idx the column number
1715 */
1716 private void insertColumnAt(final int idx) {
1717 Column newColumn = new Column(idx);
1718 for (int i = 0; i < rows.size(); i++) {
1719 Cell cell = new Cell(this, columns.get(selectedColumn).getX(),
1720 rows.get(i).getY(), COLUMN_DEFAULT_WIDTH, 1, idx, i);
1721 newColumn.add(cell);
1722 rows.get(i).cells.add(idx, cell);
1723 }
1724 columns.add(idx, newColumn);
1725
1726 for (int x = idx; x < columns.size(); x++) {
1727 for (int y = 0; y < rows.size(); y++) {
1728 columns.get(x).get(y).row = y;
1729 columns.get(x).get(y).column = x;
1730 }
1731 }
1732 for (int i = idx + 1; i < columns.size(); i++) {
1733 String oldColumnLabel = makeColumnLabel(i - 1);
1734 if (columns.get(i).label.equals(oldColumnLabel)) {
1735 columns.get(i).label = makeColumnLabel(i);
1736 }
1737 }
1738 alignGrid();
1739 }
1740
1741 /**
1742 * Insert one column to the left of a particular column.
1743 *
1744 * @param column the column number
1745 */
1746 public void insertColumnLeft(final int column) {
1747 if ((column < 0) || (column > columns.size() - 1)) {
1748 throw new IndexOutOfBoundsException("Column count is " +
1749 columns.size() + ", requested index " + column);
1750 }
1751 insertColumnAt(selectedColumn);
1752 selectedColumn++;
1753 activate(columns.get(selectedColumn).get(selectedRow));
1754 }
1755
1756 /**
1757 * Insert one column to the right of a particular column.
1758 *
1759 * @param column the column number
1760 */
1761 public void insertColumnRight(final int column) {
1762 if ((column < 0) || (column > columns.size() - 1)) {
1763 throw new IndexOutOfBoundsException("Column count is " +
1764 columns.size() + ", requested index " + column);
1765 }
1766 int idx = selectedColumn + 1;
1767 if (idx < columns.size()) {
1768 insertColumnAt(idx);
1769 activate(columns.get(selectedColumn).get(selectedRow));
1770 return;
1771 }
1772
1773 // selectedColumn is the last column, we need to perform an append.
1774 Column newColumn = new Column(idx);
1775 for (int i = 0; i < rows.size(); i++) {
1776 Cell cell = new Cell(this, columns.get(selectedColumn).getX(),
1777 rows.get(i).getY(), COLUMN_DEFAULT_WIDTH, 1, idx, i);
1778 newColumn.add(cell);
1779 rows.get(i).cells.add(cell);
1780 }
1781 columns.add(newColumn);
1782 alignGrid();
1783 activate(columns.get(selectedColumn).get(selectedRow));
1784 }
1785
1786 /**
1787 * Delete a particular column.
1788 *
1789 * @param column the column number
1790 */
1791 public void deleteColumn(final int column) {
1792 if ((column < 0) || (column > columns.size() - 1)) {
1793 throw new IndexOutOfBoundsException("Column count is " +
1794 columns.size() + ", requested index " + column);
1795 }
1796 if (columns.size() == 1) {
1797 // Don't delete the last column.
1798 return;
1799 }
1800 for (int i = 0; i < rows.size(); i++) {
1801 Cell cell = rows.get(i).cells.remove(column);
1802 getChildren().remove(cell);
1803 }
1804 columns.remove(column);
1805
1806 for (int x = column; x < columns.size(); x++) {
1807 for (int y = 0; y < rows.size(); y++) {
1808 columns.get(x).get(y).row = y;
1809 columns.get(x).get(y).column = x;
1810 }
1811 }
1812 for (int i = column; i < columns.size(); i++) {
1813 String oldColumnLabel = makeColumnLabel(i + 1);
1814 if (columns.get(i).label.equals(oldColumnLabel)) {
1815 columns.get(i).label = makeColumnLabel(i);
1816 }
1817 }
1818 if (selectedColumn == columns.size()) {
1819 selectedColumn--;
1820 }
1821 activate(columns.get(selectedColumn).get(selectedRow));
1822 bottomRightCorner();
1823 }
1824
1825 /**
1826 * Delete the selected cell, shifting cells over to the left.
1827 */
1828 public void deleteCellShiftLeft() {
1829 // All we do is copy the text from every cell in this row over.
1830 for (int i = selectedColumn + 1; i < columns.size(); i++) {
1831 setCellText(i - 1, selectedRow, getCellText(i, selectedRow));
1832 }
1833 setCellText(columns.size() - 1, selectedRow, "");
1834 }
1835
1836 /**
1837 * Delete the selected cell, shifting cells from below up.
1838 */
1839 public void deleteCellShiftUp() {
1840 // All we do is copy the text from every cell in this column up.
1841 for (int i = selectedRow + 1; i < rows.size(); i++) {
1842 setCellText(selectedColumn, i - 1, getCellText(selectedColumn, i));
1843 }
1844 setCellText(selectedColumn, rows.size() - 1, "");
1845 }
1846
1847 /**
1848 * Set a particular cell read-only (non-editable) or not.
1849 *
1850 * @param column the cell column
1851 * @param row the cell row
1852 * @param readOnly if true, the cell will be non-editable
1853 */
1854 public void setCellReadOnly(final int column, final int row,
1855 final boolean readOnly) {
1856
1857 if ((column < 0) || (column > columns.size() - 1)) {
1858 throw new IndexOutOfBoundsException("Column count is " +
1859 columns.size() + ", requested index " + column);
1860 }
1861 if ((row < 0) || (row > rows.size() - 1)) {
1862 throw new IndexOutOfBoundsException("Row count is " +
1863 rows.size() + ", requested index " + row);
1864 }
1865 rows.get(row).get(column).setReadOnly(readOnly);
1866 }
1867
1868 /**
1869 * Set an entire row of cells read-only (non-editable) or not.
1870 *
1871 * @param row the row number
1872 * @param readOnly if true, the cells will be non-editable
1873 */
1874 public void setRowReadOnly(final int row, final boolean readOnly) {
1875 if ((row < 0) || (row > rows.size() - 1)) {
1876 throw new IndexOutOfBoundsException("Row count is " +
1877 rows.size() + ", requested index " + row);
1878 }
1879 for (Cell cell: rows.get(row).cells) {
1880 cell.setReadOnly(readOnly);
1881 }
1882 }
1883
1884 /**
1885 * Set an entire column of cells read-only (non-editable) or not.
1886 *
1887 * @param column the column number
1888 * @param readOnly if true, the cells will be non-editable
1889 */
1890 public void setColumnReadOnly(final int column, final boolean readOnly) {
1891 if ((column < 0) || (column > columns.size() - 1)) {
1892 throw new IndexOutOfBoundsException("Column count is " +
1893 columns.size() + ", requested index " + column);
1894 }
1895 for (Cell cell: columns.get(column).cells) {
1896 cell.setReadOnly(readOnly);
1897 }
1898 }
1899
1900 /**
1901 * Set all borders across the entire table to Border.NONE.
1902 */
1903 public void setBorderAllNone() {
1904 topBorder = Border.NONE;
1905 leftBorder = Border.NONE;
1906 for (int i = 0; i < columns.size(); i++) {
1907 columns.get(i).rightBorder = Border.NONE;
1908 }
1909 for (int i = 0; i < rows.size(); i++) {
1910 rows.get(i).bottomBorder = Border.NONE;
1911 rows.get(i).height = 1;
1912 }
1913 bottomRightCorner();
1914 }
1915
1916 /**
1917 * Set all borders across the entire table to Border.SINGLE.
1918 */
1919 public void setBorderAllSingle() {
1920 topBorder = Border.SINGLE;
1921 leftBorder = Border.SINGLE;
1922 for (int i = 0; i < columns.size(); i++) {
1923 columns.get(i).rightBorder = Border.SINGLE;
1924 }
1925 for (int i = 0; i < rows.size(); i++) {
1926 rows.get(i).bottomBorder = Border.SINGLE;
1927 rows.get(i).height = 2;
1928 }
1929 alignGrid();
1930 }
1931
1932 /**
1933 * Set all borders around the selected cell to Border.NONE.
1934 */
1935 public void setBorderCellNone() {
1936 if (selectedRow == 0) {
1937 topBorder = Border.NONE;
1938 }
1939 if (selectedColumn == 0) {
1940 leftBorder = Border.NONE;
1941 }
1942 columns.get(selectedColumn).rightBorder = Border.NONE;
1943 rows.get(selectedRow).bottomBorder = Border.NONE;
1944 rows.get(selectedRow).height = 1;
1945 bottomRightCorner();
1946 }
1947
1948 /**
1949 * Set all borders around the selected cell to Border.SINGLE.
1950 */
1951 public void setBorderCellSingle() {
1952 if (selectedRow == 0) {
1953 topBorder = Border.SINGLE;
1954 }
1955 if (selectedColumn == 0) {
1956 leftBorder = Border.SINGLE;
1957 }
1958 columns.get(selectedColumn).rightBorder = Border.SINGLE;
1959 rows.get(selectedRow).bottomBorder = Border.SINGLE;
1960 rows.get(selectedRow).height = 2;
1961 alignGrid();
1962 }
1963
1964 /**
1965 * Set the column border to the right of the selected cell to
1966 * Border.SINGLE.
1967 */
1968 public void setBorderColumnRightSingle() {
1969 columns.get(selectedColumn).rightBorder = Border.SINGLE;
1970 alignGrid();
1971 }
1972
1973 /**
1974 * Set the column border to the right of the selected cell to
1975 * Border.SINGLE.
1976 */
1977 public void setBorderColumnLeftSingle() {
1978 if (selectedColumn == 0) {
1979 leftBorder = Border.SINGLE;
1980 } else {
1981 columns.get(selectedColumn - 1).rightBorder = Border.SINGLE;
1982 }
1983 alignGrid();
1984 }
1985
1986 /**
1987 * Set the row border above the selected cell to Border.SINGLE.
1988 */
1989 public void setBorderRowAboveSingle() {
1990 if (selectedRow == 0) {
1991 topBorder = Border.SINGLE;
1992 } else {
1993 rows.get(selectedRow - 1).bottomBorder = Border.SINGLE;
1994 rows.get(selectedRow - 1).height = 2;
1995 }
1996 alignGrid();
1997 }
1998
1999 /**
2000 * Set the row border below the selected cell to Border.SINGLE.
2001 */
2002 public void setBorderRowBelowSingle() {
2003 rows.get(selectedRow).bottomBorder = Border.SINGLE;
2004 rows.get(selectedRow).height = 2;
2005 alignGrid();
2006 }
2007
2008 /**
2009 * Set the row border below the selected cell to Border.DOUBLE.
2010 */
2011 public void setBorderRowBelowDouble() {
2012 rows.get(selectedRow).bottomBorder = Border.DOUBLE;
2013 rows.get(selectedRow).height = 2;
2014 alignGrid();
2015 }
2016
2017 /**
2018 * Set the row border below the selected cell to Border.THICK.
2019 */
2020 public void setBorderRowBelowThick() {
2021 rows.get(selectedRow).bottomBorder = Border.THICK;
2022 rows.get(selectedRow).height = 2;
2023 alignGrid();
2024 }
2025
2026 }