All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.openhtmltopdf.swing.SelectionHighlighter Maven / Gradle / Ivy

/*
 * {{{ header & license
 * Copyright (c) Nick Reddel
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License
 * as published by the Free Software Foundation; either version 2.1
 * of the License, or (at your option) any later version.
 *
 * 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.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 * }}}
 */
package com.openhtmltopdf.swing;

import java.awt.*;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.StringSelection;
import java.awt.event.ActionEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.text.CharacterIterator;
import java.text.StringCharacterIterator;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Stack;
import java.util.logging.Level;

import javax.swing.AbstractAction;
import javax.swing.SwingUtilities;
import javax.swing.TransferHandler;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.EventListenerList;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.Text;
import org.w3c.dom.ranges.DocumentRange;
import org.w3c.dom.ranges.Range;
import org.w3c.dom.traversal.DocumentTraversal;
import org.w3c.dom.traversal.NodeFilter;
import org.w3c.dom.traversal.NodeIterator;

import com.openhtmltopdf.css.style.CalculatedStyle;
import com.openhtmltopdf.layout.LayoutContext;
import com.openhtmltopdf.render.BlockBox;
import com.openhtmltopdf.render.Box;
import com.openhtmltopdf.render.InlineLayoutBox;
import com.openhtmltopdf.render.InlineText;
import com.openhtmltopdf.simple.XHTMLPanel;
import com.openhtmltopdf.util.Util;
import com.openhtmltopdf.util.XRLog;

