package org.geogebra.desktop.gui.view.algebra;
import java.awt.Font;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map.Entry;
import javax.swing.BorderFactory;
import javax.swing.JTree;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreePath;
import org.geogebra.common.gui.view.algebra.AlgebraView.SortMode;
import org.geogebra.common.kernel.Kernel;
import org.geogebra.common.kernel.StringTemplate;
import org.geogebra.common.kernel.cas.AlgoDependentCasCell;
import org.geogebra.common.kernel.geos.GProperty;
import org.geogebra.common.kernel.geos.GeoElement;
import org.geogebra.desktop.euclidian.EuclidianViewD;
import org.geogebra.desktop.main.AppD;
/**
* JTree for objects
*
* @author Mathieu
*
*/
public class AlgebraTree extends JTree {
private static final long serialVersionUID = 1L;
protected AppD app; // parent appame
protected Kernel kernel;
protected MyRendererForAlgebraTree renderer;
protected AlgebraTreeController algebraController;
/**
* The tree model.
*/
protected DefaultTreeModel model;
/**
* Root node for tree mode MODE_TYPE.
*/
protected DefaultMutableTreeNode rootType;
/**
* Nodes for tree mode MODE_TYPE
*/
protected HashMap<String, DefaultMutableTreeNode> typeNodesMap;
// store all pairs of GeoElement -> node in the Tree
protected HashMap<GeoElement, DefaultMutableTreeNode> nodeTable = new HashMap<GeoElement, DefaultMutableTreeNode>(
500);
/**
* Flag for LaTeX rendering
*/
private boolean renderLaTeX = true;
/** Creates new AlgebraView */
public AlgebraTree(AlgebraTreeController algCtrl, boolean renderLaTeX) {
app = (AppD) algCtrl.getApplication();
kernel = algCtrl.getKernel();
this.algebraController = algCtrl;
this.renderLaTeX = renderLaTeX;
algebraController.setTree(this);
initTree();
}
/**
* init the tree
*/
protected void initTree() {
initTreeCellRendererEditor();
// add listener
addMouseListener(algebraController);
addMouseMotionListener(algebraController);
// add small border
setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 0));
// initializes the tree model
model = new DefaultTreeModel(null);
initModel();
setModel(model);
setLargeModel(true);
setLabels();
// tree's options
setRootVisible(false);
// show lines from parent to children
putClientProperty("JTree.lineStyle", "Angled");
setInvokesStopCellEditing(true);
setScrollsOnExpand(true);
setRowHeight(-1); // to enable flexible height of cells
setToggleClickCount(-1);
}
protected void initTreeCellRendererEditor() {
renderer = newMyRenderer(app);
setCellRenderer(renderer);
}
/**
*
* @param app
* @return new renderer of a cell
*/
protected MyRendererForAlgebraTree newMyRenderer(AppD app) {
return new MyRendererForAlgebraTree(app, this);
}
public boolean isRenderLaTeX() {
return renderLaTeX;
}
/**
* @return The display mode of the tree, see MODE_DEPENDENCY, MODE_TYPE
*/
public SortMode getTreeMode() {
return SortMode.TYPE;
}
/**
* Method to initialize the tree model of the current tree mode. This method
* should be called whenever the tree mode is changed, it won't initialize
* anything if not necessary.
*
* This method will also actually change the model of the tree.
*/
protected void initModel() {
// don't re-init anything
if (rootType == null) {
rootType = new DefaultMutableTreeNode();
typeNodesMap = new HashMap<String, DefaultMutableTreeNode>(5);
}
checkRemoveAuxiliaryNode();
// set the root
model.setRoot(rootType);
}
/**
* check if application ask for remove
*/
protected void checkRemoveAuxiliaryNode() {
// not removed here
}
/**
* resets all fix labels of the View. This method is called by the
* application if the language setting is changed.
*/
public void setLabels() {
setTreeLabels();
}
/**
* set labels on the tree
*/
protected void setTreeLabels() {
DefaultMutableTreeNode node;
for (Entry<String, DefaultMutableTreeNode> entry : typeNodesMap
.entrySet()) {
String key = entry.getKey();
node = entry.getValue();
node.setUserObject(app.getLocalization().getMenu(key));
model.nodeChanged(node);
}
}
@Override
public void setToolTipText(String text) {
renderer.setToolTipText(text);
}
/**
* @param tree
* tree
* @param x
* x coord
* @param y
* y coord
* @return geo at this location on the tree
*/
public static GeoElement getGeoElementForLocation(JTree tree, int x,
int y) {
TreePath tp = tree.getPathForLocation(x, y);
return getGeoElementForPath(tp);
}
/**
* @param tp
* tree path
* @return geo at this path
*/
public static GeoElement getGeoElementForPath(TreePath tp) {
if (tp == null) {
return null;
}
Object ob;
DefaultMutableTreeNode node = (DefaultMutableTreeNode) tp
.getLastPathComponent();
if (node != null && (ob = node.getUserObject()) instanceof GeoElement) {
return (GeoElement) ob;
}
return null;
}
/**
* @param tp
* tree path
* @return geos as childs under this path
*/
public static ArrayList<GeoElement> getGeoChildsForPath(TreePath tp) {
if (tp == null) {
return null;
}
DefaultMutableTreeNode node = (DefaultMutableTreeNode) tp
.getLastPathComponent();
if (node == null) {
return null;
}
ArrayList<GeoElement> ret = new ArrayList<GeoElement>();
addChilds(ret, node, 0, node.getChildCount());
return ret;
}
/**
* adds a new node to the tree
*/
public void add(GeoElement geo) {
add(geo, -1);
}
/**
* do we show this geo here ?
*
* @param geo
* geo
* @return true if geo has to be shown
*/
protected boolean show(GeoElement geo) {
return geo.isLabelSet();
}
protected void add(GeoElement geo, int forceLayer) {
cancelEditing();
if (show(geo)) {
// don't add auxiliary objects if the tree is categorized by type
if (!getTreeMode().equals(SortMode.DEPENDENCY)
&& !showAuxiliaryObjects() && geo.isAuxiliaryObject()) {
return;
}
DefaultMutableTreeNode parent, node;
node = new DefaultMutableTreeNode(geo);
parent = getParentNode(geo, forceLayer);
// add node to model (alphabetically ordered)
int pos = getInsertPosition(parent, geo, getTreeMode());
model.insertNodeInto(node, parent, pos);
nodeTable.put(geo, node);
// ensure that the leaf with the new object is visible
expandPath(new TreePath(new Object[] { model.getRoot(), parent }));
}
}
/**
* removes a node from the tree
*/
public void remove(GeoElement geo) {
cancelEditing();
DefaultMutableTreeNode node = nodeTable.get(geo);
if (node != null) {
removeFromModel(node, ((DefaultTreeModel) getModel()));
}
}
public void clearView() {
nodeTable.clear();
clearTree();
model.reload();
}
/**
* renames an element and sorts list
*/
public void rename(GeoElement geo) {
remove(geo);
add(geo);
}
// TODO EuclidianView#setHighlighted() doesn't exist
/**
* updates node of GeoElement geo (needed for highlighting)
*
* @see EuclidianViewD#setHighlighted()
*/
public void update(GeoElement geo) {
DefaultMutableTreeNode node = nodeTable.get(geo);
if (node != null) {
/*
* occasional exception when animating Exception in thread
* "AWT-EventQueue-0" java.lang.ArrayIndexOutOfBoundsException: 1 >=
* 1 at java.util.Vector.elementAt(Vector.java:432) at
* javax.swing.tree.DefaultMutableTreeNode.getChildAt(
* DefaultMutableTreeNode.java:230) at
* javax.swing.tree.VariableHeightLayoutCache.treeNodesChanged(
* VariableHeightLayoutCache.java:412) at
* javax.swing.plaf.basic.BasicTreeUI$Handler.treeNodesChanged(
* BasicTreeUI.java:3669) at
* javax.swing.tree.DefaultTreeModel.fireTreeNodesChanged(
* DefaultTreeModel.java:466) at
* javax.swing.tree.DefaultTreeModel.nodesChanged(DefaultTreeModel.
* java:328) at
* javax.swing.tree.DefaultTreeModel.nodeChanged(DefaultTreeModel.
* java:261) at
* geogebra.gui.view.algebra.AlgebraView.update(AlgebraView.java:
* 726) at geogebra.kernel.Kernel.notifyUpdate(Kernel.java:2082) at
* geogebra.kernel.GeoElement.update(GeoElement.java:3269) at
* geogebra.kernel.GeoPoint.update(GeoPoint.java:1169) at
* geogebra.kernel.GeoElement.updateCascade(GeoElement.java:3313) at
* geogebra.kernel.GeoElement.updateCascade(GeoElement.java:3369) at
* geogebra.kernel.AnimationManager.actionPerformed(AnimationManager
* .java:179)
*
*/
try {
((DefaultTreeModel) getModel()).nodeChanged(node);
} catch (Exception e) {
e.printStackTrace();
}
/*
* Cancel editing if the updated geo element has been edited, but
* not otherwise because editing geos while animation is running
* won't work then (ticket #151).
*/
if (isEditing()) {
if (getEditingPath().equals(new TreePath(node.getPath()))) {
cancelEditing();
}
}
}
}
public void updatePreviewFromInputBar(GeoElement[] geos) {
// TODO
}
public void updateVisualStyle(GeoElement geo, GProperty prop) {
update(geo);
}
final public void updateAuxiliaryObject(GeoElement geo) {
remove(geo);
add(geo);
}
/**
* remove all from the tree
*/
protected void clearTree() {
rootType.removeAllChildren();
typeNodesMap.clear();
}
/**
* Remove this node from the model.
*
* @param node
* @param model
*/
protected void removeFromModel(DefaultMutableTreeNode node,
DefaultTreeModel model) {
model.removeNodeFromParent(node);
nodeTable.remove(node.getUserObject());
removeFromModelForMode(node, model);
}
/**
* Remove this node from the model.
*
* @param node
* @param model
*/
protected void removeFromModelForMode(DefaultMutableTreeNode node,
DefaultTreeModel model) {
String typeString = ((GeoElement) node.getUserObject())
.getTypeStringForAlgebraView();
DefaultMutableTreeNode parent = typeNodesMap.get(typeString);
// this has been the last node
if (parent != null && parent.getChildCount() == 0) {
typeNodesMap.remove(typeString);
model.removeNodeFromParent(parent);
}
}
public boolean showAuxiliaryObjects() {
return true;
}
/**
*
* @return current root of the tree
*/
public DefaultMutableTreeNode getRoot() {
return rootType;
}
/**
*
* @param geo1
* one geo
* @param geo2
* one other geo
* @return the indices in the tree corresponding to the two geos
*/
protected int[][] getIndices(GeoElement geo1, GeoElement geo2) {
int found = 0;
DefaultMutableTreeNode root = getRoot();
int[][] ret = new int[2][];
for (int i = 0; i < root.getChildCount() && found < 2; i++) {
DefaultMutableTreeNode child = (DefaultMutableTreeNode) root
.getChildAt(i);
for (int j = 0; j < child.getChildCount() && found < 2; j++) {
DefaultMutableTreeNode child2 = (DefaultMutableTreeNode) child
.getChildAt(j);
Object ob = child2.getUserObject();
if (ob == geo1 || ob == geo2) {
ret[found] = new int[] { i, j };
found++;
}
}
}
if (found < 2) {
return null;
}
return ret;
}
/**
*
* @param geo1
* @param geo2
* @return geos displayed in the tree between the two geos (included)
*/
public ArrayList<GeoElement> getGeosBetween(GeoElement geo1,
GeoElement geo2) {
int[][] indices = getIndices(geo1, geo2);
if (indices != null) {
int p1 = indices[0][0]; // parent index of first geo
int c1 = indices[0][1]; // child index of first geo
int p2 = indices[1][0]; // parent index of second geo
int c2 = indices[1][1]; // child index of second geo
DefaultMutableTreeNode root = getRoot();
if (p1 == p2) {// same category
DefaultMutableTreeNode node = (DefaultMutableTreeNode) root
.getChildAt(p1);
ArrayList<GeoElement> ret = new ArrayList<GeoElement>();
addChilds(ret, node, c1, c2 + 1);
return ret;
} // else, all geos between the two categories
ArrayList<GeoElement> ret = new ArrayList<GeoElement>();
DefaultMutableTreeNode node = (DefaultMutableTreeNode) root
.getChildAt(p1);
addChilds(ret, node, c1, node.getChildCount());
for (int i = p1 + 1; i < p2; i++) {
node = (DefaultMutableTreeNode) root.getChildAt(i);
// add childs only if node is expanded
if (!isCollapsed(new TreePath(node.getPath()))) {
addChilds(ret, node, 0, node.getChildCount());
}
}
node = (DefaultMutableTreeNode) root.getChildAt(p2);
addChilds(ret, node, 0, c2 + 1);
return ret;
}
return null;
}
private static void addChilds(ArrayList<GeoElement> list,
DefaultMutableTreeNode node, int start, int end) {
Object ob;
for (int i = start; i < end; i++) {
DefaultMutableTreeNode child = (DefaultMutableTreeNode) node
.getChildAt(i);
if (child != null
&& (ob = child.getUserObject()) instanceof GeoElement) {
list.add((GeoElement) ob);
}
}
}
protected DefaultMutableTreeNode getParentNode(GeoElement geo,
int forceLayer) {
DefaultMutableTreeNode parent;
// get type node
String typeString = geo.getTypeStringForAlgebraView();
parent = typeNodesMap.get(typeString);
// do we have to create the parent node?
if (parent == null) {
String transTypeString = geo.translatedTypeStringForAlgebraView();
parent = new DefaultMutableTreeNode(transTypeString);
typeNodesMap.put(typeString, parent);
// find insert pos
int pos = rootType.getChildCount();
for (int i = 0; i < pos; i++) {
DefaultMutableTreeNode child = (DefaultMutableTreeNode) rootType
.getChildAt(i);
if (transTypeString.compareTo(child.toString()) < 0) {
pos = i;
break;
}
}
model.insertNodeInto(parent, rootType, pos);
}
return parent;
}
/**
* Gets the insert position for newGeo to insert it in alphabetical order in
* parent node. Note: all children of parent must have instances of
* GeoElement as user objects.
*
* @param mode
*/
final public static int getInsertPosition(DefaultMutableTreeNode parent,
GeoElement newGeo, SortMode mode) {
// label of inserted geo
// String newLabel = newGeo.getLabel();
// standard case: binary search
int left = 0;
int right = parent.getChildCount();
if (right == 0) {
return right;
}
// bigger then last?
DefaultMutableTreeNode node = (DefaultMutableTreeNode) parent
.getLastChild();
// String nodeLabel = ((GeoElement) node.getUserObject()).getLabel();
GeoElement geo2 = ((GeoElement) node.getUserObject());
if (compare(newGeo, geo2, mode)) {
return right;
}
// binary search
while (right > left) {
int middle = (left + right) / 2;
node = (DefaultMutableTreeNode) parent.getChildAt(middle);
// nodeLabel = ((GeoElement) node.getUserObject()).getLabel();
geo2 = ((GeoElement) node.getUserObject());
if (!compare(newGeo, geo2, mode)) {
right = middle;
} else {
left = middle + 1;
}
}
// insert at correct position
return right;
}
private static boolean compare(GeoElement geo1, GeoElement geo2,
SortMode mode) {
switch (mode) {
case ORDER:
int geo1Index = -1;
int geo2index = -1;
// use index of twinGeo instead of corresponding geoCasCell
if (geo1.getParentAlgorithm() != null && geo1
.getParentAlgorithm() instanceof AlgoDependentCasCell) {
geo1Index = geo1.getAlgoDepCasCellGeoConstIndex();
} else {
geo1Index = geo1.getConstructionIndex();
}
// use index of twinGeo instead of corresponding geoCasCell
if (geo2.getParentAlgorithm() != null && geo2
.getParentAlgorithm() instanceof AlgoDependentCasCell) {
geo2index = geo2.getAlgoDepCasCellGeoConstIndex();
} else {
geo2index = geo2.getConstructionIndex();
}
return geo1Index > geo2index;
default: // alphabetical
return GeoElement.compareLabels(
geo1.getLabel(StringTemplate.defaultTemplate),
geo2.getLabel(StringTemplate.defaultTemplate)) > 0;
}
}
public int getIconShownHeight() {
return renderer.getIconShown().getIconHeight();
}
public int getOpenIconHeight() {
return renderer.getOpenIcon().getIconHeight();
}
public void updateFonts() {
Font font = app.getPlainFont();
setFont(font);
renderer.setFont(font);
}
}