/* * Copyright 2006-2012 ICEsoft Technologies Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the * License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an "AS * IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language * governing permissions and limitations under the License. */ package org.icepdf.core.views.swing; import org.icepdf.core.pobjects.Page; import org.icepdf.core.pobjects.Document; import org.icepdf.core.pobjects.annotations.Annotation; import org.icepdf.core.pobjects.annotations.LinkAnnotation; import org.icepdf.core.pobjects.annotations.AnnotationState; import org.icepdf.core.util.ColorUtil; import org.icepdf.core.util.Defs; import org.icepdf.core.util.PropertyConstants; import org.icepdf.core.views.DocumentViewController; import org.icepdf.core.views.DocumentViewModel; import org.icepdf.core.views.AnnotationComponent; import org.icepdf.core.views.PageViewComponent; import javax.swing.*; import javax.swing.event.MouseInputListener; import java.awt.*; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.awt.event.MouseEvent; import java.awt.geom.AffineTransform; import java.awt.geom.GeneralPath; import java.awt.geom.NoninvertibleTransformException; import java.awt.geom.Rectangle2D; import java.util.logging.Level; import java.util.logging.Logger; /** * Represents an editable annotations component. This class wraps an Annotation * object which already knows how to paint itself and adds a hooks for * editing the annotation via mouse manipulation. * * @since 4.0 */ public class AnnotationComponentImpl extends JComponent implements FocusListener, MouseInputListener, AnnotationComponent { private static final Logger logger = Logger.getLogger(AnnotationComponentImpl.class.toString()); // disable/enable file caching, overrides fileCachingSize. private static boolean isInteractiveAnnotationsEnabled; private static Color annotationHighlightColor; private static float annotationHighlightAlpha; static { // enables interactive annotation support. isInteractiveAnnotationsEnabled = Defs.sysPropertyBoolean( "org.icepdf.core.annotations.interactive.enabled", true); // sets annotation selected highlight colour try { String color = Defs.sysProperty( "org.icepdf.core.views.page.annotation.highlight.color", "#000000"); int colorValue = ColorUtil.convertColor(color); annotationHighlightColor = new Color(colorValue >= 0 ? colorValue : Integer.parseInt("000000", 16)); } catch (NumberFormatException e) { if (logger.isLoggable(Level.WARNING)) { logger.warning("Error reading page annotation highlight colour"); } } // set the annotation alpha value. // sets annotation selected highlight colour try { String alpha = Defs.sysProperty( "org.icepdf.core.views.page.annotation.highlight.alpha", "0.4"); annotationHighlightAlpha = Float.parseFloat(alpha); } catch (NumberFormatException e) { if (logger.isLoggable(Level.WARNING)) { logger.warning("Error reading page annotation highlight alpha"); } annotationHighlightAlpha = 0.4f; } } public static final int resizeBoxSize = 4; // reusable border private static ResizableBorder resizableBorder = new ResizableBorder(resizeBoxSize); private AbstractPageViewComponent pageViewComponent; private DocumentViewController documentViewController; private DocumentViewModel documentViewModel; private float currentZoom; private float currentRotation; protected Annotation annotation; private boolean isMousePressed; private boolean resized; private boolean wasResized; // border state flags. private boolean isEditable; private boolean isRollover; private boolean isLinkAnnot; private boolean isBorderStyle; private boolean isSelected; // selection, move and resize handling. private int cursor; private Point startPos; private AnnotationState previousAnnotationState; public AnnotationComponentImpl(Annotation annotation, DocumentViewController documentViewController, AbstractPageViewComponent pageViewComponent, DocumentViewModel documentViewModel) { this.pageViewComponent = pageViewComponent; this.documentViewModel = documentViewModel; this.documentViewController = documentViewController; this.annotation = annotation; addMouseListener(this); addMouseMotionListener(this); // disabled focus until we are ready to implement our own handler. // setFocusable(true); // addFocusListener(this); // setup a resizable border. setLayout(new BorderLayout()); setBorder(resizableBorder); // set component location and original size. Page currentPage = pageViewComponent.getPageLock(this); AffineTransform at = currentPage.getPageTransform( documentViewModel.getPageBoundary(), documentViewModel.getViewRotation(), documentViewModel.getViewZoom()); pageViewComponent.releasePageLock(currentPage, this); Rectangle location = at.createTransformedShape(annotation.getUserSpaceRectangle()).getBounds(); setBounds(location); // update zoom and rotation state currentRotation = documentViewModel.getViewRotation(); currentZoom = documentViewModel.getViewZoom(); // get border info isLinkAnnot = annotation instanceof LinkAnnotation; isBorderStyle = annotation.isBorder(); } public Document getDocument() { return documentViewModel.getDocument(); } public int getPageIndex() { return pageViewComponent.getPageIndex(); } public PageViewComponent getParentPageView() { return pageViewComponent; } public AbstractPageViewComponent getPageViewComponent() { return pageViewComponent; } public void removeMouseListeners() { removeMouseListener(this); removeMouseMotionListener(this); } public Annotation getAnnotation() { return annotation; } public void focusGained(FocusEvent e) { // repaint(); } public void focusLost(FocusEvent e) { // repaint(); // if we've lost focus then drop the selected state // isSelected = false; } private void resize() { if (getParent() != null) { ((JComponent) getParent()).revalidate(); } resized = true; } /** * Refreshses the components bounds for the current page transformation. * Bounds have are allready in user space. */ public void refreshDirtyBounds(){ Page currentPage = pageViewComponent.getPageLock(this); AffineTransform at = currentPage.getPageTransform( documentViewModel.getPageBoundary(), documentViewModel.getViewRotation(), documentViewModel.getViewZoom()); pageViewComponent.releasePageLock(currentPage, this); setBounds(commonBoundsNormalization(new GeneralPath( annotation.getUserSpaceRectangle()), at)); } /** * Refreshes/transforms the page space bounds back to user space. This * must be done in order refresh the annotation user space rectangle after * UI manipulation, otherwise the annotation will be incorrectly located * on the next repaint. */ public void refreshAnnotationRect(){ Page currentPage = pageViewComponent.getPageLock(this); AffineTransform at = currentPage.getPageTransform( documentViewModel.getPageBoundary(), documentViewModel.getViewRotation(), documentViewModel.getViewZoom()); pageViewComponent.releasePageLock(currentPage, this); try { at = at.createInverse(); } catch (NoninvertibleTransformException e1) { e1.printStackTrace(); } // store the new annotation rectangle in its original user space Rectangle2D rect = annotation.getUserSpaceRectangle(); Rectangle bounds = getBounds(); rect.setRect(commonBoundsNormalization(new GeneralPath(bounds), at)); } /** * Normalizes and the given path with the specified transform. The method * also rounds the Rectangle2D bounds values when creating a new rectangle * instead of trunkating the values. * * @param shapePath path to apply transform to * @param at tranfor to apply to shapePath * @return bound value of the shape path. */ private Rectangle commonBoundsNormalization(GeneralPath shapePath, AffineTransform at){ shapePath.transform(at); Rectangle2D pageSpaceBound = shapePath.getBounds2D(); return new Rectangle( (int)Math.round(pageSpaceBound.getX()), (int)Math.round(pageSpaceBound.getY()), (int)Math.round(pageSpaceBound.getWidth()), (int)Math.round(pageSpaceBound.getHeight())); } public void validate() { if (currentZoom != documentViewModel.getViewZoom() || currentRotation != documentViewModel.getViewRotation()) { refreshDirtyBounds(); currentRotation = documentViewModel.getViewRotation(); currentZoom = documentViewModel.getViewZoom(); } if (resized) { refreshAnnotationRect(); if (getParent() != null) { ((JComponent) getParent()).revalidate(); getParent().repaint(); } resized = false; wasResized = true; } } public void paintComponent(Graphics g) { Page currentPage = pageViewComponent.getPageLock(this); if (currentPage != null && currentPage.isInitiated()) { // update bounds for for component if (currentZoom != documentViewModel.getViewZoom() || currentRotation != documentViewModel.getViewRotation()) { validate(); } } pageViewComponent.releasePageLock(currentPage, this); // sniff out tool bar state to set correct annotation border isEditable = ( (documentViewModel.getViewToolMode() == DocumentViewModel.DISPLAY_TOOL_SELECTION || documentViewModel.getViewToolMode() == DocumentViewModel.DISPLAY_TOOL_LINK_ANNOTATION) && !(annotation.getFlagReadOnly() || annotation.getFlagLocked() || annotation.getFlagInvisible() || annotation.getFlagHidden())); // paint rollover effects. if (isMousePressed && !(documentViewModel.getViewToolMode() == DocumentViewModel.DISPLAY_TOOL_SELECTION || documentViewModel.getViewToolMode() == DocumentViewModel.DISPLAY_TOOL_LINK_ANNOTATION)) { Graphics2D gg2 = (Graphics2D) g; if (annotation instanceof LinkAnnotation) { LinkAnnotation linkAnnotation = (LinkAnnotation) annotation; String highlightMode = linkAnnotation.getHighlightMode(); Rectangle2D rect = new Rectangle(0, 0, getWidth(), getHeight()); if (LinkAnnotation.HIGHLIGHT_INVERT.equals(highlightMode)) { gg2.setColor(annotationHighlightColor); gg2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, annotationHighlightAlpha)); gg2.fillRect((int) rect.getX(), (int) rect.getY(), (int) rect.getWidth(), (int) rect.getHeight()); } else if (LinkAnnotation.HIGHLIGHT_OUTLINE.equals(highlightMode)) { gg2.setColor(annotationHighlightColor); gg2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, annotationHighlightAlpha)); gg2.drawRect((int) rect.getX(), (int) rect.getY(), (int) rect.getWidth(), (int) rect.getHeight()); } else if (LinkAnnotation.HIGHLIGHT_PUSH.equals(highlightMode)) { gg2.setColor(annotationHighlightColor); gg2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, annotationHighlightAlpha)); gg2.drawRect((int) rect.getX(), (int) rect.getY(), (int) rect.getWidth(), (int) rect.getHeight()); } } } } public void setBounds(int x, int y, int width, int height) { super.setBounds(x, y, width, height); } public void mouseMoved(MouseEvent me) { int toolMode = documentViewModel.getViewToolMode(); if (toolMode == DocumentViewModel.DISPLAY_TOOL_SELECTION && !(annotation.getFlagLocked() || annotation.getFlagReadOnly())) { ResizableBorder border = (ResizableBorder) getBorder(); setCursor(Cursor.getPredefinedCursor(border.getCursor(me))); }else if (toolMode == DocumentViewModel.DISPLAY_TOOL_LINK_ANNOTATION) { // keep it the same } else{ // set cursor back to the hand cursor. setCursor(documentViewController.getViewCursor( DocumentViewController.CURSOR_HAND_ANNOTATION)); } } public void mouseExited(MouseEvent mouseEvent) { setCursor(Cursor.getDefaultCursor()); isRollover = false; repaint(); } public void mouseClicked(MouseEvent e) { // clear the selection. // requestFocus(); if (!(documentViewModel.getViewToolMode() == DocumentViewModel.DISPLAY_TOOL_SELECTION || documentViewModel.getViewToolMode() == DocumentViewModel.DISPLAY_TOOL_LINK_ANNOTATION)&& isInteractiveAnnotationsEnabled) { if (documentViewController.getAnnotationCallback() != null) { documentViewController.getAnnotationCallback() .proccessAnnotationAction(annotation); } } } public void mouseEntered(MouseEvent e) { // set border back to default isRollover = documentViewModel.getViewToolMode() == DocumentViewModel.DISPLAY_TOOL_SELECTION; repaint(); } public void mousePressed(MouseEvent me) { // setup visual effect when the mouse button is pressed or held down // inside the active area of the annotation. isMousePressed = true; if (documentViewModel.getViewToolMode() == DocumentViewModel.DISPLAY_TOOL_SELECTION && isInteractiveAnnotationsEnabled && !annotation.getFlagReadOnly()) { ResizableBorder border = (ResizableBorder) getBorder(); cursor = border.getCursor(me); startPos = me.getPoint(); previousAnnotationState = new AnnotationState(this); // mark annotation as selected. documentViewController.assignSelectedAnnotation(this); } repaint(); } public void mouseDragged(MouseEvent me) { if (startPos != null && !(annotation.getFlagLocked() || annotation.getFlagReadOnly())) { int x = getX(); int y = getY(); int w = getWidth(); int h = getHeight(); int dx = me.getX() - startPos.x; int dy = me.getY() - startPos.y; switch (cursor) { case Cursor.N_RESIZE_CURSOR: if (!(h - dy < 12)) { setBounds(x, y + dy, w, h - dy); resize(); } break; case Cursor.S_RESIZE_CURSOR: if (!(h + dy < 12)) { setBounds(x, y, w, h + dy); startPos = me.getPoint(); resize(); } break; case Cursor.W_RESIZE_CURSOR: if (!(w - dx < 18)) { setBounds(x + dx, y, w - dx, h); resize(); } break; case Cursor.E_RESIZE_CURSOR: if (!(w + dx < 18)) { setBounds(x, y, w + dx, h); startPos = me.getPoint(); resize(); } break; case Cursor.NW_RESIZE_CURSOR: if (!(w - dx < 18) && !(h - dy < 18)) { setBounds(x + dx, y + dy, w - dx, h - dy); resize(); } break; case Cursor.NE_RESIZE_CURSOR: if (!(w + dx < 18) && !(h - dy < 18)) { setBounds(x, y + dy, w + dx, h - dy); startPos = new Point(me.getX(), startPos.y); resize(); } break; case Cursor.SW_RESIZE_CURSOR: if (!(w - dx < 18) && !(h + dy < 18)) { setBounds(x + dx, y, w - dx, h + dy); startPos = new Point(startPos.x, me.getY()); resize(); } break; case Cursor.SE_RESIZE_CURSOR: if (!(w + dx < 18) && !(h + dy < 18)) { setBounds(x, y, w + dx, h + dy); startPos = me.getPoint(); resize(); } break; case Cursor.MOVE_CURSOR: Rectangle bounds = getBounds(); bounds.translate(dx, dy); setBounds(bounds); resize(); } setCursor(Cursor.getPredefinedCursor(cursor)); validate(); } } public void mouseReleased(MouseEvent mouseEvent) { startPos = null; isMousePressed = false; // check to see if a move/resize occurred and if so we add the // state change to the memento in document view. if (wasResized){ wasResized = false; // fire new bounds change event, let the listener handle // how to deal with the bound change. documentViewController.firePropertyChange( PropertyConstants.ANNOTATION_BOUNDS, previousAnnotationState, new AnnotationState(this)); } repaint(); } /** * private ResizableBorder generateAnnotationBorder() { * <p/> * // none visible or locked, we don't want anything * if ((annotation.getFlagReadOnly() || * annotation.getFlagLocked() || * annotation.getFlagInvisible() || * annotation.getFlagHidden())) { * return invisibleLockedBorder; * } * <p/> * // link annotation * if (annotation instanceof LinkAnnotation) { * // link annotation that has no border * if (annotation.getBorderStyle() == null) { * return outlineResizeBorder; * } * // link annotation that has a border * else { * return resizeBorder; * } * } * <p/> * // everything else, no border but movable is selected. * else { * return moveBorder; * } * } */ /** * Is the annotation editable * @return true if editable, false otherwise. */ public boolean isEditable() { return isEditable; } public boolean isRollover() { return isRollover; } public boolean isLinkAnnot() { return isLinkAnnot; } public boolean isBorderStyle() { return isBorderStyle; } public boolean isSelected() { return isSelected; } public void setSelected(boolean selected) { isSelected = selected; } }