/******************************************************************************* * 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); } } } }