Java tutorial
/* * Copyright (c) 2008, SQL Power Group Inc. * * This file is part of DQguru * * DQguru is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * DQguru is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package ca.sqlpower.matchmaker.swingui.munge; import java.awt.AlphaComposite; import java.awt.Color; import java.awt.Component; import java.awt.Container; import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Image; import java.awt.Insets; import java.awt.Point; import java.awt.event.ActionEvent; import java.awt.event.ComponentEvent; import java.awt.event.ComponentListener; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionAdapter; import java.beans.PropertyChangeEvent; import java.util.HashSet; import java.util.List; import java.util.Set; import javax.swing.AbstractAction; import javax.swing.BorderFactory; import javax.swing.Icon; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JFormattedTextField; import javax.swing.JLabel; import javax.swing.JMenuItem; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JScrollPane; import javax.swing.JTextArea; import javax.swing.JTextField; import javax.swing.JToolBar; import javax.swing.Timer; import javax.swing.ToolTipManager; import javax.swing.tree.TreePath; import org.apache.log4j.Logger; import ca.sqlpower.matchmaker.MatchMakerObject; import ca.sqlpower.matchmaker.MatchMakerSession; import ca.sqlpower.matchmaker.munge.InputDescriptor; import ca.sqlpower.matchmaker.munge.MungeProcess; import ca.sqlpower.matchmaker.munge.MungeStep; import ca.sqlpower.matchmaker.munge.MungeStepOutput; import ca.sqlpower.matchmaker.swingui.MatchMakerSwingSession; import ca.sqlpower.matchmaker.swingui.MatchMakerTreeModel; import ca.sqlpower.object.AbstractSPListener; import ca.sqlpower.object.SPChildEvent; import ca.sqlpower.object.SPObject; import ca.sqlpower.validation.swingui.FormValidationHandler; import com.jgoodies.forms.builder.DefaultFormBuilder; import com.jgoodies.forms.layout.FormLayout; public abstract class AbstractMungeComponent extends JPanel { private static final Logger logger = org.apache.log4j.Logger.getLogger(AbstractMungeComponent.class); protected JPanel content; private final MungeStep step; /** * The point to auto scroll to */ private Point autoScrollPoint; /** * The timer to handle when to autoscroll because always calling it is way too fast. */ private Timer autoScrollTimer; /** * How often to call auto scroll */ public static final int AUTO_SCROLL_TIME = 55; /** * The background colour to use when this component is not selected. */ private Color normalBackground = new Color(0xee, 0xee, 0xee); /** * The shadow colour to use when this component is not selected. */ private Color normalShadow = new Color(0xdd, 0xdd, 0xdd); /** * The background colour to use when this component is selected. */ private Color selectedBackground = new Color(0xc5, 0xdd, 0xf7); /** * The shadow colour to use when this component is selected. */ private Color selectedShadow = new Color(0xb1, 0xc7, 0xdf); /** * The set of component types that should not have their opaqueness fiddled * with after the UI has been built. See {@link #deOpaquify(Container)} * for details. */ protected Set<Class<? extends JComponent>> opaqueComponents = new HashSet<Class<? extends JComponent>>(); private static final Image MMM_TOP = new ImageIcon( AbstractMungeComponent.class.getClassLoader().getResource("icons/mmm_top.png")).getImage(); private static final Image MMM_BOT = new ImageIcon( AbstractMungeComponent.class.getClassLoader().getResource("icons/mmm_bot.png")).getImage(); private static final ImageIcon EXPOSE_OFF = new ImageIcon( AbstractMungeComponent.class.getClassLoader().getResource("icons/expose_off.png")); private static final ImageIcon EXPOSE_ON = new ImageIcon( AbstractMungeComponent.class.getClassLoader().getResource("icons/expose_on.png")); private static final ImageIcon PLUS_OFF = new ImageIcon( AbstractMungeComponent.class.getClassLoader().getResource("icons/plus_off.png")); private static final ImageIcon PLUS_ON = new ImageIcon( AbstractMungeComponent.class.getClassLoader().getResource("icons/plus_on.png")); private static final int PLUG_OFFSET = 2; public static final int CLICK_TOLERANCE = 15; /** * The timer interval for the drop down. The length of time to wait before moving * the nib down again. */ public static final int DROP_TIMER_INTERVAL = 100; /** * The amount to drop the nib after each interval */ public static final int DROP_AMOUNT = 5; private final MatchMakerSwingSession session; private MungeComponentKeyListener mungeComKeyListener; private FormValidationHandler handler; private final JButton hideShow; /** * The icon of the nib that is dropping down. */ private Icon dropNib; /** * The current offset of the dropping nib, the number of pixles above where it will end up. */ private int dropNibOffSet; /** * The index of the currently dropping nib */ private int dropNibIndex; /** * The timer running the dropdown. Only one will exist at a time. */ private Timer dropNibTimer; /** * The index of an input where the user's mouse is near. The paintComponent method * will paint a semi transparent plug handle here if. Do nothing if it is -1 */ private int ghostIndex; /** * A panel that goes on top and labels the inputs */ private final JPanel inputNames; /** * Classes extending this component should set this value to true through * the setter if they want the names of the inputs to be visible when the * component is expanded. If this value is false then the input names will * not be visible. The default is false. */ private boolean showInputNames; /** * Holds the lables with the names of the inputs. */ private CoolJLabel[] inputLabels; /** * A panel that goes on bottom and labels the outputs */ private final JPanel outputNames; /** * Classes extending this component should set this value to true through * the setter if they want the names of the outputs to be visible when the * component is expanded. If this value is false then the output names will * not be visible. The default is false. */ private boolean showOutputNames; /** * Holds the lables with the names of the outputs. */ private CoolJLabel[] outputLabels; /** * Creates a AbstractMungeComponent for the given step that will be in the munge pen. * Sets the background and border colours to given colours. * * @param step The step connected to the UI * @param border The colour for the border around the rectangle * @param bg The background colour to the rectangle */ public AbstractMungeComponent(MungeStep step, FormValidationHandler handler, MatchMakerSession s) { if (step == null) throw new NullPointerException("Null step"); try { step.refresh(logger); } catch (Exception e1) { throw new RuntimeException("Failed to refresh step " + step.getName(), e1); } this.session = (MatchMakerSwingSession) s; this.handler = handler; this.step = step; setVisible(true); setBackground(normalBackground); autoScrollTimer = new Timer(AUTO_SCROLL_TIME, new AbstractAction() { public void actionPerformed(ActionEvent e) { logger.debug("TIMER GO!!!"); getPen().autoscroll(autoScrollPoint, AbstractMungeComponent.this); } }); autoScrollTimer.stop(); autoScrollTimer.setRepeats(true); dropNibIndex = -1; ToolTipManager toolTipManager = ToolTipManager.sharedInstance(); toolTipManager.setInitialDelay(0); bustGhost(); mungeComKeyListener = new MungeComponentKeyListener(); addKeyListener(mungeComKeyListener); step.addSPListener(new StepEventHandler()); setName(step.getName()); resizeBorders(); setOpaque(false); setFocusable(true); Dimension ps = getPreferredSize(); setBounds(0, 0, ps.width, ps.height); DefaultFormBuilder builder = new DefaultFormBuilder(new FormLayout("pref", "pref, pref, fill:pref, pref"), this); setBackground(Color.GREEN); inputNames = new JPanel(); inputNames.setOpaque(false); inputNames.setLayout(new FlowLayout()); inputLabels = new CoolJLabel[step.getMSOInputs().size()]; for (int x = 0; x < inputLabels.length; x++) { InputDescriptor id = step.getInputDescriptor(x); inputLabels[x] = new CoolJLabel(id.getName(), id.getType()); inputLabels[x].collapse(); inputNames.add(inputLabels[x]); inputLabels[x].setOpaque(false); } setInputShowNames(false); builder.append(inputNames); builder.nextLine(); if (isExpanded() && inputNames.isVisible()) { inputNames.setVisible(true); } else { inputNames.setVisible(false); } revalidate(); outputNames = new JPanel(); outputNames.setOpaque(false); outputNames.setLayout(new FlowLayout()); outputLabels = new CoolJLabel[step.getChildren(MungeStepOutput.class).size()]; for (int x = 0; x < outputLabels.length; x++) { MungeStepOutput out = step.getChildren(MungeStepOutput.class).get(x); outputLabels[x] = new CoolJLabel(out.getName(), out.getType()); outputLabels[x].collapse(); outputNames.add(outputLabels[x]); outputLabels[x].setOpaque(false); } setOutputShowNames(false); JPanel tmp = new JPanel(new FlowLayout()); tmp.setBackground(Color.BLUE); tmp.add(new JLabel(step.getName())); hideShow = new JButton(new HideShowAction()); hideShow.setIcon(EXPOSE_OFF); setupOpaqueComponents(); content = buildUI(); //returning null will prevent the +/- button form showing up if (content != null) { deOpaquify(content); JToolBar tb = new JToolBar(); hideShow.setBorder(null); hideShow.addMouseListener(new MouseAdapter() { public void mouseEntered(MouseEvent e) { hideShow.setIcon(EXPOSE_ON); hideShow.setBorder(null); } public void mouseExited(MouseEvent e) { hideShow.setIcon(EXPOSE_OFF); hideShow.setBorder(null); } }); tb.setBorder(null); tb.add(hideShow); tb.setFloatable(false); tmp.add(tb); } builder.append(tmp); builder.nextLine(); if (content != null) { builder.append(content); } builder.nextLine(); builder.append(outputNames); addMouseListener(new MungeComponentMouseListener()); addMouseMotionListener(new MungeComponentMouseMoveListener()); addComponentListener(new ComponentListener() { public void componentHidden(ComponentEvent e) { } public void componentMoved(ComponentEvent e) { getParent().repaint(); } public void componentResized(ComponentEvent e) { getParent().repaint(); } public void componentShown(ComponentEvent e) { } }); addFocusListener(new FocusListener() { public void focusGained(FocusEvent e) { //if it is being deleted if (getParent() != null) { getParent().repaint(); MatchMakerTreeModel treeModel = (MatchMakerTreeModel) session.getTree().getModel(); TreePath menuPath = treeModel.getPathForNode(getStep()); session.getTree().setSelectionPath(menuPath); } } public void focusLost(FocusEvent e) { if (getParent() != null) { getParent().repaint(); } if (autoScrollTimer.isRunning()) { autoScrollTimer.stop(); } } }); setOpaque(false); tmp.setOpaque(false); // Note, this does not take care of the content panel; only the basic // stuff added here in the constructor (most importantly, the +/- button) deOpaquify(this); deOpaquify(inputNames); setDefaults(); } /** * This will re-calculate the top and bottom borders depending on the * children of the step. */ private void resizeBorders() { int borderTop; if (!getStep().canAddInput() && getStep().getMSOInputs().size() == 0) { borderTop = MMM_TOP.getHeight(null); } else { borderTop = ConnectorIcon.getHandleInstance(Object.class).getIconHeight() + PLUG_OFFSET; } int borderBottom; if (getStep().getChildren(MungeStepOutput.class).size() == 0) { borderBottom = 0; } else { borderBottom = ConnectorIcon.getNibInstance(Object.class).getIconHeight(); } setBorder(BorderFactory.createEmptyBorder(borderTop, 1, borderBottom, MMM_TOP.getWidth(null))); } /** * Returns the unqualified name of the given class (no package name prefix). */ private String shortClassName(Class type) { if (type == null) return null; return type.getName().substring(type.getName().lastIndexOf('.') + 1); } /** * a label that can expand or collapse */ private class CoolJLabel extends JLabel { public CoolJLabel(String id, Class type) { super(id); setToolTipText(id + " (" + shortClassName(type) + ")"); addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { changeState(); } @Override public void mouseEntered(MouseEvent e) { if (getText().equals("")) { setIcon(PLUS_ON); } } @Override public void mouseExited(MouseEvent e) { if (getText().equals("")) { setIcon(PLUS_OFF); } } }); setBorder(BorderFactory.createEtchedBorder()); } public void collapse() { setText(""); setIcon(PLUS_OFF); } public void expand() { setIcon(null); setText(getToolTipText()); } public void changeState() { if (getText().equals("")) { expand(); } else { collapse(); } validate(); } } private JLabel getCoolJLabel(String id, Class type) { JLabel lab = new JLabel(id); lab.setToolTipText(id + " (" + shortClassName(type) + ")"); return lab; } /** * Adds the default set of component types that should not be made non-opaque * to the {@link #opaqueComponents} set. If your munge component uses other component * types that should also not be made non-opaque, override this method and add * your types to that set. Don't forget to call super.setupOpaqueComponents() * if you want to have the default set too. */ protected void setupOpaqueComponents() { opaqueComponents.add(JTextField.class); opaqueComponents.add(JTextArea.class); opaqueComponents.add(JFormattedTextField.class); } /** * Resets the location and expandedness to the values in the step. */ private void setDefaults() { setExpanded(step.isExpanded()); if (step.getPosition() != null) { setLocation(step.getPosition()); } } /** * Walks the tree of components rooted at c, setting all of the components * that can and should be flagged as non-opaque as such. * <p> * Components that should not be made non-opaque (such as JTextField, because * that looks silly) will be left alone. The exact set of component types * that will be left with their existing opaqueness setting is controlled by * the contents of the {@link #opaqueComponents} set. If your munge component * implementation has a preferences component that's getting made non-opaque, * just add its class to that set in your {@link #setupOpaqueComponents()} method. * * @param c */ private void deOpaquify(Container c) { for (int i = 0; i < c.getComponentCount(); i++) { Component cc = c.getComponent(i); if (cc instanceof JComponent && !opaqueComponents.contains(cc.getClass())) { ((JComponent) cc).setOpaque(false); } if (cc instanceof Container) { deOpaquify((Container) cc); } } } /** * This returns the user interface for your munge step options. This method * must be implemented individualy for each munge step. If your munge step * doesn't have any options, you should return null from this method, and * there will be no +/- button on your component. * <p> * Important note about opaqueness: The munge component's background colour * will change when it is selected. For this effect to work properly, most * of your components will have to be non-opaque. This is a pain for you * to remember and actually do for every munge component, so the AbstractMungeComponent * will walk through the panel returned by this method and set most of the contained * components to non-opaque. Some components, though, look bad when they're not * opaque, so those are left alone. If you're adding a custom (or unusual) component * to your preferences panel, and it's being made non-opaque against your wishes, * add its class to the {@link #opaqueComponents} set in your {@link #setupOpaqueComponents()} * method. * * @return The option panel or null */ protected abstract JPanel buildUI(); protected FormValidationHandler getHandler() { return handler; } /** * Returns the point where the IOConnector's top part is, for the specified input number. * This point is given relitive to this MungeComponet, to the MungePen * use the translate method of the point to correct it. * * @param inputNum The number of the IOConnector to find the position of * @return Point where the IOC is */ public Point getInputPosition(int inputNum) { int inputs = step.getMSOInputs().size(); if (!inputNames.isVisible()) { int xPos = (int) (((double) (inputNum + 1) / ((double) inputs + 1)) * getWidth()); return new Point(xPos, 0); } int xPos = inputLabels[inputNum].getX() + inputLabels[inputNum].getWidth() / 2; return new Point(xPos, 0); } /** * Returns the point where the IOConnector's top part is, for the specified output number. * This point is given relitive to this MungeComponet, to the MungePen * use the translate method of the point to correct it. * * @param inputNum The number of the IOConnector to find the position of * @return Point where the IOC is */ public Point getOutputPosition(int outputNum) { int outputs = step.getChildren(MungeStepOutput.class).size(); int xPos = (int) (((double) (outputNum + 1) / ((double) outputs + 1)) * getWidth()); Point orig = new Point(xPos, getHeight() - getBorder().getBorderInsets(this).bottom); if (outputNames.isVisible()) { orig.x = outputLabels[outputNum].getX() + outputLabels[outputNum].getWidth() / 2; } logger.debug("Output of step " + getName() + " output " + outputNum + " is at x position " + orig.x); return orig; } /** * Returns the step connected to the UI. * @return The step */ public MungeStep getStep() { return step; } /** * Returns the munge pen that this munge component is in. * * @return The pen */ protected MungePen getPen() { return (MungePen) getParent(); } private boolean isExpanded() { return step.isExpanded(); } @Override protected void paintComponent(Graphics g) { if (getPreferredSize().width != getWidth() || getPreferredSize().height != getHeight()) { setBounds(getX(), getY(), getPreferredSize().width, getPreferredSize().height); revalidate(); } Insets border = getBorder().getBorderInsets(this); Dimension dim = getSize(); dim.width -= border.left + border.right; dim.height -= border.top + border.bottom; g.drawImage(MMM_TOP, getWidth() - border.right - 1, border.top - MMM_TOP.getHeight(null) + 2, null); g.drawImage(MMM_BOT, getWidth() - border.right - 1, getHeight() - MMM_BOT.getHeight(null) - border.bottom - 1, null); int[] x = { 0, MMM_TOP.getHeight(null) - 1, dim.width + MMM_TOP.getWidth(null) - 1, dim.width - 1 }; int[] y = { MMM_TOP.getHeight(null), 1, 1, MMM_TOP.getHeight(null) }; if (!hasFocus()) { g.setColor(normalShadow); } else { g.setColor(selectedShadow); } g.translate(0, border.top - MMM_TOP.getHeight(null)); g.fillPolygon(x, y, 4); g.translate(0, -(border.top - MMM_TOP.getHeight(null))); for (int i = 0; i < getStep().getMSOInputs().size(); i++) { int xPos = getInputPosition(i).x; Icon port = ConnectorIcon.getFemaleInstance(getStep().getInputDescriptor(i).getType()); port.paintIcon(this, g, xPos, border.top - port.getIconHeight()); if (getStep().getMSOInputs().get(i) != null || ghostIndex == i) { ConnectorIcon handle = ConnectorIcon.getHandleInstance(getStep().getInputDescriptor(i).getType()); Graphics2D g2 = (Graphics2D) g.create(); if (ghostIndex == i) { AlphaComposite alpha = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.5f); g2.setComposite(alpha); } handle.paintIcon(this, g2, xPos, 0); } } for (int i = 0; i < getStep().getChildren(MungeStepOutput.class).size(); i++) { if (!isOutputConnected(i) || (dropNib != null && dropNibIndex == i)) { int xPos = getOutputPosition(i).x; Icon nib; int droppingOffset; if (dropNib == null || dropNibIndex != i) { nib = ConnectorIcon .getNibInstance(getStep().getChildren(MungeStepOutput.class).get(i).getType()); droppingOffset = 0; } else { nib = dropNib; droppingOffset = dropNibOffSet; } if (!getPen().isConnectingOutput(this, i)) { logger.debug("Painting nib at x position " + xPos); nib.paintIcon(this, g, xPos, getHeight() - border.bottom - ConnectorIcon.NIB_OVERLAP - droppingOffset); } } } g = g.create(border.left, border.top, getWidth() - border.right, getHeight() - border.bottom); if (!hasFocus()) { g.setColor(normalBackground); } else { g.setColor(selectedBackground); } g.fillRect(0, 0, dim.width - 1, dim.height - 1); } /** * Returns the list for Inputs from the step object. * * @return the list */ public List<MungeStepOutput> getInputs() { return step.getMSOInputs(); } /** * Returns the list for Outputs from the step object. * * @return the list */ public List<MungeStepOutput> getOutputs() { return step.getMungeStepOutputs(); } /** * Returns the popup menu to display when this component is right clicked on. * Defaults to just having a remove action, but can be over ridden to include more actions. * * @return The popup menu */ protected JPopupMenu getPopupMenu() { JPopupMenu ret = new JPopupMenu(); JMenuItem rm = new JMenuItem(new AbstractAction("Delete (del)") { public void actionPerformed(ActionEvent e) { remove(); } }); ret.add(rm); if (logger.isDebugEnabled()) { ret.addSeparator(); ret.add(new AbstractAction("Show Components") { public void actionPerformed(ActionEvent e) { JTextArea ta = new JTextArea(listContents(AbstractMungeComponent.this, 0)); JOptionPane.showMessageDialog(AbstractMungeComponent.this, new JScrollPane(ta)); } private String listContents(Container c, int level) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < c.getComponentCount(); i++) { Component cc = c.getComponent(i); for (int j = 0; j < level; j++) { sb.append(" "); } sb.append(cc.getClass().getName()); sb.append(": "); sb.append(cc.getBackground()); sb.append("; opaque=").append(cc.isOpaque()); sb.append("\n"); if (cc instanceof Container) { sb.append(listContents((Container) cc, level + 1)); } } return sb.toString(); } }); } return ret; } /** * The remove action for the component. */ protected void remove() { MungeStep step = getStep(); MatchMakerObject mmo = (MatchMakerObject) step.getParent(); if (mmo instanceof MungeProcess) { ((MungeProcess) mmo).removeChildAndInputs(getStep()); } } /** * Hides or expands the component. */ protected void setExpanded(boolean expanded) { if (showInputNames) { inputNames.setVisible(expanded); } if (content != null) { content.setVisible(expanded); } if (showOutputNames) { outputNames.setVisible(expanded); } if (getPen() != null) { getPen().normalize(); } validate(); updateUI(); } /** * Set the x y parameter to the current value if needed. */ public void applyChanges() { //override this method to apply changes that may not have been saved to the model. } public boolean hasUnsavedChanges() { return false; } /** * A Set of listeners that detect changes in the MungeSteps and redraws * them. XXX This listener does not get removed when the munge pen goes away * and cleans up the munge components. */ private class StepEventHandler extends AbstractSPListener { @Override public void propertyChanged(PropertyChangeEvent evt) { if (evt.getPropertyName().equals("expanded")) { setExpanded((Boolean) evt.getNewValue()); } else if (evt.getPropertyName().equals("position")) { setLocation((Point) evt.getNewValue()); } else if (evt.getPropertyName().equals("addInputs")) { repaint(); } else if (!evt.getPropertyName().equals("inputs") && !((SPObject) evt.getSource()).isMagicEnabled()) { reload(); } } @Override public void childAdded(SPChildEvent e) { resizeBorders(); } @Override public void childRemoved(SPChildEvent e) { resizeBorders(); } } /** * The action to control the +/- button */ private class HideShowAction extends AbstractAction { public void actionPerformed(ActionEvent e) { step.setExpanded(!step.isExpanded()); } } /** * An action that can be added to the JPanel in buildUI that will add an input to the mungeStep. * This should only be used if there is a variable number of inputs allowed */ protected class AddInputAction extends AbstractAction { /** * Constructs the action. * * @param title The string on the button */ public AddInputAction(String title) { super(title); } public void actionPerformed(ActionEvent e) { InputDescriptor ref = getStep().getInputDescriptor(0); getStep().addInput(new InputDescriptor(ref.getName(), ref.getType())); } } /** * An action that can be added to a munge step that when used will * change all of the inputs or output labels to the plus icon or * the actual label. */ protected class HideShowAllLabelsAction extends AbstractAction { /** * control variables */ boolean input; boolean output; boolean show; /** * Constructor to set the title * * @param The title of the action */ public HideShowAllLabelsAction(String title, boolean input, boolean output, boolean show) { super(title); this.input = input; this.output = output; this.show = show; } public void actionPerformed(ActionEvent e) { if (input) { for (CoolJLabel l : inputLabels) { if (show) l.expand(); else l.collapse(); } } if (output) { for (CoolJLabel l : outputLabels) { if (show) l.expand(); else l.collapse(); } } } } /** * An action that can be added to the JPanel in buildUI that will remove all unused outputs from the mungeStep. * This should only be used if there is a variable number of inputs allowed */ protected class RemoveUnusedInputAction extends AbstractAction { /** * Constructs the action. * * @param title The string on the button */ RemoveUnusedInputAction(String title) { super(title); } public void actionPerformed(ActionEvent e) { MungeStep step = getStep(); step.removeUnusedInput(); } } private class MungeComponentKeyListener extends KeyAdapter { @Override public void keyPressed(KeyEvent e) { super.keyPressed(e); if (e.getKeyCode() == KeyEvent.VK_DELETE) { remove(); } } } private Point diff; private class MungeComponentMouseListener implements MouseListener { Point startPoint; public void mouseClicked(MouseEvent e) { boolean showPopup = maybeShowPopup(e); if (!showPopup && e.getClickCount() == 2) { step.setExpanded(!isExpanded()); } bustGhost(); autoScrollTimer.stop(); } public void mouseEntered(MouseEvent e) { } public void mouseExited(MouseEvent e) { nibDropStop(); bustGhost(); repaint(); } public void mousePressed(MouseEvent e) { getPen().moveToFront(AbstractMungeComponent.this); if (!maybeShowPopup(e)) { diff = new Point((int) (e.getPoint().getX()), (int) (e.getPoint().getY())); if (!checkForIOConnectors(new Point(e.getX() + getX(), e.getY() + getY()))) { requestFocusInWindow(); } startPoint = getLocation(); } } public void mouseReleased(MouseEvent e) { //resets the auto scroll bounds for the munge pen MungePen mp = getPen(); mp.lockAutoScroll(false); autoScrollTimer.stop(); if (!maybeShowPopup(e) && mp.isConnecting()) { Point abs = new Point(e.getX() + getX(), e.getY() + getY()); AbstractMungeComponent amc = mp.getMungeComponentAt(abs); if (amc == null || !amc.checkForIOConnectors(abs)) { mp.requestFocusInWindow(); } mp.repaint(); } else { step.setPosition(new Point(e.getX() + getX() - diff.x, e.getY() + getY() - diff.y)); } mp.normalize(); mp.stopConnection(); mp.revalidate(); } } /** * Possibly show the popup. * * @param e The mouse event * @return true iff the popup was shown */ private boolean maybeShowPopup(MouseEvent e) { if (e.isPopupTrigger()) { JPopupMenu pop = getPopupMenu(); if (pop != null) { diff = null; pop.show(AbstractMungeComponent.this, e.getX(), e.getY()); requestFocusInWindow(); } return true; } return false; } private void bustGhost() { if (ghostIndex != -1) { ghostIndex = -1; repaint(); } } private void setGhost(int ghost) { ghostIndex = ghost; } private class MungeComponentMouseMoveListener extends MouseMotionAdapter { @Override public void mouseDragged(MouseEvent e) { nibDropStop(); Point mouse = e.getPoint(); mouse.x += getX(); mouse.y += getY(); MungePen parent = (MungePen) getParent(); if (mouse.x < 0 || mouse.y < 0) { return; } if (!parent.isConnecting()) { //diff is set to null for a right click to prevent dragging if (diff != null) { //unlocks the bounds (because the pen has no idea when the mouse is moving getPen().lockAutoScroll(false); e.translatePoint(getX(), getY()); setLocation((int) (e.getX() - diff.getX()), (int) (e.getY() - diff.getY())); //checks if auto scrolling is a good idea at the present time MungePen pen = getPen(); Insets autoScroll = pen.getAutoscrollInsets(); //does not use the mouse point because this looks better autoScrollPoint = new Point(pen.getWidth() / 2, pen.getHeight() / 2); boolean asChanged = false; if (getX() + getWidth() > pen.getWidth() - autoScroll.right) { autoScrollPoint.x = getX() + getWidth(); asChanged = true; } if (getY() + getHeight() > pen.getHeight() - autoScroll.bottom) { autoScrollPoint.y = getY() + getHeight(); asChanged = true; } if (getX() < autoScroll.left) { autoScrollPoint.x = getX(); asChanged = true; } if (getY() < autoScroll.top) { autoScrollPoint.y = getY(); asChanged = true; } if (!asChanged) { autoScrollTimer.stop(); } else { autoScrollTimer.restart(); } } } else { parent.mouseX = e.getX() + getX(); parent.mouseY = e.getY() + getY(); } parent.repaint(); } @Override public void mouseMoved(MouseEvent e) { getPen().mouseX = e.getX() + getX(); getPen().mouseY = e.getY() + getY(); Point p = e.getPoint(); p.translate(getX(), getY()); int selected = getClosestIOIndex(p, CLICK_TOLERANCE, false); if (selected != -1 && step.isInputStep()) { MungeStepOutput<?> out = step.getChildren(MungeStepOutput.class).get(selected); setToolTipText(out.getName() + " (" + shortClassName(out.getType()) + ")"); } if (dropNibIndex != selected && selected != -1 && isOutputConnected(selected)) { if (dropNibTimer != null) { dropNibTimer.stop(); } dropNibTimer = new Timer(DROP_TIMER_INTERVAL, new DropDownAction(selected, DROP_AMOUNT)); dropNibTimer.start(); } else if (dropNibIndex != selected) { nibDropStop(); } selected = getClosestIOIndex(p, CLICK_TOLERANCE, true); if (selected != -1 && getStep().getMSOInputs().get(selected) == null) { if (ghostIndex != selected) { setGhost(selected); } } else { bustGhost(); } repaint(); } } //checks to see if the mouse was near an IOC point private boolean checkForIOConnectors(Point mousePoint) { MungePen parent = getPen(); int inputIndex = getClosestIOIndex(mousePoint, CLICK_TOLERANCE, true); if (inputIndex != -1) { if (parent.isConnecting()) { parent.finishConnection(this, inputIndex, true); } else { parent.startConnection(this, inputIndex, true); } return true; } inputIndex = getClosestIOIndex(mousePoint, CLICK_TOLERANCE, false); if (inputIndex != -1) { if (parent.isConnecting()) { parent.finishConnection(this, inputIndex, false); } else { parent.startConnection(this, inputIndex, false); } return true; } return false; } /** * Returns the input or output number that is the closest to the given point * within a tolerance. * * @param mousePoint The given point * @param tol The tolerance * @param checkInputs Set to true if you are checking inputs, false if you are looking at outputs * @return The index of the closest input to the mouse event or -1 if no * inputs are close */ public int getClosestIOIndex(Point mousePoint, int tol, boolean checkInputs) { int count; if (checkInputs) { count = getStep().getMSOInputs().size(); } else { count = getStep().getMungeStepOutputs().size(); } //squared because it should be faster then using the sqrt method later tol = tol * tol; int minDist = tol; int minNum = -1; for (int x = 0; x < count; x++) { Point p; if (checkInputs) { p = getInputPosition(x); } else { p = getOutputPosition(x); } p.translate(getX(), getY()); int dist = Math.abs(p.x - mousePoint.x) * Math.abs(p.x - mousePoint.x) + Math.abs(p.y - mousePoint.y) * Math.abs(p.y - mousePoint.y); if (dist < tol && dist < minDist) { minDist = dist; minNum = x; } } return minNum; } /** * Returns true iff the output for the given index of this component is in use. * * @param index The index */ private boolean isOutputConnected(int index) { return getStep().getMungeStepOutputs().get(index).getUsage() > 0; } /** * Tells this mungecomponent that the output at the given index is in use; * * @param index the index * @param con True iff the component is in use. */ public void setConnectOutput(int index, boolean con) { logger.debug("connect " + getStep().getName() + "'s output index: " + index + " -> " + con); MungeStepOutput mso = getStep().getMungeStepOutputs().get(index); if (con) { mso.incrementUsage(); } else { mso.decrementUsage(); } } /** * Stops dropping the nib. */ private void nibDropStop() { if (dropNibTimer != null && dropNibTimer.isRunning()) { dropNib = null; dropNibTimer.stop(); dropNibIndex = -1; repaint(); } } /** * The action to be added to the timer to drop the nib. */ private class DropDownAction extends AbstractAction { int dropAmount; public DropDownAction(int index, int dropAmount) { this.dropAmount = dropAmount; dropNib = ConnectorIcon .getNibInstance(getStep().getChildren(MungeStepOutput.class).get(index).getType()); dropNibOffSet = dropNib.getIconHeight(); dropNibIndex = index; } public void actionPerformed(ActionEvent e) { dropNibOffSet -= dropAmount; if (dropNibOffSet < 0) { dropNibOffSet = 0; } repaint(); } } /** * If set to true the input names will be displayed if the user expands the component. * * @param b The value to set it to */ protected void setInputShowNames(Boolean b) { showInputNames = b; if (!b) { inputNames.setVisible(false); } else { inputNames.setVisible(isExpanded()); } } /** * If set to true the output names will be displayed if the user expands the component. * * @param b The value to set it to */ protected void setOutputShowNames(Boolean b) { showOutputNames = b; if (!b) { outputNames.setVisible(false); } else { outputNames.setVisible(isExpanded()); } } /** * reloads the content pane from the matchmaker object */ protected void reload() { if (isExpanded()) { setExpanded(false); } content = buildUI(); if (content != null) { content.setOpaque(false); deOpaquify(content); } if (isExpanded()) { setExpanded(true); } requestFocus(); } /** * helper methods called by the subclasses * ************************************************************** */ private Point getDifferencePoint() { return this.diff; } }