/* * This file is part of the OSMembrane project. * More informations under www.osmembrane.de * * The project is licensed under the GNU GENERAL PUBLIC LICENSE 3.0. * for more details about the license see http://www.osmembrane.de/license/ * * Source: $HeadURL$ ($Revision$) * Last changed: $Date$ */ package de.osmembrane.view.panels; import java.awt.Adjustable; import java.awt.Color; import java.awt.Cursor; import java.awt.GridLayout; import java.awt.Point; import java.awt.event.AdjustmentEvent; import java.awt.event.AdjustmentListener; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import java.awt.event.MouseWheelEvent; import java.awt.event.MouseWheelListener; import java.awt.geom.AffineTransform; import java.awt.geom.NoninvertibleTransformException; import java.awt.geom.Point2D; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Observable; import java.util.Observer; import javax.swing.Action; import javax.swing.BorderFactory; import javax.swing.JLayeredPane; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JScrollBar; import de.osmembrane.Application; import de.osmembrane.controller.ActionRegistry; import de.osmembrane.controller.actions.AddConnectionAction; import de.osmembrane.controller.actions.AddFunctionAction; import de.osmembrane.controller.actions.DeleteSelectionAction; import de.osmembrane.controller.actions.DuplicateFunctionAction; import de.osmembrane.controller.actions.MoveFunctionAction; import de.osmembrane.controller.events.ConnectingFunctionsEvent; import de.osmembrane.controller.events.ContainingLocationEvent; import de.osmembrane.exceptions.ControlledException; import de.osmembrane.exceptions.ExceptionSeverity; import de.osmembrane.model.ModelProxy; import de.osmembrane.model.pipeline.AbstractConnector; import de.osmembrane.model.pipeline.AbstractFunction; import de.osmembrane.model.pipeline.Connector; import de.osmembrane.model.pipeline.PipelineObserverObject; import de.osmembrane.model.pipeline.PipelineObserverObject.ChangeType; import de.osmembrane.model.settings.SettingType; import de.osmembrane.tools.I18N; import de.osmembrane.view.ViewRegistry; import de.osmembrane.view.components.JSilentScrollBar; import de.osmembrane.view.interfaces.IZoomDevice; /** * This is the pipeline view, i.e. the panel that shows the entire pipeline with * all {@link PipelineFunction}s, {@link PipelineConnector}s, and * {@link PipelineLink}s. * * <b>Note</b>: In order to work with all the fancy zoom and move stuff, the * {@link PipelinePanel} defines 2 specific coordinate systems: * <ol> * <li>The window space - int window coordinates as defined in Swing.</li> * <li>The object space - double coordinates that are stored in the model.</li> * </ol> * Therefore it might be a good idea to always specify when coordinates or * points occur whether they are treated as object and window space. * * The object space is defined as follows: a length of 1 window coordinate = a * length of 1.0 object coordinate in the initial view, window axes = object * axes. * * In order to produce the viewable output you have to apply * {@link AffineTransform}ations from the object space to the window space. In * order to translate locations from the window to the object space, you have to * do the same vice versa. * * There should never be a direct access to perform the transformations. Various * winToObj and objToWin transforming functions are provided for this matter. * * Additionally, currently happening transformation changes should only change * the currentDisplay transformation and, when final, "pre-apply" it (multiply * from the left side) to the objectToWindow transformation. * * @see "Spezifikation.pdf, chapter 2.1.5 (German)" * * @see "your Math reference book, sections 'Linear Algebra', 'Matrices', * 'Linear Transformations'" * * @author tobias_kuhn * */ public class PipelinePanel extends JPanel implements Observer, IZoomDevice { private static final long serialVersionUID = 2544369818627179591L; /** * list of {@link PipelineFunction} currently being present on the panel */ private List<PipelineFunction> functions; /** * map of {@link PipelineConnector} for the {@link Connector}s in the model, * which currently being present on the panel */ private Map<AbstractConnector, PipelineConnector> connectors; /** * The transformation which transforms the object coordinates to window * coordinates, depending on the view port and the zooming level */ private AffineTransform objectToWindow; /** * A temporary transformation to be used after objectToWindow to represent * the current changes of the display (move dragging, zoom animation) */ private AffineTransform currentDisplay; /** * The layered pane to show {@link PipelineFunction}s, * {@link PipelineConnector}s, {@link PipelineLink}s (in the particular * order defined by FUNCTION_LAYER, CONNECTOR_LAYER and LINK_LAYER) */ private JLayeredPane layeredPane; /** * The constants applicable for the layeredPane. */ private static final Integer FUNCTION_LAYER = new Integer(3); private static final Integer CONNECTOR_LAYER = new Integer(2); private static final Integer LINK_LAYER = new Integer(1); /** * Saves the point (in object coordinates) when a drag and drop action * occurs *inside* the {@link PipelinePanel} (i.e. not from the * {@link LibraryPanel}) */ private Point2D draggingFrom; /** * The standard zoom values. */ private final static double DEFAULT_ZOOM_IN = 1.25; private final static double DEFAULT_ZOOM_OUT = 0.80; protected static final double DEFAULT_ZOOM = 1.0; private final static double PIXEL_PER_ZOOM_LEVEL = 100.00; /** * The links to the {@link InspectorPanel} used for communication between * these two components. */ private InspectorPanel functionInspector; /** * The vertical and horizontal {@link JScrollBar} that can be used for * moving the view. */ private JSilentScrollBar verticalScroll; private JSilentScrollBar horizontalScroll; /** * The upper-left-most and the bottom-right-most point on the whole pipeline * in object space. */ private Point2D objTopLeft; private Point2D objBottomRight; /** * The currently selected {@link Tool}. */ private Tool activeTool; /** * The currently selected object (either a {@link PipelineFunction} or a * {@link PipelineLink}) */ private Object selected; /** * The temporary saving slot for creating a two point {@link PipelineLink}. */ private PipelineFunction connectionStart; /** * The temporary link to show the creation of a new connection */ private PipelinePreviewLink connectionPreview; /** * Initializes a new {@link PipelinePanel} * * @param functionInspector * the {@link InspectorPanel} to handle the edits for the * selected objects * @param popup * the Popup to be displayed on right clicks */ public PipelinePanel(InspectorPanel functionInspector, final JPopupMenu popup) { setLayout(new GridLayout(1, 1)); // internal values this.functions = new ArrayList<PipelineFunction>(); this.connectors = new HashMap<AbstractConnector, PipelineConnector>(); this.functionInspector = functionInspector; this.verticalScroll = new JSilentScrollBar(Adjustable.VERTICAL, 0, 0, 0, 0); this.horizontalScroll = new JSilentScrollBar(Adjustable.HORIZONTAL, 0, 0, 0, 0); AdjustmentListener al = new AdjustmentListener() { @Override public void adjustmentValueChanged(AdjustmentEvent e) { if (e.getSource() == verticalScroll) { moveTopTo(e.getValue()); arrange(false); } else if (e.getSource() == horizontalScroll) { moveLeftTo(e.getValue()); arrange(false); } } }; this.verticalScroll.addAdjustmentListener(al); this.horizontalScroll.addAdjustmentListener(al); this.objTopLeft = new Point2D.Double(); this.objBottomRight = new Point2D.Double(); this.activeTool = Tool.DEFAULT_MAGIC_TOOL; this.selected = null; this.connectionStart = null; this.connectionPreview = new PipelinePreviewLink(this); this.objectToWindow = new AffineTransform(); double zoomFactor = PipelinePanel.DEFAULT_ZOOM * (Double) ModelProxy.getInstance().getSettings() .getValue(SettingType.DEFAULT_ZOOM_SIZE); this.objectToWindow.setToScale(zoomFactor, zoomFactor); this.currentDisplay = new AffineTransform(); this.layeredPane = new JLayeredPane(); this.layeredPane.setVisible(true); this.layeredPane.setOpaque(true); add(this.layeredPane); this.layeredPane.add(this.connectionPreview); // register as observer ViewRegistry.getInstance().addObserver(this); // all listeners for all kind of events addMouseWheelListener(new MouseWheelListener() { @Override public void mouseWheelMoved(MouseWheelEvent e) { // zoom with mouse wheel if (e.getWheelRotation() < 0) { zoom(e.getPoint(), DEFAULT_ZOOM_IN); } else { zoom(e.getPoint(), DEFAULT_ZOOM_OUT); } } }); addMouseListener(new MouseListener() { @Override public void mouseReleased(MouseEvent e) { // popup on right click if (e.getButton() == MouseEvent.BUTTON3) { popup.show(e.getComponent(), e.getX(), e.getY()); } // view tool and magic switch (activeTool) { case DEFAULT_MAGIC_TOOL: if (selected != null) { break; } case VIEW_TOOL: if (e.isControlDown()) { // zoom objectToWindow.preConcatenate(currentDisplay); currentDisplay.setToIdentity(); } else { // move objectToWindow.preConcatenate(currentDisplay); currentDisplay.setToIdentity(); } arrange(true); break; } // select tool and magic switch (activeTool) { case DEFAULT_MAGIC_TOOL: case SELECTION_TOOL: if ((selected != null) && (selected instanceof PipelineFunction) && (draggingFrom != null)) { PipelineFunction pf = (PipelineFunction) selected; // getCoordinate - draggingFrom = offset to add Point newWinPos = e.getPoint(); Point2D objOffset = new Point2D.Double(pf .getModelLocation().getX() - draggingFrom.getX(), pf.getModelLocation() .getY() - draggingFrom.getY()); Point winOffset = objToWindowDelta(objOffset); newWinPos.translate(winOffset.x, winOffset.y); newWinPos = findNextFreePoint(newWinPos, pf); Point2D newObjPosition = windowToObj(newWinPos); // require a minimum distance to drag & drop if (newObjPosition.distance(pf.getModelLocation()) < PipelineFunction.PIPELINE_FUNCTION_MIN_DRAG_DISTANCE) { pf.setLocation(objToWindow(pf.getModelLocation())); pf.arrangeConnectors(); pf.arrangeLinks(); // return, so we don't create an undo step return; } // set position Action a = ActionRegistry.getInstance().get( MoveFunctionAction.class); ContainingLocationEvent cle = new ContainingLocationEvent( this, pf.getModelFunction(), newObjPosition); a.actionPerformed(cle); } } // connection tool and magic switch (activeTool) { case DEFAULT_MAGIC_TOOL: case SELECTION_TOOL: abortConnect(); } draggingFrom = null; } @Override public void mousePressed(MouseEvent e) { switch (activeTool) { case SELECTION_TOOL: selected(null); repaint(); break; case DEFAULT_MAGIC_TOOL: case VIEW_TOOL: selected(null); // start dragging draggingFrom = windowToObj(e.getPoint()); currentDisplay.setToIdentity(); break; } } @Override public void mouseExited(MouseEvent e) { } @Override public void mouseEntered(MouseEvent e) { } @Override public void mouseClicked(MouseEvent e) { } }); addMouseMotionListener(new MouseMotionListener() { @Override public void mouseMoved(MouseEvent e) { // when the start of new connection is determined, rearrange it if (connectionStart != null) { connectionPreview.setTarget(e.getPoint()); connectionPreview.regenerateLine(); repaint(); } PipelinePanel.this.connectionPreview .setVisible(PipelinePanel.this.connectionStart != null); } @Override public void mouseDragged(MouseEvent e) { // view tool and magic switch (activeTool) { case DEFAULT_MAGIC_TOOL: if (selected != null) { break; } case VIEW_TOOL: if (draggingFrom != null) { Point2D draggingTo = windowToObjFixed(e.getPoint()); if (e.isControlDown()) { // zoom double winDist = objToWindowFixed(draggingFrom) .distance(e.getPoint()) * Math.signum(draggingFrom.getY() - draggingTo.getY()); zoomTemp( draggingFrom, Math.exp(winDist * Math.log(2.0) / PIXEL_PER_ZOOM_LEVEL)); } else { // move currentDisplay.setToTranslation( objectToWindow.getScaleX() * (draggingTo.getX() - draggingFrom .getX()), objectToWindow.getScaleY() * (draggingTo.getY() - draggingFrom .getY())); arrange(true); } } break; } // selection tool and magic switch (activeTool) { case DEFAULT_MAGIC_TOOL: case SELECTION_TOOL: if ((selected != null) && (selected instanceof PipelineFunction) && (draggingFrom != null)) { PipelineFunction pf = (PipelineFunction) selected; // getCoordinate - draggingFrom Point2D objOffset = new Point2D.Double(pf .getModelLocation().getX() - draggingFrom.getX(), pf.getModelLocation() .getY() - draggingFrom.getY()); Point winOffset = objToWindowDelta(objOffset); // translate e.translatePoint(winOffset.x, winOffset.y); pf.setLocation(e.getPoint()); pf.arrangeConnectors(); pf.arrangeLinks(); // now arrange all *incoming* links, since connectors // only contain their outgoing connections for (PipelineConnector pfCon : pf.getConnectors()) { if (!pfCon.isOutpipes()) { for (PipelineLink pfInLink : pfCon.getInLinks()) { pfInLink.getLinkSource().arrangeLinks(); } } } } /* if something selected */ } } /* mouseDragged */ }); // necessary to initialize with correct model reflection update(null, new PipelineObserverObject(ChangeType.FULLCHANGE, null)); } /** * Moves objectToWindow in a way, so that the left screen (window x = 0) * will result in the object coordinate to. * * @param to * the object position to move the left window edge to */ private void moveLeftTo(double to) { // if you can't read the Math below properly, // some idiot used Eclipse/Java auto code formatter /* * o2w.scale * to + o2w.translate = 0 <=> o2w.translate = - o2w.scale * * to */ double translateToX = -objectToWindow.getScaleX() * to; /* * o2w * M = o2w_new, where o2w is of the form scale(x,y) and * translate(s,t) and M is a translation matrix from translate(u,v) and * o2w_new.translate is of the form scale(x,y) and * translateTo(translateToX,t) <=> u * x + s = translateToX v * y + t = * t <=> u = (translateToX - s) / x v = 0 */ objectToWindow.translate( (translateToX - objectToWindow.getTranslateX()) / (objectToWindow.getScaleX()), 0.0); } /** * Moves objectToWindow in a way, so that the top screen (window y = 0) will * result in the object coordinate to. * * @param to * the object position to move the top window edge to */ private void moveTopTo(double to) { // if you can't read the Math below properly, // some idiot used Eclipse/Java auto code formatter /* * o2w.scale * to + o2w.translate = 0 <=> o2w.translate = - o2w.scale * * to */ double translateToY = -objectToWindow.getScaleY() * to; /* * o2w * M = o2w_new, where o2w is of the form scale(x,y) and * translate(s,t) and M is a translation matrix from translate(u,v) and * o2w_new.translate is of the form scale(x,y) and * translateTo(s,translateToY) <=> u * x + s = s v * y + t = * translateToY <=> u = 0 v = (translateToY - t) / y */ objectToWindow.translate(0.0, (translateToY - objectToWindow.getTranslateY()) / (objectToWindow.getScaleY())); } /** * Fully translates window coordinates to object coordinates. * * @param window * window coordinates * @return window in object coordinates, null if there is an error with the * transformations which should theoretically never be the case */ protected Point2D windowToObj(Point window) { Point2D result = new Point2D.Double(); try { currentDisplay.inverseTransform(window, result); objectToWindow.inverseTransform(result, result); } catch (NoninvertibleTransformException e) { Application.handleException(e); } return result; } /** * Translates window delta coordinates to object delta coordinates. Typical * delta coordinates are object sizes. * * @param windowDelta * window delta coordinates * @return windowDelta in object delta coordinates */ private Point2D windowToObjDelta(Point windowDelta) { Point2D result = new Point2D.Double(); try { AffineTransform temp = new AffineTransform(objectToWindow); temp.preConcatenate(currentDisplay); temp.invert(); temp.deltaTransform(windowDelta, result); } catch (NoninvertibleTransformException e) { Application.handleException(e); } return result; } /** * Translates window coordinates to object coordinates based on only the * objectToWindow transformation, not the temporary display transformation. * This is necessary for dragging operations to transform only by the part * of the transformation which is currently newly determined. * * @param windowFixed * fixed window coordinates * @return window in object coordinates, only of basic transformation, null * if there is an error with the transformations which should * theoretically never be the case */ protected Point2D windowToObjFixed(Point windowFixed) { Point2D result = new Point2D.Double(); try { objectToWindow.inverseTransform(windowFixed, result); } catch (NoninvertibleTransformException e) { Application.handleException(e); } return result; } /** * Fully translates object coordinates to window coordinates. * * @param object * object coordinates * @return object in window coordinates */ protected Point objToWindow(Point2D object) { Point2D result = new Point2D.Double(); objectToWindow.transform(object, result); currentDisplay.transform(result, result); return new Point((int) result.getX(), (int) result.getY()); } /** * Translates object delta coordinates to window delta coordinates. Typical * delta coordinates are object sizes. * * @param objectDelta * object delta coordinates * @return object in window delta coordinates */ protected Point objToWindowDelta(Point2D objectDelta) { Point2D result = new Point2D.Double(); objectToWindow.deltaTransform(objectDelta, result); currentDisplay.deltaTransform(result, result); return new Point((int) result.getX(), (int) result.getY()); } /** * Translates object coordinates to window coordinates based on only the * objectToWindow transformation, not the temporary display transformation. * This is necessary for dragging operations to transform only by the part * of the transformation which is currently newly determined. * * @param objectFixed * fixed object coordinates * @return object in window coordinates, only of basic transformation */ protected Point objToWindowFixed(Point2D objectFixed) { Point2D result = new Point2D.Double(); objectToWindow.transform(objectFixed, result); return new Point((int) result.getX(), (int) result.getY()); } @Override public void zoomIn() { zoom(new Point(getWidth() / 2, getHeight() / 2), DEFAULT_ZOOM_IN); } @Override public void zoomOut() { zoom(new Point(getWidth() / 2, getHeight() / 2), DEFAULT_ZOOM_OUT); } /** * Zooms (in the objectToWindow transformation). * * @param winCenter * center of the zooming operation in window space * @param factor * zooming value. if < 1 zooms out, if > 1 zooms in */ public void zoom(Point winCenter, double factor) { // translate the center Point2D objCenter = windowToObj(winCenter); objectToWindow.translate(+objCenter.getX(), +objCenter.getY()); objectToWindow.scale(factor, factor); objectToWindow.translate(-objCenter.getX(), -objCenter.getY()); arrange(true); repaint(); } /** * Zooms temporary (in the current display transformation). * * @param objCenter * center of the zooming operation in object space * @param factor * zooming value. if < 1 zooms out, if > 1 zooms in */ public void zoomTemp(Point2D objCenter, double factor) { AffineTransform objectToWindowInverse = new AffineTransform( objectToWindow); try { objectToWindowInverse.invert(); } catch (NoninvertibleTransformException e) { Application.handleException(e); } currentDisplay.setToIdentity(); currentDisplay.concatenate(objectToWindow); currentDisplay.translate(+objCenter.getX(), +objCenter.getY()); currentDisplay.scale(factor, factor); currentDisplay.translate(-objCenter.getX(), -objCenter.getY()); currentDisplay.concatenate(objectToWindowInverse); arrange(true); repaint(); } @Override public void resetView() { double zoomFactor = PipelinePanel.DEFAULT_ZOOM * (Double) ModelProxy.getInstance().getSettings() .getValue(SettingType.DEFAULT_ZOOM_SIZE); objectToWindow.setToScale(zoomFactor, zoomFactor); arrange(true); } @Override public void showEntireView() { if (functions.size() < 1) { Application.handleException(new ControlledException(this, ExceptionSeverity.WARNING, I18N.getInstance().getString( "View.Pipeline.NoFunctionsForEntireView"))); } double left = objTopLeft.getX(); double top = objTopLeft.getY(); double right = objBottomRight.getX(); double bottom = objBottomRight.getY(); /* * Construct an affine transformation so that (left,top) |-> (0,0) and * (right, bottom) |-> (getWidth(),getHeight()) * * respect the aspect, tho */ double objAspect = (right - left) / (bottom - top); double winAspect = getWidth() / (double) getHeight(); double zoom = 1.0; if (objAspect > winAspect) { zoom = getWidth() / (right - left); } else { zoom = getHeight() / (bottom - top); } objectToWindow.setToIdentity(); objectToWindow.scale(zoom, zoom); objectToWindow.translate(-left, -top); arrange(true); } /** * <b>Note:</b> All addition and removal of objects must be done here. */ @Override public void update(Observable o, Object arg) { // check for notice from the pipeline model if (arg instanceof PipelineObserverObject) { PipelineObserverObject poo = (PipelineObserverObject) arg; switch (poo.getType()) { // new function was added case ADD_FUNCTION: PipelineFunction pfAdd = new PipelineFunction( poo.getChangedFunction(), this); functions.add(pfAdd); layeredPane.add(pfAdd, FUNCTION_LAYER); for (PipelineConnector pc : pfAdd.getConnectors()) { connectors.put(pc.getModelConnector(), pc); layeredPane.add(pc, CONNECTOR_LAYER); for (PipelineLink pl : pc.getOutLinks()) { layeredPane.add(pl, LINK_LAYER); } } // automatically select the newly added function selected(pfAdd); break; // properties of a function changed case CHANGE_FUNCTION: for (PipelineFunction pfChange : functions) { if (pfChange.getModelFunction().equals( poo.getChangedFunction())) { // if this function has links to its in-connectors, // arrange those functions too for (PipelineConnector pc : pfChange.getConnectors()) { if (!pc.isOutpipes()) { for (PipelineLink pl : pc.getInLinks()) { pl.getLinkSource().arrangeLinks(); } } } } /* if pfChange.equals(poo) */ } break; // a function got removed case DELETE_FUNCTION: for (int i = 0; i < functions.size(); i++) { PipelineFunction pfDelete = functions.get(i); if (pfDelete.getModelFunction().equals( poo.getChangedFunction())) { // clean-up on isle three layeredPane.remove(pfDelete); for (PipelineConnector pc : pfDelete.getConnectors()) { // delete in links int j = 0; while (j < pc.getInLinks().size()) { PipelineLink pl = pc.getInLinks().get(j); pl.getLinkSource().removeLinkTo( pl.getLinkDestination()); layeredPane.remove(pl); j++; } // delete out links int k = 0; while (k < pc.getOutLinks().size()) { PipelineLink pl = pc.getOutLinks().get(k); pl.getLinkSource().removeLinkTo( pl.getLinkDestination()); layeredPane.remove(pl); k++; } // delete connector connectors.remove(pc.getModelConnector()); layeredPane.remove(pc); } // deselect stuff if necessary if (pfDelete.equals(selected)) { selected(null); } functions.remove(i); break; } } break; // the whole pipeline was exchanged case FULLCHANGE: // let's pray for the GC to get this right functions.clear(); connectors.clear(); layeredPane.removeAll(); System.gc(); layeredPane.add(connectionPreview); for (AbstractFunction af : ModelProxy.getInstance() .getPipeline().getFunctions()) { PipelineFunction pfFullChange = new PipelineFunction(af, this); functions.add(pfFullChange); layeredPane.add(pfFullChange, FUNCTION_LAYER); for (PipelineConnector pc : pfFullChange.getConnectors()) { connectors.put(pc.getModelConnector(), pc); layeredPane.add(pc, CONNECTOR_LAYER); } } // start linking when all connectors are truly known for (PipelineConnector pc : connectors.values()) { pc.generateLinksFromModel(); for (PipelineLink pl : pc.getOutLinks()) { layeredPane.add(pl, LINK_LAYER); } } // deselect stuff selected(null); break; // new connection added case ADD_CONNECTION: PipelineConnector sourceAdd = findConnector(poo .getChangedConnectors()[0]); PipelineLink plAdd = sourceAdd.addLinkTo(findConnector(poo .getChangedConnectors()[1])); layeredPane.add(plAdd, LINK_LAYER); break; // connection deleted case DELETE_CONNECTION: PipelineConnector sourceDel = findConnector(poo .getChangedConnectors()[0]); PipelineLink plDel = sourceDel.removeLinkTo(findConnector(poo .getChangedConnectors()[1])); if (plDel != null) { layeredPane.remove(plDel); } break; } } arrange(true); layeredPane.repaint(); // this is better reset here connectionStart = null; // recreate topleft and bottomright calculateEdges(); updateScrollbars(); } /** * Calculates objTopLeft and objBottomRight. */ private void calculateEdges() { if (functions.size() > 0) { double left = Double.MAX_VALUE; double top = Double.MAX_VALUE; double right = Double.MIN_VALUE; double bottom = Double.MIN_VALUE; for (PipelineFunction pf : functions) { double thisX = pf.getModelLocation().getX(); double thisY = pf.getModelLocation().getY(); left = Math.min(left, thisX); top = Math.min(top, thisY); right = Math.max(right, thisX + pf.getPreferredSize().width); bottom = Math.max(bottom, thisY + pf.getPreferredSize().height); } objTopLeft.setLocation(left, top); objBottomRight.setLocation(right, bottom); } else { objTopLeft.setLocation(0, 0); objBottomRight.setLocation(0, 0); } } /** * Updates the valid value ranges of the scroll bars. */ private void updateScrollbars() { // current size of the window, not needed to be scrolled Point2D winSize = windowToObjDelta(new Point(getWidth(), getHeight())); horizontalScroll.setMinimumSilently((int) objTopLeft.getX()); horizontalScroll.setMaximumSilently((int) Math.max(0.0, (objBottomRight.getX() - winSize.getX()))); verticalScroll.setMinimumSilently((int) objTopLeft.getY()); verticalScroll.setMaximumSilently((int) Math.max(0.0, (objBottomRight.getY() - winSize.getY()))); Point2D objWindowZero = windowToObj(new Point(0, 0)); horizontalScroll.setValueSilently((int) objWindowZero.getX()); verticalScroll.setValueSilently((int) objWindowZero.getY()); } /** * Arranges all the {@link PipelineFunction}s after a move/zoom change * * @param updateScrolls * whether the scrollbars should be updated. necessary to prevent * infinite recursion */ private void arrange(boolean updateScrolls) { for (PipelineFunction pf : functions) { arrange(pf); } // now all connectors are arranged and we can arrange the links for (PipelineFunction pf : functions) { pf.arrangeLinks(); } calculateEdges(); if (updateScrolls) { updateScrollbars(); } } /** * Arrange a specific {@link PipelineFunction} after any change * * @param pf * the {@link PipelineFunction} to arrange */ private void arrange(PipelineFunction pf) { Point location = objToWindow(pf.getModelLocation()); pf.setLocation(location); Point size = new Point(pf.getPreferredSize().width, pf.getPreferredSize().height); size = objToWindowDelta(size); pf.setSize(size.x, size.y); if (pf.equals(selected)) { pf.setBorder(BorderFactory.createLineBorder(Color.BLACK, 2)); } else if (!pf.getModelFunction().isComplete()) { pf.setBorder(BorderFactory.createLineBorder(Color.RED, 2)); } else { pf.setBorder(null); } pf.arrangeConnectors(); } /** * Forwards hint display from {@link PipelineFunction}s and * {@link LibraryPanel} under the cursor to the {@link InspectorPanel}. * * @param hintText * the hint to display */ public void setHint(String hintText) { functionInspector.setHintText(hintText); } /** * Is called when a {@link LibraryFunction} that canDragAndDrop was dragged * onto the {@link PipelinePanel} * * @param libraryFunction * The new function to add * @param at * the position of the new function, in PipelinePanel's window * coordinates */ public void draggedOnto(LibraryFunction libraryFunction, Point at) { at = findNextFreePoint(at, null); // drag & drop functionality : add function Action a = ActionRegistry.getInstance().get(AddFunctionAction.class); Point2D newPosition = windowToObj(at); ContainingLocationEvent cle = new ContainingLocationEvent(this, libraryFunction.getModelFunctionPrototype(), newPosition); a.actionPerformed(cle); } /** * @return the currently selected object ({@link PipelineFunction} or * {@link PipelineLink}) */ public Object getSelected() { return selected; } /** * Called when a child object thinks it got selected. Only defined for * {@link PipelineFunction} and {@link PipelineLink}. * * @param childObject * child function to be selected */ public void selected(Object childObject) { selected = childObject; if (selected != null) { if (selected instanceof PipelineFunction) { repaint(); // edit in inspector panel PipelineFunction pf = (PipelineFunction) childObject; functionInspector.inspect(pf.getModelFunction()); } else if (selected instanceof PipelineLink) { repaint(); } else { Application.handleException(new ControlledException(this, ExceptionSeverity.UNEXPECTED_BEHAVIOR, I18N .getInstance().getString( "View.Pipeline.IllegalSelection", selected.toString()))); } } else { functionInspector.inspect(null); repaint(); } arrange(false); // enable deleting & duplicating ActionRegistry.getInstance().get(DeleteSelectionAction.class) .setEnabled(selected != null); ActionRegistry .getInstance() .get(DuplicateFunctionAction.class) .setEnabled( (selected != null) && (selected instanceof PipelineFunction)); } /** * @param activeTool * the active {@link Tool} to set * @param newCursor * the cursor associated with the new tool, or null if no change */ public void setActiveTool(Tool activeTool, Cursor newCursor) { abortConnect(); this.activeTool = activeTool; if (newCursor != null) { this.setCursor(newCursor); } } /** * @return the activeTool */ public Tool getActiveTool() { return activeTool; } /** * Sets the dragging point manually for objects that need to catch the * {@link MouseEvent} to get dragged around. (Typically, * {@link MouseListener}s should be forwarded though. But forwarding mouse * events in this case would cause a deselection of the dragged object) * * @param winDraggingFrom * point dragging started from (window space) to set */ public void setDraggingFrom(Point winDraggingFrom) { this.draggingFrom = windowToObj(winDraggingFrom); } /** * @return the verticalScroll */ public JScrollBar getVerticalScroll() { return verticalScroll; } /** * @return the horizontalScroll */ public JScrollBar getHorizontalScroll() { return horizontalScroll; } /** * @param lookFor * model {@link Connector} to look for * @return the pipeline, i.e. {@link PipelineConnector} associated with that * model {@link Connector}, null if none is found */ protected PipelineConnector findConnector(AbstractConnector lookFor) { return connectors.get(lookFor); } /** * Adds the connectionPoint as an end point to the current connection (which * has at most 2 points). This means: First call - start of new connection. * Second call - end of new connection. * * @param connectionPoint * point in the connection */ public void connect(PipelineFunction connectionPoint) { if (this.connectionStart == null) { this.connectionStart = connectionPoint; this.connectionPreview.setSource(connectionPoint); } else { ConnectingFunctionsEvent cfe = new ConnectingFunctionsEvent(this, connectionStart.getModelFunction(), connectionPoint.getModelFunction()); ActionRegistry.getInstance().get(AddConnectionAction.class) .actionPerformed(cfe); this.connectionStart = null; } } /** * Aborts a current try to connect two functions. */ public void abortConnect() { this.connectionStart = null; this.connectionPreview.setVisible(false); } /** * @return the layeredPane */ public JLayeredPane getLayeredPane() { return layeredPane; } /** * Finds out whether you can safely place a function here without occluding * another one. * * @param newPoint * {@link Point} where the new Function would be situated * @param ignore * Ignore this function. Useful if you're currently moving this * one. May be null. * @return true, if the new function would collide with an existing one, * false otherwise. */ public boolean wouldCollide(Point newPoint, PipelineFunction ignore) { /* * number of pixels to subtract from the technically necessity to have * functions touching, so that the feature is not too restrictive */ int grace = 0; for (PipelineFunction pf : functions) { if (pf.equals(ignore)) { continue; } if ((newPoint.x >= pf.getX() - pf.getWidth() + grace) && (newPoint.y >= pf.getY() - pf.getHeight() + grace) && (newPoint.x <= pf.getX() + pf.getWidth() - grace) && (newPoint.y <= pf.getY() + pf.getHeight() - grace)) { return true; } } return false; } /** * Finds the next free & usable position on the pipeline panel, based on a * preferred location. * * @param at * location where to find the nearest free point for * @param forFunc * the function for which the place should be found. May be null * @return the nearest free point in the area of at */ public Point findNextFreePoint(Point at, PipelineFunction forFunc) { Point result = new Point(at); double dist = 0.0; while (wouldCollide(result, forFunc)) { dist += 20.0; // angle between x to the right, y downwards in [0, Math.PI / 2] for (double angle = 0.0; angle < Math.PI / 2.0; angle += Math.PI / 20.0) { result = new Point(at); result.translate((int) (+Math.cos(angle) * dist), (int) (+Math.sin(angle) * dist)); if (!wouldCollide(result, forFunc)) { break; } } /* for */ } return result; } }