/*
* Copyright (c) 2007 BUSINESS OBJECTS SOFTWARE LIMITED
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of Business Objects nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
/*
* DisplayedValueGem.java
* Creation Date: Jan 28, 2003
* By: Ken Wong
*/
package org.openquark.gems.client;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import javax.swing.JOptionPane;
import org.openquark.cal.compiler.FieldName;
import org.openquark.cal.compiler.ModuleTypeInfo;
import org.openquark.cal.compiler.TypeExpr;
import org.openquark.gems.client.AutoburnLogic.AutoburnInfo;
import org.openquark.gems.client.AutoburnLogic.AutoburnUnifyStatus;
import org.openquark.gems.client.Gem.PartInput;
import org.openquark.gems.client.utilities.ExtendedUndoableEditSupport;
/**
* This class handles all the 'burning' associated tasks in the TableTop
* @author Ken Wong
* Creation Date: Jan 28th 2003
*/
class TableTopBurnManager {
private final TableTop tableTop;
/** the set of inputs which are manually burnt. Any other burnt inputs are autoburnt. */
private final transient Set<PartInput> manuallyBurntInputs;
/*
* Autoburn state
*/
/** the last gem on which we tried autoburn */
private transient Gem autoBurnLastGem;
/** the last TypeExpr with which we tried to autoburn it */
private transient TypeExpr autoBurnLastType;
/** the last result we got from autoburn */
private transient AutoburnLogic.AutoburnAction autoBurnLastResult = AutoburnLogic.AutoburnAction.NOTHING;
/**
* Constructor for a TableTopBurnManager.
* @param tableTop
*/
TableTopBurnManager(TableTop tableTop) {
this.tableTop = tableTop;
manuallyBurntInputs = new HashSet<PartInput>();
}
/**
* Returns the AutoburnAction object that describes the results of the last
* autoburn attempt
* @return AutoburnLogic.AutoburnAction
*/
AutoburnLogic.AutoburnAction getAutoburnLastResult() {
return autoBurnLastResult;
}
/**
* Returns the last gem that was autoburned
* @return gem
*/
Gem getAutoburnLastGem() {
return autoBurnLastGem;
}
/**
* Returns the type of the last autoburn attempt
* @return TypeExpr
*/
TypeExpr getAutoburnLastType() {
return autoBurnLastType;
}
/**
* Invalidates the existing autoburn state
*/
void invalidateAutoburnState() {
autoBurnLastGem = null;
autoBurnLastType = null;
autoBurnLastResult = AutoburnLogic.AutoburnAction.NOTHING;
}
/**
* Handle the user gesture to autoburn a gem (or undo it).
* Autoburning will check possible combinations of burning the given gem's inputs to
* see if there is a single possibility (combination of inputs burned) that will unify
* with a specific output type. If there is, then these inputs will be burned.
* Undoing an autoburn will unburn anything automatically burned (but not manually burned).
*
* @param thisGem Gem the gem whose state to change
* @param typeToUnify TypeExpr the type with which to unify the output
* @param burn whether to autoburn the gem as applied, or to undo the autoburn.
* @return AutoburnAction the action we performed: NOTHING, BURNED, MULTIPLE, IMPOSSIBLE or UNBURNED.
* AUTOBURN_MULTIPLE means we didn't do anything because there was more than one burn possibility.
* AUTOBURN_IMPOSSIBLE means we didn't do anything because the gems can't connect even if burned.
*/
AutoburnLogic.AutoburnAction handleAutoburnGemGesture(Gem thisGem, TypeExpr typeToUnify, boolean burn) {
ModuleTypeInfo currentModuleTypeInfo = tableTop.getCurrentModuleTypeInfo();
// If the gem can't have inputs check if it is connectable.
int numArgs = thisGem.getNInputs();
if (numArgs == 0 || thisGem instanceof CollectorGem) {
TypeExpr gemType = thisGem.getOutputPart().getType();
if (TypeExpr.canUnifyType(gemType, typeToUnify, currentModuleTypeInfo)) {
return AutoburnLogic.AutoburnAction.NOTHING;
} else {
return AutoburnLogic.AutoburnAction.IMPOSSIBLE;
}
}
// keep track of if the gem changed
boolean gemChanged = false;
if (burn) {
// see if we tried this already
if (autoBurnLastGem == thisGem && autoBurnLastType == typeToUnify && autoBurnLastResult != AutoburnLogic.AutoburnAction.UNBURNED) {
return autoBurnLastResult;
}
// update autoburn state
autoBurnLastGem = thisGem;
autoBurnLastType = typeToUnify;
if (thisGem instanceof RecordFieldSelectionGem &&
!((RecordFieldSelectionGem)thisGem).isFieldFixed() &&
!thisGem.isConnected() &&
typeToUnify.getArity() > 0) {
//special case for burning a record field selection gem input pieces
//we must consider changing the selected field.
TypeExpr[] typePieces = typeToUnify.getTypePieces(1);
FieldName fieldName = RecordFieldSelectionGem.pickFieldName(typePieces[0], typePieces[1], currentModuleTypeInfo);
if (fieldName != null) {
//there is a valid field that can be picked if the input is burnt
autoBurnLastResult = AutoburnLogic.AutoburnAction.BURNED;
gemChanged = true;
doAutoburnUserAction(thisGem, new int[] { 0 });
} else if (TypeExpr.canUnifyType(thisGem.getOutputPart().getType(), typeToUnify, currentModuleTypeInfo)) {
autoBurnLastResult = AutoburnLogic.AutoburnAction.NOTHING;
} else {
autoBurnLastResult = AutoburnLogic.AutoburnAction.IMPOSSIBLE;
}
} else {
AutoburnInfo autoburnInfo =
GemGraph.isAncestorOfBrokenGemForest(thisGem) ?
null :
AutoburnLogic.getAutoburnInfo(typeToUnify, thisGem, tableTop.getTypeCheckInfo());
AutoburnUnifyStatus autoburnUnifyStatus =
autoburnInfo != null ?
autoburnInfo.getAutoburnUnifyStatus() :
AutoburnLogic.AutoburnUnifyStatus.NOT_POSSIBLE;
if (autoburnUnifyStatus == AutoburnUnifyStatus.UNAMBIGUOUS_NOT_NECESSARY) {
// This means the gems can be connected either by burning them or by
// just connecting them without burning. In this case we want to determine
// which action is best and do that.
int noBurnCloseness = TypeExpr.getTypeCloseness(thisGem.getOutputPart().getType(), typeToUnify, currentModuleTypeInfo);
if (autoburnInfo.getMaxTypeCloseness() > noBurnCloseness) {
autoburnUnifyStatus = AutoburnUnifyStatus.UNAMBIGUOUS;
} else {
autoburnUnifyStatus = AutoburnUnifyStatus.NOT_NECESSARY;
}
// Now resume checking the basic possibilities.
}
if (autoburnUnifyStatus == AutoburnUnifyStatus.UNAMBIGUOUS) {
autoBurnLastResult = AutoburnLogic.AutoburnAction.BURNED;
gemChanged = true;
// Set the inputs according to the unambiguous burning.
// This should trigger burn events on listeners, causing the GemGraph to be re-typed.
AutoburnLogic.BurnCombination burnCombination = autoburnInfo.getBurnCombinations().get(0);
int[] burnArray = burnCombination.getInputsToBurn();
doAutoburnUserAction(thisGem, burnArray);
} else if (autoburnUnifyStatus == AutoburnUnifyStatus.AMBIGUOUS) {
autoBurnLastResult = AutoburnLogic.AutoburnAction.MULTIPLE;
} else if (autoburnUnifyStatus == AutoburnUnifyStatus.NOT_POSSIBLE) {
autoBurnLastResult = AutoburnLogic.AutoburnAction.IMPOSSIBLE;
} else {
// Catches AutoburnUnifyType.NOT_NECESSARY and AutoburnUnifyType.AMBIGUOUS_NOT_NECESSARY.
// The last one means that it could be burned but the buring is ambiguous.
// In that case just connect without buring.
autoBurnLastResult = AutoburnLogic.AutoburnAction.NOTHING;
}
}
} else {
// undo any autoburn.
gemChanged = doUnburnAutomaticallyBurnedInputsUserAction(thisGem);
// set the result type
if (gemChanged) {
autoBurnLastResult = AutoburnLogic.AutoburnAction.UNBURNED;
} else {
autoBurnLastResult = AutoburnLogic.AutoburnAction.NOTHING;
}
}
return autoBurnLastResult;
}
/**
* Set the burn status of an input and post an undoable edit.
* @param input the input to burn
* @param newBurnStatus the new burn status of the input.
*/
void doSetInputBurnStatusUserAction(Gem.PartInput input, AutoburnLogic.BurnStatus newBurnStatus) {
// save the old burn status for the undo manager
AutoburnLogic.BurnStatus oldBurnStatus = getBurnStatus(input);
// If the status will changed, burn and notify undo managers of the edit..
if (oldBurnStatus != newBurnStatus) {
burnInput(input, newBurnStatus);
tableTop.getUndoableEditSupport().postEdit(new UndoableBurnInputEdit(tableTop, input, oldBurnStatus));
}
}
/**
* Unburn any inputs which are automatically burnt
* @param gem Gem the gem on which to burn inputs
* @return boolean true if any inputs were unburnt
*/
boolean doUnburnAutomaticallyBurnedInputsUserAction(Gem gem) {
// Increment the update level to aggregate any burns with connection change edits.
ExtendedUndoableEditSupport undoableEditSupport = tableTop.getUndoableEditSupport();
undoableEditSupport.beginUpdate();
boolean anyUnburnt = false;
int numInputs = gem.getNInputs();
for (int i = 0; i < numInputs; i++) {
Gem.PartInput input = gem.getInputPart(i);
if (getBurnStatus(input) == AutoburnLogic.BurnStatus.AUTOMATICALLY_BURNT) {
doSetInputBurnStatusUserAction(input, AutoburnLogic.BurnStatus.NOT_BURNT);
anyUnburnt = true;
}
}
if (anyUnburnt) {
// Decrement the update level. This will post the edit if the level is zero.
undoableEditSupport.endUpdate();
updateForBurn();
} else {
// Discard the edit because nothing happened.
undoableEditSupport.endUpdateNoPost();
}
return anyUnburnt;
}
/**
* Update the tabletop after a burn action has taken place.
*/
private void updateForBurn() {
for (final Gem gem : tableTop.getGemGraph().getGems()) {
if (gem instanceof CodeGem) {
CodeGemEditor codeGemEditor = tableTop.getCodeGemEditor((CodeGem)gem);
codeGemEditor.updateForBurn(tableTop.getTypeCheckInfo());
}
}
tableTop.updateForGemGraph();
}
/**
* Handle the gesture where the user (un)burns a displayed input.
* A dialog may pop up asking for input, if the burning breaks a gem tree.
*
* @param dPartToBurn the Displayed part in question
* @return boolean whether the displayed input was burnt
*/
boolean handleBurnInputGesture(DisplayedGem.DisplayedPart dPartToBurn){
// check if burnable:
// must be an unconnected input that isn't on a collector gem.
// also must not be defined by anything broken.
Gem gem = dPartToBurn.getDisplayedGem().getGem();
if (!(dPartToBurn instanceof DisplayedGem.DisplayedPartInput) ||
((DisplayedGem.DisplayedPartInput)dPartToBurn).isConnected() ||
(dPartToBurn.getGem() instanceof CollectorGem) ||
(GemGraph.isAncestorOfBrokenGemForest(gem.getRootGem()))) {
return false;
}
Gem.PartInput inputToBurn = ((DisplayedGem.DisplayedPartInput)dPartToBurn).getPartInput();
// see if toggling the input burn state invalidates the tree
boolean burnOk = !(inputToBurn.burnBreaksGem(tableTop.getTypeCheckInfo()));
// if the (un)burn ok, toggle burnt state, else warn and display a dialog re: what to do
if (burnOk) {
doSetInputBurnStatusUserAction(inputToBurn, inputToBurn.isBurnt() ? AutoburnLogic.BurnStatus.NOT_BURNT : AutoburnLogic.BurnStatus.MANUALLY_BURNT);
updateForBurn();
return true;
} else {
// burning breaks the gem tree. Highlight the output connection as potentially disconnecting.
DisplayedConnection dConn = dPartToBurn.getDisplayedGem().getDisplayedOutputPart().getDisplayedConnection(); // should always be connected
tableTop.setBadDisplayedConnection(dConn, true);
tableTop.getTableTopPanel().repaint(dConn.getBounds());
// Formulate the warning.
String titleString = GemCutter.getResourceString("WarningDialogTitle");
String message;
if (inputToBurn.isBurnt()){
message = GemCutter.getResourceString("UnburnWarning");
} else {
message = GemCutter.getResourceString("BurnWarning");
}
ExtendedUndoableEditSupport undoableEditSupport = tableTop.getUndoableEditSupport();
// Show the warning. Ask what to do..
int option = JOptionPane.showConfirmDialog(tableTop.getTableTopPanel(), message, titleString,
JOptionPane.YES_NO_OPTION, javax.swing.JOptionPane.WARNING_MESSAGE);
// only do anything if the user answered yes.
if (option == JOptionPane.YES_OPTION) {
// Increment the update level to aggregate the edits.
undoableEditSupport.beginUpdate();
// disconnect the output and change the burnt state.
Connection conn = dConn.getConnection();
tableTop.doDisconnectUserAction(conn);
doSetInputBurnStatusUserAction(inputToBurn, inputToBurn.isBurnt() ? AutoburnLogic.BurnStatus.NOT_BURNT : AutoburnLogic.BurnStatus.MANUALLY_BURNT);
updateForBurn();
// Decrement the update level. This will post the edit if the level is zero.
undoableEditSupport.endUpdate();
return true;
} else {
// un-highlight the output connection
tableTop.setBadDisplayedConnection(dConn, false);
return false;
}
}
}
/**
* Get the burn status of an input
* @param input PartInput the input to check
* @return BurnStatus the burn status of the input.
*/
final AutoburnLogic.BurnStatus getBurnStatus(Gem.PartInput input){
if (input.isBurnt()) {
if (manuallyBurntInputs.contains(input)) {
return AutoburnLogic.BurnStatus.MANUALLY_BURNT;
}
return AutoburnLogic.BurnStatus.AUTOMATICALLY_BURNT;
}
return AutoburnLogic.BurnStatus.NOT_BURNT;
}
/**
* Burn an input.
* @param input the input to burn
* @param newBurnStatus the new burn status of the input.
*/
void burnInput(Gem.PartInput input, AutoburnLogic.BurnStatus newBurnStatus) {
boolean inputWasBurnt = input.isBurnt();
boolean inputWillBeBurnt = (newBurnStatus != AutoburnLogic.BurnStatus.NOT_BURNT);
// do nothing if the old and new burnt state is the same..
// TODO: Do we allow switching between auto- and manually burnt?
if (inputWasBurnt == inputWillBeBurnt) {
return;
}
if (inputWillBeBurnt) {
if (newBurnStatus == AutoburnLogic.BurnStatus.MANUALLY_BURNT) {
manuallyBurntInputs.add(input);
}
} else {
// unburn
manuallyBurntInputs.remove(input);
}
// Change the input burn state.
input.setBurnt(inputWillBeBurnt);
// Update the arguments.
if (inputWillBeBurnt) {
CollectorGem argumentTarget = GemGraph.getInputArgumentTarget(input);
if (argumentTarget != null) {
argumentTarget.updateReflectedInputs();
}
} else {
CollectorGem argumentTarget = GemGraph.getInputArgumentTarget(input);
// Add the argument to the target collector if it doesn't have any target, but it should.
// eg. it was burned before it was connected.
if (argumentTarget == null && input.getGem().getRootCollectorGem() != null) {
argumentTarget = tableTop.getTargetCollector();
argumentTarget.addArguments(tableTop.getTargetCollector().getTargetArguments().size(), Collections.singleton(input));
}
if (argumentTarget != null) {
argumentTarget.updateReflectedInputs();
}
}
}
/**
* Do the work necessary to carry out a user-initiated action to autoburn a gem.
* Set the arguments to set "autoburnt" in a gem's tree (without re-typing..).
* We only index unburnt args.
* @param gem the gem whose args to set autoburnt
* @param argumentsToBurn the array of argument indices to burn, with respect to the inputs of the gem
*/
private void doAutoburnUserAction(Gem gem, int[] argumentsToBurn) {
ExtendedUndoableEditSupport undoableEditSupport = tableTop.getUndoableEditSupport();
// Increment the update level to aggregate any burns.
undoableEditSupport.beginUpdate();
// set the burnable inputs
int numBurntArgs = argumentsToBurn.length;
for (int i = 0; i < numBurntArgs; i++) {
int argIndex = argumentsToBurn[i];
Gem.PartInput input = gem.getInputPart(argIndex);
doSetInputBurnStatusUserAction(input, AutoburnLogic.BurnStatus.AUTOMATICALLY_BURNT);
}
if (numBurntArgs > 0) {
// Decrement the update level. This will post the edit if the level is zero.
undoableEditSupport.endUpdate();
updateForBurn();
} else {
// Discard the edit because nothing happened.
undoableEditSupport.endUpdateNoPost();
}
}
/**
* Returns the set of inputs that were manually burned by the user in the GemCutter
* @return set of inputs that were manually burned
*/
Set<PartInput> getManuallyBurntSet() {
return manuallyBurntInputs;
}
}