package chatty.gui.components; import chatty.Helper; import chatty.gui.LinkListener; import chatty.gui.components.menus.ContextMenuListener; import chatty.gui.components.textpane.LinkController; import chatty.gui.components.textpane.WrapLabelView; import java.net.URI; import java.net.URISyntaxException; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.TreeMap; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.swing.JTextPane; import javax.swing.text.AbstractDocument; import javax.swing.text.AttributeSet; import javax.swing.text.BadLocationException; import javax.swing.text.BoxView; import javax.swing.text.ComponentView; import javax.swing.text.Element; import javax.swing.text.IconView; import javax.swing.text.LabelView; import javax.swing.text.MutableAttributeSet; import javax.swing.text.ParagraphView; import javax.swing.text.SimpleAttributeSet; import javax.swing.text.StyleConstants; import javax.swing.text.StyledDocument; import javax.swing.text.StyledEditorKit; import javax.swing.text.View; import javax.swing.text.ViewFactory; import javax.swing.text.html.HTML; /** * Simple text pane that turns URLs and SRL channels into clickable links. * * @author tduva */ public class ExtendedTextPane extends JTextPane { private static final Logger LOGGER = Logger.getLogger(ExtendedTextPane.class.getName()); private final StyledDocument doc; /** * The matcher for finding URLs. */ private static final Matcher urlMatcher = Helper.getUrlPattern().matcher(""); /** * The regex and matcher for finding SRL channels (e.g. #srl-abc). */ private static final String srlRegex = "#srl-([a-z0-9]{2,6})"; private static final Matcher srlMatcher = Pattern.compile(srlRegex).matcher(""); /** * To build an SRL race URL. */ private static final String SRL_URL = "http://speedrunslive.com/race/?id="; private final LinkController linkController; public ExtendedTextPane() { setEditorKit(new MyEditorKit()); linkController = new LinkController(); this.addMouseListener(linkController); this.addMouseMotionListener(linkController); doc = this.getStyledDocument(); } public void setLinkListener(LinkListener listener) { linkController.setLinkListener(listener); } public void setContextMenuListener(ContextMenuListener listener) { linkController.setContextMenuListener(listener); } /** * Set this text pane to a new text. Removes any previous content and adds * the new text while adding clickablel links. * * @param text */ @Override public void setText(String text) { try { doc.remove(0, doc.getLength()); } catch (BadLocationException ex) { LOGGER.warning("Bad location"); } printSpecials(text); } /** * Print special stuff in the text like links and emoticons differently. * * First a map of all special stuff that can be found in the text is built, * in a way that stuff doesn't overlap with previously found stuff. * * Then all the special stuff in this map is printed accordingly, while * printing the stuff inbetween with regular style. * * @param text */ protected void printSpecials(String text) { // Where stuff was found TreeMap<Integer,Integer> ranges = new TreeMap<>(); // The style of the stuff (basicially metadata) HashMap<Integer,MutableAttributeSet> rangesStyle = new HashMap<>(); findLinks(text, ranges, rangesStyle); findSrl(text, ranges, rangesStyle); // Actually print everything int lastPrintedPos = 0; Iterator<Map.Entry<Integer, Integer>> rangesIt = ranges.entrySet().iterator(); while (rangesIt.hasNext()) { Map.Entry<Integer, Integer> range = rangesIt.next(); int start = range.getKey(); int end = range.getValue(); if (start > lastPrintedPos) { // If there is anything between the special stuff, print that // first as regular text print(text.substring(lastPrintedPos, start), null); } print(text.substring(start, end + 1),rangesStyle.get(start)); lastPrintedPos = end + 1; } // If anything is left, print that as well as regular text if (lastPrintedPos < text.length()) { print(text.substring(lastPrintedPos), null); } } /** * Finds all URLs and saves them to be printed as clickable links. * * @param text The text to find the URLs in. * @param ranges The ranges in the text that are already taken by other * links. * @param rangesStyle */ private void findLinks(String text, Map<Integer, Integer> ranges, Map<Integer, MutableAttributeSet> rangesStyle) { // Find links urlMatcher.reset(text); while (urlMatcher.find()) { int start = urlMatcher.start(); int end = urlMatcher.end() - 1; if (!inRanges(start, ranges) && !inRanges(end,ranges)) { String foundUrl = urlMatcher.group(); if (foundUrl.contains("..")) { continue; } // Check if URL contains ( ) like http://example.com/test(abc) // or is just contained in ( ) like (http://example.com) // (of course this won't work perfectly, but it should be ok) if (foundUrl.endsWith(")") && !foundUrl.contains("(")) { foundUrl = foundUrl.substring(0, foundUrl.length() - 1); end--; } if (checkUrl(foundUrl)) { ranges.put(start, end); if (!foundUrl.startsWith("http")) { foundUrl = "http://"+foundUrl; } rangesStyle.put(start, url(foundUrl)); } } } } /** * Finds all SRL channels and saves them to be printed as clickable links. * * @param text The text to find the SRL channels in. * @param ranges The ranges in the text that are already take by other * links. * @param rangesStyle The style associated with a range (metadata). */ private void findSrl(String text, Map<Integer, Integer> ranges, Map<Integer, MutableAttributeSet> rangesStyle) { srlMatcher.reset(text); while (srlMatcher.find()) { int start = srlMatcher.start(); int end = srlMatcher.end() - 1; if (!inRanges(start, ranges) && !inRanges(end, ranges)) { String foundSrl = srlMatcher.group(); String url = SRL_URL+foundSrl; ranges.put(start, end); rangesStyle.put(start, url(url)); } } } /** * Print the given text with the given style. Used to be able to output * links. * * @param text * @param printStyle */ private void print(String text, AttributeSet printStyle) { try { doc.insertString(doc.getLength(), text, printStyle); } catch (BadLocationException e) { LOGGER.warning("Bad location"); } } /** * Checks if the given integer is within the range of any of the key=value * pairs of the Map (inclusive). * * @param i * @param ranges * @return */ private boolean inRanges(int i, Map<Integer,Integer> ranges) { Iterator<Map.Entry<Integer, Integer>> rangesIt = ranges.entrySet().iterator(); while (rangesIt.hasNext()) { Map.Entry<Integer, Integer> range = rangesIt.next(); if (i >= range.getKey() && i <= range.getValue()) { return true; } } return false; } /** * Checks if the Url can be later used as a URI. * * @param uriToCheck * @return */ private boolean checkUrl(String uriToCheck) { try { new URI(uriToCheck); } catch (URISyntaxException ex) { return false; } return true; } /** * Make a link style for the given URL. * * @param url * @return */ public MutableAttributeSet url(String url) { SimpleAttributeSet urlStyle = new SimpleAttributeSet(); StyleConstants.setUnderline(urlStyle, true); urlStyle.addAttribute(HTML.Attribute.HREF, url); return urlStyle; } /** * Replaces one view to wrap long words. */ private static class MyEditorKit extends StyledEditorKit { @Override public ViewFactory getViewFactory() { return new StyledViewFactory(); } static class StyledViewFactory implements ViewFactory { @Override public View create(Element elem) { String kind = elem.getName(); if (kind != null) { if (kind.equals(AbstractDocument.ContentElementName)) { return new WrapLabelView(elem); } else if (kind.equals(AbstractDocument.ParagraphElementName)) { return new ParagraphView(elem); } else if (kind.equals(AbstractDocument.SectionElementName)) { return new BoxView(elem, View.Y_AXIS); } else if (kind.equals(StyleConstants.ComponentElementName)) { return new ComponentView(elem); } else if (kind.equals(StyleConstants.IconElementName)) { return new IconView(elem); } } return new LabelView(elem); } } } }