001: /*
002: * $Id: JGraphEditorNavigator.java,v 1.6 2007/03/26 17:40:09 gaudenz Exp $
003: * Copyright (c) 2001-2005, Gaudenz Alder
004: *
005: * All rights reserved.
006: *
007: * See LICENSE file for license details. If you are unable to locate
008: * this file please contact info (at) jgraph (dot) com.
009: */
010: package com.jgraph.editor.factory;
011:
012: import java.awt.BorderLayout;
013: import java.awt.Color;
014: import java.awt.Component;
015: import java.awt.Cursor;
016: import java.awt.Dimension;
017: import java.awt.Graphics;
018: import java.awt.Point;
019: import java.awt.Rectangle;
020: import java.awt.event.AdjustmentEvent;
021: import java.awt.event.AdjustmentListener;
022: import java.awt.event.ComponentAdapter;
023: import java.awt.event.ComponentEvent;
024: import java.awt.event.ComponentListener;
025: import java.awt.event.MouseEvent;
026: import java.awt.event.MouseListener;
027: import java.awt.event.MouseMotionListener;
028: import java.awt.geom.Point2D;
029: import java.beans.PropertyChangeEvent;
030: import java.beans.PropertyChangeListener;
031: import java.lang.ref.WeakReference;
032:
033: import javax.swing.BorderFactory;
034: import javax.swing.ImageIcon;
035: import javax.swing.JPanel;
036: import javax.swing.JScrollPane;
037: import javax.swing.JViewport;
038: import javax.swing.SwingUtilities;
039:
040: import org.jgraph.JGraph;
041: import org.jgraph.event.GraphLayoutCacheEvent;
042: import org.jgraph.event.GraphLayoutCacheListener;
043: import org.jgraph.event.GraphModelEvent;
044: import org.jgraph.event.GraphModelListener;
045: import org.jgraph.graph.GraphLayoutCache;
046: import org.w3c.dom.Node;
047:
048: import com.jgraph.JGraphEditor;
049:
050: /**
051: * Birds-eye view on a graph. The displayed graph may be changed at runtime. The
052: * class provides the {@link FactoryMethod} to be added to an editor factory.
053: */
054: public class JGraphEditorNavigator extends JPanel implements
055: GraphLayoutCacheListener, GraphModelListener,
056: PropertyChangeListener, AdjustmentListener {
057:
058: /**
059: * Shared cursor objects to avoid expensive constructor calls.
060: */
061: protected static final Cursor CURSOR_DEFAULT = new Cursor(
062: Cursor.DEFAULT_CURSOR), CURSOR_HAND = new Cursor(
063: Cursor.HAND_CURSOR);
064:
065: /**
066: * Shared cursor objects to avoid expensive constructor calls.
067: */
068: public static Color DEFAULT_BACKGROUND = Color.lightGray;
069:
070: /**
071: * Component listener to udpate the scale.
072: */
073: protected ComponentListener componentListener = new ComponentAdapter() {
074:
075: /*
076: * (non-Javadoc)
077: */
078: public void componentResized(ComponentEvent e) {
079: updateScale();
080: }
081:
082: };
083:
084: /**
085: * References the inital layout cache of the backing graph.
086: */
087: protected transient GraphLayoutCache initialLayoutCache;
088:
089: /**
090: * Holds the backing graph and references the displayed (current) graph.
091: */
092: protected JGraph backingGraph;
093:
094: /**
095: * Weak reference to the current graph.
096: */
097: protected WeakReference currentGraph;
098:
099: /**
100: * Holds the navigator pane the displays the backing graph.
101: */
102: protected NavigatorPane navigatorPane;
103:
104: /**
105: * Specifies the maximum scale for the navigator view. Default is 0.5
106: */
107: protected double maximumScale = 0.5;
108:
109: /**
110: * Specifies whether the background image of the enclosing diagram pane
111: * should be visible. Default is true.
112: */
113: protected boolean isBackgroundImageVisible = true;
114:
115: /**
116: * Constructs a new graph navigator using <code>backingGraph</code> to
117: * display the graph in {@link #currentGraph}.
118: *
119: * @param backingGraph
120: * The backing graph to render the display.
121: */
122: public JGraphEditorNavigator(JGraph backingGraph) {
123: super (new BorderLayout());
124: setDoubleBuffered(true);
125: setBackingGraph(backingGraph);
126: initialLayoutCache = backingGraph.getGraphLayoutCache();
127: backingGraph.setOpaque(false);
128: backingGraph.setScale(maximumScale);
129: navigatorPane = new NavigatorPane(backingGraph);
130: backingGraph.addMouseListener(navigatorPane);
131: backingGraph.addMouseMotionListener(navigatorPane);
132:
133: // Configures the navigator
134: setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4));
135: add(navigatorPane, BorderLayout.CENTER);
136: setFocusable(false);
137:
138: // Updates the size when the component is resized
139: addComponentListener(componentListener);
140: }
141:
142: /**
143: * Returns the navigator pane that contains the backing graph.
144: *
145: * @return Returns the navigator pane.
146: */
147: public NavigatorPane getScrollPane() {
148: return navigatorPane;
149: }
150:
151: /**
152: * Returns the maximum scale to be used for the backing graph.
153: *
154: * @return Returns the maximumScale.
155: */
156: public double getMaximumScale() {
157: return maximumScale;
158: }
159:
160: /**
161: * Sets the maximum scale to be used for the backing graph.
162: *
163: * @param maximumScale
164: * The maximumScale to set.
165: */
166: public void setMaximumScale(double maximumScale) {
167: this .maximumScale = maximumScale;
168: }
169:
170: /**
171: * Returns the backing graph that is used to display {@link #currentGraph}.
172: *
173: * @return Returns the backing graph.
174: */
175: public JGraph getBackingGraph() {
176: return backingGraph;
177: }
178:
179: /**
180: * Sets the backing graph that is used to display {@link #currentGraph}.
181: *
182: * @param backingGraph
183: * The backing graph to set.
184: */
185: public void setBackingGraph(JGraph backingGraph) {
186: this .backingGraph = backingGraph;
187: }
188:
189: /**
190: * Returns the graph that is currently displayed.
191: *
192: * @return Returns the backing graph.
193: */
194: public JGraph getCurrentGraph() {
195: return (JGraph) ((currentGraph != null) ? currentGraph.get()
196: : null);
197: }
198:
199: /**
200: * @return Returns the isBackgroundImageVisible.
201: */
202: public boolean isBackgroundImageVisible() {
203: return isBackgroundImageVisible;
204: }
205:
206: /**
207: * @param isBackgroundImageVisible
208: * The isBackgroundImageVisible to set.
209: */
210: public void setBackgroundImageVisible(
211: boolean isBackgroundImageVisible) {
212: this .isBackgroundImageVisible = isBackgroundImageVisible;
213: }
214:
215: /**
216: * Sets the graph that is currently displayed.
217: *
218: * @param sourceGraph
219: * The current graph to set.
220: */
221: public void setCurrentGraph(JGraph sourceGraph) {
222: if (sourceGraph == null || getParentGraph(sourceGraph) == null) {
223: if (sourceGraph != null) {
224: JGraph oldValue = getCurrentGraph();
225:
226: // Removes listeners from the previous graph
227: if (oldValue != null && sourceGraph != oldValue) {
228: oldValue.getModel().removeGraphModelListener(this );
229: oldValue.getGraphLayoutCache()
230: .removeGraphLayoutCacheListener(this );
231: oldValue.removePropertyChangeListener(this );
232: JScrollPane scrollPane = getParentScrollPane(oldValue);
233: if (scrollPane != null) {
234: scrollPane
235: .removeComponentListener(componentListener);
236: scrollPane.getVerticalScrollBar()
237: .removeAdjustmentListener(this );
238: scrollPane.getHorizontalScrollBar()
239: .removeAdjustmentListener(this );
240: scrollPane.removePropertyChangeListener(this );
241: }
242:
243: // Restores the layout cache of the backing graph
244: backingGraph
245: .setGraphLayoutCache(initialLayoutCache);
246: }
247: this .currentGraph = new WeakReference(sourceGraph);
248:
249: // Installs change listeners to update the size
250: if (sourceGraph != null) {
251: sourceGraph.getModel().addGraphModelListener(this );
252: sourceGraph.getGraphLayoutCache()
253: .addGraphLayoutCacheListener(this );
254: sourceGraph.addPropertyChangeListener(this );
255: JScrollPane currentScrollPane = getParentScrollPane(sourceGraph);
256: if (currentScrollPane != null) {
257: currentScrollPane
258: .addComponentListener(componentListener);
259: currentScrollPane.getVerticalScrollBar()
260: .addAdjustmentListener(this );
261: currentScrollPane.getHorizontalScrollBar()
262: .addAdjustmentListener(this );
263: currentScrollPane
264: .addPropertyChangeListener(this );
265: }
266: backingGraph.setGraphLayoutCache(sourceGraph
267: .getGraphLayoutCache());
268: }
269: updateScale();
270: }
271: }
272: }
273:
274: /**
275: * Updates the scale of the backing graph.
276: */
277: protected void updateScale() {
278: SwingUtilities.invokeLater(new Runnable() {
279: public void run() {
280: JGraph graph = getCurrentGraph();
281: if (graph != null) {
282: Dimension d = graph.getPreferredSize();
283: Dimension b = graph.getBounds().getSize();
284: d.width = Math.max(d.width, b.width);
285: b.height = Math.max(d.height, b.height);
286: double scale = graph.getScale();
287: d
288: .setSize(d.width * 1 / scale, d.height * 1
289: / scale);
290: Dimension s = getScrollPane().getViewport()
291: .getSize();
292: double sx = s.getWidth() / d.getWidth();
293: double sy = s.getHeight() / d.getHeight();
294: scale = Math.min(Math.min(sx, sy),
295: getMaximumScale());
296: getBackingGraph().setScale(scale);
297: repaint();
298: }
299: }
300: });
301: }
302:
303: /*
304: * (non-Javadoc)
305: */
306: public void graphLayoutCacheChanged(GraphLayoutCacheEvent e) {
307: updateScale();
308: }
309:
310: /*
311: * (non-Javadoc)
312: */
313: public void graphChanged(GraphModelEvent e) {
314: updateScale();
315: }
316:
317: /*
318: * (non-Javadoc)
319: */
320: public void propertyChange(PropertyChangeEvent event) {
321: updateScale();
322: }
323:
324: /*
325: * (non-Javadoc)
326: */
327: public void adjustmentValueChanged(AdjustmentEvent e) {
328: navigatorPane.repaint();
329: }
330:
331: /**
332: * Helper method that returns the parent scrollpane for the specified
333: * component in the component hierarchy. If the component is itself a
334: * scrollpane then it is returned.
335: *
336: * @return Returns the parent scrollpane or component.
337: */
338: public static JScrollPane getParentScrollPane(Component component) {
339: while (component != null) {
340: if (component instanceof JScrollPane)
341: return (JScrollPane) component;
342: component = component.getParent();
343: }
344: return null;
345: }
346:
347: /**
348: * Helper method that returns the parent JGraph for the specified component
349: * in the component hierarchy. The component itself is never returned.
350: *
351: * @return Returns the parent scrollpane or component.
352: */
353: public static JGraph getParentGraph(Component component) {
354: do {
355: component = component.getParent();
356: if (component instanceof JGraph)
357: return (JGraph) component;
358: } while (component != null);
359: return null;
360: }
361:
362: /**
363: * Scrollpane that implements special painting used for the navigator
364: * preview.
365: */
366: public class NavigatorPane extends JScrollPane implements
367: MouseListener, MouseMotionListener {
368:
369: /**
370: * Holds the bounds of the finder (red box).
371: */
372: protected Rectangle currentViewport = new Rectangle();
373:
374: /**
375: * Holds the location of the last mouse event.
376: */
377: protected Point lastPoint = null;
378:
379: /**
380: * Constructs a new navigator pane using the specified backing graph to
381: * display the preview.
382: *
383: * @param backingGraph
384: * The backing graph to use for rendering.
385: */
386: public NavigatorPane(JGraph backingGraph) {
387: super (backingGraph);
388: setOpaque(false);
389: getViewport().setOpaque(false);
390: }
391:
392: /**
393: * Paints the navigator pane on the specified graphics.
394: *
395: * @param g
396: * The graphics to paint the navigator to.
397: */
398: public void paint(Graphics g) {
399: JGraph graph = getCurrentGraph();
400: JScrollPane scrollPane = getParentScrollPane(graph);
401: g.setColor(DEFAULT_BACKGROUND);
402: g.fillRect(0, 0, getWidth(), getHeight());
403: if (scrollPane != null && graph != null) {
404: JViewport viewport = scrollPane.getViewport();
405: Rectangle rect = viewport.getViewRect();
406: double scale = backingGraph.getScale()
407: / graph.getScale();
408: Dimension pSize = graph.getPreferredSize();
409: g.setColor(getBackground());
410: g.fillRect(0, 0, (int) (pSize.width * scale),
411: (int) (pSize.height * scale));
412: g.setColor(Color.WHITE);
413: currentViewport.setFrame((int) (rect.getX() * scale),
414: (int) (rect.getY() * scale), (int) (rect
415: .getWidth() * scale), (int) (rect
416: .getHeight() * scale));
417: g.fillRect(currentViewport.x, currentViewport.y,
418: currentViewport.width, currentViewport.height);
419:
420: // Draws the background image that the editor diagram pane uses
421: if (isBackgroundImageVisible()
422: && scrollPane instanceof JGraphEditorDiagramPane) {
423: JGraphEditorDiagramPane editorPane = (JGraphEditorDiagramPane) scrollPane;
424: ImageIcon icon = editorPane.getBackgroundImage();
425: if (icon != null) {
426: g.drawImage(icon.getImage(), 0, 0, (int) (icon
427: .getIconWidth() * scale), (int) (icon
428: .getIconHeight() * scale), this );
429: }
430: }
431:
432: super .paint(g);
433: g.setColor(Color.RED);
434: g.drawRect(currentViewport.x, currentViewport.y,
435: currentViewport.width, currentViewport.height);
436: }
437: }
438:
439: /*
440: * (non-Javadoc)
441: */
442: public void mouseClicked(MouseEvent e) {
443: // empty
444: }
445:
446: /*
447: * (non-Javadoc)
448: */
449: public void mousePressed(MouseEvent e) {
450: if (currentViewport.contains(e.getX(), e.getY()))
451: lastPoint = e.getPoint();
452: }
453:
454: /*
455: * (non-Javadoc)
456: */
457: public void mouseReleased(MouseEvent e) {
458: lastPoint = null;
459: }
460:
461: /*
462: * (non-Javadoc)
463: */
464: public void mouseEntered(MouseEvent e) {
465: // empty
466:
467: }
468:
469: /*
470: * (non-Javadoc)
471: */
472: public void mouseExited(MouseEvent e) {
473: // empty
474:
475: }
476:
477: /*
478: * (non-Javadoc)
479: */
480: public void mouseDragged(MouseEvent e) {
481: if (lastPoint != null) {
482: JGraph graph = getCurrentGraph();
483: JScrollPane scrollPane = getParentScrollPane(graph);
484: if (scrollPane != null && currentGraph != null) {
485: JViewport viewport = scrollPane.getViewport();
486: Rectangle rect = viewport.getViewRect();
487: double scale = backingGraph.getScale()
488: / graph.getScale();
489: double x = (e.getX() - lastPoint.getX()) / scale;
490: double y = (e.getY() - lastPoint.getY()) / scale;
491: lastPoint = e.getPoint();
492: x = rect.getX() + ((x > 0) ? rect.getWidth() : 0)
493: + x;
494: y = rect.getY() + ((y > 0) ? rect.getHeight() : 0)
495: + y;
496: Point2D pt = new Point2D.Double(x, y);
497: graph.scrollPointToVisible(pt);
498: }
499: }
500: }
501:
502: /*
503: * (non-Javadoc)
504: */
505: public void mouseMoved(MouseEvent e) {
506: if (currentViewport.contains(e.getPoint()))
507: setCursor(CURSOR_HAND);
508: else
509: setCursor(CURSOR_DEFAULT);
510: }
511:
512: }
513:
514: /**
515: * Factory method to construct a new navigator. This implementation requires
516: * an editor in order to create the backing graph using the editor's
517: * factory.
518: */
519: public static class FactoryMethod extends JGraphEditorFactoryMethod {
520:
521: /**
522: * Defines the default name for factory methods of this kind.
523: */
524: public static String NAME = "createNavigator";
525:
526: /**
527: * References an editor.
528: */
529: protected JGraphEditor editor;
530:
531: /**
532: * Constructs a new factory method for the specified editor using
533: * {@link #NAME}.
534: *
535: * @param editor
536: * The editor to create the factory method for.
537: */
538: public FactoryMethod(JGraphEditor editor) {
539: super (NAME);
540: this .editor = editor;
541: }
542:
543: /*
544: * (non-Javadoc)
545: */
546: public Component createInstance(Node configuration) {
547: JGraph backingGraph = editor.getFactory().createGraph(
548: new GraphLayoutCache());
549: backingGraph.setEnabled(false);
550: backingGraph.setFocusable(false);
551: return new JGraphEditorNavigator(backingGraph);
552: }
553:
554: }
555:
556: }
|