blob: 685fa0fd02191ff03de16b699744482b8a75f765 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2003, 2010 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()) {
// UNSUPPORTED - getAscent() not implemented in RAP
prefSize.height = (textMargin * 2) + BORDER_WIDTH
// + FigureUtilities.getFontMetrics(getFont()).getAscent();
+ FigureUtilities.getFontMetrics(getFont()).getHeight();
} else {
// UNSUPPORTED - getLeading() not implemented in RAP
prefSize.width = (textMargin * 2) + BORDER_WIDTH
// + FigureUtilities.getFontMetrics(getFont()).getAscent();
+ FigureUtilities.getFontMetrics(getFont()).getHeight();
}
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.getSINGLETON()));
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;
// UNSUPPORTED - getLeading() not implemented in RAP
// int leading = FigureUtilities.getFontMetrics(getFont()).getLeading();
int leading = 0;
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);
}
}
}
}