X-Git-Url: http://git.nikiroo.be/?a=blobdiff_plain;f=src%2Fjexer%2FTTableWidget.java;h=f25f429d52b58bc916dab54329296a12686d9e2c;hb=ab8b0034fec1f17f97cfaa96d83c030180ffaa37;hp=2e10a3faedc702295335cd740d60a85ca4a331e6;hpb=759cb83ebad2f861e50f39dab34f70eaafe6d6ed;p=fanfix.git diff --git a/src/jexer/TTableWidget.java b/src/jexer/TTableWidget.java index 2e10a3f..f25f429 100644 --- a/src/jexer/TTableWidget.java +++ b/src/jexer/TTableWidget.java @@ -31,6 +31,7 @@ package jexer; import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.ResourceBundle; import jexer.bits.CellAttributes; import jexer.event.TKeypressEvent; @@ -50,6 +51,11 @@ import static jexer.TKeypress.*; */ public class TTableWidget extends TWidget { + /** + * Translated strings. + */ + private static final ResourceBundle i18n = ResourceBundle.getBundle(TTableWidget.class.getName()); + // ------------------------------------------------------------------------ // Constants -------------------------------------------------------------- // ------------------------------------------------------------------------ @@ -74,12 +80,36 @@ public class TTableWidget extends TWidget { DOUBLE, /** - * Thick bar: \u258C (vertical, left half block) and \u2580 - * (horizontal, upper block). + * Thick bar: \u2503 (vertical heavy) and \u2501 (horizontal heavy). */ THICK, } + /** + * Row label width. + */ + private static final int ROW_LABEL_WIDTH = 8; + + /** + * Column label height. + */ + private static final int COLUMN_LABEL_HEIGHT = 1; + + /** + * Column default width. + */ + private static final int COLUMN_DEFAULT_WIDTH = 8; + + /** + * Extra rows to add. + */ + private static final int EXTRA_ROWS = 10; + + /** + * Extra columns to add. + */ + private static final int EXTRA_COLUMNS = 10 * (8 + 1); + // ------------------------------------------------------------------------ // Variables -------------------------------------------------------------- // ------------------------------------------------------------------------ @@ -117,12 +147,12 @@ public class TTableWidget extends TWidget { /** * If true, highlight the entire row of the currently-selected cell. */ - private boolean highlightRow = true; + private boolean highlightRow = false; /** * If true, highlight the entire column of the currently-selected cell. */ - private boolean highlightColumn = true; + private boolean highlightColumn = false; /** * If true, show the row labels as the first column. @@ -134,15 +164,30 @@ public class TTableWidget extends TWidget { */ private boolean showColumnLabels = true; + /** + * The top border for the first row. + */ + private Border topBorder = Border.NONE; + + /** + * The left border for the first column. + */ + private Border leftBorder = Border.NONE; + /** * Column represents a column of cells. */ public class Column { + /** + * X position of this column. + */ + private int x = 0; + /** * Width of column. */ - private int width = 8; + private int width = COLUMN_DEFAULT_WIDTH; /** * The cells of this column. @@ -155,9 +200,9 @@ public class TTableWidget extends TWidget { private String label = ""; /** - * The border for this column. + * The right border for this column. */ - private Border border = Border.NONE; + private Border rightBorder = Border.NONE; /** * Constructor sets label to lettered column. @@ -166,15 +211,7 @@ public class TTableWidget extends TWidget { * "A", column 1 will be "B", column 26 will be "AA", and so on. */ Column(int col) { - StringBuilder sb = new StringBuilder(); - for (;;) { - sb.append((char) ('A' + (col % 26))); - if (col < 26) { - break; - } - col /= 26; - } - label = sb.reverse().toString(); + label = makeColumnLabel(col); } /** @@ -195,6 +232,28 @@ public class TTableWidget extends TWidget { public Cell get(final int row) { return cells.get(row); } + + /** + * Get the X position of the cells in this column. + * + * @return the position + */ + public int getX() { + return x; + } + + /** + * Set the X position of the cells in this column. + * + * @param x the position + */ + public void setX(final int x) { + for (Cell cell: cells) { + cell.setX(x); + } + this.x = x; + } + } /** @@ -202,6 +261,11 @@ public class TTableWidget extends TWidget { */ public class Row { + /** + * Y position of this row. + */ + private int y = 0; + /** * Height of row. */ @@ -218,9 +282,9 @@ public class TTableWidget extends TWidget { private String label = ""; /** - * The border for this row. + * The bottom border for this row. */ - private Border border = Border.NONE; + private Border bottomBorder = Border.NONE; /** * Constructor sets label to numbered row. @@ -249,6 +313,26 @@ public class TTableWidget extends TWidget { public Cell get(final int column) { return cells.get(column); } + /** + * Get the Y position of the cells in this column. + * + * @return the position + */ + public int getY() { + return y; + } + + /** + * Set the Y position of the cells in this column. + * + * @param y the position + */ + public void setY(final int y) { + for (Cell cell: cells) { + cell.setY(y); + } + this.y = y; + } } @@ -283,6 +367,11 @@ public class TTableWidget extends TWidget { */ private boolean isEditing = false; + /** + * If true, the cell is read-only (non-editable). + */ + private boolean readOnly = false; + /** * Text of field before editing. */ @@ -389,16 +478,24 @@ public class TTableWidget extends TWidget { public void onKeypress(final TKeypressEvent keypress) { // System.err.println("Cell onKeypress: " + keypress); + if (readOnly) { + // Read only: do nothing. + return; + } + if (isEditing) { if (keypress.equals(kbEsc)) { // ESC cancels the edit. - field.setText(fieldText); - isEditing = false; - field.setEnabled(false); + cancelEdit(); return; } if (keypress.equals(kbEnter)) { // Enter ends editing. + + // Pass down to field first so that it can execute + // enterAction if specified. + super.onKeypress(keypress); + fieldText = field.getText(); isEditing = false; field.setEnabled(false); @@ -478,6 +575,28 @@ public class TTableWidget extends TWidget { field.setText(text); } + /** + * Cancel any pending edit. + */ + public void cancelEdit() { + // Cancel any pending edit. + if (fieldText != null) { + field.setText(fieldText); + } + isEditing = false; + field.setEnabled(false); + } + + /** + * Set an entire column of cells read-only (non-editable) or not. + * + * @param readOnly if true, the cells will be non-editable + */ + public void setReadOnly(final boolean readOnly) { + cancelEdit(); + this.readOnly = readOnly; + } + } // ------------------------------------------------------------------------ @@ -504,25 +623,38 @@ public class TTableWidget extends TWidget { // Place a grid of cells that fit in this space. int row = 0; - for (int i = 0; i < height; i += rows.get(0).height) { + for (int i = 0; i < height + EXTRA_ROWS; i += rows.get(0).height) { int column = 0; - for (int j = 0; j < width; j += columns.get(0).width) { - Cell cell = new Cell(this, j, i, columns.get(0).width, + for (int j = 0; j < width + EXTRA_COLUMNS; + j += columns.get(0).width) { + + Cell cell = new Cell(this, 0, 0, /* j, i, */ columns.get(0).width, rows.get(0).height, column, row); + // DEBUG: set a grid of cell index labels + // TODO: remove this cell.setText("" + row + " " + column); rows.get(row).add(cell); columns.get(column).add(cell); - if ((i == 0) && (j + columns.get(0).width < width)) { + if ((i == 0) && + (j + columns.get(0).width < width + EXTRA_COLUMNS) + ) { columns.add(new Column(column + 1)); } column++; } - if (i + rows.get(0).height < height) { + if (i + rows.get(0).height < height + EXTRA_ROWS) { rows.add(new Row(row + 1)); } row++; } + for (int i = 0; i < rows.size(); i++) { + rows.get(i).setY(i + (showColumnLabels ? COLUMN_LABEL_HEIGHT : 0)); + } + for (int j = 0; j < columns.size(); j++) { + columns.get(j).setX((j * COLUMN_DEFAULT_WIDTH) + + (showRowLabels ? ROW_LABEL_WIDTH : 0)); + } activate(columns.get(selectedColumn).get(selectedRow)); alignGrid(); @@ -669,7 +801,7 @@ public class TTableWidget extends TWidget { public void onResize(final TResizeEvent event) { super.onResize(event); - alignGrid(); + bottomRightCorner(); } /** @@ -679,7 +811,25 @@ public class TTableWidget extends TWidget { */ @Override public void onMenu(final TMenuEvent menu) { + TInputBox inputBox; + switch (menu.getId()) { + case TMenu.MID_TABLE_RENAME_COLUMN: + inputBox = inputBox(i18n.getString("renameColumnInputTitle"), + i18n.getString("renameColumnInputCaption"), + getColumnLabel(selectedColumn), TMessageBox.Type.OKCANCEL); + if (inputBox.isOk()) { + setColumnLabel(selectedColumn, inputBox.getText()); + } + break; + case TMenu.MID_TABLE_RENAME_ROW: + inputBox = inputBox(i18n.getString("renameRowInputTitle"), + i18n.getString("renameRowInputCaption"), + getRowLabel(selectedRow), TMessageBox.Type.OKCANCEL); + if (inputBox.isOk()) { + setRowLabel(selectedRow, inputBox.getText()); + } + break; case TMenu.MID_TABLE_VIEW_ROW_LABELS: showRowLabels = getApplication().getMenuItem(menu.getId()).getChecked(); break; @@ -693,21 +843,110 @@ public class TTableWidget extends TWidget { highlightColumn = getApplication().getMenuItem(menu.getId()).getChecked(); break; case TMenu.MID_TABLE_BORDER_NONE: + topBorder = Border.NONE; + leftBorder = Border.NONE; + for (int i = 0; i < columns.size(); i++) { + columns.get(i).rightBorder = Border.NONE; + } + for (int i = 0; i < rows.size(); i++) { + rows.get(i).bottomBorder = Border.NONE; + rows.get(i).height = 1; + } + break; case TMenu.MID_TABLE_BORDER_ALL: + topBorder = Border.SINGLE; + leftBorder = Border.SINGLE; + for (int i = 0; i < columns.size(); i++) { + columns.get(i).rightBorder = Border.SINGLE; + } + for (int i = 0; i < rows.size(); i++) { + rows.get(i).bottomBorder = Border.SINGLE; + rows.get(i).height = 2; + } + break; + case TMenu.MID_TABLE_BORDER_CELL_NONE: + if (selectedRow == 0) { + topBorder = Border.NONE; + } + if (selectedColumn == 0) { + leftBorder = Border.NONE; + } + columns.get(selectedColumn).rightBorder = Border.NONE; + rows.get(selectedRow).bottomBorder = Border.NONE; + rows.get(selectedRow).height = 1; + break; + case TMenu.MID_TABLE_BORDER_CELL_ALL: + if (selectedRow == 0) { + topBorder = Border.SINGLE; + } + if (selectedColumn == 0) { + leftBorder = Border.SINGLE; + } + columns.get(selectedColumn).rightBorder = Border.SINGLE; + rows.get(selectedRow).bottomBorder = Border.SINGLE; + rows.get(selectedRow).height = 2; + break; case TMenu.MID_TABLE_BORDER_RIGHT: + columns.get(selectedColumn).rightBorder = Border.SINGLE; + break; case TMenu.MID_TABLE_BORDER_LEFT: + if (selectedColumn == 0) { + leftBorder = Border.SINGLE; + } else { + columns.get(selectedColumn - 1).rightBorder = Border.SINGLE; + } + break; case TMenu.MID_TABLE_BORDER_TOP: + if (selectedRow == 0) { + topBorder = Border.SINGLE; + } else { + rows.get(selectedRow - 1).bottomBorder = Border.SINGLE; + rows.get(selectedRow - 1).height = 2; + } + break; case TMenu.MID_TABLE_BORDER_BOTTOM: + rows.get(selectedRow).bottomBorder = Border.SINGLE; + rows.get(selectedRow).height = 2; + break; case TMenu.MID_TABLE_BORDER_DOUBLE_BOTTOM: + rows.get(selectedRow).bottomBorder = Border.DOUBLE; + rows.get(selectedRow).height = 2; + break; case TMenu.MID_TABLE_BORDER_THICK_BOTTOM: + rows.get(selectedRow).bottomBorder = Border.THICK; + rows.get(selectedRow).height = 2; + break; case TMenu.MID_TABLE_DELETE_LEFT: + deleteCellShiftLeft(); + activate(columns.get(selectedColumn).get(selectedRow)); + break; case TMenu.MID_TABLE_DELETE_UP: + deleteCellShiftUp(); + activate(columns.get(selectedColumn).get(selectedRow)); + break; case TMenu.MID_TABLE_DELETE_ROW: + deleteRow(selectedRow); + activate(columns.get(selectedColumn).get(selectedRow)); + break; case TMenu.MID_TABLE_DELETE_COLUMN: + deleteColumn(selectedColumn); + activate(columns.get(selectedColumn).get(selectedRow)); + break; case TMenu.MID_TABLE_INSERT_LEFT: + insertColumnLeft(selectedColumn); + activate(columns.get(selectedColumn).get(selectedRow)); + break; case TMenu.MID_TABLE_INSERT_RIGHT: + insertColumnRight(selectedColumn); + activate(columns.get(selectedColumn).get(selectedRow)); + break; case TMenu.MID_TABLE_INSERT_ABOVE: + insertRowAbove(selectedColumn); + activate(columns.get(selectedColumn).get(selectedRow)); + break; case TMenu.MID_TABLE_INSERT_BELOW: + insertRowBelow(selectedColumn); + activate(columns.get(selectedColumn).get(selectedRow)); break; case TMenu.MID_TABLE_COLUMN_NARROW: columns.get(selectedColumn).width--; @@ -716,11 +955,8 @@ public class TTableWidget extends TWidget { cell.field.setWidth(columns.get(selectedColumn).width); } for (int i = selectedColumn + 1; i < columns.size(); i++) { - for (Cell cell: columns.get(i).cells) { - cell.setX(cell.getX() - 1); - } + columns.get(i).setX(columns.get(i).getX() - 1); } - alignGrid(); break; case TMenu.MID_TABLE_COLUMN_WIDEN: columns.get(selectedColumn).width++; @@ -729,11 +965,8 @@ public class TTableWidget extends TWidget { cell.field.setWidth(columns.get(selectedColumn).width); } for (int i = selectedColumn + 1; i < columns.size(); i++) { - for (Cell cell: columns.get(i).cells) { - cell.setX(cell.getX() + 1); - } + columns.get(i).setX(columns.get(i).getX() + 1); } - alignGrid(); break; case TMenu.MID_TABLE_FILE_SAVE_CSV: // TODO @@ -745,6 +978,7 @@ public class TTableWidget extends TWidget { super.onMenu(menu); } + // Fix/redraw the display. alignGrid(); } @@ -769,7 +1003,7 @@ public class TTableWidget extends TWidget { } putStringXY(columns.get(i).get(top).getX(), 0, String.format(" %-" + - (columns.get(i).get(top).getWidth() - 2) + (columns.get(i).width - 2) + "s ", columns.get(i).label), (i == selectedColumn ? labelColorSelected : labelColor)); } @@ -787,6 +1021,129 @@ public class TTableWidget extends TWidget { } } + // Draw vertical borders. + if (leftBorder == Border.SINGLE) { + vLineXY((showRowLabels ? ROW_LABEL_WIDTH : 0), + (topBorder == Border.NONE ? 0 : 1) + + (showColumnLabels ? COLUMN_LABEL_HEIGHT : 0), + getHeight(), '\u2502', borderColor); + } + for (int i = left; i < columns.size(); i++) { + if (columns.get(i).get(top).isVisible() == false) { + break; + } + if (columns.get(i).rightBorder == Border.SINGLE) { + vLineXY(columns.get(i).getX() + columns.get(i).width, + (topBorder == Border.NONE ? 0 : 1) + + (showColumnLabels ? COLUMN_LABEL_HEIGHT : 0), + getHeight(), '\u2502', borderColor); + } + } + + // Draw horizontal borders. + if (topBorder == Border.SINGLE) { + hLineXY((showRowLabels ? ROW_LABEL_WIDTH : 0), + (showColumnLabels ? COLUMN_LABEL_HEIGHT : 0), + getWidth(), '\u2500', borderColor); + } + for (int i = top; i < rows.size(); i++) { + if (rows.get(i).get(left).isVisible() == false) { + break; + } + if (rows.get(i).bottomBorder == Border.SINGLE) { + hLineXY((leftBorder == Border.NONE ? 0 : 1) + + (showRowLabels ? ROW_LABEL_WIDTH : 0), + rows.get(i).getY() + rows.get(i).height - 1, + getWidth(), '\u2500', borderColor); + } else if (rows.get(i).bottomBorder == Border.DOUBLE) { + hLineXY((leftBorder == Border.NONE ? 0 : 1) + + (showRowLabels ? ROW_LABEL_WIDTH : 0), + rows.get(i).getY() + rows.get(i).height - 1, + getWidth(), '\u2550', borderColor); + } else if (rows.get(i).bottomBorder == Border.THICK) { + hLineXY((leftBorder == Border.NONE ? 0 : 1) + + (showRowLabels ? ROW_LABEL_WIDTH : 0), + rows.get(i).getY() + rows.get(i).height - 1, + getWidth(), '\u2501', borderColor); + } + } + // Top-left corner if needed + if ((topBorder == Border.SINGLE) && (leftBorder == Border.SINGLE)) { + putCharXY((showRowLabels ? ROW_LABEL_WIDTH : 0), + (showColumnLabels ? COLUMN_LABEL_HEIGHT : 0), + '\u250c', borderColor); + } + + // Now draw the correct corners + for (int i = top; i < rows.size(); i++) { + if (rows.get(i).get(left).isVisible() == false) { + break; + } + for (int j = left; j < columns.size(); j++) { + if (columns.get(j).get(i).isVisible() == false) { + break; + } + if ((i == top) && (topBorder == Border.SINGLE) + && (columns.get(j).rightBorder == Border.SINGLE) + ) { + // Top tee + putCharXY(columns.get(j).getX() + columns.get(j).width, + (showColumnLabels ? COLUMN_LABEL_HEIGHT : 0), + '\u252c', borderColor); + } + if ((j == left) && (leftBorder == Border.SINGLE) + && (rows.get(i).bottomBorder == Border.SINGLE) + ) { + // Left tee + putCharXY((showRowLabels ? ROW_LABEL_WIDTH : 0), + rows.get(i).getY() + rows.get(i).height - 1, + '\u251c', borderColor); + } + if ((columns.get(j).rightBorder == Border.SINGLE) + && (rows.get(i).bottomBorder == Border.SINGLE) + ) { + // Intersection of single bars + putCharXY(columns.get(j).getX() + columns.get(j).width, + rows.get(i).getY() + rows.get(i).height - 1, + '\u253c', borderColor); + } + if ((j == left) && (leftBorder == Border.SINGLE) + && (rows.get(i).bottomBorder == Border.DOUBLE) + ) { + // Left tee: single bar vertical, double bar horizontal + putCharXY((showRowLabels ? ROW_LABEL_WIDTH : 0), + rows.get(i).getY() + rows.get(i).height - 1, + '\u255e', borderColor); + } + if ((j == left) && (leftBorder == Border.SINGLE) + && (rows.get(i).bottomBorder == Border.THICK) + ) { + // Left tee: single bar vertical, thick bar horizontal + putCharXY((showRowLabels ? ROW_LABEL_WIDTH : 0), + rows.get(i).getY() + rows.get(i).height - 1, + '\u251d', borderColor); + } + if ((columns.get(j).rightBorder == Border.SINGLE) + && (rows.get(i).bottomBorder == Border.DOUBLE) + ) { + // Intersection: single bar vertical, double bar + // horizontal + putCharXY(columns.get(j).getX() + columns.get(j).width, + rows.get(i).getY() + rows.get(i).height - 1, + '\u256a', borderColor); + } + if ((columns.get(j).rightBorder == Border.SINGLE) + && (rows.get(i).bottomBorder == Border.THICK) + ) { + // Intersection: single bar vertical, thick bar + // horizontal + putCharXY(columns.get(j).getX() + columns.get(j).width, + rows.get(i).getY() + rows.get(i).height - 1, + '\u253f', borderColor); + } + } + } + // Now draw the window borders. super.draw(); } @@ -795,6 +1152,24 @@ public class TTableWidget extends TWidget { // TTable ----------------------------------------------------------------- // ------------------------------------------------------------------------ + /** + * Generate the default letter name for a column number. + * + * @param col column number to use for this column. Column 0 will be + * "A", column 1 will be "B", column 26 will be "AA", and so on. + */ + private String makeColumnLabel(int col) { + StringBuilder sb = new StringBuilder(); + for (;;) { + sb.append((char) ('A' + (col % 26))); + if (col < 26) { + break; + } + col /= 26; + } + return sb.reverse().toString(); + } + /** * Get the currently-selected cell. * @@ -901,241 +1276,702 @@ public class TTableWidget extends TWidget { return rows.size(); } + /** - * Get the full horizontal width of this table. - * - * @return the width required to render the entire table + * Push top and left to the bottom-most right corner of the available + * grid. */ - private int getMaximumWidth() { - int totalWidth = 0; + private void bottomRightCorner() { + int viewColumns = getWidth(); if (showRowLabels == true) { - // For now, all row labels are 8 cells wide. TODO: make this - // adjustable. - totalWidth += 8; - } - for (Cell cell: getSelectedRow().cells) { - totalWidth += cell.getWidth() + 1; + viewColumns -= ROW_LABEL_WIDTH; } - return totalWidth; + + // Set left and top such that the table stays on screen if possible. + top = rows.size() - getHeight(); + left = columns.size() - (getWidth() / (viewColumns / (COLUMN_DEFAULT_WIDTH + 1))); + // Now ensure the selection is visible. + alignGrid(); } /** - * Get the full vertical height of this table. - * - * @return the height required to render the entire table + * Align the grid so that the selected cell is fully visible. */ - private int getMaximumHeight() { - int totalHeight = 0; + private void alignGrid() { + int viewColumns = getWidth(); + if (showRowLabels == true) { + viewColumns -= ROW_LABEL_WIDTH; + } + if (leftBorder != Border.NONE) { + viewColumns--; + } + int viewRows = getHeight(); if (showColumnLabels == true) { - // For now, all column labels are 1 cell tall. TODO: make this - // adjustable. - totalHeight += 1; + viewRows -= COLUMN_LABEL_HEIGHT; } - for (Cell cell: getSelectedColumn().cells) { - totalHeight += cell.getHeight(); - // TODO: handle top/bottom borders. + if (topBorder != Border.NONE) { + viewRows--; } - return totalHeight; - } - /** - * Align the grid so that the selected cell is fully visible. - */ - private void alignGrid() { + // If we pushed left or right, adjust the box to include the new + // selected cell. + if (selectedColumn < left) { + left = selectedColumn - 1; + } + if (left < 0) { + left = 0; + } + if (selectedRow < top) { + top = selectedRow - 1; + } + if (top < 0) { + top = 0; + } /* - * We start by assuming that all cells are visible, and then mark as - * invisible those that are outside the viewable area. + * viewColumns and viewRows now contain the available columns and + * rows available to view the selected cell. We adjust left and top + * to ensure the selected cell is within view, and then make all + * cells outside the box between (left, top) and (right, bottom) + * invisible. + * + * We need to calculate right and bottom now. */ + int right = left; + + boolean done = false; + while (!done) { + int rightCellX = (showRowLabels ? ROW_LABEL_WIDTH : 0); + if (leftBorder != Border.NONE) { + rightCellX++; + } + int maxCellX = rightCellX + viewColumns; + right = left; + boolean selectedIsVisible = false; + int selectedX = 0; + for (int x = left; x < columns.size(); x++) { + if (x == selectedColumn) { + selectedX = rightCellX; + if (selectedX + columns.get(x).width + 1 <= maxCellX) { + selectedIsVisible = true; + } + } + rightCellX += columns.get(x).width + 1; + if (rightCellX >= maxCellX) { + break; + } + right++; + } + if (right < selectedColumn) { + // selectedColumn is outside the view range. Push left over, + // and calculate again. + left++; + } else if (left == selectedColumn) { + // selectedColumn doesn't fit inside the view range, but we + // can't go over any further either. Bail out. + done = true; + } else if (selectedIsVisible == false) { + // selectedColumn doesn't fit inside the view range, continue + // on. + left++; + } else { + // selectedColumn is fully visible, all done. + assert (selectedIsVisible == true); + done = true; + } + + } // while (!done) + + // We have the left/right range correct, set cell visibility and + // column X positions. + int leftCellX = showRowLabels ? ROW_LABEL_WIDTH : 0; + if (leftBorder != Border.NONE) { + leftCellX++; + } for (int x = 0; x < columns.size(); x++) { - for (int y = 0; y < rows.size(); y++) { - Cell cell = rows.get(y).cells.get(x); - cell.setVisible(true); - - // Special case: mouse double-clicks can lead to multiple - // cells in editing mode. Only allow a cell to remain - // editing if it is fact the active widget. - if (cell.isEditing && !cell.isActive()) { - cell.fieldText = cell.field.getText(); - cell.isEditing = false; - cell.field.setEnabled(false); + if ((x < left) || (x > right)) { + for (int i = 0; i < rows.size(); i++) { + columns.get(x).get(i).setVisible(false); + columns.get(x).setX(getWidth() + 1); } + continue; + } + for (int i = 0; i < rows.size(); i++) { + columns.get(x).get(i).setVisible(true); } + columns.get(x).setX(leftCellX); + leftCellX += columns.get(x).width + 1; } - // Adjust X locations to be visible ----------------------------------- + int bottom = top; - // Determine if we need to shift left or right. - int leftCellX = 0; - if (showRowLabels == true) { - // For now, all row labels are 8 cells wide. TODO: make this - // adjustable. - leftCellX += 8; - } - Row row = getSelectedRow(); - Cell selectedColumnCell = null; - for (int i = 0; i < row.cells.size(); i++) { - if (i == selectedColumn) { - selectedColumnCell = row.cells.get(i); - break; + done = false; + while (!done) { + int bottomCellY = (showColumnLabels ? COLUMN_LABEL_HEIGHT : 0); + if (topBorder != Border.NONE) { + bottomCellY++; } - leftCellX += row.cells.get(i).getWidth() + 1; - } - // There should always be a selected column. - assert (selectedColumnCell != null); + int maxCellY = bottomCellY + viewRows; + bottom = top; + for (int y = top; y < rows.size(); y++) { + bottomCellY += rows.get(y).height; + if (bottomCellY >= maxCellY) { + break; + } + bottom++; + } + if (bottom < selectedRow) { + // selectedRow is outside the view range. Push top down, and + // calculate again. + top++; + } else { + // selectedRow is inside the view range, done. + done = true; + } + } // while (!done) - int excessWidth = leftCellX + selectedColumnCell.getWidth() + 1 - getWidth(); - if (excessWidth > 0) { - leftCellX -= excessWidth; + // We have the top/bottom range correct, set cell visibility and + // row Y positions. + int topCellY = showColumnLabels ? COLUMN_LABEL_HEIGHT : 0; + if (topBorder != Border.NONE) { + topCellY++; } - if (leftCellX < 0) { - if (showRowLabels == true) { - leftCellX = 8; - } else { - leftCellX = 0; + for (int y = 0; y < rows.size(); y++) { + if ((y < top) || (y > bottom)) { + for (int i = 0; i < columns.size(); i++) { + rows.get(y).get(i).setVisible(false); + } + rows.get(y).setY(getHeight() + 1); + continue; } + for (int i = 0; i < columns.size(); i++) { + rows.get(y).get(i).setVisible(true); + } + rows.get(y).setY(topCellY); + topCellY += rows.get(y).height; } - /* - * leftCellX now contains the basic left offset necessary to draw the - * cells such that the selected cell (column) is fully visible within - * this widget's given width. Or, if the widget is too narrow to - * display the full cell, leftCellX is 0 or 8. - * - * Now reset all of the X positions of the other cells so that the - * selected cell X is leftCellX. - */ + // Last thing: cancel any edits that are not the selected cell. for (int y = 0; y < rows.size(); y++) { - // All cells to the left of selected cell. - int newCellX = leftCellX; - left = selectedColumn; - for (int x = selectedColumn - 1; x >= 0; x--) { - newCellX -= rows.get(y).cells.get(x).getWidth() + 1; - if (newCellX >= (showRowLabels ? 8 : 0)) { - rows.get(y).cells.get(x).setVisible(true); - rows.get(y).cells.get(x).setX(newCellX); - left--; - } else { - // This cell won't be visible. - rows.get(y).cells.get(x).setVisible(false); + for (int x = 0; x < columns.size(); x++) { + if ((x == selectedColumn) && (y == selectedRow)) { + continue; } + rows.get(y).get(x).cancelEdit(); } + } + } - // Selected cell. - rows.get(y).cells.get(selectedColumn).setX(leftCellX); - assert (rows.get(y).cells.get(selectedColumn).isVisible()); + /** + * Save contents to file. + * + * @param filename file to save to + * @throws IOException if a java.io operation throws + */ + public void saveToFilename(final String filename) throws IOException { + // TODO + } - // All cells to the right of selected cell. - newCellX = leftCellX + selectedColumnCell.getWidth() + 1; - for (int x = selectedColumn + 1; x < columns.size(); x++) { - if (newCellX <= getWidth()) { - rows.get(y).cells.get(x).setVisible(true); - rows.get(y).cells.get(x).setX(newCellX); - } else { - // This cell won't be visible. - rows.get(y).cells.get(x).setVisible(false); - } - newCellX += rows.get(y).cells.get(x).getWidth() + 1; - } + /** + * Set the selected cell location. + * + * @param column the selected cell location column + * @param row the selected cell location row + */ + public void setSelectedCell(final int column, final int row) { + if ((column < 0) || (column > columns.size() - 1)) { + throw new IndexOutOfBoundsException("Column count is " + + columns.size() + ", requested index " + column); + } + if ((row < 0) || (row > rows.size() - 1)) { + throw new IndexOutOfBoundsException("Row count is " + + rows.size() + ", requested index " + row); } + selectedColumn = column; + selectedRow = row; + alignGrid(); + } - // Adjust Y locations to be visible ----------------------------------- - // The same logic as above, but applied to the column Y. + /** + * Get a particular cell. + * + * @param column the cell column + * @param row the cell row + * @return the cell + */ + public Cell getCell(final int column, final int row) { + if ((column < 0) || (column > columns.size() - 1)) { + throw new IndexOutOfBoundsException("Column count is " + + columns.size() + ", requested index " + column); + } + if ((row < 0) || (row > rows.size() - 1)) { + throw new IndexOutOfBoundsException("Row count is " + + rows.size() + ", requested index " + row); + } + return rows.get(row).get(column); + } - // Determine if we need to shift up or down. - int topCellY = 0; - if (showColumnLabels == true) { - // For now, all column labels are 1 cell high. TODO: make this - // adjustable. - topCellY += 1; - } - Column column = getSelectedColumn(); - Cell selectedRowCell = null; - for (int i = 0; i < column.cells.size(); i++) { - if (i == selectedRow) { - selectedRowCell = column.cells.get(i); - break; - } - topCellY += column.cells.get(i).getHeight(); - // TODO: if a border is selected, add 1 to topCellY. + /** + * Get the text of a particular cell. + * + * @param column the cell column + * @param row the cell row + * @return the text in the cell + */ + public String getCellText(final int column, final int row) { + if ((column < 0) || (column > columns.size() - 1)) { + throw new IndexOutOfBoundsException("Column count is " + + columns.size() + ", requested index " + column); + } + if ((row < 0) || (row > rows.size() - 1)) { + throw new IndexOutOfBoundsException("Row count is " + + rows.size() + ", requested index " + row); } - // There should always be a selected row. - assert (selectedRowCell != null); + return rows.get(row).get(column).getText(); + } - int excessHeight = topCellY + selectedRowCell.getHeight() - getHeight() - 1; - if (showColumnLabels == true) { - excessHeight += 1; + /** + * Set the text of a particular cell. + * + * @param column the cell column + * @param row the cell row + * @param text the text to put into the cell + */ + public void setCellText(final int column, final int row, + final String text) { + + if ((column < 0) || (column > columns.size() - 1)) { + throw new IndexOutOfBoundsException("Column count is " + + columns.size() + ", requested index " + column); } - if (excessHeight > 0) { - topCellY -= excessHeight; + if ((row < 0) || (row > rows.size() - 1)) { + throw new IndexOutOfBoundsException("Row count is " + + rows.size() + ", requested index " + row); } - if (topCellY < 0) { - if (showColumnLabels == true) { - topCellY = 1; - } else { - topCellY = 0; - } + rows.get(row).get(column).setText(text); + } + + /** + * Set the action to perform when the user presses enter on a particular + * cell. + * + * @param column the cell column + * @param row the cell row + * @param action the action to perform when the user presses enter on the + * cell + */ + public void setCellEnterAction(final int column, final int row, + final TAction action) { + + if ((column < 0) || (column > columns.size() - 1)) { + throw new IndexOutOfBoundsException("Column count is " + + columns.size() + ", requested index " + column); } + if ((row < 0) || (row > rows.size() - 1)) { + throw new IndexOutOfBoundsException("Row count is " + + rows.size() + ", requested index " + row); + } + rows.get(row).get(column).field.setEnterAction(action); + } + + /** + * Set the action to perform when the user updates a particular cell. + * + * @param column the cell column + * @param row the cell row + * @param action the action to perform when the user updates the cell + */ + public void setCellUpdateAction(final int column, final int row, + final TAction action) { + + if ((column < 0) || (column > columns.size() - 1)) { + throw new IndexOutOfBoundsException("Column count is " + + columns.size() + ", requested index " + column); + } + if ((row < 0) || (row > rows.size() - 1)) { + throw new IndexOutOfBoundsException("Row count is " + + rows.size() + ", requested index " + row); + } + rows.get(row).get(column).field.setUpdateAction(action); + } + + /** + * Get the width of a column. + * + * @param column the column number + * @return the width of the column + */ + public int getColumnWidth(final int column) { + if ((column < 0) || (column > columns.size() - 1)) { + throw new IndexOutOfBoundsException("Column count is " + + columns.size() + ", requested index " + column); + } + return columns.get(column).width; + } + + /** + * Set the width of a column. + * + * @param column the column number + * @param width the new width of the column + */ + public void setColumnWidth(final int column, final int width) { + if ((column < 0) || (column > columns.size() - 1)) { + throw new IndexOutOfBoundsException("Column count is " + + columns.size() + ", requested index " + column); + } + columns.get(column).width = width; + } + + /** + * Get the label of a column. + * + * @param column the column number + * @return the label of the column + */ + public String getColumnLabel(final int column) { + if ((column < 0) || (column > columns.size() - 1)) { + throw new IndexOutOfBoundsException("Column count is " + + columns.size() + ", requested index " + column); + } + return columns.get(column).label; + } + + /** + * Set the label of a column. + * + * @param column the column number + * @param label the new label of the column + */ + public void setColumnLabel(final int column, final String label) { + if ((column < 0) || (column > columns.size() - 1)) { + throw new IndexOutOfBoundsException("Column count is " + + columns.size() + ", requested index " + column); + } + columns.get(column).label = label; + } + + /** + * Get the label of a row. + * + * @param row the row number + * @return the label of the row + */ + public String getRowLabel(final int row) { + if ((row < 0) || (row > rows.size() - 1)) { + throw new IndexOutOfBoundsException("Row count is " + + rows.size() + ", requested index " + row); + } + return rows.get(row).label; + } + + /** + * Set the label of a row. + * + * @param row the row number + * @param label the new label of the row + */ + public void setRowLabel(final int row, final String label) { + if ((row < 0) || (row > rows.size() - 1)) { + throw new IndexOutOfBoundsException("Row count is " + + rows.size() + ", requested index " + row); + } + rows.get(row).label = label; + } + + /** + * Insert one row at a particular index. + * + * @param idx the row number + */ + private void insertRowAt(final int idx) { + Row newRow = new Row(idx); + for (int i = 0; i < columns.size(); i++) { + Cell cell = new Cell(this, columns.get(i).getX(), + rows.get(selectedRow).getY(), COLUMN_DEFAULT_WIDTH, 1, i, idx); + newRow.add(cell); + columns.get(i).cells.add(idx, cell); + } + rows.add(idx, newRow); - /* - * topCellY now contains the basic top offset necessary to draw the - * cells such that the selected cell (row) is fully visible within - * this widget's given height. Or, if the widget is too short to - * display the full cell, topCellY is 0 or 1. - * - * Now reset all of the Y positions of the other cells so that the - * selected cell Y is topCellY. - */ for (int x = 0; x < columns.size(); x++) { + for (int y = idx; y < rows.size(); y++) { + columns.get(x).get(y).row = y; + columns.get(x).get(y).column = x; + } + } + for (int i = idx + 1; i < rows.size(); i++) { + String oldRowLabel = Integer.toString(i - 1); + if (rows.get(i).label.equals(oldRowLabel)) { + rows.get(i).label = Integer.toString(i); + } + } + alignGrid(); + } - if (columns.get(x).get(0).isVisible() == false) { - // This column won't be visible as determined by the checks - // above, just continue to the next. - continue; + /** + * Insert one row above a particular row. + * + * @param row the row number + */ + public void insertRowAbove(final int row) { + if ((row < 0) || (row > rows.size() - 1)) { + throw new IndexOutOfBoundsException("Row count is " + + rows.size() + ", requested index " + row); + } + insertRowAt(selectedRow); + selectedRow++; + } + + /** + * Insert one row below a particular row. + * + * @param row the row number + */ + public void insertRowBelow(final int row) { + if ((row < 0) || (row > rows.size() - 1)) { + throw new IndexOutOfBoundsException("Row count is " + + rows.size() + ", requested index " + row); + } + int idx = selectedRow + 1; + if (idx < rows.size()) { + insertRowAt(idx); + return; + } + + // selectedRow is the last row, we need to perform an append. + Row newRow = new Row(idx); + for (int i = 0; i < columns.size(); i++) { + Cell cell = new Cell(this, columns.get(i).getX(), + rows.get(selectedRow).getY(), COLUMN_DEFAULT_WIDTH, 1, i, idx); + newRow.add(cell); + columns.get(i).cells.add(cell); + } + rows.add(newRow); + alignGrid(); + } + + /** + * Delete a particular row. + * + * @param row the row number + */ + public void deleteRow(final int row) { + if ((row < 0) || (row > rows.size() - 1)) { + throw new IndexOutOfBoundsException("Row count is " + + rows.size() + ", requested index " + row); + } + if (rows.size() == 1) { + // Don't delete the last row. + return; + } + for (int i = 0; i < columns.size(); i++) { + Cell cell = columns.get(i).cells.remove(row); + getChildren().remove(cell); + } + rows.remove(row); + + for (int x = 0; x < columns.size(); x++) { + for (int y = row; y < rows.size(); y++) { + columns.get(x).get(y).row = y; + columns.get(x).get(y).column = x; + } + } + for (int i = row; i < rows.size(); i++) { + String oldRowLabel = Integer.toString(i + 1); + if (rows.get(i).label.equals(oldRowLabel)) { + rows.get(i).label = Integer.toString(i); } + } + if (selectedRow == rows.size()) { + selectedRow--; + } + bottomRightCorner(); + } - // All cells above the selected cell. - int newCellY = topCellY; - top = selectedRow; - for (int y = selectedRow - 1; y >= 0; y--) { - newCellY -= rows.get(y).cells.get(x).getHeight(); - if (newCellY >= (showColumnLabels == true ? 1 : 0)) { - rows.get(y).cells.get(x).setVisible(true); - rows.get(y).cells.get(x).setY(newCellY); - top--; - } else { - // This cell won't be visible. - rows.get(y).cells.get(x).setVisible(false); - } + /** + * Insert one column at a particular index. + * + * @param idx the column number + */ + private void insertColumnAt(final int idx) { + Column newColumn = new Column(idx); + for (int i = 0; i < rows.size(); i++) { + Cell cell = new Cell(this, columns.get(selectedColumn).getX(), + rows.get(i).getY(), COLUMN_DEFAULT_WIDTH, 1, idx, i); + newColumn.add(cell); + rows.get(i).cells.add(idx, cell); + } + columns.add(idx, newColumn); + + for (int x = idx; x < columns.size(); x++) { + for (int y = 0; y < rows.size(); y++) { + columns.get(x).get(y).row = y; + columns.get(x).get(y).column = x; } + } + for (int i = idx + 1; i < columns.size(); i++) { + String oldColumnLabel = makeColumnLabel(i - 1); + if (columns.get(i).label.equals(oldColumnLabel)) { + columns.get(i).label = makeColumnLabel(i); + } + } + alignGrid(); + } - // Selected cell. - columns.get(x).cells.get(selectedRow).setY(topCellY); + /** + * Insert one column to the left of a particular column. + * + * @param column the column number + */ + public void insertColumnLeft(final int column) { + if ((column < 0) || (column > columns.size() - 1)) { + throw new IndexOutOfBoundsException("Column count is " + + columns.size() + ", requested index " + column); + } + insertColumnAt(selectedColumn); + selectedColumn++; + } - // All cells below the selected cell. - newCellY = topCellY + selectedRowCell.getHeight(); - for (int y = selectedRow + 1; y < rows.size(); y++) { - if (newCellY <= getHeight()) { - rows.get(y).cells.get(x).setVisible(true); - rows.get(y).cells.get(x).setY(newCellY); - } else { - // This cell won't be visible. - rows.get(y).cells.get(x).setVisible(false); - } - newCellY += rows.get(y).cells.get(x).getHeight(); + /** + * Insert one column to the right of a particular column. + * + * @param column the column number + */ + public void insertColumnRight(final int column) { + if ((column < 0) || (column > columns.size() - 1)) { + throw new IndexOutOfBoundsException("Column count is " + + columns.size() + ", requested index " + column); + } + int idx = selectedColumn + 1; + if (idx < columns.size()) { + insertColumnAt(idx); + return; + } + + // selectedColumn is the last column, we need to perform an append. + Column newColumn = new Column(idx); + for (int i = 0; i < rows.size(); i++) { + Cell cell = new Cell(this, columns.get(selectedColumn).getX(), + rows.get(i).getY(), COLUMN_DEFAULT_WIDTH, 1, idx, i); + newColumn.add(cell); + rows.get(i).cells.add(cell); + } + columns.add(newColumn); + alignGrid(); + } + + /** + * Delete a particular column. + * + * @param column the column number + */ + public void deleteColumn(final int column) { + if ((column < 0) || (column > columns.size() - 1)) { + throw new IndexOutOfBoundsException("Column count is " + + columns.size() + ", requested index " + column); + } + if (columns.size() == 1) { + // Don't delete the last column. + return; + } + for (int i = 0; i < rows.size(); i++) { + Cell cell = rows.get(i).cells.remove(column); + getChildren().remove(cell); + } + columns.remove(column); + + for (int x = column; x < columns.size(); x++) { + for (int y = 0; y < rows.size(); y++) { + columns.get(x).get(y).row = y; + columns.get(x).get(y).column = x; + } + } + for (int i = column; i < columns.size(); i++) { + String oldColumnLabel = makeColumnLabel(i + 1); + if (columns.get(i).label.equals(oldColumnLabel)) { + columns.get(i).label = makeColumnLabel(i); } } + if (selectedColumn == columns.size()) { + selectedColumn--; + } + bottomRightCorner(); + } + + /** + * Delete the selected cell, shifting cells over to the left. + */ + public void deleteCellShiftLeft() { + // All we do is copy the text from every cell in this row over. + for (int i = selectedColumn + 1; i < columns.size(); i++) { + setCellText(i - 1, selectedRow, getCellText(i, selectedRow)); + } + setCellText(columns.size() - 1, selectedRow, ""); + } + /** + * Delete the selected cell, shifting cells from below up. + */ + public void deleteCellShiftUp() { + // All we do is copy the text from every cell in this column up. + for (int i = selectedRow + 1; i < rows.size(); i++) { + setCellText(selectedColumn, i - 1, getCellText(selectedColumn, i)); + } + setCellText(selectedColumn, rows.size() - 1, ""); } /** - * Save contents to file. + * Set a particular cell read-only (non-editable) or not. * - * @param filename file to save to - * @throws IOException if a java.io operation throws + * @param column the cell column + * @param row the cell row + * @param readOnly if true, the cell will be non-editable */ - public void saveToFilename(final String filename) throws IOException { - // TODO + public void setCellReadOnly(final int column, final int row, + final boolean readOnly) { + + if ((column < 0) || (column > columns.size() - 1)) { + throw new IndexOutOfBoundsException("Column count is " + + columns.size() + ", requested index " + column); + } + if ((row < 0) || (row > rows.size() - 1)) { + throw new IndexOutOfBoundsException("Row count is " + + rows.size() + ", requested index " + row); + } + rows.get(row).get(column).setReadOnly(readOnly); + } + + /** + * Set an entire row of cells read-only (non-editable) or not. + * + * @param row the row number + * @param readOnly if true, the cells will be non-editable + */ + public void setRowReadOnly(final int row, final boolean readOnly) { + if ((row < 0) || (row > rows.size() - 1)) { + throw new IndexOutOfBoundsException("Row count is " + + rows.size() + ", requested index " + row); + } + for (Cell cell: rows.get(row).cells) { + cell.setReadOnly(readOnly); + } + } + + /** + * Set an entire column of cells read-only (non-editable) or not. + * + * @param column the column number + * @param readOnly if true, the cells will be non-editable + */ + public void setColumnReadOnly(final int column, final boolean readOnly) { + if ((column < 0) || (column > columns.size() - 1)) { + throw new IndexOutOfBoundsException("Column count is " + + columns.size() + ", requested index " + column); + } + for (Cell cell: columns.get(column).cells) { + cell.setReadOnly(readOnly); + } } }