/** * $Id: mxGraphComponent.java,v 1.1 2012/11/15 13:26:50 gaudenz Exp $ * Copyright (c) 2009-2010, Gaudenz Alder, David Benson */ package com.mxgraph.swing; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Component; import java.awt.Cursor; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Point; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.Stroke; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; 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.MouseMotionListener; import java.awt.image.BufferedImage; import java.awt.print.PageFormat; import java.awt.print.Printable; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.EventObject; import java.util.Hashtable; import java.util.Iterator; import java.util.List; import java.util.Map; import javax.swing.BorderFactory; import javax.swing.BoundedRangeModel; import javax.swing.ImageIcon; import javax.swing.JComponent; import javax.swing.JPanel; import javax.swing.JScrollBar; import javax.swing.JScrollPane; import javax.swing.RepaintManager; import javax.swing.SwingUtilities; import javax.swing.ToolTipManager; import javax.swing.TransferHandler; import com.mxgraph.canvas.mxGraphics2DCanvas; import com.mxgraph.canvas.mxICanvas; import com.mxgraph.model.mxGraphModel; import com.mxgraph.model.mxGraphModel.Filter; import com.mxgraph.model.mxIGraphModel; import com.mxgraph.swing.handler.mxCellHandler; import com.mxgraph.swing.handler.mxConnectionHandler; import com.mxgraph.swing.handler.mxEdgeHandler; import com.mxgraph.swing.handler.mxElbowEdgeHandler; import com.mxgraph.swing.handler.mxGraphHandler; import com.mxgraph.swing.handler.mxGraphTransferHandler; import com.mxgraph.swing.handler.mxPanningHandler; import com.mxgraph.swing.handler.mxSelectionCellsHandler; import com.mxgraph.swing.handler.mxVertexHandler; import com.mxgraph.swing.util.mxCellOverlay; import com.mxgraph.swing.util.mxICellOverlay; import com.mxgraph.swing.view.mxCellEditor; import com.mxgraph.swing.view.mxICellEditor; import com.mxgraph.swing.view.mxInteractiveCanvas; import com.mxgraph.util.mxConstants; import com.mxgraph.util.mxEvent; import com.mxgraph.util.mxEventObject; import com.mxgraph.util.mxEventSource; import com.mxgraph.util.mxEventSource.mxIEventListener; import com.mxgraph.util.mxPoint; import com.mxgraph.util.mxRectangle; import com.mxgraph.util.mxResources; import com.mxgraph.util.mxUtils; import com.mxgraph.view.mxCellState; import com.mxgraph.view.mxEdgeStyle; import com.mxgraph.view.mxEdgeStyle.mxEdgeStyleFunction; import com.mxgraph.view.mxGraph; import com.mxgraph.view.mxGraphView; import com.mxgraph.view.mxTemporaryCellStates; /** * For setting the preferred size of the viewport for scrolling, use * mxGraph.setMinimumGraphSize. This component is a combined scrollpane with an * inner mxGraphControl. The control contains the actual graph display. * * To set the background color of the graph, use the following code: * * <pre> * graphComponent.getViewport().setOpaque(true); * graphComponent.getViewport().setBackground(newColor); * </pre> * * This class fires the following events: * * mxEvent.START_EDITING fires before starting the in-place editor for an * existing cell in startEditingAtCell. The <code>cell</code> property contains * the cell that is being edit and the <code>event</code> property contains * optional EventObject which was passed to startEditingAtCell. * * mxEvent.LABEL_CHANGED fires between begin- and endUpdate after the call to * mxGraph.cellLabelChanged in labelChanged. The <code>cell</code> property * contains the cell, the <code>value</code> property contains the new value for * the cell and the optional <code>event</code> property contains the * EventObject that started the edit. * * mxEvent.ADD_OVERLAY and mxEvent.REMOVE_OVERLAY fire afer an overlay was added * or removed using add-/removeOverlay. The <code>cell</code> property contains * the cell for which the overlay was added or removed and the * <code>overlay</code> property contain the mxOverlay. * * mxEvent.BEFORE_PAINT and mxEvent.AFTER_PAINT fire before and after the paint * method is called on the component. The <code>g</code> property contains the * graphics context which is used for painting. */ public class mxGraphComponent extends JScrollPane implements Printable { /** * */ private static final long serialVersionUID = -30203858391633447L; /** * */ public static final int GRID_STYLE_DOT = 0; /** * */ public static final int GRID_STYLE_CROSS = 1; /** * */ public static final int GRID_STYLE_LINE = 2; /** * */ public static final int GRID_STYLE_DASHED = 3; /** * */ public static final int ZOOM_POLICY_NONE = 0; /** * */ public static final int ZOOM_POLICY_PAGE = 1; /** * */ public static final int ZOOM_POLICY_WIDTH = 2; /** * */ public static ImageIcon DEFAULT_EXPANDED_ICON = null; /** * */ public static ImageIcon DEFAULT_COLLAPSED_ICON = null; /** * */ public static ImageIcon DEFAULT_WARNING_ICON = null; /** * Specifies the default page scale. Default is 1.4 */ public static final double DEFAULT_PAGESCALE = 1.4; /** * Loads the collapse and expand icons. */ static { DEFAULT_EXPANDED_ICON = new ImageIcon( mxGraphComponent.class .getResource("/com/mxgraph/swing/images/expanded.gif")); DEFAULT_COLLAPSED_ICON = new ImageIcon( mxGraphComponent.class .getResource("/com/mxgraph/swing/images/collapsed.gif")); DEFAULT_WARNING_ICON = new ImageIcon( mxGraphComponent.class .getResource("/com/mxgraph/swing/images/warning.gif")); } /** * */ protected mxGraph graph; /** * */ protected mxGraphControl graphControl; /** * */ protected mxEventSource eventSource = new mxEventSource(this); /** * */ protected mxICellEditor cellEditor; /** * */ protected mxConnectionHandler connectionHandler; /** * */ protected mxPanningHandler panningHandler; /** * */ protected mxSelectionCellsHandler selectionCellsHandler; /** * */ protected mxGraphHandler graphHandler; /** * The transparency of previewed cells from 0.0. to 0.1. 0.0 indicates * transparent, 1.0 indicates opaque. Default is 1. */ protected float previewAlpha = 0.5f; /** * Specifies the <mxImage> to be returned by <getBackgroundImage>. Default * is null. */ protected ImageIcon backgroundImage; /** * Background page format. */ protected PageFormat pageFormat = new PageFormat(); /** * */ protected mxInteractiveCanvas canvas; /** * */ protected BufferedImage tripleBuffer; /** * */ protected Graphics2D tripleBufferGraphics; /** * Defines the scaling for the background page metrics. Default is * {@link #DEFAULT_PAGESCALE}. */ protected double pageScale = DEFAULT_PAGESCALE; /** * Specifies if the background page should be visible. Default is false. */ protected boolean pageVisible = false; /** * If the pageFormat should be used to determine the minimal graph bounds * even if the page is not visible (see pageVisible). Default is false. */ protected boolean preferPageSize = false; /** * Specifies if a dashed line should be drawn between multiple pages. */ protected boolean pageBreaksVisible = true; /** * Specifies the color of page breaks */ protected Color pageBreakColor = Color.darkGray; /** * Specifies the number of pages in the horizontal direction. */ protected int horizontalPageCount = 1; /** * Specifies the number of pages in the vertical direction. */ protected int verticalPageCount = 1; /** * Specifies if the background page should be centered by automatically * setting the translate in the view. Default is true. This does only apply * if pageVisible is true. */ protected boolean centerPage = true; /** * Color of the background area if layout view. */ protected Color pageBackgroundColor = new Color(144, 153, 174); /** * */ protected Color pageShadowColor = new Color(110, 120, 140); /** * */ protected Color pageBorderColor = Color.black; /** * Specifies if the grid is visible. Default is false. */ protected boolean gridVisible = false; /** * */ protected Color gridColor = new Color(192, 192, 192); /** * Whether or not to scroll the scrollable container the graph exists in if * a suitable handler is active and the graph bounds already exist extended * in the direction of mouse travel. */ protected boolean autoScroll = true; /** * Whether to extend the graph bounds and scroll towards the limit of those * new bounds in the direction of mouse travel if a handler is active while * the mouse leaves the container that the graph exists in. */ protected boolean autoExtend = true; /** * */ protected boolean dragEnabled = true; /** * */ protected boolean importEnabled = true; /** * */ protected boolean exportEnabled = true; /** * Specifies if folding (collapse and expand via an image icon in the graph * should be enabled). Default is true. */ protected boolean foldingEnabled = true; /** * Specifies the tolerance for mouse clicks. Default is 4. */ protected int tolerance = 4; /** * Specifies if swimlanes are selected when the mouse is released over the * swimlanes content area. Default is true. */ protected boolean swimlaneSelectionEnabled = true; /** * Specifies if the content area should be transparent to events. Default is * true. */ protected boolean transparentSwimlaneContent = true; /** * */ protected int gridStyle = GRID_STYLE_DOT; /** * */ protected ImageIcon expandedIcon = DEFAULT_EXPANDED_ICON; /** * */ protected ImageIcon collapsedIcon = DEFAULT_COLLAPSED_ICON; /** * */ protected ImageIcon warningIcon = DEFAULT_WARNING_ICON; /** * */ protected boolean antiAlias = true; /** * */ protected boolean textAntiAlias = true; /** * Specifies <escape> should be invoked when the escape key is pressed. * Default is true. */ protected boolean escapeEnabled = true; /** * If true, when editing is to be stopped by way of selection changing, data * in diagram changing or other means stopCellEditing is invoked, and * changes are saved. This is implemented in a mouse listener in this class. * Default is true. */ protected boolean invokesStopCellEditing = true; /** * If true, pressing the enter key without pressing control will stop * editing and accept the new value. This is used in <mxKeyHandler> to stop * cell editing. Default is false. */ protected boolean enterStopsCellEditing = false; /** * Specifies the zoom policy. Default is ZOOM_POLICY_PAGE. The zoom policy * does only apply if pageVisible is true. */ protected int zoomPolicy = ZOOM_POLICY_PAGE; /** * Internal flag to not reset zoomPolicy when zoom was set automatically. */ private transient boolean zooming = false; /** * Specifies the factor used for zoomIn and zoomOut. Default is 1.2 (120%). */ protected double zoomFactor = 1.2; /** * Specifies if the viewport should automatically contain the selection * cells after a zoom operation. Default is false. */ protected boolean keepSelectionVisibleOnZoom = false; /** * Specifies if the zoom operations should go into the center of the actual * diagram rather than going from top, left. Default is true. */ protected boolean centerZoom = true; /** * Specifies if an image buffer should be used for painting the component. * Default is false. */ protected boolean tripleBuffered = false; /** * Used for debugging the dirty region. */ public boolean showDirtyRectangle = false; /** * Maps from cells to lists of heavyweights. */ protected Hashtable<Object, Component[]> components = new Hashtable<Object, Component[]>(); /** * Maps from cells to lists of overlays. */ protected Hashtable<Object, mxICellOverlay[]> overlays = new Hashtable<Object, mxICellOverlay[]>(); /** * Boolean flag to disable centering after the first time. */ private transient boolean centerOnResize = true; /** * Updates the heavyweight component structure after any changes. */ protected mxIEventListener updateHandler = new mxIEventListener() { public void invoke(Object sender, mxEventObject evt) { updateComponents(); graphControl.updatePreferredSize(); } }; /** * */ protected mxIEventListener repaintHandler = new mxIEventListener() { public void invoke(Object source, mxEventObject evt) { mxRectangle dirty = (mxRectangle) evt.getProperty("region"); Rectangle rect = (dirty != null) ? dirty.getRectangle() : null; if (rect != null) { rect.grow(1, 1); } // Updates the triple buffer repaintTripleBuffer(rect); // Repaints the control using the optional triple buffer graphControl.repaint((rect != null) ? rect : getViewport() .getViewRect()); // ---------------------------------------------------------- // Shows the dirty region as a red rectangle (for debugging) JPanel panel = (JPanel) getClientProperty("dirty"); if (showDirtyRectangle) { if (panel == null) { panel = new JPanel(); panel.setOpaque(false); panel.setBorder(BorderFactory.createLineBorder(Color.RED)); putClientProperty("dirty", panel); graphControl.add(panel); } if (dirty != null) { panel.setBounds(dirty.getRectangle()); } panel.setVisible(dirty != null); } else if (panel != null && panel.getParent() != null) { panel.getParent().remove(panel); putClientProperty("dirty", null); repaint(); } // ---------------------------------------------------------- } }; /** * */ protected PropertyChangeListener viewChangeHandler = new PropertyChangeListener() { /** * */ public void propertyChange(PropertyChangeEvent evt) { if (evt.getPropertyName().equals("view")) { mxGraphView oldView = (mxGraphView) evt.getOldValue(); mxGraphView newView = (mxGraphView) evt.getNewValue(); if (oldView != null) { oldView.removeListener(updateHandler); } if (newView != null) { newView.addListener(mxEvent.SCALE, updateHandler); newView.addListener(mxEvent.TRANSLATE, updateHandler); newView.addListener(mxEvent.SCALE_AND_TRANSLATE, updateHandler); newView.addListener(mxEvent.UP, updateHandler); newView.addListener(mxEvent.DOWN, updateHandler); } } else if (evt.getPropertyName().equals("model")) { mxGraphModel oldModel = (mxGraphModel) evt.getOldValue(); mxGraphModel newModel = (mxGraphModel) evt.getNewValue(); if (oldModel != null) { oldModel.removeListener(updateHandler); } if (newModel != null) { newModel.addListener(mxEvent.CHANGE, updateHandler); } } } }; /** * Resets the zoom policy if the scale is changed manually. */ protected mxIEventListener scaleHandler = new mxIEventListener() { /** * */ public void invoke(Object sender, mxEventObject evt) { if (!zooming) { zoomPolicy = ZOOM_POLICY_NONE; } } }; /** * * @param graph */ public mxGraphComponent(mxGraph graph) { setCellEditor(createCellEditor()); canvas = createCanvas(); // Initializes the buffered view and graphControl = createGraphControl(); installFocusHandler(); installKeyHandler(); installResizeHandler(); setGraph(graph); // Adds the viewport view and initializes handlers setViewportView(graphControl); createHandlers(); installDoubleClickHandler(); } /** * installs a handler to set the focus to the container. */ protected void installFocusHandler() { graphControl.addMouseListener(new MouseAdapter() { public void mousePressed(MouseEvent e) { if (!hasFocus()) { requestFocus(); } } }); } /** * Handles escape keystrokes. */ protected void installKeyHandler() { addKeyListener(new KeyAdapter() { public void keyPressed(KeyEvent e) { if (e.getKeyCode() == KeyEvent.VK_ESCAPE && isEscapeEnabled()) { escape(e); } } }); } /** * Applies the zoom policy if the size of the component changes. */ protected void installResizeHandler() { addComponentListener(new ComponentAdapter() { public void componentResized(ComponentEvent e) { zoomAndCenter(); } }); } /** * Adds handling of edit and stop-edit events after all other handlers have * been installed. */ protected void installDoubleClickHandler() { graphControl.addMouseListener(new MouseAdapter() { public void mouseReleased(MouseEvent e) { if (isEnabled()) { if (!e.isConsumed() && isEditEvent(e)) { Object cell = getCellAt(e.getX(), e.getY(), false); if (cell != null && getGraph().isCellEditable(cell)) { startEditingAtCell(cell, e); } } else { // Other languages use focus traversal here, in Java // we explicitely stop editing after a click elsewhere stopEditing(!invokesStopCellEditing); } } } }); } /** * */ protected mxICellEditor createCellEditor() { return new mxCellEditor(this); } /** * */ public void setGraph(mxGraph value) { mxGraph oldValue = graph; // Uninstalls listeners for existing graph if (graph != null) { graph.removeListener(repaintHandler); graph.getModel().removeListener(updateHandler); graph.getView().removeListener(updateHandler); graph.removePropertyChangeListener(viewChangeHandler); graph.getView().removeListener(scaleHandler); } graph = value; // Updates the buffer if the model changes graph.addListener(mxEvent.REPAINT, repaintHandler); // Installs the update handler to sync the overlays and controls graph.getModel().addListener(mxEvent.CHANGE, updateHandler); // Repaint after the following events is handled via // mxGraph.repaint-events // The respective handlers are installed in mxGraph.setView mxGraphView view = graph.getView(); view.addListener(mxEvent.SCALE, updateHandler); view.addListener(mxEvent.TRANSLATE, updateHandler); view.addListener(mxEvent.SCALE_AND_TRANSLATE, updateHandler); view.addListener(mxEvent.UP, updateHandler); view.addListener(mxEvent.DOWN, updateHandler); graph.addPropertyChangeListener(viewChangeHandler); // Resets the zoom policy if the scale changes graph.getView().addListener(mxEvent.SCALE, scaleHandler); graph.getView().addListener(mxEvent.SCALE_AND_TRANSLATE, scaleHandler); // Invoke the update handler once for initial state updateHandler.invoke(graph.getView(), null); firePropertyChange("graph", oldValue, graph); } /** * * @return Returns the object that contains the graph. */ public mxGraph getGraph() { return graph; } /** * Creates the inner control that handles tooltips, preferred size and can * draw cells onto a canvas. */ protected mxGraphControl createGraphControl() { return new mxGraphControl(); } /** * * @return Returns the control that renders the graph. */ public mxGraphControl getGraphControl() { return graphControl; } /** * Creates the connection-, panning and graphhandler (in this order). */ protected void createHandlers() { setTransferHandler(createTransferHandler()); panningHandler = createPanningHandler(); selectionCellsHandler = createSelectionCellsHandler(); connectionHandler = createConnectionHandler(); graphHandler = createGraphHandler(); } /** * */ protected TransferHandler createTransferHandler() { return new mxGraphTransferHandler(); } /** * */ protected mxSelectionCellsHandler createSelectionCellsHandler() { return new mxSelectionCellsHandler(this); } /** * */ protected mxGraphHandler createGraphHandler() { return new mxGraphHandler(this); } /** * */ public mxSelectionCellsHandler getSelectionCellsHandler() { return selectionCellsHandler; } /** * */ public mxGraphHandler getGraphHandler() { return graphHandler; } /** * */ protected mxConnectionHandler createConnectionHandler() { return new mxConnectionHandler(this); } /** * */ public mxConnectionHandler getConnectionHandler() { return connectionHandler; } /** * */ protected mxPanningHandler createPanningHandler() { return new mxPanningHandler(this); } /** * */ public mxPanningHandler getPanningHandler() { return panningHandler; } /** * */ public boolean isEditing() { return getCellEditor().getEditingCell() != null; } /** * */ public mxICellEditor getCellEditor() { return cellEditor; } /** * */ public void setCellEditor(mxICellEditor value) { mxICellEditor oldValue = cellEditor; cellEditor = value; firePropertyChange("cellEditor", oldValue, cellEditor); } /** * @return the tolerance */ public int getTolerance() { return tolerance; } /** * @param value * the tolerance to set */ public void setTolerance(int value) { int oldValue = tolerance; tolerance = value; firePropertyChange("tolerance", oldValue, tolerance); } /** * */ public PageFormat getPageFormat() { return pageFormat; } /** * */ public void setPageFormat(PageFormat value) { PageFormat oldValue = pageFormat; pageFormat = value; firePropertyChange("pageFormat", oldValue, pageFormat); } /** * */ public double getPageScale() { return pageScale; } /** * */ public void setPageScale(double value) { double oldValue = pageScale; pageScale = value; firePropertyChange("pageScale", oldValue, pageScale); } /** * Returns the size of the area that layouts can operate in. */ public mxRectangle getLayoutAreaSize() { if (pageVisible) { Dimension d = getPreferredSizeForPage(); return new mxRectangle(new Rectangle(d)); } else { return new mxRectangle(new Rectangle(graphControl.getSize())); } } /** * */ public ImageIcon getBackgroundImage() { return backgroundImage; } /** * */ public void setBackgroundImage(ImageIcon value) { ImageIcon oldValue = backgroundImage; backgroundImage = value; firePropertyChange("backgroundImage", oldValue, backgroundImage); } /** * @return the pageVisible */ public boolean isPageVisible() { return pageVisible; } /** * Fires a property change event for <code>pageVisible</code>. zoomAndCenter * should be called if this is set to true. * * @param value * the pageVisible to set */ public void setPageVisible(boolean value) { boolean oldValue = pageVisible; pageVisible = value; firePropertyChange("pageVisible", oldValue, pageVisible); } /** * @return the preferPageSize */ public boolean isPreferPageSize() { return preferPageSize; } /** * Fires a property change event for <code>preferPageSize</code>. * * @param value * the preferPageSize to set */ public void setPreferPageSize(boolean value) { boolean oldValue = preferPageSize; preferPageSize = value; firePropertyChange("preferPageSize", oldValue, preferPageSize); } /** * @return the pageBreaksVisible */ public boolean isPageBreaksVisible() { return pageBreaksVisible; } /** * @param value * the pageBreaksVisible to set */ public void setPageBreaksVisible(boolean value) { boolean oldValue = pageBreaksVisible; pageBreaksVisible = value; firePropertyChange("pageBreaksVisible", oldValue, pageBreaksVisible); } /** * @return the pageBreakColor */ public Color getPageBreakColor() { return pageBreakColor; } /** * @param pageBreakColor the pageBreakColor to set */ public void setPageBreakColor(Color pageBreakColor) { this.pageBreakColor = pageBreakColor; } /** * @param value * the horizontalPageCount to set */ public void setHorizontalPageCount(int value) { int oldValue = horizontalPageCount; horizontalPageCount = value; firePropertyChange("horizontalPageCount", oldValue, horizontalPageCount); } /** * */ public int getHorizontalPageCount() { return horizontalPageCount; } /** * @param value * the verticalPageCount to set */ public void setVerticalPageCount(int value) { int oldValue = verticalPageCount; verticalPageCount = value; firePropertyChange("verticalPageCount", oldValue, verticalPageCount); } /** * */ public int getVerticalPageCount() { return verticalPageCount; } /** * @return the centerPage */ public boolean isCenterPage() { return centerPage; } /** * zoomAndCenter should be called if this is set to true. * * @param value * the centerPage to set */ public void setCenterPage(boolean value) { boolean oldValue = centerPage; centerPage = value; firePropertyChange("centerPage", oldValue, centerPage); } /** * @return the pageBackgroundColor */ public Color getPageBackgroundColor() { return pageBackgroundColor; } /** * Sets the color that appears behind the page. * * @param value * the pageBackgroundColor to set */ public void setPageBackgroundColor(Color value) { Color oldValue = pageBackgroundColor; pageBackgroundColor = value; firePropertyChange("pageBackgroundColor", oldValue, pageBackgroundColor); } /** * @return the pageShadowColor */ public Color getPageShadowColor() { return pageShadowColor; } /** * @param value * the pageShadowColor to set */ public void setPageShadowColor(Color value) { Color oldValue = pageShadowColor; pageShadowColor = value; firePropertyChange("pageShadowColor", oldValue, pageShadowColor); } /** * @return the pageShadowColor */ public Color getPageBorderColor() { return pageBorderColor; } /** * @param value * the pageBorderColor to set */ public void setPageBorderColor(Color value) { Color oldValue = pageBorderColor; pageBorderColor = value; firePropertyChange("pageBorderColor", oldValue, pageBorderColor); } /** * @return the keepSelectionVisibleOnZoom */ public boolean isKeepSelectionVisibleOnZoom() { return keepSelectionVisibleOnZoom; } /** * @param value * the keepSelectionVisibleOnZoom to set */ public void setKeepSelectionVisibleOnZoom(boolean value) { boolean oldValue = keepSelectionVisibleOnZoom; keepSelectionVisibleOnZoom = value; firePropertyChange("keepSelectionVisibleOnZoom", oldValue, keepSelectionVisibleOnZoom); } /** * @return the zoomFactor */ public double getZoomFactor() { return zoomFactor; } /** * @param value * the zoomFactor to set */ public void setZoomFactor(double value) { double oldValue = zoomFactor; zoomFactor = value; firePropertyChange("zoomFactor", oldValue, zoomFactor); } /** * @return the centerZoom */ public boolean isCenterZoom() { return centerZoom; } /** * @param value * the centerZoom to set */ public void setCenterZoom(boolean value) { boolean oldValue = centerZoom; centerZoom = value; firePropertyChange("centerZoom", oldValue, centerZoom); } /** * */ public void setZoomPolicy(int value) { int oldValue = zoomPolicy; zoomPolicy = value; if (zoomPolicy != ZOOM_POLICY_NONE) { zoom(zoomPolicy == ZOOM_POLICY_PAGE, true); } firePropertyChange("zoomPolicy", oldValue, zoomPolicy); } /** * */ public int getZoomPolicy() { return zoomPolicy; } /** * Callback to process an escape keystroke. * * @param e */ public void escape(KeyEvent e) { if (selectionCellsHandler != null) { selectionCellsHandler.reset(); } if (connectionHandler != null) { connectionHandler.reset(); } if (graphHandler != null) { graphHandler.reset(); } if (cellEditor != null) { cellEditor.stopEditing(true); } } /** * Clones and inserts the given cells into the graph using the move method * and returns the inserted cells. This shortcut is used if cells are * inserted via datatransfer. */ public Object[] importCells(Object[] cells, double dx, double dy, Object target, Point location) { return graph.moveCells(cells, dx, dy, true, target, location); } /** * Refreshes the display and handles. */ public void refresh() { graph.refresh(); selectionCellsHandler.refresh(); } /** * Returns an mxPoint representing the given event in the unscaled, * non-translated coordinate space and applies the grid. */ public mxPoint getPointForEvent(MouseEvent e) { return getPointForEvent(e, true); } /** * Returns an mxPoint representing the given event in the unscaled, * non-translated coordinate space and applies the grid. */ public mxPoint getPointForEvent(MouseEvent e, boolean addOffset) { double s = graph.getView().getScale(); mxPoint tr = graph.getView().getTranslate(); double off = (addOffset) ? graph.getGridSize() / 2 : 0; double x = graph.snap(e.getX() / s - tr.getX() - off); double y = graph.snap(e.getY() / s - tr.getY() - off); return new mxPoint(x, y); } /** * */ public void startEditing() { startEditingAtCell(null); } /** * */ public void startEditingAtCell(Object cell) { startEditingAtCell(cell, null); } /** * */ public void startEditingAtCell(Object cell, EventObject evt) { if (cell == null) { cell = graph.getSelectionCell(); if (cell != null && !graph.isCellEditable(cell)) { cell = null; } } if (cell != null) { eventSource.fireEvent(new mxEventObject(mxEvent.START_EDITING, "cell", cell, "event", evt)); cellEditor.startEditing(cell, evt); } } /** * */ public String getEditingValue(Object cell, EventObject trigger) { return graph.convertValueToString(cell); } /** * */ public void stopEditing(boolean cancel) { cellEditor.stopEditing(cancel); } /** * Sets the label of the specified cell to the given value using * mxGraph.cellLabelChanged and fires mxEvent.LABEL_CHANGED while the * transaction is in progress. Returns the cell whose label was changed. * * @param cell * Cell whose label should be changed. * @param value * New value of the label. * @param evt * Optional event that triggered the change. */ public Object labelChanged(Object cell, Object value, EventObject evt) { mxIGraphModel model = graph.getModel(); model.beginUpdate(); try { graph.cellLabelChanged(cell, value, graph.isAutoSizeCell(cell)); eventSource.fireEvent(new mxEventObject(mxEvent.LABEL_CHANGED, "cell", cell, "value", value, "event", evt)); } finally { model.endUpdate(); } return cell; } /** * Returns the (unscaled) preferred size for the current page format (scaled * by pageScale). */ protected Dimension getPreferredSizeForPage() { return new Dimension((int) Math.round(pageFormat.getWidth() * pageScale * horizontalPageCount), (int) Math.round(pageFormat.getHeight() * pageScale * verticalPageCount)); } /** * Returns the vertical border between the page and the control. */ public int getVerticalPageBorder() { return (int) Math.round(pageFormat.getWidth() * pageScale); } /** * Returns the horizontal border between the page and the control. */ public int getHorizontalPageBorder() { return (int) Math.round(0.5 * pageFormat.getHeight() * pageScale); } /** * Returns the scaled preferred size for the current graph. */ protected Dimension getScaledPreferredSizeForGraph() { mxRectangle bounds = graph.getGraphBounds(); int border = graph.getBorder(); return new Dimension( (int) Math.round(bounds.getX() + bounds.getWidth()) + border + 1, (int) Math.round(bounds.getY() + bounds.getHeight()) + border + 1); } /** * Should be called by a hook inside mxGraphView/mxGraph */ protected mxPoint getPageTranslate(double scale) { Dimension d = getPreferredSizeForPage(); Dimension bd = new Dimension(d); if (!preferPageSize) { bd.width += 2 * getHorizontalPageBorder(); bd.height += 2 * getVerticalPageBorder(); } double width = Math.max(bd.width, (getViewport().getWidth() - 8) / scale); double height = Math.max(bd.height, (getViewport().getHeight() - 8) / scale); double dx = Math.max(0, (width - d.width) / 2); double dy = Math.max(0, (height - d.height) / 2); return new mxPoint(dx, dy); } /** * Invoked after the component was resized to update the zoom if the zoom * policy is not none and/or update the translation of the diagram if * pageVisible and centerPage are true. */ public void zoomAndCenter() { if (zoomPolicy != ZOOM_POLICY_NONE) { // Centers only on the initial zoom call zoom(zoomPolicy == ZOOM_POLICY_PAGE, centerOnResize || zoomPolicy == ZOOM_POLICY_PAGE); centerOnResize = false; } else if (pageVisible && centerPage) { mxPoint translate = getPageTranslate(graph.getView().getScale()); graph.getView().setTranslate(translate); } else { getGraphControl().updatePreferredSize(); } } /** * Zooms into the graph by zoomFactor. */ public void zoomIn() { zoom(zoomFactor); } /** * Function: zoomOut * * Zooms out of the graph by <zoomFactor>. */ public void zoomOut() { zoom(1 / zoomFactor); } /** * */ public void zoom(double factor) { mxGraphView view = graph.getView(); double newScale = (double) ((int) (view.getScale() * 100 * factor)) / 100; if (newScale != view.getScale() && newScale > 0.04) { mxPoint translate = (pageVisible && centerPage) ? getPageTranslate(newScale) : new mxPoint(); graph.getView().scaleAndTranslate(newScale, translate.getX(), translate.getY()); if (keepSelectionVisibleOnZoom && !graph.isSelectionEmpty()) { getGraphControl().scrollRectToVisible( view.getBoundingBox(graph.getSelectionCells()) .getRectangle()); } else { maintainScrollBar(true, factor, centerZoom); maintainScrollBar(false, factor, centerZoom); } } } /** * */ public void zoomTo(final double newScale, final boolean center) { mxGraphView view = graph.getView(); final double scale = view.getScale(); mxPoint translate = (pageVisible && centerPage) ? getPageTranslate(newScale) : new mxPoint(); graph.getView().scaleAndTranslate(newScale, translate.getX(), translate.getY()); // Causes two repaints on the scrollpane, namely one for the scale // change with the new preferred size and one for the change of // the scrollbar position. The latter cannot be done immediately // because the scrollbar keeps the value <= max - extent, and if // max is changed the value change will trigger a syncScrollPane // WithViewport in BasicScrollPaneUI, which will update the value // for the previous maximum (ie. it must be invoked later). SwingUtilities.invokeLater(new Runnable() { public void run() { maintainScrollBar(true, newScale / scale, center); maintainScrollBar(false, newScale / scale, center); } }); } /** * Function: zoomActual * * Resets the zoom and panning in the view. */ public void zoomActual() { mxPoint translate = (pageVisible && centerPage) ? getPageTranslate(1) : new mxPoint(); graph.getView() .scaleAndTranslate(1, translate.getX(), translate.getY()); if (isPageVisible()) { // Causes two repaints, see zoomTo for more details SwingUtilities.invokeLater(new Runnable() { public void run() { Dimension pageSize = getPreferredSizeForPage(); if (getViewport().getWidth() > pageSize.getWidth()) { scrollToCenter(true); } else { JScrollBar scrollBar = getHorizontalScrollBar(); if (scrollBar != null) { scrollBar.setValue((scrollBar.getMaximum() / 3) - 4); } } if (getViewport().getHeight() > pageSize.getHeight()) { scrollToCenter(false); } else { JScrollBar scrollBar = getVerticalScrollBar(); if (scrollBar != null) { scrollBar.setValue((scrollBar.getMaximum() / 4) - 4); } } } }); } } /** * */ public void zoom(final boolean page, final boolean center) { if (pageVisible && !zooming) { zooming = true; try { int off = (getPageShadowColor() != null) ? 8 : 0; // Adds some extra space for the shadow and border double width = getViewport().getWidth() - off; double height = getViewport().getHeight() - off; Dimension d = getPreferredSizeForPage(); double pageWidth = d.width; double pageHeight = d.height; double scaleX = width / pageWidth; double scaleY = (page) ? height / pageHeight : scaleX; // Rounds the new scale to 5% steps final double newScale = (double) ((int) (Math.min(scaleX, scaleY) * 20)) / 20; if (newScale > 0) { mxGraphView graphView = graph.getView(); final double scale = graphView.getScale(); mxPoint translate = (centerPage) ? getPageTranslate(newScale) : new mxPoint(); graphView.scaleAndTranslate(newScale, translate.getX(), translate.getY()); // Causes two repaints, see zoomTo for more details final double factor = newScale / scale; SwingUtilities.invokeLater(new Runnable() { public void run() { if (center) { if (page) { scrollToCenter(true); scrollToCenter(false); } else { scrollToCenter(true); maintainScrollBar(false, factor, false); } } else if (factor != 1) { maintainScrollBar(true, factor, false); maintainScrollBar(false, factor, false); } } }); } } finally { zooming = false; } } } /** * */ protected void maintainScrollBar(boolean horizontal, double factor, boolean center) { JScrollBar scrollBar = (horizontal) ? getHorizontalScrollBar() : getVerticalScrollBar(); if (scrollBar != null) { BoundedRangeModel model = scrollBar.getModel(); int newValue = (int) Math.round(model.getValue() * factor) + (int) Math.round((center) ? (model.getExtent() * (factor - 1) / 2) : 0); model.setValue(newValue); } } /** * */ public void scrollToCenter(boolean horizontal) { JScrollBar scrollBar = (horizontal) ? getHorizontalScrollBar() : getVerticalScrollBar(); if (scrollBar != null) { final BoundedRangeModel model = scrollBar.getModel(); final int newValue = ((model.getMaximum()) / 2) - model.getExtent() / 2; model.setValue(newValue); } } /** * Scrolls the graph so that it shows the given cell. * * @param cell */ public void scrollCellToVisible(Object cell) { scrollCellToVisible(cell, false); } /** * Scrolls the graph so that it shows the given cell. * * @param cell */ public void scrollCellToVisible(Object cell, boolean center) { mxCellState state = graph.getView().getState(cell); if (state != null) { mxRectangle bounds = state; if (center) { bounds = (mxRectangle) bounds.clone(); bounds.setX(bounds.getCenterX() - getWidth() / 2); bounds.setWidth(getWidth()); bounds.setY(bounds.getCenterY() - getHeight() / 2); bounds.setHeight(getHeight()); } getGraphControl().scrollRectToVisible(bounds.getRectangle()); } } /** * * @param x * @param y * @return Returns the cell at the given location. */ public Object getCellAt(int x, int y) { return getCellAt(x, y, true); } /** * * @param x * @param y * @param hitSwimlaneContent * @return Returns the cell at the given location. */ public Object getCellAt(int x, int y, boolean hitSwimlaneContent) { return getCellAt(x, y, hitSwimlaneContent, null); } /** * Returns the bottom-most cell that intersects the given point (x, y) in * the cell hierarchy starting at the given parent. * * @param x * X-coordinate of the location to be checked. * @param y * Y-coordinate of the location to be checked. * @param parent * <mxCell> that should be used as the root of the recursion. * Default is <defaultParent>. * @return Returns the child at the given location. */ public Object getCellAt(int x, int y, boolean hitSwimlaneContent, Object parent) { if (parent == null) { parent = graph.getDefaultParent(); } if (parent != null) { Point previousTranslate = canvas.getTranslate(); double previousScale = canvas.getScale(); try { canvas.setScale(graph.getView().getScale()); canvas.setTranslate(0, 0); mxIGraphModel model = graph.getModel(); mxGraphView view = graph.getView(); Rectangle hit = new Rectangle(x, y, 1, 1); int childCount = model.getChildCount(parent); for (int i = childCount - 1; i >= 0; i--) { Object cell = model.getChildAt(parent, i); Object result = getCellAt(x, y, hitSwimlaneContent, cell); if (result != null) { return result; } else if (graph.isCellVisible(cell)) { mxCellState state = view.getState(cell); if (state != null && canvas.intersects(this, hit, state) && (!graph.isSwimlane(cell) || hitSwimlaneContent || (transparentSwimlaneContent && !canvas .hitSwimlaneContent(this, state, x, y)))) { return cell; } } } } finally { canvas.setScale(previousScale); canvas.setTranslate(previousTranslate.x, previousTranslate.y); } } return null; } /** * */ public void setSwimlaneSelectionEnabled(boolean value) { boolean oldValue = swimlaneSelectionEnabled; swimlaneSelectionEnabled = value; firePropertyChange("swimlaneSelectionEnabled", oldValue, swimlaneSelectionEnabled); } /** * */ public boolean isSwimlaneSelectionEnabled() { return swimlaneSelectionEnabled; } /** * */ public Object[] selectRegion(Rectangle rect, MouseEvent e) { Object[] cells = getCells(rect); if (cells.length > 0) { selectCellsForEvent(cells, e); } else if (!graph.isSelectionEmpty() && !e.isConsumed()) { graph.clearSelection(); } return cells; } /** * Returns the cells inside the given rectangle. * * @return Returns the cells inside the given rectangle. */ public Object[] getCells(Rectangle rect) { return getCells(rect, null); } /** * Returns the children of the given parent that are contained in the given * rectangle (x, y, width, height). The result is added to the optional * result array, which is returned from the function. If no result array is * specified then a new array is created and returned. * * @return Returns the children inside the given rectangle. */ public Object[] getCells(Rectangle rect, Object parent) { Collection<Object> result = new ArrayList<Object>(); if (rect.width > 0 || rect.height > 0) { if (parent == null) { parent = graph.getDefaultParent(); } if (parent != null) { Point previousTranslate = canvas.getTranslate(); double previousScale = canvas.getScale(); try { canvas.setScale(graph.getView().getScale()); canvas.setTranslate(0, 0); mxIGraphModel model = graph.getModel(); mxGraphView view = graph.getView(); int childCount = model.getChildCount(parent); for (int i = 0; i < childCount; i++) { Object cell = model.getChildAt(parent, i); mxCellState state = view.getState(cell); if (graph.isCellVisible(cell) && state != null) { if (canvas.contains(this, rect, state)) { result.add(cell); } else { result.addAll(Arrays .asList(getCells(rect, cell))); } } } } finally { canvas.setScale(previousScale); canvas.setTranslate(previousTranslate.x, previousTranslate.y); } } } return result.toArray(); } /** * Selects the cells for the given event. */ public void selectCellsForEvent(Object[] cells, MouseEvent event) { if (isToggleEvent(event)) { graph.addSelectionCells(cells); } else { graph.setSelectionCells(cells); } } /** * Selects the cell for the given event. */ public void selectCellForEvent(Object cell, MouseEvent e) { boolean isSelected = graph.isCellSelected(cell); if (isToggleEvent(e)) { if (isSelected) { graph.removeSelectionCell(cell); } else { graph.addSelectionCell(cell); } } else if (!isSelected || graph.getSelectionCount() != 1) { graph.setSelectionCell(cell); } } /** * Returns true if the absolute value of one of the given parameters is * greater than the tolerance. */ public boolean isSignificant(double dx, double dy) { return Math.abs(dx) > tolerance || Math.abs(dy) > tolerance; } /** * Returns the icon used to display the collapsed state of the specified * cell state. This returns null for all edges. */ public ImageIcon getFoldingIcon(mxCellState state) { if (state != null && isFoldingEnabled() && !getGraph().getModel().isEdge(state.getCell())) { Object cell = state.getCell(); boolean tmp = graph.isCellCollapsed(cell); if (graph.isCellFoldable(cell, !tmp)) { return (tmp) ? collapsedIcon : expandedIcon; } } return null; } /** * */ public Rectangle getFoldingIconBounds(mxCellState state, ImageIcon icon) { mxIGraphModel model = graph.getModel(); boolean isEdge = model.isEdge(state.getCell()); double scale = getGraph().getView().getScale(); int x = (int) Math.round(state.getX() + 4 * scale); int y = (int) Math.round(state.getY() + 4 * scale); int w = (int) Math.max(8, icon.getIconWidth() * scale); int h = (int) Math.max(8, icon.getIconHeight() * scale); if (isEdge) { mxPoint pt = graph.getView().getPoint(state); x = (int) pt.getX() - w / 2; y = (int) pt.getY() - h / 2; } return new Rectangle(x, y, w, h); } /** * */ public boolean hitFoldingIcon(Object cell, int x, int y) { if (cell != null) { mxIGraphModel model = graph.getModel(); // Draws the collapse/expand icons boolean isEdge = model.isEdge(cell); if (foldingEnabled && (model.isVertex(cell) || isEdge)) { mxCellState state = graph.getView().getState(cell); if (state != null) { ImageIcon icon = getFoldingIcon(state); if (icon != null) { return getFoldingIconBounds(state, icon).contains(x, y); } } } } return false; } /** * * @param enabled */ public void setToolTips(boolean enabled) { if (enabled) { ToolTipManager.sharedInstance().registerComponent(graphControl); } else { ToolTipManager.sharedInstance().unregisterComponent(graphControl); } } /** * */ public boolean isConnectable() { return connectionHandler.isEnabled(); } /** * @param connectable */ public void setConnectable(boolean connectable) { connectionHandler.setEnabled(connectable); } /** * */ public boolean isPanning() { return panningHandler.isEnabled(); } /** * @param enabled */ public void setPanning(boolean enabled) { panningHandler.setEnabled(enabled); } /** * @return the autoScroll */ public boolean isAutoScroll() { return autoScroll; } /** * @param value * the autoScroll to set */ public void setAutoScroll(boolean value) { autoScroll = value; } /** * @return the autoExtend */ public boolean isAutoExtend() { return autoExtend; } /** * @param value * the autoExtend to set */ public void setAutoExtend(boolean value) { autoExtend = value; } /** * @return the escapeEnabled */ public boolean isEscapeEnabled() { return escapeEnabled; } /** * @param value * the escapeEnabled to set */ public void setEscapeEnabled(boolean value) { boolean oldValue = escapeEnabled; escapeEnabled = value; firePropertyChange("escapeEnabled", oldValue, escapeEnabled); } /** * @return the escapeEnabled */ public boolean isInvokesStopCellEditing() { return invokesStopCellEditing; } /** * @param value * the invokesStopCellEditing to set */ public void setInvokesStopCellEditing(boolean value) { boolean oldValue = invokesStopCellEditing; invokesStopCellEditing = value; firePropertyChange("invokesStopCellEditing", oldValue, invokesStopCellEditing); } /** * @return the enterStopsCellEditing */ public boolean isEnterStopsCellEditing() { return enterStopsCellEditing; } /** * @param value * the enterStopsCellEditing to set */ public void setEnterStopsCellEditing(boolean value) { boolean oldValue = enterStopsCellEditing; enterStopsCellEditing = value; firePropertyChange("enterStopsCellEditing", oldValue, enterStopsCellEditing); } /** * @return the dragEnabled */ public boolean isDragEnabled() { return dragEnabled; } /** * @param value * the dragEnabled to set */ public void setDragEnabled(boolean value) { boolean oldValue = dragEnabled; dragEnabled = value; firePropertyChange("dragEnabled", oldValue, dragEnabled); } /** * @return the gridVisible */ public boolean isGridVisible() { return gridVisible; } /** * Fires a property change event for <code>gridVisible</code>. * * @param value * the gridVisible to set */ public void setGridVisible(boolean value) { boolean oldValue = gridVisible; gridVisible = value; firePropertyChange("gridVisible", oldValue, gridVisible); } /** * @return the gridVisible */ public boolean isAntiAlias() { return antiAlias; } /** * Fires a property change event for <code>antiAlias</code>. * * @param value * the antiAlias to set */ public void setAntiAlias(boolean value) { boolean oldValue = antiAlias; antiAlias = value; firePropertyChange("antiAlias", oldValue, antiAlias); } /** * @return the gridVisible */ public boolean isTextAntiAlias() { return antiAlias; } /** * Fires a property change event for <code>textAntiAlias</code>. * * @param value * the textAntiAlias to set */ public void setTextAntiAlias(boolean value) { boolean oldValue = textAntiAlias; textAntiAlias = value; firePropertyChange("textAntiAlias", oldValue, textAntiAlias); } /** * */ public float getPreviewAlpha() { return previewAlpha; } /** * */ public void setPreviewAlpha(float value) { float oldValue = previewAlpha; previewAlpha = value; firePropertyChange("previewAlpha", oldValue, previewAlpha); } /** * @return the tripleBuffered */ public boolean isTripleBuffered() { return tripleBuffered; } /** * Hook for dynamic triple buffering condition. */ public boolean isForceTripleBuffered() { // LATER: Dynamic condition (cell density) to use triple // buffering for a large number of cells on a small rect return false; } /** * @param value * the tripleBuffered to set */ public void setTripleBuffered(boolean value) { boolean oldValue = tripleBuffered; tripleBuffered = value; firePropertyChange("tripleBuffered", oldValue, tripleBuffered); } /** * @return the gridColor */ public Color getGridColor() { return gridColor; } /** * Fires a property change event for <code>gridColor</code>. * * @param value * the gridColor to set */ public void setGridColor(Color value) { Color oldValue = gridColor; gridColor = value; firePropertyChange("gridColor", oldValue, gridColor); } /** * @return the gridStyle */ public int getGridStyle() { return gridStyle; } /** * Fires a property change event for <code>gridStyle</code>. * * @param value * the gridStyle to set */ public void setGridStyle(int value) { int oldValue = gridStyle; gridStyle = value; firePropertyChange("gridStyle", oldValue, gridStyle); } /** * Returns importEnabled. */ public boolean isImportEnabled() { return importEnabled; } /** * Sets importEnabled. */ public void setImportEnabled(boolean value) { boolean oldValue = importEnabled; importEnabled = value; firePropertyChange("importEnabled", oldValue, importEnabled); } /** * Returns all cells which may be imported via datatransfer. */ public Object[] getImportableCells(Object[] cells) { return mxGraphModel.filterCells(cells, new Filter() { public boolean filter(Object cell) { return canImportCell(cell); } }); } /** * Returns true if the given cell can be imported via datatransfer. This * returns importEnabled. */ public boolean canImportCell(Object cell) { return isImportEnabled(); } /** * @return the exportEnabled */ public boolean isExportEnabled() { return exportEnabled; } /** * @param value * the exportEnabled to set */ public void setExportEnabled(boolean value) { boolean oldValue = exportEnabled; exportEnabled = value; firePropertyChange("exportEnabled", oldValue, exportEnabled); } /** * Returns all cells which may be exported via datatransfer. */ public Object[] getExportableCells(Object[] cells) { return mxGraphModel.filterCells(cells, new Filter() { public boolean filter(Object cell) { return canExportCell(cell); } }); } /** * Returns true if the given cell can be exported via datatransfer. */ public boolean canExportCell(Object cell) { return isExportEnabled(); } /** * @return the foldingEnabled */ public boolean isFoldingEnabled() { return foldingEnabled; } /** * @param value * the foldingEnabled to set */ public void setFoldingEnabled(boolean value) { boolean oldValue = foldingEnabled; foldingEnabled = value; firePropertyChange("foldingEnabled", oldValue, foldingEnabled); } /** * */ public boolean isEditEvent(MouseEvent e) { return (e != null) ? e.getClickCount() == 2 : false; } /** * * @param event * @return Returns true if the given event should toggle selected cells. */ public boolean isCloneEvent(MouseEvent event) { return (event != null) ? event.isControlDown() : false; } /** * * @param event * @return Returns true if the given event should toggle selected cells. */ public boolean isToggleEvent(MouseEvent event) { // NOTE: IsMetaDown always returns true for right-clicks on the Mac, so // toggle selection for left mouse buttons requires CMD key to be pressed, // but toggle for right mouse buttons requires CTRL to be pressed. return (event != null) ? ((mxUtils.IS_MAC) ? ((SwingUtilities .isLeftMouseButton(event) && event.isMetaDown()) || (SwingUtilities .isRightMouseButton(event) && event.isControlDown())) : event.isControlDown()) : false; } /** * * @param event * @return Returns true if the given event allows the grid to be applied. */ public boolean isGridEnabledEvent(MouseEvent event) { return (event != null) ? !event.isAltDown() : false; } /** * Note: This is not used during drag and drop operations due to limitations * of the underlying API. To enable this for move operations set dragEnabled * to false. * * @param event * @return Returns true if the given event is a panning event. */ public boolean isPanningEvent(MouseEvent event) { return (event != null) ? event.isShiftDown() && event.isControlDown() : false; } /** * Note: This is not used during drag and drop operations due to limitations * of the underlying API. To enable this for move operations set dragEnabled * to false. * * @param event * @return Returns true if the given event is constrained. */ public boolean isConstrainedEvent(MouseEvent event) { return (event != null) ? event.isShiftDown() : false; } /** * Note: This is not used during drag and drop operations due to limitations * of the underlying API. To enable this for move operations set dragEnabled * to false. * * @param event * @return Returns true if the given event is constrained. */ public boolean isForceMarqueeEvent(MouseEvent event) { return (event != null) ? event.isAltDown() : false; } /** * */ public mxPoint snapScaledPoint(mxPoint pt) { return snapScaledPoint(pt, 0, 0); } /** * */ public mxPoint snapScaledPoint(mxPoint pt, double dx, double dy) { if (pt != null) { double scale = graph.getView().getScale(); mxPoint trans = graph.getView().getTranslate(); pt.setX((graph.snap(pt.getX() / scale - trans.getX() + dx / scale) + trans .getX()) * scale - dx); pt.setY((graph.snap(pt.getY() / scale - trans.getY() + dy / scale) + trans .getY()) * scale - dy); } return pt; } /** * Prints the specified page on the specified graphics using * <code>pageFormat</code> for the page format. * * @param g * The graphics to paint the graph on. * @param printFormat * The page format to use for printing. * @param page * The page to print * @return Returns {@link Printable#PAGE_EXISTS} or * {@link Printable#NO_SUCH_PAGE}. */ public int print(Graphics g, PageFormat printFormat, int page) { int result = NO_SUCH_PAGE; // Disables double-buffering before printing RepaintManager currentManager = RepaintManager .currentManager(mxGraphComponent.this); currentManager.setDoubleBufferingEnabled(false); // Gets the current state of the view mxGraphView view = graph.getView(); // Stores the old state of the view boolean eventsEnabled = view.isEventsEnabled(); mxPoint translate = view.getTranslate(); // Disables firing of scale events so that there is no // repaint or update of the original graph while pages // are being printed view.setEventsEnabled(false); // Uses the view to create temporary cell states for each cell mxTemporaryCellStates tempStates = new mxTemporaryCellStates(view, 1 / pageScale); try { view.setTranslate(new mxPoint(0, 0)); mxGraphics2DCanvas canvas = createCanvas(); canvas.setGraphics((Graphics2D) g); canvas.setScale(1 / pageScale); view.revalidate(); mxRectangle graphBounds = graph.getGraphBounds(); Dimension pSize = new Dimension((int) Math.ceil(graphBounds.getX() + graphBounds.getWidth()) + 1, (int) Math.ceil(graphBounds .getY() + graphBounds.getHeight()) + 1); int w = (int) (printFormat.getImageableWidth()); int h = (int) (printFormat.getImageableHeight()); int cols = (int) Math.max( Math.ceil((double) (pSize.width - 5) / (double) w), 1); int rows = (int) Math.max( Math.ceil((double) (pSize.height - 5) / (double) h), 1); if (page < cols * rows) { int dx = (int) ((page % cols) * printFormat.getImageableWidth()); int dy = (int) (Math.floor(page / cols) * printFormat .getImageableHeight()); g.translate(-dx + (int) printFormat.getImageableX(), -dy + (int) printFormat.getImageableY()); g.setClip(dx, dy, (int) (dx + printFormat.getWidth()), (int) (dy + printFormat.getHeight())); graph.drawGraph(canvas); result = PAGE_EXISTS; } } finally { view.setTranslate(translate); tempStates.destroy(); view.setEventsEnabled(eventsEnabled); // Enables double-buffering after printing currentManager.setDoubleBufferingEnabled(true); } return result; } /** * */ public mxInteractiveCanvas getCanvas() { return canvas; } /** * */ public BufferedImage getTripleBuffer() { return tripleBuffer; } /** * Hook for subclassers to replace the graphics canvas for rendering and and * printing. This must be overridden to return a custom canvas if there are * any custom shapes. */ public mxInteractiveCanvas createCanvas() { // NOTE: http://forum.jgraph.com/questions/3354/ reports that we should not // pass image observer here as it will cause JVM to enter infinite loop. return new mxInteractiveCanvas(); } /** * * @param state * Cell state for which a handler should be created. * @return Returns the handler to be used for the given cell state. */ public mxCellHandler createHandler(mxCellState state) { if (graph.getModel().isVertex(state.getCell())) { return new mxVertexHandler(this, state); } else if (graph.getModel().isEdge(state.getCell())) { mxEdgeStyleFunction style = graph.getView().getEdgeStyle(state, null, null, null); if (graph.isLoop(state) || style == mxEdgeStyle.ElbowConnector || style == mxEdgeStyle.SideToSide || style == mxEdgeStyle.TopToBottom) { return new mxElbowEdgeHandler(this, state); } return new mxEdgeHandler(this, state); } return new mxCellHandler(this, state); } // // Heavyweights // /** * Hook for subclassers to create the array of heavyweights for the given * state. */ public Component[] createComponents(mxCellState state) { return null; } /** * */ public void insertComponent(mxCellState state, Component c) { getGraphControl().add(c, 0); } /** * */ public void removeComponent(Component c, Object cell) { if (c.getParent() != null) { c.getParent().remove(c); } } /** * */ public void updateComponent(mxCellState state, Component c) { int x = (int) state.getX(); int y = (int) state.getY(); int width = (int) state.getWidth(); int height = (int) state.getHeight(); Dimension s = c.getMinimumSize(); if (s.width > width) { x -= (s.width - width) / 2; width = s.width; } if (s.height > height) { y -= (s.height - height) / 2; height = s.height; } c.setBounds(x, y, width, height); } /** * */ public void updateComponents() { Object root = graph.getModel().getRoot(); Hashtable<Object, Component[]> result = updateComponents(root); // Components now contains the mappings which are no // longer used, the result contains the new mappings removeAllComponents(components); components = result; if (!overlays.isEmpty()) { Hashtable<Object, mxICellOverlay[]> result2 = updateCellOverlays(root); // Overlays now contains the mappings from cells which // are no longer in the model, the result contains the // mappings from cells which still exists, regardless // from whether a state exists for a particular cell removeAllOverlays(overlays); overlays = result2; } } /** * */ public void removeAllComponents(Hashtable<Object, Component[]> map) { Iterator<Map.Entry<Object, Component[]>> it = map.entrySet().iterator(); while (it.hasNext()) { Map.Entry<Object, Component[]> entry = it.next(); Component[] c = entry.getValue(); for (int i = 0; i < c.length; i++) { removeComponent(c[i], entry.getKey()); } } } /** * */ public void removeAllOverlays(Hashtable<Object, mxICellOverlay[]> map) { Iterator<Map.Entry<Object, mxICellOverlay[]>> it = map.entrySet() .iterator(); while (it.hasNext()) { Map.Entry<Object, mxICellOverlay[]> entry = it.next(); mxICellOverlay[] c = entry.getValue(); for (int i = 0; i < c.length; i++) { removeCellOverlayComponent(c[i], entry.getKey()); } } } /** * */ public Hashtable<Object, Component[]> updateComponents(Object cell) { Hashtable<Object, Component[]> result = new Hashtable<Object, Component[]>(); Component[] c = components.remove(cell); mxCellState state = getGraph().getView().getState(cell); if (state != null) { if (c == null) { c = createComponents(state); if (c != null) { for (int i = 0; i < c.length; i++) { insertComponent(state, c[i]); } } } if (c != null) { result.put(cell, c); for (int i = 0; i < c.length; i++) { updateComponent(state, c[i]); } } } // Puts the component back into the map so that it will be removed else if (c != null) { components.put(cell, c); } int childCount = getGraph().getModel().getChildCount(cell); for (int i = 0; i < childCount; i++) { result.putAll(updateComponents(getGraph().getModel().getChildAt( cell, i))); } return result; } // // Validation and overlays // /** * Validates the graph by validating each descendant of the given cell or * the root of the model. Context is an object that contains the validation * state for the complete validation run. The validation errors are attached * to their cells using <setWarning>. This function returns true if no * validation errors exist in the graph. */ public String validateGraph() { return validateGraph(graph.getModel().getRoot(), new Hashtable<Object, Object>()); } /** * Validates the graph by validating each descendant of the given cell or * the root of the model. Context is an object that contains the validation * state for the complete validation run. The validation errors are attached * to their cells using <setWarning>. This function returns true if no * validation errors exist in the graph. * * @param cell * Cell to start the validation recursion. * @param context * Object that represents the global validation state. */ public String validateGraph(Object cell, Hashtable<Object, Object> context) { mxIGraphModel model = graph.getModel(); mxGraphView view = graph.getView(); boolean isValid = true; int childCount = model.getChildCount(cell); for (int i = 0; i < childCount; i++) { Object tmp = model.getChildAt(cell, i); Hashtable<Object, Object> ctx = context; if (graph.isValidRoot(tmp)) { ctx = new Hashtable<Object, Object>(); } String warn = validateGraph(tmp, ctx); if (warn != null) { String html = warn.replaceAll("\n", "<br>"); int len = html.length(); setCellWarning(tmp, html.substring(0, Math.max(0, len - 4))); } else { setCellWarning(tmp, null); } isValid = isValid && warn == null; } StringBuffer warning = new StringBuffer(); // Adds error for invalid children if collapsed (children invisible) if (graph.isCellCollapsed(cell) && !isValid) { warning.append(mxResources.get("containsValidationErrors", "Contains Validation Errors") + "\n"); } // Checks edges and cells using the defined multiplicities if (model.isEdge(cell)) { String tmp = graph.getEdgeValidationError(cell, model.getTerminal(cell, true), model.getTerminal(cell, false)); if (tmp != null) { warning.append(tmp); } } else { String tmp = graph.getCellValidationError(cell); if (tmp != null) { warning.append(tmp); } } // Checks custom validation rules String err = graph.validateCell(cell, context); if (err != null) { warning.append(err); } // Updates the display with the warning icons before any potential // alerts are displayed if (model.getParent(cell) == null) { view.validate(); } return (warning.length() > 0 || !isValid) ? warning.toString() : null; } /** * Adds an overlay for the specified cell. This method fires an addoverlay * event and returns the new overlay. * * @param cell * Cell to add the overlay for. * @param overlay * Overlay to be added for the cell. */ public mxICellOverlay addCellOverlay(Object cell, mxICellOverlay overlay) { mxICellOverlay[] arr = getCellOverlays(cell); if (arr == null) { arr = new mxICellOverlay[] { overlay }; } else { mxICellOverlay[] arr2 = new mxICellOverlay[arr.length + 1]; System.arraycopy(arr, 0, arr2, 0, arr.length); arr2[arr.length] = overlay; arr = arr2; } overlays.put(cell, arr); mxCellState state = graph.getView().getState(cell); if (state != null) { updateCellOverlayComponent(state, overlay); } eventSource.fireEvent(new mxEventObject(mxEvent.ADD_OVERLAY, "cell", cell, "overlay", overlay)); return overlay; } /** * Returns the array of overlays for the given cell or null, if no overlays * are defined. * * @param cell * Cell whose overlays should be returned. */ public mxICellOverlay[] getCellOverlays(Object cell) { return overlays.get(cell); } /** * Removes and returns the given overlay from the given cell. This method * fires a remove overlay event. If no overlay is given, then all overlays * are removed using removeOverlays. * * @param cell * Cell whose overlay should be removed. * @param overlay * Optional overlay to be removed. */ public mxICellOverlay removeCellOverlay(Object cell, mxICellOverlay overlay) { if (overlay == null) { removeCellOverlays(cell); } else { mxICellOverlay[] arr = getCellOverlays(cell); if (arr != null) { // TODO: Use arraycopy from/to same array to speed this up List<mxICellOverlay> list = new ArrayList<mxICellOverlay>( Arrays.asList(arr)); if (list.remove(overlay)) { removeCellOverlayComponent(overlay, cell); } arr = list.toArray(new mxICellOverlay[list.size()]); overlays.put(cell, arr); } } return overlay; } /** * Removes all overlays from the given cell. This method fires a * removeoverlay event for each removed overlay and returns the array of * overlays that was removed from the cell. * * @param cell * Cell whose overlays should be removed. */ public mxICellOverlay[] removeCellOverlays(Object cell) { mxICellOverlay[] ovls = overlays.remove(cell); if (ovls != null) { // Removes the overlays from the cell hierarchy for (int i = 0; i < ovls.length; i++) { removeCellOverlayComponent(ovls[i], cell); } } return ovls; } /** * Notified when an overlay has been removed from the graph. This * implementation removes the given overlay from its parent if it is a * component inside a component hierarchy. */ protected void removeCellOverlayComponent(mxICellOverlay overlay, Object cell) { if (overlay instanceof Component) { Component comp = (Component) overlay; if (comp.getParent() != null) { comp.setVisible(false); comp.getParent().remove(comp); eventSource.fireEvent(new mxEventObject(mxEvent.REMOVE_OVERLAY, "cell", cell, "overlay", overlay)); } } } /** * Notified when an overlay has been removed from the graph. This * implementation removes the given overlay from its parent if it is a * component inside a component hierarchy. */ protected void updateCellOverlayComponent(mxCellState state, mxICellOverlay overlay) { if (overlay instanceof Component) { Component comp = (Component) overlay; if (comp.getParent() == null) { getGraphControl().add(comp, 0); } mxRectangle rect = overlay.getBounds(state); if (rect != null) { comp.setBounds(rect.getRectangle()); comp.setVisible(true); } else { comp.setVisible(false); } } } /** * Removes all overlays in the graph. */ public void clearCellOverlays() { clearCellOverlays(null); } /** * Removes all overlays in the graph for the given cell and all its * descendants. If no cell is specified then all overlays are removed from * the graph. This implementation uses removeOverlays to remove the overlays * from the individual cells. * * @param cell * Optional cell that represents the root of the subtree to * remove the overlays from. Default is the root in the model. */ public void clearCellOverlays(Object cell) { mxIGraphModel model = graph.getModel(); if (cell == null) { cell = model.getRoot(); } removeCellOverlays(cell); // Recursively removes all overlays from the children int childCount = model.getChildCount(cell); for (int i = 0; i < childCount; i++) { Object child = model.getChildAt(cell, i); clearCellOverlays(child); // recurse } } /** * Creates an overlay for the given cell using the warning and image or * warningImage and returns the new overlay. If the warning is null or a * zero length string, then all overlays are removed from the cell instead. * * @param cell * Cell whose warning should be set. * @param warning * String that represents the warning to be displayed. */ public mxICellOverlay setCellWarning(Object cell, String warning) { return setCellWarning(cell, warning, null, false); } /** * Creates an overlay for the given cell using the warning and image or * warningImage and returns the new overlay. If the warning is null or a * zero length string, then all overlays are removed from the cell instead. * * @param cell * Cell whose warning should be set. * @param warning * String that represents the warning to be displayed. * @param icon * Optional image to be used for the overlay. Default is * warningImageBasename. */ public mxICellOverlay setCellWarning(Object cell, String warning, ImageIcon icon) { return setCellWarning(cell, warning, icon, false); } /** * Creates an overlay for the given cell using the warning and image or * warningImage and returns the new overlay. If the warning is null or a * zero length string, then all overlays are removed from the cell instead. * * @param cell * Cell whose warning should be set. * @param warning * String that represents the warning to be displayed. * @param icon * Optional image to be used for the overlay. Default is * warningImageBasename. * @param select * Optional boolean indicating if a click on the overlay should * select the corresponding cell. Default is false. */ public mxICellOverlay setCellWarning(final Object cell, String warning, ImageIcon icon, boolean select) { if (warning != null && warning.length() > 0) { icon = (icon != null) ? icon : warningIcon; // Creates the overlay with the image and warning mxCellOverlay overlay = new mxCellOverlay(icon, warning); // Adds a handler for single mouseclicks to select the cell if (select) { overlay.addMouseListener(new MouseAdapter() { /** * Selects the associated cell in the graph */ public void mousePressed(MouseEvent e) { if (getGraph().isEnabled()) { getGraph().setSelectionCell(cell); } } }); overlay.setCursor(new Cursor(Cursor.HAND_CURSOR)); } // Sets and returns the overlay in the graph return addCellOverlay(cell, overlay); } else { removeCellOverlays(cell); } return null; } /** * Returns a hashtable with all entries from the overlays variable where a * cell still exists in the model. The entries are removed from the global * hashtable so that the remaining entries reflect those whose cell have * been removed from the model. If no state is available for a given cell * then its overlays are temporarly removed from the rendering control, but * kept in the result. */ public Hashtable<Object, mxICellOverlay[]> updateCellOverlays(Object cell) { Hashtable<Object, mxICellOverlay[]> result = new Hashtable<Object, mxICellOverlay[]>(); mxICellOverlay[] c = overlays.remove(cell); mxCellState state = getGraph().getView().getState(cell); if (c != null) { if (state != null) { for (int i = 0; i < c.length; i++) { updateCellOverlayComponent(state, c[i]); } } else { for (int i = 0; i < c.length; i++) { removeCellOverlayComponent(c[i], cell); } } result.put(cell, c); } int childCount = getGraph().getModel().getChildCount(cell); for (int i = 0; i < childCount; i++) { result.putAll(updateCellOverlays(getGraph().getModel().getChildAt( cell, i))); } return result; } /** * */ protected void paintBackground(Graphics g) { Rectangle clip = g.getClipBounds(); Rectangle rect = paintBackgroundPage(g); if (isPageVisible()) { g.clipRect(rect.x + 1, rect.y + 1, rect.width - 1, rect.height - 1); } // Paints the clipped background image paintBackgroundImage(g); // Paints the grid directly onto the graphics paintGrid(g); g.setClip(clip); } /** * */ protected Rectangle paintBackgroundPage(Graphics g) { mxPoint translate = graph.getView().getTranslate(); double scale = graph.getView().getScale(); int x0 = (int) Math.round(translate.getX() * scale) - 1; int y0 = (int) Math.round(translate.getY() * scale) - 1; Dimension d = getPreferredSizeForPage(); int w = (int) Math.round(d.width * scale) + 2; int h = (int) Math.round(d.height * scale) + 2; if (isPageVisible()) { // Draws the background behind the page Color c = getPageBackgroundColor(); if (c != null) { g.setColor(c); mxUtils.fillClippedRect(g, 0, 0, getGraphControl().getWidth(), getGraphControl().getHeight()); } // Draws the page drop shadow c = getPageShadowColor(); if (c != null) { g.setColor(c); mxUtils.fillClippedRect(g, x0 + w, y0 + 6, 6, h - 6); mxUtils.fillClippedRect(g, x0 + 8, y0 + h, w - 2, 6); } // Draws the page Color bg = getBackground(); if (getViewport().isOpaque()) { bg = getViewport().getBackground(); } g.setColor(bg); mxUtils.fillClippedRect(g, x0 + 1, y0 + 1, w, h); // Draws the page border c = getPageBorderColor(); if (c != null) { g.setColor(c); g.drawRect(x0, y0, w, h); } } if (isPageBreaksVisible() && (horizontalPageCount > 1 || verticalPageCount > 1)) { // Draws the pagebreaks // TODO: Use clipping Graphics2D g2 = (Graphics2D) g; Stroke previousStroke = g2.getStroke(); g2.setStroke(new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, new float[] { 1, 2 }, 0)); g2.setColor(pageBreakColor); for (int i = 1; i <= horizontalPageCount - 1; i++) { int dx = i * w / horizontalPageCount; g2.drawLine(x0 + dx, y0 + 1, x0 + dx, y0 + h); } for (int i = 1; i <= verticalPageCount - 1; i++) { int dy = i * h / verticalPageCount; g2.drawLine(x0 + 1, y0 + dy, x0 + w, y0 + dy); } // Restores the graphics g2.setStroke(previousStroke); } return new Rectangle(x0, y0, w, h); } /** * */ protected void paintBackgroundImage(Graphics g) { if (backgroundImage != null) { mxPoint translate = graph.getView().getTranslate(); double scale = graph.getView().getScale(); g.drawImage(backgroundImage.getImage(), (int) (translate.getX() * scale), (int) (translate.getY() * scale), (int) (backgroundImage.getIconWidth() * scale), (int) (backgroundImage.getIconHeight() * scale), this); } } /** * Paints the grid onto the given graphics object. */ protected void paintGrid(Graphics g) { if (isGridVisible()) { g.setColor(getGridColor()); Rectangle clip = g.getClipBounds(); if (clip == null) { clip = getGraphControl().getBounds(); } double left = clip.getX(); double top = clip.getY(); double right = left + clip.getWidth(); double bottom = top + clip.getHeight(); // Double the grid line spacing if smaller than half the gridsize int style = getGridStyle(); int gridSize = graph.getGridSize(); int minStepping = gridSize; // Smaller stepping for certain styles if (style == GRID_STYLE_CROSS || style == GRID_STYLE_DOT) { minStepping /= 2; } // Fetches some global display state information mxPoint trans = graph.getView().getTranslate(); double scale = graph.getView().getScale(); double tx = trans.getX() * scale; double ty = trans.getY() * scale; // Sets the distance of the grid lines in pixels double stepping = gridSize * scale; if (stepping < minStepping) { int count = (int) Math .round(Math.ceil(minStepping / stepping) / 2) * 2; stepping = count * stepping; } double xs = Math.floor((left - tx) / stepping) * stepping + tx; double xe = Math.ceil(right / stepping) * stepping; double ys = Math.floor((top - ty) / stepping) * stepping + ty; double ye = Math.ceil(bottom / stepping) * stepping; switch (style) { case GRID_STYLE_CROSS: { // Sets the dot size int cs = (stepping > 16.0) ? 2 : 1; for (double x = xs; x <= xe; x += stepping) { for (double y = ys; y <= ye; y += stepping) { // FIXME: Workaround for rounding errors when adding // stepping to // xs or ys multiple times (leads to double grid lines // when zoom // is set to eg. 121%) x = Math.round((x - tx) / stepping) * stepping + tx; y = Math.round((y - ty) / stepping) * stepping + ty; int ix = (int) Math.round(x); int iy = (int) Math.round(y); g.drawLine(ix - cs, iy, ix + cs, iy); g.drawLine(ix, iy - cs, ix, iy + cs); } } break; } case GRID_STYLE_LINE: { xe += (int) Math.ceil(stepping); ye += (int) Math.ceil(stepping); int ixs = (int) Math.round(xs); int ixe = (int) Math.round(xe); int iys = (int) Math.round(ys); int iye = (int) Math.round(ye); for (double x = xs; x <= xe; x += stepping) { // FIXME: Workaround for rounding errors when adding // stepping to // xs or ys multiple times (leads to double grid lines when // zoom // is set to eg. 121%) x = Math.round((x - tx) / stepping) * stepping + tx; int ix = (int) Math.round(x); g.drawLine(ix, iys, ix, iye); } for (double y = ys; y <= ye; y += stepping) { // FIXME: Workaround for rounding errors when adding // stepping to // xs or ys multiple times (leads to double grid lines when // zoom // is set to eg. 121%) y = Math.round((y - ty) / stepping) * stepping + ty; int iy = (int) Math.round(y); g.drawLine(ixs, iy, ixe, iy); } break; } case GRID_STYLE_DASHED: { Graphics2D g2 = (Graphics2D) g; Stroke stroke = g2.getStroke(); xe += (int) Math.ceil(stepping); ye += (int) Math.ceil(stepping); int ixs = (int) Math.round(xs); int ixe = (int) Math.round(xe); int iys = (int) Math.round(ys); int iye = (int) Math.round(ye); // Creates a set of strokes with individual dash offsets // for each direction Stroke[] strokes = new Stroke[] { new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 1, new float[] { 3, 1 }, Math.max(0, iys) % 4), new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 1, new float[] { 2, 2 }, Math.max(0, iys) % 4), new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 1, new float[] { 1, 1 }, 0), new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 1, new float[] { 2, 2 }, Math.max(0, iys) % 4) }; for (double x = xs; x <= xe; x += stepping) { g2.setStroke(strokes[((int) (x / stepping)) % strokes.length]); // FIXME: Workaround for rounding errors when adding // stepping to // xs or ys multiple times (leads to double grid lines when // zoom // is set to eg. 121%) double xx = Math.round((x - tx) / stepping) * stepping + tx; int ix = (int) Math.round(xx); g.drawLine(ix, iys, ix, iye); } strokes = new Stroke[] { new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 1, new float[] { 3, 1 }, Math.max(0, ixs) % 4), new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 1, new float[] { 2, 2 }, Math.max(0, ixs) % 4), new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 1, new float[] { 1, 1 }, 0), new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 1, new float[] { 2, 2 }, Math.max(0, ixs) % 4) }; for (double y = ys; y <= ye; y += stepping) { g2.setStroke(strokes[((int) (y / stepping)) % strokes.length]); // FIXME: Workaround for rounding errors when adding // stepping to // xs or ys multiple times (leads to double grid lines when // zoom // is set to eg. 121%) double yy = Math.round((y - ty) / stepping) * stepping + ty; int iy = (int) Math.round(yy); g.drawLine(ixs, iy, ixe, iy); } g2.setStroke(stroke); break; } default: // DOT_GRID_MODE { for (double x = xs; x <= xe; x += stepping) { for (double y = ys; y <= ye; y += stepping) { // FIXME: Workaround for rounding errors when adding // stepping to // xs or ys multiple times (leads to double grid lines // when zoom // is set to eg. 121%) x = Math.round((x - tx) / stepping) * stepping + tx; y = Math.round((y - ty) / stepping) * stepping + ty; int ix = (int) Math.round(x); int iy = (int) Math.round(y); g.drawLine(ix, iy, ix, iy); } } } } } } // // Triple Buffering // /** * Updates the buffer (if one exists) and repaints the given cell state. */ public void redraw(mxCellState state) { if (state != null) { Rectangle dirty = state.getBoundingBox().getRectangle(); repaintTripleBuffer(new Rectangle(dirty)); dirty = SwingUtilities.convertRectangle(graphControl, dirty, this); repaint(dirty); } } /** * Checks if the triple buffer exists and creates a new one if it does not. * Also compares the size of the buffer with the size of the graph and drops * the buffer if it has a different size. */ public void checkTripleBuffer() { mxRectangle bounds = graph.getGraphBounds(); int width = (int) Math.ceil(bounds.getX() + bounds.getWidth() + 2); int height = (int) Math.ceil(bounds.getY() + bounds.getHeight() + 2); if (tripleBuffer != null) { if (tripleBuffer.getWidth() != width || tripleBuffer.getHeight() != height) { // Resizes the buffer (destroys existing and creates new) destroyTripleBuffer(); } } if (tripleBuffer == null) { createTripleBuffer(width, height); } } /** * Creates the tripleBufferGraphics and tripleBuffer for the given dimension * and draws the complete graph onto the triplebuffer. * * @param width * @param height */ protected void createTripleBuffer(int width, int height) { try { tripleBuffer = mxUtils.createBufferedImage(width, height, null); tripleBufferGraphics = tripleBuffer.createGraphics(); mxUtils.setAntiAlias(tripleBufferGraphics, antiAlias, textAntiAlias); // Repaints the complete buffer repaintTripleBuffer(null); } catch (OutOfMemoryError error) { // ignore } } /** * Destroys the tripleBuffer and tripleBufferGraphics objects. */ public void destroyTripleBuffer() { if (tripleBuffer != null) { tripleBuffer = null; tripleBufferGraphics.dispose(); tripleBufferGraphics = null; } } /** * Clears and repaints the triple buffer at the given rectangle or repaints * the complete buffer if no rectangle is specified. * * @param dirty */ public void repaintTripleBuffer(Rectangle dirty) { if (tripleBuffered && tripleBufferGraphics != null) { if (dirty == null) { dirty = new Rectangle(tripleBuffer.getWidth(), tripleBuffer.getHeight()); } // Clears and repaints the dirty rectangle using the // graphics canvas as a renderer mxUtils.clearRect(tripleBufferGraphics, dirty, null); tripleBufferGraphics.setClip(dirty); graphControl.drawGraph(tripleBufferGraphics, true); tripleBufferGraphics.setClip(null); } } // // Redirected to event source // /** * @return Returns true if event dispatching is enabled in the event source. * @see com.mxgraph.util.mxEventSource#isEventsEnabled() */ public boolean isEventsEnabled() { return eventSource.isEventsEnabled(); } /** * @param eventsEnabled * @see com.mxgraph.util.mxEventSource#setEventsEnabled(boolean) */ public void setEventsEnabled(boolean eventsEnabled) { eventSource.setEventsEnabled(eventsEnabled); } /** * @param eventName * @param listener * @see com.mxgraph.util.mxEventSource#addListener(java.lang.String, * com.mxgraph.util.mxEventSource.mxIEventListener) */ public void addListener(String eventName, mxIEventListener listener) { eventSource.addListener(eventName, listener); } /** * @param listener * Listener instance. */ public void removeListener(mxIEventListener listener) { eventSource.removeListener(listener); } /** * @param eventName * Name of the event. * @param listener * Listener instance. */ public void removeListener(mxIEventListener listener, String eventName) { eventSource.removeListener(listener, eventName); } /** * * @author gaudenz * */ public class mxGraphControl extends JComponent { /** * */ private static final long serialVersionUID = -8916603170766739124L; /** * Specifies a translation for painting. This should only be used during * mouse drags and must be reset after any interactive repaints. Default * is (0,0). This should not be null. */ protected Point translate = new Point(0, 0); /** * */ public mxGraphControl() { addMouseListener(new MouseAdapter() { public void mouseReleased(MouseEvent e) { if (translate.x != 0 || translate.y != 0) { translate = new Point(0, 0); repaint(); } } }); } /** * Returns the translate. */ public Point getTranslate() { return translate; } /** * Sets the translate. */ public void setTranslate(Point value) { translate = value; } /** * */ public mxGraphComponent getGraphContainer() { return mxGraphComponent.this; } /** * Overrides parent method to add extend flag for making the control * larger during previews. */ public void scrollRectToVisible(Rectangle aRect, boolean extend) { super.scrollRectToVisible(aRect); if (extend) { extendComponent(aRect); } } /** * Implements extension of the component in all directions. For * extension below the origin (into negative space) the translate will * temporaly be used and reset with the next mouse released event. */ protected void extendComponent(Rectangle rect) { int right = rect.x + rect.width; int bottom = rect.y + rect.height; Dimension d = new Dimension(getPreferredSize()); Dimension sp = getScaledPreferredSizeForGraph(); mxRectangle min = graph.getMinimumGraphSize(); double scale = graph.getView().getScale(); boolean update = false; if (rect.x < 0) { translate.x = Math.max(translate.x, Math.max(0, -rect.x)); d.width = sp.width; if (min != null) { d.width = (int) Math.max(d.width, Math.round(min.getWidth() * scale)); } d.width += translate.x; update = true; } else if (right > getWidth()) { d.width = Math.max(right, getWidth()); update = true; } if (rect.y < 0) { translate.y = Math.max(translate.y, Math.max(0, -rect.y)); d.height = sp.height; if (min != null) { d.height = (int) Math.max(d.height, Math.round(min.getHeight() * scale)); } d.height += translate.y; update = true; } else if (bottom > getHeight()) { d.height = Math.max(bottom, getHeight()); update = true; } if (update) { setPreferredSize(d); setMinimumSize(d); revalidate(); } } /** * */ public String getToolTipText(MouseEvent e) { String tip = getSelectionCellsHandler().getToolTipText(e); if (tip == null) { Object cell = getCellAt(e.getX(), e.getY()); if (cell != null) { if (hitFoldingIcon(cell, e.getX(), e.getY())) { tip = mxResources.get("collapse-expand"); } else { tip = graph.getToolTipForCell(cell); } } } if (tip != null && tip.length() > 0) { return tip; } return super.getToolTipText(e); } /** * Updates the preferred size for the given scale if the page size * should be preferred or the page is visible. */ public void updatePreferredSize() { double scale = graph.getView().getScale(); Dimension d = null; if (preferPageSize || pageVisible) { Dimension page = getPreferredSizeForPage(); if (!preferPageSize) { page.width += 2 * getHorizontalPageBorder(); page.height += 2 * getVerticalPageBorder(); } d = new Dimension((int) (page.width * scale), (int) (page.height * scale)); } else { d = getScaledPreferredSizeForGraph(); } mxRectangle min = graph.getMinimumGraphSize(); if (min != null) { d.width = (int) Math.max(d.width, Math.round(min.getWidth() * scale)); d.height = (int) Math.max(d.height, Math.round(min.getHeight() * scale)); } if (!getPreferredSize().equals(d)) { setPreferredSize(d); setMinimumSize(d); revalidate(); } } /** * */ public void paint(Graphics g) { g.translate(translate.x, translate.y); eventSource.fireEvent(new mxEventObject(mxEvent.BEFORE_PAINT, "g", g)); super.paint(g); eventSource .fireEvent(new mxEventObject(mxEvent.AFTER_PAINT, "g", g)); g.translate(-translate.x, -translate.y); } /** * */ public void paintComponent(Graphics g) { super.paintComponent(g); // Draws the background paintBackground(g); // Creates or destroys the triple buffer as needed if (tripleBuffered) { checkTripleBuffer(); } else if (tripleBuffer != null) { destroyTripleBuffer(); } // Paints the buffer in the canvas onto the dirty region if (tripleBuffer != null) { mxUtils.drawImageClip(g, tripleBuffer, this); } // Paints the graph directly onto the graphics else { Graphics2D g2 = (Graphics2D) g; RenderingHints tmp = g2.getRenderingHints(); // Sets the graphics in the canvas try { mxUtils.setAntiAlias(g2, antiAlias, textAntiAlias); drawGraph(g2, true); } finally { // Restores the graphics state g2.setRenderingHints(tmp); } } eventSource.fireEvent(new mxEventObject(mxEvent.PAINT, "g", g)); } /** * */ public void drawGraph(Graphics2D g, boolean drawLabels) { Graphics2D previousGraphics = canvas.getGraphics(); boolean previousDrawLabels = canvas.isDrawLabels(); Point previousTranslate = canvas.getTranslate(); double previousScale = canvas.getScale(); try { canvas.setScale(graph.getView().getScale()); canvas.setDrawLabels(drawLabels); canvas.setTranslate(0, 0); canvas.setGraphics(g); // Draws the graph using the graphics canvas drawFromRootCell(); } finally { canvas.setScale(previousScale); canvas.setTranslate(previousTranslate.x, previousTranslate.y); canvas.setDrawLabels(previousDrawLabels); canvas.setGraphics(previousGraphics); } } /** * Hook to draw the root cell into the canvas. */ protected void drawFromRootCell() { drawCell(canvas, graph.getModel().getRoot()); } /** * */ protected boolean hitClip(mxGraphics2DCanvas canvas, mxCellState state) { Rectangle rect = getExtendedCellBounds(state); return (rect == null || canvas.getGraphics().hitClip(rect.x, rect.y, rect.width, rect.height)); } /** * @param state the cached state of the cell whose extended bounds are to be calculated * @return the bounds of the cell, including the label and shadow and allowing for rotation */ protected Rectangle getExtendedCellBounds(mxCellState state) { Rectangle rect = null; // Takes rotation into account double rotation = mxUtils.getDouble(state.getStyle(), mxConstants.STYLE_ROTATION); mxRectangle tmp = mxUtils.getBoundingBox(new mxRectangle(state), rotation); // Adds scaled stroke width int border = (int) Math .ceil(mxUtils.getDouble(state.getStyle(), mxConstants.STYLE_STROKEWIDTH) * graph.getView().getScale()) + 1; tmp.grow(border); if (mxUtils.isTrue(state.getStyle(), mxConstants.STYLE_SHADOW)) { tmp.setWidth(tmp.getWidth() + mxConstants.SHADOW_OFFSETX); tmp.setHeight(tmp.getHeight() + mxConstants.SHADOW_OFFSETX); } // Adds the bounds of the label if (state.getLabelBounds() != null) { tmp.add(state.getLabelBounds()); } rect = tmp.getRectangle(); return rect; } /** * Draws the given cell onto the specified canvas. This is a modified * version of mxGraph.drawCell which paints the label only if the * corresponding cell is not being edited and invokes the cellDrawn hook * after all descendants have been painted. * * @param canvas * Canvas onto which the cell should be drawn. * @param cell * Cell that should be drawn onto the canvas. */ public void drawCell(mxICanvas canvas, Object cell) { mxCellState state = graph.getView().getState(cell); if (state != null && isCellDisplayable(state.getCell()) && (!(canvas instanceof mxGraphics2DCanvas) || hitClip( (mxGraphics2DCanvas) canvas, state))) { graph.drawState(canvas, state, cell != cellEditor.getEditingCell()); } // Handles special ordering for edges (all in foreground // or background) or draws all children in order boolean edgesFirst = graph.isKeepEdgesInBackground(); boolean edgesLast = graph.isKeepEdgesInForeground(); if (edgesFirst) { drawChildren(cell, true, false); } drawChildren(cell, !edgesFirst && !edgesLast, true); if (edgesLast) { drawChildren(cell, true, false); } if (state != null) { cellDrawn(canvas, state); } } /** * Draws the child edges and/or all other children in the given cell * depending on the boolean arguments. */ protected void drawChildren(Object cell, boolean edges, boolean others) { mxIGraphModel model = graph.getModel(); int childCount = model.getChildCount(cell); for (int i = 0; i < childCount; i++) { Object child = model.getChildAt(cell, i); boolean isEdge = model.isEdge(child); if ((others && !isEdge) || (edges && isEdge)) { drawCell(canvas, model.getChildAt(cell, i)); } } } /** * */ protected void cellDrawn(mxICanvas canvas, mxCellState state) { if (isFoldingEnabled() && canvas instanceof mxGraphics2DCanvas) { mxIGraphModel model = graph.getModel(); mxGraphics2DCanvas g2c = (mxGraphics2DCanvas) canvas; Graphics2D g2 = g2c.getGraphics(); // Draws the collapse/expand icons boolean isEdge = model.isEdge(state.getCell()); if (state.getCell() != graph.getCurrentRoot() && (model.isVertex(state.getCell()) || isEdge)) { ImageIcon icon = getFoldingIcon(state); if (icon != null) { Rectangle bounds = getFoldingIconBounds(state, icon); g2.drawImage(icon.getImage(), bounds.x, bounds.y, bounds.width, bounds.height, this); } } } } /** * Returns true if the given cell is not the current root or the root in * the model. This can be overridden to not render certain cells in the * graph display. */ protected boolean isCellDisplayable(Object cell) { return cell != graph.getView().getCurrentRoot() && cell != graph.getModel().getRoot(); } } /** * */ public static class mxMouseRedirector implements MouseListener, MouseMotionListener { /** * */ protected mxGraphComponent graphComponent; /** * */ public mxMouseRedirector(mxGraphComponent graphComponent) { this.graphComponent = graphComponent; } /* * (non-Javadoc) * * @see * java.awt.event.MouseListener#mouseClicked(java.awt.event.MouseEvent) */ public void mouseClicked(MouseEvent e) { graphComponent.getGraphControl().dispatchEvent( SwingUtilities.convertMouseEvent(e.getComponent(), e, graphComponent.getGraphControl())); } /* * (non-Javadoc) * * @see * java.awt.event.MouseListener#mouseEntered(java.awt.event.MouseEvent) */ public void mouseEntered(MouseEvent e) { // Redirecting this would cause problems on the Mac // and is technically incorrect anyway } /* * (non-Javadoc) * * @see * java.awt.event.MouseListener#mouseExited(java.awt.event.MouseEvent) */ public void mouseExited(MouseEvent e) { mouseClicked(e); } /* * (non-Javadoc) * * @see * java.awt.event.MouseListener#mousePressed(java.awt.event.MouseEvent) */ public void mousePressed(MouseEvent e) { mouseClicked(e); } /* * (non-Javadoc) * * @see * java.awt.event.MouseListener#mouseReleased(java.awt.event.MouseEvent) */ public void mouseReleased(MouseEvent e) { mouseClicked(e); } /* * (non-Javadoc) * * @see * java.awt.event.MouseMotionListener#mouseDragged(java.awt.event.MouseEvent * ) */ public void mouseDragged(MouseEvent e) { mouseClicked(e); } /* * (non-Javadoc) * * @see * java.awt.event.MouseMotionListener#mouseMoved(java.awt.event.MouseEvent * ) */ public void mouseMoved(MouseEvent e) { mouseClicked(e); } } }