package de.unisiegen.tpml.graphics.smallstep;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.Rectangle;
import java.util.Enumeration;
import javax.swing.Scrollable;
import javax.swing.SwingUtilities;
import javax.swing.event.TreeModelEvent;
import de.unisiegen.tpml.core.ProofGuessException;
import de.unisiegen.tpml.core.ProofNode;
import de.unisiegen.tpml.core.smallstep.SmallStepProofModel;
import de.unisiegen.tpml.core.smallstep.SmallStepProofNode;
import de.unisiegen.tpml.graphics.AbstractProofComponent;
import de.unisiegen.tpml.graphics.renderer.EnvironmentRenderer;
import de.unisiegen.tpml.graphics.renderer.PrettyStringRenderer;
/**
* The layouting of the SmallStep-GUI is a bit more complicated as the
* layouting of the other two GUIs because of two reasons.<br>
* <code>First</code>: One node within the <i>SmallStep-Tree</i> actualy needs
* to rows to get placed and<br>
* <code>Second</code>: Rules of each node have different widths but the need
* to get placed all aligned.<br>
* <br>
* When layouting this component (that is done in the {@link #relayout()}-method)
* first all nodes within the tree are check if they have an
* {@link de.unisiegen.tpml.graphics.smallstep.SmallStepNodeComponent} assigned
* to them. Then the tree is scanned for the node with the widest
* {@link de.unisiegen.tpml.graphics.smallstep.SmallStepNodeComponent#rules}.
* This value is assigned to each node, this way all nodes know the maximum width
* of all rules and they can use this for their own width. By doing so, all nodes
* will be horizontaly aligned.<br>
* <br>
* When this all is done the actual placing of the nodes is done.
* {@see #placeNode(SmallStepProofNode, int, int)} for this.
*
* @author Marcell Fischbach
* @author Benedikt Meurer
* @version $Rev$
*
* @see de.unisiegen.tpml.graphics.AbstractProofComponent
* @see de.unisiegen.tpml.graphics.smallstep.SmallStepView
* @see de.unisiegen.tpml.graphics.smallstep.SmallStepNodeComponent
* @see de.unisiegen.tpml.graphics.smallstep.SmallStepRulesComponent
* @see de.unisiegen.tpml.graphics.smallstep.SmallStepRuleLabel
*/
public class SmallStepComponent extends AbstractProofComponent implements Scrollable, Cloneable
{
/**
*
*/
private static final long serialVersionUID = 1022005553523956037L;
/**
* The origin {@link SmallStepProofModel}
*/
private SmallStepProofModel model;
/**
* The border in pixels around the nodes
*/
private int border;
/**
* The spacing between the nodes
*/
private int spacing;
/**
* The visible width is set via {@link #setAvailableWidth(int)}
* by the ScrollPane of {@link SmallStepView}.
*/
private int availableWidth;
/**
* Contains the <i>ProofNode</i> that has been inserted
* last.
*/
private ProofNode jumpNode;
/**
* the advanced value of the smallstepper.
*/
private boolean advanced;
/**
* Sets the default values.<br>
* <br>
* A border of 20 pixels and spacing of 10 pixels.<br>
* <br>
* To get a propper start a {@link #relayout()} is called
* manually.
*
* @param pProofModel
* @param pAdvanced
*/
public SmallStepComponent (SmallStepProofModel pProofModel, boolean pAdvanced)
{
super(pProofModel);
this.currentlyLayouting = false;
setLayout(null);
this.model = pProofModel;
this.border = 20;
this.spacing = 10;
this.advanced = pAdvanced;
// trigger the first layouting
relayout();
}
/**
* Returns <code>true</code> if the small step component is in advanced mode, <code>false</code>
* if its in beginner mode.
*
* @return <code>true</code> if advanced mode is active.
*
* @see #setAdvanced(boolean)
*/
boolean isAdvanced()
{
return this.advanced;
}
/**
* If <code>advanced</code> is <code>true</code>, the small step component will display
* only axiom rules in the rule menu, otherwise, in beginner mode, meta rules will also
* be displayed.
*
* @param pAdvanced <code>true</code> to display only axiom rules.
*
* @see #isAdvanced()
*/
void setAdvanced(boolean pAdvanced)
{
// check if we have a new setting
if (this.advanced != pAdvanced)
{
// remember the new setting
this.advanced = pAdvanced;
// make sure all nodes have valid user objects
checkForUserObject((SmallStepProofNode) this.proofModel.getRoot());
// update all active nodes
Enumeration<ProofNode> enumeration = this.proofModel.getRoot().postorderEnumeration();
while (enumeration.hasMoreElements())
{
// tell the component belonging to this node, that we have a new advanced state
SmallStepProofNode node = (SmallStepProofNode) enumeration.nextElement();
SmallStepNodeComponent component = (SmallStepNodeComponent) node.getUserObject();
component.setAdvanced(pAdvanced);
}
}
}
/**
* Sets the width that is available by the {@link SmallStepView}.
*/
@Override
public void setAvailableWidth(int pAvailableWidth)
{
this.availableWidth = pAvailableWidth;
relayout();
}
/**
* Returns the first child of the given node.<br>
* <br>
* If the node has no children, <i>null</i> is returned.
*
* @param node
* @return
*/
private SmallStepProofNode getFirstChild(SmallStepProofNode node)
{
try
{
return node.getFirstChild();
}
catch (Exception e)
{
// nothing
}
return null;
}
/**
* Traversing the ProofTree recursivly and adds a SmallStepNodeComponent
* where none is.<br>
* <br>
* Usualy only at newly added nodes the SmallStepNodeComponent is missing.
*
* @param node When calling this method: the rootNode of the tree.
*/
void checkForUserObject(SmallStepProofNode node)
{
if (node == null)
{
return;
}
SmallStepNodeComponent nodeComponent = (SmallStepNodeComponent) node.getUserObject();
if (nodeComponent == null)
{
// create the noded that has not been there yet
nodeComponent = new SmallStepNodeComponent(node, this.model, this.translator, this.spacing, this.advanced);
// add the needed listener
nodeComponent.addSmallStepNodeListener(new SmallStepNodeListener() {
public void nodeChanged(SmallStepNodeComponent pNode)
{
SmallStepComponent.this.relayout();
}
public void repaintAll()
{
SmallStepComponent.this.repaint();
}
public void requestJumpToNode(ProofNode pNode)
{
SmallStepComponent.this.setJumpNode(pNode);
}
});
nodeComponent.update();
// save it to the node
node.setUserObject(nodeComponent);
// and add the SmallStepNodeComponent to the gui
add(nodeComponent);
}
checkForUserObject(getFirstChild(node));
}
/**
* Causes all userobject from all nodes to reset the layout.<br>
* <br>
* Resetting means that every {@link PrettyStringRenderer} and
* {@link EnvironmentRenderer} recalculates their needed font sizes.
*/
private void resetUserObject(SmallStepProofNode node)
{
if (node == null)
{
return;
}
SmallStepNodeComponent nodeComponent = (SmallStepNodeComponent) node.getUserObject();
if (nodeComponent == null)
{
return;
}
nodeComponent.reset();
resetUserObject(getFirstChild(node));
}
/**
* Traverses the ProofTree recursivly and checks the needed size for
* the rule combo on the left-hand-side.<br>
* <br>
* The <i>currentWidth</i> is the current maximum width that has been
* evaluated. When calling this function this should be somthing small.
* Just set it to <b>0</b>.
*
* @param node When calling this method: the rootNode of the tree.
* @param pCurrentWidth Used internaly. Should be set to <b>0</b>.
* @return
*/
int checkMaxRuleWidth(SmallStepProofNode node, int pCurrentWidth)
{
int currentWidth = pCurrentWidth;
if (node == null)
{
return currentWidth;
}
// get the size of the current node
SmallStepNodeComponent nodeComponent = (SmallStepNodeComponent) node.getUserObject();
int nodeWidth = nodeComponent.getMinRuleSize().width;
// only the maximum width is of interest
currentWidth = Math.max(currentWidth, nodeWidth);
// return the recursive result of the next node
return checkMaxRuleWidth(getFirstChild(node), currentWidth);
}
/**
* Traverses the ProofTree recursivly and informing every node for
* the maximum size of the rule combo on the left-hand-side.
*
* @param node When calling this method: the rootNode of the tree.
* @param maxRuleWidth The maximum Width of the rules.
*/
void updateMaxRuleWidth(SmallStepProofNode node, int maxRuleWidth)
{
if (node == null)
{
return;
}
// inform the node of the max rule width
SmallStepNodeComponent nodeComponent = (SmallStepNodeComponent) node.getUserObject();
nodeComponent.setMaxRuleWidth(maxRuleWidth);
// proceed with the next child node
updateMaxRuleWidth(getFirstChild(node), maxRuleWidth);
}
/**
* Traverses the ProofTree recursivly and checks the size of
* the expression for every node.
*
* @param node When calling this method: the rootNode of the tree.
*/
void checkExpressionSize(SmallStepProofNode node)
{
if (node == null)
{
return;
}
SmallStepNodeComponent nodeComponent = (SmallStepNodeComponent) node.getUserObject();
nodeComponent.checkNeededExpressionSize(this.availableWidth - this.border);
// proceed with the next child
checkExpressionSize(getFirstChild(node));
}
/**
* Iterates through the entire tree an places every node.<br>
* <br>
* There are some things to take care of, when the nodes get placed:<br>
* <br>
* The expression and the rules of the parent node are in one row so
* when the nodes get placed the actualHeight of each node must be the
* maximum of both. They are placed together in on step. So the rules
* of each node are placed together with the expression of its child node,
* if there is one.<br>
* If there is no parent node (that would be the first node, the root node),
* only the expression needs to get places.<br>
* If the node has no child (that would be the last node in the tree),
* the rules must be placed directly because there is no child node that
* would place them.
*
* @param node The rootNode
* @param pX The horizontal start position
* @param pY Ther vertical start position
* @return The size needed to show all the nodes.
*/
Dimension placeNode(SmallStepProofNode pNode, int pX, int pY)
{
int x = pX;
int y = pY;
this.actualPageSpaceCounter = y;
SmallStepProofNode node = pNode;
Dimension size = new Dimension(0, 0);
// save the space the next node will be moved down
int movedDown = 0;
int lastNodeHeight = this.actualPageSpaceCounter;
while (node != null)
{
SmallStepNodeComponent nodeComponent = (SmallStepNodeComponent) node.getUserObject();
// set the origin of this node
nodeComponent.setOrigion(new Point(x, y));
// if the node has no parent node it appears to be the rootNode
//
// the expression of the rootNode can be placed without checking anything
if (node.getParent() == null)
{
nodeComponent.placeExpression();
// move the positioning
y += nodeComponent.getRuleTop();
this.actualPageSpaceCounter = y;
// evaluate the new dimensions
size.height = y;
}
else
{
// evaluate the max size of this nodes expression and the parent
// nodes rules
SmallStepProofNode parentNode = node.getParent();
SmallStepNodeComponent parentNodeComponent = (SmallStepNodeComponent) parentNode.getUserObject();
Dimension expSize = nodeComponent.getExpressionSize();
Dimension ruleSize = parentNodeComponent.getRuleSize();
int maxHeight = Math.max(expSize.height, ruleSize.height);
// provide printing
// if the actualPageSpaceCounter has not enough space for the next node perform a pagebrak
if (this.actualPageSpaceCounter + maxHeight + lastNodeHeight > this.availableHeight)
{
{
// save the space the node is moved down
movedDown = (this.availableHeight - this.actualPageSpaceCounter)/2; //Wäää?
// move the next node down
y += movedDown;
// restart the actualPageSpaceCounter
this.actualPageSpaceCounter = -movedDown; //Wäää?
// inform both component about the actual height they should use to
// place them
parentNodeComponent.setActualRuleHeight(maxHeight);
nodeComponent.setActualExpressionHeight(maxHeight);
// let both components place theire elements
parentNodeComponent.placeRules();
nodeComponent.placeExpression();
// this finishes the parentNode so it can be placed
parentNodeComponent.setBounds();
// the additional height come from the actual node
y += nodeComponent.getRuleTop();
this.actualPageSpaceCounter += nodeComponent.getRuleTop();
//System.out.println(maxHeight+" - "+nodeComponent.getRuleTop());
// evaluate the new dimensions
size.height = y;
// the actual width of the entire component can now be checked
// on the finshed node. the parent node
size.width = Math.max(size.width, x + parentNodeComponent.getSize().width);
}
}
else
{
// inform both component about the actual height they should use to
// place them
parentNodeComponent.setActualRuleHeight(maxHeight);
nodeComponent.setActualExpressionHeight(maxHeight);
// let both components place theire elements
// the movedDown saves the information how far the the parent-Rules must be moved down...
parentNodeComponent.placeRules(movedDown);
nodeComponent.placeExpression();
// this finishes the parentNode so it can be placed
parentNodeComponent.setBounds(movedDown);
movedDown = 0;
// the additional height come from the actual node
y += nodeComponent.getRuleTop();
this.actualPageSpaceCounter += nodeComponent.getRuleTop();
//System.out.println(maxHeight+" - "+nodeComponent.getRuleTop());
// evaluate the new dimensions
size.height = y;
// the actual width of the entire component can now be checked
// on the finshed node. the parent node
size.width = Math.max(size.width, x + parentNodeComponent.getSize().width);
}
lastNodeHeight = maxHeight;
//tmpPaper += maxHeight;
}
// if the node has no children the rules need to get
// placed here with the expression
if (getFirstChild(node) == null)
{
if (this.model.isFinished())
{
nodeComponent.hideRules();
nodeComponent.setBounds();
size.width = Math.max(size.width, x + nodeComponent.getSize().width);
}
else
{
// the rules can savely be positioned
nodeComponent.placeRules();
// and the node itself can be placed
nodeComponent.setBounds();
// evaluate the new dimension
size.height += nodeComponent.getActualRuleHeight();
// the actual width of the entire component can now be checked
// on the finshed node.
size.width = Math.max(size.width, x + nodeComponent.getSize().width);
}
return size;
}
node = node.getFirstChild();
}
return size;
}
/**
* Does the entire layouting of the SmallStepComponent.<br>
* <br>
* All nodes in the tree will get a SmallStepNodeComponent, the size
* of the widest rule combo on the left-hand-site is evaluated. <br>
* In order to render all expression alligned every node is informed of
* this width.
*
*/
@Override
protected void relayout()
{
if (this.currentlyLayouting)
{
return;
}
this.currentlyLayouting = true;
SwingUtilities.invokeLater(new Runnable() {
public void run()
{
doRelayout();
}
});
}
/**
* dose a relayout
*
*/
protected void doRelayout()
{
// get the rootNode it will be used many time
SmallStepProofNode rootNode = (SmallStepProofNode) SmallStepComponent.this.getProofModel().getRoot();
// check if all nodes have a propper SmallStepNodeComponent
checkForUserObject(rootNode);
// find the maximum width of the rules and inform the entire tree
int maxRuleWidth = checkMaxRuleWidth(rootNode, 0);
updateMaxRuleWidth(rootNode, maxRuleWidth);
// evaluate the the sizes of the expression
checkExpressionSize(rootNode);
// now that the basics for the nodes are found,
// they can be placed
Dimension size = placeNode(rootNode, SmallStepComponent.this.getThisBorder(), SmallStepComponent.this.getThisBorder());
// the needed size evaluaded by placing the nodes gets
// widened a bit to have a nice border around the component
size.width += SmallStepComponent.this.getThisBorder();
size.height += SmallStepComponent.this.getThisBorder();
// this size is used to determin all the sizes of the component
setPreferredSize(size);
setSize(size);
setMinimumSize(size);
setMaximumSize(size);
SmallStepComponent.this.setCurrentlyLayouting(false);
SmallStepComponent.this.jumpToNodeVisible();
}
/**
* Resets all user objects by calling {@link #resetUserObject(SmallStepProofNode)}
* on the rootNode of the model. <br>
* This will cause the {@link PrettyStringRenderer} to update all fonts an to
* recalculate the line wrappings etc...
*/
@Override
protected void resetLayout()
{
resetUserObject((SmallStepProofNode) this.proofModel.getRoot());
}
/**
* Just saves the node that has been inserted.
*/
@Override
protected void nodesInserted(TreeModelEvent event)
{
Object[] children = event.getChildren();
if (children != null)
{
this.jumpNode = (ProofNode) children[0];
}
else
{
this.jumpNode = null;
}
}
/**
* Causes an {@link SmallStepNodeComponent#update() on all
* nodes that have changed.<br>
* <br>
* When all updates are done relayouts the View.
*
* @see #relayout()
*/
@Override
protected void nodesChanged(TreeModelEvent event)
{
boolean relayout = false;
Object[] children = event.getChildren();
if (children == null)
{
// if the children are null and the path only contains one element
// this element is the root node.
if (event.getPath().length == 1)
{
SmallStepProofNode proofNode = (SmallStepProofNode) event.getPath()[0];
SmallStepNodeComponent nodeComponent = (SmallStepNodeComponent) proofNode.getUserObject();
if (nodeComponent != null)
{
nodeComponent.update();
relayout = true;
}
}
}
else
{
for (int i = 0; i < children.length; i++)
{
if (children[i] instanceof ProofNode)
{
SmallStepProofNode proofNode = (SmallStepProofNode) children[i];
SmallStepNodeComponent nodeComponent = (SmallStepNodeComponent) proofNode.getUserObject();
if (nodeComponent != null)
{
nodeComponent.update();
relayout = true;
}
}
}
}
if (relayout)
{
relayout();
}
}
/**
* Removes all userobjects from the nodes that will be removed
* by the {@link SmallStepProofModel} later.
*/
@Override
protected void nodesRemoved(TreeModelEvent event)
{
Object[] children = event.getChildren();
if (children == null)
{
return;
}
for (int i = 0; i < children.length; i++)
{
if (children[i] instanceof ProofNode)
{
SmallStepProofNode proofNode = (SmallStepProofNode) children[i];
SmallStepNodeComponent nodeComponent = (SmallStepNodeComponent) proofNode.getUserObject();
if (nodeComponent != null)
{
remove(nodeComponent);
proofNode.setUserObject(null);
}
}
}
}
/**
* Just causes a relayout.
*
* @see #relayout()
*/
@Override
protected void treeContentChanged()
{
relayout();
}
/**
* Finds the last unprooven node within the ProofTree und calls a guess on
* the proofModel with the node.
*
* @throws IllegalStateException
* @throws ProofGuessException
*/
public void guess() throws IllegalStateException, ProofGuessException
{
Enumeration<ProofNode> enumeration = this.proofModel.getRoot().postorderEnumeration();
while (enumeration.hasMoreElements())
{
ProofNode node = enumeration.nextElement();
if (!node.isProven())
{
this.proofModel.guess(node);
return;
}
}
throw new IllegalStateException("Unable to find next node");
}
/**
* Scroll the current visible area to the rectangle where
* the saved {@link #jumpNode} lays.
*
*/
void jumpToNodeVisible()
{
if (this.jumpNode == null)
{
return;
}
// get the Component nodes to evaluate the positions
// on the viewport
SmallStepNodeComponent node = (SmallStepNodeComponent) this.jumpNode.getUserObject();
if (node == null)
{
return;
}
// get the visible rect to ensure the x coordinate is in the
// visible area. only vertical scolling is requested
Rectangle visibleRect = this.getVisibleRect();
Rectangle rect = new Rectangle();
rect.x = visibleRect.x;
rect.y = node.getY();
rect.width = 1;
rect.height = node.getHeight();
this.scrollRectToVisible(rect);
this.jumpNode = null;
}
//
// Methods for painting purposes
//
@Override
protected void paintComponent(Graphics gc)
{
// if you want to see the whole small step component you might make it visible here
gc.setColor(Color.WHITE);
gc.fillRect(0, 0, getWidth() - 1, getHeight() - 1);
}
//
// Implementation of the Scrollable interface
//
public Dimension getPreferredScrollableViewportSize()
{
return getPreferredSize();
}
public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction)
{
return 10;
}
public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction)
{
return 25;
}
public boolean getScrollableTracksViewportWidth()
{
return false;
}
public boolean getScrollableTracksViewportHeight()
{
return false;
}
public SmallStepComponent clone (){
try {
return (SmallStepComponent)super.clone();
} catch (CloneNotSupportedException e) {
return null;
}
}
/**
* @param pJumpNode the jumpNode to set
*/
public void setJumpNode(ProofNode pJumpNode)
{
this.jumpNode = pJumpNode;
}
/**
* @return the border
*/
public int getThisBorder()
{
return this.border;
}
@Override
protected void forcedRelayout()
{
doRelayout();
}
}