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