fix javadoc
[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
73162e7f
KL
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
1dac6b8d
KL
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 */
73162e7f 140 private boolean highlightRow = false;
1dac6b8d
KL
141
142 /**
143 * If true, highlight the entire column of the currently-selected cell.
144 */
73162e7f 145 private boolean highlightColumn = false;
9c172016
KL
146
147 /**
148 * If true, show the row labels as the first column.
149 */
150 private boolean showRowLabels = true;
1dac6b8d 151
68ee64d0
KL
152 /**
153 * If true, show the column labels as the first row.
154 */
155 private boolean showColumnLabels = true;
156
73162e7f
KL
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
1dac6b8d
KL
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 */
68ee64d0 185 private String label = "";
1dac6b8d
KL
186
187 /**
73162e7f 188 * The right border for this column.
1dac6b8d 189 */
73162e7f 190 private Border rightBorder = Border.NONE;
1dac6b8d 191
9c172016
KL
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
1dac6b8d
KL
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 /**
73162e7f 251 * The bottom border for this row.
1dac6b8d 252 */
73162e7f 253 private Border bottomBorder = Border.NONE;
1dac6b8d 254
9c172016
KL
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
1dac6b8d
KL
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
2e1384cc 344 field = addField(0, 0, width, false);
1dac6b8d 345 field.setEnabled(false);
1dac6b8d
KL
346 field.setBackgroundChar(' ');
347 }
348
349 // --------------------------------------------------------------------
350 // Event handlers -----------------------------------------------------
351 // --------------------------------------------------------------------
352
759cb83e
KL
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;
73162e7f 373 ((TTableWidget) getParent()).alignGrid(false);
759cb83e
KL
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;
73162e7f 391 ((TTableWidget) getParent()).alignGrid(false);
759cb83e
KL
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;
73162e7f 409 ((TTableWidget) getParent()).alignGrid(false);
759cb83e
KL
410 }
411 }
412
1dac6b8d
KL
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
1dac6b8d 462 if (isAbsoluteActive()) {
9c172016
KL
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");
1dac6b8d 469 }
9c172016
KL
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");
1dac6b8d
KL
482 }
483
77961919
KL
484 assert (isVisible() == true);
485
1dac6b8d
KL
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.
9c172016
KL
532 rows.add(new Row(0));
533 columns.add(new Column(0));
1dac6b8d
KL
534
535 // Place a grid of cells that fit in this space.
536 int row = 0;
73162e7f 537 for (int i = 0; i < height + EXTRA_ROWS; i += rows.get(0).height) {
1dac6b8d 538 int column = 0;
73162e7f
KL
539 for (int j = 0; j < width + EXTRA_COLUMNS;
540 j += columns.get(0).width) {
541
1dac6b8d
KL
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);
73162e7f
KL
548 if ((i == 0) &&
549 (j + columns.get(0).width < width + EXTRA_COLUMNS)
550 ) {
9c172016 551 columns.add(new Column(column + 1));
1dac6b8d
KL
552 }
553 column++;
554 }
73162e7f 555 if (i + rows.get(0).height < height + EXTRA_ROWS) {
9c172016 556 rows.add(new Row(row + 1));
1dac6b8d
KL
557 }
558 row++;
559 }
560 activate(columns.get(selectedColumn).get(selectedRow));
9c172016 561
73162e7f 562 alignGrid(true);
77961919
KL
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
1dac6b8d
KL
575 }
576
577 // ------------------------------------------------------------------------
578 // Event handlers ---------------------------------------------------------
579 // ------------------------------------------------------------------------
580
759cb83e
KL
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
1dac6b8d
KL
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 }
9c172016
KL
620
621 // If editing, pass to that cell and do nothing else.
1dac6b8d
KL
622 if (getSelectedCell().isEditing) {
623 super.onKeypress(keypress);
624 return;
625 }
626
73162e7f
KL
627 boolean forceGridAlign = false;
628
1dac6b8d 629 if (keypress.equals(kbLeft)) {
759cb83e 630 // Left
1dac6b8d
KL
631 if (selectedColumn > 0) {
632 selectedColumn--;
633 }
634 activate(columns.get(selectedColumn).get(selectedRow));
635 } else if (keypress.equals(kbRight)) {
759cb83e 636 // Right
1dac6b8d
KL
637 if (selectedColumn < columns.size() - 1) {
638 selectedColumn++;
639 }
640 activate(columns.get(selectedColumn).get(selectedRow));
641 } else if (keypress.equals(kbUp)) {
759cb83e 642 // Up
1dac6b8d
KL
643 if (selectedRow > 0) {
644 selectedRow--;
645 }
646 activate(columns.get(selectedColumn).get(selectedRow));
647 } else if (keypress.equals(kbDown)) {
759cb83e 648 // Down
1dac6b8d
KL
649 if (selectedRow < rows.size() - 1) {
650 selectedRow++;
651 }
652 activate(columns.get(selectedColumn).get(selectedRow));
653 } else if (keypress.equals(kbHome)) {
759cb83e 654 // Home - leftmost column
1dac6b8d
KL
655 selectedColumn = 0;
656 activate(columns.get(selectedColumn).get(selectedRow));
73162e7f 657 forceGridAlign = true;
1dac6b8d 658 } else if (keypress.equals(kbEnd)) {
759cb83e 659 // End - rightmost column
1dac6b8d
KL
660 selectedColumn = columns.size() - 1;
661 activate(columns.get(selectedColumn).get(selectedRow));
73162e7f 662 forceGridAlign = true;
1dac6b8d 663 } else if (keypress.equals(kbPgUp)) {
759cb83e
KL
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));
73162e7f 671 forceGridAlign = true;
1dac6b8d 672 } else if (keypress.equals(kbPgDn)) {
759cb83e
KL
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));
73162e7f 680 forceGridAlign = true;
1dac6b8d 681 } else if (keypress.equals(kbCtrlHome)) {
759cb83e
KL
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));
73162e7f 687 forceGridAlign = true;
1dac6b8d 688 } else if (keypress.equals(kbCtrlEnd)) {
759cb83e
KL
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));
73162e7f 694 forceGridAlign = true;
1dac6b8d
KL
695 } else {
696 // Pass to the Cell.
697 super.onKeypress(keypress);
698 }
9c172016
KL
699
700 // We may have scrolled off screen. Reset positions as needed to
701 // make the newly selected cell visible.
73162e7f 702 alignGrid(forceGridAlign);
1dac6b8d
KL
703 }
704
2e1384cc
KL
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
73162e7f 714 alignGrid(true);
2e1384cc
KL
715 }
716
1dac6b8d
KL
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()) {
77961919
KL
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;
1dac6b8d
KL
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);
759cb83e 758 cell.field.setWidth(columns.get(selectedColumn).width);
1dac6b8d 759 }
9c172016
KL
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 }
73162e7f 765 alignGrid(false);
1dac6b8d
KL
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);
759cb83e 771 cell.field.setWidth(columns.get(selectedColumn).width);
1dac6b8d 772 }
9c172016
KL
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 }
73162e7f 778 alignGrid(false);
1dac6b8d
KL
779 break;
780 case TMenu.MID_TABLE_FILE_SAVE_CSV:
759cb83e
KL
781 // TODO
782 break;
1dac6b8d 783 case TMenu.MID_TABLE_FILE_SAVE_TEXT:
759cb83e 784 // TODO
1dac6b8d
KL
785 break;
786 default:
787 super.onMenu(menu);
788 }
77961919 789
73162e7f 790 alignGrid(false);
1dac6b8d
KL
791 }
792
793 // ------------------------------------------------------------------------
794 // TWidget ----------------------------------------------------------------
795 // ------------------------------------------------------------------------
796
68ee64d0 797 /**
77961919 798 * Draw the table row/column labels, and borders.
68ee64d0
KL
799 */
800 @Override
801 public void draw() {
77961919
KL
802 CellAttributes labelColor = getTheme().getColor("ttable.label");
803 CellAttributes labelColorSelected = getTheme().getColor("ttable.label.selected");
68ee64d0
KL
804 CellAttributes borderColor = getTheme().getColor("ttable.border");
805
806 // Column labels.
807 if (showColumnLabels == true) {
68ee64d0 808 for (int i = left; i < columns.size(); i++) {
2e1384cc
KL
809 if (columns.get(i).get(top).isVisible() == false) {
810 break;
811 }
812 putStringXY(columns.get(i).get(top).getX(), 0,
759cb83e 813 String.format(" %-" +
73162e7f 814 (columns.get(i).width - 2)
759cb83e 815 + "s ", columns.get(i).label),
77961919 816 (i == selectedColumn ? labelColorSelected : labelColor));
68ee64d0
KL
817 }
818 }
819
820 // Row labels.
821 if (showRowLabels == true) {
68ee64d0 822 for (int i = top; i < rows.size(); i++) {
2e1384cc
KL
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),
77961919 828 (i == selectedRow ? labelColorSelected : labelColor));
68ee64d0
KL
829 }
830 }
831
832 // Now draw the window borders.
833 super.draw();
834 }
835
1dac6b8d
KL
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
759cb83e
KL
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));
73162e7f 901 alignGrid(true);
759cb83e
KL
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));
73162e7f 925 alignGrid(true);
759cb83e
KL
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
9c172016
KL
946 /**
947 * Align the grid so that the selected cell is fully visible.
73162e7f
KL
948 *
949 * @param force if true, always move the grid as needed
9c172016 950 */
73162e7f
KL
951 private void alignGrid(final boolean force) {
952 boolean resetRowY = false;
953 boolean resetColumnX = false;
954
955 if (selectedColumn < left) {
956 left = selectedColumn;
957 resetColumnX = true;
958 resetRowY = true;
959 }
960 if (selectedRow < top) {
961 top = selectedRow;
962 resetColumnX = true;
963 resetRowY = true;
964 }
965
966 if (force == true) {
967 resetRowY = true;
968 resetColumnX = true;
969 } else if ((getSelectedCell().getX() + getSelectedCell().getWidth() + 1 >
970 getWidth() - 1)
971 || (columns.get(left).get(0).getX() <
972 (showRowLabels == true ? ROW_LABEL_WIDTH : 0))
973 ) {
974 resetColumnX = true;
975 resetRowY = true;
976 } else if ((getSelectedCell().getY() + getSelectedCell().getHeight() >
977 getHeight())
978 || (rows.get(top).get(0).getY() <
979 (showColumnLabels == true ? COLUMN_LABEL_HEIGHT : 0))
980 ) {
981 resetColumnX = true;
982 resetRowY = true;
983 }
984
985 if ((resetColumnX == false) && (resetRowY == false)) {
986 // Nothing to do, bail out.
987 return;
988 }
68ee64d0 989
2e1384cc
KL
990 /*
991 * We start by assuming that all cells are visible, and then mark as
992 * invisible those that are outside the viewable area.
993 */
994 for (int x = 0; x < columns.size(); x++) {
995 for (int y = 0; y < rows.size(); y++) {
73162e7f 996 Cell cell = rows.get(y).get(x);
759cb83e
KL
997 cell.setVisible(true);
998
73162e7f
KL
999 // Special case: mouse double-clicks can lead to
1000 // multiple cells in editing mode. Only allow a cell
1001 // to remain editing if it is fact the active widget.
759cb83e
KL
1002 if (cell.isEditing && !cell.isActive()) {
1003 cell.fieldText = cell.field.getText();
1004 cell.isEditing = false;
1005 cell.field.setEnabled(false);
1006 }
2e1384cc
KL
1007 }
1008 }
1009
68ee64d0
KL
1010 // Adjust X locations to be visible -----------------------------------
1011
9c172016 1012 // Determine if we need to shift left or right.
9c172016
KL
1013 int leftCellX = 0;
1014 if (showRowLabels == true) {
1015 // For now, all row labels are 8 cells wide. TODO: make this
1016 // adjustable.
73162e7f 1017 leftCellX += ROW_LABEL_WIDTH;
9c172016 1018 }
68ee64d0
KL
1019 Row row = getSelectedRow();
1020 Cell selectedColumnCell = null;
1021 for (int i = 0; i < row.cells.size(); i++) {
1022 if (i == selectedColumn) {
1023 selectedColumnCell = row.cells.get(i);
1024 break;
1025 }
1026 leftCellX += row.cells.get(i).getWidth() + 1;
1027 }
1028 // There should always be a selected column.
1029 assert (selectedColumnCell != null);
9c172016 1030
73162e7f 1031 if (resetColumnX == true) {
9c172016 1032
73162e7f
KL
1033 // We need to adjust everything so that the selected cell is
1034 // visible.
1035
1036 int excessWidth = leftCellX + selectedColumnCell.getWidth() + 1 - getWidth();
1037 if (excessWidth > 0) {
1038 leftCellX -= excessWidth;
1039 }
1040 if (leftCellX < 0) {
1041 if (showRowLabels == true) {
1042 leftCellX = ROW_LABEL_WIDTH;
68ee64d0 1043 } else {
73162e7f 1044 leftCellX = 0;
68ee64d0
KL
1045 }
1046 }
1047
73162e7f
KL
1048 /*
1049 * leftCellX now contains the basic left offset necessary to draw
1050 * the cells such that the selected cell (column) is fully
1051 * visible within this widget's given width. Or, if the widget
1052 * is too narrow to display the full cell, leftCellX is 0 or
1053 * ROW_LABEL_WIDTH.
1054 *
1055 * Now reset all of the X positions of the other cells so that
1056 * the selected cell X is leftCellX.
1057 */
1058 for (int y = 0; y < rows.size(); y++) {
1059 // All cells to the left of selected cell.
1060 int newCellX = leftCellX;
1061 left = selectedColumn - 1;
1062 for (int x = selectedColumn - 1; x >= 0; x--) {
1063 newCellX -= rows.get(y).get(x).getWidth() + 1;
1064 rows.get(y).get(x).setX(newCellX);
1065 if (newCellX >= (showRowLabels ? ROW_LABEL_WIDTH : 0)) {
1066 if ((rows.get(y).get(0).getY() < (showColumnLabels ? COLUMN_LABEL_HEIGHT : 0))
1067 || (rows.get(y).get(0).getY() >= getHeight())
1068 ) {
1069 // This row isn't visible.
1070 rows.get(y).get(x).setVisible(false);
1071 } else {
1072 rows.get(y).get(x).setVisible(true);
1073 }
1074 left--;
1075 } else {
1076 // This cell won't be visible.
1077 rows.get(y).get(x).setVisible(false);
1078 }
1079 }
1080 left++;
1081
1082 // Selected cell.
1083 rows.get(y).get(selectedColumn).setX(leftCellX);
1084 if ((rows.get(y).get(0).getY() < (showColumnLabels ? COLUMN_LABEL_HEIGHT : 0))
1085 || (rows.get(y).get(0).getY() >= getHeight())
1086 ) {
1087 // This row isn't visible.
1088 rows.get(y).get(selectedColumn).setVisible(false);
68ee64d0 1089 } else {
73162e7f
KL
1090 rows.get(y).get(selectedColumn).setVisible(true);
1091 }
1092
1093 assert (rows.get(y).get(left).getX() >= 0);
1094 assert (rows.get(y).get(left).getX() + rows.get(y).get(left).getWidth() >= (showRowLabels ? ROW_LABEL_WIDTH : 0));
1095
1096 // All cells to the right of selected cell.
1097 newCellX = leftCellX + selectedColumnCell.getWidth() + 1;
1098 for (int x = selectedColumn + 1; x < columns.size(); x++) {
1099 rows.get(y).get(x).setX(newCellX);
1100 if (newCellX <= getWidth()) {
1101 if ((rows.get(y).get(0).getY() < (showColumnLabels ? COLUMN_LABEL_HEIGHT : 0))
1102 || (rows.get(y).get(0).getY() >= getHeight())
1103 ) {
1104 // This row isn't visible.
1105 rows.get(y).get(x).setVisible(false);
1106 } else {
1107 rows.get(y).get(x).setVisible(true);
1108 }
1109 } else {
1110 // This cell won't be visible.
1111 rows.get(y).get(x).setVisible(false);
1112 }
1113 newCellX += rows.get(y).get(x).getWidth() + 1;
68ee64d0 1114 }
68ee64d0 1115 }
73162e7f
KL
1116
1117 } // if (resetColumnX == true)
68ee64d0
KL
1118
1119 // Adjust Y locations to be visible -----------------------------------
1120 // The same logic as above, but applied to the column Y.
9c172016 1121
68ee64d0 1122 // Determine if we need to shift up or down.
68ee64d0
KL
1123 int topCellY = 0;
1124 if (showColumnLabels == true) {
1125 // For now, all column labels are 1 cell high. TODO: make this
1126 // adjustable.
1127 topCellY += 1;
1128 }
1129 Column column = getSelectedColumn();
1130 Cell selectedRowCell = null;
1131 for (int i = 0; i < column.cells.size(); i++) {
1132 if (i == selectedRow) {
1133 selectedRowCell = column.cells.get(i);
1134 break;
1135 }
1136 topCellY += column.cells.get(i).getHeight();
1137 // TODO: if a border is selected, add 1 to topCellY.
1138 }
1139 // There should always be a selected row.
1140 assert (selectedRowCell != null);
1141
73162e7f 1142 if (resetRowY == true) {
68ee64d0 1143
73162e7f
KL
1144 // We need to adjust everything so that the selected cell is
1145 // visible.
77961919 1146
73162e7f
KL
1147 int excessHeight = topCellY + selectedRowCell.getHeight() - getHeight() - 1;
1148 if (showColumnLabels == true) {
1149 excessHeight += COLUMN_LABEL_HEIGHT;
2e1384cc 1150 }
73162e7f
KL
1151 if (excessHeight > 0) {
1152 topCellY -= excessHeight;
1153 }
1154 if (topCellY < 0) {
1155 if (showColumnLabels == true) {
1156 topCellY = COLUMN_LABEL_HEIGHT;
68ee64d0 1157 } else {
73162e7f 1158 topCellY = 0;
68ee64d0
KL
1159 }
1160 }
1161
73162e7f
KL
1162 /*
1163 * topCellY now contains the basic top offset necessary to draw
1164 * the cells such that the selected cell (row) is fully visible
1165 * within this widget's given height. Or, if the widget is too
1166 * short to display the full cell, topCellY is 0 or
1167 * COLUMN_LABEL_HEIGHT.
1168 *
1169 * Now reset all of the Y positions of the other cells so that
1170 * the selected cell Y is topCellY.
1171 */
1172 for (int x = 0; x < columns.size(); x++) {
1173 // All cells above the selected cell.
1174 int newCellY = topCellY;
1175 top = selectedRow - 1;
1176 for (int y = selectedRow - 1; y >= 0; y--) {
1177 newCellY -= columns.get(x).get(y).getHeight();
1178 columns.get(x).get(y).setY(newCellY);
1179 if (newCellY >= (showColumnLabels ? COLUMN_LABEL_HEIGHT : 0)) {
1180 if ((columns.get(x).get(0).getX() < (showRowLabels ? ROW_LABEL_WIDTH : 0))
1181 || (columns.get(x).get(0).getX() >= getWidth())
1182 ) {
1183 // This column isn't visible.
1184 columns.get(x).get(y).setVisible(false);
1185 } else {
1186 columns.get(x).get(y).setVisible(true);
1187 }
1188 top--;
1189 } else {
1190 // This cell won't be visible.
1191 columns.get(x).get(y).setVisible(false);
1192 }
1193 }
1194 top++;
1195
1196 // Selected cell.
1197 columns.get(x).get(selectedRow).setY(topCellY);
1198 if ((columns.get(x).get(0).getX() < (showRowLabels ? ROW_LABEL_WIDTH : 0))
1199 || (columns.get(x).get(0).getX() >= getWidth())
1200 ) {
1201 // This column isn't visible.
1202 columns.get(x).get(selectedRow).setVisible(false);
68ee64d0 1203 } else {
73162e7f
KL
1204 columns.get(x).get(selectedRow).setVisible(true);
1205 }
1206
1207 assert (columns.get(x).get(top).getY() >= 0);
1208 assert (columns.get(x).get(top).getY() + columns.get(x).get(top).getHeight() >= (showColumnLabels ? COLUMN_LABEL_HEIGHT : 0));
1209
1210 // All cells below the selected cell.
1211 newCellY = topCellY + selectedRowCell.getHeight();
1212 for (int y = selectedRow + 1; y < rows.size(); y++) {
1213 columns.get(x).get(y).setY(newCellY);
1214 if (newCellY <= getHeight()) {
1215 if ((columns.get(x).get(0).getX() < (showRowLabels ? ROW_LABEL_WIDTH : 0))
1216 || (columns.get(x).get(0).getX() >= getWidth())
1217 ) {
1218 // This column isn't visible.
1219 columns.get(x).get(y).setVisible(false);
1220 } else {
1221 columns.get(x).get(y).setVisible(true);
1222 }
1223 } else {
1224 // This cell won't be visible.
1225 columns.get(x).get(y).setVisible(false);
1226 }
1227 newCellY += columns.get(x).get(y).getHeight();
68ee64d0 1228 }
68ee64d0 1229 }
73162e7f
KL
1230
1231 } // if (resetRowY == true)
9c172016
KL
1232
1233 }
1234
759cb83e
KL
1235 /**
1236 * Save contents to file.
1237 *
1238 * @param filename file to save to
1239 * @throws IOException if a java.io operation throws
1240 */
1241 public void saveToFilename(final String filename) throws IOException {
1242 // TODO
1243 }
1244
1dac6b8d 1245}