/**
 * 

* A simple Selection and Highlighter class for * {@link com.openhtmltopdf.simple.XHTMLPanel}. *

*

* The current selection is available as a DOM Range via getSelectionRange. There is also a Swing * action to copy the selection contents to the clipboard: * {@link com.openhtmltopdf.swing.SelectionHighlighter.CopyAction}, which * should be installed on the SelectionHighlighter *

*

* Usage: create the XHTMLPanel, create an instance * of this class then call install. See also: * /demos/samples/src/SelectionHighlighterTest.java *

* * With thanks to Swing's DefaultCaret * * @author Nick Reddel */ public class SelectionHighlighter implements MouseMotionListener, MouseListener { private static final String PARA_EQUIV = "&!= 0; i -= 2) { if (listeners[i] == ChangeListener.class) { // Lazily create the event: if (changeEvent == null) changeEvent = new ChangeEvent(this); ((ChangeListener) listeners[i + 1]).stateChanged(changeEvent); } } } public void install(XHTMLPanel panel) { this.panel = panel; if (!checkDocument()) { return; } panel.setTransferHandler(handler); panel.addMouseListener(this); panel.addMouseMotionListener(this); } public void deinstall(XHTMLPanel panel) { if (panel.getTransferHandler() == handler) { panel.setTransferHandler(null); } panel.removeMouseListener(this); panel.removeMouseMotionListener(this); } private boolean checkDocument() { while (true) { if (this.document != panel.getDocument() || textInlineMap == null) { this.document = panel.getDocument(); textInlineMap = null; this.dotInfo = null; this.markInfo = null; this.lastSelectionRange = null; try { this.docRange = (DocumentRange) panel.getDocument(); this.docTraversal = (DocumentTraversal) panel.getDocument(); if (this.document != null && this.createMaps()) { return true; } try { Thread.sleep(10); } catch (InterruptedException e) { return false; } } catch (ClassCastException cce) { XRLog.layout(Level.WARNING, "Document instance cannot create ranges: no selection possible"); return false; } } return true; } } public void setDot(ViewModelInfo pos) { this.dotInfo = pos; this.markInfo = pos; fireStateChanged(); updateHighlights(); updateSystemSelection(); } public void mouseDragged(MouseEvent e) { if ((!e.isConsumed()) && SwingUtilities.isLeftMouseButton(e)) { moveCaret(convertMouseEventToScale(e)); } } public void mouseMoved(MouseEvent e) { } public void mouseClicked(MouseEvent e) { // TODO: double-triple click handler } public void mouseEntered(MouseEvent e) { } public void mouseExited(MouseEvent e) { } public void mousePressed(MouseEvent e) { int nclicks = e.getClickCount(); if (SwingUtilities.isLeftMouseButton(e)) { if (e.isConsumed()) { } else { adjustCaretAndFocus(e); MouseEvent newE = convertMouseEventToScale(e); adjustCaretAndFocus(newE); if (nclicks == 2) { selectWord(newE); } } } } void adjustCaretAndFocus(MouseEvent e) { adjustCaret(e); adjustFocus(false); } /** * Adjusts the caret location based on the MouseEvent. */ private void adjustCaret(MouseEvent e) { if ((e.getModifiers() & ActionEvent.SHIFT_MASK) != 0 && this.dotInfo != null) { moveCaret(e); } else { positionCaret(e); } } private void positionCaret(MouseEvent e) { ViewModelInfo pos = infoFromPoint(e); if (pos != null) { setDot(pos); } } /** * Adjusts the focus, if necessary. * * @param inWindow * if true indicates requestFocusInWindow should be used */ private void adjustFocus(boolean inWindow) { if ((panel != null) && panel.isEnabled() && panel.isRequestFocusEnabled()) { if (inWindow) { panel.requestFocusInWindow(); } else { panel.requestFocus(); } } } private void selectWord(MouseEvent e) { // TODO Auto-generated method stub } public void mouseReleased(MouseEvent e) { // TODO Auto-generated method stub } public XHTMLPanel getComponent() { return this.panel; } protected void moveCaret(MouseEvent e) { ViewModelInfo pos = infoFromPoint(e); if (pos != null) { moveDot(pos); } } public void selectAll() { if (this.getComponent() == null || this.getComponent().getWidth() == 0 || this.getComponent().getHeight() == 0) { return; } checkDocument(); NodeIterator nodeIterator = this.docTraversal.createNodeIterator(this.document .getDocumentElement(), NodeFilter.SHOW_TEXT, null, false); Text firstText = null; Text lastText = null; while (true) { Node n = nodeIterator.nextNode(); if (n == null) { break; } if (!textInlineMap.containsKey(n)) { continue; } lastText = (Text) n; if (firstText == null) { firstText = lastText; } } if (firstText == null) { return; } Range r = docRange.createRange(); r.setStart(firstText, 0); ViewModelInfo firstPoint = new ViewModelInfo(r, (InlineText) ((List) textInlineMap .get(firstText)).get(0)); r = docRange.createRange(); try { // possibly some dom impls don't handle this? r.setStart(lastText, lastText.getLength()); } catch (Exception e) { r.setStart(lastText, Math.max(0, lastText.getLength() - 1)); } List l = (List) textInlineMap.get(firstText); ViewModelInfo lastPoint = new ViewModelInfo(r, (InlineText) l.get(l.size() - 1)); setDot(firstPoint); moveDot(lastPoint); } public void moveDot(ViewModelInfo pos) { this.dotInfo = pos; if (this.markInfo == null) { this.markInfo = pos; } fireStateChanged(); updateHighlights(); updateSystemSelection(); InlineText iT = this.dotInfo.text; InlineLayoutBox iB = iT.getParent(); adjustVisibility(new Rectangle(iB.getAbsX() + iT.getX(), iB.getAbsY(), 1, iB.getBaseline())); } private void updateHighlights() { List modified = new ArrayList(); StringBuffer hlText = new StringBuffer(); if (this.dotInfo == null) { getComponent().getRootBox().clearSelection(modified); getComponent().repaint(); lastHighlightedString = ""; return; } Range range = getSelectionRange(); if (lastSelectionRange != null && range.compareBoundaryPoints(Range.START_TO_START, lastSelectionRange) == 0 && range.compareBoundaryPoints(Range.END_TO_END, lastSelectionRange) == 0) { return; } lastHighlightedString = ""; lastSelectionRange = range.cloneRange(); if (range.compareBoundaryPoints(Range.START_TO_END, range) == 0) { getComponent().getRootBox().clearSelection(modified); } else { boolean endBeforeStart = (this.markInfo.range.compareBoundaryPoints( Range.START_TO_START, this.dotInfo.range) >= 0); // TODO: track modifications getComponent().getRootBox().clearSelection(modified); InlineText t1 = (endBeforeStart) ? this.dotInfo.text : this.markInfo.text; InlineText t2 = (!endBeforeStart) ? this.dotInfo.text : this.markInfo.text; if (t1 == null || t2 == null) { // TODO: need general debug here (never print to system.err; use XRLog instead) // TODO: is this just a warning, or should we bail out XRLog.general(Level.FINE, "null text node"); } final Range acceptRange = docRange.createRange(); final Range tr = range; NodeFilter f = new NodeFilter() { public short acceptNode(Node n) { acceptRange.setStart(n, 0); if (tr.getStartContainer() == n) { return FILTER_ACCEPT; } if ((acceptRange.compareBoundaryPoints(Range.START_TO_START, tr) < 0 || acceptRange .compareBoundaryPoints(Range.END_TO_START, tr) > 0) && n != tr.getStartContainer() && n != tr.getEndContainer()) { return NodeFilter.FILTER_SKIP; } return NodeFilter.FILTER_ACCEPT; } }; NodeIterator nodeIterator = this.docTraversal.createNodeIterator(range .getCommonAncestorContainer(), NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT | NodeFilter.SHOW_CDATA_SECTION, f, false); Box box; boolean lastNodeWasBox = false; for (Node n = nodeIterator.nextNode(); n != null; n = nodeIterator.nextNode()) { if (n.getNodeType() == Node.ELEMENT_NODE) { box = getBoxForElement((Element) n); if (box instanceof BlockBox && !lastNodeWasBox) { hlText.append(PARA_EQUIV); lastNodeWasBox = true; } else { lastNodeWasBox = false; } } else { lastNodeWasBox = false; Text t = (Text) n; List iTs = getInlineTextsForText(t); if (iTs == null) { // shouldn't happen continue; } int selTxtSt = (t == range.getStartContainer()) ? range.getStartOffset() : 0; int selTxtEnd = (t == range.getEndContainer()) ? range.getEndOffset() : t .getNodeValue().length(); hlText.append(t.getNodeValue().substring(selTxtSt, selTxtEnd)); for (Iterator itr = iTs.iterator(); itr.hasNext();) { InlineText iT = (InlineText) itr.next(); iT.setSelectionStart((short) Math.max(0, Math.min(selTxtSt, iT.getEnd()) - iT.getStart())); iT.setSelectionEnd((short) Math.max(0, Math.min(iT.getEnd(), selTxtEnd) - iT.getStart())); } } } } String s = normalizeSpaces(hlText.toString()); getComponent().repaint(); lastHighlightedString = Util.replace(s, PARA_EQUIV, "\n\n"); // lastModified = modified; } public String normalizeSpaces(String s) { if (s == null) return null; StringBuffer buf = new StringBuffer(); CharacterIterator iter = new StringCharacterIterator(s); boolean inWhitespace = false; // Flag set if we're in a second // consecutive whitespace for (char c = iter.first(); c != CharacterIterator.DONE; c = iter.next()) { if (Character.isWhitespace(c)) { if (!inWhitespace) { buf.append(' '); inWhitespace = true; } } else { inWhitespace = false; buf.append(c); } } return buf.toString(); } private Box getElementContainerBox(InlineText t) { Box b = t.getParent(); while (b.getElement() == null) { b = b.getParent(); } return b; } private boolean createMaps() { if (panel.getRootBox() == null) { return false; } textInlineMap = new LinkedHashMap(); elementBoxMap = new HashMap(); Stack s = new Stack(); s.push(panel.getRootBox()); while (!s.empty()) { Box b = (Box) s.pop(); Element element = b.getElement(); if (element != null && !elementBoxMap.containsKey(element)) { elementBoxMap.put(element, b); } if (b instanceof InlineLayoutBox) { InlineLayoutBox ilb = (InlineLayoutBox) b; for (Iterator it = ilb.getInlineChildren().iterator(); it.hasNext();) { Object o = it.next(); if (o instanceof InlineText) { InlineText t = (InlineText) o; Text txt = t.getTextNode(); if (!textInlineMap.containsKey(txt)) { textInlineMap.put(txt, new ArrayList()); } ((List) textInlineMap.get(txt)).add(t); } else { s.push((Box) o); } } } else { Iterator childIterator = b.getChildIterator(); while (childIterator.hasNext()) { s.push(childIterator.next()); } } } return true; } private List getInlineTextsForText(Text t) { return (List) textInlineMap.get(t); } private Box getBoxForElement(Element elt) { return (Box) elementBoxMap.get(elt); } private void updateSystemSelection() { if (this.dotInfo != this.markInfo && panel != null) { Clipboard clip = panel.getToolkit().getSystemSelection(); if (clip != null) { String selectedText = lastHighlightedString; try { clip.setContents(new StringSelection(selectedText), null); } catch (IllegalStateException ise) { // clipboard was unavailable // no need to provide error feedback to user since updating // the system selection is not a user invoked action } } } } void copy() { if (this.dotInfo != this.markInfo && panel != null) { Clipboard clip = panel.getToolkit().getSystemClipboard(); if (clip != null) { String selectedText = lastHighlightedString; try { clip.setContents(new StringSelection(selectedText), null); } catch (IllegalStateException ise) { // clipboard was unavailable // no need to provide error feedback to user since updating // the system selection is not a user invoked action } } } } List getInlineLayoutBoxes(Box b, boolean ignoreChildElements) { Stack boxes = new Stack(); List ilbs = new ArrayList(); boxes.push(b); while (!boxes.empty()) { b = (Box) boxes.pop(); if (b instanceof InlineLayoutBox) { ilbs.add((InlineLayoutBox) b); } else { for (Iterator it = b.getChildIterator(); it.hasNext();) { Box child = (Box) it.next(); if (!ignoreChildElements || child.getElement() == null) { boxes.push(child); } } } } return ilbs; } ViewModelInfo infoFromPoint(MouseEvent e) { checkDocument(); Range r = docRange.createRange(); InlineText fndTxt = null; Box box = panel.getRootLayer().find(panel.getLayoutContext(), e.getX(), e.getY(), true); if (box == null) { return null; } Element elt = null; int offset = 0; InlineLayoutBox ilb = null; boolean containsWholeIlb = false; if (box instanceof InlineLayoutBox) { ilb = (InlineLayoutBox) box; } else { for (; ilb == null;) { List ilbs = getInlineLayoutBoxes(box, false); for (int i = ilbs.size() - 1; i >= 0; i--) { InlineLayoutBox ilbt = (InlineLayoutBox) ilbs.get(i); if (ilbt.getAbsY() <= e.getY() && ilbt.getAbsX() <= e.getX()) { if (ilb == null || (ilbt.getAbsY() > ilb.getAbsY()) || (ilbt.getAbsY() == ilb.getAbsY() && ilbt.getX() > ilb.getX())) { if (ilbt.isContainsVisibleContent()) { boolean hasDecentTextNode = false; int x = ilbt.getAbsX(); for (Iterator it = ilbt.getInlineChildren().iterator(); it .hasNext();) { Object o = it.next(); if (o instanceof InlineText) { InlineText txt = (InlineText) o; if (txt.getTextNode() != null) { hasDecentTextNode = true; break; } } } if (hasDecentTextNode) { ilb = ilbt; } } } containsWholeIlb = true; } } if (ilb == null) { if (box.getParent() == null) { return null; } box = box.getParent(); } } } int x = ilb.getAbsX(); InlineText lastItxt = null; for (Iterator it = ilb.getInlineChildren().iterator(); it.hasNext();) { Object o = it.next(); if (o instanceof InlineText) { InlineText txt = (InlineText) o; if (txt.getTextNode() != null) { if ((e.getX() >= x + txt.getX() && e.getX() < x + txt.getX() + txt.getWidth()) || containsWholeIlb) { fndTxt = txt; break; } else { if (e.getX() < x + txt.getX()) { // assume inline image or somesuch if (lastItxt != null) { fndTxt = lastItxt; break; } } } } lastItxt = txt; } } LayoutContext lc = panel.getLayoutContext(); if (fndTxt == null) { // TODO: need general debug flag here; not sure if this is an error condition and if the logging is necessary if (false) { XRLog.general(Level.FINE, ilb.dump(lc, "", Box.DUMP_RENDER)); XRLog.general(Level.FINE, ilb.getParent().dump(lc, "", Box.DUMP_RENDER)); XRLog.general(Level.FINE, ilb.getParent().getParent().dump(lc, "", Box.DUMP_RENDER)); } return null; } String txt = fndTxt.getMasterText(); CalculatedStyle style = ilb.getStyle(); if (containsWholeIlb) { offset = fndTxt.getEnd(); } else { for (offset = fndTxt.getStart(); offset < fndTxt.getEnd(); offset++) { int w = getTextWidth(lc, style, txt.substring(fndTxt.getStart(), offset + 1)); if (w + x + fndTxt.getX() > e.getX()) { break; } } } Node node = fndTxt.getTextNode(); try { r.setStart(node, offset); } catch (Exception ex) { // maybe differs for dom impl? anyway, fix for issue 216 r.setStart(node, ((Text) node).getLength() - 1); } return new ViewModelInfo(r, fndTxt); } private int getTextWidth(LayoutContext c, CalculatedStyle cs, String s) { return c.getTextRenderer().getWidth(c.getFontContext(), c.getFont(cs.getFont(c)), s); } public Range getSelectionRange() { if (this.dotInfo == null || this.dotInfo.range == null) { return null; } Range r = docRange.createRange(); // some xml parsers don't allow end




© 2015 - 2024 Weber Informatics LLC | Privacy Policy