/* * 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. */ /* * TableTopPanel.java * Creation date: Dec 18th 2002 * By: Ken Wong */ package org.openquark.gems.client; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Component; import java.awt.Cursor; import java.awt.Dimension; import java.awt.EventQueue; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Image; import java.awt.Insets; import java.awt.Point; import java.awt.Polygon; import java.awt.Rectangle; import java.awt.Shape; import java.awt.Toolkit; import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.Transferable; import java.awt.datatransfer.UnsupportedFlavorException; import java.awt.dnd.DragSource; import java.awt.dnd.DropTarget; import java.awt.dnd.DropTargetDragEvent; import java.awt.dnd.DropTargetDropEvent; import java.awt.dnd.DropTargetEvent; import java.awt.dnd.DropTargetListener; import java.awt.event.ActionEvent; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.awt.event.FocusAdapter; import java.awt.event.FocusEvent; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.geom.AffineTransform; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.awt.geom.RectangularShape; import java.awt.image.BufferedImage; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.WeakHashMap; import java.util.logging.Level; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.ImageIcon; import javax.swing.JLayeredPane; import javax.swing.JMenu; import javax.swing.JMenuItem; import javax.swing.JOptionPane; import javax.swing.JPopupMenu; import javax.swing.JSeparator; import javax.swing.JViewport; import javax.swing.KeyStroke; import javax.swing.LayoutFocusTraversalPolicy; import javax.swing.Scrollable; import javax.swing.SwingConstants; import javax.swing.SwingUtilities; import javax.swing.ToolTipManager; import javax.swing.event.CaretEvent; import javax.swing.event.CaretListener; import javax.swing.event.PopupMenuEvent; import javax.swing.event.PopupMenuListener; import javax.swing.text.BadLocationException; import org.openquark.cal.compiler.FieldName; import org.openquark.cal.compiler.ModuleTypeInfo; import org.openquark.cal.compiler.ScopedEntityNamingPolicy; import org.openquark.cal.compiler.TypeExpr; import org.openquark.cal.services.GemEntity; import org.openquark.cal.valuenode.ValueNode; import org.openquark.gems.client.DisplayedGem.DisplayedPart; import org.openquark.gems.client.DisplayedGem.DisplayedPartBody; 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.GemCutter.GUIState; import org.openquark.gems.client.utilities.ExtendedUndoManager; import org.openquark.gems.client.utilities.ExtendedUndoableEditSupport; import org.openquark.gems.client.utilities.MouseClickDragListener; import org.openquark.gems.client.utilities.MouseClickDragListener.DragMode; import org.openquark.gems.client.valueentry.ValueEditorAdapter; import org.openquark.gems.client.valueentry.ValueEditorContext; import org.openquark.gems.client.valueentry.ValueEditorEvent; import org.openquark.gems.client.valueentry.ValueEditorHierarchyManager; import org.openquark.gems.client.valueentry.ValueEditorManager; import org.openquark.gems.client.valueentry.ValueEntryPanel; import org.openquark.util.UnsafeCast; import org.openquark.util.ui.UIUtilities; /** * This class represents the JPanel that is responsible for displaying the contents on the tabletop. * Most of the work is done in the TableTop class, and so this class serves primarily as a UI class. * * Note: many functions in this class were moved from the TableTop class. * * @author Ken Wong */ public class TableTopPanel extends JLayeredPane implements Scrollable { /* * Static members ------------------------------------------------------- */ private static final long serialVersionUID = 3138348301033889627L; /** Whether the system supports drag images natively (otherwise we must render our own). */ static final boolean DRAG_IMAGE_SUPPORTED = DragSource.isDragImageSupported(); /** The minimum horizontal spacing between gems that are placed with the automatic placement algorithm. */ static final int SEARCH_HORIZONTAL_SPACING = 8; /* * Cursors and images */ static final ImageIcon blankImageIconSmall; static final ImageIcon burnImageIconSmall; static final ImageIcon burnNoParkImageIconSmall; static final ImageIcon burnQuestionImageIconSmall; static final ImageIcon connectImageIconSmall; static final ImageIcon connectNoParkImageIconSmall; static final ImageIcon collectorImageIconSmall; static final ImageIcon emitterImageIconSmall; static final ImageIcon reflectorImageIconSmall; static final ImageIcon valueGemImageIconSmall; public static final Cursor burnCursor; public static final Cursor burnNoParkCursor; public static final Cursor burnQuestionCursor; public static final Cursor cloneGemCursor; public static final Cursor connectCursor; public static final Cursor connectNoParkCursor; // Initialize the small images and the cursors static { // First get some useful reference objects. Toolkit tk = Toolkit.getDefaultToolkit(); Dimension bestsize = tk.getBestCursorSize(32,32); // Get the images. Use ImageIcon to ensure that they're loaded. ImageIcon burnImageIcon = new ImageIcon(TableTop.class.getResource("/Resources/cursorBurn.gif")); ImageIcon burnNoParkImageIcon = new ImageIcon(TableTop.class.getResource("/Resources/cursorBurnNoPark.gif")); ImageIcon burnQuestionImageIcon = new ImageIcon(TableTop.class.getResource("/Resources/cursorBurnQuestion.gif")); ImageIcon cloneCursorImageIcon = new ImageIcon(GemCutter.class.getResource("/Resources/cursorCloneGem.gif")); ImageIcon connectImageIcon = new ImageIcon(GemCutter.class.getResource("/Resources/cursorConnect.gif")); ImageIcon connectNoParkImageIcon = new ImageIcon(GemCutter.class.getResource("/Resources/cursorConnectNoPark.gif")); ImageIcon collectorImageIcon = new ImageIcon(GemCutter.class.getResource("/Resources/collector.gif")); ImageIcon emitterImageIcon = new ImageIcon(GemCutter.class.getResource("/Resources/emitter.gif")); ImageIcon reflectorImageIcon = new ImageIcon(GemCutter.class.getResource("/Resources/reflector.gif")); ImageIcon valueGemImageIcon = new ImageIcon(GemCutter.class.getResource("/Resources/constant.gif")); System.setProperty("gemcutter.photolook", "true"); // These small image icons are used by Intellicut int smallWidth = 12; int smallHeight = 12; blankImageIconSmall = new ImageIcon(new BufferedImage(smallWidth, smallHeight, BufferedImage.TYPE_INT_ARGB)); burnImageIconSmall = new ImageIcon(burnImageIcon.getImage().getScaledInstance(smallWidth, smallHeight, Image.SCALE_SMOOTH)); burnNoParkImageIconSmall = new ImageIcon(burnNoParkImageIcon.getImage().getScaledInstance(smallWidth, smallHeight, Image.SCALE_SMOOTH)); burnQuestionImageIconSmall = new ImageIcon(burnQuestionImageIcon.getImage().getScaledInstance(smallWidth, smallHeight, Image.SCALE_SMOOTH)); connectImageIconSmall = new ImageIcon(connectImageIcon.getImage().getScaledInstance(smallWidth, smallHeight, Image.SCALE_SMOOTH)); connectNoParkImageIconSmall = new ImageIcon(connectNoParkImageIcon.getImage().getScaledInstance(smallWidth, smallHeight, Image.SCALE_SMOOTH)); collectorImageIconSmall = new ImageIcon(collectorImageIcon.getImage().getScaledInstance(smallWidth, smallHeight, Image.SCALE_SMOOTH)); emitterImageIconSmall = new ImageIcon(UIUtilities.cropImage(UIUtilities.shiftImage(new ImageIcon( emitterImageIcon.getImage().getScaledInstance(smallWidth - 1, smallHeight - 1, Image.SCALE_SMOOTH)).getImage(), 2, 3), 1, 0, 0, 2)); reflectorImageIconSmall = new ImageIcon(UIUtilities.cropImage(UIUtilities.shiftImage(new ImageIcon(reflectorImageIcon.getImage().getScaledInstance(smallWidth - 1, smallHeight - 2, Image.SCALE_SMOOTH)).getImage(), 1, 3), 0, 0, 0, 1)); valueGemImageIconSmall = new ImageIcon(valueGemImageIcon.getImage().getScaledInstance(smallWidth, smallHeight, Image.SCALE_SMOOTH)); // Do we support custom cursors? if (bestsize.width > 15) { // declare the hotspot for the cursor, some useful constants. // hotSpot values must be less than the Dimension returned by getBestCursorSize Point burnHotSpot = new Point(8, 15); Point connectHotSpot = new Point(1, 1); // Scale the images to the cursor size. BufferedImage scaledBurnCursorImage = GemCutterPaintHelper.getResizedImage(burnImageIcon.getImage(), bestsize); BufferedImage scaledBurnNoParkCursorImage = GemCutterPaintHelper.getResizedImage(burnNoParkImageIcon.getImage(), bestsize); BufferedImage scaledCloneCursorImage = GemCutterPaintHelper.getResizedImage(cloneCursorImageIcon.getImage(), bestsize); BufferedImage scaledBurnQuestionCursorImage = GemCutterPaintHelper.getResizedImage(burnQuestionImageIcon.getImage(), bestsize); BufferedImage scaledConnectCursorImage = GemCutterPaintHelper.getResizedImage(connectImageIcon.getImage(), bestsize); BufferedImage scaledConnectNoParkCursorImage = GemCutterPaintHelper.getResizedImage(connectNoParkImageIcon.getImage(), bestsize); // define the cursors burnCursor = tk.createCustomCursor(scaledBurnCursorImage, burnHotSpot, "BurnCursor"); burnNoParkCursor = tk.createCustomCursor(scaledBurnNoParkCursorImage, burnHotSpot, "BurnNoParkCursor"); burnQuestionCursor = tk.createCustomCursor(scaledBurnQuestionCursorImage, burnHotSpot, "BurnQuestionCursor"); cloneGemCursor = tk.createCustomCursor(scaledCloneCursorImage, connectHotSpot, "CloneGemCursor"); connectCursor = tk.createCustomCursor(scaledConnectCursorImage, connectHotSpot, "ConnectCursor"); connectNoParkCursor = tk.createCustomCursor(scaledConnectNoParkCursorImage, connectHotSpot, "ConnectNoParkCursor"); } else { // Platform don't support custom cursors. // Linux is probably the only platform that doesn't support custom cursors. burnCursor = Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR); burnNoParkCursor = DragSource.DefaultLinkNoDrop; burnQuestionCursor = Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR); cloneGemCursor = Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR); connectCursor = Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR); connectNoParkCursor = DragSource.DefaultLinkNoDrop; } } /** Our own reference to gemCutter. */ private final GemCutter gemCutter; /** Our own reference to tableTop. */ private final TableTop tableTop; /** The handler for mouse events in edit mode. */ private final TableTopMouseHandler tableTopMouseHandler; /** The handler for mouse event in run mode. */ private final RunModeMouseHandler runModeMouseHandler; /** The painter for the TableTop. */ private final TableTopGemPainter gemPainter; /**Map from value gem to the value entry panel used to edit its value. */ private final Map<ValueGem, ValueEntryPanel> valueGemPanelMap; /** * The popup menu currently being shown by the table top. This is either * the table top popup, gem part popup, gem popup or run mode popup. */ private JPopupMenu currentPopupMenu = null; /** The location the popup menu has displayed at or will be displayed at. */ private Point currentPopupLocation = new Point(); // ensure this is never null.. /** Whether or not a popup menu is allowed to be shown. */ private boolean popupShouldShow = true; /** The location at which gems should be pasted if paste was invoked from a popup menu. */ private Point pasteLocation = null; /** The table top background image. */ private BufferedImage backgroundImage = null; /** The x offset of the background image used to create the illusion of 'scrolling' */ private int backgroundImageOriginOffsetX = 0; /** The y offset of the background image used to create the illusion of 'scrolling' */ private int backgroundImageOriginOffsetY = 0; /** The DisplayedGem that is used as the anchor for selections (mouse or keyboard) using the SHIFT key. */ private DisplayedGem shiftSelectionAnchorGem = null; /** * Flag to indicate that a paint is occurring, and not to do any drawings on the TableTop * until the full paint is finished (flag reset). */ private int isPainting; /** * Draw action enum pattern. * Creation date: (31/08/2001 10:58:43 AM) * @author Edward Lam */ static final class DrawAction { static final DrawAction DRAW = new DrawAction (); static final DrawAction UNDRAW = new DrawAction (); static final DrawAction REDRAW = new DrawAction (); /** * Constructor for a draw action */ private DrawAction() { } } /** * Select Mode enum pattern to describe the different selection modes that can * arise while selecting gems with the mouse or keyboard. * Creation date: (12/04/01 11:21:43 AM) * @author Edward Lam */ static final class SelectMode { static final SelectMode REPLACE_SELECT = new SelectMode (); // Regular static final SelectMode TOGGLE = new SelectMode (); // Control meta static final SelectMode SELECT = new SelectMode (); // Shift meta /** * Constructor for a drag select mode */ private SelectMode() { } } /** * An action used to add a new reflector for a collector to the table top. * This is used by the 'Add Other Emitter' menu item for the non-gem * table top menu. */ private class AddReflectorAction extends AbstractAction { private static final long serialVersionUID = -7296061875095016427L; private final CollectorGem collector; public AddReflectorAction(CollectorGem collector) { super(collector.getUnqualifiedName()); this.collector = collector; } public void actionPerformed(ActionEvent evt) { DisplayedGem dGem = tableTop.createDisplayedReflectorGem(currentPopupLocation, collector); ExtendedUndoableEditSupport editSupport = tableTop.getUndoableEditSupport(); editSupport.beginUpdate(); tableTop.doAddGemUserAction(dGem, currentPopupLocation); editSupport.setEditName(GemCutterMessages.getString("UndoText_Add", dGem.getDisplayText())); editSupport.endUpdate(); } } /** * An editable text field that accepts valid CAL identifiers for variable names * Creation date: (10/29/01 4:04:00 PM) * @author Edward Lam */ class EditableGemNameField extends EditableIdentifierNameField.VariableName { private static final long serialVersionUID = -500686040771757761L; /** The gem to which this refers */ private final Gem gem; /** The name of the before the name gets changed. */ private String oldName; /** Keeps track of whether this component has been removed from the tableTop */ // is there a better way?? private boolean removed = false; /** The undo manager for this text field. */ private ExtendedUndoManager undoManager; /** * Constructor for a new EditableGemNameField. * @param initialText the text initially displayed in this field * @param gem the gem to edit the name for */ EditableGemNameField(String initialText, Gem gem) { super(); this.gem = gem; // we have to set this before we call isValidName() ... initialize(initialText); } /** * Initializes this text field * @param initialText */ private void initialize(String initialText) { oldName = gem instanceof CollectorGem ? ((CollectorGem)gem).getUnqualifiedName() : ((CodeGem)gem).getUnqualifiedName(); // Hopefully, the initial text is a valid name! if (!isValidName(initialText)) { throw new IllegalArgumentException("Programming Error: attempting to initialize the name of a variable with invalid name: " + initialText); } // set the initial text setInitialText(initialText); setText(initialText); // set the font of the text setFont(gem instanceof CodeGem ? GemCutterPaintHelper.getTitleFont() : GemCutterPaintHelper.getBoldFont()); // update the size of the text area to reflect the size of the text updateSize(); // starts out with all text selected selectAll(); // set up the undo manager undoManager = new ExtendedUndoManager(); getDocument().addUndoableEditListener(undoManager); // moving focus away commits the text entered and closes this component addFocusListener(new FocusAdapter(){ public void focusLost(FocusEvent e) { commitText(); } }); // intercept some key events addKeyListener(new KeyAdapter(){ public void keyPressed(KeyEvent e) { int keyCode = e.getKeyCode(); // pressing "ESC" cancels text entry and closes this component if (keyCode == KeyEvent.VK_ESCAPE) { cancelEntry(); } KeyStroke keyStroke = KeyStroke.getKeyStrokeForEvent(e); // handle undo and redo if (keyStroke.equals(GemCutterActionKeys.ACCELERATOR_UNDO)) { if (undoManager.canUndo()) { undoManager.undo(); textChanged(); } e.consume(); } else if (keyStroke.equals(GemCutterActionKeys.ACCELERATOR_REDO)) { if (undoManager.canRedo()) { undoManager.redo(); textChanged(); } e.consume(); } else if (keyStroke.equals(GemCutterActionKeys.ACCELERATOR_ARRANGE_GRAPH) || keyStroke.equals(GemCutterActionKeys.ACCELERATOR_NEW)) { // We have to intercept accelerators for these so that the GemCutter // doesn't get screwed up when the text field doesn't match the action result. e.consume(); } } }); // ensure the cursor is visible when it moves addCaretListener(new CaretListener(){ public void caretUpdate(CaretEvent e){ // just ensure the caret is visible scrollCaretToVisible(); } }); } /** * Cancel text entry (press "ESC" ..) */ protected void cancelEntry(){ // Revert to the last valid name setText(getInitialText()); // What to do, what to do.. textCommittedInvalid(); } /** * Close this window (if not already gone..) */ synchronized void closeField(){ // check if we've removed this already if (!removed) { removed = true; TableTopPanel.this.remove(this); TableTopPanel.this.repaint(EditableGemNameField.this.getBounds()); // we have to do this or else you can just keep typing (..!) setEnabled(false); // Update the tabletop for the new gem graph state. // Among other things, this will ensure arg name disambiguation with respect to the new collector name. tableTop.updateForGemGraph(); } // trigger a focusLost() on this component if it had focus TableTopPanel.this.requestFocus(); } /** * If this has been placed in the tabletop, make sure the caret is visible */ void scrollCaretToVisible(){ if (TableTopPanel.this.isAncestorOf(this)) { int dotPos = getCaret().getDot(); try { Rectangle caretRect = modelToView(dotPos); caretRect.width += 1; Rectangle convertedRect = SwingUtilities.convertRectangle(this, caretRect, TableTopPanel.this); TableTopPanel.this.scrollRectToVisible(convertedRect); } catch (BadLocationException e) { // Nowhere to scroll. Oh well. } } } /** * Notify that the text of this text field has changed. Called upon insertUpdate() and remove() completion. * Eg. If the current result is not valid, maybe do something about it (like warn the user somehow..) */ protected void textChanged(){ super.textChanged(); tableTop.resizeForGems(); // ensure the caret is visible scrollCaretToVisible(); } /** * Returns whether a name is a valid name for this field * @param name the name to check for validity */ protected boolean isValidName(String name){ if (!(super.isValidName(name))) { return false; } return tableTop.isAvailableCodeOrCollectorName(name, gem); } /** * Take appropriate action if the result of the text change is invalid. */ protected void textChangeInvalid(){ // Signal the user. setForeground(Color.lightGray); // update the gem name to display the new text (despite being invalid) updateGemName(getText()); // set a tooltip saying that the text is invalid String text = GemCutter.getResourceString("ToolTip_InvalidVariableName"); String[] lines = ToolTipHelpers.splitTextIntoLines(text, 300, getFont(), ((Graphics2D)TableTopPanel.this.getGraphics()).getFontRenderContext()); text = "<html>" + lines [0]; for (int i = 1; i < lines.length; i++) { text += "<br>" + lines[i]; } setToolTipText(text + "</html>"); // update the text field to reflect the new size of the text updateSize(); } /** * Take appropriate action if the result of the text change is valid. */ protected void textChangeValid(){ // do validation checking - paint text colors differently depending on the result setForeground(Color.black); // update the gem name to display the new text updateGemName(getText()); // clear any tooltip saying that the text is invalid setToolTipText(null); // update the text field to reflect the new size of the text updateSize(); } /** * Take appropriate action if the text committed is valid. */ protected void textCommittedInvalid(){ // the text is already reverted. Update the gem name to reflect this. String revertedText = getText(); updateGemName(revertedText); // close this component closeField(); } /** * Take appropriate action if the text committed is valid. */ protected void textCommittedValid(){ // Update the gem name. String committedText = getText(); updateGemName(committedText); // close this component closeField(); // the text size wouldn't change on commit so no need to repaint let gems // Notify the undo manager of the name change, if any String newName = gem instanceof CollectorGem ? ((CollectorGem)gem).getUnqualifiedName() : ((CodeGem)gem).getUnqualifiedName(); if (!newName.equals(oldName)) { if (gem instanceof CollectorGem) { tableTop.getUndoableEditSupport().postEdit(new UndoableChangeCollectorNameEdit(tableTop, (CollectorGem)gem, oldName)); } else if (gem instanceof CodeGem) { tableTop.getUndoableEditSupport().postEdit(new UndoableChangeCodeGemNameEdit(tableTop, (CodeGem)gem, oldName)); } } } /** * Update the name of the gem represented by this text field. * @param newName String the new name for the let gem */ private void updateGemName(String newName){ if (gem instanceof CodeGem) { tableTop.renameCodeGem((CodeGem)gem, newName); } else { ((CollectorGem)gem).setName(newName); } } /** * Update the size of this field. */ private void updateSize(){ Insets insets = getInsets(); // The X dimension is based on the size of the text for the name (plus some margins) FontMetrics fm = getFontMetrics(getFont()); // Calculate width and height int newWidth = fm.stringWidth(getText()) + insets.right + insets.left + 1; int newHeight = fm.getHeight(); setSize(new Dimension(newWidth, newHeight)); } } /** * Listener which is invoked when the keyboard is used.. * Creation date: (12/09/2001 9:37:19 AM) * @author Edward Lam */ class KeyStrokeHandler extends KeyAdapter { /** * Called when a key is released * @param evt */ public void keyReleased(KeyEvent evt) { // If the user is dragging, and Ctrl is released, then we want to get out of the Ctrl-drag mode if (evt.getKeyCode() == KeyEvent.VK_CONTROL) { if (tableTopMouseHandler.isGemDragging()) { tableTopMouseHandler.setDragMode(TableTopDragMode.GEMDRAGGING); } } } /** * Called when a key is pressed. * @param evt KeyEvent the related KeyEvent */ public void keyPressed(KeyEvent evt) { // Only pay attention to this key event if none of the popup menus are open if (currentPopupMenu != null && currentPopupMenu.isVisible()) { return; } NavigationDirection navDirection = null; DisplayedGem focusedGem = tableTop.getFocusedDisplayedGem(); switch (evt.getKeyCode()) { case KeyEvent.VK_CONTROL: if (tableTopMouseHandler.isGemDragging()) { tableTopMouseHandler.setDragMode(TableTopDragMode.CTRLDRAGGING); } break; case KeyEvent.VK_UP: navDirection = NavigationDirection.UP; break; case KeyEvent.VK_DOWN: navDirection = NavigationDirection.DOWN; break; case KeyEvent.VK_LEFT: navDirection = NavigationDirection.LEFT; break; case KeyEvent.VK_RIGHT: navDirection = NavigationDirection.RIGHT; break; case KeyEvent.VK_SPACE: if (focusedGem != null) { // What we do depends on the modifiers if (evt.isControlDown() && evt.isShiftDown()) { // Singleton select the focused Gem when both CTRL + SHIFT are used with the SPACE // and update the shift selection anchor Gem tableTop.selectDisplayedGem(focusedGem, true); shiftSelectionAnchorGem = focusedGem; } else if (evt.isControlDown() || !tableTop.isSelected(focusedGem)) { // Toggle the selection state of the focused gem (if there is one) // and make the focused gem the new selection anchor. tableTop.toggleSelected(focusedGem); shiftSelectionAnchorGem = focusedGem; } } return; case KeyEvent.VK_ESCAPE: // todoSN - This may be temporary, although it may be good to clear the selection with Escape. // Wait to see what happens once proper focus switching is working in the GemCutter. // Clear any selections and return focus to the TableTop if (!tableTopMouseHandler.isUsefulDragMode(tableTopMouseHandler.getDragMode())) { tableTop.selectDisplayedGem(null, true); shiftSelectionAnchorGem = null; tableTop.setFocusedDisplayedGem(null); requestFocus(); return; } else { Graphics2D g2d = (Graphics2D)getGraphics(); tableTopMouseHandler.drawDragGhost(TableTopPanel.DrawAction.UNDRAW, TableTopPanel.SelectMode.REPLACE_SELECT, g2d); g2d.dispose(); tableTopMouseHandler.setDragMode(TableTopDragMode.USELESS); break; } default: return; } // If the navigation direction is not null and there is a // focused Gem we know an arrow was pressed for navigation if (navDirection != null && focusedGem != null) { // Get the next gem to gain focus DisplayedGem nextGem = tableTop.findNearestDisplayedGem(navDirection, focusedGem); // Did we find a Gem in the right direction? if (nextGem != null) { // Decide how to handle the change of focus. if (evt.isShiftDown()) { // If the selection anchor is null then we are just starting to select // some range of Gems. if (shiftSelectionAnchorGem == null) { shiftSelectionAnchorGem = nextGem; } Rectangle2D rect = getRectangleForDisplayedGems(shiftSelectionAnchorGem, nextGem); tableTop.selectGems(rect, TableTopPanel.SelectMode.REPLACE_SELECT); } else if (!evt.isControlDown()) { // No shift key and no CTRL key so singleton select the next Gem // and update the selection anchor. tableTop.selectDisplayedGem(nextGem, true); shiftSelectionAnchorGem = nextGem; } // Shift the focus scrollRectToVisible(nextGem.getBounds()); tableTop.setFocusedDisplayedGem(nextGem); } // Consume the key event so that the scroll bars don't get the arrow key event evt.consume(); } } } /** * Navigation direction enum pattern. * Creation date: (04/09/2002 12:19:00 PM). * @author Steve Norton */ static final class NavigationDirection { final String direction; static final NavigationDirection UP = new NavigationDirection("Up"); static final NavigationDirection DOWN = new NavigationDirection("Down"); static final NavigationDirection LEFT = new NavigationDirection("Left"); static final NavigationDirection RIGHT = new NavigationDirection("Right"); /** * Constructor for a navigation direction. */ private NavigationDirection(String direction) { this.direction = direction; } } /** * Mouse handler for when the gem cutter is in run mode. * Creation date: Oct 09th 2002 * @author Ken Wong */ private class RunModeMouseHandler extends MouseAdapter { public void mousePressed(MouseEvent e) { maybeShowPopup(e); } public void mouseReleased(MouseEvent e) { maybeShowPopup(e); } } /** * Component listener for the TableTop * Creation date: (12/14/01 10:28:43 AM) * @author Edward Lam */ class TableTopComponentListener extends ComponentAdapter { /** * Invoked when the component's size changes. */ public void componentResized(ComponentEvent e) { // If the TargetGem is docked, move it to the top-right corner of the component tableTop.checkTargetDockLocation(); // Repaint the overview. gemCutter.getOverviewPanel().repaint(); } } /** * Handler for drag and drop events. * Creation date: (03/15/2002 2:03:35 PM) * @author Edward Lam */ class TableTopDragAndDropHandler implements DropTargetListener { /** The image that shows up when dragging. * Null if not dragging-and-dropping or if there is no drag image set. */ private BufferedImage dragImage = null; /** The offset of the mouse from the image origin while dragging. */ private Point mousePointOffset = new Point(0, 0); /** The bounds of the last drawn drag ghost. */ private final Rectangle lastGhostRect = new Rectangle(); /** * Constructor for a drag-and-drop handler */ TableTopDragAndDropHandler() { } /* * Methods implementing DropTargetListener ************************************************************ */ /** * Called when a drag operation has encountered the <code>DropTarget</code>. * <P> * @param dtde the <code>DropTargetDragEvent</code> */ public void dragEnter(DropTargetDragEvent dtde) { DataFlavor SCDF = GemEntitySelection.getEntityListDF(); if (dtde.isDataFlavorSupported(SCDF)) { dtde.acceptDrag(dtde.getDropAction()); // Set the image to drag and its offset dragImage = gemCutter.getBrowserTree().getDragImage(); mousePointOffset = gemCutter.getBrowserTree().getDragOffset(); } else { dtde.rejectDrag(); } // HACK: Make up a dummy mouse event and let the ToolTipManager know that the mouse has entered // the table top. We need to do this because a normal mouse entered event does not get fired when we // enter via DnD. This seems to have the unfortunate effect of confusing the tool tip manager so that // it doesn't display tool tips when it should. Notifying the manager when the mouse enters // via DnD seems to avoid the confusion. MouseEvent mouseEvt = new MouseEvent(TableTopPanel.this, MouseEvent.MOUSE_ENTERED, 0, 0, dtde.getLocation().x, dtde.getLocation().y, 1, false); ToolTipManager.sharedInstance().mouseEntered(mouseEvt); } /** * The drag operation has departed the <code>DropTarget</code> without dropping. * <P> * @param dte the <code>DropTargetEvent</code> */ public void dragExit(DropTargetEvent dte) { // If necessary, erase the last ghost image if (!TableTopPanel.DRAG_IMAGE_SUPPORTED) { paintImmediately(lastGhostRect.getBounds()); } } /** * Called when a drag operation is ongoing on the <code>DropTarget</code>. * <P> * @param dtde the <code>DropTargetDragEvent</code> */ public void dragOver(DropTargetDragEvent dtde){ if (!dtde.isDataFlavorSupported(GemEntitySelection.getEntityListDF())) { dtde.rejectDrag(); return; } dtde.acceptDrag(dtde.getDropAction()); // draw a ghost image // To draw the drag image: // First, repaint the real estate the drag image last occupied. // Note that simply calling repaint() won't work because it effectively delays the repainting, // possibly until after you have drawn the new drag image, and therefore, erases all or part of it. // You really must paint the area immediately, using the, you guessed it, paintImmediately() method. // // Second, you draw the ghost image in its new location. // Note that you draw the image the same distance away from the mouse pointer as when the node // was first clicked. if (!TableTopPanel.DRAG_IMAGE_SUPPORTED && dragImage != null) { Graphics2D g2d = (Graphics2D)getGraphics(); // Calculate new location Point mouseLocation = dtde.getLocation(); int newX = mouseLocation.x - mousePointOffset.x; int newY = mouseLocation.y - mousePointOffset.y; // Update if the location changed if (newX != lastGhostRect.x || newY != lastGhostRect.y) { // Erase the last ghost image and cue line paintImmediately(lastGhostRect.getBounds()); // Remember where you are about to draw the new ghost image lastGhostRect.setBounds(newX, newY, dragImage.getWidth(), dragImage.getHeight()); // Draw the ghost image g2d.drawImage(dragImage, AffineTransform.getTranslateInstance(lastGhostRect.getX(), lastGhostRect.getY()), null); } g2d.dispose(); } } /** * The drag operation has terminated with a drop on this <code>DropTarget</code>. * This method is responsible for undertaking the transfer of the data associated with the * gesture. The <code>DropTargetDropEvent</code> provides a means to obtain a <code>Transferable</code> * object that represents the data object(s) to be transfered.<P> * From this method, the <code>DropTargetListener</code> shall accept or reject the drop via the * acceptDrop(int dropAction) or rejectDrop() methods of the <code>DropTargetDropEvent</code> parameter. * <P> * Subsequent to acceptDrop(), but not before, <code>DropTargetDropEvent</code>'s getTransferable() * method may be invoked, and data transfer may be performed via the returned <code>Transferable</code>'s * getTransferData() method. * <P> * At the completion of a drop, an implementation of this method is required to signal the success/failure * of the drop by passing an appropriate <code>boolean</code> to the <code>DropTargetDropEvent</code>'s * dropComplete(boolean success) method. * <P> * Note: The actual processing of the data transfer is not required to finish before this method returns. * It may be deferred until later. * <P> * @param dtde the <code>DropTargetDropEvent</code> */ public void drop(DropTargetDropEvent dtde) { try { // Get the transferable object Transferable trans = dtde.getTransferable(); // We currently only accept our special DataFlavor DataFlavor SCDF = GemEntitySelection.getEntityListDF(); if (trans.isDataFlavorSupported(SCDF)) { dtde.acceptDrop(dtde.getDropAction()); // Create Gem objects for items dropped in from the gem browser List<Object> scs = UnsafeCast.<List<Object>>unsafeCast(trans.getTransferData(SCDF)); // Get the drop location Point dropXY = dtde.getLocation(); int scsLen = scs.size(); if (scsLen < 1) { return; } // Increment the update level for the edit undo. This will aggregate the drops. tableTop.getUndoableEditSupport().beginUpdate(); // Now create the gems. Start out with the first one GemEntity gemEntity = (GemEntity)scs.get(0); DisplayedGem dGem = tableTop.createDisplayedFunctionalAgentGem(dropXY, gemEntity); // Adjust so that the the middle of the first gem appears under the pointer. Rectangle dGemBounds = dGem.getBounds(); int halfGemWidth = dGemBounds.width / 2; int halfGemHeight = dGemBounds.height / 2; dropXY.translate(-halfGemWidth, -halfGemHeight); // Now add the gem. dGem.setLocation(dropXY); tableTop.doAddGemUserAction(dGem, dropXY); // Now all the other gems (if any). for (int i = 1; i < scsLen; i++) { gemEntity = (GemEntity)scs.get(i); // Move the dropXY a bit - the previous displayed gem's height plus some constant factor. dropXY.translate(0, dGem.getBounds().height + DisplayConstants.MULTI_DROP_OFFSET); // Create a new gem dGem = tableTop.createDisplayedFunctionalAgentGem(dropXY, gemEntity); // Now add the gem. tableTop.doAddGemUserAction(dGem, dropXY); } // make sure the table top is large enough to hold any dropped gems tableTop.resizeForGems(); // Override the default undo name if more than one gem dropped. if (scsLen > 1) { tableTop.getUndoableEditSupport().setEditName(GemCutter.getResourceString("UndoText_AddGems")); } // Decrement the update level. This will post the edit if the level is zero. tableTop.getUndoableEditSupport().endUpdate(); } else { // We don't understand this dtde.rejectDrop(); } } catch (UnsupportedFlavorException ufe) { // Bad data flavour in drop - should never happen! String msgText = GemCutter.getResourceString("BadDropDataFlavour") + "\n" + ufe; JOptionPane.showMessageDialog(TableTopPanel.this, msgText, GemCutter.getResourceString("WindowTitle"), JOptionPane.ERROR_MESSAGE); dtde.rejectDrop(); } catch (java.io.IOException ioe) { // Other dodginess has occurred String msgText = GemCutter.getResourceString("BadDropIO") + "\n" + ioe; JOptionPane.showMessageDialog(TableTopPanel.this, msgText, GemCutter.getResourceString("WindowTitle"), JOptionPane.ERROR_MESSAGE); dtde.rejectDrop(); } finally { // Say that we're done with the drag and dropping thingy dtde.getDropTargetContext().dropComplete(true); // reset drag image info // so we don't accidentally reuse old info if one source supplies an image and another does not. dragImage = null; mousePointOffset.move(0,0); } } /** * Called if the user has modified the current drop gesture. * <P> * @param dtde the <code>DropTargetDragEvent</code> */ public void dropActionChanged(DropTargetDragEvent dtde) { DataFlavor SCDF = GemEntitySelection.getEntityListDF(); if (dtde.isDataFlavorSupported(SCDF)) { dtde.acceptDrag(dtde.getDropAction()); } else { dtde.rejectDrag(); } } } /** * Drag action enum pattern. * Creation date: (31/08/2001 10:58:43 AM) * @author Edward Lam */ private static final class TableTopDragMode extends MouseClickDragListener.DragMode { /* * GEMDRAGGING - dragging gems around the tabletop. * CTRLDRAGGING - 'cloning' function * CONNECTING - connecting a gem * DISCONNECTING - disconnecting a gem. Connecting to another gem is allowed. * SELECTING - selecting gems * USELESS - dragging but with no effect */ private static final DragMode GEMDRAGGING = new TableTopDragMode ("Gem Dragging"); private static final DragMode CTRLDRAGGING = new TableTopDragMode ("Ctrl - Gem Dragging"); private static final DragMode CONNECTING = new TableTopDragMode ("Connecting"); private static final DragMode DISCONNECTING = new TableTopDragMode ("Disconnecting"); private static final DragMode SELECTING = new TableTopDragMode ("Selecting"); private static final DragMode USELESS = new TableTopDragMode ("Useless"); /** * Constructor for a drag mode. */ private TableTopDragMode(String name) { super(name); } } /** * Event listener for the TableTop * Creation date: (12/04/01 3:21:43 PM) * @author Edward Lam */ class TableTopMouseHandler extends MouseClickDragListener { /** * Class to hold info for redrawing * Creation date: (12/17/01 12:18:43 PM) * @author Edward Lam */ private class RedrawInfo { final Point pressedAt; final Point dragPos; final DragMode dragMode; final DisplayedGem[] dragList; final SelectMode selectMode; /** * Constructor */ RedrawInfo(Point pressedAt, Point dragPos, DragMode dragMode, DisplayedGem[] dragList, SelectMode selectMode){ this.pressedAt = pressedAt; this.dragPos = dragPos; this.dragMode = dragMode; this.dragList = dragList; this.selectMode = selectMode; } } // // Mouse states and state associated with it ------------------------------------------------------- // /** The gem that we pressed on and are dragging. */ private DisplayedGem clickGem; /** The displayed input or output where the connection drag originated. */ private DisplayedPartConnectable connectionDragAnchorPart; /** If disconnecting, the part that was disconnected. */ private DisplayedPartConnectable disconnectedDisplayedPart = null; /** What the last select mode was. */ private SelectMode lastSelectMode; /** List of Gems being dragged. */ private DisplayedGem[] dragList; /** The last position for dragging. */ private Point dragPos; /** The clip area present for the last drag. */ private Shape lastDragClipArea; /** The colour present for the last drag. */ private Color lastDragColour; /** Information pertaining to the most recent draw (if any). */ private RedrawInfo redrawInfo = null; /** Whether a drag operation started by pressing & dragging over a VEP. */ private boolean dragStartedOverVEP = false; // Maybe uncomment when disconnection happens properly. // /** (PartInput->AutoburnLogic.BurnStatus) If CONNECTING or DISCONNECTING, map from inputs which have been automatically burnt or unburnt, // * but have not have had edits committed (since the action may be canceled) to their burn state before the action was initiated */ // private Map transientInputToOldBurnStateMap = new HashMap(); // /** * Constructor for a TableTopMouseHandler */ TableTopMouseHandler() { super(); } /** * Returns the drag mode * @return DragMode */ DragMode getDragMode() { return dragMode; } /** * Move the drag mode into the aborted state. */ protected void abortDrag() { try { // Finish the ghost drawing (undraw the last lot) Graphics2D g2d = (Graphics2D)getGraphics(); drawDragGhost(DrawAction.UNDRAW, lastSelectMode, g2d); g2d.dispose(); // if we're connecting, undo automatically burned inputs on the source if (dragMode == TableTopDragMode.CONNECTING || dragMode == TableTopDragMode.DISCONNECTING) { DisplayedGem burnGem = connectionDragAnchorPart.getDisplayedGem(); tableTop.getBurnManager().doUnburnAutomaticallyBurnedInputsUserAction(burnGem.getGem()); // Decrement the update level. This will post the edit if the level is zero. tableTop.getUndoableEditSupport().endUpdate(); } } finally { // Set our drag states appropriately super.abortDrag(); tableTop.selectDisplayedGem(null, false); tableTop.setFocusedDisplayedGem(null); setCursor(null); } } /** * Assuming that we are in the TableTopDragMode.CONNECTING state, change the tabletop state to take into account * the current drag position. This changes the cursor, and may attempt to carry out or undo autoburn * on the connection source. * @param where Point The mouse location to check. */ private void changeStateForConnecting(Point where) { // Check if we are over a part that is not yet bound to another part. DisplayedGem.DisplayedPart partUnder = tableTop.getGemPartUnder(where); // Make sure we are trying to connect inputs to outputs and vice versa. if ((connectionDragAnchorPart instanceof DisplayedPartOutput && partUnder instanceof DisplayedPartInput) || (connectionDragAnchorPart instanceof DisplayedPartInput && partUnder instanceof DisplayedPartOutput)){ // Assign the source and sink parts depending what we are hovering over. PartConnectable sourcePart; PartInput sinkPart; if (connectionDragAnchorPart instanceof DisplayedPartOutput) { sourcePart = connectionDragAnchorPart.getPartConnectable(); sinkPart = ((DisplayedPartInput)partUnder).getPartInput(); } else { sourcePart = ((DisplayedPartConnectable)partUnder).getPartConnectable(); sinkPart = ((DisplayedPartInput)connectionDragAnchorPart).getPartInput(); } Gem sourceGem = sourcePart.getGem(); Gem sinkGem = sinkPart.getGem(); if (sourceGem instanceof ValueGem) { // Check if a value gem can be connected. // They get special treatment since autoburning doesn't apply to value gems. ModuleTypeInfo currentModuleTypeInfo = tableTop.getCurrentModuleTypeInfo(); ValueEditorManager valueEditorManager = gemCutter.getValueEditorManager(); if (!valueEditorManager.canInputDefaultValue(sinkPart.getType())) { setCursor(connectNoParkCursor); } else if (GemGraph.isCompositionConnectionValid(sourcePart, sinkPart, currentModuleTypeInfo)) { setCursor(connectCursor); } else if (GemGraph.isDefaultableValueGemSource(sourcePart, sinkPart, gemCutter.getConnectionContext())) { setCursor(connectCursor); } else { setCursor(connectNoParkCursor); } } else if (sinkGem instanceof RecordFieldSelectionGem && !((RecordFieldSelectionGem)sinkGem).isFieldFixed()) { if (GemGraph.isValidConnectionToRecordFieldSelection(sourcePart, sinkPart, tableTop.getCurrentModuleTypeInfo()) != null) { setCursor(connectCursor); } else { setCursor(connectNoParkCursor); } } else if (GemGraph.arePartsConnectable(sourcePart, sinkPart)) { // Check if the parts can be connected either through burning or direct connection. // Even if they can be connected without autoburning we want to at least try burning them. // Why? Because it might be better to burn the gem than just connecting it. We want to // at least consider that possibility and recommend the best choice to the user. if (tableTop.getBurnManager().getAutoburnLastResult() == AutoburnLogic.AutoburnAction.BURNED) { // If the gem has already been burnt previously then keep showing the burn cursor. // Don't try and burn the gem again since that will just show the connect cursor, // since the autoburn logic already sees the gem as being burnt. setCursor(burnCursor); } else if (GemGraph.isConnectionValid(sourcePart, sinkPart)) { // Try making a connection using autoburning. AutoburnLogic.AutoburnAction autoBurnResult = tableTop.getBurnManager().handleAutoburnGemGesture(sourceGem, sinkPart.getType(), true); if (autoBurnResult == AutoburnLogic.AutoburnAction.BURNED) { setCursor(burnCursor); } else if (autoBurnResult == AutoburnLogic.AutoburnAction.MULTIPLE) { setCursor(burnQuestionCursor); } else if (autoBurnResult == AutoburnLogic.AutoburnAction.IMPOSSIBLE) { setCursor(connectNoParkCursor); } else { setCursor(connectCursor); } } else { // Won't connect through autoburning. setCursor(connectNoParkCursor); } } else { // The connection as a whole is invalid (eg. destination already connected). setCursor(connectNoParkCursor); } } else { // We're not trying to connect a sink. Reset the cursor. setCursor(null); // If we aren't disconnecting, we should undo any autoburn we previously performed (if any). if (dragMode != TableTopDragMode.DISCONNECTING && tableTop.getBurnManager().getAutoburnLastResult() == AutoburnLogic.AutoburnAction.BURNED) { tableTop.getBurnManager().handleAutoburnGemGesture(connectionDragAnchorPart.getGem(), null, false); } } } /** * Check whether the tabletop needs expanding, and take care of the expansion grunt work if necessary. * Note that the "where" parameter may be modified during the execution of this method, to take * into account the new coordinates in an expanded tabletop. * @param where Point The mouse location to check. * @return boolean Whether the tabletop expanded. */ private boolean checkExpand(Point where) { Rectangle visibleRect = getVisibleRect(); // if the point is in the visible bounds of the tabletop, we're ok. if (visibleRect.contains(where)) { return false; } // otherwise, we may need to expand. Declare the expand flag. boolean expand = false; // the dimension of the new tabletop if we expand Dimension dim = new Dimension(); int whereX = where.x; int whereY = where.y; // // x-axis expansion // if (whereX < 0) { // expand left expand = true; int moveDistance = -(whereX); // Translate the gemgraph and tabletop points to the right tableTop.moveAllGems(moveDistance, 0); pressedAt.x += moveDistance; where.x += moveDistance; backgroundImageOriginOffsetX += moveDistance; // update the last clip bounds so that the old drag ghosts will be undrawn properly if (lastDragClipArea != null) { Rectangle rect = lastDragClipArea.getBounds(); rect.x += moveDistance; lastDragClipArea = rect; } dim = new Dimension(getWidth() + moveDistance, getHeight()); } else if (whereX > getSize().width) { // Expand right. expand = true; int expandDistanceX = whereX - getSize().width; dim = new Dimension(getWidth() + expandDistanceX, getHeight()); } // // y-axis expansion // if (whereY < 0) { // Expand up expand = true; int moveDistance = -(whereY); // Translate the gemgraph and tabletop points down tableTop.moveAllGems(0, moveDistance); pressedAt.y += moveDistance; where.y += moveDistance; backgroundImageOriginOffsetY += moveDistance; // update the last clip bounds so that the old drag ghosts will be undrawn properly if (lastDragClipArea != null) { Rectangle rect = lastDragClipArea.getBounds(); rect.y += moveDistance; lastDragClipArea = rect; } dim = new Dimension(getWidth(), getHeight() + moveDistance); } else if (whereY > getSize().height) { // Expand down. expand = true; int expandDistanceY = whereY - getSize().height + 1; // + 1 so we can see the last pixel dim = new Dimension(getWidth(), getHeight() + expandDistanceY); } // now take appropriate action now that we decided whether or not to expand if (expand) { // Set the size of the tabletop. This also invokes revalidation since it's a JContainer. setSize(dim); setPreferredSize(dim); revalidate(); } return expand; } /** * Draw the appropriate drag ghost according to the current drag mode * - the drag ghost may be a connection line, gem drag ghost, or drag selection outline. * * @param drawAction the current draw action. If this is DrawAction.REDRAW, then dragAction and dragSelectMode * do not have to be provided. * @param selectMode the mode to draw the drag ghost in. This can be one of three modes: * <br>REPLACE_SELECT: Solid line - select only enclosed Gems * <br>TOGGLE: Wavy line - toggle selection of enclosed Gems * <br>SELECT: Dotted line - select enclosed Gems, preserving ones already selected * <br>This parameter can be ignored if the drag action is not drag selecting. * @param graphics Graphics2D the graphics context to use. * */ synchronized void drawDragGhost(DrawAction drawAction, SelectMode selectMode, Graphics2D graphics) { Point pressedAt, dragPos; DragMode dragMode; DisplayedGem[] dragList; // set draw info based on whether or not we are redrawing. if (drawAction == DrawAction.REDRAW) { if (redrawInfo == null) { return; // nothing to redraw } // reload the draw info pressedAt = redrawInfo.pressedAt; dragPos = redrawInfo.dragPos; dragMode = redrawInfo.dragMode; dragList = redrawInfo.dragList; selectMode = redrawInfo.selectMode; } else { try { // there's a chance that click and drag positions could be modified from the event dispatch thread // and that this method is called from the painting thread. Thus, click and drag positions could be // concurrently used and modified in the middle of this thread! Thus we clone the points locally: pressedAt = (Point)this.pressedAt.clone(); dragPos = (Point)this.dragPos.clone(); dragMode = this.dragMode; dragList = this.dragList; } catch (NullPointerException npe) { // nothing to (un)draw.. return; } } // check for nothing to do if (lastDragClipArea == null && drawAction == DrawAction.UNDRAW) { redrawInfo = null; return; } // Get the current graphics context Graphics2D g2d = (Graphics2D)getGraphics(); // clip further, and update our clip areas, if necessary if (drawAction == DrawAction.DRAW || drawAction == DrawAction.REDRAW) { Shape graphicsClip = graphics.getClip(); if (graphicsClip != null) { // clip to the clip area derived from the passed in graphics object g2d.setClip(graphicsClip); } else { // make sure there is a clip, so that the check for null above doesn't fail inappropriately on undraw g2d.setClip(getVisibleRect()); } // update the latest clip area lastDragClipArea = g2d.getClip(); // update redraw info redrawInfo = new RedrawInfo(pressedAt, dragPos, dragMode, dragList, selectMode); } else if (drawAction == DrawAction.UNDRAW) { // use the clip area present when the ghost was last drawn g2d.setClip(lastDragClipArea); // update the latest clip area lastDragClipArea = null; // clear redraw info if we're undrawing, since there will be nothing to redraw redrawInfo = null; } // Enter XOR mode g2d.setXORMode(Color.white); // Now (un)draw the drag ghost according to the drag action indicated if (dragMode == TableTopDragMode.GEMDRAGGING || dragMode == TableTopDragMode.CTRLDRAGGING) { // For each gem, draw a ghost for (final DisplayedGem dGem : dragList) { // The ghost is the gem body translated by the drag offset Shape ghost = dGem.getDisplayedGemShape().getBodyShape(); // Translate into the correct location (how we do this depends on the type) int translateX = dragPos.x - pressedAt.x; int translateY = dragPos.y - pressedAt.y; if (ghost instanceof Polygon) { ((Polygon)ghost).translate(translateX, translateY); } else if (ghost instanceof RectangularShape) { RectangularShape rectGhost = (RectangularShape)ghost; Rectangle bounds = rectGhost.getBounds(); rectGhost.setFrame(bounds.x + translateX, bounds.y + translateY, bounds.getWidth(), bounds.getHeight()); } g2d.setStroke(new BasicStroke((float) 2.0)); g2d.draw(ghost); } } else if (dragMode == TableTopDragMode.CONNECTING || dragMode == TableTopDragMode.DISCONNECTING) { // Figure out which point is the beginning point of the connection based on the type of // the part where the connection originated. Point fromPoint, toPoint; if (tableTop.getGemPartUnder(pressedAt) instanceof DisplayedPartOutput) { fromPoint = pressedAt; toPoint = dragPos; } else { fromPoint = dragPos; toPoint = pressedAt; } ConnectionRoute route = new ConnectionRoute(fromPoint, toPoint); DisplayedConnection.genConnectionRoute(route, DisplayConstants.REVERSE_CONNECTION_HOOK_SIZE); // set, save the draw color if (drawAction == DrawAction.DRAW || drawAction == DrawAction.REDRAW) { // Set colour to the appropriate colour for the output type implied. Color connectColour = tableTop.getTypeColour(connectionDragAnchorPart); g2d.setColor(connectColour); // save the last drag colour so we can undraw later in the same colour lastDragColour = connectColour; } else { // undraw in the last drag colour g2d.setColor(lastDragColour); } // Draw a connection line route.draw(g2d); } else if (dragMode == TableTopDragMode.SELECTING) { // Set stroke characteristics. Depends on mode if (selectMode == SelectMode.TOGGLE) { g2d.setStroke(new BasicStroke(2f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); //g2d.setStroke(new com.sun.glf.goodies.WaveStroke(1, 5, 2)); } else if (selectMode == SelectMode.SELECT) { g2d.setStroke(new BasicStroke(2f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND, 8f, new float[]{6f, 6f}, 0f)); } else { // the default case: DragSelectMode.REPLACE_SELECT: g2d.setStroke(new BasicStroke(2f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); //g2d.setStroke(new com.sun.glf.goodies.TextStroke("Select", new Font("dialog", Font.PLAIN, 12), true, 0)); } // It turns out drawing four lines is significantly faster than calling drawRect(int, int, int, int) // This is most noticeable in xor mode (which we use for this drag ghost) g2d.drawLine(pressedAt.x, pressedAt.y, pressedAt.x, dragPos.y); g2d.drawLine(pressedAt.x, pressedAt.y, dragPos.x, pressedAt.y ); g2d.drawLine(pressedAt.x, dragPos.y, dragPos.x, dragPos.y); g2d.drawLine(dragPos.x, pressedAt.y, dragPos.x, dragPos.y); } // dispose the graphics object g2d.dispose(); } /** * Carry out setup appropriate to enter the drag state. Principal effect is to change dragMode as appropriate. * @param e MouseEvent the mouse event which triggered entry into the drag state. */ public void enterDragState(MouseEvent e) { // Store the focus state and move the focus to the table top for now tableTop.saveFocus(); requestFocus(); // popups should not be shown in most drag states popupShouldShow = false; // find the drag mode appropriate to the place on which we initiated the drag DragMode nextMode = getDragModeForDragOrigin(); ExtendedUndoableEditSupport undoableEditSupport = tableTop.getUndoableEditSupport(); if (nextMode == TableTopDragMode.GEMDRAGGING && SwingUtilities.isLeftMouseButton(e)) { if (e.isControlDown()) { // Ensure that the gem beneath the cursor is selected when we begin the drag. DisplayedGem clickedGem = tableTop.getGemUnder(pressedAt); tableTop.selectDisplayedGem(clickedGem, false); tableTop.setFocusedDisplayedGem(clickedGem); shiftSelectionAnchorGem = clickedGem; repaint(); nextMode = TableTopDragMode.CTRLDRAGGING; } // dragging gems around setDragMode(nextMode); // Create the dragList of selected Gems dragList = tableTop.getSelectedDisplayedGems(); } else if (nextMode == TableTopDragMode.CONNECTING) { // connecting up gems dragMode = nextMode; // Increment the update level to aggregate any burns with connection change edits. undoableEditSupport.beginUpdate(); } else if (nextMode == TableTopDragMode.DISCONNECTING) { // disconnecting gems dragMode = nextMode; // Increment the update level to aggregate any unburns with connection change edits. undoableEditSupport.beginUpdate(); // Get the part which the user pressed. Should be connectable! DisplayedPartConnectable partPressed = (DisplayedPartConnectable)tableTop.getGemPartUnder(pressedAt); // Keep track of the disconnected part. disconnectedDisplayedPart = partPressed; // Get the gem connection to disconnect. DisplayedConnection disconnectConn = partPressed.getDisplayedConnection(); // Adjust the apparent click point to be the point of the arrow of the part that is // not being disconnected. if (partPressed instanceof DisplayedPartOutput) { connectionDragAnchorPart = disconnectConn.getDestination(); } else { connectionDragAnchorPart = disconnectConn.getSource(); } pressedAt = connectionDragAnchorPart.getConnectionPoint(); // Indicate to the user that a drag disconnect is possible. setCursor(connectCursor); // Disconnect the connection. tableTop.handleDisconnectGesture(disconnectConn.getConnection()); // Undo any autoburns if we disconnected an output if (partPressed instanceof DisplayedPartOutput) { DisplayedGem burnGem = partPressed.getDisplayedGem(); tableTop.getBurnManager().doUnburnAutomaticallyBurnedInputsUserAction(burnGem.getGem()); } } else if (nextMode == TableTopDragMode.SELECTING && SwingUtilities.isLeftMouseButton(e)) { // drag selecting dragMode = nextMode; } else { // not dragging really, so we can display popups dragMode = TableTopDragMode.USELESS; popupShouldShow = true; } } /** * Carry out setup appropriate to exit the drag state. * Undraw the drag ghost and carry out translating/connecting/selecting * @param e MouseEvent the mouse event which caused an exit from the drag state */ public void exitDragState(MouseEvent e) { // Finish the ghost drawing (undraw the last lot) if (isUsefulDragMode(dragMode)) { Graphics2D g2d = (Graphics2D)getGraphics(); drawDragGhost(DrawAction.UNDRAW, SelectMode.REPLACE_SELECT, g2d); g2d.dispose(); } // Where are we now? // We use dragPos instead of e.getPoint() so that the gem drops where the drag ghost shows it. Point where = dragPos; ExtendedUndoableEditSupport undoableEditSupport = tableTop.getUndoableEditSupport(); // Now do whatever based on the drag mode if (dragMode == TableTopDragMode.CTRLDRAGGING) { // Ctrl Drag is used as a clone function // We want to group this stuff as one single action undoableEditSupport.beginUpdate(); undoableEditSupport.setEditName(GemCutter.getResourceString("UndoText_CopyDrag")); DisplayedGemSelection displayedGemSelection = new DisplayedGemSelection(dragList, gemCutter); Rectangle rect = dragList[0].getBounds(); for (int i = 1; i < dragList.length; i++) { rect.add(dragList[i].getBounds()); } int x = where.x - (pressedAt.x - rect.x); int y = where.y - (pressedAt.y - rect.y); tableTop.doPasteUserAction(displayedGemSelection, new Point(x, y)); undoableEditSupport.endUpdate(); } else if (dragMode == TableTopDragMode.GEMDRAGGING) { // Increment the update level for the edit undo. This will aggregate the gem translations. undoableEditSupport.beginUpdate(); if (dragList.length > 0) { undoableEditSupport.setEditName(dragList.length > 1 ? GemCutter.getResourceString("UndoText_MoveGems") : GemCutter.getResourceString("UndoText_MoveGem")); } // Do the move for each selected gem for (final DisplayedGem displayedGem : dragList) { Point newGemLocation = displayedGem.getLocation(); newGemLocation.translate(where.x - pressedAt.x, where.y - pressedAt.y); // Perform the translation tableTop.doChangeGemLocationUserAction(displayedGem, newGemLocation); } // Decrement the update level. This will post the edit if the level is zero. undoableEditSupport.endUpdate(); } else if (dragMode == TableTopDragMode.CONNECTING || dragMode == TableTopDragMode.DISCONNECTING) { // see if we can connect anything DisplayedPart partUnder = tableTop.getGemPartUnder(where); boolean connected = false; if (partUnder != null) { Connection newConnection = null; if (connectionDragAnchorPart instanceof DisplayedPartOutput && partUnder instanceof DisplayedPartInput) { newConnection = tableTop.handleConnectGemPartsGesture(connectionDragAnchorPart.getPartConnectable(), ((DisplayedPartInput)partUnder).getPartInput()); } else if (partUnder instanceof DisplayedPartConnectable && connectionDragAnchorPart instanceof DisplayedPartInput) { newConnection = tableTop.handleConnectGemPartsGesture(((DisplayedPartConnectable)partUnder).getPartConnectable(), ((DisplayedPartInput)connectionDragAnchorPart).getPartInput()); } connected = (newConnection != null); } // Undo any autoburns if we didn't connect anything if (!connected && connectionDragAnchorPart instanceof DisplayedPartOutput) { DisplayedGem burnGem = connectionDragAnchorPart.getDisplayedGem(); tableTop.getBurnManager().doUnburnAutomaticallyBurnedInputsUserAction(burnGem.getGem()); } if (!connected && dragMode == TableTopDragMode.CONNECTING) { // Don't post the edit if connecting and nothing happened. undoableEditSupport.endUpdateNoPost(); } else if (connected && dragMode == TableTopDragMode.DISCONNECTING && disconnectedDisplayedPart == partUnder){ // Also don't post the edit if all we did was reconnect a part that we disconnected. undoableEditSupport.endUpdateNoPost(); } else { if (dragMode == TableTopDragMode.CONNECTING) { undoableEditSupport.setEditName(GemCutter.getResourceString("UndoText_ConnectGems")); } else { undoableEditSupport.setEditName(GemCutter.getResourceString("UndoText_DisconnectGems")); } // Decrement the update level, possibly triggering the edit to be posted. undoableEditSupport.endUpdate(); } } else if (dragMode == TableTopDragMode.SELECTING && pressedAt != null) { // calculate the bounds of the select area Rectangle hitRect = new Rectangle(pressedAt); hitRect.add(where); // Perform appropriate selection operation for each intersecting gem. // We need to use the TOGGLE selection mode if the drag was started with the // CTRL modifier or the SELECT selection mode if the drag was started with the // SHIFT modifier... otherwise just use the last mode. The SHIFT modifier seems to // mask the CTRL modifier in this instance so look for that first here. SelectMode selMode; if (dragInitiatedWithSHIFT) { selMode = SelectMode.SELECT; } else if (dragInitiatedWithCTRL) { selMode = SelectMode.TOGGLE; } else { selMode = lastSelectMode; } tableTop.selectGems(hitRect, selMode); // todoSN - the Windows Desktop does not alter the focused icon when drag selection is // performed so we won't here either. Unfortunately, this may result in no gems having // focus if a gem is not specifically clicked so we will give focus on a drag if the // existing focused gem is null. This is probably just a temporary fix for this issue. // If the existing focused gem is null give focus to the Gem that is closest to the point // where dragging started and inside the drag rectangle. DisplayedGem[] selectedGems = tableTop.getSelectedDisplayedGems(); double dist = -1; DisplayedGem gemToFocusOn = null; for (final DisplayedGem dGem : selectedGems) { Point2D centrePoint = dGem.getCenterPoint(); if (hitRect.contains(centrePoint)) { double thisDist = pressedAt.distance(centrePoint); if (dist < 0 || thisDist < dist) { dist = thisDist; gemToFocusOn = dGem; } } } // Actually set the focus here if we found a gem to give focus to. If the user is // replace selecting then we need to update the focus no matter what. if (tableTop.getFocusedDisplayedGem() == null && (gemToFocusOn != null || lastSelectMode == SelectMode.REPLACE_SELECT)) { tableTop.setFocusedDisplayedGem(gemToFocusOn); } // Only update the selection anchor if the SHIFT and CTRL modifiers are NOT used on // mouse button release. if (!e.isShiftDown() && !e.isControlDown()) { shiftSelectionAnchorGem = gemToFocusOn; } } // Dragging is finished. Reset dragMode. dragMode = DragMode.NOTDRAGGING; } /** * Get the drag mode appropriate to the origin of the current drag * @return DragMode the drag mode appropriate to the origin of the current drag. */ private DragMode getDragModeForDragOrigin() { // Get the part which the user pressed DisplayedPart partPressed = null; if (pressedAt != null) { partPressed = tableTop.getGemPartUnder(pressedAt); } // default is selecting (if not dragging or composing) DragMode returnMode = TableTopDragMode.SELECTING; // Did they hit anything? if (partPressed != null) { if (partPressed instanceof DisplayedPartConnectable && ((DisplayedPartConnectable)partPressed).getPartConnectable().isConnected()){ // Disconnecting returnMode = TableTopDragMode.DISCONNECTING; } else if (partPressed instanceof DisplayedPartBody) { // Dragging gem(s) returnMode = TableTopDragMode.GEMDRAGGING; } else if (partPressed instanceof DisplayedPartConnectable) { // We got an input or output. returnMode = TableTopDragMode.CONNECTING; } } return returnMode; } /** * Get the select mode appropriate to the modifiers on the mouse event * @param e MouseEvent the related mouse event * @return SelectMode the selection mode appropriate to the modifiers on the mouse event */ private SelectMode getSelectModeForEvent(MouseEvent e) { if (e.isShiftDown()) { return SelectMode.SELECT; } else if (e.isControlDown()) { return SelectMode.TOGGLE; } else { return SelectMode.REPLACE_SELECT; } } /** * Whether this drag mode actually enables accomplishing anything. * Gem dragging, connecting, disconnecting, and selecting are useful. * Aborted, useless, and not-dragging states are not useful. * @param mode DragMode the DragMode to check * @return boolean true if accomplishing anything with this drag. */ protected final boolean isUsefulDragMode(DragMode mode) { return (mode == TableTopDragMode.GEMDRAGGING || mode == TableTopDragMode.CONNECTING || mode == TableTopDragMode.DISCONNECTING || mode == TableTopDragMode.SELECTING || mode == TableTopDragMode.CTRLDRAGGING); } /** * If the current dragmode is either GEMDRAGGING or CTRLDRAGGING, * then it is considered a 'gemdragging' action. * @return boolean */ boolean isGemDragging() { return ((dragMode == TableTopDragMode.GEMDRAGGING) || (dragMode == TableTopDragMode.CTRLDRAGGING)); } void setDragMode(DragMode mode) { if (mode == TableTopDragMode.CTRLDRAGGING) { setCursor(cloneGemCursor); } else { setCursor(null); } dragMode = mode; } /** * Add a gem to the tabletop if appropriate. * This should be called from mousePressed() * @return boolean true only if a gem was added to the tabletop */ private boolean maybeAddGem() { // If we are adding a Gem, click position indicates the position of the Gem if (gemCutter.getGUIState() == GemCutter.GUIState.ADD_GEM) { // Tell the GemCutter where to add the gem DisplayedGem addingGem = gemCutter.getAddingDisplayedGem(); if (addingGem != null) { tableTop.doAddGemUserAction(addingGem, pressedAt); } else { DisplayedPart part = tableTop.getGemPartUnder(pressedAt); boolean showedIntellicutForPart = false; // Check if the user clicked on a part and if we should start Intellicut for that. if (part instanceof DisplayedPartConnectable) { showedIntellicutForPart = tableTop.maybeStartIntellicutMode(part); } // Just show Intellicut for the table top if we didn't show it for a part if (!showedIntellicutForPart) { // If there was a part use it's bounds as the display rect. That way if the user clicks on // a gem body part, the list wont obscure the part. Rectangle displayRect = part != null ? part.getBounds() : new Rectangle(pressedAt); // Use the lower-right of the display rect as the drop point. That way if the user clicks // a gem body part the new gem will appear next to the old one, not over it. Point dropPoint = new Point(displayRect.x + displayRect.width, displayRect.y + displayRect.height); gemCutter.getIntellicutManager().startIntellicutModeForTableTop(displayRect, dropPoint); } } // Back to edit mode gemCutter.enterGUIState(GemCutter.GUIState.EDIT); // Disallow further action (eg. drag) abortDrag(); // We added a gem return true; } return false; } /** * Invoked when a mouse button is pressed on a component and then * dragged. Mouse drag events will continue to be delivered to * the component where the first originated until the mouse button is * released (regardless of whether the mouse position is within the * bounds of the component). */ public void mouseDragged(MouseEvent e) { try { // If we're still painting, or we bailed out, do nothing. if (isPainting != 0 || dragMode == DragMode.ABORTED) { return; } // If needed forward the mouse event to the vep. if (dragStartedOverVEP && valueEntryPanelHit(e.getPoint())) { forwardMouseEvent (getValueEntryPanel((ValueGem)clickGem.getGem()), e); return; } if (dragStartedOverVEP) { return; } // Defer to the superclass method super.mouseDragged(e); } catch (Throwable t) { // some error occurred. Treat this as an aborted drag. abortDrag(); t.printStackTrace(); } } /** * {@inheritDoc} */ public void mouseEntered(MouseEvent e) { } /** * {@inheritDoc} */ public void mouseExited(MouseEvent e) { } /** * Invoked when the mouse has been moved on a component * (with no buttons no down). */ public void mouseMoved(MouseEvent e) { // Test if we are over a gem part DisplayedPart partUnder = tableTop.getGemPartUnder(e.getPoint()); // If we are dragging over a VEP make sure to display the correct cursor. if (valueEntryPanelHit(e.getPoint())) { ValueEntryPanel vep = getValueEntryPanel((ValueGem)partUnder.getGem()); Point vepPoint = SwingUtilities.convertPoint(TableTopPanel.this, e.getPoint(), vep); setCursor(vep.getCursor(vepPoint)); } else { setCursor(Cursor.getDefaultCursor()); } // display some help maybe if (gemCutter.getGUIState() == GemCutter.GUIState.EDIT) { // unconnected connectable parts of a non-broken gem if (partUnder instanceof DisplayedPartConnectable && // connectable !((DisplayedPartConnectable)partUnder).getPartConnectable().isConnected() && // not connected !((partUnder.getGem().getRootGem() != null) && // not ancestor of a broken forest GemGraph.isAncestorOfBrokenGemForest(partUnder.getGem().getRootGem()))) { if (partUnder instanceof DisplayedPartInput) { // double click to burn/unburn an unconnected input gemCutter.getStatusMessageDisplayer().setMessageFromResource(TableTopPanel.this, "SM_DblClickBurn", StatusMessageDisplayer.MessageType.PERSISTENT); } else { gemCutter.getStatusMessageDisplayer().clearMessage(TableTopPanel.this); } } else { gemCutter.getStatusMessageDisplayer().clearMessage(TableTopPanel.this); } } else { // not in edit mode gemCutter.getStatusMessageDisplayer().clearMessage(TableTopPanel.this); } } /** * Invoked when a mouse button has been pressed on a component. */ public void mousePressed(MouseEvent e){ try { // move focus to the tabletop requestFocus(); dragStartedOverVEP = false; // A mousePress should stop Intellicut. IntellicutManager.IntellicutMode prevIntellicutMode = tableTop.getIntellicutManager().getIntellicutMode(); tableTop.getIntellicutManager().stopIntellicut(); // Ignore clicks unless we're editing or adding gems GemCutter.GUIState GUIState = gemCutter.getGUIState(); if ((GUIState != GemCutter.GUIState.EDIT) && (GUIState != GemCutter.GUIState.ADD_GEM)) { return; } // Call the superclass method super.mousePressed(e); // see if this resulted in an aborted drag if (dragMode == DragMode.ABORTED) { return; } // add a gem if appropriate if (maybeAddGem()) { return; } DisplayedPart partPressed = tableTop.getGemPartUnder(pressedAt); // Now do whatever based on what was pressed on mousePressedOn(e, partPressed, prevIntellicutMode); // Added for Linux compatibility (KDE popups are shown on mouse down) if (!isUsefulDragMode(dragMode)) { maybeShowPopup(e); } } catch (Throwable t) { // some error occurred. Treat this as an aborted drag. abortDrag(); t.printStackTrace(); } } /** * Take action based on what was pressed. * @param e the relevant event * @param partPressed the part which was pressed * @param prevIntellicutMode the intellicut mode before the press occurred */ public void mousePressedOn(MouseEvent e, DisplayedPart partPressed, IntellicutManager.IntellicutMode prevIntellicutMode) { // Did they hit anything? if (partPressed != null) { if (partPressed instanceof DisplayedPartBody) { // Make sure the popup menus for running gems are closed. Their // ability to alter focus and selection of gems on the TableTop // interferes with the selection and focus shifting done here because // they don't actually close until after we are done. gemCutter.closeRunPopupMenus(); // Update the gem which was clicked on. clickGem = partPressed.getDisplayedGem(); // Selection state changes depend on keyboard modifiers if (e.isControlDown() && e.isShiftDown()) { // Only worry about the left button here if (SwingUtilities.isLeftMouseButton(e)) { // If the selection anchor is null use the clicked Gem if (shiftSelectionAnchorGem == null) { shiftSelectionAnchorGem = clickGem; } // Select all the Gems from the selection anchor to this Gem // and update the focused Gem. This selection set should union with // any existing selection set. DO NOT update the selection anchor Rectangle2D rectangle = getRectangleForDisplayedGems(shiftSelectionAnchorGem, clickGem); tableTop.selectGems(rectangle, SelectMode.SELECT); tableTop.setFocusedDisplayedGem(clickGem); } } else if (e.isControlDown()) { // Do nothing } else if (e.isShiftDown()) { // Only worry about the left button on shift+presses here. We'll have to // worry about the shift+right button in the 'on clicking' event if (SwingUtilities.isLeftMouseButton(e)) { // If the shift selection anchor is null use the clicked Gem if (shiftSelectionAnchorGem == null) { shiftSelectionAnchorGem = clickGem; } // Select all the Gems from the selection anchor to this Gem // and update the focused Gem. This new selection set should REPLACE // any existing selection set. DO NOT update the selection anchor Rectangle2D rectangle = getRectangleForDisplayedGems(shiftSelectionAnchorGem, clickGem); tableTop.selectGems(rectangle, SelectMode.REPLACE_SELECT); tableTop.setFocusedDisplayedGem(clickGem); } } else { // Gem is selected, all others deselected, unless the Gem // is already selected (in which case this is a NOP for now - it will be handled // by mouseReallyClicked() later on) if (!tableTop.isSelected(clickGem)) { tableTop.selectDisplayedGem(clickGem, true); } // Give the Gem that was just clicked the focus and // reset the selection anchor tableTop.setFocusedDisplayedGem(clickGem); shiftSelectionAnchorGem = clickGem; // If needed forward the mouse event to the vep. if (valueEntryPanelHit(e.getPoint())) { forwardMouseEvent (getValueEntryPanel((ValueGem)clickGem.getGem()), e); dragStartedOverVEP = true; } } //Create the dragList of selected Gems dragList = tableTop.getSelectedDisplayedGems(); } else if (partPressed instanceof DisplayedPartOutput) { DisplayedPartOutput outPart = (DisplayedPartOutput)partPressed; // If we were in intellicut mode part sink, possibly auto connect it. if (prevIntellicutMode == IntellicutManager.IntellicutMode.PART_INPUT && tableTop.handleIntellicutAutoConnectGesture(outPart)) { // disable dragging if we autoconnected setDragMode(DragMode.ABORTED); return; } // We could be starting a drag. Adjust the apparent press point to be the point of the arrow pressedAt = outPart.getConnectionPoint(); // Set the source part connectionDragAnchorPart = (DisplayedPartConnectable) partPressed; } else if (partPressed instanceof DisplayedPartInput) { DisplayedPartInput sinkPart = (DisplayedPartInput)partPressed; // If we were in intellicut mode part source, possibly auto connect it. if (prevIntellicutMode == IntellicutManager.IntellicutMode.PART_OUTPUT && tableTop.handleIntellicutAutoConnectGesture(sinkPart)) { // disable dragging if we autoconnected setDragMode(DragMode.ABORTED); return; } // We could be starting a drag. Adjust the apparent press point to be the connection point pressedAt = sinkPart.getConnectionPoint(); // Set the source part connectionDragAnchorPart = (DisplayedPartConnectable) partPressed; } } else { // Nothing hit - deselect everything unless a meta key is pressed // Selection state changes depend on shift state if (!(e.isShiftDown() || e.isControlDown())) { tableTop.selectDisplayedGem(null, false); tableTop.setFocusedDisplayedGem(null); shiftSelectionAnchorGem = null; } } } /** * Surrogate method for mouseClicked. Called only when our definition of click occurs. * @param e MouseEvent the relevant event * @return boolean true if the click was a double click */ public boolean mouseReallyClicked(MouseEvent e) { // call the superclass method boolean doubleClicked = super.mouseReallyClicked(e); // If needed forward the mouse event to the vep. if (valueEntryPanelHit(e.getPoint())) { forwardMouseEvent (getValueEntryPanel((ValueGem)clickGem.getGem()), e); return doubleClicked; } // Test if we hit any part of the gem DisplayedPart partClicked = tableTop.getGemPartUnder(e.getPoint()); if (partClicked != null) { // if the user double left clicks on an input part, burn if appropriate if (SwingUtilities.isLeftMouseButton(e) && doubleClicked) { tableTop.getBurnManager().handleBurnInputGesture(partClicked); } // Get the gem which was clicked on DisplayedGem displayedGemClicked = tableTop.getGemUnder(e.getPoint()); Gem gemClicked = displayedGemClicked.getGem(); // If the part clicked was a body part... could be one of several things to do if (partClicked instanceof DisplayedPartBody) { // If this is a double left click we want to open the editors for the CodeGem and CollectorGem. // todoSN - should the ValueGem editor be given focus here? if (doubleClicked && SwingUtilities.isLeftMouseButton(e)) { if (gemClicked instanceof CodeGem) { tableTop.showCodeGemEditor((CodeGem)displayedGemClicked.getGem(), true); } else if (gemClicked instanceof CollectorGem) { if (tableTop.isSelected(displayedGemClicked)) { tableTop.displayLetNameEditor((CollectorGem)gemClicked); } } else if (gemClicked instanceof RecordFieldSelectionGem) { Action action = getChangeRecordSelectionFieldAction(gemClicked); if (action.isEnabled()) { // action will only be enabled when RecordFieldSelection Gem can be edited, // ie. when it is not connected to any broken gems tableTop.displayRecordFieldSelectionEditor((RecordFieldSelectionGem)gemClicked); } } else if (gemClicked instanceof RecordCreationGem){ // find the field that was clicked on int fieldIndex = displayedGemClicked.getDisplayedGemShape().inputNameTagHit(e.getPoint()); if (fieldIndex != -1) { FieldName fieldToRename = ((RecordCreationGem)gemClicked).getFieldName(fieldIndex); Action action = getRenameRecordFieldAction((RecordCreationGem)gemClicked, fieldToRename.getCalSourceForm()); if (action.isEnabled()) { tableTop.displayFieldRenameEditor((RecordCreationGem)gemClicked, fieldToRename); } } } } // Handle the single click cases // NOTE: The CTRL+SHIFT modifiers work the same as just the SHIFT modifier so we // don't need to do anything special for the combination, however it is important // that the SHIFT portion of the if statement come before the CTRL portion so // that it gets executed when the SHIFT+CTRL combo is used. if (e.isShiftDown()) { // Only worry about the right button here. if (SwingUtilities.isRightMouseButton(e)) { // If the clicked gem is not selected then singleton select it, but do NOT give it focus. if (!tableTop.isSelected(clickGem)) { tableTop.selectDisplayedGem(clickGem, true); } // todoSN - Right now we don't have a permanent focused Gem so if it is null here assign one if (tableTop.getFocusedDisplayedGem() == null) { tableTop.setFocusedDisplayedGem(clickGem); } } } else if (e.isControlDown()) { // Worry about the left button here. if (SwingUtilities.isLeftMouseButton(e)) { // Toggle the selection state of this Gem, give focus to it and make it the shift selection anchor tableTop.toggleSelected(clickGem); tableTop.setFocusedDisplayedGem(clickGem); shiftSelectionAnchorGem = clickGem; } } else { // There were no modifiers! Only worry about the left button here. The right mouse // button will trigger the context menu on the button release event. if (SwingUtilities.isLeftMouseButton(e)) { // When there is a selection set and the clicked Gem is in that set // we need to un-select all but the clicked gem here. DisplayedGem[] selectedGems = tableTop.getSelectedDisplayedGems(); if (selectedGems != null && selectedGems.length > 1) { for (final DisplayedGem dGem : selectedGems) { if (dGem != displayedGemClicked) { tableTop.selectDisplayedGem(dGem, false); } } } } } } } return doubleClicked; } /** * Surrogate method for mouseDragged. Called only when our definition of drag occurs. * Note that the setup for the transition from pressed to dragged is carried out in the mouseDragged() method. * @param e MouseEvent the relevant event * @param where Point the (possibly adjusted from e) coordinates of the drag * @param wasDragging boolean True: this is a continuation of a drag. False: first call upon transition * from pressed to drag. */ public void mouseReallyDragged(MouseEvent e, Point where, boolean wasDragging) { if (isGemDragging()) { // expand the tabletop if necessary. This also translates "where" into the new coordinates. checkExpand(where); // Must do the target gem relocating here. Or else we will have repainting problems later. tableTop.checkTargetDockLocation(); // update the drag position, making sure that the new point is visible. Rectangle visibleRect = new Rectangle(where); updateDragPosition(where, visibleRect, wasDragging); } else if (dragMode == TableTopDragMode.CONNECTING || dragMode == TableTopDragMode.DISCONNECTING) { // update the apparent click point in case the source moved (eg. the connected gem morphs) pressedAt = connectionDragAnchorPart.getConnectionPoint(); // update the drag position, making sure that the new point is visible. Rectangle visibleRect = new Rectangle(where); updateDragPosition(where, visibleRect, wasDragging); // update the tabletop state to take into account the present drag position while connecting changeStateForConnecting(where); // undo autoburns if we're dragging away from an output DisplayedPart displayedPartUnder = tableTop.getGemPartUnder(where); Gem autoBurnLastGem = tableTop.getBurnManager().getAutoburnLastGem(); if (dragMode == TableTopDragMode.DISCONNECTING && connectionDragAnchorPart instanceof DisplayedPartInput && autoBurnLastGem != null && (displayedPartUnder == null || displayedPartUnder.getGem() != autoBurnLastGem) && tableTop.getBurnManager().getAutoburnLastResult() == AutoburnLogic.AutoburnAction.BURNED) { tableTop.getBurnManager().doUnburnAutomaticallyBurnedInputsUserAction(autoBurnLastGem); } // clear the status message gemCutter.getStatusMessageDisplayer().clearMessage(TableTopPanel.this); } else if (dragMode == TableTopDragMode.SELECTING) { // update the drag position, making sure that the new point is visible. Rectangle visibleRect = new Rectangle(where); updateDragPosition(where, visibleRect, wasDragging); // Determine the next drag sel mode lastSelectMode = getSelectModeForEvent(e); } } /** * Invoked when a mouse button has been released on a component. */ public void mouseReleased(MouseEvent e) { // show popup menu if appropriate if (!isUsefulDragMode(dragMode)) { maybeShowPopup(e); } // Ignore clicks unless we're editing if (gemCutter.getGUIState() != GemCutter.GUIState.EDIT) { return; } // If needed forward the mouse event to the vep. if (valueEntryPanelHit(e.getPoint())) { forwardMouseEvent (getValueEntryPanel((ValueGem)clickGem.getGem()), e); } try { // defer to the superclass method to do click/drag super.mouseReleased(e); } finally { // Clear press/drag state resetMouseStates(); // Restore focus if we took it away tableTop.restoreFocus(); // resize the tabletop if necessary, to take into account any new gem state tableTop.resizeForGems(); // if the menu was not to be shown due to a drag, we can show it again now // that the mouse has been released popupShouldShow = true; } } /** * Forward the given MouseEvent to the specified component by generating a new fake * event and posting it on the system event queue. * @param parent the parent component to forward the event to * @param e the MouseEvent to forward */ private void forwardMouseEvent(Component parent, MouseEvent e) { popupShouldShow = false; EventQueue queue = Toolkit.getDefaultToolkit().getSystemEventQueue(); Point newLocation = SwingUtilities.convertPoint((Component) e.getSource(), e.getPoint(), parent); Component newSource = parent.getComponentAt(newLocation); if (newSource == null) { // Happens if you click on the border of a component. return; } newLocation = SwingUtilities.convertPoint(parent, newLocation, newSource); queue.postEvent(new MouseEvent(newSource, e.getID(), System.currentTimeMillis(), e.getModifiers(), newLocation.x, newLocation.y, e.getClickCount(), e.isPopupTrigger(), e.getButton())); } /** * Determine if a value entry panel is under the given point. * @param point the click point * @return true if the value entry panel of a displayed value gem was hit. **/ private boolean valueEntryPanelHit(Point point) { DisplayedGem displayedGem = tableTop.getGemUnder(point); return !isUsefulDragMode(getDragMode()) && displayedGem != null && displayedGem.getGem() instanceof ValueGem && getValueEntryPanel((ValueGem)displayedGem.getGem()).getBounds().contains(point); } /** * Repaint the drag ghost (if any) */ private void repaintDrag(Graphics2D g2d) { // class DragRepainter implements Runnable{ // // Graphics2D g2d; // RedrawInfo redrawInfo; // // DragRepainter(Graphics2D g2d, RedrawInfo redrawInfo) { // this.g2d = g2d; // this.redrawInfo = redrawInfo; // save a copy of the present redraw info // } // public void run() { // // only repaint the drag connection line if there hasn't been another // // drag ghost painted in the meantime (ie. redraw info hasn't been updated..) // if (TableTopMouseHandler.this.redrawInfo == redrawInfo) { // drawDragGhost(DrawAction.REDRAW, null, g2d); // } // } // } // Say that the last drag ghost is in the undrawn state so that intervening drags will only // draw (rather than first undraw). lastDragClipArea = null; // Invoke later because we want other painting to finish before painting the drag. // Otherwise, for some reason (maybe because of double buffering?) the drag ghost disappears // when repaint returns. //SwingUtilities.invokeLater(new DragRepainter(g2d, redrawInfo)); } /** * Reset mouse states */ void resetMouseStates(){ // Exit the pressed state pressedAt = null; // Dragging is finished. Reset dragPos and dragMode dragPos = null; setDragMode(DragMode.NOTDRAGGING); // Reset the cursor setCursor(null); // invalidate autoburn attempt state tableTop.getBurnManager().invalidateAutoburnState(); } /** * Update the state of the tabletop to reflect the new mouse drag position. * This takes care of undrawing and drawing old and new drag ghosts, as well as updating dragPos, and prevDragPos and * lastScrollDistanceX/Y if necessary. * @param newDragPos Point The new mouse drag position * @param visibleRect Rectangle The rectangle which we would like to see after the position update * @param wasDragging boolean if we were dragging before (and therefore must undraw the old drag ghost) */ private void updateDragPosition(Point newDragPos, Rectangle visibleRect, boolean wasDragging) { // signal that we're painting isPainting++; // get the current graphics context Graphics2D g2d = (Graphics2D)getGraphics(); // Turn off ghosts at last position. // This is performed before scrollRect..() to avoid having a ghost in the middle of the screen. if (wasDragging){ drawDragGhost(DrawAction.UNDRAW, SelectMode.REPLACE_SELECT, g2d); } // ensure the new ghost is visible scrollRectToVisible(visibleRect); // This position is new drag position dragPos = newDragPos; // Turn on ghosts at this position g2d.setClip(getVisibleRect()); drawDragGhost(DrawAction.DRAW, SelectMode.REPLACE_SELECT, g2d); // dispose our graphics object g2d.dispose(); // we're no longer painting isPainting--; } } /** * Default constructor for this class. * @param tableTop * @param gemCutter */ TableTopPanel(TableTop tableTop, GemCutter gemCutter) { this.tableTop = tableTop; this.gemCutter = gemCutter; this.valueGemPanelMap = new WeakHashMap<ValueGem, ValueEntryPanel>(); this.gemPainter = new TableTopGemPainter(tableTop); this.isPainting = 0; // Register listeners for a number of event classes this.tableTopMouseHandler = new TableTopMouseHandler(); this.runModeMouseHandler = new RunModeMouseHandler(); addMouseListener(tableTopMouseHandler); addMouseMotionListener(tableTopMouseHandler); addComponentListener(new TableTopComponentListener()); addKeyListener(new KeyStrokeHandler()); // Register a drop target listener DropTargetListener dropTargetListener = new TableTopDragAndDropHandler(); setDropTarget(new DropTarget(this, dropTargetListener)); setToolTipText(GemCutter.getResourceString("TableTopToolTip")); setFocusCycleRoot(true); setFocusTraversalPolicy(new LayoutFocusTraversalPolicy()); } /** * Enable or disable mouse events on the TableTop, allowing or disallowing gems from being moved. * @param notRunning whether to enable or disable mouse events */ void enableMouseEvents(boolean notRunning) { if (notRunning) { removeMouseListener(runModeMouseHandler); addMouseListener(tableTopMouseHandler); addMouseMotionListener(tableTopMouseHandler); } else { addMouseListener(runModeMouseHandler); removeMouseListener(tableTopMouseHandler); removeMouseMotionListener(tableTopMouseHandler); } } /** * Handle the addition of a value gem to the tableTop. * This adds a value entry panel for use in editing the value for a value gem. * @param valueGem the value gem in question. */ void handleValueGemAdded(ValueGem valueGem) { ValueEntryPanel valueEntryPanel = getValueEntryPanel(valueGem); // Initially not visible until the user selects the gem. valueEntryPanel.setVisible(false); // Update the position of the panel. updateValueGemPanelLocation(valueGem); // Add the panel to this component.. TableTopPanel.this.add(valueEntryPanel); // Have to reset closing flag if the gem has been previously placed (eg. add gem, undo, add gem again). valueEntryPanel.setEditorIsClosing(false); // Add to the hierarchy manager. gemCutter.getValueEditorHierarchyManager().addTopValueEditor(valueEntryPanel); TableTopPanel.this.revalidate(); } /** * Handle the removal of a value gem from the tableTop. * @param valueGem the value gem which was removed. */ void handleValueGemRemoved(ValueGem valueGem) { gemCutter.getValueEditorHierarchyManager().removeValueEditor(getValueEntryPanel(valueGem), true); } /** * Handle the situation where a value gem was moved. * @param valueGem the value gem which was moved. */ void handleValueGemMoved(ValueGem valueGem) { updateValueGemPanelLocation(valueGem); } /** * Update the location of the value gem's value entry panel to match the gem location. * @param valueGem the value gem whose panel should have its location updated. */ private void updateValueGemPanelLocation(ValueGem valueGem) { Point currentLocation = tableTop.getDisplayedGem(valueGem).getLocation(); int vepX = currentLocation.x + DisplayConstants.BEVEL_WIDTH_X + 1; int vepY = currentLocation.y + DisplayConstants.BEVEL_WIDTH_Y + 1; getValueEntryPanel(valueGem).setLocation(new Point(vepX, vepY)); } /** * Get the value entry panel used to edit the value for a given value gem. * @param valueGem the value gem in question. * @return the value entry panel used to edit the value gem's value. */ ValueEntryPanel getValueEntryPanel(final ValueGem valueGem) { // Lazily create the value panel on demand. if (!valueGemPanelMap.containsKey(valueGem)) { final ValueEditorHierarchyManager valueEditorHierarchyManager = gemCutter.getValueEditorHierarchyManager(); ValueEditorManager valueEditorManager = valueEditorHierarchyManager.getValueEditorManager(); ValueNode valueNode = valueGem.getValueNode(); // Create the value entry panel. final ValueEntryPanel valueEntryPanel = (ValueEntryPanel)valueEditorManager.getValueEditorDirector().getRootValueEditor(valueEditorHierarchyManager, valueNode, null, 0, null); // Add it to the map. valueGemPanelMap.put(valueGem, valueEntryPanel); // add a listener to propagate changes in the value gem to the VEP. valueGem.addValueChangeListener(new ValueGemChangeListener() { public void valueChanged(ValueGemChangeEvent e) { ValueGem valueGem = (ValueGem)e.getSource(); valueEditorHierarchyManager.collapseHierarchy(valueEntryPanel, false); valueEntryPanel.changeOwnerValue(valueGem.getValueNode()); valueEntryPanel.setSize(valueEntryPanel.getPreferredSize()); valueEntryPanel.revalidate(); } }); // Set size of the panel. valueEntryPanel.setSize(valueEntryPanel.getPreferredSize()); // Add a listener to propagate changes in the VEP to the value gem. valueEntryPanel.addValueEditorListener(new ValueEditorAdapter() { public void valueCommitted(ValueEditorEvent evt) { ValueNode oldValue = evt.getOldValue(); ValueNode newValue = ((ValueEntryPanel)evt.getSource()).getValueNode(); if (!oldValue.sameValue(newValue)) { valueGem.changeValue(newValue); } } }); // Add a listener so that a change in the size of the VEP will trigger a change the size of the displayed gem. valueEntryPanel.addComponentListener(new ComponentAdapter() { // change the size of the displayed value gem if the VEP is resized public void componentResized(ComponentEvent e) { tableTop.getDisplayedGem(valueGem).sizeChanged(); } // Re-position displayed gem if the VEP moves // This will happen if the VEP size changes (as a result of a type change), and its size is clamped to the parent's bounds. // eg. stick a new VEP on the right edge of the TableTop, and change its type to String. public void componentMoved(ComponentEvent e) { Point vepLocation = valueEntryPanel.getLocation(); int newX = vepLocation.x - DisplayConstants.BEVEL_WIDTH_X - 1; int newY = vepLocation.y - DisplayConstants.BEVEL_WIDTH_Y - 1; Point newPoint = new Point(newX, newY); tableTop.getDisplayedGem(valueGem).setLocation(newPoint); } }); // Set the vep's context for type switching. valueEntryPanel.setContext(new ValueEditorContext() { public TypeExpr getLeastConstrainedTypeExpr() { return tableTop.getGemGraph().getLeastConstrainedValueType(valueGem, tableTop.getTypeCheckInfo()); } }); // Set it up so that VEP commits are handled as user edits. valueEntryPanel.addValueEditorListener(new ValueEditorAdapter() { public void valueCommitted(ValueEditorEvent evt) { tableTop.handleValueGemCommitted(valueGem, evt.getOldValue(), valueEntryPanel.getValueNode()); } }); } return valueGemPanelMap.get(valueGem); } /** * Sets whether or not the ValueGems should be enabled/editable. * @param enable true to enable, false to disable. */ void setValueGemsEnabled(boolean enable) { Set<Gem> gemSet = tableTop.getGemGraph().getGems(); for (final ValueGem valueGem : valueGemPanelMap.keySet()) { if (gemSet.contains(valueGem)) { ValueEntryPanel vep = getValueEntryPanel(valueGem); vep.setEditable(enable); // Make sure the value panels change colour accordingly. repaint(vep.getBounds()); } } } /** * Revalidates the value gems on the tabletop after a connection * (eg. if you connect an add gem to 2 value gems, then you specialize one, * then this method will ensure that the other's appearance gets updated as well */ void revalidateValueGemPanels() { Set<Gem> gemSet = tableTop.getGemGraph().getGems(); for (final ValueGem valueGem : valueGemPanelMap.keySet()) { if (gemSet.contains(valueGem)) { ValueEntryPanel valueEntryPanel = getValueEntryPanel(valueGem); valueEntryPanel.refreshDisplay(); // If a value gem is connected to a broken code gem, its least constrained type // will be null. In that case disable editing it. valueEntryPanel.setEditable(valueEntryPanel.getContext().getLeastConstrainedTypeExpr() != null); // Repaint the value gem ghost image to display the right value entry panel. repaint(tableTop.getDisplayedGem(valueGem).getBounds()); } } } /** * Hides the popup menu */ void hidePopup() { if (currentPopupMenu != null) { currentPopupMenu.setVisible(false); } } /** * Sets the background for the tabletop * @param image */ void setBackground(BufferedImage image) { this.backgroundImage = image; repaint(); } /** * Paint the TableTop. * @param g Graphics */ public void paintComponent(Graphics g) { if (backgroundImage != null) { // Paint a tiled image Rectangle bounds = g.getClipBounds(); // Determine which tiled instances intersect with bounds and draw them int imageWidth = backgroundImage.getWidth(); int imageHeight = backgroundImage.getHeight(); backgroundImageOriginOffsetX = backgroundImageOriginOffsetX % imageWidth; backgroundImageOriginOffsetY = backgroundImageOriginOffsetY % imageHeight; if (backgroundImageOriginOffsetX > 0) { backgroundImageOriginOffsetX -= imageWidth; } if (backgroundImageOriginOffsetY > 0) { backgroundImageOriginOffsetY-=imageHeight; } int offsetX = (bounds.x / imageWidth) * imageWidth + backgroundImageOriginOffsetX; int offsetY = (bounds.y / imageHeight) * imageHeight + backgroundImageOriginOffsetY; for (int yRegistration = offsetY; yRegistration < bounds.y + bounds.height; yRegistration += imageHeight) { for (int xRegistration = offsetX; xRegistration < bounds.x + bounds.width; xRegistration += imageWidth) { g.drawImage(backgroundImage, xRegistration, yRegistration, null); } } } else { // Just draw a plain background. g.setColor(Color.WHITE); g.fillRect(0, 0, getWidth(), getHeight()); } // Signal that we're still painting. isPainting++; // Get a Graphics2D object to paint some of the components with Graphics2D g2d = (Graphics2D) g; // Call the graph to paint itself into this graphics context // All Gems get painted, and the links established between their 'ports' (output and inputs) // Links will be colourised depending on their validity and type compatibility paintGemGraph(g2d); // paint the drag ghost tableTopMouseHandler.repaintDrag(g2d); // we're no longer painting isPainting--; } /** * Paint this GemGraph within the graphics context passed in. * @param g2d Graphics2D the graphics context */ public void paintGemGraph(Graphics2D g2d) { // Paint each Gem in the 'graph', and the edges between them // Start with the connections for (final DisplayedConnection displayedConnection : tableTop.getDisplayedConnections()) { gemPainter.paintConnection(displayedConnection, g2d); } // Now the intellicut lines tableTop.getIntellicutManager().paintIntellicutLines(g2d); // Now the Gems themselves for (final DisplayedGem displayedGem : tableTop.getDisplayedGems()) { gemPainter.paintGem(displayedGem, g2d); } } /** * {@inheritDoc} * Overriden to spot when updates are forced and cause the overview to update. */ public void repaint() { super.repaint(); // Update the overview gemCutter.getOverviewPanel().repaint(); } /** * {@inheritDoc} * Overriden to spot when updates are forced and cause the overview to update. */ public void repaint(Rectangle rect) { super.repaint(rect); // Update the overview gemCutter.getOverviewPanel().repaint(); } /** * Resets the state of the mouse handler, painting and VEP's */ void resetState() { // reset mouse states (in case there was an exception) tableTopMouseHandler.resetMouseStates(); // Remove children, in case there are any orphaned VEPs removeAll(); // Painting stuff isPainting = 0; repaint(); } /** * If the mouse event is a popup trigger and a popup menu should show, then this method * will display the appropriate popup menu for the location clicked on. * @param e the related mouse event */ void maybeShowPopup(MouseEvent e) { if (!popupShouldShow || !e.isPopupTrigger()) { return; } if (gemCutter.getGUIState() == GUIState.RUN) { currentPopupMenu = getRunModePopupMenu(); currentPopupMenu.show(this, e.getX(), e.getY()); return; } currentPopupLocation = e.getPoint(); DisplayedGem gem = tableTop.getGemUnder(currentPopupLocation); DisplayedPart part = tableTop.getGemPartUnder(currentPopupLocation); if (part instanceof DisplayedPartBody) { currentPopupMenu = getGemPopupMenu(gem, true); } else if (part instanceof DisplayedPartConnectable) { if (e.isControlDown()) { tableTop.maybeStartIntellicutMode(part); } else { currentPopupMenu = getGemPartPopupMenu((DisplayedPartConnectable) part, true); } } else if (part == null) { if (e.isControlDown()) { tableTop.getIntellicutManager().startIntellicutModeForTableTop(new Rectangle(currentPopupLocation), currentPopupLocation); } else { currentPopupMenu = getNonGemPopupMenu(true); } } else { throw new IllegalStateException("unknown part for popup menu"); } currentPopupMenu.show(this, e.getX(), e.getY()); } /** * @return the popup menu to show for the table top while the GemCutter is in run mode */ JPopupMenu getRunModePopupMenu() { JPopupMenu runModePopupMenu = new JPopupMenu(); runModePopupMenu.add(GemCutter.makeNewMenuItem(gemCutter.getResumeRunAction())); runModePopupMenu.add(GemCutter.makeNewMenuItem(gemCutter.getStopAction())); runModePopupMenu.add(GemCutter.makeNewMenuItem(gemCutter.getResetAction())); return runModePopupMenu; } /** * @return the JPopupMenu to show when the user right clicks on the table top, not on a gem or part * @param forTableTop whether the popup menu is for the table top and should include table top specific items * * <BR><BR>todoFW: find a better way to share popup menu code with the TableTopExplorer */ JPopupMenu getNonGemPopupMenu(boolean forTableTop) { JPopupMenu nonGemPopupMenu = new JPopupMenu(); final JMenuItem addReflectorMenuItem = GemCutter.makeNewMenuItem(getAddReflectorAction()); final JMenu addOtherReflectorMenu = GemCutter.makeNewMenu(GemCutter.getResourceString("PopItem_AddOtherReflector")); if (forTableTop) { nonGemPopupMenu.add(GemCutter.makeNewMenuItem(getAddGemAction())); } nonGemPopupMenu.add(GemCutter.makeNewMenuItem(getAddValueGemAction())); nonGemPopupMenu.add(GemCutter.makeNewMenuItem(getAddCodeGemAction())); nonGemPopupMenu.add(GemCutter.makeNewMenuItem(getAddCollectorAction())); nonGemPopupMenu.add(addReflectorMenuItem); nonGemPopupMenu.add(addOtherReflectorMenu); nonGemPopupMenu.add(GemCutter.makeNewMenuItem(getAddRecordCreationGemAction())); nonGemPopupMenu.add(GemCutter.makeNewMenuItem(getAddRecordFieldSelectionGemAction())); nonGemPopupMenu.addSeparator(); nonGemPopupMenu.add(getCopySpecialMenu()); nonGemPopupMenu.add(GemCutter.makeNewMenuItem(getPasteGemsAction())); if (forTableTop) { nonGemPopupMenu.addSeparator(); nonGemPopupMenu.add(GemCutter.makeNewMenuItem(getTidyTableTopAction())); nonGemPopupMenu.add(GemCutter.makeNewMenuItem(getFitTableTopAction())); } // This listener enables/disables the add emitter item and resets the popup location nonGemPopupMenu.addPopupMenuListener(new PopupMenuListener() { public void popupMenuWillBecomeVisible(PopupMenuEvent e) { // Get the names of all the collectors. List<String> sortedReflectorNames = new ArrayList<String>(); Map<String, CollectorGem> nameToCollectorMap = new HashMap<String, CollectorGem>(); for (final Gem gem : gemCutter.getTableTop().getGemGraph().getCollectors()) { CollectorGem collectorGem = (CollectorGem)gem; nameToCollectorMap.put(collectorGem.getUnqualifiedName(), collectorGem); sortedReflectorNames.add(collectorGem.getUnqualifiedName()); } Collections.sort(sortedReflectorNames, String.CASE_INSENSITIVE_ORDER); // Update the text of the add reflector menu item CollectorGem reflectorCollector = gemCutter.getCollectorForAddingReflector(); addReflectorMenuItem.setEnabled(reflectorCollector != null); if (reflectorCollector != null) { addReflectorMenuItem.setText(GemCutter.getResourceString("PopItem_AddReflectorFor") + reflectorCollector.getUnqualifiedName()); } else { addReflectorMenuItem.setText(GemCutter.getResourceString("PopItem_AddReflector")); } addOtherReflectorMenu.removeAll(); addOtherReflectorMenu.setEnabled(sortedReflectorNames.size() > 0); for (final String reflectorName : sortedReflectorNames) { reflectorCollector = nameToCollectorMap.get(reflectorName); addOtherReflectorMenu.add(GemCutter.makeNewMenuItem(new AddReflectorAction(reflectorCollector))); } } public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { } public void popupMenuCanceled(PopupMenuEvent e) { } }); return nonGemPopupMenu; } /** * @return the "Copy Special" menu, identical to that of the gem cutter. */ private JMenu getCopySpecialMenu() { JMenu originalMenu = gemCutter.getCopySpecialMenu(); JMenu copySpecialMenu = GemCutter.makeNewMenu(originalMenu.getText()); int items = originalMenu.getItemCount(); for (int i = 0; i < items; i++) { copySpecialMenu.add(GemCutter.makeNewMenuItem(originalMenu.getItem(i).getAction())); } return copySpecialMenu; } /** * @param part the part that was clicked on * @param forTableTop whether the menu is for the table top and should include table top specific items * @return the JPopupMenu to show for the given part */ JPopupMenu getGemPartPopupMenu(DisplayedPartConnectable part, boolean forTableTop) { JPopupMenu partPopupMenu = new JPopupMenu(); JSeparator separator = new JSeparator(); if (forTableTop) { partPopupMenu.add(GemCutter.makeNewMenuItem(getIntellicutAction(part))); partPopupMenu.add(separator); } if (part instanceof DisplayedPartInput) { DisplayedPartInput inputPart = (DisplayedPartInput) part; if (!inputPart.isConnected()) { partPopupMenu.add(GemCutter.makeNewMenuItem(getConnectValueGemAction(inputPart))); if (!(inputPart.getPartInput().getGem() instanceof CollectorGem)) { if (inputPart.getPartInput().isBurnt()) { partPopupMenu.add(GemCutter.makeNewMenuItem(getUnburnAction(inputPart))); } else { partPopupMenu.add(GemCutter.makeNewMenuItem(getBurnAction(inputPart))); } } } // Create popup menu item for retargeting inputs. final JMenu retargetInputMenu = GemCutter.makeNewMenu(GemCutter.getResourceString("PopItem_RetargetInput")); partPopupMenu.add(retargetInputMenu); // Get the targetable collectors, enable the menu item if not empty and if the input is not connected. List<CollectorGem> targetableCollectorList = getTargetableCollectors(inputPart.getPartInput()); retargetInputMenu.setEnabled(!targetableCollectorList.isEmpty() && !inputPart.isConnected()); CollectorGem currentArgTarget = GemGraph.getInputArgumentTarget(inputPart.getPartInput()); // Create submenu items for retargeting to an enclosing collector. for (final CollectorGem targetableCollector : targetableCollectorList) { // Make the new menu item. JMenuItem newMenuItem = GemCutter.makeNewMenuItem(getRetargetInputArgumentAction(inputPart.getPartInput(), targetableCollector)); // Enable if its not already targeted to this collector. newMenuItem.setEnabled(targetableCollector != currentArgTarget); // Add the menu item. retargetInputMenu.add(newMenuItem); } } else if (part instanceof DisplayedPartOutput) { DisplayedPartOutput outputPart = (DisplayedPartOutput) part; if (!outputPart.isConnected()) { partPopupMenu.add(GemCutter.makeNewMenuItem(getConnectCollectorAction(outputPart))); } } if (part.isConnected()) { partPopupMenu.add(GemCutter.makeNewMenuItem(getDisconnectAction(part))); partPopupMenu.add(GemCutter.makeNewMenuItem(getSplitConnectionAction(part))); } // If there is nothing after the separator then remove it. if (partPopupMenu.getComponentCount() == 2) { partPopupMenu.remove(separator); } return partPopupMenu; } /** * @param partInput an input * @return Map from Collector gem which can be targeted to the collector gem to which the collector * at the root of the input's gem subtree must be retargeted if the input is retargeted to the first collector gem. */ private List<CollectorGem> getTargetableCollectors(PartInput partInput) { // Get the collector gem at the root of the gem tree to which the input is connected. CollectorGem inputGemRoot = partInput.getGem().getRootCollectorGem(); // Can't retarget if the root is not a collector gem. if (inputGemRoot == null) { return Collections.emptyList(); } // The input can be targeted to the root gem, or any of its enclosing collectors. // So, the targetable collectors are the root, any collectors targetable by the root, and any collectors enclosing those. // We have to ignore the input under consideration, otherwise the call to getTargetableCollectors() will see that the argument // is targeting whatever it's targeting, and use that as a constraint. Set<CollectorGem> targetableCollectorSet = new HashSet<CollectorGem>(); for (final CollectorGem targetableCollector : getTargetableCollectors(inputGemRoot, null, partInput)) { targetableCollectorSet.addAll(GemGraph.obtainEnclosingCollectors(targetableCollector)); } // The input can always be targeted at the root gem... targetableCollectorSet.add(inputGemRoot); // Convert to an array, then sort. // First, the target gem. Then, all other collectors, ordered alphabetically by name. CollectorGem[] targetableCollectorArray = targetableCollectorSet.toArray(new CollectorGem[targetableCollectorSet.size()]); final CollectorGem gemGraphTarget = tableTop.getGemGraph().getTargetCollector(); Arrays.sort(targetableCollectorArray, new Comparator<CollectorGem>() { public int compare(CollectorGem o1, CollectorGem o2) { if (o1 == gemGraphTarget) { return -1; } if (o2 == gemGraphTarget) { return 1; } return o1.getUnqualifiedName().compareTo(o2.getUnqualifiedName()); } }); // Return as a list. return Arrays.asList(targetableCollectorArray); } /** * @param collectorGem a collector gem * @param collectorsToCheck if non-null, only the collectors in this set will be checked if they are targetable. * If null, all collectors in the gem graph will be checked. * @param inputToIgnore the input to exclude from consideration, if any. Null if none. * @return the collector gems to which the given collector gem may validly be targeted. */ private List<CollectorGem> getTargetableCollectors(CollectorGem collectorGem, Set<CollectorGem> collectorsToCheck, PartInput inputToIgnore) { // Targetable collectors must satisfy a set of conditions: // Collectors: // No circular collector dependencies. // If there are any reflectors attached to the same subtree as reflectors for the retargeting collector gem, // the collector at the root of the tree must be able to access the definitions for those reflectors, as well // as the definition of the retargeting collector. If there is no collector at the root of the tree, // it must be possible to create a collector which can see all definitions. // Arguments: // Arguments on dependee trees which target collectors at outer scopes must still be able to target those collectors after retarget. // Can't retarget the target gem. CollectorGem targetCollector = collectorGem.getTargetCollectorGem(); if (targetCollector == null) { return Collections.emptyList(); } // // Get root collectors for emitters for any collectors enclosed by the retargeting collector gem, // where the root collector encloses and is not equal to the collector being retargeted. // ie. the collectors at the roots of the subtrees to which the emitters are connected, // if the root collector encloses and is not equal to the retargeting collector. // Set<CollectorGem> collectorSet = tableTop.getGemGraph().getCollectors(); Set<CollectorGem> enclosedCollectorGemReflectorStrictlyEnclosingRootCollectorSet = new HashSet<CollectorGem>(); for (final Gem gem : collectorSet) { CollectorGem nextCollector = (CollectorGem)gem; if (collectorGem.enclosesCollector(nextCollector)) { for (final ReflectorGem reflectorGem : nextCollector.getReflectors()) { CollectorGem rootCollectorGem = reflectorGem.getRootCollectorGem(); if (rootCollectorGem != null && rootCollectorGem != collectorGem && rootCollectorGem.enclosesCollector(collectorGem)) { enclosedCollectorGemReflectorStrictlyEnclosingRootCollectorSet.add(rootCollectorGem); } } } } // // Get the innermost enclosing collector which has targeting arguments from inputs which exist on subtrees whose root // collectors are enclosed by collectorGem. // This will be used to check the condition that arguments on dependee trees which target collectors at enclosing scopes // must still be able to target those collectors. // CollectorGem innermostEnclosingCollectorWithEnclosedTargetingArguments = null; enclosingCollectorLoop: for (CollectorGem enclosingCollector = targetCollector; enclosingCollector != null; enclosingCollector = enclosingCollector.getTargetCollectorGem()) { for (final PartInput targetArgument : enclosingCollector.getTargetArguments()) { CollectorGem rootCollectorGem = targetArgument.getGem().getRootCollectorGem(); // Check for the input to ignore.. if (targetArgument == inputToIgnore) { continue; } if (rootCollectorGem == null) { // Shouldn't be able to retarget an input on a subtree not rooted in a collector. GemCutter.CLIENT_LOGGER.log(Level.WARNING, "Targeting input has no root collector: " + targetArgument); continue; } if (collectorGem.enclosesCollector(rootCollectorGem)) { innermostEnclosingCollectorWithEnclosedTargetingArguments = enclosingCollector; break enclosingCollectorLoop; } } } // For each subtree, not rooted by a collector, which has a reflector whose // collector is enclosed by collectorGem, the set of targets of (non-collectorGem-enclosed) collectors for the other // reflectors on that subtree. // // If two collectors have the same target, then they are siblings. // // This will be used to check the condition that: // For each subtree not rooted in a collector, we must be able to create a collector for the root which can // see all the collectors for the reflectors in that subtree. Set<Set<CollectorGem>> nonCollectorGemEnclosedSameSubtreeReflectorCollectorParentSets = new HashSet<Set<CollectorGem>>(); { // This could be faster... Set<Gem> uncollectedRoots = tableTop.getGemGraph().getRoots(); uncollectedRoots.removeAll(collectorSet); for (final Gem uncollectedRoot : uncollectedRoots) { // Get reflectors which exist on the same subtree as the collector's reflector. Set<ReflectorGem> subTreeReflectors = UnsafeCast.<Set<ReflectorGem>>unsafeCast(GemGraph.obtainSubTreeGems(uncollectedRoot, ReflectorGem.class)); // Calculate the set of collectors for reflectors in the subtree, where the collectors are not // enclosed by collector gem. Set<CollectorGem> nonEnclosedReflectorCollectorParentSet = new HashSet<CollectorGem>(); for (final ReflectorGem gem : subTreeReflectors) { CollectorGem subtreeReflectorCollector = gem.getCollector(); if (collectorGem.enclosesCollector(subtreeReflectorCollector)) { // Ensure the set is added to the set of collector sets only if there is a reflector collector which // is enclosed by collector gem. nonCollectorGemEnclosedSameSubtreeReflectorCollectorParentSets.add(nonEnclosedReflectorCollectorParentSet); } else { // Add to the set of non-enclosed reflector collectors. CollectorGem subtreeReflectorCollectorTarget = subtreeReflectorCollector.getTargetCollectorGem(); if (subtreeReflectorCollectorTarget != null) { nonEnclosedReflectorCollectorParentSet.add(subtreeReflectorCollectorTarget); } } } } // Note that we may have added an empty set.. } // // Iterate over the collectors in the set to check, checking each to see if it can be targeted. // if (collectorsToCheck == null) { collectorsToCheck = tableTop.getGemGraph().getCollectors(); } Set<CollectorGem> targetableCollectors = new HashSet<CollectorGem>(); targetableGemCandidateLoop : for (final CollectorGem targetableCollectorCandidate : collectorsToCheck) { // Guard against circular collector dependencies: disallow retargeting to an enclosed collector (or itself). // Check that collectorGem is not an ancestor of targetableGemCandidate. if (collectorGem.enclosesCollector(targetableCollectorCandidate)) { continue targetableGemCandidateLoop; } // Arguments on dependee trees which target collectors at outer scopes (wrt collectorGem) must still be able to target those collectors. // In practice, this means we only have to check that (the innermost enclosing collector which has targeting arguments which exist on // subtrees whose root collectors are enclosed by collectorGem) will still enclose collectorGem. if (innermostEnclosingCollectorWithEnclosedTargetingArguments != null && !innermostEnclosingCollectorWithEnclosedTargetingArguments.enclosesCollector(targetableCollectorCandidate)) { continue targetableGemCandidateLoop; } // Reflectors for collectors enclosed by the retargeting collector gem must be visible to their collector root. // So: for a tree with a reflector for such a collector, // if the collector root is also enclosed, relative visibility will not change by retargeting. // if the collector root is not also enclosed, it must have targetableCollectorCandidate as an ancestor or sibling. for (final CollectorGem reflectorRootCollector : enclosedCollectorGemReflectorStrictlyEnclosingRootCollectorSet) { boolean reflectorRootCollectorSeesCandidate = false; // Check if they have the same parent. if (reflectorRootCollector.getTargetCollectorGem() == targetCollector) { reflectorRootCollectorSeesCandidate = true; } // Check if the candidate is an ancestor. reflectorRootCollectorSeesCandidate |= targetableCollectorCandidate.enclosesCollector(reflectorRootCollector); if (!reflectorRootCollectorSeesCandidate) { // The targetable collector candidate isn't visible to the root of this tree. continue targetableGemCandidateLoop; } } // If there are any reflectors attached to the same subtree as reflectors for the retargeting collector gem, // the collector at the root of the tree must be able to access the definitions for those reflectors, as well // as the definition of the retargeting collector. // For subtrees rooted in collectors, the previous check is sufficient. // For subtrees not rooted in collectors, we check that there is a collector which can see both definitions. ie. // If there is a reflector in the subtree whose collector is enclosed by collectorGem (or is collectorGem), // any reflector in the subtree whose collector is not enclosed by collectorGem must be a // sibling of collectorGem or its ancestors (before and) after retargeting. // If there is no reflector in the subtree whose collector is enclosed by collectorGem, there is nothing // to worry about, since this means relative collector visibilities won't change as a result of the retargeting. // // For one collector to be visible to another, it must be the same collector, a sibling, an ancestor, // a sibling of an ancestor, or a child // This can be simplified to: its target must enclose the other collector. // So, in order to be able to define a collector which can see all the reflector definitions in an unrooted subtree, // the parents of the reflectors' collectors must form an ancestor (enclosement) chain. // Since we know that the collectors enclosed by the collector to retarget are not changed with respect to each other, // we only have to check that the collector to which to retarget is part of the ancestor chain. for (final Set<CollectorGem> nonCollectorGemEnclosedSameSubtreeReflectorCollectorParentSet : nonCollectorGemEnclosedSameSubtreeReflectorCollectorParentSets) { CollectorGem candidateTarget = targetableCollectorCandidate.getTargetCollectorGem(); Set<CollectorGem> setToCheck = new HashSet<CollectorGem>(nonCollectorGemEnclosedSameSubtreeReflectorCollectorParentSet); if (candidateTarget != null) { setToCheck.add(candidateTarget); } if (!GemGraph.formsAncestorChain(setToCheck)) { continue targetableGemCandidateLoop; } } // If we're here, the targetable gem candidate satisfies all the above constraints. targetableCollectors.add(targetableCollectorCandidate); } // // Now put the collectors in order. // The target gem always comes first. Then all other collectors, sorted by name. // // If the gem graph target is targetable, note this and remove. CollectorGem gemGraphTarget = tableTop.getGemGraph().getTargetCollector(); boolean hasGemGraphTarget = targetableCollectors.remove(gemGraphTarget); // Order all other collectors by name. CollectorGem[] collectorsArray = targetableCollectors.toArray(new CollectorGem[targetableCollectors.size()]); Arrays.sort(collectorsArray, new Comparator<CollectorGem>() { public int compare(CollectorGem o1, CollectorGem o2) { return o1.getUnqualifiedName().compareTo(o2.getUnqualifiedName()); } }); // Compose the list. List<CollectorGem> targetableCollectorsList = new ArrayList<CollectorGem>(collectorsArray.length + (hasGemGraphTarget ? 1 : 0)); if (hasGemGraphTarget) { targetableCollectorsList.add(gemGraphTarget); } targetableCollectorsList.addAll(Arrays.asList(collectorsArray)); return targetableCollectorsList; } /** * @param displayedGem the gem to show the popup menu for * @param forTableTop whether the menu is for the table top and should include table top specific items * @return the JPopupMenu to show for the given gem */ JPopupMenu getGemPopupMenu(DisplayedGem displayedGem, boolean forTableTop) { JPopupMenu gemPopupMenu = new JPopupMenu(); gemPopupMenu.add(GemCutter.makeNewMenuItem(getRunGemAction(displayedGem))); gemPopupMenu.add(GemCutter.makeNewMenuItem(getDeleteGemsAction())); gemPopupMenu.addSeparator(); gemPopupMenu.add(GemCutter.makeNewMenuItem(gemCutter.getCutAction())); gemPopupMenu.add(GemCutter.makeNewMenuItem(gemCutter.getCopyAction())); gemPopupMenu.add(getCopySpecialMenu()); if (forTableTop) { gemPopupMenu.add(GemCutter.makeNewMenuItem(getSelectSubTreeAction())); gemPopupMenu.addSeparator(); gemPopupMenu.add(GemCutter.makeNewMenuItem(getTidySelectionAction(displayedGem))); gemPopupMenu.add(GemCutter.makeNewMenuItem(getTidySubTreeAction(displayedGem))); } Gem gem = displayedGem.getGem(); if (gem instanceof FunctionalAgentGem) { gemPopupMenu.addSeparator(); gemPopupMenu.add(GemCutter.makeNewMenuItem(getViewPropertiesAction((FunctionalAgentGem)gem))); } else if (gem instanceof CodeGem) { gemPopupMenu.addSeparator(); gemPopupMenu.add(GemCutter.makeNewMenuItem(getRenameGemAction(gem))); gemPopupMenu.add(GemCutter.makeNewMenuItem(getEditCodeGemAction((CodeGem)gem))); } else if (gem instanceof RecordFieldSelectionGem) { gemPopupMenu.addSeparator(); gemPopupMenu.add(GemCutter.makeNewMenuItem(getChangeRecordSelectionFieldAction(gem))); } else if (gem instanceof RecordCreationGem) { gemPopupMenu.addSeparator(); // If a recordCreationGem is connected @ output, disable all menu items boolean shouldEnable = !gem.getOutputPart().isConnected(); // ADD field menu item JMenuItem addRecordFieldMenu = GemCutter.makeNewMenuItem(getAddNewRecordFieldAction(gem)); addRecordFieldMenu.setEnabled(shouldEnable); gemPopupMenu.add(addRecordFieldMenu); // get all the fields List<String> allFields = ((RecordCreationGem)gem).getCopyOfFieldsList(); // DELETE field submenu final JMenu deleteRecordFieldMenu = GemCutter.makeNewMenu(GemCutter.getResourceString("PopItem_DeleteRecordField")); gemPopupMenu.add(deleteRecordFieldMenu); List<String> deletableFields = ((RecordCreationGem)gem).getDeletableFields(tableTop); deleteRecordFieldMenu.setEnabled(shouldEnable); // RENAME field submenu final JMenu renameRecordFieldMenu = GemCutter.makeNewMenu(GemCutter.getResourceString("PopItem_RenameRecordField")); gemPopupMenu.add(renameRecordFieldMenu); List<String> renamableFields = ((RecordCreationGem)gem).getRenamableFields(tableTop); renameRecordFieldMenu.setEnabled(shouldEnable); // Add the submenu items for (final String field : allFields) { // DELETABLE fields for submenu items, enable only if the field is deletable JMenuItem deleteMenuItem = GemCutter.makeNewMenuItem(getDeleteRecordFieldAction((RecordCreationGem)gem, field)); deleteMenuItem.setEnabled(deletableFields.contains(field)); deleteRecordFieldMenu.add(deleteMenuItem); // RENAMABLE fields for submenu items enable only if the field is renamable JMenuItem renameMenuItem = GemCutter.makeNewMenuItem(getRenameRecordFieldAction((RecordCreationGem)gem, field)); renameMenuItem.setEnabled(renamableFields.contains(field)); renameRecordFieldMenu.add(renameMenuItem); } // If disabled, set tool tips to indicate why if(!shouldEnable) { addRecordFieldMenu.setToolTipText(GemCutter.getResourceString("CannotModifyFields_tooltip", "add")); deleteRecordFieldMenu.setToolTipText(GemCutter.getResourceString("CannotModifyFields_tooltip", "delete")); renameRecordFieldMenu.setToolTipText(GemCutter.getResourceString("CannotModifyFields_tooltip", "rename")); } } else if (gem instanceof CollectorGem) { CollectorGem collectorGem = (CollectorGem)gem; CollectorGem targetCollector = collectorGem.getTargetCollectorGem(); gemPopupMenu.addSeparator(); // Create popup menu item for retargeting collectors. final JMenu retargetCollectorMenu = GemCutter.makeNewMenu(GemCutter.getResourceString("PopItem_RetargetCollector")); gemPopupMenu.add(retargetCollectorMenu); // Get the targetable collectors, enable the menu item if not empty. List<CollectorGem> targetableCollectors = getTargetableCollectors(collectorGem, null, null); retargetCollectorMenu.setEnabled(!targetableCollectors.isEmpty()); // Create submenu items for retargeting to a collector. for (final CollectorGem targetableCollector : targetableCollectors) { // Make the new menu item. JMenuItem newMenuItem = GemCutter.makeNewMenuItem(getRetargetCollectorAction(collectorGem, targetableCollector)); // Enable if its not already targeted to this collector. newMenuItem.setEnabled(targetableCollector != targetCollector); // Add the menu item. retargetCollectorMenu.add(newMenuItem); } // Create other menu items. gemPopupMenu.add(GemCutter.makeNewMenuItem(getRenameGemAction(gem))); gemPopupMenu.add(GemCutter.makeNewMenuItem(getAddReflectorAction(collectorGem))); gemPopupMenu.add(GemCutter.makeNewMenuItem(getSaveGemAction(collectorGem))); gemPopupMenu.add(GemCutter.makeNewMenuItem(getEditPropertiesAction(collectorGem))); } return gemPopupMenu; } /** * @param part the part the action is for * @return the action to disconnect a connected gem part */ Action getDisconnectAction(final DisplayedPartConnectable part) { Action disconnectAction = new AbstractAction (GemCutter.getResourceString("PopItem_Disconnect")) { private static final long serialVersionUID = 2309882084879614674L; public void actionPerformed(ActionEvent evt) { Connection connection = part.getPartConnectable().getConnection(); // Start the update now so that any auto-unburning is part of the same edit. tableTop.getUndoableEditSupport().beginUpdate(); tableTop.handleDisconnectGesture(connection); if (part instanceof DisplayedPartOutput) { tableTop.getBurnManager().doUnburnAutomaticallyBurnedInputsUserAction(part.getGem()); } else { tableTop.getBurnManager().doUnburnAutomaticallyBurnedInputsUserAction(connection.getSource().getGem()); } tableTop.getUndoableEditSupport().setEditName(GemCutter.getResourceString("UndoText_DisconnectGems")); tableTop.getUndoableEditSupport().endUpdate(); } }; disconnectAction.putValue(Action.MNEMONIC_KEY, Integer.valueOf(GemCutterActionKeys.MNEMONIC_DISCONNECT)); return disconnectAction; } /** * @param part a displayed output or input part with the connection to be split * @return the action to split a connection into a collector / emitter pair */ Action getSplitConnectionAction(final DisplayedPartConnectable part) { Action splitConnectionAction = new AbstractAction(GemCutter.getResourceString("PopItem_SplitConnection")) { private static final long serialVersionUID = 3698559866440806289L; public void actionPerformed(ActionEvent evt) { Connection connection = part.getPartConnectable().getConnection(); tableTop.doSplitConnectionUserAction(connection); } }; splitConnectionAction.putValue(Action.MNEMONIC_KEY, Integer.valueOf(GemCutterActionKeys.MNEMONIC_SPLITCONNECTION)); return splitConnectionAction; } /** * @param partInput the part to connect a value gem to * @return a new Action for connection a ValueGem to the given part */ Action getConnectValueGemAction(final DisplayedPartInput partInput) { Action connectAction = new AbstractAction(GemCutter.getResourceString("PopItem_ConnectValueGem"), new ImageIcon(getClass().getResource("/Resources/constant.gif"))) { private static final long serialVersionUID = 6931521030141991559L; public void actionPerformed(ActionEvent evt) { DisplayedGem dGem = tableTop.createDisplayedValueGem(new Point()); ExtendedUndoableEditSupport editSupport = tableTop.getUndoableEditSupport(); editSupport.beginUpdate(); tableTop.doAddGemUserAction(dGem, partInput.getConnectionPoint()); tidyAsConnected(dGem.getDisplayedOutputPart(), partInput, false); tableTop.handleConnectGemPartsGesture(dGem.getGem().getOutputPart(), partInput.getPartInput()); editSupport.setEditName(GemCutterMessages.getString("UndoText_Add", dGem.getDisplayText())); editSupport.endUpdate(); } }; // Check if a value gem can be connected here DisplayedGem displayedValueGem = tableTop.createDisplayedValueGem(new Point()); boolean connectable = GemGraph.isDefaultableValueGemSource(displayedValueGem.getDisplayedOutputPart().getPartOutput(), partInput.getPartInput(), gemCutter.getConnectionContext()); connectAction.setEnabled(connectable); return connectAction; } /** * @param inputPart the input part whose argument is being retargeted. * @param targetableCollector the collector to which the argument is being retargeted. * @return a new Action to retarget the input's argument to the given collector. */ Action getRetargetInputArgumentAction(final Gem.PartInput inputPart, final CollectorGem targetableCollector) { Action retargetInputArgumentAction = new AbstractAction(targetableCollector.getUnqualifiedName()) { private static final long serialVersionUID = 9119710886417993470L; public void actionPerformed(ActionEvent evt) { tableTop.handleRetargetInputArgumentGesture(inputPart, targetableCollector); } }; retargetInputArgumentAction.putValue(Action.MNEMONIC_KEY, Integer.valueOf(GemCutterActionKeys.MNEMONIC_RETARGET_INPUT)); return retargetInputArgumentAction; } /** * @param collectorGemToTarget the input part whose argument is being retargeted. * @param targetableCollector the collector to which the argument is being retargeted. * @return a new Action to retarget the input's argument to the given collector. */ Action getRetargetCollectorAction(final CollectorGem collectorGemToTarget, final CollectorGem targetableCollector) { Action retargetInputArgumentAction = new AbstractAction(targetableCollector.getUnqualifiedName()) { private static final long serialVersionUID = -1932705179986107636L; public void actionPerformed(ActionEvent evt) { tableTop.handleRetargetCollectorGesture(collectorGemToTarget, targetableCollector); } }; retargetInputArgumentAction.putValue(Action.MNEMONIC_KEY, Integer.valueOf(GemCutterActionKeys.MNEMONIC_RETARGET_COLLECTOR)); return retargetInputArgumentAction; } /** * @param displayedPartOutput the part to connect a collector to * @return a new action for connecting a collector to the given part */ Action getConnectCollectorAction(final DisplayedPartOutput displayedPartOutput) { Action connectAction = new AbstractAction(GemCutter.getResourceString("PopItem_ConnectCollector"), new ImageIcon(getClass().getResource("/Resources/collector.gif"))) { private static final long serialVersionUID = -7979201959532611095L; public void actionPerformed(ActionEvent evt) { // Calculate the target for the collector gem to create. // The friendliest thing to do is to minimize the depth of the collector, so this will be the // parent of the deepest collector among reflectors in the subtree. int targetingCollectorMaxDepth = 1; // depth of collectors targeting collectorToTarget. CollectorGem collectorToTarget = tableTop.getTargetCollector(); Set<ReflectorGem> subtreeReflectors = UnsafeCast.<Set<ReflectorGem>>unsafeCast(GemGraph.obtainSubTreeGems(displayedPartOutput.getGem(), ReflectorGem.class)); for (final ReflectorGem subtreeReflector : subtreeReflectors) { CollectorGem subtreeReflectorCollector = subtreeReflector.getCollector(); int subtreeReflectorCollectorDepth = GemGraph.getCollectorDepth(subtreeReflectorCollector); if (subtreeReflectorCollectorDepth > targetingCollectorMaxDepth) { targetingCollectorMaxDepth = subtreeReflectorCollectorDepth; collectorToTarget = subtreeReflectorCollector.getTargetCollectorGem(); } } // Now actually create, add, and connect the collector. DisplayedGem dGem = tableTop.createDisplayedCollectorGem(new Point(0, 0), collectorToTarget); ExtendedUndoableEditSupport editSupport = tableTop.getUndoableEditSupport(); editSupport.beginUpdate(); tableTop.doAddGemUserAction(dGem, displayedPartOutput.getConnectionPoint()); tidyAsConnected(displayedPartOutput, dGem.getDisplayedInputPart(0), true); tableTop.handleConnectGemPartsGesture(displayedPartOutput.getPartOutput(), dGem.getGem().getInputPart(0)); editSupport.setEditName(GemCutterMessages.getString("UndoText_Add", dGem.getDisplayText())); editSupport.endUpdate(); tableTop.displayLetNameEditor((CollectorGem)dGem.getGem()); } }; return connectAction; } /** * Tidy up two gems as though they were connected. * @param displayedPartOutput the output of one of the gems. * @param displayedPartInput the input of the other gem. * @param anchorOutput whether the output should be the anchor. * If false, the input will be the anchor for tidying (ie. will not move in the tidy operation). */ private void tidyAsConnected(DisplayedPartOutput displayedPartOutput, DisplayedPartInput displayedPartInput, boolean anchorOutput) { // Save old connection info. DisplayedConnection oldOutputConnection = displayedPartOutput.getDisplayedConnection(); DisplayedConnection oldInputConnection = displayedPartInput.getDisplayedConnection(); // Create a temporary connection between the parts. DisplayedConnection tempConnection = new DisplayedConnection(displayedPartOutput, displayedPartInput); displayedPartInput.bindDisplayedConnection(tempConnection); displayedPartOutput.bindDisplayedConnection(tempConnection); // tidy the temporary connection.. DisplayedGem[] displayedGems = {displayedPartInput.getDisplayedGem(), displayedPartOutput.getDisplayedGem()}; Graph.LayoutArranger layoutArranger = new Graph.LayoutArranger(displayedGems); DisplayedPartConnectable anchorPart = anchorOutput ? (DisplayedPartConnectable)displayedPartOutput : displayedPartInput; tableTop.doTidyUserAction(layoutArranger, anchorPart.getDisplayedGem()); // Restore the old connection info. displayedPartOutput.bindDisplayedConnection(oldOutputConnection); displayedPartInput.bindDisplayedConnection(oldInputConnection); } /** * Returns the action to burn a gem part. * @param part the gem part to burn * @return Action */ Action getBurnAction(final DisplayedPartInput part) { Action burnAction = new AbstractAction (GemCutter.getResourceString("PopItem_Burn"), new ImageIcon(getClass().getResource("/Resources/burnMenuIcon.gif"))) { private static final long serialVersionUID = -4017522251797979086L; public void actionPerformed(ActionEvent evt) { tableTop.getBurnManager().handleBurnInputGesture(part); } }; burnAction.putValue(Action.MNEMONIC_KEY, Integer.valueOf(GemCutterActionKeys.MNEMONIC_BURN)); return burnAction; } /** * Returns the action to unburn a gem part. * @param part the gem part to burn * @return Action */ Action getUnburnAction(final DisplayedPartInput part) { Action unburnAction = new AbstractAction (GemCutter.getResourceString("PopItem_Unburn"), new ImageIcon(getClass().getResource("/Resources/unburnMenuIcon.gif"))) { private static final long serialVersionUID = -1510333509310634733L; public void actionPerformed(ActionEvent evt) { tableTop.getBurnManager().handleBurnInputGesture(part); } }; unburnAction.putValue(Action.MNEMONIC_KEY, Integer.valueOf(GemCutterActionKeys.MNEMONIC_UNBURN)); return unburnAction; } /** * Returns the action that adds a value gem. * @return Action */ private Action getAddValueGemAction() { Action addValueGemAction = new AbstractAction (GemCutter.getResourceString("PopItem_AddValueGem"), new ImageIcon(getClass().getResource("/Resources/constant.gif"))) { private static final long serialVersionUID = 6775526511235880852L; public void actionPerformed(ActionEvent evt) { TableTop tableTop = gemCutter.getTableTop(); DisplayedGem dGem = tableTop.createDisplayedValueGem(currentPopupLocation); ExtendedUndoableEditSupport editSupport = tableTop.getUndoableEditSupport(); editSupport.beginUpdate(); tableTop.doAddGemUserAction(dGem, currentPopupLocation); editSupport.setEditName(GemCutterMessages.getString("UndoText_Add", dGem.getDisplayText())); editSupport.endUpdate(); } }; addValueGemAction.putValue(Action.MNEMONIC_KEY, Integer.valueOf(GemCutterActionKeys.MNEMONIC_ADD_VALUE_GEM)); return addValueGemAction; } /** * Returns the action that adds a new gem (aka Intellicut). * @return Action */ private Action getAddGemAction() { Action addGemAction = new AbstractAction (GemCutter.getResourceString("PopItem_AddGem"), new ImageIcon(getClass().getResource("/Resources/addNewGem.gif"))) { private static final long serialVersionUID = 2943954914796413469L; public void actionPerformed(ActionEvent evt) { gemCutter.getIntellicutManager().startIntellicutModeForTableTop(new Rectangle(currentPopupLocation), currentPopupLocation); } }; addGemAction.putValue(Action.MNEMONIC_KEY, Integer.valueOf(GemCutterActionKeys.MNEMONIC_INTELLICUT)); addGemAction.putValue(Action.ACCELERATOR_KEY, GemCutterActionKeys.ACCELERATOR_INTELLICUT); return addGemAction; } /** * Returns the action that adds a code gem. * @return Action */ private Action getAddCodeGemAction() { Action addCodeGemAction = new AbstractAction (GemCutter.getResourceString("PopItem_AddCodeGem"), new ImageIcon(getClass().getResource("/Resources/code.gif"))) { private static final long serialVersionUID = 9129526613956299918L; public void actionPerformed(ActionEvent evt) { TableTop tableTop = gemCutter.getTableTop(); DisplayedGem dGem = tableTop.createDisplayedCodeGem(currentPopupLocation); ExtendedUndoableEditSupport editSupport = tableTop.getUndoableEditSupport(); editSupport.beginUpdate(); tableTop.doAddGemUserAction(dGem, currentPopupLocation); editSupport.setEditName(GemCutterMessages.getString("UndoText_Add", dGem.getDisplayText())); editSupport.endUpdate(); } }; addCodeGemAction.putValue(Action.MNEMONIC_KEY, Integer.valueOf(GemCutterActionKeys.MNEMONIC_ADD_CODE_GEM)); return addCodeGemAction; } /** * Returns the action that adds a new collector. * @return Action */ private Action getAddCollectorAction() { Action addCollectorAction = new AbstractAction (GemCutter.getResourceString("PopItem_AddCollector"), new ImageIcon(getClass().getResource("/Resources/collector.gif"))) { private static final long serialVersionUID = 6261574181526925026L; public void actionPerformed(ActionEvent evt) { TableTop tableTop = gemCutter.getTableTop(); DisplayedGem dGem = tableTop.createDisplayedCollectorGem(currentPopupLocation, tableTop.getTargetCollector()); ExtendedUndoableEditSupport editSupport = tableTop.getUndoableEditSupport(); editSupport.beginUpdate(); tableTop.doAddGemUserAction(dGem, currentPopupLocation); editSupport.setEditName(GemCutterMessages.getString("UndoText_Add", dGem.getDisplayText())); editSupport.endUpdate(); tableTop.displayLetNameEditor((CollectorGem)dGem.getGem()); } }; addCollectorAction.putValue(Action.MNEMONIC_KEY, Integer.valueOf(GemCutterActionKeys.MNEMONIC_ADD_COLLECTOR_GEM)); return addCollectorAction; } /** * Returns the action that adds a RecordFieldSelection gem. * @return Action */ private Action getAddRecordFieldSelectionGemAction() { Action AddRecordFieldSelectionGemAction = new AbstractAction (GemCutter.getResourceString("PopItem_AddRecordFieldSelectionGem"), new ImageIcon(getClass().getResource("/Resources/recordFieldSelectionGem.gif"))) { private static final long serialVersionUID = -642626553800845519L; public void actionPerformed(ActionEvent evt) { TableTop tableTop = gemCutter.getTableTop(); DisplayedGem dGem = tableTop.createDisplayedRecordFieldSelectionGem(currentPopupLocation); ExtendedUndoableEditSupport editSupport = tableTop.getUndoableEditSupport(); editSupport.beginUpdate(); tableTop.doAddGemUserAction(dGem, currentPopupLocation); editSupport.setEditName(GemCutterMessages.getString("UndoText_Add", dGem.getDisplayText())); editSupport.endUpdate(); } }; AddRecordFieldSelectionGemAction.putValue(Action.MNEMONIC_KEY, Integer.valueOf(GemCutterActionKeys.MNEMONIC_ADD_RECORD_FIELD_SELECTION_GEM)); return AddRecordFieldSelectionGemAction; } /** * Returns the action that adds a new RecordCreationGem * @return Action */ private Action getAddRecordCreationGemAction() { Action AddRecordCreationGemAction = new AbstractAction (GemCutter.getResourceString("PopItem_AddRecordCreationGem"), new ImageIcon(getClass().getResource("/Resources/recordCreationGem.gif"))) { private static final long serialVersionUID = -1527976012193744771L; public void actionPerformed(ActionEvent evt) { TableTop tableTop = gemCutter.getTableTop(); DisplayedGem dGem = tableTop.createDisplayedRecordCreationGem(currentPopupLocation); ExtendedUndoableEditSupport editSupport = tableTop.getUndoableEditSupport(); editSupport.beginUpdate(); tableTop.doAddGemUserAction(dGem, currentPopupLocation); editSupport.setEditName(GemCutterMessages.getString("UndoText_Add", dGem.getDisplayText())); editSupport.endUpdate(); } }; AddRecordCreationGemAction.putValue(Action.MNEMONIC_KEY, Integer.valueOf(GemCutterActionKeys.MNEMONIC_ADD_RECORD_CREATION_GEM)); return AddRecordCreationGemAction; } /** * Returns the action that adds a new reflector. * @return Action */ private Action getAddReflectorAction() { Action addReflectorAction = new AbstractAction(GemCutter.getResourceString("PopItem_AddReflector"), new ImageIcon(getClass().getResource("/Resources/reflector.gif"))) { private static final long serialVersionUID = -1527976012193744771L; public void actionPerformed(ActionEvent evt) { TableTop tableTop = gemCutter.getTableTop(); DisplayedGem dGem = tableTop.createDisplayedReflectorGem(currentPopupLocation, gemCutter.getCollectorForAddingReflector()); ExtendedUndoableEditSupport editSupport = tableTop.getUndoableEditSupport(); editSupport.beginUpdate(); tableTop.doAddGemUserAction(dGem, currentPopupLocation); editSupport.setEditName(GemCutterMessages.getString("UndoText_Add", dGem.getDisplayText())); editSupport.endUpdate(); } }; addReflectorAction.putValue(Action.MNEMONIC_KEY, Integer.valueOf(GemCutterActionKeys.MNEMONIC_ADD_REFLECTOR_GEM)); return addReflectorAction; } /** * Returns the action that tidies the table top. * @return Action */ private Action getTidyTableTopAction() { Action tidyTableTopAction = new AbstractAction (GemCutter.getResourceString("ArrangeGraph")) { private static final long serialVersionUID = -5117623881659632367L; public void actionPerformed(ActionEvent evt) { tableTop.doTidyTableTopAction(); repaint (); } }; tidyTableTopAction.putValue(Action.MNEMONIC_KEY, Integer.valueOf(GemCutterActionKeys.MNEMONIC_ARRANGE_GRAPH)); tidyTableTopAction.putValue(Action.ACCELERATOR_KEY, GemCutterActionKeys.ACCELERATOR_ARRANGE_GRAPH); return tidyTableTopAction; } /** * Returns the action that fits the table top. * @return Action */ private Action getFitTableTopAction() { Action fitTableTopAction = new AbstractAction (GemCutter.getResourceString("FitTableTop")) { private static final long serialVersionUID = 8996494637943723837L; public void actionPerformed(ActionEvent evt) { tableTop.doShrinkTableTopUserAction(); repaint(); } }; fitTableTopAction.putValue(Action.ACCELERATOR_KEY, GemCutterActionKeys.ACCELERATOR_FIT_TABLETOP); fitTableTopAction.putValue(Action.MNEMONIC_KEY, Integer.valueOf(GemCutterActionKeys.MNEMONIC_FIT_TABLETOP)); return fitTableTopAction; } /** * Returns the Intellicut popup menu action. * @return Action */ private Action getIntellicutAction(final DisplayedPartConnectable part) { Action intellicutAction = new AbstractAction(GemCutter.getResourceString("PopItem_Intellicut"), new ImageIcon(getClass().getResource("/Resources/intellicut.gif"))) { private static final long serialVersionUID = 3589489782252599609L; public void actionPerformed(ActionEvent e) { if (part != null) { tableTop.maybeStartIntellicutMode(part); } else { tableTop.getIntellicutManager().startIntellicutModeForTableTop(new Rectangle(currentPopupLocation), currentPopupLocation); } } }; boolean enabled = true; if (part != null) { boolean burnt = false; if (part instanceof DisplayedPartInput) { burnt = ((DisplayedPartInput) part).getPartInput().isBurnt(); } enabled = part.getPartConnectable().getType() != null && !part.isConnected() && !burnt; } intellicutAction.setEnabled(enabled); intellicutAction.putValue(Action.ACCELERATOR_KEY, GemCutterActionKeys.ACCELERATOR_INTELLICUT); intellicutAction.putValue(Action.MNEMONIC_KEY, Integer.valueOf(GemCutterActionKeys.MNEMONIC_INTELLICUT)); return intellicutAction; } /** * @return the paste gems action */ private Action getPasteGemsAction() { Action action = new AbstractAction(GemCutter.getResourceString("Paste"), new ImageIcon(GemCutter.class.getResource("/Resources/paste.gif"))) { private static final long serialVersionUID = -4338435545421989629L; public void actionPerformed(ActionEvent e) { pasteLocation = currentPopupLocation; gemCutter.pasteFromClipboard(); pasteLocation = null; } }; action.putValue(Action.MNEMONIC_KEY, Integer.valueOf(GemCutterActionKeys.MNEMONIC_PASTE)); action.setEnabled(gemCutter.getPasteAction().isEnabled()); return action; } /** * @param gem the gem the action is for * @return the run gem action */ private Action getRunGemAction(final DisplayedGem gem) { Action action = new AbstractAction(GemCutter.getResourceString("PopItem_RunGem"), new ImageIcon(GemCutter.class.getResource("/Resources/play.gif"))) { private static final long serialVersionUID = 8017728923330766355L; public void actionPerformed(ActionEvent e) { gemCutter.runTarget(gem); } }; action.putValue(Action.MNEMONIC_KEY, Integer.valueOf(GemCutterActionKeys.MNEMONIC_RUN)); action.setEnabled(gem.getGem().isRunnable()); return action; } /** * @return the delete gems action */ private Action getDeleteGemsAction() { Action action = new AbstractAction(GemCutter.getResourceString("PopItem_DeleteGem")) { private static final long serialVersionUID = 6190720497415948604L; public void actionPerformed(ActionEvent e) { Set<Gem> selectedGems = new HashSet<Gem>(Arrays.asList(tableTop.getSelectedGems())); tableTop.handleDeleteGemsGesture(selectedGems); } }; action.putValue(Action.MNEMONIC_KEY, Integer.valueOf(GemCutterActionKeys.MNEMONIC_DELETE)); Gem[] selectedGems = tableTop.getSelectedGems(); if (selectedGems.length > 1) { action.putValue(Action.NAME, GemCutter.getResourceString("PopItem_DeleteGems")); } else { action.putValue(Action.NAME, GemCutter.getResourceString("PopItem_DeleteGem")); boolean selectedTargetOnly = selectedGems.length != 0 && selectedGems[0] == tableTop.getTargetCollector(); action.setEnabled(!selectedTargetOnly); } return action; } /** * @return the select gem subtree action */ private Action getSelectSubTreeAction() { Action action = new AbstractAction(GemCutter.getResourceString("PopItem_SelectSubTree")) { private static final long serialVersionUID = 5395741198879471469L; public void actionPerformed(ActionEvent e) { tableTop.doSelectSubtreeUser(tableTop.getSelectedGems()); } }; return action; } /** * @param gem the gem the action is for * @return the tidy selection action */ private Action getTidySelectionAction(final DisplayedGem gem) { Action action = new AbstractAction(GemCutter.getResourceString("PopItem_TidySelection")) { private static final long serialVersionUID = -8127271684299167786L; public void actionPerformed(ActionEvent e) { tableTop.doTidyUserAction(new Graph.LayoutArranger(tableTop.getSelectedDisplayedGems()), gem); } }; action.setEnabled(tableTop.getSelectedDisplayedGems().length > 1); return action; } /** * @param gem the gem the action is for * @return the tidy sub trees action */ private Action getTidySubTreeAction(final DisplayedGem gem) { Action action = new AbstractAction(GemCutter.getResourceString("PopItem_TidySubtrees")) { private static final long serialVersionUID = 7701234830348136274L; public void actionPerformed(ActionEvent e) { Gem[] gems = tableTop.getSelectedGems(); List<DisplayedGem> displayedGems = tableTop.doSelectSubtreeUser(gems); DisplayedGem[] validNodes = new DisplayedGem[displayedGems.size()]; tableTop.doTidyUserAction(new Graph.LayoutArranger(displayedGems.toArray(validNodes)), gem); } }; // Only enable the action if an input is actually connected. boolean enabled = false; Gem.PartInput[] inputs = gem.getGem().getInputParts(); for (final PartInput input : inputs) { if (input.isConnected()) { enabled = true; break; } } action.setEnabled(enabled); return action; } /** * @param collectorGem the gem the action is for * @return the save gem action */ private Action getSaveGemAction(final CollectorGem collectorGem) { Action action = new AbstractAction(GemCutter.getResourceString("PopItem_SaveGem"), new ImageIcon(GemCutter.class.getResource("/Resources/save.gif"))) { private static final long serialVersionUID = -5967804767355281406L; public void actionPerformed(ActionEvent e) { gemCutter.saveGem(); } }; action.setEnabled(collectorGem.isRunnable()); return action; } /** * @param gem the gem the action is for * @return the rename gem action */ private Action getRenameGemAction(final Gem gem) { Action action = new AbstractAction(GemCutter.getResourceString("PopItem_RenameGem")) { private static final long serialVersionUID = -1805921578643387361L; public void actionPerformed(ActionEvent e) { if (gem instanceof CodeGem) { tableTop.displayCodeNameEditor((CodeGem)gem); } else if (gem instanceof CollectorGem) { tableTop.displayLetNameEditor((CollectorGem)gem); } else { throw new IllegalArgumentException("can only rename code or collector gems"); } } }; return action; } /** * Gets the action for changing the record field to be extracted. * @param gem the gem the action is for * @return the change extracted field action */ private Action getChangeRecordSelectionFieldAction(final Gem gem) { Action action = new AbstractAction(GemCutter.getResourceString("PopItem_ChangeSelectedField")) { private static final long serialVersionUID = 2446643701742147405L; public void actionPerformed(ActionEvent e) { if (gem instanceof RecordFieldSelectionGem) { tableTop.displayRecordFieldSelectionEditor((RecordFieldSelectionGem)gem); } else { throw new IllegalArgumentException("can only change extracted field on RecordFieldSelection gems"); } } }; if (gem instanceof RecordFieldSelectionGem) { //prevent editing of the gem when the tree is broken as it could lead to a more inconsistent state if ( GemGraph.isAncestorOfBrokenGemForest(gem.getRootGem())) { action.setEnabled(false); } } return action; } /** * Gets the action for renaming a record field * @param gem the gem the action is for * @return the renaming field action */ private Action getRenameRecordFieldAction(final Gem gem, final String fieldToRename) { Action action = new AbstractAction(fieldToRename.toString()) { private static final long serialVersionUID = 806578875701283074L; public void actionPerformed(ActionEvent e) { if (gem instanceof RecordCreationGem) { tableTop.displayFieldRenameEditor((RecordCreationGem)gem, FieldName.make(fieldToRename)); } else { throw new IllegalArgumentException("Can only rename field name on a RecordCreationGem"); } } }; if (gem instanceof RecordCreationGem) { //prevent editing of the gem when the output is connected if (gem.getOutputPart().isConnected()) { action.setEnabled(false); } } return action; } /** * Gets the action for adding a new record field * @param gem the gem the action is for * @return the adding new record field action */ private Action getAddNewRecordFieldAction(final Gem gem) { Action action = new AbstractAction(GemCutter.getResourceString("PopItem_AddNewRecordField")) { private static final long serialVersionUID = -1676013303676395830L; public void actionPerformed(ActionEvent e) { if (gem instanceof RecordCreationGem) { tableTop.doAddRecordFieldUserAction((RecordCreationGem)gem); } else { throw new IllegalArgumentException("can only add new field on RecordCreation gems"); } } }; return action; } /** * Gets the action for deleting an existing record field * @param gem the gem the action is for * @param fieldToDelete the field to be deleted * @return the deleting existing field action */ private Action getDeleteRecordFieldAction(final Gem gem, final String fieldToDelete) { Action action = new AbstractAction(fieldToDelete.toString()) { private static final long serialVersionUID = -959693970346246044L; public void actionPerformed(ActionEvent e) { if (gem instanceof RecordCreationGem) { tableTop.doDeleteRecordFieldUserAction((RecordCreationGem)gem, fieldToDelete); } } }; return action; } /** * @param collectorGem the collector gem the action is for * @return the add reflector gem action */ private Action getAddReflectorAction(final CollectorGem collectorGem) { Action action = new AbstractAction(GemCutter.getResourceString("PopItem_AddReflector"), new ImageIcon(GemCutter.class.getResource("/Resources/reflector.gif"))) { private static final long serialVersionUID = 6212576237546675112L; public void actionPerformed(ActionEvent e) { DisplayedGem eGem = tableTop.createDisplayedReflectorGem(new Point(10, 10), collectorGem); gemCutter.setAddingDisplayedGem(eGem); gemCutter.enterGUIState(GemCutter.GUIState.ADD_GEM); } }; return action; } /** * @param collectorGem the collector gem the action is for * @return the edit properties action */ private Action getEditPropertiesAction(final CollectorGem collectorGem) { Action action = new AbstractAction(GemCutter.getResourceString("PopItem_EditGemProperties"), new ImageIcon(GemCutter.class.getResource("/Resources/nav_edit.gif"))) { private static final long serialVersionUID = -7376813996995048934L; public void actionPerformed(ActionEvent e) { gemCutter.getNavigatorOwner().editMetadata(collectorGem); } }; action.setEnabled(collectorGem.isConnected()); return action; } /** * @param faGem the functional agent gem the action is for * @return the view properties action */ private Action getViewPropertiesAction(final FunctionalAgentGem faGem) { Action action = new AbstractAction(GemCutter.getResourceString("PopItem_ViewGemProperties")) { private static final long serialVersionUID = 945262478582661871L; public void actionPerformed(ActionEvent e) { GemEntity gemEntity = faGem.getGemEntity(); gemCutter.getNavigatorOwner().displayMetadata(gemEntity, true); } }; return action; } /** * @param codeGem the code gem the action is for * @return the edit code gem action */ private Action getEditCodeGemAction(final CodeGem codeGem) { Action action = new AbstractAction(GemCutter.getResourceString("PopItem_OpenCodeEditor"), new ImageIcon(GemCutter.class.getResource("/Resources/selectedCodeEditorOpen.gif"))) { private static final long serialVersionUID = 1866715362157650129L; public void actionPerformed(ActionEvent e) { tableTop.showCodeGemEditor(codeGem, !tableTop.isCodeEditorVisible(codeGem)); } }; if (tableTop.isCodeEditorVisible(codeGem)) { action.putValue(Action.NAME, GemCutter.getResourceString("PopItem_CloseCodeEditor")); } else { action.putValue(Action.NAME, GemCutter.getResourceString("PopItem_OpenCodeEditor")); } return action; } /** * This will display the intellicut menu by the currently selected gem * or if no gem is selected in the top-left of the table top. This is called * by the GemCutter if the user presses the intellicut keyboard shortcut. */ void displayIntellicut() { DisplayedGem target = tableTop.getFocusedDisplayedGem(); Rectangle menuLocation = null; if (target == null) { Rectangle visible = getVisibleRect(); menuLocation = new Rectangle(visible.x + 10, visible.y + 10); } else { menuLocation = target.getBounds(); } tableTop.getIntellicutManager().startIntellicutModeForTableTop(menuLocation); } /** * Get the tooltip text when over the TableTop. * @return the tooltip text to display * @param mouseEvent where we are now */ public String getToolTipText(MouseEvent mouseEvent) { // The toolTip to return (default is no tool tip). String toolTip = null; // Where are we now? Point where = mouseEvent.getPoint(); // Check for tooltip hotspots DisplayedPart displayedPart = tableTop.getGemPartUnder(where); // The naming policy to use for tooltips ScopedEntityNamingPolicy namingPolicy; ModuleTypeInfo currentModuleTypeInfo = tableTop.getCurrentModuleTypeInfo(); if (currentModuleTypeInfo == null) { namingPolicy = ScopedEntityNamingPolicy.FULLY_QUALIFIED; } else { namingPolicy = new ScopedEntityNamingPolicy.UnqualifiedUnlessAmbiguous(currentModuleTypeInfo); } // If it is an input or an output, then we deal with it here. if (displayedPart != null && displayedPart instanceof DisplayedPartConnectable) { PartConnectable part = ((DisplayedPartConnectable)displayedPart).getPartConnectable(); toolTip = ToolTipHelpers.getPartToolTip(part, tableTop.getGemGraph(), namingPolicy, this); } else if (displayedPart instanceof DisplayedPartBody) { Gem gem = ((DisplayedPartBody) displayedPart).getGem(); if (gem instanceof FunctionalAgentGem) { toolTip = ToolTipHelpers.getFunctionalAgentToolTip((FunctionalAgentGem) gem, this, GemCutter.getLocaleFromPreferences()); } else if (gem instanceof CollectorGem) { CollectorGem collectorGem = (CollectorGem)gem; CollectorGem targetGem = collectorGem.getTargetCollectorGem(); String targetGemString = (targetGem == null) ? GemCutterMessages.getString("NullCollectorTarget") : targetGem.getUnqualifiedName(); StringBuilder text = new StringBuilder("<html>"); text.append(GemCutterMessages.getString("CollectorTargetToolTip", targetGemString)); text.append("<br>"); text.append(GemCutterMessages.getString("ResultTypeToolTip", tableTop.getGemGraph().getTypeString(gem.getResultType(), namingPolicy))); text.append("</html>"); toolTip = text.toString(); } else if (gem instanceof ValueGem) { ValueEntryPanel vep = getValueEntryPanel((ValueGem)gem); Point vepPoint = SwingUtilities.convertPoint(this, where, vep); toolTip = vep.getToolTipText(vepPoint); } } return toolTip; } /** * returns the gempainter used to paint the gem graph * @return TableTopGemPainter */ TableTopGemPainter getGemPainter() { return gemPainter; } /** * Returns the preferredSize of a JViewport whose view is this Scrollable * @return Dimension */ public Dimension getPreferredScrollableViewportSize() { return getPreferredSize(); } /** * Returns the "block" increment for scrolling in the specified direction. */ public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) { return (orientation == SwingConstants.VERTICAL) ? visibleRect.height : visibleRect.width; } /** * Return true if a viewport should always force the height of this Scrollable to match the height of the viewport. * Currently, always returns false unless this scrollable's height is less than the Viewport's height. */ public boolean getScrollableTracksViewportHeight() { if (getParent() instanceof JViewport) { return (((JViewport)getParent()).getHeight() > getPreferredSize().height); } return false; } /** * Return true if a viewport should always force the width of this Scrollable to match the width of the viewport. * Currently, always returns false unless this scrollable's width is less than the Viewport's width. */ public boolean getScrollableTracksViewportWidth() { if (getParent() instanceof JViewport) { return (((JViewport)getParent()).getWidth() > getPreferredSize().width); } return false; } /** * Returns the "unit" increment for scrolling in the specified direction. */ public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) { // TEMP: Gotta figure out an optimal unit increment. return 30; } /** * @return the location at which new gems should be pasted if paste was invoked form a popup menu * This will be null if paste was not invoked from a popup menu and gems should be pasted at a default * location. */ Point getPasteLocation(){ return pasteLocation; } /** * Return the rectangle formed by the two specified Gems. * The rectangle is defined by the centre points of the two Gems. * @param fromGem DisplayedGem - the Gem that is the origin of the rectangle * @param toGem DisplayedGem - the Gem that closes the rectangle * @return Rectangle */ private static Rectangle2D getRectangleForDisplayedGems(DisplayedGem fromGem, DisplayedGem toGem) { Point2D fromGemCentrePoint = fromGem.getCenterPoint(); Rectangle2D rect = new Rectangle2D.Double(fromGemCentrePoint.getX(), fromGemCentrePoint.getY(), 0, 0); rect.add(toGem.getCenterPoint()); return rect; } /** * returns whether tabletop is in photolook. * @return boolean */ boolean isPhotoLook() { return gemCutter.isPhotoLook(); } }