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