/**************************************************************************
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;
}
}
}