/** * $Id: mxGraphOutline.java,v 1.1 2012/11/15 13:26:50 gaudenz Exp $ * Copyright (c) 2008, Gaudenz Alder */ package com.mxgraph.swing; import java.awt.BasicStroke; import java.awt.Color; 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.Stroke; import java.awt.event.AdjustmentEvent; import java.awt.event.AdjustmentListener; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.awt.event.ComponentListener; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import java.awt.geom.AffineTransform; import java.awt.image.BufferedImage; import javax.swing.JComponent; import javax.swing.JScrollBar; import com.mxgraph.util.mxEvent; import com.mxgraph.util.mxEventObject; import com.mxgraph.util.mxEventSource.mxIEventListener; import com.mxgraph.util.mxPoint; import com.mxgraph.util.mxRectangle; import com.mxgraph.util.mxUtils; import com.mxgraph.view.mxGraphView; /** * An outline view for a specific graph component. */ public class mxGraphOutline extends JComponent { /** * */ private static final long serialVersionUID = -2521103946905154267L; /** * */ public static Color DEFAULT_ZOOMHANDLE_FILL = new Color(0, 255, 255); /** * */ protected mxGraphComponent graphComponent; /** * TODO: Not yet implemented. */ protected BufferedImage tripleBuffer; /** * Holds the graphics of the triple buffer. */ protected Graphics2D tripleBufferGraphics; /** * True if the triple buffer needs a full repaint. */ protected boolean repaintBuffer = false; /** * Clip of the triple buffer to be repainted. */ protected mxRectangle repaintClip = null; /** * */ protected boolean tripleBuffered = true; /** * */ protected Rectangle finderBounds = new Rectangle(); /** * */ protected Point zoomHandleLocation = null; /** * */ protected boolean finderVisible = true; /** * */ protected boolean zoomHandleVisible = true; /** * */ protected boolean useScaledInstance = false; /** * */ protected boolean antiAlias = false; /** * */ protected boolean drawLabels = false; /** * Specifies if the outline should be zoomed to the page if the graph * component is in page layout mode. Default is true. */ protected boolean fitPage = true; /** * Not yet implemented. * * Border to add around the page bounds if wholePage is true. * Default is 4. */ protected int outlineBorder = 10; /** * */ protected MouseTracker tracker = new MouseTracker(); /** * */ protected double scale = 1; /** * */ protected Point translate = new Point(); /** * */ protected transient boolean zoomGesture = false; /** * */ protected mxIEventListener repaintHandler = new mxIEventListener() { public void invoke(Object source, mxEventObject evt) { updateScaleAndTranslate(); mxRectangle dirty = (mxRectangle) evt.getProperty("region"); if (dirty != null) { repaintClip = new mxRectangle(dirty); } else { repaintBuffer = true; } if (dirty != null) { updateFinder(true); dirty.grow(1 / scale); dirty.setX(dirty.getX() * scale + translate.x); dirty.setY(dirty.getY() * scale + translate.y); dirty.setWidth(dirty.getWidth() * scale); dirty.setHeight(dirty.getHeight() * scale); repaint(dirty.getRectangle()); } else { updateFinder(false); repaint(); } } }; /** * */ protected ComponentListener componentHandler = new ComponentAdapter() { public void componentResized(ComponentEvent e) { if (updateScaleAndTranslate()) { repaintBuffer = true; updateFinder(false); repaint(); } else { updateFinder(true); } } }; /** * */ protected AdjustmentListener adjustmentHandler = new AdjustmentListener() { /** * */ public void adjustmentValueChanged(AdjustmentEvent e) { if (updateScaleAndTranslate()) { repaintBuffer = true; updateFinder(false); repaint(); } else { updateFinder(true); } } }; /** * */ public mxGraphOutline(mxGraphComponent graphComponent) { addComponentListener(componentHandler); addMouseMotionListener(tracker); addMouseListener(tracker); setGraphComponent(graphComponent); setEnabled(true); setOpaque(true); } /** * Fires a property change event for <code>tripleBuffered</code>. * * @param tripleBuffered the tripleBuffered to set */ public void setTripleBuffered(boolean tripleBuffered) { boolean oldValue = this.tripleBuffered; this.tripleBuffered = tripleBuffered; if (!tripleBuffered) { destroyTripleBuffer(); } firePropertyChange("tripleBuffered", oldValue, tripleBuffered); } /** * */ public boolean isTripleBuffered() { return tripleBuffered; } /** * Fires a property change event for <code>drawLabels</code>. * * @param drawLabels the drawLabels to set */ public void setDrawLabels(boolean drawLabels) { boolean oldValue = this.drawLabels; this.drawLabels = drawLabels; repaintTripleBuffer(null); firePropertyChange("drawLabels", oldValue, drawLabels); } /** * */ public boolean isDrawLabels() { return drawLabels; } /** * Fires a property change event for <code>antiAlias</code>. * * @param antiAlias the antiAlias to set */ public void setAntiAlias(boolean antiAlias) { boolean oldValue = this.antiAlias; this.antiAlias = antiAlias; repaintTripleBuffer(null); firePropertyChange("antiAlias", oldValue, antiAlias); } /** * @return the antiAlias */ public boolean isAntiAlias() { return antiAlias; } /** * */ public void setVisible(boolean visible) { super.setVisible(visible); // Frees memory if the outline is hidden if (!visible) { destroyTripleBuffer(); } } /** * */ public void setFinderVisible(boolean visible) { finderVisible = visible; } /** * */ public void setZoomHandleVisible(boolean visible) { zoomHandleVisible = visible; } /** * Fires a property change event for <code>fitPage</code>. * * @param fitPage the fitPage to set */ public void setFitPage(boolean fitPage) { boolean oldValue = this.fitPage; this.fitPage = fitPage; if (updateScaleAndTranslate()) { repaintBuffer = true; updateFinder(false); } firePropertyChange("fitPage", oldValue, fitPage); } /** * */ public boolean isFitPage() { return fitPage; } /** * */ public mxGraphComponent getGraphComponent() { return graphComponent; } /** * Fires a property change event for <code>graphComponent</code>. * * @param graphComponent the graphComponent to set */ public void setGraphComponent(mxGraphComponent graphComponent) { mxGraphComponent oldValue = this.graphComponent; if (this.graphComponent != null) { this.graphComponent.getGraph().removeListener(repaintHandler); this.graphComponent.getGraphControl().removeComponentListener( componentHandler); this.graphComponent.getHorizontalScrollBar() .removeAdjustmentListener(adjustmentHandler); this.graphComponent.getVerticalScrollBar() .removeAdjustmentListener(adjustmentHandler); } this.graphComponent = graphComponent; if (this.graphComponent != null) { this.graphComponent.getGraph().addListener(mxEvent.REPAINT, repaintHandler); this.graphComponent.getGraphControl().addComponentListener( componentHandler); this.graphComponent.getHorizontalScrollBar().addAdjustmentListener( adjustmentHandler); this.graphComponent.getVerticalScrollBar().addAdjustmentListener( adjustmentHandler); } if (updateScaleAndTranslate()) { repaintBuffer = true; repaint(); } firePropertyChange("graphComponent", oldValue, graphComponent); } /** * 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() { if (tripleBuffer != null) { if (tripleBuffer.getWidth() != getWidth() || tripleBuffer.getHeight() != getHeight()) { // Resizes the buffer (destroys existing and creates new) destroyTripleBuffer(); } } if (tripleBuffer == null) { createTripleBuffer(getWidth(), getHeight()); } } /** * 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(); // 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 clip */ public void repaintTripleBuffer(Rectangle clip) { if (tripleBuffered && tripleBufferGraphics != null) { if (clip == null) { clip = new Rectangle(tripleBuffer.getWidth(), tripleBuffer.getHeight()); } // Clears and repaints the dirty rectangle using the // graphics canvas of the graph component as a renderer mxUtils.clearRect(tripleBufferGraphics, clip, null); tripleBufferGraphics.setClip(clip); paintGraph(tripleBufferGraphics); tripleBufferGraphics.setClip(null); repaintBuffer = false; repaintClip = null; } } /** * */ public void updateFinder(boolean repaint) { Rectangle rect = graphComponent.getViewport().getViewRect(); int x = (int) Math.round(rect.x * scale); int y = (int) Math.round(rect.y * scale); int w = (int) Math.round((rect.x + rect.width) * scale) - x; int h = (int) Math.round((rect.y + rect.height) * scale) - y; updateFinderBounds(new Rectangle(x + translate.x, y + translate.y, w + 1, h + 1), repaint); } /** * */ public void updateFinderBounds(Rectangle bounds, boolean repaint) { if (bounds != null && !bounds.equals(finderBounds)) { Rectangle old = new Rectangle(finderBounds); finderBounds = bounds; // LATER: Fix repaint region to be smaller if (repaint) { old = old.union(finderBounds); old.grow(3, 3); repaint(old); } } } /** * */ public void paintComponent(Graphics g) { super.paintComponent(g); paintBackground(g); if (graphComponent != null) { // Creates or destroys the triple buffer as needed if (tripleBuffered) { checkTripleBuffer(); } else if (tripleBuffer != null) { destroyTripleBuffer(); } // Updates the dirty region from the buffered graph image if (tripleBuffer != null) { if (repaintBuffer) { repaintTripleBuffer(null); } else if (repaintClip != null) { repaintClip.grow(1 / scale); repaintClip.setX(repaintClip.getX() * scale + translate.x); repaintClip.setY(repaintClip.getY() * scale + translate.y); repaintClip.setWidth(repaintClip.getWidth() * scale); repaintClip.setHeight(repaintClip.getHeight() * scale); repaintTripleBuffer(repaintClip.getRectangle()); } mxUtils.drawImageClip(g, tripleBuffer, this); } // Paints the graph directly onto the graphics else { paintGraph(g); } paintForeground(g); } } /** * Paints the background. */ protected void paintBackground(Graphics g) { if (graphComponent != null) { Graphics2D g2 = (Graphics2D) g; AffineTransform tx = g2.getTransform(); try { // Draws the background of the outline if a graph exists g.setColor(graphComponent.getPageBackgroundColor()); mxUtils.fillClippedRect(g, 0, 0, getWidth(), getHeight()); g2.translate(translate.x, translate.y); g2.scale(scale, scale); // Draws the scaled page background if (!graphComponent.isPageVisible()) { Color bg = graphComponent.getBackground(); if (graphComponent.getViewport().isOpaque()) { bg = graphComponent.getViewport().getBackground(); } g.setColor(bg); Dimension size = graphComponent.getGraphControl().getSize(); // Paints the background of the drawing surface mxUtils.fillClippedRect(g, 0, 0, size.width, size.height); g.setColor(g.getColor().darker().darker()); g.drawRect(0, 0, size.width, size.height); } else { // Paints the page background using the graphics scaling graphComponent.paintBackgroundPage(g); } } finally { g2.setTransform(tx); } } else { // Draws the background of the outline if no graph exists g.setColor(getBackground()); mxUtils.fillClippedRect(g, 0, 0, getWidth(), getHeight()); } } /** * Paints the graph outline. */ public void paintGraph(Graphics g) { if (graphComponent != null) { Graphics2D g2 = (Graphics2D) g; AffineTransform tx = g2.getTransform(); try { Point tr = graphComponent.getGraphControl().getTranslate(); g2.translate(translate.x + tr.getX() * scale, translate.y + tr.getY() * scale); g2.scale(scale, scale); // Draws the scaled graph graphComponent.getGraphControl().drawGraph(g2, drawLabels); } finally { g2.setTransform(tx); } } } /** * Paints the foreground. Foreground is dynamic and should never be made * part of the triple buffer. It is painted on top of the buffer. */ protected void paintForeground(Graphics g) { if (graphComponent != null) { Graphics2D g2 = (Graphics2D) g; Stroke stroke = g2.getStroke(); g.setColor(Color.BLUE); g2.setStroke(new BasicStroke(3)); g.drawRect(finderBounds.x, finderBounds.y, finderBounds.width, finderBounds.height); if (zoomHandleVisible) { g2.setStroke(stroke); g.setColor(DEFAULT_ZOOMHANDLE_FILL); g.fillRect(finderBounds.x + finderBounds.width - 6, finderBounds.y + finderBounds.height - 6, 8, 8); g.setColor(Color.BLACK); g.drawRect(finderBounds.x + finderBounds.width - 6, finderBounds.y + finderBounds.height - 6, 8, 8); } } } /** * Returns true if the scale or translate has changed. */ public boolean updateScaleAndTranslate() { double newScale = 1; int dx = 0; int dy = 0; if (this.graphComponent != null) { Dimension graphSize = graphComponent.getGraphControl().getSize(); Dimension outlineSize = getSize(); int gw = (int) graphSize.getWidth(); int gh = (int) graphSize.getHeight(); if (gw > 0 && gh > 0) { boolean magnifyPage = graphComponent.isPageVisible() && isFitPage() && graphComponent.getHorizontalScrollBar().isVisible() && graphComponent.getVerticalScrollBar().isVisible(); double graphScale = graphComponent.getGraph().getView() .getScale(); mxPoint trans = graphComponent.getGraph().getView() .getTranslate(); int w = (int) outlineSize.getWidth() - 2 * outlineBorder; int h = (int) outlineSize.getHeight() - 2 * outlineBorder; if (magnifyPage) { gw -= 2 * Math.round(trans.getX() * graphScale); gh -= 2 * Math.round(trans.getY() * graphScale); } newScale = Math.min((double) w / gw, (double) h / gh); dx += (int) Math .round((outlineSize.getWidth() - gw * newScale) / 2); dy += (int) Math .round((outlineSize.getHeight() - gh * newScale) / 2); if (magnifyPage) { dx -= Math.round(trans.getX() * newScale * graphScale); dy -= Math.round(trans.getY() * newScale * graphScale); } } } if (newScale != scale || translate.x != dx || translate.y != dy) { scale = newScale; translate.setLocation(dx, dy); return true; } else { return false; } } /** * */ public class MouseTracker implements MouseListener, MouseMotionListener { /** * */ protected Point start = null; /* * (non-Javadoc) * @see java.awt.event.MouseListener#mousePressed(java.awt.event.MouseEvent) */ public void mousePressed(MouseEvent e) { zoomGesture = hitZoomHandle(e.getX(), e.getY()); if (graphComponent != null && !e.isConsumed() && !e.isPopupTrigger() && (finderBounds.contains(e.getPoint()) || zoomGesture)) { start = e.getPoint(); } } /* * (non-Javadoc) * @see java.awt.event.MouseMotionListener#mouseDragged(java.awt.event.MouseEvent) */ public void mouseDragged(MouseEvent e) { if (isEnabled() && start != null) { if (zoomGesture) { Rectangle bounds = graphComponent.getViewport() .getViewRect(); double viewRatio = bounds.getWidth() / bounds.getHeight(); bounds = new Rectangle(finderBounds); bounds.width = (int) Math .max(0, (e.getX() - bounds.getX())); bounds.height = (int) Math.max(0, (bounds.getWidth() / viewRatio)); updateFinderBounds(bounds, true); } else { // TODO: To enable constrained moving, that is, moving // into only x- or y-direction when shift is pressed, // we need the location of the first mouse event, since // the movement can not be constrained for incremental // steps as used below. int dx = (int) ((e.getX() - start.getX()) / scale); int dy = (int) ((e.getY() - start.getY()) / scale); // Keeps current location as start for delta movement // of the scrollbars start = e.getPoint(); graphComponent.getHorizontalScrollBar().setValue( graphComponent.getHorizontalScrollBar().getValue() + dx); graphComponent.getVerticalScrollBar().setValue( graphComponent.getVerticalScrollBar().getValue() + dy); } } } /* * (non-Javadoc) * @see java.awt.event.MouseListener#mouseReleased(java.awt.event.MouseEvent) */ public void mouseReleased(MouseEvent e) { if (start != null) { if (zoomGesture) { double dx = e.getX() - start.getX(); double w = finderBounds.getWidth(); final JScrollBar hs = graphComponent .getHorizontalScrollBar(); final double sx; if (hs != null) { sx = (double) hs.getValue() / hs.getMaximum(); } else { sx = 0; } final JScrollBar vs = graphComponent.getVerticalScrollBar(); final double sy; if (vs != null) { sy = (double) vs.getValue() / vs.getMaximum(); } else { sy = 0; } mxGraphView view = graphComponent.getGraph().getView(); double scale = view.getScale(); double newScale = scale - (dx * scale) / w; double factor = newScale / scale; view.setScale(newScale); if (hs != null) { hs.setValue((int) (sx * hs.getMaximum() * factor)); } if (vs != null) { vs.setValue((int) (sy * vs.getMaximum() * factor)); } } zoomGesture = false; start = null; } } /** * */ public boolean hitZoomHandle(int x, int y) { return new Rectangle(finderBounds.x + finderBounds.width - 6, finderBounds.y + finderBounds.height - 6, 8, 8).contains(x, y); } /* * (non-Javadoc) * @see java.awt.event.MouseMotionListener#mouseMoved(java.awt.event.MouseEvent) */ public void mouseMoved(MouseEvent e) { if (hitZoomHandle(e.getX(), e.getY())) { setCursor(new Cursor(Cursor.HAND_CURSOR)); } else if (finderBounds.contains(e.getPoint())) { setCursor(new Cursor(Cursor.MOVE_CURSOR)); } else { setCursor(new Cursor(Cursor.DEFAULT_CURSOR)); } } /* * (non-Javadoc) * @see java.awt.event.MouseListener#mouseClicked(java.awt.event.MouseEvent) */ public void mouseClicked(MouseEvent e) { // ignore } /* * (non-Javadoc) * @see java.awt.event.MouseListener#mouseEntered(java.awt.event.MouseEvent) */ public void mouseEntered(MouseEvent e) { // ignore } /* * (non-Javadoc) * @see java.awt.event.MouseListener#mouseExited(java.awt.event.MouseEvent) */ public void mouseExited(MouseEvent e) { // ignore } } }