hello world
[fanfix.git] / src / jexer / TTableWidget.java
CommitLineData
1dac6b8d
KL
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 */
29package jexer;
30
759cb83e 31import java.io.IOException;
1dac6b8d
KL
32import java.util.ArrayList;
33import java.util.List;
34
68ee64d0 35import jexer.bits.CellAttributes;
1dac6b8d
KL
36import jexer.event.TKeypressEvent;
37import jexer.event.TMenuEvent;
759cb83e 38import jexer.event.TMouseEvent;
2e1384cc 39import jexer.event.TResizeEvent;
1dac6b8d
KL
40import jexer.menu.TMenu;
41import 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 */
51public 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 // Variables --------------------------------------------------------------
85 // ------------------------------------------------------------------------
86
87 /**
88 * The underlying data, organized as columns.
89 */
90 private ArrayList<Column> columns = new ArrayList<Column>();
91
92 /**
93 * The underlying data, organized as rows.
94 */
95 private ArrayList<Row> rows = new ArrayList<Row>();
96
97 /**
98 * The row in model corresponding to the top-left visible cell.
99 */
100 private int top = 0;
101
102 /**
103 * The column in model corresponding to the top-left visible cell.
104 */
105 private int left = 0;
106
107 /**
108 * The row in model corresponding to the currently selected cell.
109 */
110 private int selectedRow = 0;
111
112 /**
113 * The column in model corresponding to the currently selected cell.
114 */
115 private int selectedColumn = 0;
116
117 /**
118 * If true, highlight the entire row of the currently-selected cell.
119 */
9c172016 120 private boolean highlightRow = true;
1dac6b8d
KL
121
122 /**
123 * If true, highlight the entire column of the currently-selected cell.
124 */
9c172016
KL
125 private boolean highlightColumn = true;
126
127 /**
128 * If true, show the row labels as the first column.
129 */
130 private boolean showRowLabels = true;
1dac6b8d 131
68ee64d0
KL
132 /**
133 * If true, show the column labels as the first row.
134 */
135 private boolean showColumnLabels = true;
136
1dac6b8d
KL
137 /**
138 * Column represents a column of cells.
139 */
140 public class Column {
141
142 /**
143 * Width of column.
144 */
145 private int width = 8;
146
147 /**
148 * The cells of this column.
149 */
150 private ArrayList<Cell> cells = new ArrayList<Cell>();
151
152 /**
153 * Column label.
154 */
68ee64d0 155 private String label = "";
1dac6b8d
KL
156
157 /**
158 * The border for this column.
159 */
160 private Border border = Border.NONE;
161
9c172016
KL
162 /**
163 * Constructor sets label to lettered column.
164 *
165 * @param col column number to use for this column. Column 0 will be
166 * "A", column 1 will be "B", column 26 will be "AA", and so on.
167 */
168 Column(int col) {
169 StringBuilder sb = new StringBuilder();
170 for (;;) {
171 sb.append((char) ('A' + (col % 26)));
172 if (col < 26) {
173 break;
174 }
175 col /= 26;
176 }
177 label = sb.reverse().toString();
178 }
179
1dac6b8d
KL
180 /**
181 * Add an entry to this column.
182 *
183 * @param cell the cell to add
184 */
185 public void add(final Cell cell) {
186 cells.add(cell);
187 }
188
189 /**
190 * Get an entry from this column.
191 *
192 * @param row the entry index to get
193 * @return the cell at row
194 */
195 public Cell get(final int row) {
196 return cells.get(row);
197 }
198 }
199
200 /**
201 * Row represents a row of cells.
202 */
203 public class Row {
204
205 /**
206 * Height of row.
207 */
208 private int height = 1;
209
210 /**
211 * The cells of this row.
212 */
213 private ArrayList<Cell> cells = new ArrayList<Cell>();
214
215 /**
216 * Row label.
217 */
218 private String label = "";
219
220 /**
221 * The border for this row.
222 */
223 private Border border = Border.NONE;
224
9c172016
KL
225 /**
226 * Constructor sets label to numbered row.
227 *
228 * @param row row number to use for this row
229 */
230 Row(final int row) {
231 label = Integer.toString(row);
232 }
233
1dac6b8d
KL
234 /**
235 * Add an entry to this column.
236 *
237 * @param cell the cell to add
238 */
239 public void add(final Cell cell) {
240 cells.add(cell);
241 }
242
243 /**
244 * Get an entry from this row.
245 *
246 * @param column the entry index to get
247 * @return the cell at column
248 */
249 public Cell get(final int column) {
250 return cells.get(column);
251 }
252
253 }
254
255 /**
256 * Cell represents an editable cell in the table. Normally, navigation
257 * to a cell only highlights it; pressing Enter or F2 will switch to
258 * editing mode.
259 */
260 public class Cell extends TWidget {
261
262 // --------------------------------------------------------------------
263 // Variables ----------------------------------------------------------
264 // --------------------------------------------------------------------
265
266 /**
267 * The field containing the cell's data.
268 */
269 private TField field;
270
271 /**
272 * The column of this cell.
273 */
274 private int column;
275
276 /**
277 * The row of this cell.
278 */
279 private int row;
280
281 /**
282 * If true, the cell is being edited.
283 */
284 private boolean isEditing = false;
285
286 /**
287 * Text of field before editing.
288 */
289 private String fieldText;
290
291 // --------------------------------------------------------------------
292 // Constructors -------------------------------------------------------
293 // --------------------------------------------------------------------
294
295 /**
296 * Public constructor.
297 *
298 * @param parent parent widget
299 * @param x column relative to parent
300 * @param y row relative to parent
301 * @param width width of widget
302 * @param height height of widget
303 * @param column column index of this cell
304 * @param row row index of this cell
305 */
306 public Cell(final TTableWidget parent, final int x, final int y,
307 final int width, final int height, final int column,
308 final int row) {
309
310 super(parent, x, y, width, height);
311 this.column = column;
312 this.row = row;
313
2e1384cc 314 field = addField(0, 0, width, false);
1dac6b8d 315 field.setEnabled(false);
1dac6b8d
KL
316 field.setBackgroundChar(' ');
317 }
318
319 // --------------------------------------------------------------------
320 // Event handlers -----------------------------------------------------
321 // --------------------------------------------------------------------
322
759cb83e
KL
323 /**
324 * Handle mouse double-click events.
325 *
326 * @param mouse mouse double-click event
327 */
328 @Override
329 public void onMouseDoubleClick(final TMouseEvent mouse) {
330 // Use TWidget's code to pass the event to the children.
331 super.onMouseDown(mouse);
332
333 // Double-click means to start editing.
334 fieldText = field.getText();
335 isEditing = true;
336 field.setEnabled(true);
337 activate(field);
338
339 if (isActive()) {
340 // Let the table know that I was activated.
341 ((TTableWidget) getParent()).selectedRow = row;
342 ((TTableWidget) getParent()).selectedColumn = column;
343 ((TTableWidget) getParent()).alignGrid();
344 }
345 }
346
347 /**
348 * Handle mouse press events.
349 *
350 * @param mouse mouse button press event
351 */
352 @Override
353 public void onMouseDown(final TMouseEvent mouse) {
354 // Use TWidget's code to pass the event to the children.
355 super.onMouseDown(mouse);
356
357 if (isActive()) {
358 // Let the table know that I was activated.
359 ((TTableWidget) getParent()).selectedRow = row;
360 ((TTableWidget) getParent()).selectedColumn = column;
361 ((TTableWidget) getParent()).alignGrid();
362 }
363 }
364
365 /**
366 * Handle mouse release events.
367 *
368 * @param mouse mouse button release event
369 */
370 @Override
371 public void onMouseUp(final TMouseEvent mouse) {
372 // Use TWidget's code to pass the event to the children.
373 super.onMouseDown(mouse);
374
375 if (isActive()) {
376 // Let the table know that I was activated.
377 ((TTableWidget) getParent()).selectedRow = row;
378 ((TTableWidget) getParent()).selectedColumn = column;
379 ((TTableWidget) getParent()).alignGrid();
380 }
381 }
382
1dac6b8d
KL
383 /**
384 * Handle keystrokes.
385 *
386 * @param keypress keystroke event
387 */
388 @Override
389 public void onKeypress(final TKeypressEvent keypress) {
390 // System.err.println("Cell onKeypress: " + keypress);
391
392 if (isEditing) {
393 if (keypress.equals(kbEsc)) {
394 // ESC cancels the edit.
395 field.setText(fieldText);
396 isEditing = false;
397 field.setEnabled(false);
398 return;
399 }
400 if (keypress.equals(kbEnter)) {
401 // Enter ends editing.
402 fieldText = field.getText();
403 isEditing = false;
404 field.setEnabled(false);
405 return;
406 }
407 // Pass down to field.
408 super.onKeypress(keypress);
409 }
410
411 if (keypress.equals(kbEnter) || keypress.equals(kbF2)) {
412 // Enter or F2 starts editing.
413 fieldText = field.getText();
414 isEditing = true;
415 field.setEnabled(true);
416 activate(field);
417 return;
418 }
419 }
420
421 // --------------------------------------------------------------------
422 // TWidget ------------------------------------------------------------
423 // --------------------------------------------------------------------
424
425 /**
426 * Draw this cell.
427 */
428 @Override
429 public void draw() {
430 TTableWidget table = (TTableWidget) getParent();
431
1dac6b8d 432 if (isAbsoluteActive()) {
9c172016
KL
433 if (isEditing) {
434 field.setActiveColorKey("tfield.active");
435 field.setInactiveColorKey("tfield.inactive");
436 } else {
437 field.setActiveColorKey("ttable.selected");
438 field.setInactiveColorKey("ttable.selected");
1dac6b8d 439 }
9c172016
KL
440 } else if (((table.selectedColumn == column)
441 && ((table.selectedRow == row)
442 || (table.highlightColumn == true)))
443 || ((table.selectedRow == row)
444 && ((table.selectedColumn == column)
445 || (table.highlightRow == true)))
446 ) {
447 field.setActiveColorKey("ttable.active");
448 field.setInactiveColorKey("ttable.active");
449 } else {
450 field.setActiveColorKey("ttable.active");
451 field.setInactiveColorKey("ttable.inactive");
1dac6b8d
KL
452 }
453
77961919
KL
454 assert (isVisible() == true);
455
1dac6b8d
KL
456 super.draw();
457 }
458
459 // --------------------------------------------------------------------
460 // TTable.Cell --------------------------------------------------------
461 // --------------------------------------------------------------------
462
463 /**
464 * Get field text.
465 *
466 * @return field text
467 */
468 public final String getText() {
469 return field.getText();
470 }
471
472 /**
473 * Set field text.
474 *
475 * @param text the new field text
476 */
477 public void setText(final String text) {
478 field.setText(text);
479 }
480
481 }
482
483 // ------------------------------------------------------------------------
484 // Constructors -----------------------------------------------------------
485 // ------------------------------------------------------------------------
486
487 /**
488 * Public constructor.
489 *
490 * @param parent parent widget
491 * @param x column relative to parent
492 * @param y row relative to parent
493 * @param width width of widget
494 * @param height height of widget
495 */
496 public TTableWidget(final TWidget parent, final int x, final int y,
497 final int width, final int height) {
498
499 super(parent, x, y, width, height);
500
501 // Initialize the starting row and column.
9c172016
KL
502 rows.add(new Row(0));
503 columns.add(new Column(0));
1dac6b8d
KL
504
505 // Place a grid of cells that fit in this space.
506 int row = 0;
507 for (int i = 0; i < height; i += rows.get(0).height) {
508 int column = 0;
509 for (int j = 0; j < width; j += columns.get(0).width) {
510 Cell cell = new Cell(this, j, i, columns.get(0).width,
511 rows.get(0).height, column, row);
512
513 cell.setText("" + row + " " + column);
514 rows.get(row).add(cell);
515 columns.get(column).add(cell);
516 if ((i == 0) && (j + columns.get(0).width < width)) {
9c172016 517 columns.add(new Column(column + 1));
1dac6b8d
KL
518 }
519 column++;
520 }
521 if (i + rows.get(0).height < height) {
9c172016 522 rows.add(new Row(row + 1));
1dac6b8d
KL
523 }
524 row++;
525 }
526 activate(columns.get(selectedColumn).get(selectedRow));
9c172016
KL
527
528 alignGrid();
77961919
KL
529
530 // Set the menu to match the flags.
531 getApplication().getMenuItem(TMenu.MID_TABLE_VIEW_ROW_LABELS).
532 setChecked(showRowLabels);
533 getApplication().getMenuItem(TMenu.MID_TABLE_VIEW_COLUMN_LABELS).
534 setChecked(showColumnLabels);
535 getApplication().getMenuItem(TMenu.MID_TABLE_VIEW_HIGHLIGHT_ROW).
536 setChecked(highlightRow);
537 getApplication().getMenuItem(TMenu.MID_TABLE_VIEW_HIGHLIGHT_COLUMN).
538 setChecked(highlightColumn);
539
540
1dac6b8d
KL
541 }
542
543 // ------------------------------------------------------------------------
544 // Event handlers ---------------------------------------------------------
545 // ------------------------------------------------------------------------
546
759cb83e
KL
547 /**
548 * Handle mouse press events.
549 *
550 * @param mouse mouse button press event
551 */
552 @Override
553 public void onMouseDown(final TMouseEvent mouse) {
554 if (mouse.isMouseWheelUp() || mouse.isMouseWheelDown()) {
555 // Treat wheel up/down as 3 up/down
556 TKeypressEvent keyEvent;
557 if (mouse.isMouseWheelUp()) {
558 keyEvent = new TKeypressEvent(kbUp);
559 } else {
560 keyEvent = new TKeypressEvent(kbDown);
561 }
562 for (int i = 0; i < 3; i++) {
563 onKeypress(keyEvent);
564 }
565 return;
566 }
567
568 // Use TWidget's code to pass the event to the children.
569 super.onMouseDown(mouse);
570 }
571
1dac6b8d
KL
572 /**
573 * Handle keystrokes.
574 *
575 * @param keypress keystroke event
576 */
577 @Override
578 public void onKeypress(final TKeypressEvent keypress) {
579 if (keypress.equals(kbTab)
580 || keypress.equals(kbShiftTab)
581 ) {
582 // Squash tab and back-tab. They don't make sense in the TTable
583 // grid context.
584 return;
585 }
9c172016
KL
586
587 // If editing, pass to that cell and do nothing else.
1dac6b8d
KL
588 if (getSelectedCell().isEditing) {
589 super.onKeypress(keypress);
590 return;
591 }
592
593 if (keypress.equals(kbLeft)) {
759cb83e 594 // Left
1dac6b8d
KL
595 if (selectedColumn > 0) {
596 selectedColumn--;
597 }
598 activate(columns.get(selectedColumn).get(selectedRow));
599 } else if (keypress.equals(kbRight)) {
759cb83e 600 // Right
1dac6b8d
KL
601 if (selectedColumn < columns.size() - 1) {
602 selectedColumn++;
603 }
604 activate(columns.get(selectedColumn).get(selectedRow));
605 } else if (keypress.equals(kbUp)) {
759cb83e 606 // Up
1dac6b8d
KL
607 if (selectedRow > 0) {
608 selectedRow--;
609 }
610 activate(columns.get(selectedColumn).get(selectedRow));
611 } else if (keypress.equals(kbDown)) {
759cb83e 612 // Down
1dac6b8d
KL
613 if (selectedRow < rows.size() - 1) {
614 selectedRow++;
615 }
616 activate(columns.get(selectedColumn).get(selectedRow));
617 } else if (keypress.equals(kbHome)) {
759cb83e 618 // Home - leftmost column
1dac6b8d
KL
619 selectedColumn = 0;
620 activate(columns.get(selectedColumn).get(selectedRow));
621 } else if (keypress.equals(kbEnd)) {
759cb83e 622 // End - rightmost column
1dac6b8d
KL
623 selectedColumn = columns.size() - 1;
624 activate(columns.get(selectedColumn).get(selectedRow));
625 } else if (keypress.equals(kbPgUp)) {
759cb83e
KL
626 // PgUp - Treat like multiple up
627 for (int i = 0; i < getHeight() - 2; i++) {
628 if (selectedRow > 0) {
629 selectedRow--;
630 }
631 }
632 activate(columns.get(selectedColumn).get(selectedRow));
1dac6b8d 633 } else if (keypress.equals(kbPgDn)) {
759cb83e
KL
634 // PgDn - Treat like multiple up
635 for (int i = 0; i < getHeight() - 2; i++) {
636 if (selectedRow < rows.size() - 1) {
637 selectedRow++;
638 }
639 }
640 activate(columns.get(selectedColumn).get(selectedRow));
1dac6b8d 641 } else if (keypress.equals(kbCtrlHome)) {
759cb83e
KL
642 // Ctrl-Home - go to top-left
643 selectedRow = 0;
644 selectedColumn = 0;
645 activate(columns.get(selectedColumn).get(selectedRow));
646 activate(columns.get(selectedColumn).get(selectedRow));
1dac6b8d 647 } else if (keypress.equals(kbCtrlEnd)) {
759cb83e
KL
648 // Ctrl-End - go to bottom-right
649 selectedRow = rows.size() - 1;
650 selectedColumn = columns.size() - 1;
651 activate(columns.get(selectedColumn).get(selectedRow));
652 activate(columns.get(selectedColumn).get(selectedRow));
1dac6b8d
KL
653 } else {
654 // Pass to the Cell.
655 super.onKeypress(keypress);
656 }
9c172016
KL
657
658 // We may have scrolled off screen. Reset positions as needed to
659 // make the newly selected cell visible.
660 alignGrid();
1dac6b8d
KL
661 }
662
2e1384cc
KL
663 /**
664 * Handle widget resize events.
665 *
666 * @param event resize event
667 */
668 @Override
669 public void onResize(final TResizeEvent event) {
670 super.onResize(event);
671
672 alignGrid();
673 }
674
1dac6b8d
KL
675 /**
676 * Handle posted menu events.
677 *
678 * @param menu menu event
679 */
680 @Override
681 public void onMenu(final TMenuEvent menu) {
682 switch (menu.getId()) {
77961919
KL
683 case TMenu.MID_TABLE_VIEW_ROW_LABELS:
684 showRowLabels = getApplication().getMenuItem(menu.getId()).getChecked();
685 break;
686 case TMenu.MID_TABLE_VIEW_COLUMN_LABELS:
687 showColumnLabels = getApplication().getMenuItem(menu.getId()).getChecked();
688 break;
689 case TMenu.MID_TABLE_VIEW_HIGHLIGHT_ROW:
690 highlightRow = getApplication().getMenuItem(menu.getId()).getChecked();
691 break;
692 case TMenu.MID_TABLE_VIEW_HIGHLIGHT_COLUMN:
693 highlightColumn = getApplication().getMenuItem(menu.getId()).getChecked();
694 break;
1dac6b8d
KL
695 case TMenu.MID_TABLE_BORDER_NONE:
696 case TMenu.MID_TABLE_BORDER_ALL:
697 case TMenu.MID_TABLE_BORDER_RIGHT:
698 case TMenu.MID_TABLE_BORDER_LEFT:
699 case TMenu.MID_TABLE_BORDER_TOP:
700 case TMenu.MID_TABLE_BORDER_BOTTOM:
701 case TMenu.MID_TABLE_BORDER_DOUBLE_BOTTOM:
702 case TMenu.MID_TABLE_BORDER_THICK_BOTTOM:
703 case TMenu.MID_TABLE_DELETE_LEFT:
704 case TMenu.MID_TABLE_DELETE_UP:
705 case TMenu.MID_TABLE_DELETE_ROW:
706 case TMenu.MID_TABLE_DELETE_COLUMN:
707 case TMenu.MID_TABLE_INSERT_LEFT:
708 case TMenu.MID_TABLE_INSERT_RIGHT:
709 case TMenu.MID_TABLE_INSERT_ABOVE:
710 case TMenu.MID_TABLE_INSERT_BELOW:
711 break;
712 case TMenu.MID_TABLE_COLUMN_NARROW:
713 columns.get(selectedColumn).width--;
714 for (Cell cell: getSelectedColumn().cells) {
715 cell.setWidth(columns.get(selectedColumn).width);
759cb83e 716 cell.field.setWidth(columns.get(selectedColumn).width);
1dac6b8d 717 }
9c172016
KL
718 for (int i = selectedColumn + 1; i < columns.size(); i++) {
719 for (Cell cell: columns.get(i).cells) {
720 cell.setX(cell.getX() - 1);
721 }
722 }
723 alignGrid();
1dac6b8d
KL
724 break;
725 case TMenu.MID_TABLE_COLUMN_WIDEN:
726 columns.get(selectedColumn).width++;
727 for (Cell cell: getSelectedColumn().cells) {
728 cell.setWidth(columns.get(selectedColumn).width);
759cb83e 729 cell.field.setWidth(columns.get(selectedColumn).width);
1dac6b8d 730 }
9c172016
KL
731 for (int i = selectedColumn + 1; i < columns.size(); i++) {
732 for (Cell cell: columns.get(i).cells) {
733 cell.setX(cell.getX() + 1);
734 }
735 }
736 alignGrid();
1dac6b8d
KL
737 break;
738 case TMenu.MID_TABLE_FILE_SAVE_CSV:
759cb83e
KL
739 // TODO
740 break;
1dac6b8d 741 case TMenu.MID_TABLE_FILE_SAVE_TEXT:
759cb83e 742 // TODO
1dac6b8d
KL
743 break;
744 default:
745 super.onMenu(menu);
746 }
77961919
KL
747
748 alignGrid();
1dac6b8d
KL
749 }
750
751 // ------------------------------------------------------------------------
752 // TWidget ----------------------------------------------------------------
753 // ------------------------------------------------------------------------
754
68ee64d0 755 /**
77961919 756 * Draw the table row/column labels, and borders.
68ee64d0
KL
757 */
758 @Override
759 public void draw() {
77961919
KL
760 CellAttributes labelColor = getTheme().getColor("ttable.label");
761 CellAttributes labelColorSelected = getTheme().getColor("ttable.label.selected");
68ee64d0
KL
762 CellAttributes borderColor = getTheme().getColor("ttable.border");
763
764 // Column labels.
765 if (showColumnLabels == true) {
68ee64d0 766 for (int i = left; i < columns.size(); i++) {
2e1384cc
KL
767 if (columns.get(i).get(top).isVisible() == false) {
768 break;
769 }
770 putStringXY(columns.get(i).get(top).getX(), 0,
759cb83e
KL
771 String.format(" %-" +
772 (columns.get(i).get(top).getWidth() - 2)
773 + "s ", columns.get(i).label),
77961919 774 (i == selectedColumn ? labelColorSelected : labelColor));
68ee64d0
KL
775 }
776 }
777
778 // Row labels.
779 if (showRowLabels == true) {
68ee64d0 780 for (int i = top; i < rows.size(); i++) {
2e1384cc
KL
781 if (rows.get(i).get(left).isVisible() == false) {
782 break;
783 }
784 putStringXY(0, rows.get(i).get(left).getY(),
785 String.format(" %-6s ", rows.get(i).label),
77961919 786 (i == selectedRow ? labelColorSelected : labelColor));
68ee64d0
KL
787 }
788 }
789
790 // Now draw the window borders.
791 super.draw();
792 }
793
1dac6b8d
KL
794 // ------------------------------------------------------------------------
795 // TTable -----------------------------------------------------------------
796 // ------------------------------------------------------------------------
797
798 /**
799 * Get the currently-selected cell.
800 *
801 * @return the selected cell
802 */
803 public Cell getSelectedCell() {
804 assert (rows.get(selectedRow) != null);
805 assert (rows.get(selectedRow).get(selectedColumn) != null);
806 assert (columns.get(selectedColumn) != null);
807 assert (columns.get(selectedColumn).get(selectedRow) != null);
808 assert (rows.get(selectedRow).get(selectedColumn) ==
809 columns.get(selectedColumn).get(selectedRow));
810
811 return (columns.get(selectedColumn).get(selectedRow));
812 }
813
814 /**
815 * Get the currently-selected column.
816 *
817 * @return the selected column
818 */
819 public Column getSelectedColumn() {
820 assert (selectedColumn >= 0);
821 assert (columns.size() > selectedColumn);
822 assert (columns.get(selectedColumn) != null);
823 return columns.get(selectedColumn);
824 }
825
826 /**
827 * Get the currently-selected row.
828 *
829 * @return the selected row
830 */
831 public Row getSelectedRow() {
832 assert (selectedRow >= 0);
833 assert (rows.size() > selectedRow);
834 assert (rows.get(selectedRow) != null);
835 return rows.get(selectedRow);
836 }
837
759cb83e
KL
838 /**
839 * Get the currently-selected column number. 0 is the left-most column.
840 *
841 * @return the selected column number
842 */
843 public int getSelectedColumnNumber() {
844 return selectedColumn;
845 }
846
847 /**
848 * Set the currently-selected column number. 0 is the left-most column.
849 *
850 * @param column the column number to select
851 */
852 public void setSelectedColumnNumber(final int column) {
853 if ((column < 0) || (column > columns.size() - 1)) {
854 throw new IndexOutOfBoundsException("Column count is " +
855 columns.size() + ", requested index " + column);
856 }
857 selectedColumn = column;
858 activate(columns.get(selectedColumn).get(selectedRow));
859 alignGrid();
860 }
861
862 /**
863 * Get the currently-selected row number. 0 is the top-most row.
864 *
865 * @return the selected row number
866 */
867 public int getSelectedRowNumber() {
868 return selectedRow;
869 }
870
871 /**
872 * Set the currently-selected row number. 0 is the left-most column.
873 *
874 * @param row the row number to select
875 */
876 public void setSelectedRowNumber(final int row) {
877 if ((row < 0) || (row > rows.size() - 1)) {
878 throw new IndexOutOfBoundsException("Row count is " +
879 rows.size() + ", requested index " + row);
880 }
881 selectedRow = row;
882 activate(columns.get(selectedColumn).get(selectedRow));
883 alignGrid();
884 }
885
886 /**
887 * Get the number of columns.
888 *
889 * @return the number of columns
890 */
891 public int getColumnCount() {
892 return columns.size();
893 }
894
895 /**
896 * Get the number of rows.
897 *
898 * @return the number of rows
899 */
900 public int getRowCount() {
901 return rows.size();
902 }
903
9c172016
KL
904 /**
905 * Get the full horizontal width of this table.
906 *
907 * @return the width required to render the entire table
908 */
759cb83e 909 private int getMaximumWidth() {
9c172016
KL
910 int totalWidth = 0;
911 if (showRowLabels == true) {
912 // For now, all row labels are 8 cells wide. TODO: make this
913 // adjustable.
914 totalWidth += 8;
915 }
916 for (Cell cell: getSelectedRow().cells) {
917 totalWidth += cell.getWidth() + 1;
918 }
919 return totalWidth;
920 }
921
68ee64d0
KL
922 /**
923 * Get the full vertical height of this table.
924 *
925 * @return the height required to render the entire table
926 */
759cb83e 927 private int getMaximumHeight() {
68ee64d0
KL
928 int totalHeight = 0;
929 if (showColumnLabels == true) {
930 // For now, all column labels are 1 cell tall. TODO: make this
931 // adjustable.
932 totalHeight += 1;
933 }
934 for (Cell cell: getSelectedColumn().cells) {
935 totalHeight += cell.getHeight();
936 // TODO: handle top/bottom borders.
937 }
938 return totalHeight;
939 }
940
9c172016
KL
941 /**
942 * Align the grid so that the selected cell is fully visible.
943 */
944 private void alignGrid() {
68ee64d0 945
2e1384cc
KL
946 /*
947 * We start by assuming that all cells are visible, and then mark as
948 * invisible those that are outside the viewable area.
949 */
950 for (int x = 0; x < columns.size(); x++) {
951 for (int y = 0; y < rows.size(); y++) {
759cb83e
KL
952 Cell cell = rows.get(y).cells.get(x);
953 cell.setVisible(true);
954
955 // Special case: mouse double-clicks can lead to multiple
956 // cells in editing mode. Only allow a cell to remain
957 // editing if it is fact the active widget.
958 if (cell.isEditing && !cell.isActive()) {
959 cell.fieldText = cell.field.getText();
960 cell.isEditing = false;
961 cell.field.setEnabled(false);
962 }
2e1384cc
KL
963 }
964 }
965
68ee64d0
KL
966 // Adjust X locations to be visible -----------------------------------
967
9c172016 968 // Determine if we need to shift left or right.
9c172016
KL
969 int leftCellX = 0;
970 if (showRowLabels == true) {
971 // For now, all row labels are 8 cells wide. TODO: make this
972 // adjustable.
973 leftCellX += 8;
974 }
68ee64d0
KL
975 Row row = getSelectedRow();
976 Cell selectedColumnCell = null;
977 for (int i = 0; i < row.cells.size(); i++) {
978 if (i == selectedColumn) {
979 selectedColumnCell = row.cells.get(i);
980 break;
981 }
982 leftCellX += row.cells.get(i).getWidth() + 1;
983 }
984 // There should always be a selected column.
985 assert (selectedColumnCell != null);
9c172016 986
2e1384cc
KL
987 int excessWidth = leftCellX + selectedColumnCell.getWidth() + 1 - getWidth();
988 if (excessWidth > 0) {
989 leftCellX -= excessWidth;
68ee64d0
KL
990 }
991 if (leftCellX < 0) {
2e1384cc
KL
992 if (showRowLabels == true) {
993 leftCellX = 8;
994 } else {
995 leftCellX = 0;
996 }
68ee64d0 997 }
9c172016 998
68ee64d0
KL
999 /*
1000 * leftCellX now contains the basic left offset necessary to draw the
1001 * cells such that the selected cell (column) is fully visible within
1002 * this widget's given width. Or, if the widget is too narrow to
2e1384cc 1003 * display the full cell, leftCellX is 0 or 8.
68ee64d0
KL
1004 *
1005 * Now reset all of the X positions of the other cells so that the
1006 * selected cell X is leftCellX.
1007 */
1008 for (int y = 0; y < rows.size(); y++) {
1009 // All cells to the left of selected cell.
1010 int newCellX = leftCellX;
1011 left = selectedColumn;
1012 for (int x = selectedColumn - 1; x >= 0; x--) {
2e1384cc 1013 newCellX -= rows.get(y).cells.get(x).getWidth() + 1;
759cb83e 1014 if (newCellX >= (showRowLabels ? 8 : 0)) {
68ee64d0
KL
1015 rows.get(y).cells.get(x).setVisible(true);
1016 rows.get(y).cells.get(x).setX(newCellX);
1017 left--;
1018 } else {
1019 // This cell won't be visible.
1020 rows.get(y).cells.get(x).setVisible(false);
1021 }
1022 }
1023
1024 // Selected cell.
1025 rows.get(y).cells.get(selectedColumn).setX(leftCellX);
2e1384cc 1026 assert (rows.get(y).cells.get(selectedColumn).isVisible());
68ee64d0
KL
1027
1028 // All cells to the right of selected cell.
1029 newCellX = leftCellX + selectedColumnCell.getWidth() + 1;
1030 for (int x = selectedColumn + 1; x < columns.size(); x++) {
2e1384cc 1031 if (newCellX <= getWidth()) {
68ee64d0
KL
1032 rows.get(y).cells.get(x).setVisible(true);
1033 rows.get(y).cells.get(x).setX(newCellX);
1034 } else {
1035 // This cell won't be visible.
1036 rows.get(y).cells.get(x).setVisible(false);
1037 }
1038 newCellX += rows.get(y).cells.get(x).getWidth() + 1;
1039 }
1040 }
1041
1042 // Adjust Y locations to be visible -----------------------------------
1043 // The same logic as above, but applied to the column Y.
9c172016 1044
68ee64d0 1045 // Determine if we need to shift up or down.
68ee64d0
KL
1046 int topCellY = 0;
1047 if (showColumnLabels == true) {
1048 // For now, all column labels are 1 cell high. TODO: make this
1049 // adjustable.
1050 topCellY += 1;
1051 }
1052 Column column = getSelectedColumn();
1053 Cell selectedRowCell = null;
1054 for (int i = 0; i < column.cells.size(); i++) {
1055 if (i == selectedRow) {
1056 selectedRowCell = column.cells.get(i);
1057 break;
1058 }
1059 topCellY += column.cells.get(i).getHeight();
1060 // TODO: if a border is selected, add 1 to topCellY.
1061 }
1062 // There should always be a selected row.
1063 assert (selectedRowCell != null);
1064
2e1384cc
KL
1065 int excessHeight = topCellY + selectedRowCell.getHeight() - getHeight() - 1;
1066 if (showColumnLabels == true) {
1067 excessHeight += 1;
1068 }
1069 if (excessHeight > 0) {
1070 topCellY -= excessHeight;
68ee64d0
KL
1071 }
1072 if (topCellY < 0) {
2e1384cc
KL
1073 if (showColumnLabels == true) {
1074 topCellY = 1;
1075 } else {
1076 topCellY = 0;
1077 }
68ee64d0
KL
1078 }
1079
1080 /*
1081 * topCellY now contains the basic top offset necessary to draw the
1082 * cells such that the selected cell (row) is fully visible within
1083 * this widget's given height. Or, if the widget is too short to
2e1384cc 1084 * display the full cell, topCellY is 0 or 1.
68ee64d0
KL
1085 *
1086 * Now reset all of the Y positions of the other cells so that the
1087 * selected cell Y is topCellY.
1088 */
1089 for (int x = 0; x < columns.size(); x++) {
77961919
KL
1090
1091 if (columns.get(x).get(0).isVisible() == false) {
1092 // This column won't be visible as determined by the checks
1093 // above, just continue to the next.
1094 continue;
2e1384cc
KL
1095 }
1096
68ee64d0
KL
1097 // All cells above the selected cell.
1098 int newCellY = topCellY;
1099 top = selectedRow;
1100 for (int y = selectedRow - 1; y >= 0; y--) {
1101 newCellY -= rows.get(y).cells.get(x).getHeight();
2e1384cc 1102 if (newCellY >= (showColumnLabels == true ? 1 : 0)) {
68ee64d0
KL
1103 rows.get(y).cells.get(x).setVisible(true);
1104 rows.get(y).cells.get(x).setY(newCellY);
1105 top--;
1106 } else {
1107 // This cell won't be visible.
1108 rows.get(y).cells.get(x).setVisible(false);
1109 }
1110 }
1111
1112 // Selected cell.
2e1384cc 1113 columns.get(x).cells.get(selectedRow).setY(topCellY);
68ee64d0 1114
2e1384cc 1115 // All cells below the selected cell.
68ee64d0
KL
1116 newCellY = topCellY + selectedRowCell.getHeight();
1117 for (int y = selectedRow + 1; y < rows.size(); y++) {
2e1384cc 1118 if (newCellY <= getHeight()) {
68ee64d0
KL
1119 rows.get(y).cells.get(x).setVisible(true);
1120 rows.get(y).cells.get(x).setY(newCellY);
1121 } else {
1122 // This cell won't be visible.
1123 rows.get(y).cells.get(x).setVisible(false);
1124 }
1125 newCellY += rows.get(y).cells.get(x).getHeight();
1126 }
1127 }
9c172016
KL
1128
1129 }
1130
759cb83e
KL
1131 /**
1132 * Save contents to file.
1133 *
1134 * @param filename file to save to
1135 * @throws IOException if a java.io operation throws
1136 */
1137 public void saveToFilename(final String filename) throws IOException {
1138 // TODO
1139 }
1140
1dac6b8d 1141}