1 /*
2  * $Id: EditorRuler.java,v 1.2 2009-11-24 12:00:28 gaudenz Exp $
3  * Copyright (c) 2001-2005, Gaudenz Alder
4  *
5  * All rights reserved.
6  *
7  * See LICENSE file for license details. If you are unable to locate
8  * this file please contact info (at) jgraph (dot) com.
9  */
10 package com.mxgraph.examples.swing.editor;
11 
12 import java.awt.Color;
13 import java.awt.Dimension;
14 import java.awt.Font;
15 import java.awt.Graphics;
16 import java.awt.Graphics2D;
17 import java.awt.Point;
18 import java.awt.Rectangle;
19 import java.awt.RenderingHints;
20 import java.awt.dnd.DropTarget;
21 import java.awt.dnd.DropTargetDragEvent;
22 import java.awt.dnd.DropTargetDropEvent;
23 import java.awt.dnd.DropTargetEvent;
24 import java.awt.dnd.DropTargetListener;
25 import java.awt.event.MouseEvent;
26 import java.awt.event.MouseMotionListener;
27 import java.awt.geom.AffineTransform;
28 import java.awt.geom.Point2D;
29 import java.text.NumberFormat;
30 import java.util.TooManyListenersException;
31 
32 import javax.swing.BorderFactory;
33 import javax.swing.JComponent;
34 
35 import com.mxgraph.swing.mxGraphComponent;
36 import com.mxgraph.util.mxEvent;
37 import com.mxgraph.util.mxEventObject;
38 import com.mxgraph.util.mxPoint;
39 import com.mxgraph.util.mxEventSource.mxIEventListener;
40 import com.mxgraph.view.mxGraph;
41 
42 /**
43  * Component that displays a ruler for a JGraph component.
44  */
45 public class EditorRuler extends JComponent implements MouseMotionListener,
46 		DropTargetListener
47 {
48 
49 	/**
50 	 *
51 	 */
52 	private static final long serialVersionUID = -6310912355878668096L;
53 
54 	/**
55 	 * Defines the constants for horizontal and vertical orientation.
56 	 */
57 	public static int ORIENTATION_HORIZONTAL = 0, ORIENTATION_VERTICAL = 1;
58 
59 	/**
60 	 * Internal constant used to describe the screen resolution (DPI). Default
61 	 * is 72.
62 	 */
63 	protected static int INCH = 72;
64 
65 	/**
66 	 * Internal constant used to describe the screen resolution (DPI). Default
67 	 * is 72.
68 	 */
69 	protected static int DEFAULT_PAGESCALE = 1;
70 
71 	/**
72 	 * Internal constant used to describe the screen resolution (DPI). Default
73 	 * is 72.
74 	 */
75 	protected static boolean DEFAULT_ISMETRIC = true;
76 
77 	/**
78 	 * Holds the shared number formatter.
79 	 *
80 	 * @see NumberFormat#getInstance()
81 	 */
82 	public static final NumberFormat numberFormat = NumberFormat.getInstance();
83 
84 	/**
85 	 * Configuers the number format.
86 	 */
87 	static
88 	{
89 		numberFormat.setMaximumFractionDigits(2);
90 	}
91 
92 	/**
93 	 * Defines the inactive background border. Default is a not-so-dark gray.
94 	 */
95 	protected Color inactiveBackground = new Color(170, 170, 170);
96 
97 	/**
98 	 * Specifies the orientation.
99 	 */
100 	protected int orientation = ORIENTATION_HORIZONTAL;
101 
102 	/**
103 	 * Specified that start and length of the active region, ie the region to
104 	 * paint with the background border. This is used for example to indicate
105 	 * the printable region of a graph.
106 	 */
107 	protected int activeoffset, activelength;
108 
109 	/**
110 	 * Specifies the scale for the metrics. Default is
111 	 * {@link JGraphEditorDiagramPane#DEFAULT_PAGESCALE}.
112 	 */
113 	protected double scale = DEFAULT_PAGESCALE;
114 
115 	/**
116 	 * Specifies the unit system. Default is
117 	 * {@link JGraphEditorDiagramPane#DEFAULT_ISMETRIC}.
118 	 */
119 	protected boolean metric = DEFAULT_ISMETRIC;
120 
121 	/**
122 	 *
123 	 */
124 	protected Font labelFont = new Font("Tahoma", Font.PLAIN, 9);
125 
126 	/**
127 	 * Specifies height or width of the ruler. Default is 15 pixels.
128 	 */
129 	protected int rulerSize = 16;
130 
131 	/**
132 	 * Specifies the minimum distance between two major ticks. Default is 30.
133 	 */
134 	protected int tickDistance = 30;
135 
136 	/**
137 	 * Reference to the attached graph.
138 	 */
139 	protected mxGraphComponent graphComponent;
140 
141 	/**
142 	 * Holds the current and first mouse point.
143 	 */
144 	protected Point mouse = new Point();
145 
146 	/**
147 	 * Parameters to control the display.
148 	 */
149 	protected double increment, units;
150 
151 	/**
152 	 *
153 	 */
154 	protected transient mxIEventListener repaintHandler = new mxIEventListener()
155 	{
156 		public void invoke(Object source, mxEventObject evt)
157 		{
158 			repaint();
159 		}
160 	};
161 
162 	/**
163 	 * Constructs a new ruler for the specified graph and orientation.
164 	 *
165 	 * @param graph
166 	 *            The graph to create the ruler for.
167 	 * @param orientation
168 	 *            The orientation to use for the ruler.
169 	 */
EditorRuler(mxGraphComponent graphComponent, int orientation)170 	public EditorRuler(mxGraphComponent graphComponent, int orientation)
171 	{
172 		this.orientation = orientation;
173 		this.graphComponent = graphComponent;
174 		updateIncrementAndUnits();
175 
176 		graphComponent.getGraph().getView().addListener(
177 				mxEvent.SCALE, repaintHandler);
178 		graphComponent.getGraph().getView().addListener(
179 				mxEvent.TRANSLATE, repaintHandler);
180 		graphComponent.getGraph().getView().addListener(
181 				mxEvent.SCALE_AND_TRANSLATE, repaintHandler);
182 
183 		graphComponent.getGraphControl().addMouseMotionListener(this);
184 
185 		DropTarget dropTarget = graphComponent.getDropTarget();
186 
187 		try
188 		{
189 			if (dropTarget != null)
190 			{
191 				dropTarget.addDropTargetListener(this);
192 			}
193 		}
194 		catch (TooManyListenersException tmle)
195 		{
196 			// should not happen... swing drop target is multicast
197 		}
198 
199 		setBorder(BorderFactory.createLineBorder(Color.black));
200 	}
201 
202 	/**
203 	 * Sets the start of the active region in pixels.
204 	 *
205 	 * @param offset
206 	 *            The start of the active region.
207 	 */
setActiveOffset(int offset)208 	public void setActiveOffset(int offset)
209 	{
210 		activeoffset = (int) (offset * scale);
211 	}
212 
213 	/**
214 	 * Sets the length of the active region in pixels.
215 	 *
216 	 * @param length
217 	 *            The length of the active region.
218 	 */
setActiveLength(int length)219 	public void setActiveLength(int length)
220 	{
221 		activelength = (int) (length * scale);
222 	}
223 
224 	/**
225 	 * Returns true if the ruler uses metric units.
226 	 *
227 	 * @return Returns if the ruler is metric.
228 	 */
isMetric()229 	public boolean isMetric()
230 	{
231 		return metric;
232 	}
233 
234 	/**
235 	 * Sets if the ruler uses metric units.
236 	 *
237 	 * @param isMetric
238 	 *            Whether to use metric units.
239 	 */
setMetric(boolean isMetric)240 	public void setMetric(boolean isMetric)
241 	{
242 		this.metric = isMetric;
243 		updateIncrementAndUnits();
244 		repaint();
245 	}
246 
247 	/**
248 	 * Returns the ruler's horizontal or vertical size.
249 	 *
250 	 * @return Returns the rulerSize.
251 	 */
getRulerSize()252 	public int getRulerSize()
253 	{
254 		return rulerSize;
255 	}
256 
257 	/**
258 	 * Sets the ruler's horizontal or vertical size.
259 	 *
260 	 * @param rulerSize
261 	 *            The rulerSize to set.
262 	 */
setRulerSize(int rulerSize)263 	public void setRulerSize(int rulerSize)
264 	{
265 		this.rulerSize = rulerSize;
266 	}
267 
268 	/**
269 	 *
270 	 */
setTickDistance(int tickDistance)271 	public void setTickDistance(int tickDistance)
272 	{
273 		this.tickDistance = tickDistance;
274 	}
275 
276 	/**
277 	 *
278 	 */
getTickDistance()279 	public int getTickDistance()
280 	{
281 		return tickDistance;
282 	}
283 
284 	/**
285 	 * Returns the preferred size by replacing the respective component of the
286 	 * graph's preferred size with {@link #rulerSize}.
287 	 *
288 	 * @return Returns the preferred size for the ruler.
289 	 */
getPreferredSize()290 	public Dimension getPreferredSize()
291 	{
292 		Dimension dim = graphComponent.getGraphControl().getPreferredSize();
293 
294 		if (orientation == ORIENTATION_VERTICAL)
295 		{
296 			dim.width = rulerSize;
297 		}
298 		else
299 		{
300 			dim.height = rulerSize;
301 		}
302 
303 		return dim;
304 	}
305 
306 	/*
307 	 * (non-Javadoc)
308 	 * @see java.awt.dnd.DropTargetListener#dragEnter(java.awt.dnd.DropTargetDragEvent)
309 	 */
dragEnter(DropTargetDragEvent arg0)310 	public void dragEnter(DropTargetDragEvent arg0)
311 	{
312 		// empty
313 	}
314 
315 	/*
316 	 * (non-Javadoc)
317 	 * @see java.awt.dnd.DropTargetListener#dragExit(java.awt.dnd.DropTargetEvent)
318 	 */
dragExit(DropTargetEvent arg0)319 	public void dragExit(DropTargetEvent arg0)
320 	{
321 		// empty
322 	}
323 
324 	/*
325 	 * (non-Javadoc)
326 	 * @see java.awt.dnd.DropTargetListener#dragOver(java.awt.dnd.DropTargetDragEvent)
327 	 */
dragOver(final DropTargetDragEvent arg0)328 	public void dragOver(final DropTargetDragEvent arg0)
329 	{
330 		updateMousePosition(arg0.getLocation());
331 	}
332 
333 	/*
334 	 * (non-Javadoc)
335 	 * @see java.awt.dnd.DropTargetListener#drop(java.awt.dnd.DropTargetDropEvent)
336 	 */
drop(DropTargetDropEvent arg0)337 	public void drop(DropTargetDropEvent arg0)
338 	{
339 		// empty
340 	}
341 
342 	/*
343 	 * (non-Javadoc)
344 	 * @see java.awt.dnd.DropTargetListener#dropActionChanged(java.awt.dnd.DropTargetDragEvent)
345 	 */
dropActionChanged(DropTargetDragEvent arg0)346 	public void dropActionChanged(DropTargetDragEvent arg0)
347 	{
348 		// empty
349 	}
350 
351 	/*
352 	 * (non-Javadoc)
353 	 */
mouseMoved(MouseEvent e)354 	public void mouseMoved(MouseEvent e)
355 	{
356 		updateMousePosition(e.getPoint());
357 	}
358 
359 	/*
360 	 * (non-Javadoc)
361 	 */
mouseDragged(MouseEvent e)362 	public void mouseDragged(MouseEvent e)
363 	{
364 		updateMousePosition(e.getPoint());
365 	}
366 
367 	/**
368 	 * Repaints the mouse position.
369 	 */
updateMousePosition(Point pt)370 	protected void updateMousePosition(Point pt)
371 	{
372 		Point old = mouse;
373 		mouse = pt;
374 		repaint(old.x, old.y);
375 		repaint(mouse.x, mouse.y);
376 	}
377 
378 	/**
379 	 * Updates the local variables used for painting based on the current scale
380 	 * and unit system.
381 	 */
updateIncrementAndUnits()382 	protected void updateIncrementAndUnits()
383 	{
384 		double graphScale = graphComponent.getGraph().getView().getScale();
385 
386 		if (metric)
387 		{
388 			units = INCH / 2.54; // 2.54 dots per centimeter
389 			units *= graphComponent.getPageScale() * graphScale;
390 			increment = units;
391 		}
392 		else
393 		{
394 			units = INCH;
395 			units *= graphComponent.getPageScale() * graphScale;
396 			increment = units / 2;
397 		}
398 	}
399 
400 	/**
401 	 * Repaints the ruler between the specified 0 and x or y depending on the
402 	 * orientation.
403 	 *
404 	 * @param x
405 	 *            The endpoint for repainting a horizontal ruler.
406 	 * @param y
407 	 *            The endpoint for repainting a vertical ruler.
408 	 */
repaint(int x, int y)409 	public void repaint(int x, int y)
410 	{
411 		if (orientation == ORIENTATION_VERTICAL)
412 		{
413 			repaint(0, y, rulerSize, 1);
414 		}
415 		else
416 		{
417 			repaint(x, 0, 1, rulerSize);
418 		}
419 	}
420 
421 	/**
422 	 * Paints the ruler.
423 	 *
424 	 * @param g
425 	 *            The graphics to paint the ruler to.
426 	 */
paintComponent(Graphics g)427 	public void paintComponent(Graphics g)
428 	{
429 		mxGraph graph = graphComponent.getGraph();
430 		Rectangle clip = g.getClipBounds();
431 		updateIncrementAndUnits();
432 
433 		// Fills clipping area with background.
434 		if (activelength > 0 && inactiveBackground != null)
435 		{
436 			g.setColor(inactiveBackground);
437 		}
438 		else
439 		{
440 			g.setColor(getBackground());
441 		}
442 
443 		g.fillRect(clip.x, clip.y, clip.width, clip.height);
444 
445 		// Draws the active region.
446 		g.setColor(getBackground());
447 		Point2D p = new Point2D.Double(activeoffset, activelength);
448 
449 		if (orientation == ORIENTATION_HORIZONTAL)
450 		{
451 			g.fillRect((int) p.getX(), clip.y, (int) p.getY(), clip.height);
452 		}
453 		else
454 		{
455 			g.fillRect(clip.x, (int) p.getX(), clip.width, (int) p.getY());
456 		}
457 
458 		double left = clip.getX();
459 		double top = clip.getY();
460 		double right = left + clip.getWidth();
461 		double bottom = top + clip.getHeight();
462 
463 		// Fetches some global display state information
464 		mxPoint trans = graph.getView().getTranslate();
465 		double scale = graph.getView().getScale();
466 		double tx = trans.getX() * scale;
467 		double ty = trans.getY() * scale;
468 
469 		// Sets the distance of the grid lines in pixels
470 		double stepping = increment;
471 
472 		if (stepping < tickDistance)
473 		{
474 			int count = (int) Math
475 					.round(Math.ceil(tickDistance / stepping) / 2) * 2;
476 			stepping = count * stepping;
477 		}
478 
479 		// Creates a set of strokes with individual dash offsets
480 		// for each direction
481 		((Graphics2D) g).setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
482 				RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
483 		g.setFont(labelFont);
484 		g.setColor(Color.black);
485 
486 		int smallTick = rulerSize - rulerSize / 3;
487 		int middleTick = rulerSize / 2;
488 
489 		// TODO: Merge into a single drawing loop for both orientations
490 		if (orientation == ORIENTATION_HORIZONTAL)
491 		{
492 			double xs = Math.floor((left - tx) / stepping) * stepping + tx;
493 			double xe = Math.ceil(right / stepping) * stepping;
494 			xe += (int) Math.ceil(stepping);
495 
496 			for (double x = xs; x <= xe; x += stepping)
497 			{
498 				// FIXME: Workaround for rounding errors when adding stepping to
499 				// xs or ys multiple times (leads to double grid lines when zoom
500 				// is set to eg. 121%)
501 				double xx = Math.round((x - tx) / stepping) * stepping + tx;
502 
503 				int ix = (int) Math.round(xx);
504 				g.drawLine(ix, rulerSize, ix, 0);
505 
506 				String text = format((x - tx) / increment);
507 				g.drawString(text, ix + 2, labelFont.getSize());
508 
509 				ix += (int) Math.round(stepping / 4);
510 				g.drawLine(ix, rulerSize, ix, smallTick);
511 
512 				ix += (int) Math.round(stepping / 4);
513 				g.drawLine(ix, rulerSize, ix, middleTick);
514 
515 				ix += (int) Math.round(stepping / 4);
516 				g.drawLine(ix, rulerSize, ix, smallTick);
517 			}
518 		}
519 		else
520 		{
521 			double ys = Math.floor((top - ty) / stepping) * stepping + ty;
522 			double ye = Math.ceil(bottom / stepping) * stepping;
523 			ye += (int) Math.ceil(stepping);
524 
525 			for (double y = ys; y <= ye; y += stepping)
526 			{
527 				// FIXME: Workaround for rounding errors when adding stepping to
528 				// xs or ys multiple times (leads to double grid lines when zoom
529 				// is set to eg. 121%)
530 				y = Math.round((y - ty) / stepping) * stepping + ty;
531 
532 				int iy = (int) Math.round(y);
533 				g.drawLine(rulerSize, iy, 0, iy);
534 
535 				String text = format((y - ty) / increment);
536 
537 				// Rotates the labels in the vertical ruler
538 				AffineTransform at = ((Graphics2D) g).getTransform();
539 				((Graphics2D) g).rotate(-Math.PI / 2, 0, iy);
540 				g.drawString(text, 1, iy + labelFont.getSize());
541 				((Graphics2D) g).setTransform(at);
542 
543 				iy += (int) Math.round(stepping / 4);
544 				g.drawLine(rulerSize, iy, smallTick, iy);
545 
546 				iy += (int) Math.round(stepping / 4);
547 				g.drawLine(rulerSize, iy, middleTick, iy);
548 
549 				iy += (int) Math.round(stepping / 4);
550 				g.drawLine(rulerSize, iy, smallTick, iy);
551 			}
552 		}
553 
554 		// Draw Mouseposition
555 		g.setColor(Color.green);
556 
557 		if (orientation == ORIENTATION_HORIZONTAL)
558 		{
559 			g.drawLine(mouse.x, rulerSize, mouse.x, 0);
560 		}
561 		else
562 		{
563 			g.drawLine(rulerSize, mouse.y, 0, mouse.y);
564 		}
565 	}
566 
567 	/**
568 	 * Fixes the formatting of -0.
569 	 */
format(double value)570 	private final String format(double value)
571 	{
572 		String text = numberFormat.format(value);
573 
574 		if (text.equals("-0"))
575 		{
576 			text = "0";
577 		}
578 
579 		return text;
580 	}
581 
582 }