use of gov.sandia.n2a.ui.eq.undo.ChangeVariable in project n2a by frothga.
the class NodeVariable method makeAdd.
@Override
public Undoable makeAdd(String type, JTree tree, MNode data, Point location) {
if (type.isEmpty()) {
FilteredTreeModel model = (FilteredTreeModel) tree.getModel();
if (model.getChildCount(this) == 0 || tree.isCollapsed(new TreePath(getPath())))
return ((NodeBase) parent).makeAdd("Variable", tree, data, location);
type = "Equation";
}
if (isBinding && !type.equals("Annotation"))
return ((NodeBase) parent).makeAdd(type, tree, data, location);
if (type.equals("Equation")) {
if (data != null) {
// includes @
String key = data.key();
// Determine if pasting over empty variable (no equations of any type except a naked combiner)
Variable.ParsedValue existing = new Variable.ParsedValue(source.get());
boolean hasEquations = !existing.condition.isEmpty() || !existing.expression.isEmpty();
if (!hasEquations) {
// unfiltered
Enumeration<?> children = children();
while (children.hasMoreElements()) {
Object c = children.nextElement();
if (c instanceof NodeEquation) {
hasEquations = true;
break;
}
}
}
if (// no equations, or possibly a naked combiner
!hasEquations) {
String value = existing.combiner + data.get() + key;
if (value.endsWith("@"))
value = value.substring(0, value.length() - 1);
return new ChangeVariable(this, source.key(), value);
}
// Determine if pasting over an existing equation
NodeBase existingEquation = child(key);
if (existingEquation != null) {
// remove the @, since ChangeEquation expects strings from ParsedValue
key = key.substring(1);
String combiner = existing.combiner;
String newValue = data.get();
String existingValue = existingEquation.source.get();
if (!newValue.equals(existingValue))
return new ChangeEquation(this, key, combiner, existingValue, key, combiner, newValue);
// else the user intent is to duplicate the equation for convenience before editing it.
// In this case, we need to create a new equation with alternate key.
data = new MVolatile(existingValue, "@" + key + "&&");
}
}
// Determine index for new equation
int index = 0;
NodeBase child = null;
TreePath path = tree.getLeadSelectionPath();
if (path != null)
child = (NodeBase) path.getLastPathComponent();
if (child != null && child.getParent() == this)
index = getIndex(child);
while (index > 0 && !(getChildAt(index) instanceof NodeEquation)) index--;
if (index < getChildCount() && getChildAt(index) instanceof NodeEquation)
index++;
// Create an AddEquation action
return new AddEquation(this, index, data);
} else if (type.equals("Annotation")) {
// Determine index at which to insert new annotation
int index = 0;
int count = getChildCount();
while (index < count && !(children.get(index) instanceof NodeReference)) index++;
return new AddAnnotation(this, index, data);
} else if (type.equals("Annotations")) {
// In this case, everything under this node will be rebuilt, so no need to worry about insertion index.
return new ChangeAnnotations(this, data);
} else if (type.equals("Reference")) {
return new AddReference(this, getChildCount(), data);
} else if (type.equals("References")) {
return new ChangeReferences(this, data);
}
// refer all other requests up the tree
return ((NodeBase) parent).makeAdd(type, tree, data, location);
}
use of gov.sandia.n2a.ui.eq.undo.ChangeVariable in project n2a by frothga.
the class NodeVariable method applyEdit.
/**
* Enforces all the different use cases associated with editing of variables.
* This is the most complex node class, and does the most work. Some of the use cases include:
* Create a new variable.
* Move an existing variable tree, perhaps overriding an inherited one, perhaps also with a change of value.
* Insert an equation under ourselves.
* Insert an equation under another variable.
*/
@Override
public void applyEdit(JTree tree) {
String input = toString();
UndoManager um = MainFrame.instance.undoManager;
boolean canceled = um.getPresentationName().equals("AddVariable");
if (input.isEmpty()) {
delete(canceled);
return;
}
String[] parts = input.split("=", 2);
String nameAfter = parts[0].trim().replaceAll("[ \\n\\t]", "");
String valueAfter;
if (// Explicit assignment
parts.length > 1) {
valueAfter = parts[1].trim();
if (valueAfter.startsWith("$kill"))
valueAfter = valueAfter.substring(5).trim();
} else {
// Input was a variable name with no assignment.
valueAfter = "";
}
// What follows is a series of analyses, most having to do with enforcing constraints
// on name change (which implies moving the variable tree or otherwise modifying another variable).
// Handle a naked expression.
String nameBefore = source.key();
String valueBefore = getValue();
if (// Not a proper variable name. The user actually passed a naked expression, so resurrect the old (probably auto-assigned) variable name.
!isValidIdentifier(nameAfter)) {
nameAfter = nameBefore;
valueAfter = input;
}
// Handle creation of $inherit node.
FilteredTreeModel model = (FilteredTreeModel) tree.getModel();
boolean canInject = getChildCount() == 0 && source.isFromTopDocument();
// Only a heuristic. Could also be an existing variable with no equation.
boolean newlyCreated = canInject && valueBefore.isEmpty();
NodeBase parent = (NodeBase) getParent();
if (nameAfter.equals("$inherit")) {
if (parent.child(nameAfter) == null) {
if (newlyCreated) {
parent.source.clear(nameBefore);
// No need to update GUI, because AddInherit rebuilds parent.
um.apply(new AddInherit((NodePart) parent, valueAfter));
} else {
um.apply(new ChangeVariableToInherit(this, valueAfter));
}
return;
}
// Reject name change, because $inherit already exists. User should edit it directly.
nameAfter = nameBefore;
}
// Prevent illegal name change. (Don't override another top-level node. Don't overwrite a non-variable node.)
NodeBase nodeAfter = parent.child(nameAfter);
if (nodeAfter != null) {
boolean isVariable = nodeAfter instanceof NodeVariable;
boolean different = nodeAfter != this;
boolean topdoc = nodeAfter.source.isFromTopDocument();
boolean revoked = nodeAfter.source.get().equals("$kill");
if (!isVariable || (different && topdoc && !revoked && !canInject)) {
nameAfter = nameBefore;
nodeAfter = this;
}
}
// If there's nothing to do, then repaint the node and quit.
if (nameBefore.equals(nameAfter) && valueBefore.equals(valueAfter)) {
setUserObject();
model.nodeChanged(this);
return;
}
// Detect and handle special cases
if (// There exists a variable in the target location, so we may end up injecting an equation into a multiconditional expression.
nodeAfter != null) {
// In this section, "dest" refers to state of target node before it is overwritten, while "after" refers to newly input values from user.
Variable.ParsedValue piecesDest = new Variable.ParsedValue(((NodeVariable) nodeAfter).getValue());
Variable.ParsedValue piecesAfter = new Variable.ParsedValue(valueAfter);
boolean expressionAfter = !piecesAfter.expression.isEmpty() || !piecesAfter.condition.isEmpty();
// If the user doesn't specify a combiner, absorb it from our destination.
if (piecesAfter.combiner.isEmpty())
piecesAfter.combiner = piecesDest.combiner;
int equationCount = 0;
NodeEquation equationMatch = null;
Enumeration<?> childrenAfter = nodeAfter.children();
while (childrenAfter.hasMoreElements()) {
Object c = childrenAfter.nextElement();
if (c instanceof NodeEquation) {
equationCount++;
NodeEquation e = (NodeEquation) c;
if (e.source.key().substring(1).equals(piecesAfter.condition))
equationMatch = e;
}
}
if (nodeAfter == this) {
if (// Inject an equation into ourselves.
equationCount > 0 && expressionAfter) {
if (// New equation
equationMatch == null) {
// It is possible to add an equation revocation here without there being an existing equation to revoke.
um.apply(new AddEquation(this, piecesAfter.condition, piecesAfter.combiner, piecesAfter.expression));
} else // Overwrite an existing equation
{
Variable.ParsedValue piecesMatch = new Variable.ParsedValue(piecesDest.combiner + equationMatch.source.get() + equationMatch.source.key());
um.apply(new ChangeEquation(this, piecesMatch.condition, piecesMatch.combiner, piecesMatch.expression, piecesAfter.condition, piecesAfter.combiner, piecesAfter.expression));
}
return;
}
} else // Node has been renamed.
{
if (// Inject into/over an existing variable.
canInject) {
// Remove this variable, regardless of what we do to nodeAfter.
um.addEdit(new CompoundEdit());
um.apply(new DeleteVariable(this, canceled));
// Decide what change (if any) to apply to nodeAfter.
if (expressionAfter) {
NodeVariable nva = (NodeVariable) nodeAfter;
if (equationCount == 0) {
if (// Directly overwrite the target, since they share the say name and condition.
piecesAfter.condition.equals(piecesDest.condition)) {
um.apply(new ChangeVariable(nva, nameAfter, valueAfter, getKeyPath()));
} else // Inject new equation and change target into a multiconditional variable.
{
// Possible to revoke non-existent equation
um.apply(new AddEquation(nva, piecesAfter.condition, piecesAfter.combiner, piecesAfter.expression, getKeyPath()));
}
} else {
if (// Add new equation to an existing multiconditional.
equationMatch == null) {
// Possible to revoke non-existent equation
um.apply(new AddEquation(nva, piecesAfter.condition, piecesAfter.combiner, piecesAfter.expression, getKeyPath()));
} else // Overwrite an existing equation in a multiconditional
{
Variable.ParsedValue piecesMatch = new Variable.ParsedValue(piecesDest.combiner + equationMatch.source.get() + equationMatch.source.key());
um.apply(new ChangeEquation(nva, piecesMatch.condition, piecesMatch.combiner, piecesMatch.expression, piecesAfter.condition, piecesAfter.combiner, piecesAfter.expression, getKeyPath()));
}
}
}
um.endCompoundEdit();
return;
}
}
}
// The @ will be hidden most of the time, but it will distinguish a variable from a part.
if (valueAfter.isEmpty() && !hasEquations())
valueAfter = "@";
um.apply(new ChangeVariable(this, nameAfter, valueAfter));
}
use of gov.sandia.n2a.ui.eq.undo.ChangeVariable in project n2a by frothga.
the class NodePart method suggestConnections.
/**
* Locates likely endpoints for unsatisfied connections.
* Subroutine of equation tree transfer handler. Should be called while a compound edit is open.
*/
public static void suggestConnections(List<NodePart> fromParts, List<NodePart> toParts) {
// Set connectionTarget flags
for (NodePart fromPart : fromParts) fromPart.connectionTarget = false;
for (NodePart toPart : toParts) toPart.connectionTarget = false;
for (NodePart toPart : toParts) {
if (toPart.connectionBindings == null)
continue;
for (Entry<String, NodePart> e : toPart.connectionBindings.entrySet()) {
NodePart p = e.getValue();
if (p != null)
p.connectionTarget = true;
}
}
for (NodePart fromPart : fromParts) {
if (fromPart.connectionBindings == null)
continue;
for (Entry<String, NodePart> e : fromPart.connectionBindings.entrySet()) {
NodePart p = e.getValue();
if (p != null)
p.connectionTarget = true;
}
}
// Scan for matches
Dimension viewSize = PanelModel.instance.panelEquations.panelEquationGraph.getExtentSize();
// Anything beyond this is too far out of sight to allow automatic connection.
double limit = viewSize.width + viewSize.height;
for (NodePart fromPart : fromParts) {
// Collect candidates for each unsatisfied connection.
List<UnsatisfiedConnection> unsatisfied = fromPart.getUnsatisfiedConnections();
for (UnsatisfiedConnection u : unsatisfied) {
u.candidates = new ArrayList<NodePart>();
for (NodePart toPart : toParts) {
// Is toPart a descendant of any of the classes acceptable to the connection?
boolean match = false;
Set<String> ancestors = toPart.getAncestors();
for (String c : u.classes) {
if (ancestors.contains(c)) {
match = true;
break;
}
}
if (match)
u.candidates.add(toPart);
}
}
// since in later code we iterate backward through the list of candidates.
if (fromPart.graph != null) {
Point fromCenter = fromPart.graph.getCenter();
Map<Double, NodePart> sorted = new TreeMap<Double, NodePart>();
for (UnsatisfiedConnection u : unsatisfied) {
for (NodePart toPart : u.candidates) {
double distance = Double.MAX_VALUE;
if (toPart.graph != null) {
Point toCenter = toPart.graph.getCenter();
int dx = fromCenter.x - toCenter.x;
int dy = fromCenter.y - toCenter.y;
distance = Math.sqrt(dx * dx + dy * dy);
}
sorted.put(distance, toPart);
}
u.candidates.clear();
for (Entry<Double, NodePart> e : sorted.entrySet()) {
double distance = e.getKey();
if (distance < Double.MAX_VALUE && distance > limit)
continue;
u.candidates.add(e.getValue());
}
}
}
// Narrow down lists of candidates to one best choice for each endpoint.
// The complicated tests below are merely heuristics for better connection choices,
// not absolute rules.
int count = unsatisfied.size();
for (int i = 0; i < count; i++) {
UnsatisfiedConnection u = unsatisfied.get(i);
// Minimize connections to the same target.
for (int k = u.candidates.size() - 1; k >= 0; k--) {
NodePart c = u.candidates.get(k);
// Check candidates for peer endpoints.
boolean duplicate = false;
for (int j = i + 1; j < count && !duplicate; j++) {
UnsatisfiedConnection v = unsatisfied.get(j);
duplicate = v.candidates.contains(c);
}
if (duplicate) {
if (u.candidates.size() > 1)
u.candidates.remove(k);
continue;
}
// Also scan satisfied endpoints, if any.
if (fromPart.connectionBindings.values().contains(c) && u.candidates.size() > 1)
u.candidates.remove(k);
}
// Minimize connections to a target that already receives connections.
for (int k = u.candidates.size() - 1; k >= 0; k--) {
NodePart c = u.candidates.get(k);
if (c.connectionTarget && u.candidates.size() > 1)
u.candidates.remove(k);
}
// Reserve the endpoint.
if (// If more than one candidate still exists, then implicitly they are all unique to this binding.
u.candidates.size() == 1) {
NodePart c = u.candidates.get(0);
for (int j = i + 1; j < count; j++) {
UnsatisfiedConnection v = unsatisfied.get(j);
if (v.candidates.size() > 1)
v.candidates.remove(c);
}
}
}
// If all endpoints go to nodes that are already connected, then don't connect at all.
// (Instead, assume that user is started a new constellation, and simply inserted the connection object first.)
boolean targetsFull = true;
for (UnsatisfiedConnection u : unsatisfied) {
if (u.candidates.size() != 1 || !u.candidates.get(0).connectionTarget) {
targetsFull = false;
break;
}
}
if (targetsFull)
continue;
// Assign the first candidate, as it should be sorted closest to fromPart.
for (UnsatisfiedConnection u : unsatisfied) {
if (u.candidates.size() == 0)
continue;
NodePart toPart = u.candidates.get(0);
// This must exist.
NodeVariable v = (NodeVariable) fromPart.child(u.alias);
MainFrame.instance.undoManager.apply(new ChangeVariable(v, u.alias, toPart.source.key()));
toPart.connectionTarget = true;
}
}
}
Aggregations