/*
* 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.
*/
/*
* IntellicutManager.java
* Creation date: Oct 23, 2002.
* By: Edward Lam
*/
package org.openquark.gems.client;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import java.awt.geom.Line2D;
import java.util.HashSet;
import java.util.Set;
import javax.swing.JComponent;
import javax.swing.JLayeredPane;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import org.openquark.cal.compiler.TypeExpr;
import org.openquark.cal.services.GemEntity;
import org.openquark.gems.client.AutoburnLogic.AutoburnInfo;
import org.openquark.gems.client.AutoburnLogic.AutoburnUnifyStatus;
import org.openquark.gems.client.DisplayedGem.DisplayedPart;
import org.openquark.gems.client.DisplayedGem.DisplayedPartConnectable;
import org.openquark.gems.client.DisplayedGem.DisplayedPartInput;
import org.openquark.gems.client.DisplayedGem.DisplayedPartOutput;
import org.openquark.gems.client.Gem.PartConnectable;
import org.openquark.gems.client.Gem.PartInput;
import org.openquark.gems.client.Gem.PartOutput;
import org.openquark.gems.client.IntellicutListModel.FilterLevel;
import org.openquark.gems.client.IntellicutListModelAdapter.IntellicutListEntry;
/**
* The IntellicutManager manages intellicut for the GemCutter and TableTop.
* @author Edward Lam
*/
public class IntellicutManager {
/** The key for the intellicut popup enabled preference. */
public static final String INTELLICUT_POPUP_ENABLED_PREF_KEY = "intellicutPopupEnabled";
/** The key for the intellicut popup delay preference. */
public static final String INTELLICUT_POPUP_DELAY_PREF_KEY = "intellicutPopupDelay";
/** The key for the gem filtering level intellicut preference. */
public static final String INTELLICUT_GEM_FILTER_LEVEL_PREF_KEY = "intellicutShowGemsFilterLevel";
/** Default value for the intellicut popup enabled preference. */
public static final boolean INTELLICUT_POPUP_ENABLED_DEFAULT = true;
/** Default value for the intellicut popup delay preference. */
public static final int INTELLICUT_POPUP_DELAY_DEFAULT = 3;
/** Default value for the gem filter level intellicut preference. */
public static final FilterLevel INTELLICUT_GEM_FILTER_LEVEL_DEFAULT = FilterLevel.SHOW_ALL;
/** The X distance that a newly added gem will be away from the intellicutPart's location. */
private static final int DROP_DISTANCE_X = 30;
/** The GemCutter instance for which Intellicut is being managed. */
private final GemCutter gemCutter;
/** Indicates which Intellicut mode we're currently in. */
private IntellicutMode intellicutMode;
/** The Part on which intellicut is activated. */
private DisplayedPartConnectable intellicutPart;
/** The intellicut panel in use. If this is null then there is no panel. */
private IntellicutPanel intellicutPanel;
/**
* The timer that starts when we are within range of a potential intellicut part input.
* If the timer fires, then we show the IntellicutPanel.
*/
private Timer intellicutPanelShowTimer;
/**
* Intellicut mode enum pattern.
* These denote what intellicut mode we're currently in.
* @author Edward Lam
*/
public static final class IntellicutMode {
private final String typeString;
private IntellicutMode(String s) {
typeString = s;
}
@Override
public String toString() {
return typeString;
}
/** Not in intellicut mode. */
public static final IntellicutMode NOTHING = new IntellicutMode("NOTHING");
/** The intellicut part is an input. */
public static final IntellicutMode PART_INPUT = new IntellicutMode("PART_INPUT");
/** The intellicut part is an output. */
public static final IntellicutMode PART_OUTPUT = new IntellicutMode("PART_OUTPUT");
}
/**
* A class to encapsulate the information about an intellicut operation.
* @author Frank Worsley
*/
public static final class IntellicutInfo {
/** Default intellicut list into for normal type closeness without autoburning. */
public static final IntellicutInfo DEFAULT_INFO = new IntellicutInfo(AutoburnUnifyStatus.NOT_NECESSARY, -1, 0, false, 0);
/** The type closeness if directly connecting to the intellicut part. */
private final int noBurnTypeCloseness;
/** The type closeness if connecting via burning. */
private final int burnTypeCloseness;
/** The autoburn status. */
private final AutoburnUnifyStatus autoburnStatus;
/** The number of other gems that reference this gem in their body */
private final int referenceFrequency;
/** True if this candidate is the same type as the target AND neither the target
* nor the candidate are polymorphic types.
*
* We want to treat gems that have the same type as being especially close, but we
* only want to do that for nonpolymorphic types, because we don't want to wind up
* in a situation where, for example, we rate unsafeCoerce (type: a->b) as a better
* match for the function input of map (type: a->b) than fst (type: (a,b) -> a),
* because in fact a function with a more concrete type is likely to be a better match
* than another function of polymorphic type.
*/
private final boolean sameNonpolymorphicType;
/** Smallest reference frequency that is still in the top 20% */
private int referenceFrequencyTopFifthThreshold;
/** Gems that have a higher reference frequency than this and are the same
* nonpolymorphic type as the target gem will be included in Best Gems even
* if they don't match the usual criteria.
*/
private int sameNonPolymorphicTypeReferenceFrequencyThreshold;
/**
* Constructs a new IntellicutInfo object.
* @param autoburnStatus the autoburn status
* @param maxBurnTypeCloseness the maximum type closeness for burning
* @param noBurnTypeCloseness the type closeness for directly connecting
* @param sameNonpolymorphicType whether the candidate represented by this IntellicutInfo
* object has the same type as the target and is not polymorphic
* @param referenceFrequency Reference frequency of the candidate represented by this IntellicutInfo object
*/
public IntellicutInfo(AutoburnUnifyStatus autoburnStatus, int maxBurnTypeCloseness, int noBurnTypeCloseness, boolean sameNonpolymorphicType, int referenceFrequency) {
if (autoburnStatus == null) {
throw new NullPointerException();
}
this.noBurnTypeCloseness = noBurnTypeCloseness;
this.burnTypeCloseness = maxBurnTypeCloseness;
this.autoburnStatus = autoburnStatus;
this.sameNonpolymorphicType = sameNonpolymorphicType;
this.referenceFrequency = referenceFrequency;
referenceFrequencyTopFifthThreshold = 0;
sameNonPolymorphicTypeReferenceFrequencyThreshold = 0;
}
public IntellicutInfo(AutoburnUnifyStatus autoburnStatus, int maxBurnTypeCloseness, int noBurnTypeCloseness) {
this(autoburnStatus, maxBurnTypeCloseness, noBurnTypeCloseness, false, 0);
}
/**
* @return the number of times other gems reference this gem in their body
*/
public int getReferenceFrequency() {
return referenceFrequency;
}
/**
* @return the type closeness achieved by directly connecting to the intellicut part
*/
public int getNoBurnTypeCloseness() {
return noBurnTypeCloseness;
}
/**
* @return the type closeness achieved by connecting to the intellicut part via burning
*/
public int getBurnTypeCloseness() {
return burnTypeCloseness;
}
/**
* @return the bigger of no burn and burn type closeness
*/
public int getMaxTypeCloseness() {
return Math.max(noBurnTypeCloseness, burnTypeCloseness);
}
/**
* @return true if the reference frequency is in the top fifth of reference frequencies
*/
public boolean isReferenceFrequencyInTopFifth() {
return referenceFrequency >= referenceFrequencyTopFifthThreshold;
}
/**
* @return True if this candidate is the same type as the target AND neither the target
* nor the candidate are polymorphic types.
*
* We want to treat gems that have the same type as being especially close, even if their
* reference frequency is not high enough that they would normally be in the Best Gems
* list. If we are looking for a gem that accepts a RelativeTime, and there are only 2 gems
* with an argument of type RelativeTime, then it seems wrong to exclude those gems from
* the Best Gems list, even if they aren't all that common. So, we have a special case
* where the top 10 gems (by reference frequency) with the same type are included in
* Best Gems even if their reference frequency is not in the top 10% globally.
*
* However, we only want to do that for nonpolymorphic types, because we don't want to wind up
* in a situation where, for example, we rate unsafeCoerce (type: a->b) as a better
* match for the function input of map (type: a->b) than fst (type: (a,b) -> a),
* because in fact a function with a more concrete type is likely to be a better match
* than another function of polymorphic type.
*/
public boolean isSameNonpolymorphicType() {
return sameNonpolymorphicType;
}
/**
* @return true if this gem's reference frequency is above the threshold set for
* forcing gems with the same non-polymorphic type as the target into the
* Best Gems list.
*/
public boolean isSameNonpolymorphicTypeThreshold() {
return referenceFrequency >= sameNonPolymorphicTypeReferenceFrequencyThreshold;
}
/**
* @return the autoburn status for connecting to the intellicut part
*/
public AutoburnUnifyStatus getAutoburnUnifyStatus() {
return autoburnStatus;
}
/**
* Set the thresholds used to determine whether a given metric is in the top 20%.
* @param referenceFrequencyTopFifthThreshold The smallest reference frequency that is in the top fifth
*/
public void setTopFifthThresholds(int referenceFrequencyTopFifthThreshold) {
this.referenceFrequencyTopFifthThreshold = referenceFrequencyTopFifthThreshold;
}
/**
* Set the reference frequency threshold for forcing gems with the same non-polymorphic type
* as the target into the Best Gems list.
* @param sameNonPolymorphicTypeReferenceFrequencyThreshold
*/
public void setSameNonpolymorphicTypeReferenceFrequencyThreshold(int sameNonPolymorphicTypeReferenceFrequencyThreshold) {
this.sameNonPolymorphicTypeReferenceFrequencyThreshold = sameNonPolymorphicTypeReferenceFrequencyThreshold;
}
}
/**
* This mouse motion listener for the table top will start intellicut if the mouse hovers over the
* same unconnected part long enough and the GemCutter is in the GUI_STATE_EDIT.
* If the intellicutDismissTimer is running it is restarted if there is mouse motion.
*/
private class TableTopMouseMotionListener extends MouseMotionAdapter {
/**
* For the purpose of determining whether or not to show the IntellicutPanel.
* This variable keeps track of the last PartInput/PartOuput that the mouse was on.
* Note: Sometimes cleared to null when we move out of range of the part.
*/
private PartConnectable lastPartConnectable;
@Override
public void mouseMoved(MouseEvent evt) {
// Determine if we are over an unconnected (and unburnt) source/sink part.
DisplayedPart partOver = gemCutter.getTableTop().getGemPartUnder(evt.getPoint());
if (partOver instanceof DisplayedPartOutput ||
(partOver instanceof DisplayedPartInput && !((DisplayedPartInput)partOver).getPartInput().isBurnt())) {
DisplayedPartConnectable dPartConnectable = (DisplayedPartConnectable)partOver;
PartConnectable partConnectable = dPartConnectable.getPartConnectable();
if (!partConnectable.isConnected()) {
if (lastPartConnectable == null || lastPartConnectable == partConnectable) {
// We're hovering over the same part or a new part. Therefore we start a new show timer
// if there isn't a timer already running.
lastPartConnectable = null;
if (intellicutPanelShowTimer == null && gemCutter.getGUIState() == GemCutter.GUIState.EDIT) {
startIntellicutPanelShowTimer(dPartConnectable);
}
} else {
// We're hovering over a new part connectable. Start a new timer for
// this connectable and remember it in case we hover over it again.
stopIntellicutPanelShowTimer();
if (gemCutter.getGUIState() == GemCutter.GUIState.EDIT) {
startIntellicutPanelShowTimer(dPartConnectable);
lastPartConnectable = partConnectable;
}
}
} else {
// We've moved over a part that's connected, so stop the timer.
stopIntellicutPanelShowTimer();
}
} else {
// We're not over a part at all, so stop the timer.
stopIntellicutPanelShowTimer();
}
}
/**
* Stops the currently running intellicutPanelShowTimer and set the
* intellicutPanelShowTimer and lastPartConnectable to null.
*/
private void stopIntellicutPanelShowTimer() {
if (intellicutPanelShowTimer != null) {
intellicutPanelShowTimer.stop();
intellicutPanelShowTimer = null;
lastPartConnectable = null;
}
}
/**
* Start the intellicutPanelShowTimer if automatic intellicut is enabled.
*/
private void startIntellicutPanelShowTimer(DisplayedPartConnectable dPartConnectable) {
boolean enabled = GemCutter.getPreferences().getBoolean(INTELLICUT_POPUP_ENABLED_PREF_KEY, INTELLICUT_POPUP_ENABLED_DEFAULT);
if (enabled) {
int delay = GemCutter.getPreferences().getInt(INTELLICUT_POPUP_DELAY_PREF_KEY, INTELLICUT_POPUP_DELAY_DEFAULT) * 1000;
intellicutPanelShowTimer = new Timer(delay, new IntellicutShowTimerActionListener(dPartConnectable));
intellicutPanelShowTimer.setRepeats(false);
intellicutPanelShowTimer.start();
}
}
}
/**
* ActionListener for the intellicut show timer. It starts intellicut if the mouse hovers over
* a part input on the table top.
*/
private class IntellicutShowTimerActionListener implements ActionListener {
/** The part that intellicut will be started with if the timer fires. */
private final DisplayedPartConnectable dPartConnectable;
public IntellicutShowTimerActionListener(DisplayedPartConnectable dConnectablePart) {
this.dPartConnectable = dConnectablePart;
}
public void actionPerformed(ActionEvent evt) {
if (dPartConnectable == null) {
return;
}
// Sometimes (ie: broken code gem) the TypeExpr is null and should not have intellicut used on it.
if (dPartConnectable.getPartConnectable().getType() == null) {
return;
}
// Don't start intellicut if the connection point has scrolled outside of
// the visible rectangle of the table top.
TableTopPanel tableTop = gemCutter.getTableTopPanel();
if (!tableTop.getVisibleRect().contains(dPartConnectable.getConnectionPoint())) {
return;
}
// Start intellicut if it is not already running for this part.
if (intellicutMode == IntellicutMode.NOTHING || intellicutPart != dPartConnectable) {
startIntellicut(dPartConnectable);
displayIntellicutPanelOnTableTop(dPartConnectable);
}
}
}
/**
* Constructor for an IntellicutManager
* @param gemCutter the gemcutter for which intellicut is being managed.
*/
IntellicutManager(GemCutter gemCutter) {
this.gemCutter = gemCutter;
intellicutPart = null;
intellicutMode = IntellicutMode.NOTHING;
// Add the listener that displays the Intellicut popup if if the user hovers over a part.
gemCutter.getTableTopPanel().addMouseMotionListener(new TableTopMouseMotionListener());
}
/**
* Get the part for which to perform intellicut. This may be null if intellicut is being
* performed on the table top.
* @return the intellicut part
*/
DisplayedPartConnectable getIntellicutPart() {
return intellicutPart;
}
/**
* @return the current intellicut mode
*/
IntellicutMode getIntellicutMode() {
return intellicutMode;
}
/**
* @return the intellicut panel in use or null if the panel is not open
*/
IntellicutPanel getIntellicutPanel() {
return intellicutPanel;
}
/**
* @see #displayIntellicutPanel(DisplayedGem.DisplayedPartConnectable, Rectangle, boolean, Point, JComponent)
* @param displayRect the point where the panel should be displayed and also the gem drop point
*/
private void displayIntellicutPanelOnTableTop(Rectangle displayRect) {
displayIntellicutPanelOnTableTop(displayRect, null);
}
/**
* @see #displayIntellicutPanel(DisplayedGem.DisplayedPartConnectable, Rectangle, boolean, Point, JComponent)
* @param displayRect the point where the panel should be displayed and also the gem drop point
* @param preferredDropPoint the point where the new gem should appear
*/
private void displayIntellicutPanelOnTableTop(Rectangle displayRect, Point preferredDropPoint) {
displayIntellicutPanel(null, displayRect, false, preferredDropPoint, gemCutter.getTableTopPanel());
}
/**
* @see #displayIntellicutPanel(DisplayedGem.DisplayedPartConnectable, Rectangle, boolean, Point, JComponent)
* @param dPartConnectable the PartConnectable which intellicut should match gems to
*/
private void displayIntellicutPanelOnTableTop(DisplayedPartConnectable dPartConnectable) {
boolean alignLeft = dPartConnectable instanceof DisplayedPartInput;
Rectangle displayRect = new Rectangle(dPartConnectable.getConnectionPoint());
displayIntellicutPanel(dPartConnectable, displayRect, alignLeft, displayRect.getLocation(), gemCutter.getTableTopPanel());
}
/**
* Displays the Intellicut panel at the specified location with matches for the specified PartConnectable.
* If the part connectable is null it will display all possible gems that can be added to the table top.
* The preferred drop point can be null in which case an appropriate drop-point, close to the top-left of
* the table top area will be chosen automatically. The display rectangle must be specified in the coordinate
* space of the source component. The panel will be displayed so that its corner lies on one of the rectangle's
* corners and does not obscure any part of the rectangle.
*
* @param displayedPart the part which intellicut should match gems to (can be null to just add a gem)
* @param displayRect the rectangle along whose edges the panel will be displayed
* @param alignLeft whether or not we want to align to the left of the display rectangle
* @param preferredDropPoint the point on the table top where the gem should appear
* @param source the Component that wants to display Intellicut and in whose coordinate space the display rectangle lies
*/
private void displayIntellicutPanel(DisplayedPartConnectable displayedPart, Rectangle displayRect, boolean alignLeft, Point preferredDropPoint, JComponent source) {
// There may or may not be a part depending on how Intellicut was started
Gem.PartConnectable part = null;
if (displayedPart != null) {
part = displayedPart.getPartConnectable();
}
IntellicutPanelAdapter intellicutAdapter = new IntellicutPanelAdapter(part, gemCutter, preferredDropPoint, source);
intellicutPanel = intellicutAdapter.getIntellicutPanel();
if (part == null || part instanceof Gem.PartInput) {
// If we are dealing with an input or are just clicking on the table top,
// then add the collectors so that the user can easily add emitters for them.
Set<CollectorGem> collectors = gemCutter.getTableTop().getGemGraph().getCollectors();
intellicutPanel.getIntellicutListModel().getAdapter().addAdditionalDataObjects(collectors);
}
intellicutPanel.loadListModel();
intellicutPanel.makeTransparent();
positionIntellicutPanel(displayRect, alignLeft, source);
intellicutPanel.getIntellicutList().requestFocus();
// Update the gem browser to display icons matching the intellicut list.
gemCutter.getBrowserTree().repaint();
}
/**
* Positions the intellicut panel inside the layered pane. It is positioned so that it lies below the
* specified display rectangle. It appears to the left or right of the display rectangle
* depending on the alignLeft parameter.
* @param displayRect the preferred point at which the panel should be displayed
* @param alignLeft whether the panel should be to the left of the point
* @param source the source component in whose coordinate space the point/area lie
*/
private void positionIntellicutPanel(Rectangle displayRect, boolean alignLeft, JComponent source) {
JLayeredPane jlp = gemCutter.getLayeredPane();
// Transform the coordinates between the source component and the layered pane.
displayRect = SwingUtilities.convertRectangle(source, displayRect, jlp);
// Calculate the ideal location for the panel.
Dimension preferredSize = intellicutPanel.getPreferredSize();
Dimension frameSize = jlp.getSize();
Point displayPoint = new Point(displayRect.x + displayRect.width, displayRect.y + displayRect.height);
if (alignLeft) {
displayPoint.x = displayRect.x - preferredSize.width;
}
// If the panel extends below the bottom of the window, move it up
if (preferredSize.height > (frameSize.height - displayPoint.y)) {
displayPoint.y = displayRect.y - preferredSize.height;
if (displayPoint.y < 0) {
displayPoint.y = 0;
}
}
// If the panel extends past the sides of the window, move it left or right
if (preferredSize.width > (frameSize.width - displayPoint.x)) {
displayPoint.x = displayRect.x - preferredSize.width;
} else if (displayPoint.x < 0) {
displayPoint.x = displayRect.x + displayRect.width;
}
// Finally add the panel to the layered pane.
intellicutPanel.setLocation(displayPoint);
intellicutPanel.setSize(preferredSize);
jlp.add(intellicutPanel, JLayeredPane.PALETTE_LAYER, 0);
}
/**
* Starts intellicut mode for a component. The display rectangle for the panel
* must be specified in the source component's coordinate space. The source
* component will regain focus when the intellicut panel is closed.
*
* @param partClicked the part for which intellicut is activated.
* @param displayRect the rectangle along whose corners the panel should appear
* @param alignLeft if the list should be displayed to the left of the gem
* @param dropPoint where the gem will be dropped if it is unattached
* @param source the component in whose coordinate space the displayRect lies
*/
void startIntellicutMode(DisplayedPartConnectable partClicked, Rectangle displayRect, boolean alignLeft, Point dropPoint, JComponent source){
startIntellicut(partClicked);
displayIntellicutPanel(intellicutPart, displayRect, alignLeft, dropPoint, source);
}
/**
* Starts intellicut mode on the table top.
* @param location the point where intellicut should be displayed
*/
void startIntellicutModeForTableTop(Rectangle location) {
startIntellicut(null);
displayIntellicutPanelOnTableTop(location);
}
/**
* Starts the intellicut mode on the table top.
* @param location the point where intellicut should be displayed
* @param preferredDropPoint the preferred drop point for the new gem
*/
void startIntellicutModeForTableTop(Rectangle location, Point preferredDropPoint) {
startIntellicut(null);
displayIntellicutPanelOnTableTop(location, preferredDropPoint);
}
/**
* Starts intellicut mode on the table top.
* @param partClicked DisplayedPart the part for which intellicut is activated
*/
void startIntellicutModeForTableTop(DisplayedPartConnectable partClicked) {
startIntellicut(partClicked);
displayIntellicutPanelOnTableTop(intellicutPart);
}
/**
* Starts basic Intellicut for the given intellicut part. This does not
* display the IntellicutPanel for the part.
* @param intellicutPart the part to start intellicut for
*/
private void startIntellicut(DisplayedPartConnectable intellicutPart) {
stopIntellicut();
this.intellicutPart = intellicutPart;
if (intellicutPart instanceof DisplayedPartInput) {
intellicutMode = IntellicutMode.PART_INPUT;
} else if (intellicutPart instanceof DisplayedPartOutput) {
intellicutMode = IntellicutMode.PART_OUTPUT;
} else {
intellicutMode = IntellicutMode.NOTHING;
}
// Draw the intellicut lines on the table top.
gemCutter.getTableTopPanel().repaint();
}
/**
* This fully stops intellicut by closing the intellicut panel and stopping the pulsing.
*/
void stopIntellicut() {
// Note that we don't want to set the intellicut part to null.
// Some parts of the code need to know what the last intellicut part was.
// To stop intellicut it is sufficient to set the mode to be NOTHING.
intellicutMode = IntellicutMode.NOTHING;
gemCutter.getStatusMessageDisplayer().clearMessage(gemCutter.getBrowserTree());
if (intellicutPanelShowTimer != null) {
intellicutPanelShowTimer.stop();
intellicutPanelShowTimer = null;
}
if (intellicutPanel != null) {
intellicutPanel.close();
intellicutPanel = null;
}
gemCutter.getBrowserTree().repaint();
gemCutter.getTableTopPanel().repaint();
}
/**
* This makes a connection between the given part and the current intellicut part. This is called by
* the table top if the user clicks on another part input while the intellicut pulsing is active.
* @param part the part to which intellicut should attempt to connect the current intellicut part
* @return whether a connection was made
*/
boolean attemptIntellicutAutoConnect(DisplayedPartConnectable part) {
PartInput inPart = null;
PartOutput outPart = null;
// Copy the reference to avoid threading issues.
DisplayedPartConnectable intellicutPartConnectable = intellicutPart;
// Assign the correct parts to inPart and outPart.
if (intellicutPartConnectable instanceof DisplayedPartInput) {
inPart = (PartInput) intellicutPartConnectable.getPartConnectable();
} else if (intellicutPartConnectable instanceof DisplayedPartOutput) {
outPart = (PartOutput) intellicutPartConnectable.getPartConnectable();
}
if (part instanceof DisplayedPartInput) {
inPart = (PartInput) part.getPartConnectable();
} else if (part instanceof DisplayedPartOutput) {
outPart = (PartOutput) part.getPartConnectable();
}
// Check if parts are valid. They maye be burnt, connected or have a null type.
// A null type occurs if parts belong to a broken gem graph.
if (inPart == null || inPart.isBurnt() || inPart.isConnected() || inPart.getType() == null ||
outPart == null || outPart.isConnected() || outPart.getType() == null) {
return false;
}
Gem inGem = inPart.getGem();
Gem outGem = outPart.getGem();
boolean makeIntellicutConnection = false;
// Connection not possible if both parts are from the same gem.
if (outGem != inGem) {
TableTop tableTop = gemCutter.getTableTop();
// Check if it is possible to make the connection by burning or connecting directly.
if (!GemGraph.isAncestorOfBrokenGemForest(outGem)) {
TypeExpr destTypeExpr = inPart.getType();
AutoburnLogic.AutoburnAction burnAction = tableTop.getBurnManager().handleAutoburnGemGesture(outGem, destTypeExpr, true);
if (burnAction != AutoburnLogic.AutoburnAction.IMPOSSIBLE && burnAction != AutoburnLogic.AutoburnAction.MULTIPLE) {
makeIntellicutConnection = true;
// Complete the connection.
Connection connection = tableTop.doConnectIfValidUserAction(outPart, inPart);
if (connection == null) {
gemCutter.getTableTop().showCannotConnectDialog();
makeIntellicutConnection = false;
}
}
}
}
return makeIntellicutConnection;
}
/**
* Attempts to automatically connect the given displayed gem to the intellicut part.
* Undoable edits will be posted for any connections and burnings.
* @param dGem the gem to connect
* @return true if a connection was made, false otherwise. A connection is not made if there are
* multiple actions that result in the same type closeness (ie: burning/not burning is ambiguous).
*/
boolean attemptIntellicutAutoConnect(DisplayedGem dGem) {
if (intellicutPart == null) {
return true;
}
Connection connection = null;
PartConnectable part = intellicutPart.getPartConnectable();
if (intellicutMode == IntellicutMode.PART_INPUT) {
// Figure out if we should connect the gem by burning it or by just connecting it.
// We want to perform whatever action results in the highest type closeness.
AutoburnInfo autoburnInfo = AutoburnLogic.getAutoburnInfo(part.getType(), dGem.getGem(), gemCutter.getTypeCheckInfo());
AutoburnUnifyStatus burnStatus = autoburnInfo.getAutoburnUnifyStatus();
boolean attemptToConnect = burnStatus.isAutoConnectable();
if (burnStatus == AutoburnUnifyStatus.UNAMBIGUOUS) {
// Perform the burn if it is unambiguous.
attemptToConnect = autoburnGem(dGem, autoburnInfo);
} else if (burnStatus == AutoburnUnifyStatus.UNAMBIGUOUS_NOT_NECESSARY) {
// Only burn it if that is better than not burning it.
int noBurnTypeCloseness = TypeExpr.getTypeCloseness(part.getType(), dGem.getGem().getOutputPart().getType(), gemCutter.getPerspective().getWorkingModuleTypeInfo());
if (autoburnInfo.getMaxTypeCloseness() > noBurnTypeCloseness) {
attemptToConnect = autoburnGem(dGem, autoburnInfo);
}
}
if (attemptToConnect) {
connection = gemCutter.getTableTop().doConnectIfValidUserAction(dGem.getGem().getOutputPart(), (PartInput) part);
}
} else if (intellicutMode == IntellicutMode.PART_OUTPUT){
PartInput inputToConnect = GemGraph.isAutoConnectable(dGem.getGem(), (PartOutput) part, gemCutter.getConnectionContext());
if (inputToConnect != null) {
connection = gemCutter.getTableTop().doConnectIfValidUserAction((PartOutput) part, inputToConnect);
}
}
// Position the gem next to the part.
DisplayedGem intellicutGem = intellicutPart.getDisplayedGem();
int y = intellicutGem.getLocation().y + intellicutGem.getBounds().height/2 - dGem.getBounds().height/2;
int x = (intellicutPart instanceof DisplayedPartOutput) ?
intellicutGem.getLocation().x + intellicutGem.getBounds().width + DROP_DISTANCE_X :
intellicutGem.getLocation().x - dGem.getBounds().width - DROP_DISTANCE_X;
gemCutter.getTableTop().changeGemLocation(dGem, new Point(x, y));
// If the gem was connected, use the layout arranger to tidy up.
if (intellicutPart.isConnected()) {
DisplayedGem[] displayedGems = {dGem, intellicutGem};
Graph.LayoutArranger layoutArranger = new Graph.LayoutArranger(displayedGems);
gemCutter.getTableTop().doTidyUserAction(layoutArranger, intellicutGem);
}
// Update the TableTop in case anything happened.
gemCutter.getTableTop().updateForGemGraph();
return connection != null;
}
/**
* Attempt to autoburn a gem's input parts so its output can connect to the given part.
* Undoable edits will be posted for any burnings.
* @param dGem the displayed gem that has to get burned
* @param autoburnInfo the autoburn info to use for burning
* @return true if the burn was performed, false otherwise
*/
private boolean autoburnGem(DisplayedGem dGem, AutoburnInfo autoburnInfo) {
if (autoburnInfo.getAutoburnUnifyStatus().isUnambiguous()) {
AutoburnLogic.BurnCombination burnCombination = autoburnInfo.getBurnCombinations().get(0);
int[] argsToBurn = burnCombination.getInputsToBurn();
int numBurns = argsToBurn.length;
for (int i = 0; i < numBurns; i++) {
int burnIndex = argsToBurn[i];
Gem.PartInput input = dGem.getGem().getInputPart(burnIndex);
gemCutter.getTableTop().getBurnManager().doSetInputBurnStatusUserAction(input, AutoburnLogic.BurnStatus.AUTOMATICALLY_BURNT);
}
return true;
}
return false;
}
/**
* @param gemEntity the entity to get intellicut info for
* @return the intellicut info for the given entity
*/
IntellicutInfo getIntellicutInfo(GemEntity gemEntity) {
if (intellicutMode != IntellicutMode.NOTHING) {
IntellicutListModel listModel = intellicutPanel.getIntellicutListModel();
IntellicutListEntry listEntry = listModel.getAdapter().getListEntryForData(gemEntity);
if (listEntry != null) {
return listEntry.getIntellicutInfo();
} else {
return new IntellicutInfo(AutoburnUnifyStatus.NOT_POSSIBLE, -1, -1);
}
}
throw new IllegalStateException("intellicut is not active");
}
/**
* Paints the intellicut lines on the table top if pulsing is active.
* @param g2d Graphics2D the graphics context
*/
void paintIntellicutLines(Graphics2D g2d) {
if (intellicutMode == IntellicutMode.NOTHING || intellicutPart == null) {
return;
}
// Grab all the parts that could unify with the intellicut part.
Set<DisplayedPartConnectable> intellicutCheckParts = new HashSet<DisplayedPartConnectable>();
for (final DisplayedGem displayedGem : gemCutter.getTableTop().getDisplayedGems()) {
if (intellicutMode == IntellicutMode.PART_OUTPUT) {
int nArgs = displayedGem.getNDisplayedArguments();
for (int i = 0; i < nArgs; i++) {
DisplayedPartInput dInput = displayedGem.getDisplayedInputPart(i);
if (dInput != null) {
intellicutCheckParts.add(dInput);
}
}
} else if (intellicutMode == IntellicutMode.PART_INPUT) {
DisplayedPartOutput dOutput = displayedGem.getDisplayedOutputPart();
if (dOutput != null) {
intellicutCheckParts.add(dOutput);
}
} else {
throw new IllegalStateException("intellicut mode not supported: " + intellicutMode);
}
}
Color prevColour = g2d.getColor();
// Now draw all the appropriate lines.
for (final DisplayedPartConnectable nextPart : intellicutCheckParts) {
g2d.setColor(gemCutter.getTableTop().getTypeColour(nextPart));
if (intellicutMode == IntellicutMode.PART_OUTPUT) {
maybePaintIntellicutLine((DisplayedPartOutput) intellicutPart, (DisplayedPartInput) nextPart, g2d);
} else {
maybePaintIntellicutLine((DisplayedPartOutput) nextPart, (DisplayedPartInput) intellicutPart, g2d);
}
}
g2d.setColor(prevColour);
}
/**
* Paints the appropriate intellicut line between two displayed parts on the table top, if the parts
* can be connected using intelilcut.
* @param dSourcePart the source part
* @param dSinkPart the sink part
* @param g2d the graphics object to draw with
*/
private void maybePaintIntellicutLine(DisplayedPartOutput dSourcePart, DisplayedPartInput dSinkPart, Graphics2D g2d) {
if (dSourcePart == null || dSinkPart == null) {
return;
}
PartConnectable sourcePart = dSourcePart.getPartConnectable();
PartConnectable sinkPart = dSinkPart.getPartConnectable();
int lineDashSize = -1;
int lineSpaceSize = -1;
float lineDashPhase = -1;
if (GemGraph.arePartsConnectable(sourcePart, sinkPart) && GemGraph.isConnectionValid(sourcePart, sinkPart)) {
// Get the burn status for the two gems
Gem sourceGem = sourcePart.getGem();
TypeExpr destTypeExpr = sinkPart.getType();
AutoburnUnifyStatus autoburnUnifyStatus = GemGraph.isAncestorOfBrokenGemForest(sourceGem) ?
AutoburnUnifyStatus.NOT_POSSIBLE
: AutoburnLogic.getAutoburnInfo(destTypeExpr, sourceGem, gemCutter.getTypeCheckInfo()).getAutoburnUnifyStatus();
if (autoburnUnifyStatus.isConnectableWithoutBurning()) {
// we can link directly without burning
lineDashSize = 10;
lineSpaceSize = 10;
lineDashPhase = 0;
} else if (autoburnUnifyStatus.isUnambiguous()) {
// we can link via unambiguous burning
lineDashSize = 1;
lineSpaceSize = 9;
lineDashPhase = 0;
}
}
// Draw the dashed line if the parts can be connected
if (lineDashSize > 0) {
BasicStroke connectionStroke =
new BasicStroke((float) 1.0, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_MITER,
lineDashSize, new float[] {lineDashSize, lineSpaceSize}, lineDashPhase);
g2d.setStroke(connectionStroke);
double startX = dSourcePart.getConnectionPoint().getX();
double startY = dSourcePart.getConnectionPoint().getY();
double endX = dSinkPart.getConnectionPoint().getX();
double endY = dSinkPart.getConnectionPoint().getY();
Line2D.Double connectLine = new Line2D.Double(startX, startY, endX, endY);
g2d.draw(connectLine);
g2d.setStroke(new BasicStroke());
}
}
}