/*******************************************************************************
* Copyright (c) 2014 Open Door Logistics (www.opendoorlogistics.com)
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Lesser Public License v3
* which accompanies this distribution, and is available at http://www.gnu.org/licenses/lgpl.txt
******************************************************************************/
package com.opendoorlogistics.studio.tables.grid;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.FocusEvent;
import java.util.*;
import javax.swing.JTable;
import javax.swing.JViewport;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.TableModelEvent;
final public class SelectionManager {
private final GridTable table;
private final boolean forceRowSelection;
private Point focusPoint;
private SelectionState selectionState = SelectionState.SHEET;
private OneDimensionalSelection rowSelection = new OneDimensionalSelection();
private OneDimensionalSelection colSelection = new OneDimensionalSelection();
// store by row, col
private TreeMap<Integer, TreeSet<Integer>> selectedCells = new TreeMap<>();
private boolean isExt;
private Point extAnchorPoint;
private Point extStretchPoint;
private enum SelectionState {
ROW, SHEET, COL
}
private static class OneDimensionalSelection {
private TreeSet<Integer> selected = new TreeSet<>();
private boolean isExt;
private Integer extAnchor;
private Integer extStretch;
private Integer focus;
private int[] getRowRangeContainingSelectedCells() {
int ret[] = new int[2];
ret[0] = Integer.MAX_VALUE;
ret[1] = Integer.MIN_VALUE;
if (selected.size() > 0) {
ret[0] = selected.first();
ret[1] = selected.last();
}
int[] ext = getExtRange();
if (ext != null) {
ret[0] = Math.min(ret[0], ext[0]);
ret[1] = Math.max(ret[1], ext[1]);
}
return ret;
}
private void focusLost(FocusEvent e) {
focus = null;
}
private TreeSet<Integer> getSelected() {
TreeSet<Integer> ret = new TreeSet<>(selected);
int[] ext = getExtRange();
if (ext != null) {
for (int i = ext[0]; i <= ext[1]; i++) {
ret.add(i);
}
}
return ret;
}
private void add(int i) {
selected.add(i);
}
private boolean isSelected(int i) {
if (selected.contains(i)) {
return true;
}
int[] ext = getExtRange();
if (ext != null) {
if (ext[0] <= i && i <= ext[1]) {
return true;
}
}
return false;
}
private int[] getExtRange() {
if (isExt) {
int min = Math.min(extAnchor, extStretch);
int max = Math.max(extAnchor, extStretch);
return new int[] { min, max };
}
return null;
}
private void clearSelection() {
extAnchor = null;
extStretch = null;
selected.clear();
isExt = false;
}
void changeSelection(int i, boolean toggle, boolean extend) {
// update focus point
Integer oldFocus = focus;
focus = i;
// update the stretch point if still in extend
if (extend && isExt) {
extStretch = focus;
}
// check for entering extend; only set the anchor point then
if (!isExt && extend) {
extAnchor = oldFocus != null ? oldFocus : focus;
extStretch = focus;
isExt = true;
}
// process if not in extend
if (!extend) {
if (!toggle) {
selected.clear();
}
add(focus);
}
}
void checkForLeavingExt(boolean toggle, boolean extend) {
// check for leaving row extend.. add to extended region to selection if control pressed
if (!extend && isExt) {
if (toggle) {
int[] ext = getExtRange();
for (int j = ext[0]; j <= ext[1]; j++) {
add(j);
}
}
isExt = false;
extAnchor = null;
extStretch = null;
}
}
}
public SelectionManager(GridTable table, boolean forceRowSelection) {
this.table = table;
this.forceRowSelection = forceRowSelection;
if(forceRowSelection){
selectionState = SelectionState.ROW;
}
}
private int[] getRowRangeContainingSelectedCells() {
int ret[] = new int[2];
ret[0] = Integer.MAX_VALUE;
ret[1] = Integer.MIN_VALUE;
switch (selectionState) {
case SHEET:
if (selectedCells.size() > 0) {
ret[0] = selectedCells.firstKey();
ret[1] = selectedCells.lastKey();
}
Rectangle ext = getExtendRectangle();
if (ext != null) {
ret[0] = Math.min(ret[0], ext.y);
ret[1] = Math.max(ret[1], ext.y + ext.height - 1);
}
break;
case ROW:
return rowSelection.getRowRangeContainingSelectedCells();
case COL:
int lastRow = table.getLastFilledRowNumber();
if (lastRow >= 0) {
ret[0] = 0;
ret[1] = lastRow;
}
break;
default:
break;
}
return ret;
}
void focusLost(FocusEvent e) {
focusPoint = null;
rowSelection.focusLost(e);
}
boolean isFocused(int row, int col) {
return focusPoint != null && focusPoint.y == row && focusPoint.x == col;
}
private static List<Point> getPoints(Rectangle rectangle) {
ArrayList<Point> ret = new ArrayList<>();
for (int x = rectangle.x; x < rectangle.x + rectangle.width; x++) {
for (int y = rectangle.y; y < rectangle.y + rectangle.height; y++) {
ret.add(new Point(x, y));
}
}
return ret;
}
private static List<Point> getPoints(TreeMap<Integer, TreeSet<Integer>> map) {
ArrayList<Point> ret = new ArrayList<>();
for (Map.Entry<Integer, TreeSet<Integer>> entry : map.entrySet()) {
int row = entry.getKey();
for (int col : entry.getValue()) {
ret.add(new Point(col, row));
}
}
return ret;
}
/**
* Get columns with one or more selected cells or which are selected
* themselves in the column header. Columns are returned in ascending order.
* @return
*/
List<Integer> getSelectedColumns(){
TreeSet<Integer> tmp = new TreeSet<>();
if(selectionState == SelectionState.COL){
tmp = colSelection.getSelected();
}else{
for(Point point : PasteLogic.toSingleList(getSelectedPoints()) ){
tmp.add(point.x);
}
}
return new ArrayList<>(tmp);
}
List<List<Point>> getSelectedPoints() {
ArrayList<List<Point>> ret = new ArrayList<>();
switch (selectionState) {
case ROW:
int nbCols = table.getModel().getColumnCount();
for (int row : rowSelection.getSelected()) {
ArrayList<Point> pointsRow = new ArrayList<>();
for (int i = 1; i < nbCols; i++) {
pointsRow.add(new Point(i, row));
}
if (pointsRow.size() > 0) {
ret.add(pointsRow);
}
}
break;
case COL:
int lastRow = table.getLastFilledRowNumber();
for (int row = 0; row <= lastRow; row++) {
ArrayList<Point> pointsRow = new ArrayList<>();
for (int col : colSelection.getSelected()) {
pointsRow.add(new Point(col, row));
}
if (pointsRow.size() > 0) {
ret.add(pointsRow);
}
}
break;
case SHEET:
// copy selected
TreeMap<Integer, TreeSet<Integer>> tmpMap = new TreeMap<>();
addPoints(getPoints(selectedCells), tmpMap);
// add the extended rectangle
Rectangle rect = getExtendRectangle();
if (rect != null) {
addPoints(getPoints(rect), tmpMap);
}
// read out by row
for (Map.Entry<Integer, TreeSet<Integer>> row : tmpMap.entrySet()) {
ArrayList<Point> rowList = new ArrayList<>();
ret.add(rowList);
for (int col : row.getValue()) {
rowList.add(new Point(col, row.getKey()));
}
}
break;
default:
break;
}
return ret;
}
private Rectangle getExtendRectangle() {
if (!isExt) {
return null;
}
Point min = new Point(Math.min(extAnchorPoint.x, extStretchPoint.x), Math.min(extAnchorPoint.y, extStretchPoint.y));
Point max = new Point(Math.max(extAnchorPoint.x, extStretchPoint.x), Math.max(extAnchorPoint.y, extStretchPoint.y));
return new Rectangle(min.x, min.y, max.x - min.x + 1, max.y - min.y + 1);
}
private static void addPointMatrix(List<List<Point>> points, TreeMap<Integer, TreeSet<Integer>> selected) {
for (List<Point> row : points) {
addPoints(row, selected);
}
}
private static void addPoints(Iterable<Point> points, TreeMap<Integer, TreeSet<Integer>> selected) {
for (Point point : points) {
addPoint(point, selected);
}
}
private static void addPoint(Point point, TreeMap<Integer, TreeSet<Integer>> selected) {
TreeSet<Integer> row = selected.get(point.y);
if (row == null) {
row = new TreeSet<>();
selected.put(point.y, row);
}
row.add(point.x);
}
void setSelectedCell(int row, int col) {
clearSelection();
addPoint(new Point(col, row), selectedCells);
}
void setSelectedCells(Iterable<Point> points) {
clearSelection();
addPoints(points, selectedCells);
}
private void changeState(SelectionState newState, boolean toggle) {
if (toggle) {
// keep everything currently selected
addPointMatrix(getSelectedPoints(), selectedCells);
} else {
// clear everything
clearSelection();
}
if(forceRowSelection){
selectionState = SelectionState.ROW;
}
else{
selectionState = newState;
}
}
void changeSelection(int rowIndex, int columnIndex, boolean toggle, boolean extend) {
// if we're in force row selection mode then ignore column selection events
if(forceRowSelection && (rowIndex==-1 && columnIndex!=-1)){
return;
}
// get selected row range
int[] oldSelRowRange = getRowRangeContainingSelectedCells();
// check for leaving extension state first of all as we don't enter other states if still extending
switch (selectionState) {
case SHEET:
checkForLeavingExt(toggle, extend);
break;
case ROW:
rowSelection.checkForLeavingExt(toggle, extend);
break;
case COL:
colSelection.checkForLeavingExt(toggle, extend);
break;
}
// leave sheet state?
if (selectionState == SelectionState.SHEET && !isExt) {
if (columnIndex == 0 && rowIndex >= 0) {
// enter row state from sheet state
changeState(SelectionState.ROW, toggle);
}
if (rowIndex == -1 && columnIndex > 0) {
// enter column state from sheet state
changeState(SelectionState.COL, toggle);
}
}
// leave row state?
if (selectionState == SelectionState.ROW && !rowSelection.isExt) {
if (columnIndex > 0) {
if (rowIndex >= 0) {
// enter sheet state from row state
changeState(SelectionState.SHEET, toggle);
} else {
// enter col state from row state
changeState(SelectionState.COL, toggle);
}
rowSelection.clearSelection();
}
}
// leave col state?
boolean leftHeader=false;
if (selectionState == SelectionState.COL && !colSelection.isExt) {
if (rowIndex >= 0) {
if (columnIndex > 0) {
// enter sheet state from col state
changeState(SelectionState.SHEET, toggle);
} else {
// enter row state from col state
changeState(SelectionState.ROW, toggle);
}
colSelection.clearSelection();
leftHeader= true;
}
}
// update focus point
Point oldFocus = focusPoint;
focusPoint = new Point(Math.max(columnIndex, 1),Math.max(rowIndex,0));
if (selectionState == SelectionState.SHEET) {
// update the stretch point if still in extend
if (isExt) {
extStretchPoint = new Point(focusPoint);
}
// check for entering extend; only set the anchor point then
if (!isExt && extend && columnIndex > 0 && rowIndex >= 0) {
extAnchorPoint = oldFocus != null ? new Point(oldFocus) : new Point(focusPoint);
extStretchPoint = focusPoint;
isExt = true;
}
// process if not in extend
if (!extend && columnIndex > 0 && rowIndex >= 0) {
if (!toggle) {
selectedCells.clear();
}
addPoint(focusPoint, selectedCells);
}
} else if (selectionState == SelectionState.ROW) {
rowSelection.changeSelection(Math.max(rowIndex,0), toggle, extend);
}
else{
colSelection.changeSelection(Math.max(columnIndex,1), toggle, extend);
}
// get new range of selected rows and then flag to repaint anything within the union of old and new range
int[] newSelRowRange = getRowRangeContainingSelectedCells();
int[] potentialChangeRange = new int[] { Math.min(oldSelRowRange[0], newSelRowRange[0]), Math.max(oldSelRowRange[1], newSelRowRange[1]) };
if (potentialChangeRange[0] <= potentialChangeRange[1]) {
// its a valid interval
table.valueChanged(new ListSelectionEvent(this, potentialChangeRange[0], potentialChangeRange[1], false));
}
// also check for focus change
if (oldFocus != null && oldFocus.equals(focusPoint) == false) {
table.tableChanged(new TableModelEvent(table.getModel(), oldFocus.y, oldFocus.y));
}
// and for header needing repainting
if(leftHeader){
table.getTableHeader().repaint();
}
}
private void checkForLeavingExt(boolean toggle, boolean extend) {
if (!extend && isExt) {
if (toggle) {
addPoints(getPoints(getExtendRectangle()), selectedCells);
}
isExt = false;
extAnchorPoint = null;
extStretchPoint = null;
}
}
boolean isRowSelected(int row) {
switch (selectionState) {
case SHEET:
return false;
case ROW:
return rowSelection.isSelected(row);
case COL:
return row <= table.getLastFilledRowNumber();
}
return false;
}
boolean isCellSelected(int row, int column) {
switch (selectionState) {
case ROW:
return rowSelection.isSelected(row);
case SHEET:
TreeSet<Integer> rowSet = selectedCells.get(row);
if (rowSet != null && rowSet.contains(column)) {
return true;
}
Rectangle ext = getExtendRectangle();
if (ext != null && ext.contains(column, row)) {
return true;
}
break;
case COL:
return row <= table.getLastFilledRowNumber() && colSelection.isSelected(column);
}
return false;
}
void clearSelection() {
selectionState =forceRowSelection? SelectionState.ROW: SelectionState.SHEET;
isExt = false;
extAnchorPoint = null;
extStretchPoint = null;
selectedCells.clear();
rowSelection.clearSelection();
colSelection.clearSelection();
table.repaint();
table.getTableHeader().repaint();
}
Point getFocusPoint() {
if (focusPoint != null) {
return new Point(focusPoint);
}
return null;
}
void setFocusPoint(Point newPoint) {
// don't allow the dummy column to be selected
if (newPoint.x == 0) {
newPoint.x = 1;
}
Point oldFocusPoint = focusPoint;
focusPoint = newPoint;
int minChangedRow = newPoint.y;
int maxChangedRow = newPoint.y;
if (oldFocusPoint != null && focusPoint.equals(oldFocusPoint) == false) {
// focus point has changed
minChangedRow = Math.min(minChangedRow, oldFocusPoint.y);
maxChangedRow = Math.max(maxChangedRow, oldFocusPoint.y);
}
// fire listeners *after* focus state has changed
table.tableChanged(new TableModelEvent(table.getModel(), minChangedRow, maxChangedRow));
scrollToVisible(table, focusPoint.y, focusPoint.x);
}
/**
* See http://stackoverflow.com/questions/853020/jtable-scrolling-to-a-specified-row-index
*
* @param table
* @param rowIndex
* @param vColIndex
*/
private static void scrollToVisible(JTable table, int rowIndex, int vColIndex) {
if (!(table.getParent() instanceof JViewport)) {
return;
}
JViewport viewport = (JViewport) table.getParent();
// This rectangle is relative to the table where the
// northwest corner of cell (0,0) is always (0,0).
Rectangle rect = table.getCellRect(rowIndex, vColIndex, true);
// The location of the viewport relative to the table
Point pt = viewport.getViewPosition();
// Translate the cell location so that it is relative
// to the view, assuming the northwest corner of the view is (0,0)
rect.setLocation(rect.x - pt.x, rect.y - pt.y);
// Scroll the area into view
viewport.scrollRectToVisible(rect);
}
}