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