001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.help;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.Rectangle;
007import java.util.Objects;
008import java.util.regex.Matcher;
009import java.util.regex.Pattern;
010
011import javax.swing.JOptionPane;
012import javax.swing.event.HyperlinkEvent;
013import javax.swing.event.HyperlinkListener;
014import javax.swing.text.AttributeSet;
015import javax.swing.text.BadLocationException;
016import javax.swing.text.Document;
017import javax.swing.text.Element;
018import javax.swing.text.SimpleAttributeSet;
019import javax.swing.text.html.HTML.Tag;
020import javax.swing.text.html.HTMLDocument;
021
022import org.openstreetmap.josm.gui.HelpAwareOptionPane;
023import org.openstreetmap.josm.gui.widgets.JosmEditorPane;
024import org.openstreetmap.josm.tools.Logging;
025import org.openstreetmap.josm.tools.OpenBrowser;
026
027/**
028 * Handles clicks on hyperlinks inside {@link HelpBrowser}.
029 * @since 14807
030 */
031public class HyperlinkHandler implements HyperlinkListener {
032
033    private final IHelpBrowser browser;
034    private final JosmEditorPane help;
035
036    /**
037     * Constructs a new {@code HyperlinkHandler}.
038     * @param browser help browser
039     * @param help inner help pane
040     */
041    public HyperlinkHandler(IHelpBrowser browser, JosmEditorPane help) {
042        this.browser = Objects.requireNonNull(browser);
043        this.help = Objects.requireNonNull(help);
044    }
045
046    /**
047     * Scrolls the help browser to the element with id <code>id</code>
048     *
049     * @param id the id
050     * @return true, if an element with this id was found and scrolling was successful; false, otherwise
051     */
052    protected boolean scrollToElementWithId(String id) {
053        Document d = help.getDocument();
054        if (d instanceof HTMLDocument) {
055            Element element = ((HTMLDocument) d).getElement(id);
056            try {
057                if (element != null) {
058                    // Deprecated API to replace only when migrating to Java 9 (replacement not available in Java 8)
059                    @SuppressWarnings("deprecation")
060                    Rectangle r = help.modelToView(element.getStartOffset());
061                    if (r != null) {
062                        Rectangle vis = help.getVisibleRect();
063                        r.height = vis.height;
064                        help.scrollRectToVisible(r);
065                        return true;
066                    }
067                }
068            } catch (BadLocationException e) {
069                Logging.warn(tr("Bad location in HTML document. Exception was: {0}", e.toString()));
070                Logging.error(e);
071            }
072        }
073        return false;
074    }
075
076    /**
077     * Checks whether the hyperlink event originated on a &lt;a ...&gt; element with
078     * a relative href consisting of a URL fragment only, i.e.
079     * &lt;a href="#thisIsALocalFragment"&gt;. If so, replies the fragment, i.e. "thisIsALocalFragment".
080     *
081     * Otherwise, replies <code>null</code>
082     *
083     * @param e the hyperlink event
084     * @return the local fragment or <code>null</code>
085     */
086    protected String getUrlFragment(HyperlinkEvent e) {
087        AttributeSet set = e.getSourceElement().getAttributes();
088        Object value = set.getAttribute(Tag.A);
089        if (!(value instanceof SimpleAttributeSet))
090            return null;
091        SimpleAttributeSet atts = (SimpleAttributeSet) value;
092        value = atts.getAttribute(javax.swing.text.html.HTML.Attribute.HREF);
093        if (value == null)
094            return null;
095        String s = (String) value;
096        Matcher m = Pattern.compile("(?:"+browser.getUrl()+")?#(.+)").matcher(s);
097        if (m.matches())
098            return m.group(1);
099        return null;
100    }
101
102    @Override
103    public void hyperlinkUpdate(HyperlinkEvent e) {
104        if (e.getEventType() != HyperlinkEvent.EventType.ACTIVATED)
105            return;
106        if (e.getURL() == null || e.getURL().toExternalForm().startsWith(browser.getUrl()+'#')) {
107            // Probably hyperlink event on a an A-element with a href consisting of a fragment only, i.e. "#ALocalFragment".
108            String fragment = getUrlFragment(e);
109            if (fragment != null) {
110                // first try to scroll to an element with id==fragment. This is the way
111                // table of contents are built in the JOSM wiki. If this fails, try to
112                // scroll to a <A name="..."> element.
113                //
114                if (!scrollToElementWithId(fragment)) {
115                    help.scrollToReference(fragment);
116                }
117            } else {
118                HelpAwareOptionPane.showOptionDialog(
119                        HelpBrowser.getInstance(),
120                        tr("Failed to open help page. The target URL is empty."),
121                        tr("Failed to open help page"),
122                        JOptionPane.ERROR_MESSAGE,
123                        null, /* no icon */
124                        null, /* standard options, just OK button */
125                        null, /* default is standard */
126                        null /* no help context */
127                );
128            }
129        } else if (e.getURL().toExternalForm().endsWith("action=edit")) {
130            OpenBrowser.displayUrl(e.getURL().toExternalForm());
131        } else {
132            String url = e.getURL().toExternalForm();
133            browser.setUrl(url);
134            if (url.startsWith(HelpUtil.getWikiBaseUrl())) {
135                browser.openUrl(url);
136            } else {
137                OpenBrowser.displayUrl(url);
138            }
139        }
140    }
141}