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 cliks 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 <a ...> element with 078 * a relative href consisting of a URL fragment only, i.e. 079 * <a href="#thisIsALocalFragment">. 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}