/**
*
*/
package org.geogebra.desktop.cas.view;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.SystemColor;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.Collections;
import javax.swing.CellEditor;
import javax.swing.JTable;
import javax.swing.JViewport;
import javax.swing.ListSelectionModel;
import javax.swing.SwingUtilities;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.event.MouseInputAdapter;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.table.TableCellRenderer;
import org.geogebra.common.cas.view.CASTable;
import org.geogebra.common.kernel.Kernel;
import org.geogebra.common.kernel.arithmetic.MyArbitraryConstant;
import org.geogebra.common.kernel.geos.GeoCasCell;
import org.geogebra.common.kernel.geos.GeoNumeric;
import org.geogebra.common.main.App;
import org.geogebra.common.main.GeoGebraColorConstants;
import org.geogebra.desktop.awt.GColorD;
import org.geogebra.desktop.gui.GuiManagerD;
import org.geogebra.desktop.gui.layout.DockManagerD;
import org.geogebra.desktop.gui.layout.DockPanelD;
import org.geogebra.desktop.main.AppD;
/**
* @author Quan
*
*/
public class CASTableD extends JTable implements CASTable {
private static final long serialVersionUID = 1L;
/** column of the table containing CAS cells */
public final static int COL_CAS_CELLS = 0;
private CASTableModel tableModel;
private Kernel kernel;
/** application */
protected AppD app;
/** CAS view */
protected CASViewD view;
/** cell editor */
CASTableCellEditorD editor;
private CASTableCellRenderer renderer;
private int currentWidth;
private boolean rightClick = false;
private int clickedRow;
/** row that was last rolled over or -1 if mouse exited CAS view */
protected int rollOverRow = -1;
/** whether the mouse is hovering over output */
protected boolean isOutputRollOver;
/** whether current cell input/output should be highlighted */
protected boolean highlight = false;
/** whether Alt key is down */
protected boolean isAltDown;
/**
* Constructs a <code>CASTable</code> that displays CAS cells
*
* @param view
* CASView that accommodates the table
*/
public CASTableD(final CASViewD view) {
this.view = view;
app = view.getApp();
kernel = app.getKernel();
setShowGrid(true);
setGridColor(
GColorD.getAwtColor(GeoGebraColorConstants.TABLE_GRID_COLOR));
setBackground(Color.white);
tableModel = new CASTableModel();
this.setModel(tableModel);
this.setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION);
// init editor and renderer
editor = new CASTableCellEditorD(view);
renderer = new CASTableCellRenderer(view);
getColumnModel().getColumn(COL_CAS_CELLS).setCellEditor(editor);
getColumnModel().getColumn(COL_CAS_CELLS).setCellRenderer(renderer);
setTableHeader(null);
/**
* Remove all mouse listeners to make sure they don't start editing
* cells when a row is clicked. This is need to be able to have full
* control over whether editing should be started or the output of a
* cell inserted into another one. This also prevents Exception in
* thread "AWT-EventQueue-0" java.lang.NullPointerException at
* javax.swing.plaf.basic.BasicTableUI$Handler.mousePressed(Unknown
* Source)
*/
for (MouseListener ml : getMouseListeners()) {
removeMouseListener(ml);
}
// listen to mouse pressed on table cells, make sure to start editing
addMouseListener(new MyMouseListener());
// add listener for mouse roll over
RollOverListener rollOverListener = new RollOverListener();
addMouseMotionListener(rollOverListener);
addMouseListener(rollOverListener);
// keep editor value after changing width
addComponentListener(new ComponentAdapter() {
@Override
public void componentResized(ComponentEvent e) {
if (getCurrentWidth() == getWidth()) {
return;
}
setCurrentWidth(getWidth());
if (isEditing()) {
// keep editor value after resizing
int row = getEditor().getEditingRow();
if (row >= 0 && row < getRowCount()) {
getEditor().stopCellEditing();
updateRow(row);
}
}
}
});
// tableModel listener to resize the column width after row updates
// note: this only adjusts column 0
tableModel.addTableModelListener(new TableModelListener() {
@Override
public void tableChanged(TableModelEvent e) {
if (e.getType() == TableModelEvent.UPDATE
|| e.getType() == TableModelEvent.DELETE) {
TableCellRenderer tableCellRenderer;
int prefWidth = 0;
// iterate through all rows and get max preferred width
for (int r = 0; r < getRowCount(); r++) {
tableCellRenderer = getCellRenderer(r, 0);
int w = prepareRenderer(tableCellRenderer, r, 0)
.getPreferredSize().width;
prefWidth = Math.max(prefWidth, w);
}
// adjust the width
if (prefWidth != getTable().getColumnModel().getColumn(0)
.getPreferredWidth()) {
getTable().getColumnModel().getColumn(0)
.setPreferredWidth(prefWidth);
getTable().getColumnModel().getColumn(0)
.setMinWidth(prefWidth);
}
}
}
});
// Set the width of the index column;
// this.getColumn(this.getColumnName(CASPara.indexCol)).setMinWidth(30);
// this.getColumn(this.getColumnName(CASPara.indexCol)).setMaxWidth(30);
// this.sizeColumnsToFit(0);
// this.setSurrendersFocusOnKeystroke(true);
this.getSelectionModel()
.addListSelectionListener(new SelectionListener(this));
this.setFont(app.getPlainFont());
}
/**
* listen to mouse pressed on table cells, make sure to start editing
*
*/
protected class MyMouseListener extends MouseAdapter {
@Override
public void mousePressed(MouseEvent e) {
setClickedRow(rowAtPoint(e.getPoint()));
// make sure the CAS view gets the focus and its toolbar when
// clicked on the table
// for some reason this is not working out of the box as
// DockManager.eventDispatched()
// sometimes thinks that this click comes from the EuclidianView
GuiManagerD gui = (GuiManagerD) app.getGuiManager();
DockManagerD dockManager = gui.getLayout().getDockManager();
DockPanelD panel = dockManager.getFocusedPanel();
if (panel == null || panel.getViewId() != App.VIEW_CAS) {
dockManager.setFocusedPanel(App.VIEW_CAS);
}
e.consume();
}
@Override
public void mouseReleased(MouseEvent e) {
if (getClickedRow() < 0) {
e.consume();
return;
}
setRightClick(AppD.isRightClickForceMetaDown(e));
GeoCasCell clickedCell = getTable().getGeoCasCell(getClickedRow());
if (isRightClick() && isOutputPanelClicked(e.getPoint())) {
if (!clickedCell.isEmpty() && !clickedCell.isError()) {
RowContentPopupMenu popupMenu = new RowContentPopupMenu(app,
clickedCell, getEditor(), getTable(),
RowContentPopupMenu.Panel.OUTPUT);
popupMenu.show(e.getComponent(), e.getX(), e.getY());
}
}
e.consume();
if (isRightClick()) {
return;
}
/*
* set/unset euclidian visibility for CasCells if there is no
* twinGeo which can be displayed, run the plot method which creates
* a name and a twinGeo for the cell
*/
if (isEditing() && getEditor().getEditingRow() != getClickedRow()) {
if (e.isAltDown()) {
getEditor().insertText("$" + (getClickedRow() + 1));
}
// output panel click
else if (isOutputPanelClicked(e.getPoint())
&& clickedCell.showOutput()
&& !clickedCell.isOutputEmpty()) {
if (!clickedCell.isError()) {
getEditor().insertText(
view.getRowOutputValue(getClickedRow()));
}
} else {
getSelectionModel().setSelectionInterval(getClickedRow(),
getClickedRow());
startEditingRow(getClickedRow());
return;
}
view.styleBar.updateStyleBar();
repaint();
// set clickedRow selected
} else {
getSelectionModel().setSelectionInterval(getClickedRow(),
getClickedRow());
startEditingRow(getClickedRow());
}
}
}
/**
* Handles mouse over events
*/
protected class RollOverListener extends MouseInputAdapter {
@Override
public void mouseMoved(MouseEvent e) {
int row = rowAtPoint(e.getPoint());
if (row != getOpenRow()
&& (row != rollOverRow
|| isOutputRollOver != isOutputPanelClicked(
e.getPoint())
|| isAltDown != e.isAltDown())) {
rollOverRow = row;
isOutputRollOver = isOutputPanelClicked(e.getPoint());
isAltDown = e.isAltDown();
repaint();
}
highlight = e.isAltDown() || (isOutputRollOver
&& getGeoCasCell(row).showOutput()
&& getGeoCasCell(row).getLaTeXOutput() != null
&& getGeoCasCell(row).getLaTeXOutput().length() > 0);
if (isOutputRollOver) {
setToolTipText(getGeoCasCell(row).getTooltipText(true, true));
} else {
setToolTipText(null);
}
}
@Override
public void mouseExited(MouseEvent e) {
setToolTipText(null);
rollOverRow = -1;
repaint();
}
}
/**
* Selection listener to repaint selection frame when selection changes
*/
public static class SelectionListener implements ListSelectionListener {
private JTable table;
/**
* @param table
* CAS table to be listened to
*/
SelectionListener(JTable table) {
this.table = table;
}
@Override
public void valueChanged(ListSelectionEvent e) {
if (!e.getValueIsAdjusting()) {
table.repaint();
}
}
}
/**
* Returns the CAS view which uses this table
*
* @return CAS view
*/
public CASViewD getCASView() {
return view;
}
/**
* Returns whether the output panel of a cell row was clicked.
*
* @param p
* clicked position in table coordinates
* @return true if output panel of a cell row was clicked
*/
boolean isOutputPanelClicked(Point p) {
int row = rowAtPoint(p);
if (row < 0) {
return false;
}
// calculate sum of row heights before
int rowHeightsAbove = 0;
for (int i = 0; i < row; i++) {
rowHeightsAbove += getRowHeight(i);
}
// get height of input panel in clicked row
TableCellRenderer tableCellRenderer = getCellRenderer(row, 0);
CASTableCell tableCell = (CASTableCell) prepareRenderer(
tableCellRenderer, row, 0);
int inputAreaHeight = tableCell.getInputPanelHeight();
// check if we clicked below input area
boolean outputClicked = p.y > rowHeightsAbove + inputAreaHeight;
return outputClicked;
}
@Override
public boolean isEditing() {
return editor != null && editor.isEditing();
}
/**
* Stops editing of current cell
*/
@Override
public void stopEditing() {
if (!isEditing()) {
return;
}
// stop editing
CellEditor editor1 = (CellEditor) getEditorComponent();
if (editor1 != null) {
editor1.stopCellEditing();
}
}
/**
* Returns the cell editor
*
* @return cell editor
*/
@Override
public CASTableCellEditorD getEditor() {
return editor;
}
/**
* Inserts a row at selectedRow and starts editing the new row.
*
* @param selectedRow
* row index
* @param newValue
* new value of the cell
* @param startEditing
* true to start editing
*/
@Override
public void insertRow(final int selectedRow, GeoCasCell newValue,
final boolean startEditing) {
if (startEditing) {
stopEditing();
}
GeoCasCell toInsert = newValue;
if (toInsert == null) {
toInsert = new GeoCasCell(kernel.getConstruction());
if (selectedRow != tableModel.getRowCount()) {
// tell construction about new GeoCasCell if it is not at the
// end
kernel.getConstruction().setCasCellRow(toInsert, selectedRow);
} else if (selectedRow != 0) {
// we insert below last cell
// if last cell is empty, add it to construction list
// so its row number will be updated
GeoCasCell last = (GeoCasCell) tableModel
.getValueAt(selectedRow - 1, COL_CAS_CELLS);
if (last != null && last.isEmpty()) {
kernel.getConstruction().addToConstructionList(last, true);
}
}
}
// update keys (rows) in arbitrary constant table
updateAfterInsertArbConstTable(selectedRow);
tableModel.insertRow(selectedRow, new Object[] { toInsert });
// make sure the row is shown when at the bottom of the viewport
getTable().scrollRectToVisible(
getTable().getCellRect(selectedRow, 0, false));
// update height of new row
if (startEditing) {
startEditingRow(selectedRow);
}
}
/**
* Updates arbitraryConstantTable in construction.
*
* @param selectedRow
* row index (starting from 0) where cell insertion is done
*/
private void updateAfterInsertArbConstTable(int selectedRow) {
if (kernel.getConstruction().getArbitraryConsTable().size() > 0) {
// find last row number
Integer max = Collections.max(
kernel.getConstruction().getArbitraryConsTable().keySet());
for (int key = max; key >= selectedRow; key--) {
MyArbitraryConstant myArbConst = kernel.getConstruction()
.getArbitraryConsTable().get(key);
if (myArbConst != null
&& !kernel.getConstruction().isCasCellUpdate()
&& !kernel.getConstruction().isFileLoading()
&& kernel.getConstruction().isNotXmlLoading()) {
kernel.getConstruction().getArbitraryConsTable()
.remove(key);
kernel.getConstruction().getArbitraryConsTable()
.put(key + 1, myArbConst);
}
}
}
}
/**
* Puts casCell into given row.
*
* @param row
* row index (starting from 0)
* @param casCell
* CAS cell
*/
@Override
final public void setRow(final int row, final GeoCasCell casCell) {
if (row < 0) {
return;
}
// cancel editing
if (editor.isEditing() && editor.getEditingRow() == row) {
editor.cancelCellEditing();
}
int rowCount = tableModel.getRowCount();
if (row < rowCount) {
if (casCell == tableModel.getValueAt(row, COL_CAS_CELLS)) {
tableModel.fireTableRowsUpdated(row, row);
} else {
tableModel.setValueAt(casCell, row, COL_CAS_CELLS);
}
} else {
// add new rows
for (int pos = rowCount; pos <= row; pos++) {
tableModel.addRow(new Object[] { "" });
}
tableModel.setValueAt(casCell, row, COL_CAS_CELLS);
}
}
/**
* Returns the preferred height of a row. The result is equal to the tallest
* cell in the row.
*
* @param rowIndex
* Row-Index.
* @return The preferred height.
*
* @see "http://www.exampledepot.com/egs/javax.swing.table/RowHeight.html"
*/
public int getPreferredRowHeight(int rowIndex) {
// Get the current default height for all rows
int height = getRowHeight();
// Determine highest cell in the row
for (int c = 0; c < getColumnCount(); c++) {
TableCellRenderer tableCellRenderer = getCellRenderer(rowIndex, c);
Component comp = prepareRenderer(tableCellRenderer, rowIndex, c);
int h = comp.getPreferredSize().height; // + 2*margin;
height = Math.max(height, h);
}
return height;
}
/**
* The height of each row is set to the preferred height of the tallest cell
* in that row.
*/
public void packRows() {
packRows(0, getRowCount());
}
/**
* For each row >= start and < end, the height of a row is set to the
* preferred height of the tallest cell in that row.
*
* @param start
* start row
* @param end
* end row
*/
public void packRows(int start, int end) {
for (int r = start; r < end; r++) {
// Get the preferred height
int h = getPreferredRowHeight(r);
// Now set the row height using the preferred height
if (getRowHeight(r) != h) {
setRowHeight(r, h);
}
}
}
/**
* Updates given row
*
* @param row
* row to update
*/
public void updateRow(int row) {
tableModel.fireTableRowsUpdated(row, row);
}
/**
* Updates all rows
*/
public void updateAllRows() {
int rowCount = tableModel.getRowCount();
if (rowCount > 0) {
tableModel.fireTableRowsUpdated(0, rowCount - 1);
}
}
/**
* @param row
* row index (starting from 0)
* @return CAS cell on given row
*/
@Override
public GeoCasCell getGeoCasCell(int row) {
if (row < 0 || row > tableModel.getRowCount() - 1) {
return null;
}
Object cell = tableModel.getValueAt(row, COL_CAS_CELLS);
return cell instanceof GeoCasCell ? (GeoCasCell) cell : null;
}
/**
* Delete all rows
*/
@Override
public void deleteAllRows() {
tableModel.setRowCount(0);
// kernel.getConstruction().setArbitraryConsTable(
// new HashMap<Integer, MyArbitraryConstant>());
}
/**
* Delete a row, and set the focus at the right position
*
* @param row
* row (staring from 0)
*/
@Override
public void deleteRow(int row) {
if (row > -1 && row < tableModel.getRowCount()) {
tableModel.removeRow(row);
}
// update keys (rows) in arbitrary constant table
updateAfterDeleteArbConstTable(row);
}
/**
* Updates arbitraryConstantTable in construction.
*
* @param row
* row index (starting from 0) where cell is deleted
*/
private void updateAfterDeleteArbConstTable(int row) {
MyArbitraryConstant arbConst = kernel.getConstruction()
.getArbitraryConsTable().remove(row);
if (arbConst != null) {
for (GeoNumeric geoNum : arbConst.getConstList()) {
kernel.getConstruction().removeFromConstructionList(geoNum);
kernel.getConstruction().removeLabel(geoNum);
kernel.notifyRemove(geoNum);
}
}
if (kernel.getConstruction().getArbitraryConsTable().size() > 0) {
// find last row number
Integer max = Collections.max(
kernel.getConstruction().getArbitraryConsTable().keySet());
for (int key = row + 1; key <= max; key++) {
MyArbitraryConstant myArbConst = kernel.getConstruction()
.getArbitraryConsTable().get(key);
if (myArbConst != null) {
kernel.getConstruction().getArbitraryConsTable()
.remove(key);
kernel.getConstruction().getArbitraryConsTable()
.put(key - 1, myArbConst);
}
}
}
}
/**
* Set the focus on the specified row
*
* @param editRow
* row number (starting from 0)
*/
@Override
public void startEditingRow(final int editRow) {
rollOverRow = -1;
if (editRow >= tableModel.getRowCount()) {
// insert new row, this starts editing
view.insertRow(null, true);
} else {
// start editing
doEditCellAt(editRow);
}
}
private void doEditCellAt(final int editRow) {
if (editRow < 0) {
return;
}
setRowSelectionInterval(editRow, editRow);
scrollRectToVisible(getCellRect(editRow, COL_CAS_CELLS, true));
editCellAt(editRow, COL_CAS_CELLS);
// use invokeLater to prevent the scrollpane from stealing the focus
// when scrollbars are made visible
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
boolean success = editCellAt(editRow, COL_CAS_CELLS);
if (success) {
editor.setInputAreaFocused();
}
}
});
}
@Override
public void setFont(Font ft) {
super.setFont(ft);
if (editor != null) {
if (isEditing()) {
editor.stopCellEditing();
}
editor.setFont(getFont());
}
if (renderer != null) {
renderer.setFont(getFont());
}
repaint();
}
// ===============================================================
// Workaround for java horizontal scrolling bug, see:
// http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4127936
/**
* When the viewport shrinks below the preferred size, stop tracking the
* viewport width
*/
@Override
public boolean getScrollableTracksViewportWidth() {
if (autoResizeMode != AUTO_RESIZE_OFF) {
if (getParent() instanceof JViewport) {
return (((JViewport) getParent())
.getWidth() > getPreferredSize().width);
}
}
return false;
}
/**
* When the viewport shrinks below the preferred size, return the minimum
* size so that scrollbars will be shown
*/
@Override
public Dimension getPreferredSize() {
if (getParent() instanceof JViewport) {
if (((JViewport) getParent())
.getWidth() < super.getPreferredSize().width) {
return getMinimumSize();
}
}
return super.getPreferredSize();
}
// End horizontal scrolling fix
// ================================================================
/**
* When the table is smaller than the viewport fill this extra space with
* the same background color as the table.
*/
@Override
protected void configureEnclosingScrollPane() {
super.configureEnclosingScrollPane();
Container p = getParent();
if (p instanceof JViewport) {
((JViewport) p).setBackground(getBackground());
}
}
/**
* Updates labels to match current locale
*/
@Override
public void setLabels() {
editor.setLabels();
}
/**
* Updates component orientation to match current locale
*/
public void setOrientation() {
editor.setOrientation();
renderer.setOrientation();
}
/**
* @return the clickedRow
*/
public int getClickedRow() {
return clickedRow;
}
/**
* @return the rightClick
*/
public boolean isRightClick() {
return rightClick;
}
/**
* @param rightClick
* the rightClick to set
*/
public void setRightClick(boolean rightClick) {
this.rightClick = rightClick;
}
/**
* @param clickedRow
* the clickedRow to set
*/
public void setClickedRow(int clickedRow) {
this.clickedRow = clickedRow;
view.getCASStyleBar().setSelectedRow(this.getGeoCasCell(clickedRow));
}
/**
* @return the table
*/
public CASTableD getTable() {
return this;
}
/**
* @return the currentWidth
*/
public int getCurrentWidth() {
return currentWidth;
}
/**
* @param currentWidth
* the currentWidth to set
*/
public void setCurrentWidth(int currentWidth) {
this.currentWidth = currentWidth;
}
/**
* @return currently open row
*/
public int getOpenRow() {
if (getEditingRow() >= 0) {
return getEditingRow();
}
return (getRowCount() - 1);
}
/** dash pattern for selection */
final static float dash1[] = { 2f, 1f };
/** dashed stroke for selection */
final static BasicStroke dashed = new BasicStroke(1.0f,
BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, dash1, 0.0f);
/**
* Highlights the selected row
*/
@Override
public void paint(Graphics graphics) {
super.paint(graphics);
Graphics2D g2 = (Graphics2D) graphics;
if (this.getSelectedRow() >= 0) {
Rectangle r = getCellRect(getSelectedRow(), 0, true);
g2.setColor(SystemColor.controlHighlight);
g2.drawRect(r.x, r.y, r.width - 2, r.height - 2);
if (isEditing()) {
CASTableCell panel = (CASTableCell) getCellRenderer(
getSelectedRow(), 0).getTableCellRendererComponent(this,
null, false, false, rollOverRow, COL_CAS_CELLS);
int offset = panel.outputPanel.getY();
r.height = r.height - offset;
// g2.drawRect(r.x+1,r.y+1,r.width-4,r.height-4);
g2.setColor(Color.red);
// g2.drawRect(r.x+2,r.y+2,r.width-6,r.height-6);;
}
}
CASTableCell rollOverCell = null;
{
// shade the all rows except the editing row
g2.setColor(new Color(0, 100, 100, 15));
if (rollOverRow >= 0 && highlight) {
rollOverCell = (CASTableCell) getCellRenderer(rollOverRow,
COL_CAS_CELLS).getTableCellRendererComponent(this, null,
false, false, rollOverRow, COL_CAS_CELLS);
int offset = rollOverCell.outputPanel.getY();
Rectangle r = getCellRect(rollOverRow, COL_CAS_CELLS, true);
if (!isAltDown) {
r.y = r.y + offset;
r.height = r.height - offset;
}
g2.setColor(new Color(0, 0, 200, 40));
g2.fillRect(r.x + 2, r.y + 2, r.width - 6, r.height - 6);
g2.setColor(Color.GRAY);
g2.setStroke(dashed);
g2.drawRect(r.x + 1, r.y + 1, r.width - 4, r.height - 4);
}
}
}
@Override
public App getApplication() {
return app;
}
@Override
public void resetRowNumbers(int from) {
// do nothing
}
@Override
public boolean hasEditor() {
return this.editor != null;
}
public boolean keepEditing(boolean failure, int rowNum) {
return failure;
}
}