/*******************************************************************************
* Copyright (c) 2003, 2005 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.gef.internal.ui.rulers;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.widgets.Display;
import org.eclipse.draw2d.ColorConstants;
import org.eclipse.draw2d.Figure;
import org.eclipse.draw2d.FigureUtilities;
import org.eclipse.draw2d.Graphics;
import org.eclipse.draw2d.ImageUtilities;
import org.eclipse.draw2d.geometry.Dimension;
import org.eclipse.draw2d.geometry.Point;
import org.eclipse.draw2d.geometry.Rectangle;
import org.eclipse.draw2d.geometry.Transposer;
import org.eclipse.gef.editparts.ZoomListener;
import org.eclipse.gef.editparts.ZoomManager;
import org.eclipse.gef.rulers.RulerProvider;
/**
* @author Pratik Shah
*/
public class RulerFigure
extends Figure
{
/**
* These fields allow the client to customize the look of the ruler.
*/
public int smallMarkWidth = 1;
public int mediumMarkWidth = 3;
public int textMargin = 3;
public int minPixelsBetweenMarks = 7;
public int minPixelsBetweenMajorMarks = 47;
protected Transposer transposer = new Transposer();
protected ZoomManager zoomManager;
private boolean drawFocus = false;
/*
* This is an artificial border. When asked for the preferred size, the figure adds
* this width to its preferred width. The border is painted in the paintFigure(Graphics)
* method.
*/
private static final int BORDER_WIDTH = 3;
private boolean horizontal;
private int unit, interval, divisions;
private double dpu = -1.0;
private ZoomListener zoomListener = new ZoomListener() {
public void zoomChanged(double newZoomValue) {
handleZoomChanged();
}
};
public RulerFigure(boolean isHorizontal, int measurementUnit) {
setHorizontal(isHorizontal);
setUnit(measurementUnit);
setBackgroundColor(ColorConstants.listBackground);
setForegroundColor(ColorConstants.listForeground);
setOpaque(true);
setLayoutManager(new RulerLayout());
}
protected double getDPU() {
if (dpu <= 0) {
if (getUnit() == RulerProvider.UNIT_PIXELS) {
dpu = 1.0;
} else {
dpu = transposer.t(new Dimension(Display.getCurrent().getDPI())).height;
if (getUnit() == RulerProvider.UNIT_CENTIMETERS) {
dpu = dpu / 2.54;
}
}
if (zoomManager != null) {
dpu = dpu * zoomManager.getZoom();
}
}
return dpu;
}
public boolean getDrawFocus() {
return drawFocus;
}
public Dimension getPreferredSize(int wHint, int hHint) {
Dimension prefSize = new Dimension();
if (isHorizontal()) {
prefSize.height = (textMargin * 2) + BORDER_WIDTH
+ FigureUtilities.getFontMetrics(getFont()).getAscent();
} else {
prefSize.width = (textMargin * 2) + BORDER_WIDTH
+ FigureUtilities.getFontMetrics(getFont()).getAscent();
}
return prefSize;
}
public int getUnit() {
return unit;
}
protected void handleZoomChanged() {
dpu = -1.0;
repaint();
layout();
}
/* (non-Javadoc)
* @see org.eclipse.draw2d.Figure#invalidate()
*/
public void invalidate() {
super.invalidate();
dpu = -1.0;
}
public boolean isHorizontal() {
return horizontal;
}
/*
* @TODO:Pratik re-comment this algorithm and the setInterval method
*/
/*
* (non-Javadoc)
* @see org.eclipse.draw2d.Figure#paintFigure(org.eclipse.draw2d.Graphics)
*/
protected void paintFigure(Graphics graphics) {
/*
* @TODO:Pratik maybe you can break this method into a few methods. that might
* make it a little easier to read and understand. plus, sub-classes could customize
* certain parts.
*/
double dotsPerUnit = getDPU();
Rectangle clip = transposer.t(graphics.getClip(Rectangle.SINGLETON));
Rectangle figClientArea = transposer.t(getClientArea());
// Use the x and width of the client area, but the y and height of the clip as the
// bounds of the area which is to be repainted. This will increase performance as the
// entire ruler will not be repainted everytime.
Rectangle clippedBounds = clip;
clippedBounds.x = figClientArea.x;
clippedBounds.width = figClientArea.width - BORDER_WIDTH;
// Paint the background
if (isOpaque()) {
graphics.fillRectangle(transposer.t(clippedBounds));
}
/*
* A major mark is one that goes all the way from the left edge to the right edge of
* a ruler and for which a number is displayed. Determine the minimum number of
* pixels that are to be left between major marks. This will, in turn, help
* determine how many units are to be displayed per major mark. A major mark should
* have at least enough pixels to display the text and its padding. We take into the
* consideration the max of text's width and height so that for horizontal and
* vertical rulers that are of the same height, the number of units per major mark is
* the same.
*/
int unitsPerMajorMark = (int)(minPixelsBetweenMajorMarks / dotsPerUnit);
if (minPixelsBetweenMajorMarks % dotsPerUnit != 0.0) {
unitsPerMajorMark++;
}
if (interval > 0) {
/*
* If the client specified how many units are to be displayed per major mark, use
* that. If, however, showing that many units wouldn't leave enough room for the
* text, than take its smallest multiple that would leave enough room.
*/
int intervalMultiple = interval;
while (intervalMultiple < unitsPerMajorMark) {
intervalMultiple += interval;
}
unitsPerMajorMark = intervalMultiple;
} else if (unitsPerMajorMark != 1 && unitsPerMajorMark % 2 != 0) {
// if the number of units per major mark is calculated dynamically, ensure that
// it is an even number.
unitsPerMajorMark++;
}
/*
* divsPerMajorMark indicates the number of divisions that a major mark should be
* divided into. for eg., a value of 2 would mean that a major mark would be shown
* as having two parts. that means that there would be a marker showing the beginning
* and end of the major marker and another right in the middle.
*/
int divsPerMajorMark;
if (divisions > 0
&& dotsPerUnit * unitsPerMajorMark / divisions >= minPixelsBetweenMarks) {
/*
* If the client has specified the number of divisions per major mark, use that
* unless it would cause the minimum space between marks to be less than
* minPixelsBetweenMarks
*/
divsPerMajorMark = divisions;
} else {
/*
* If the client hasn't specified the number of divisions per major mark or the
* one that the client has specified is invalid, then calculate it dynamically.
* This algorithm will try to display 10 divisions per CM, and 16 per INCH.
* However, if that puts the marks too close together (i.e., the space between
* them is less than minPixelsBetweenMarks), then it keeps decreasing the number
* of divisions by a factor of 2 until there is enough space between them.
*/
divsPerMajorMark = 2;
if (getUnit() == RulerProvider.UNIT_CENTIMETERS) {
divsPerMajorMark = 10;
} else if (getUnit() == RulerProvider.UNIT_INCHES) {
divsPerMajorMark = 8;
}
while (dotsPerUnit * unitsPerMajorMark / divsPerMajorMark < minPixelsBetweenMarks) {
divsPerMajorMark /= 2;
if (divsPerMajorMark == 0) {
break;
}
}
// This should never happen unless the client has specified a
// minPixelsBetweenMarks that is larger than minPixelsBetweenMajorMarks (which
// is calculated using the text's size -- size of the largest number to be
// displayed).
if (divsPerMajorMark == 0) {
divsPerMajorMark = 1;
}
}
/*
* mediumMarkerDivNum is used to determine which mark (line drawn to indicate a
* point on the ruler) in a major mark will be of medium size. If its value is 1 then
* every mark will be of medium size. If its value is 5, then every 5th mark will
* be of medium size (the rest being of small size).
*/
int mediumMarkerDivNum = 1;
switch (divsPerMajorMark) {
case 20:
case 10:
case 5:
mediumMarkerDivNum = 5;
break;
case 16:
case 8:
mediumMarkerDivNum = 4;
break;
case 4:
mediumMarkerDivNum = 2;
break;
case 2:
mediumMarkerDivNum = 1;
}
/*
* dotsPerDivision = number of pixels between each mark = number of pixels in a
* division
*/
double dotsPerDivision = dotsPerUnit * unitsPerMajorMark / divsPerMajorMark;
/*
* startMark is the division/mark from which we are going to start painting. It
* should be the last major mark (one for which a number is displayed) that is before
* the top of the clip rectangle.
*/
int startMark = (int)(clippedBounds.y / (dotsPerUnit * unitsPerMajorMark))
* divsPerMajorMark;
if (clippedBounds.y < 0) {
// -2 / 10 = 0, not -1. so, if the top of the clip is negative, we need to move
// the startMark back by a whole major mark.
startMark -= divsPerMajorMark;
}
// endMark is the first non-visible mark (doesn't have to be a major mark) that is
// beyond the end of the clip region
int endMark = (int)(((clippedBounds.y + clippedBounds.height) / dotsPerDivision)) + 1;
int leading = FigureUtilities.getFontMetrics(getFont()).getLeading();
Rectangle forbiddenZone = new Rectangle();
for (int div = startMark; div <= endMark; div++) {
// y is the vertical position of the mark
int y = (int)(div * dotsPerDivision);
if (div % divsPerMajorMark == 0) {
String num = "" + (div / divsPerMajorMark) * unitsPerMajorMark; //$NON-NLS-1$
if (isHorizontal()) {
Dimension numSize = FigureUtilities.getStringExtents(num, getFont());
/*
* If the width is even, we want to increase it by 1. This will ensure
* that when marks are erased because they are too close to the
* number, they are erased from both sides of that number.
*/
if (numSize.width % 2 == 0)
numSize.width++;
Point textLocation = new Point(y - (numSize.width / 2),
clippedBounds.x + textMargin - leading);
forbiddenZone.setLocation(textLocation);
forbiddenZone.setSize(numSize);
forbiddenZone.expand(1, 1);
graphics.fillRectangle(forbiddenZone);
// Uncomment the following line of code if you want to see a line at
// the exact position of the major mark
// graphics.drawLine(y, clippedBounds.x, y, clippedBounds.x + clippedBounds.width);
graphics.drawText(num, textLocation);
} else {
Image numImage = ImageUtilities.createRotatedImageOfString(num, getFont(),
getForegroundColor(), getBackgroundColor());
Point textLocation = new Point(clippedBounds.x + textMargin,
y - (numImage.getBounds().height / 2));
forbiddenZone.setLocation(textLocation);
forbiddenZone.setSize(numImage.getBounds().width,
numImage.getBounds().height);
forbiddenZone.expand(1, 1 + (numImage.getBounds().height % 2 == 0 ? 1 : 0));
graphics.fillRectangle(forbiddenZone);
graphics.drawImage(numImage, textLocation);
numImage.dispose();
}
} else if ((div % divsPerMajorMark) % mediumMarkerDivNum == 0) {
// this is a medium mark, so its length should be longer than the small marks
Point start = transposer.t(
new Point((clippedBounds.getRight().x - mediumMarkWidth) / 2, y));
Point end = transposer.t(new Point(((clippedBounds.getRight().x
- mediumMarkWidth) / 2) + mediumMarkWidth, y));
if (!forbiddenZone.contains(start)) {
graphics.drawLine(start, end);
}
} else {
// small mark
Point start = transposer.t(
new Point((clippedBounds.getRight().x - smallMarkWidth) / 2, y));
Point end = transposer.t(new Point(((clippedBounds.getRight().x
- smallMarkWidth) / 2) + smallMarkWidth, y));
if (!forbiddenZone.contains(start)) {
graphics.drawLine(start, end);
}
}
}
// paint the border
clippedBounds.expand(BORDER_WIDTH, 0);
graphics.setForegroundColor(ColorConstants.buttonDarker);
graphics.drawLine(transposer.t(clippedBounds.getTopRight().translate(-1, -1)),
transposer.t(clippedBounds.getBottomRight().translate(-1, -1)));
}
public void setDrawFocus(boolean drawFocus) {
if (this.drawFocus != drawFocus) {
this.drawFocus = drawFocus;
repaint();
}
}
public void setHorizontal(boolean isHorizontal) {
horizontal = isHorizontal;
transposer.setEnabled(isHorizontal);
}
/**
* Allows the client to set the number of units to be displayed per major mark, and the
* number of divisions to be shown per major mark.
*
* A number on the ruler is considered to be a major mark.
*
* @param unitsPerMajorMark if less than 1, it will be ignored; if there is not enough
* space to display that many units per major mark, its smallest multiple that leaves
* enough room will be used.
* @param divisionsPerMajorMark if less than 1, it will be ignored; if displaying
* that many divisions does not leave enough room between marks, it will be ignored.
*
*/
public void setInterval(int unitsPerMajorMark, int divisionsPerMajorMark) {
interval = unitsPerMajorMark;
divisions = divisionsPerMajorMark;
repaint();
}
public void setUnit(int newUnit) {
if (unit != newUnit) {
unit = newUnit;
dpu = -1.0;
repaint();
}
}
public void setZoomManager(ZoomManager manager) {
if (zoomManager != manager) {
if (zoomManager != null) {
zoomManager.removeZoomListener(zoomListener);
}
zoomManager = manager;
if (zoomManager != null) {
zoomManager.addZoomListener(zoomListener);
}
}
}
}