/************************************************************************** OmegaT - Computer Assisted Translation (CAT) tool with fuzzy matching, translation memory, keyword search, glossaries, and translation leveraging into updated projects. Copyright (C) 2015 Chihiro Hio, Aaron Madlon-Kay Home page: http://www.omegat.org/ Support center: http://groups.yahoo.com/group/OmegaT/ This file is part of OmegaT. OmegaT is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. OmegaT 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. **************************************************************************/ package org.omegat.util.gui; import java.awt.Cursor; import java.awt.Desktop; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.net.URI; import java.net.URISyntaxException; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.swing.JTextPane; import javax.swing.SwingUtilities; import javax.swing.Timer; import javax.swing.text.AbstractDocument; import javax.swing.text.AttributeSet; import javax.swing.text.BadLocationException; import javax.swing.text.DocumentFilter; import javax.swing.text.Element; import javax.swing.text.MutableAttributeSet; import javax.swing.text.SimpleAttributeSet; import javax.swing.text.StyleConstants; import javax.swing.text.StyledDocument; import org.omegat.util.Log; /** * Adapted from omegat-plugin-linkbuilder by Chihiro Hio, provided under GPLv3. * * @see <a href="https://github.com/hiohiohio/omegat-plugin-linkbuilder"> * Original</a> * * @author Chihiro Hio * @author Aaron Madlon-Kay */ public class JTextPaneLinkifier { private static final String ATTR_LINK = "linkbuilder_link"; public static void linkify(JTextPane jTextPane) { final MouseAdapter mouseAdapter = new AttributeInserterMouseListener(jTextPane); // Adding mouse listner for actions jTextPane.addMouseListener(mouseAdapter); // settings for mouseover (changing cursor) jTextPane.addMouseMotionListener(mouseAdapter); // Those are the main called points from user's activities. setDocumentFilter(jTextPane); jTextPane.addPropertyChangeListener("document", new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent evt) { Object source = evt.getSource(); if (source instanceof JTextPane) { setDocumentFilter((JTextPane) source); } } }); } private static void setDocumentFilter(final JTextPane textPane) { final StyledDocument doc = textPane.getStyledDocument(); if (doc instanceof AbstractDocument) { final AbstractDocument abstractDocument = (AbstractDocument) doc; abstractDocument.setDocumentFilter(new AttributeInserterDocumentFilter(doc)); } } private interface IAttributeAction { public void execute(); } private static class AttributeInserterMouseListener extends MouseAdapter { private final JTextPane jTextPane; public AttributeInserterMouseListener(final JTextPane jTextPane) { this.jTextPane = jTextPane; } @Override public void mouseClicked(final MouseEvent e) { if (e.getButton() == MouseEvent.BUTTON1) { final StyledDocument doc = jTextPane.getStyledDocument(); final Element characterElement = doc.getCharacterElement(jTextPane.viewToModel(e.getPoint())); final AttributeSet as = characterElement.getAttributes(); final Object attr = as.getAttribute(ATTR_LINK); if (attr instanceof IAttributeAction) { ((IAttributeAction) attr).execute(); } } else { super.mouseClicked(e); } } @Override public void mouseMoved(final MouseEvent e) { final StyledDocument doc = jTextPane.getStyledDocument(); final Element characterElement = doc.getCharacterElement(jTextPane.viewToModel(e.getPoint())); final AttributeSet as = characterElement.getAttributes(); final Object attr = as.getAttribute(ATTR_LINK); if (attr instanceof IAttributeAction) { jTextPane.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); } else { jTextPane.setCursor(Cursor.getDefaultCursor()); } } } private static class AttributeInserterDocumentFilter extends DocumentFilter { private static final int REFRESH_DELAY = 200; // Regular Expression for URL validation // From https://gist.github.com/dperini/729294 // See lib/Licenses.txt private static final String REGEX_URL = "(?:(?:https?|ftp):\\/\\/)(?:\\S+(?::\\S*)?@)?(?:(?!(?:10|127)(?:\\.\\d{1,3}){3})(?!(?:169\\.254|192\\.168)(?:\\.\\d{1,3}){2})(?!172\\.(?:1[6-9]|2\\d|3[0-1])(?:\\.\\d{1,3}){2})(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))|(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)(?:\\.(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[a-z\\u00a1-\\uffff]{2,}))\\.?)(?::\\d{2,5})?(?:[/?#]\\S*)?"; private static final Pattern URL_PATTERN = Pattern.compile(REGEX_URL, Pattern.CASE_INSENSITIVE); private static final AttributeSet DEFAULT_ATTRIBUTES = new SimpleAttributeSet(); private static final AttributeSet LINK_ATTRIBUTES; static { MutableAttributeSet tmp = new SimpleAttributeSet(); StyleConstants.setUnderline(tmp, true); StyleConstants.setForeground(tmp, Styles.EditorColor.COLOR_HYPERLINK.getColor()); LINK_ATTRIBUTES = tmp; } private final StyledDocument doc; private final Timer timer; // as default constructor public AttributeInserterDocumentFilter(StyledDocument doc) { this.doc = doc; timer = new Timer(REFRESH_DELAY, new ActionListener() { @Override public void actionPerformed(ActionEvent e) { refreshPane(); } }); timer.setRepeats(false); } @Override public void insertString(FilterBypass fb, int offset, String string, AttributeSet attr) throws BadLocationException { super.insertString(fb, offset, string, attr); if (attr != null && attr.isDefined(StyleConstants.ComposedTextAttribute)) { // ignore } else { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { refreshPane(); } }); } } @Override public void remove(FilterBypass fb, int offset, int length) throws BadLocationException { boolean refresh = true; final AttributeSet attr = ((StyledDocument) fb.getDocument()).getCharacterElement(offset).getAttributes(); if (attr != null && attr.isDefined(StyleConstants.ComposedTextAttribute)) { refresh = false; } super.remove(fb, offset, length); if (refresh && length != 0 && fb.getDocument().getLength() != 0) { timer.restart(); } } @Override public void replace(FilterBypass fb, int offset, int length, String text, AttributeSet attrs) throws BadLocationException { super.replace(fb, offset, length, text, attrs); if (fb.getDocument().getLength() != 0) { timer.restart(); } } private void refreshPane() { final int docLength = doc.getLength(); if (docLength == 0) { return; } try { // clear attributes for (int i = 0; i < docLength; ++i) { if (doc.getCharacterElement(i).getAttributes().containsAttributes(LINK_ATTRIBUTES)) { doc.setCharacterAttributes(i, 1, DEFAULT_ATTRIBUTES, true); } } // URL detection final String text = doc.getText(0, docLength); final Matcher matcher = URL_PATTERN.matcher(text); while (matcher.find()) { final int offset = matcher.start(); final int targetLength = matcher.end() - offset; try { // Transform into clickable text AttributeSet atts = makeAttributes(offset, new URI(matcher.group())); doc.setCharacterAttributes(offset, targetLength, atts, true); } catch (URISyntaxException ex) { Log.log(ex); } } } catch (BadLocationException ex) { Log.log(ex); } } private AttributeSet makeAttributes(final int offset, final URI target) { SimpleAttributeSet atts = new SimpleAttributeSet(doc.getCharacterElement(offset).getAttributes()); atts.addAttributes(LINK_ATTRIBUTES); atts.addAttribute(ATTR_LINK, new IAttributeAction() { @Override public void execute() { try { Desktop.getDesktop().browse(target); } catch (Exception e) { Log.log(e); } } }); return atts; } } }