package org.geogebra.common.gui.view.spreadsheet;
import java.util.Arrays;
import java.util.Comparator;
import java.util.TreeSet;
import org.geogebra.common.kernel.Construction;
import org.geogebra.common.kernel.Kernel;
import org.geogebra.common.kernel.geos.GeoElement;
import org.geogebra.common.kernel.kernelND.GeoElementND;
import org.geogebra.common.main.App;
import org.geogebra.common.main.SpreadsheetTableModel;
import org.geogebra.common.plugin.EventType;
public abstract class CopyPasteCut {
// ggb support classes
protected Kernel kernel;
protected App app;
private SpreadsheetTableModel tableModel;
private SpreadsheetViewInterface view;
private MyTableInterface table;
/**
* Stores copied cell geo values as a tab-delimited string.
*/
private StringBuilder cellBufferStr;
/**
* Stores copied cell geos as GeoElement[columns][rows]
*/
private GeoElement[][] cellBufferGeo;
/**
* Records the first row of the current cell range copy source
*/
protected int sourceColumn1;
/**
* Records the first column of the current cell range copy source
*/
protected int sourceRow1;
/**
* Stores construction index values while performing a paste
*/
private Object[] constructionIndexes;
/***************************************
* Constructor
*/
public CopyPasteCut(App app) {
tableModel = app.getSpreadsheetTableModel();
this.app = app;
kernel = app.getKernel();
}
private SpreadsheetViewInterface getView() {
if (view == null) {
view = app.getGuiManager()
.getSpreadsheetView();
}
return view;
}
protected MyTableInterface getTable() {
if (table == null) {
table = getView().getSpreadsheetTable();
}
return table;
}
/**
* Combines the GeoElement.toValueStrings from a given block of cell geos
* into a single tab-delimited string. This string is stored in (1) the
* global String field cellBufferStr and (2) the system clipboard.
*
* If skipGeoCopy = false, the geos are also stored in the global
* GeoElement[][] field cellBufferGeo
*
* The cell block is defined by upper-left corner (column1, row1) and lower
* left corner (column2, row2)
*
* @param column1
* @param row1
* @param column2
* @param row2
* @param skipGeoCopy
*/
abstract public void copy(int column1, int row1, int column2, int row2,
boolean skipGeoCopy);
/**
* Copies the contents of the cell block defined by upper-left corner
* (column1, row1) and lower left corner (column2, row2) into the system
* clipboard and then deletes these geos.
*
* TODO: The external buffer is nulled out so that a followup paste will not
* perform a relative copy. This needs to be fixed, relative copy is
* expected by the user.
*
* @param column1
* @param row1
* @param column2
* @param row2
* @return
*/
public boolean cut(int column1, int row1, int column2, int row2) {
copy(column1, row1, column2, row2, false);
// null out the external buffer so that paste will not do a relative
// copy
setCellBufferStr(null);
return delete(column1, row1, column2, row2);
}
/**
* Pastes data from the clipboard into the given spreadsheet cell range.
*
* @param cr
* the target cell range
* @return
*/
public boolean paste(CellRange cr) {
return paste(cr.getMinColumn(), cr.getMinRow(), cr.getMaxColumn(),
cr.getMaxRow());
}
/**
* Pastes data from the clipboard into the given spreadsheet cells.
*
* @param column1
* first column of the target cell range
* @param row1
* first row of the target cell range
* @param column2
* last column of the target cell range
* @param row2
* last row of the target cell range
* @return
*/
abstract public boolean paste(int column1, int row1, int column2, int row2);
/**
* Copies geos from the field cellBufferGeo and then pastes (renames) them
* into the given target cell range using relative cell references in their
* definitions. The data may be pasted multiple times to fill in the target
* rectangle (and maybe overflow a bit)
*
* @param column1
* first column of the target cell range
* @param row1
* first row of the target cell range
* @param column2
* last column of the target cell range
* @param row2
* last row of the target cell range
* @return
*/
public boolean pasteInternalMultiple(int column1, int row1, int column2,
int row2) {
boolean succ = true;
Construction cons = kernel.getConstruction();
try {
int columnStep = getCellBufferGeo().length;
int rowStep = getCellBufferGeo()[0].length;
int maxColumn = column2;
int maxRow = row2;
// paste all data if just one cell selected
// ie overflow selection rectangle
if (row2 == row1 && column2 == column1) {
maxColumn = column1 + columnStep;
maxRow = row1 + rowStep;
}
// collect all redefine operations
cons.startCollectingRedefineCalls();
// paste data multiple times to fill in the selection rectangle (and
// maybe overflow a bit)
for (int c = column1; c <= column2; c += columnStep) {
for (int r = row1; r <= row2; r += rowStep) {
succ = succ && pasteInternal(c, r, maxColumn, maxRow);
}
}
// now do all redefining and build new construction
cons.processCollectedRedefineCalls();
} catch (Exception ex) {
ex.printStackTrace(System.out);
app.showError(ex.getMessage());
} finally {
cons.stopCollectingRedefineCalls();
app.setDefaultCursor();
}
return succ;
}
/**
* Creates copies of the geos stored in the global field cellBufferGeo. The
* copied values are named as spreadsheet cells corresponding to the given
* target cell range and the original source cell locations. Relative cell
* references are then applied to match the location of these new geos.
*
* The target cell range is defined by upper left corner (column1, row1) and
* lower right corner (maxColumn, maxRow).
*
* @param column1
* minimum target column
* @param row1
* minimum target row
* @param maxColumn
* maximum target column
* @param maxRow
* maximum target row
* @return
* @throws Exception
*/
public boolean pasteInternal(int column1, int row1, int maxColumn,
int maxRow) throws Exception {
int width = getCellBufferGeo().length;
if (width == 0) {
return false;
}
int height = getCellBufferGeo()[0].length;
if (height == 0) {
return false;
}
app.setWaitCursor();
boolean succ = false;
// Application.debug("height = " + height+" width = "+width);
int x1 = sourceColumn1;
int y1 = sourceRow1;
int x2 = sourceColumn1 + width - 1;
int y2 = sourceRow1 + height - 1;
int x3 = column1;
int y3 = row1;
int x4 = column1 + width - 1;
int y4 = row1 + height - 1;
GeoElementND[][] values2 = RelativeCopy.getValues(app, x3, y3, x4, y4);
/*
* for (int i = 0; i < values2.length; ++ i) { for (int j = 0; j <
* values2[i].length; ++ j) { if (values2[i][j] != null) {
* values2[i][j].remove(); values2[i][j] = null; } } } /*
*/
int size = (x2 - x1 + 1) * (y2 - y1 + 1);
if (constructionIndexes == null || constructionIndexes.length < size) {
constructionIndexes = new Object[size];
}
int count = 0;
// ensure the table is large enough to contain the new data
if (tableModel.getRowCount() < y4 + 1) {
tableModel.setRowCount(y4 + 1);
}
if (tableModel.getColumnCount() < x4 + 1) {
tableModel.setColumnCount(x4 + 1);
}
GeoElement[][] values1 = getCellBufferGeo();// RelativeCopy.getValues(table,
// x1, y1, x2, y2);
try {
for (int x = x1; x <= x2; ++x) {
int ix = x - x1;
for (int y = y1; y <= y2; ++y) {
int iy = y - y1;
// check if we're pasting back into what we're copying from
boolean inSource = x + (x3 - x1) <= x2
&& x + (x3 - x1) >= x1 && y + (y3 - y1) <= y2
&& y + (y3 - y1) >= y1;
// Application.debug("x1="+x1+" x2="+x2+" x3="+x3+"
// x4="+x4+" x="+x+" ix="+ix);
// Application.debug("y1="+y1+" y2="+y2+" y3="+y3+"
// y4="+y4+" y="+y+" iy="+iy);
if (ix + column1 <= maxColumn && iy + row1 <= maxRow// ) {
// //
// check
// not
// outside
// selection
// rectangle
&& (!inSource)) { // check we're not pasting over
// what we're copying
if (values1[ix][iy] != null) {
// just record the coordinates for pasting
constructionIndexes[count] = new Record(
values1[ix][iy].getConstructionIndex(), ix,
iy, x3 - x1, y3 - y1);
count++;
}
// values2[ix][iy] =
// RelativeCopy.doCopyNoStoringUndoInfo0(kernel, table,
// values1[ix][iy], values2[ix][iy], x3 - x1, y3 - y1);
// if (values1[ix][iy] != null && values2[ix][iy] !=
// null)
// values2[ix][iy].setAllVisualProperties(values1[ix][iy]);
}
}
}
// sort according to the construction index
// so that objects are pasted in the correct order
Arrays.sort(constructionIndexes, 0, count, getComparator());
// do the pasting
for (int i = 0; i < count; i++) {
Record r = (Record) constructionIndexes[i];
int ix = r.getx1();
int iy = r.gety1();
values2[ix][iy] = RelativeCopy.doCopyNoStoringUndoInfo0(kernel,
app, values1[ix][iy], values2[ix][iy], r.getx2(),
r.gety2());
}
succ = true;
} catch (Exception e) {
e.printStackTrace();
} finally {
app.setDefaultCursor();
}
return succ;
}
/**
* Pastes data from 2D String array into a given cell range. The data may be
* pasted multiple times to fill in an oversized target rectangle (and maybe
* overflow a bit).
*
* @param data
* @param column1
* minimum target column
* @param row1
* minimum target row
* @param column2
* maximum target column
* @param row2
* maximum target row
* @return
*/
protected boolean pasteExternalMultiple(String[][] data, CellRange cr) {
return pasteExternalMultiple(data, cr.getMinColumn(), cr.getMinRow(),
cr.getMaxColumn(), cr.getMaxRow());
}
/**
* Pastes data from 2D String array into a given set of cells. The data may
* be pasted multiple times to fill in an oversized target rectangle (and
* maybe overflow a bit).
*
* @param data
* @param column1
* minimum target column
* @param row1
* minimum target row
* @param column2
* maximum target column
* @param row2
* maximum target row
* @return
*/
protected boolean pasteExternalMultiple(String[][] data, int column1,
int row1, int column2, int row2) {
boolean oldEqualsSetting = app.getSettings().getSpreadsheet()
.equalsRequired();
app.getSettings().getSpreadsheet().setEqualsRequired(true);
boolean succ = true;
// Fixing NPE in chrome:
if (data == null) {
return false;
} else if (data[0] == null) {
return false;
}
int rowStep = data.length;
int columnStep = data[0].length;
if (columnStep == 0) {
return false;
}
int maxColumn = column2;
int maxRow = row2;
// paste all data if just one cell selected
// ie overflow selection rectangle
if (row2 == row1 && column2 == column1) {
maxColumn = column1 + columnStep;
maxRow = row1 + rowStep;
}
// paste data multiple times to fill in the selection rectangle (and
// maybe overflow a bit)
for (int c = column1; c <= column2; c += columnStep) {
for (int r = row1; r <= row2; r += rowStep) {
succ = succ && pasteExternal(data, c, r, maxColumn, maxRow);
}
}
app.getSettings().getSpreadsheet().setEqualsRequired(oldEqualsSetting);
return succ;
}
/**
* Creates new cell geos using the string values stored in the given
* String[][]. Cells are named to correspond with the target cell range
* defined by upper left corner (column1, row1) and lower right corner
* (maxColumn, maxRow). Does not apply relative cell references.
*
* @param data
* @param column1
* @param row1
* @param maxColumn
* @param maxRow
* @return
*/
public boolean pasteExternal(String[][] data, int column1, int row1,
int maxColumn, int maxRow) {
app.setWaitCursor();
boolean succ = false;
try {
if (tableModel.getRowCount() < row1 + data.length) {
tableModel.setRowCount(row1 + data.length);
}
GeoElementND[][] values2 = new GeoElement[data.length][];
int maxLen = -1;
for (int row = row1; row < row1 + data.length; ++row) {
if (row < 0 || row > maxRow) {
continue;
}
int iy = row - row1;
values2[iy] = new GeoElement[data[iy].length];
if (maxLen < data[iy].length) {
maxLen = data[iy].length;
}
if (tableModel.getColumnCount() < column1 + data[iy].length) {
tableModel.setColumnCount(column1 + data[iy].length);
}
for (int column = column1; column < column1
+ data[iy].length; ++column) {
if (column < 0 || column > maxColumn) {
continue;
}
int ix = column - column1;
// Application.debug(iy + " " + ix + " [" + data[iy][ix] +
// "]");
if (data[iy][ix] == null) {
continue;
}
data[iy][ix] = data[iy][ix].trim();
if (data[iy][ix].length() == 0) {
GeoElement value0 = RelativeCopy.getValue(app, column,
row);
if (value0 != null) {
// Application.debug(value0.toValueString());
// MyCellEditor.prepareAddingValueToTable(kernel,
// table, null, value0, column, row);
// value0.remove();
value0.removeOrSetUndefinedIfHasFixedDescendent();
}
} else {
GeoElement value0 = RelativeCopy.getValue(app, column,
row);
values2[iy][ix] = RelativeCopy
.prepareAddingValueToTableNoStoringUndoInfo(
kernel, app, data[iy][ix], value0,
column, row, true);
// values2[iy][ix].setAuxiliaryObject(values2[iy][ix].isGeoNumeric());
values2[iy][ix].setAuxiliaryObject(true);
}
}
}
// Application.debug("maxLen=" + maxLen);
app.repaintSpreadsheet();
/*
* if (values2.length == 1 || maxLen == 1) {
* createPointsAndAList1(values2); } if (values2.length == 2 ||
* maxLen == 2) { createPointsAndAList2(values2); }
*/
succ = true;
} catch (Exception ex) {
// app.showError(ex.getMessage());
// Util.handleException(table, ex);
ex.printStackTrace();
} finally {
app.setDefaultCursor();
}
return succ;
}
public boolean delete(int column1, int row1, int column2, int row2) {
return delete(app, column1, row1, column2, row2,
getTable().getSelectionType());
}
public void deleteAll() {
delete(0, 0, tableModel.getColumnCount(), tableModel.getRowCount());
}
public static boolean delete(App app, int column1, int row1, int column2,
int row2, int selectionType) {
boolean succ = false;
TreeSet<GeoElement> toRemove = new TreeSet<GeoElement>();
for (int column = column1; column <= column2; ++column) {
for (int row = row1; row <= row2; ++row) {
GeoElement value0 = RelativeCopy.getValue(app, column, row);
if (value0 != null && !value0.isProtected(EventType.REMOVE)) {
toRemove.add(value0);
}
}
}
app.getKernel().setSpreadsheetBatchRunning(true);
int size = toRemove.size();
for (int i = 0; i < size; i++) {
toRemove.last().removeOrSetUndefinedIfHasFixedDescendent();
succ = true;
toRemove.remove(toRemove.last());
}
app.getKernel().setSpreadsheetBatchRunning(false);
// Let the trace manager know about the delete
// TODO add SelectAll
if (selectionType == MyTableInterface.COLUMN_SELECT) {
app.getTraceManager().handleColumnDelete(column1, column2);
} else {
app.getTraceManager().handleColumnDelete(column1, row1, column2,
row2);
}
if (succ) {
app.getKernel().notifyRepaint();
}
return succ;
}
private static class Record {
int id, x1, y1, x2, y2;
public Record(int id, int x1, int y1, int x2, int y2) {
this.id = id;
this.x1 = x1;
this.x2 = x2;
this.y1 = y1;
this.y2 = y2;
}
public int getx1() {
return x1;
}
public int getx2() {
return x2;
}
public int gety1() {
return y1;
}
public int gety2() {
return y2;
}
}
/**
* used to sort Records based on the id (which is the construction index)
*/
public static Comparator getComparator() {
if (comparator == null) {
comparator = new Comparator() {
@Override
public int compare(Object a, Object b) {
Record itemA = (Record) a;
Record itemB = (Record) b;
return itemA.id - itemB.id;
}
};
}
return comparator;
}
/**
* @return copied cell geo values as a tab-delimited string.
*/
protected StringBuilder getCellBufferStr() {
return cellBufferStr;
}
/**
* @param cellBufferStr
* copied cell geo values as a tab-delimited string.
*/
protected void setCellBufferStr(StringBuilder cellBufferStr) {
this.cellBufferStr = cellBufferStr;
}
protected GeoElement[][] getCellBufferGeo() {
return cellBufferGeo;
}
protected void setCellBufferGeo(GeoElement[][] cellBufferGeo) {
this.cellBufferGeo = cellBufferGeo;
}
private static Comparator comparator;
}