/*! * This program is free software; you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software * Foundation. * * You should have received a copy of the GNU Lesser General Public License along with this * program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html * or from the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU Lesser General Public License for more details. * * Copyright (c) 2002-2013 Pentaho Corporation.. All rights reserved. */ package org.pentaho.reporting.designer.core.editor.report; import org.pentaho.reporting.designer.core.model.CachedLayoutData; import org.pentaho.reporting.designer.core.model.ModelUtility; import org.pentaho.reporting.designer.core.settings.WorkspaceSettings; import org.pentaho.reporting.designer.core.util.DrawSelectionType; import org.pentaho.reporting.designer.core.util.IconLoader; import org.pentaho.reporting.engine.classic.core.Element; import org.pentaho.reporting.engine.classic.core.RootLevelBand; import org.pentaho.reporting.engine.classic.core.util.geom.StrictGeomUtility; import java.awt.*; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.awt.image.ImageObserver; /** * The selection overlay has a coordinate system that matches the origin (0,0) of the band. However, internally it holds * all coordinates as *non*-normalized coordinates, so the zoom-factor is already calculated in. * * @author Thomas Morgner */ public class SelectionOverlayInformation { public enum InRangeIndicator { NOT_IN_RANGE, MOVE, TOP_LEFT, TOP_CENTER, TOP_RIGHT, MIDDLE_LEFT, MIDDLE_RIGHT, BOTTOM_LEFT, BOTTOM_CENTER, BOTTOM_RIGHT, } private static final Color SELECTION_COLOR = Color.BLUE; private double zoomFactor; private Element selectedElement; private Rectangle2D.Double elementBounds; private Rectangle2D.Double nearRangeElementBounds; private long layoutAge; private static final BasicStroke DOTTED_STROKE = new BasicStroke( 1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 1, new float[] { 2, 2 }, 1 ); private static final int SELECTION_RANGE = 3; private CachedLayoutData selectedElementData; public SelectionOverlayInformation( final Element selectedElement ) { this.selectedElement = selectedElement; this.selectedElementData = ModelUtility.getCachedLayoutData( selectedElement ); elementBounds = new Rectangle2D.Double(); nearRangeElementBounds = new Rectangle2D.Double(); layoutAge = -1; } public Element getSelectedElement() { return selectedElement; } public double getZoomFactor() { return zoomFactor; } public void validate( final double zoomFactor ) { final double oldZoom = this.zoomFactor; if ( oldZoom != zoomFactor || this.layoutAge != selectedElementData.getLayoutAge() ) { this.zoomFactor = zoomFactor; this.layoutAge = selectedElementData.getLayoutAge(); final double x = StrictGeomUtility.toExternalValue( selectedElementData.getX() ); final double y = StrictGeomUtility.toExternalValue( selectedElementData.getY() ); final double width = StrictGeomUtility.toExternalValue( selectedElementData.getWidth() ); final double height = StrictGeomUtility.toExternalValue( selectedElementData.getHeight() ); elementBounds.setFrame( x * zoomFactor, y * zoomFactor, width * zoomFactor, height * zoomFactor ); nearRangeElementBounds.setFrame ( elementBounds.getX() - SELECTION_RANGE, elementBounds.getY() - SELECTION_RANGE, elementBounds.getWidth() + 2 * SELECTION_RANGE, elementBounds.getHeight() + 2 * SELECTION_RANGE ); } } public InRangeIndicator getMouseInRangeIndicator( final Point2D normalizedPoint ) { final double x = normalizedPoint.getX() * zoomFactor; final double y = normalizedPoint.getY() * zoomFactor; // See if we are close ... if not we should return fast if ( nearRangeElementBounds.contains( x, y ) == false ) { return InRangeIndicator.NOT_IN_RANGE; } // We are close ... see if we are on an important edge if ( isNear( x, elementBounds.getX() ) ) { if ( isNear( y, elementBounds.getY() ) ) { return InRangeIndicator.TOP_LEFT; } else if ( isNear( y, elementBounds.getY() + ( elementBounds.getHeight() / 2 ) ) ) { return InRangeIndicator.MIDDLE_LEFT; } else if ( isNear( y, elementBounds.getY() + elementBounds.getHeight() ) ) { return InRangeIndicator.BOTTOM_LEFT; } } else if ( isNear( x, elementBounds.getX() + ( elementBounds.getWidth() / 2 ) ) ) { if ( isNear( y, elementBounds.getY() ) ) { return InRangeIndicator.TOP_CENTER; } else if ( isNear( y, elementBounds.getY() + elementBounds.getHeight() ) ) { return InRangeIndicator.BOTTOM_CENTER; } } else if ( isNear( x, elementBounds.getX() + elementBounds.getWidth() ) ) { if ( isNear( y, elementBounds.getY() ) ) { return InRangeIndicator.TOP_RIGHT; } else if ( isNear( y, elementBounds.getY() + ( elementBounds.getHeight() / 2 ) ) ) { return InRangeIndicator.MIDDLE_RIGHT; } else if ( isNear( y, elementBounds.getY() + elementBounds.getHeight() ) ) { return InRangeIndicator.BOTTOM_RIGHT; } } // We are not on an important edge ... see if the point is in range // of the element at all. // NOTE: this check should not be performed unless we already know // that we are in the near range. if ( elementContainsPoint( x, y ) == false ) { return InRangeIndicator.NOT_IN_RANGE; } return InRangeIndicator.MOVE; } /** * Performs a contains test that accounts for the fact that some elements have no height or width. In that case, we * will check the near bounds. * <p/> * NOTE: this method should not be used until after the nearRangeElementBounds has been checked. * * @param x the x-value of the point being checked * @param y the y-value of the point being checked * @return <code>true</code> if the point is contained in the bounds of the element, <code>false</code> otherwise. */ private boolean elementContainsPoint( final double x, final double y ) { if ( elementBounds.contains( x, y ) ) { return true; } else if ( elementBounds.getHeight() < 1.0 || elementBounds.getWidth() < 1.0 ) { // The following line does not need to be executed because this method should not be called // unless the nearRangeElementBounds check has not been executed. // return nearRangeElementBounds.contains(x, y); return true; } return false; } private boolean isNear( final double location, final double center ) { return Math.abs( location - center ) < SELECTION_RANGE; } public void draw( final Graphics2D g2, final ImageObserver obs ) { g2.setStroke( new BasicStroke( 1 ) ); if ( WorkspaceSettings.getInstance().isAlwaysDrawElementFrames() ) { g2.setColor( Color.LIGHT_GRAY ); g2.draw( elementBounds ); } if ( selectedElement instanceof RootLevelBand ) { return; } final DrawSelectionType type = WorkspaceSettings.getInstance().getDrawSelectionType(); if ( type == DrawSelectionType.CLAMP ) { g2.setColor( SELECTION_COLOR ); drawClampRectangle( g2, elementBounds ); } else if ( type == DrawSelectionType.OUTLINE ) { g2.setColor( Color.GRAY ); g2.setStroke( DOTTED_STROKE ); g2.draw( elementBounds ); final Image img = IconLoader.getInstance().getSelectionEdge().getImage(); final int halfWidth = img.getWidth( null ) / 2; final int halfHeight = img.getHeight( null ) / 2; final int leftEdge = -halfWidth + (int) elementBounds.getX(); final int rightEdge = -halfWidth + (int) ( elementBounds.getX() + elementBounds.getWidth() ); final int bottomEdge = -halfHeight + (int) ( elementBounds.getY() + elementBounds.getHeight() ); final int centerHeight = -halfHeight + (int) ( elementBounds.getY() + ( elementBounds.getHeight() / 2 ) ); final int centerWidth = -halfWidth + (int) ( elementBounds.getX() + ( elementBounds.getWidth() / 2 ) ); final int topEdge = -halfHeight + (int) elementBounds.getY(); g2.drawImage( img, leftEdge, topEdge, obs ); g2.drawImage( img, centerWidth, topEdge, obs ); g2.drawImage( img, rightEdge, topEdge, obs ); g2.drawImage( img, leftEdge, centerHeight, obs ); g2.drawImage( img, rightEdge, centerHeight, obs ); g2.drawImage( img, leftEdge, bottomEdge, obs ); g2.drawImage( img, centerWidth, bottomEdge, obs ); g2.drawImage( img, rightEdge, bottomEdge, obs ); } } private void drawClampRectangle( final Graphics2D g2d, final Rectangle2D rect ) { // top final double x = rect.getX(); final int x1 = (int) x; final int y1 = (int) rect.getY(); g2d.drawLine( x1, y1, x1 + SELECTION_RANGE, y1 ); g2d.drawLine( x1, y1, x1, y1 + SELECTION_RANGE ); final double width = rect.getWidth(); final double centerX = x + width / 2; g2d.drawLine( (int) centerX - 2, y1, (int) centerX + 2, y1 ); g2d.drawLine( (int) centerX, y1 + 1, (int) centerX, y1 + 2 ); final double x2 = x + width; g2d.drawLine( (int) ( x2 - SELECTION_RANGE ), y1, (int) x2, y1 ); g2d.drawLine( (int) x2, y1, (int) x2, y1 + SELECTION_RANGE ); // middle final double centerY = rect.getY() + rect.getHeight() / 2; g2d.drawLine( x1, (int) centerY - 2, x1, (int) centerY + 2 ); g2d.drawLine( x1 + 1, (int) centerY, x1 + 2, (int) centerY ); g2d.drawLine( (int) x2, (int) centerY - 2, (int) x2, (int) centerY + 2 ); g2d.drawLine( (int) ( x2 - 2 ), (int) centerY, (int) x2 - 1, (int) centerY ); // low final double y2 = rect.getY() + rect.getHeight(); g2d.drawLine( x1, (int) ( y2 - SELECTION_RANGE ), x1, (int) y2 ); g2d.drawLine( x1, (int) y2, x1 + SELECTION_RANGE, (int) y2 ); g2d.drawLine( (int) centerX - 2, (int) y2, (int) centerX + 2, (int) y2 ); g2d.drawLine( (int) centerX, (int) y2 - 2, (int) centerX, (int) y2 - 1 ); g2d.drawLine( (int) ( x2 - SELECTION_RANGE ), (int) y2, (int) x2, (int) y2 ); g2d.drawLine( (int) x2, (int) ( y2 - SELECTION_RANGE ), (int) x2, (int) y2 ); } public String toString() { return "org.pentaho.reporting.designer.core.editor.report.SelectionOverlayInformation{" + // NON-NLS "zoomFactor=" + zoomFactor + // NON-NLS ", selectedElement=" + selectedElement + // NON-NLS ", elementBounds=" + elementBounds + // NON-NLS ", layoutAge=" + layoutAge + // NON-NLS '}'; // NON-NLS } }