/*
* 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.common;
import org.icepdf.core.pobjects.Page;
import org.icepdf.core.pobjects.graphics.text.GlyphText;
import org.icepdf.core.pobjects.graphics.text.LineText;
import org.icepdf.core.pobjects.graphics.text.PageText;
import org.icepdf.core.pobjects.graphics.text.WordText;
import org.icepdf.core.util.ColorUtil;
import org.icepdf.core.util.Defs;
import org.icepdf.core.views.DocumentViewController;
import org.icepdf.core.views.DocumentViewModel;
import org.icepdf.core.views.swing.AbstractPageViewComponent;
import javax.swing.event.MouseInputListener;
import java.awt.*;
import java.awt.event.MouseEvent;
import java.awt.geom.*;
import java.util.ArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Handles Paint and mouse/keyboard logic around text selection and search
* highlighting. there is on text handler isntance of each pageComponent
* used to dispaly the document.
* <p/>
* The highlight colour by default is #FFF600 but can be set using color or
* hex values names using the system property "org.icepdf.core.views.page.text.highlightColor"
* <p/>
* The highlight colour by default is #FFF600 but can be set using color or
* hex values names using the system property "org.icepdf.core.views.page.text.selectionColor"
* <p/>
*
* @since 4.0
*/
public class TextSelectionPageHandler extends SelectionBoxHandler
implements MouseInputListener {
private static final Logger logger =
Logger.getLogger(TextSelectionPageHandler.class.toString());
/**
* Tranparencey value used to simulate text highlighting.
*/
public static final float selectionAlpha = 0.3f;
// text selection colour
public static Color selectionColor;
static {
// sets the shadow colour of the decorator.
try {
String color = Defs.sysProperty(
"org.icepdf.core.views.page.text.selectionColor", "#0077FF");
int colorValue = ColorUtil.convertColor(color);
selectionColor =
new Color(colorValue >= 0 ? colorValue :
Integer.parseInt("0077FF", 16));
} catch (NumberFormatException e) {
if (logger.isLoggable(Level.WARNING)) {
logger.warning("Error reading text selection colour");
}
}
}
// text highlight colour
public static Color highlightColor;
static {
// sets the shadow colour of the decorator.
try {
String color = Defs.sysProperty(
"org.icepdf.core.views.page.text.highlightColor", "#CC00FF");
int colorValue = ColorUtil.convertColor(color);
highlightColor =
new Color(colorValue >= 0 ? colorValue :
Integer.parseInt("FFF600", 16));
} catch (NumberFormatException e) {
if (logger.isLoggable(Level.WARNING)) {
logger.warning("Error reading text highlight colour");
}
}
}
// parent page component
private AbstractPageViewComponent pageViewComponent;
private DocumentViewController documentViewController;
private DocumentViewModel documentViewModel;
/**
* New Text selection handler. Make sure to correctly and and remove
* this mouse and text listeners.
*
* @param pageViewComponent page component that this handler is bound to.
* @param documentViewModel view model.
*/
public TextSelectionPageHandler(AbstractPageViewComponent pageViewComponent,
DocumentViewModel documentViewModel) {
this.pageViewComponent = pageViewComponent;
this.documentViewModel = documentViewModel;
}
/**
* Document view controller callback setup. Has to be done after the
* contructor.
*
* @param documentViewController document controller callback.
*/
public void setDocumentViewController(
DocumentViewController documentViewController) {
this.documentViewController = documentViewController;
}
/**
* When mouse is double clicked we select the word the mouse if over. When
* the mouse is triple clicked we select the line of text that the mouse
* is over.
*/
public void mouseClicked(MouseEvent e) {
// double click we select the whole line.
if (e.getClickCount() == 3) {
if (documentViewModel.getViewToolMode() ==
DocumentViewModel.DISPLAY_TOOL_TEXT_SELECTION) {
Page currentPage = pageViewComponent.getPageLock(this);
// handle text selection mouse coordinates
Point mouseLocation = (Point) e.getPoint().clone();
lineSelectHandler(currentPage, mouseLocation);
pageViewComponent.releasePageLock(currentPage, this);
}
}
// single click we select word that was clicked.
else if (e.getClickCount() == 2) {
if (documentViewModel.getViewToolMode() ==
DocumentViewModel.DISPLAY_TOOL_TEXT_SELECTION) {
Page currentPage = pageViewComponent.getPageLock(this);
// handle text selection mouse coordinates
Point mouseLocation = (Point) e.getPoint().clone();
wordSelectHandler(currentPage, mouseLocation);
currentPage.getViewText().getSelected();
pageViewComponent.releasePageLock(currentPage, this);
}
}
}
public void clearSelection() {
// on mouse click clear the currently selected sprints
Page currentPage = pageViewComponent.getPageLock(this);
// clear selected text.
if (currentPage.getViewText() != null) {
currentPage.getViewText().clearSelected();
}
pageViewComponent.releasePageLock(currentPage, this);
// reset painted rectangle
currentRect = new Rectangle(0, 0, 0, 0);
updateDrawableRect(pageViewComponent.getWidth(), pageViewComponent.getHeight());
pageViewComponent.repaint();
}
/**
* Invoked when a mouse button has been pressed on a component.
*/
public void mousePressed(MouseEvent e) {
clearSelection();
// text selection box.
if (documentViewModel.getViewToolMode() ==
DocumentViewModel.DISPLAY_TOOL_TEXT_SELECTION) {
int x = e.getX();
int y = e.getY();
currentRect = new Rectangle(x, y, 0, 0);
updateDrawableRect(pageViewComponent.getWidth(), pageViewComponent.getHeight());
pageViewComponent.repaint();
}
}
/**
* Invoked when a mouse button has been released on a component.
*/
public void mouseReleased(MouseEvent e) {
if (documentViewModel.getViewToolMode() ==
DocumentViewModel.DISPLAY_TOOL_TEXT_SELECTION) {
// update selection rectangle
updateSelectionSize(e, pageViewComponent);
// write out selected text.
if (logger.isLoggable(Level.FINE)) {
Page currentPage = pageViewComponent.getPageLock(this);
// handle text selection mouse coordinates
logger.fine(currentPage.getViewText().getSelected().toString());
pageViewComponent.releasePageLock(currentPage, this);
}
// clear the rectangle
clearRectangle(pageViewComponent);
pageViewComponent.repaint();
}
}
/**
* Invoked when the mouse enters a component.
*/
public void mouseEntered(MouseEvent e) {
}
/**
* Invoked when the mouse exits a component.
*/
public void mouseExited(MouseEvent e) {
}
/**
* Invoked when a mouse button is pressed on a component and then
* dragged. <code>MOUSE_DRAGGED</code> events will continue to be
* delivered to the component where the drag originated until the
* mouse button is released (regardless of whether the mouse position
* is within the bounds of the component).
* <p/>
* Due to platform-dependent Drag&Drop implementations,
* <code>MOUSE_DRAGGED</code> events may not be delivered during a native
* Drag&Drop operation.
*/
public void mouseDragged(MouseEvent e) {
if (documentViewModel.getViewToolMode() ==
DocumentViewModel.DISPLAY_TOOL_TEXT_SELECTION) {
// rectangle select tool
updateSelectionSize(e, pageViewComponent);
// lock and unlock content before iterating over the pageText tree.
Page currentPage = pageViewComponent.getPageLock(this);
multilineSelectHandler(currentPage, e.getPoint());
pageViewComponent.releasePageLock(currentPage, this);
}
}
public void setSelectionRectangle(Point cursorLocation, Rectangle selection) {
if (documentViewModel.getViewToolMode() ==
DocumentViewModel.DISPLAY_TOOL_TEXT_SELECTION) {
// rectangle select tool
setSelectionSize(selection, pageViewComponent);
// lock and unlock content before iterating over the pageText tree.
Page currentPage = pageViewComponent.getPageLock(this);
multilineSelectHandler(currentPage, cursorLocation);
pageViewComponent.releasePageLock(currentPage, this);
}
}
/**
* Invoked when the mouse cursor has been moved onto a component
* but no buttons have been pushed.
*/
public void mouseMoved(MouseEvent e) {
// change state of mouse from pointer to text selection icon
if (documentViewModel.getViewToolMode() ==
DocumentViewModel.DISPLAY_TOOL_TEXT_SELECTION) {
Page currentPage = pageViewComponent.getPageLock(this);
selectionMouseCursor(currentPage, e.getPoint());
pageViewComponent.releasePageLock(currentPage, this);
}
}
/**
* Utility for detecting and changing the cursor to the text selection tool
* when over text in the doucument.
*
* @param currentPage page to looking for text inersection on.
* @param mouseLocation location of mouse.
*/
private void selectionMouseCursor(Page currentPage, Point mouseLocation) {
if (currentPage != null &&
currentPage.isInitiated()) {
// get page text
PageText pageText = currentPage.getViewText();
if (pageText != null) {
// get page transform, same for all calculations
AffineTransform pageTransform = currentPage.getPageTransform(
Page.BOUNDARY_CROPBOX,
documentViewModel.getViewRotation(),
documentViewModel.getViewZoom());
ArrayList<LineText> pageLines = pageText.getPageLines();
boolean found = false;
Point2D.Float pageMouseLocation =
convertMouseToPageSpace(mouseLocation, pageTransform);
for (LineText pageLine : pageLines) {
// check for containment, if so break into words.
if (pageLine.getBounds().contains(pageMouseLocation)) {
found = true;
documentViewController.setViewCursor(
DocumentViewController.CURSOR_TEXT_SELECTION);
break;
}
}
if (!found) {
documentViewController.setViewCursor(
DocumentViewController.CURSOR_SELECT);
}
}
}
}
/**
* Convert the mouse cooridates to the space specified by the pageTransform
* matrix. This is a utility method for conveting the mouse coordinates
* to page space so that it can be used in a contains calculation for text
* selection.
*
* @param mousePoint point to convert space of
* @param pageTransform tranform
* @return page space mouse coordinates.
*/
private Point2D.Float convertMouseToPageSpace(Point mousePoint,
AffineTransform pageTransform) {
Point2D.Float pageMouseLocation = new Point2D.Float();
try {
pageTransform.createInverse().transform(
mousePoint, pageMouseLocation);
} catch (NoninvertibleTransformException e) {
logger.log(Level.SEVERE,
"Error converting mouse point to page space.", e);
}
return pageMouseLocation;
}
/**
* Converts the rectangle to the space specified by the page tranform. This
* is a utility method for converting a selection rectangle to page space
* so that an intersection can be calculated to determine a selected state.
*
* @param mouseRect rectangle to convert space of
* @param pageTransform page transform
* @return converted rectangle.
*/
private Rectangle2D convertRectangleToPageSpace(Rectangle mouseRect,
AffineTransform pageTransform) {
GeneralPath shapePath;
try {
AffineTransform tranform = pageTransform.createInverse();
shapePath = new GeneralPath(mouseRect);
shapePath.transform(tranform);
return shapePath.getBounds2D();
} catch (NoninvertibleTransformException e) {
logger.log(Level.SEVERE,
"Error converting mouse point to page space.", e);
}
return null;
}
/**
* Utility for selecting multiple lines via l-> right type select. This
* method should only be called from within a locked page content
*
* @param currentPage page to looking for text inersection on.
* @param mouseLocation location of mouse.
*/
private void multilineSelectHandler(Page currentPage, Point mouseLocation) {
if (currentPage != null &&
currentPage.isInitiated()) {
// get page text
PageText pageText = currentPage.getViewText();
if (pageText != null) {
// clear the currently selected state, ignore highlighted.
currentPage.getViewText().clearSelected();
// get page transform, same for all calculations
AffineTransform pageTransform = currentPage.getPageTransform(
Page.BOUNDARY_CROPBOX,
documentViewModel.getViewRotation(),
documentViewModel.getViewZoom());
LineText firstPageLine = null;
Point2D.Float pageMouseLocation =
convertMouseToPageSpace(mouseLocation, pageTransform);
Rectangle2D pageRectToDraw =
convertRectangleToPageSpace(rectToDraw, pageTransform);
ArrayList<LineText> pageLines = pageText.getPageLines();
for (LineText pageLine : pageLines) {
// check for containment, if so break into words.
if (pageLine.intersects(pageRectToDraw)) {
pageLine.setHasSelected(true);
if (firstPageLine == null) {
firstPageLine = pageLine;
}
if (pageLine.getBounds().contains(pageMouseLocation)) {
ArrayList<WordText> lineWords = pageLine.getWords();
for (WordText word : lineWords) {
if (word.intersects(pageRectToDraw)) {
word.setHasHighlight(true);
ArrayList<GlyphText> glyphs = word.getGlyphs();
for (GlyphText glyph : glyphs) {
if (glyph.intersects(pageRectToDraw)) {
glyph.setSelected(true);
pageViewComponent.repaint();
}
}
}
}
} else if (firstPageLine == pageLine) {
// left to right selection
// if (currentRect.width > 0 ){
selectLeftToRight(pageLine, pageTransform);
// }else{
// selectRightToLeft(pageLine, pageTransform););
// }
} else {
pageLine.selectAll();
}
}
}
}
}
}
/**
* Utility for right to left selection, NOT Correct
*
* @param pageLine page line to select.
* @param pageTransform page transform.
*/
private void selectRightToLeft(LineText pageLine,
AffineTransform pageTransform) {
// ArrayList<WordText> lineWords = pageLine.getWords();
// Rectangle2D pageRectToDraw =
// convertRectangleToPageSpace(rectToDraw, pageTransform);
// for (WordText word : lineWords) {
// if (word.intersects(pageRectToDraw)) {
// word.setHasHighlight(true);
// ArrayList<GlyphText> glyphs = word.getGlyphs();
// GlyphText glyph = null;
// for (int i = glyphs.size() - 1; i >= 0; i--) {
// if (glyph.intersects(pageRectToDraw)) {
// glyph.setSelected(true);
// pageViewComponent.repaint();
// }
// }
// }
// }
}
/**
* Simple left to right, top down type selection model, not perfect.
*
* @param pageLine page line to select.
* @param pageTransform page transform.
*/
private void selectLeftToRight(LineText pageLine,
AffineTransform pageTransform) {
GlyphText fistGlyph = null;
Rectangle2D pageRectToDraw =
convertRectangleToPageSpace(rectToDraw, pageTransform);
ArrayList<WordText> lineWords = pageLine.getWords();
for (WordText word : lineWords) {
if (word.intersects(pageRectToDraw)) {
word.setHasHighlight(true);
ArrayList<GlyphText> glyphs = word.getGlyphs();
for (GlyphText glyph : glyphs) {
if (glyph.intersects(pageRectToDraw)) {
if (fistGlyph == null) {
fistGlyph = glyph;
}
glyph.setSelected(true);
} else if (fistGlyph != null) {
glyph.setSelected(true);
}
}
}
// select the rest
else if (fistGlyph != null) {
word.selectAll();
}
}
pageViewComponent.repaint();
}
/**
* Utility for selecting multiple lines via rectangle like tool. The
* selection works based on the intersection of the rectangle and glyph
* bounding box.
* <p/>
* This method should only be called from within a locked page content
*
* @param currentPage page to looking for text inersection on.
* @param mouseLocation location of mouse.
*/
private void rectangleSelectHandler(Page currentPage, Point mouseLocation) {
// detect L->R or R->L
if (currentPage != null &&
currentPage.isInitiated()) {
// get page text
PageText pageText = currentPage.getViewText();
if (pageText != null) {
// clear the currently selected state, ignore highlighted.
currentPage.getViewText().clearSelected();
// get page transform, same for all calculations
AffineTransform pageTransform = currentPage.getPageTransform(
Page.BOUNDARY_CROPBOX,
documentViewModel.getViewRotation(),
documentViewModel.getViewZoom());
Rectangle2D pageRectToDraw =
convertRectangleToPageSpace(rectToDraw, pageTransform);
ArrayList<LineText> pageLines = pageText.getPageLines();
for (LineText pageLine : pageLines) {
// check for containment, if so break into words.
if (pageLine.intersects(pageRectToDraw)) {
pageLine.setHasSelected(true);
ArrayList<WordText> lineWords = pageLine.getWords();
for (WordText word : lineWords) {
if (word.intersects(pageRectToDraw)) {
word.setHasHighlight(true);
ArrayList<GlyphText> glyphs = word.getGlyphs();
for (GlyphText glyph : glyphs) {
if (glyph.intersects(pageRectToDraw)) {
glyph.setSelected(true);
pageViewComponent.repaint();
}
}
}
}
}
}
}
}
}
/**
* Utility for selecting multiple lines via rectangle like tool. The
* selection works based on the intersection of the rectangle and glyph
* bounding box.
* <p/>
* This method should only be called from within a locked page content
*
* @param currentPage page to looking for text inersection on.
* @param mouseLocation location of mouse.
*/
private void wordSelectHandler(Page currentPage, Point mouseLocation) {
if (currentPage != null &&
currentPage.isInitiated()) {
// get page text
PageText pageText = currentPage.getViewText();
if (pageText != null) {
// clear the currently selected state, ignore highlighted.
currentPage.getViewText().clearSelected();
// get page transform, same for all calculations
AffineTransform pageTransform = currentPage.getPageTransform(
Page.BOUNDARY_CROPBOX,
documentViewModel.getViewRotation(),
documentViewModel.getViewZoom());
Point2D.Float pageMouseLocation =
convertMouseToPageSpace(mouseLocation, pageTransform);
ArrayList<LineText> pageLines = pageText.getPageLines();
for (LineText pageLine : pageLines) {
// check for containment, if so break into words.
if (pageLine.getBounds().contains(pageMouseLocation)) {
pageLine.setHasSelected(true);
ArrayList<WordText> lineWords = pageLine.getWords();
for (WordText word : lineWords) {
// if (word.contains(pageTransform, mouseLocation)) {
if (word.getBounds().contains(pageMouseLocation)) {
word.selectAll();
pageViewComponent.repaint();
break;
}
}
}
}
}
}
}
/**
* Utility for selecting a LineText which is usually a sentence in the
* document. This is usually triggered by a tripple click of the mouse
*
* @param currentPage page to select
* @param mouseLocation location of mouse
*/
private void lineSelectHandler(Page currentPage, Point mouseLocation) {
if (currentPage != null &&
currentPage.isInitiated()) {
// get page text
PageText pageText = currentPage.getViewText();
if (pageText != null) {
// clear the currently selected state, ignore highlighted.
currentPage.getViewText().clearSelected();
// get page transform, same for all calculations
AffineTransform pageTransform = currentPage.getPageTransform(
Page.BOUNDARY_CROPBOX,
documentViewModel.getViewRotation(),
documentViewModel.getViewZoom());
Point2D.Float pageMouseLocation =
convertMouseToPageSpace(mouseLocation, pageTransform);
ArrayList<LineText> pageLines = pageText.getPageLines();
for (LineText pageLine : pageLines) {
// check for containment, if so break into words.
if (pageLine.getBounds().contains(pageMouseLocation)) {
pageLine.selectAll();
pageViewComponent.repaint();
break;
}
}
}
}
}
/**
* Utility for painting the highlight and selected
*
* @param g graphics to paint to.
*/
public void paintSelectedText(Graphics g) {
// ready outline paint
Graphics2D gg = (Graphics2D) g;
AffineTransform prePaintTransform = gg.getTransform();
Color oldColor = gg.getColor();
Stroke oldStroke = gg.getStroke();
gg.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
selectionAlpha));
gg.setColor(selectionColor);
gg.setStroke(new BasicStroke(1.0f));
Page currentPage = pageViewComponent.getPageLock(this);
if (currentPage != null && currentPage.isInitiated()) {
PageText pageText = currentPage.getViewText();
if (pageText != null) {
// get page transformation
AffineTransform pageTransform = currentPage.getPageTransform(
documentViewModel.getPageBoundary(),
documentViewModel.getViewRotation(),
documentViewModel.getViewZoom());
// paint the sprites
GeneralPath textPath;
for (LineText lineText : pageText.getPageLines()) {
for (WordText wordText : lineText.getWords()) {
// paint whole word
if (wordText.isSelected() || wordText.isHighlighted()) {
textPath = new GeneralPath(wordText.getBounds());
textPath.transform(pageTransform);
// paint highlight over any selected
if (wordText.isSelected()) {
gg.setColor(selectionColor);
gg.fill(textPath);
}
if (wordText.isHighlighted()) {
gg.setColor(highlightColor);
gg.fill(textPath);
}
}
// check children
else {
for (GlyphText glyph : wordText.getGlyphs()) {
if (glyph.isSelected()) {
textPath = new GeneralPath(glyph.getBounds());
textPath.transform(pageTransform);
gg.setColor(selectionColor);
gg.fill(textPath);
}
}
}
}
}
}
}
pageViewComponent.releasePageLock(currentPage, this);
// pain selection box
paintSelectionBox(g);
// restore graphics state to where we left it.
gg.setTransform(prePaintTransform);
gg.setStroke(oldStroke);
gg.setColor(oldColor);
// paint words for bounds test.
// paintTextBounds(g);
}
/**
* Utility for painting text bounds.
*
* @param g graphics context to paint to.
*/
private void paintTextBounds(Graphics g) {
Page currentPage = pageViewComponent.getPageLock(this);
// get page transformation
AffineTransform pageTransform = currentPage.getPageTransform(
documentViewModel.getPageBoundary(),
documentViewModel.getViewRotation(),
documentViewModel.getViewZoom());
Graphics2D gg = (Graphics2D) g;
Color oldColor = g.getColor();
g.setColor(Color.red);
PageText pageText = currentPage.getViewText();
ArrayList<LineText> pageLines = pageText.getPageLines();
for (LineText lineText : pageLines) {
for (WordText wordText : lineText.getWords()) {
for (GlyphText glyph : wordText.getGlyphs()) {
g.setColor(Color.black);
GeneralPath glyphSpritePath =
new GeneralPath(glyph.getBounds());
glyphSpritePath.transform(pageTransform);
gg.draw(glyphSpritePath);
}
// if (!wordText.isWhiteSpace()) {
// g.setColor(Color.blue);
// GeneralPath glyphSpritePath =
// new GeneralPath(wordText.getBounds());
// glyphSpritePath.transform(pageTransform);
// gg.draw(glyphSpritePath);
// }
}
g.setColor(Color.red);
GeneralPath glyphSpritePath =
new GeneralPath(lineText.getBounds());
glyphSpritePath.transform(pageTransform);
gg.draw(glyphSpritePath);
}
g.setColor(oldColor);
pageViewComponent.releasePageLock(currentPage, this);
}
}