source: josm/trunk/src/org/openstreetmap/josm/gui/help/HelpBrowser.java@ 13661

Last change on this file since 13661 was 13457, checked in by Don-vip, 6 years ago

fix #15985 - fix handling of relative links in help browser

  • Property svn:eol-style set to native
File size: 24.1 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.help;
3
4import static org.openstreetmap.josm.gui.help.HelpUtil.buildAbsoluteHelpTopic;
5import static org.openstreetmap.josm.gui.help.HelpUtil.getHelpTopicEditUrl;
6import static org.openstreetmap.josm.gui.help.HelpUtil.getHelpTopicUrl;
7import static org.openstreetmap.josm.tools.I18n.tr;
8
9import java.awt.BorderLayout;
10import java.awt.Dimension;
11import java.awt.GraphicsEnvironment;
12import java.awt.Rectangle;
13import java.awt.event.ActionEvent;
14import java.awt.event.WindowAdapter;
15import java.awt.event.WindowEvent;
16import java.io.IOException;
17import java.io.StringReader;
18import java.nio.charset.StandardCharsets;
19import java.util.Locale;
20import java.util.regex.Matcher;
21import java.util.regex.Pattern;
22
23import javax.swing.AbstractAction;
24import javax.swing.JButton;
25import javax.swing.JFrame;
26import javax.swing.JMenuItem;
27import javax.swing.JOptionPane;
28import javax.swing.JPanel;
29import javax.swing.JScrollPane;
30import javax.swing.JSeparator;
31import javax.swing.JToolBar;
32import javax.swing.SwingUtilities;
33import javax.swing.event.ChangeEvent;
34import javax.swing.event.ChangeListener;
35import javax.swing.event.HyperlinkEvent;
36import javax.swing.event.HyperlinkListener;
37import javax.swing.text.AttributeSet;
38import javax.swing.text.BadLocationException;
39import javax.swing.text.Document;
40import javax.swing.text.Element;
41import javax.swing.text.SimpleAttributeSet;
42import javax.swing.text.html.HTML.Tag;
43import javax.swing.text.html.HTMLDocument;
44import javax.swing.text.html.StyleSheet;
45
46import org.openstreetmap.josm.Main;
47import org.openstreetmap.josm.actions.JosmAction;
48import org.openstreetmap.josm.gui.HelpAwareOptionPane;
49import org.openstreetmap.josm.gui.MainApplication;
50import org.openstreetmap.josm.gui.MainMenu;
51import org.openstreetmap.josm.gui.util.WindowGeometry;
52import org.openstreetmap.josm.gui.widgets.JosmEditorPane;
53import org.openstreetmap.josm.gui.widgets.JosmHTMLEditorKit;
54import org.openstreetmap.josm.io.CachedFile;
55import org.openstreetmap.josm.tools.ImageProvider;
56import org.openstreetmap.josm.tools.InputMapUtils;
57import org.openstreetmap.josm.tools.LanguageInfo.LocaleType;
58import org.openstreetmap.josm.tools.Logging;
59import org.openstreetmap.josm.tools.OpenBrowser;
60
61/**
62 * Help browser displaying HTML pages fetched from JOSM wiki.
63 */
64public class HelpBrowser extends JFrame implements IHelpBrowser {
65
66 /** the unique instance */
67 private static HelpBrowser instance;
68
69 /** the menu item in the windows menu. Required to properly hide on dialog close */
70 private JMenuItem windowMenuItem;
71
72 /** the help browser */
73 private JosmEditorPane help;
74
75 /** the help browser history */
76 private transient HelpBrowserHistory history;
77
78 /** the currently displayed URL */
79 private String url;
80
81 private final transient HelpContentReader reader;
82
83 private static final JosmAction FOCUS_ACTION = new JosmAction(tr("JOSM Help Browser"), "help", "", null, false, false) {
84 @Override
85 public void actionPerformed(ActionEvent e) {
86 HelpBrowser.getInstance().setVisible(true);
87 }
88 };
89
90 /**
91 * Constructs a new {@code HelpBrowser}.
92 */
93 public HelpBrowser() {
94 reader = new HelpContentReader(HelpUtil.getWikiBaseUrl());
95 build();
96 }
97
98 /**
99 * Replies the unique instance of the help browser
100 *
101 * @return the unique instance of the help browser
102 */
103 public static synchronized HelpBrowser getInstance() {
104 if (instance == null) {
105 instance = new HelpBrowser();
106 }
107 return instance;
108 }
109
110 /**
111 * Show the help page for help topic <code>helpTopic</code>.
112 *
113 * @param helpTopic the help topic
114 */
115 public static void setUrlForHelpTopic(final String helpTopic) {
116 final HelpBrowser browser = getInstance();
117 SwingUtilities.invokeLater(() -> {
118 browser.openHelpTopic(helpTopic);
119 browser.setVisible(true);
120 browser.toFront();
121 });
122 }
123
124 /**
125 * Launches the internal help browser and directs it to the help page for
126 * <code>helpTopic</code>.
127 *
128 * @param helpTopic the help topic
129 */
130 public static void launchBrowser(String helpTopic) {
131 HelpBrowser browser = getInstance();
132 browser.openHelpTopic(helpTopic);
133 browser.setVisible(true);
134 browser.toFront();
135 }
136
137 /**
138 * Builds the style sheet used in the internal help browser
139 *
140 * @return the style sheet
141 */
142 protected StyleSheet buildStyleSheet() {
143 StyleSheet ss = new StyleSheet();
144 final String css;
145 try (CachedFile cf = new CachedFile("resource://data/help-browser.css")) {
146 css = new String(cf.getByteContent(), StandardCharsets.ISO_8859_1);
147 } catch (IOException e) {
148 Logging.error(tr("Failed to read CSS file ''help-browser.css''. Exception is: {0}", e.toString()));
149 Logging.error(e);
150 return ss;
151 }
152 ss.addRule(css);
153 return ss;
154 }
155
156 /**
157 * Builds toolbar.
158 * @return the toolbar
159 */
160 protected JToolBar buildToolBar() {
161 JToolBar tb = new JToolBar();
162 tb.add(new JButton(new HomeAction(this)));
163 tb.add(new JButton(new BackAction(this)));
164 tb.add(new JButton(new ForwardAction(this)));
165 tb.add(new JButton(new ReloadAction(this)));
166 tb.add(new JSeparator());
167 tb.add(new JButton(new OpenInBrowserAction(this)));
168 tb.add(new JButton(new EditAction(this)));
169 return tb;
170 }
171
172 /**
173 * Builds GUI.
174 */
175 protected final void build() {
176 help = new JosmEditorPane();
177 JosmHTMLEditorKit kit = new JosmHTMLEditorKit();
178 kit.setStyleSheet(buildStyleSheet());
179 help.setEditorKit(kit);
180 help.setEditable(false);
181 help.addHyperlinkListener(new HyperlinkHandler());
182 help.setContentType("text/html");
183 history = new HelpBrowserHistory(this);
184
185 JPanel p = new JPanel(new BorderLayout());
186 setContentPane(p);
187
188 p.add(new JScrollPane(help), BorderLayout.CENTER);
189
190 addWindowListener(new WindowAdapter() {
191 @Override public void windowClosing(WindowEvent e) {
192 setVisible(false);
193 }
194 });
195
196 p.add(buildToolBar(), BorderLayout.NORTH);
197 InputMapUtils.addEscapeAction(getRootPane(), new AbstractAction() {
198 @Override
199 public void actionPerformed(ActionEvent e) {
200 setVisible(false);
201 }
202 });
203
204 setMinimumSize(new Dimension(400, 200));
205 setTitle(tr("JOSM Help Browser"));
206 }
207
208 @Override
209 public void setVisible(boolean visible) {
210 if (visible) {
211 new WindowGeometry(
212 getClass().getName() + ".geometry",
213 WindowGeometry.centerInWindow(
214 getParent(),
215 new Dimension(600, 400)
216 )
217 ).applySafe(this);
218 } else if (isShowing()) { // Avoid IllegalComponentStateException like in #8775
219 new WindowGeometry(this).remember(getClass().getName() + ".geometry");
220 }
221 MainMenu menu = MainApplication.getMenu();
222 if (menu != null && menu.windowMenu != null) {
223 if (windowMenuItem != null && !visible) {
224 menu.windowMenu.remove(windowMenuItem);
225 windowMenuItem = null;
226 }
227 if (windowMenuItem == null && visible) {
228 windowMenuItem = MainMenu.add(menu.windowMenu, FOCUS_ACTION, MainMenu.WINDOW_MENU_GROUP.VOLATILE);
229 }
230 }
231 super.setVisible(visible);
232 }
233
234 /**
235 * Load help topic.
236 * @param content topic contents
237 */
238 protected void loadTopic(String content) {
239 Document document = help.getEditorKit().createDefaultDocument();
240 try {
241 help.getEditorKit().read(new StringReader(content), document, 0);
242 } catch (IOException | BadLocationException e) {
243 Logging.error(e);
244 }
245 help.setDocument(document);
246 }
247
248 @Override
249 public String getUrl() {
250 return url;
251 }
252
253 /**
254 * Displays a warning page when a help topic doesn't exist yet.
255 *
256 * @param relativeHelpTopic the help topic
257 */
258 protected void handleMissingHelpContent(String relativeHelpTopic) {
259 // i18n: do not translate "warning-header" and "warning-body"
260 String message = tr("<html><p class=\"warning-header\">Help content for help topic missing</p>"
261 + "<p class=\"warning-body\">Help content for the help topic <strong>{0}</strong> is "
262 + "not available yet. It is missing both in your local language ({1}) and in English.<br><br>"
263 + "Please help to improve the JOSM help system and fill in the missing information. "
264 + "You can both edit the <a href=\"{2}\">help topic in your local language ({1})</a> and "
265 + "the <a href=\"{3}\">help topic in English</a>."
266 + "</p></html>",
267 relativeHelpTopic,
268 Locale.getDefault().getDisplayName(),
269 getHelpTopicEditUrl(buildAbsoluteHelpTopic(relativeHelpTopic, LocaleType.DEFAULT)),
270 getHelpTopicEditUrl(buildAbsoluteHelpTopic(relativeHelpTopic, LocaleType.ENGLISH))
271 );
272 loadTopic(message);
273 }
274
275 /**
276 * Displays a error page if a help topic couldn't be loaded because of network or IO error.
277 *
278 * @param relativeHelpTopic the help topic
279 * @param e the exception
280 */
281 protected void handleHelpContentReaderException(String relativeHelpTopic, HelpContentReaderException e) {
282 String message = tr("<html><p class=\"error-header\">Error when retrieving help information</p>"
283 + "<p class=\"error-body\">The content for the help topic <strong>{0}</strong> could "
284 + "not be loaded. The error message is (untranslated):<br>"
285 + "<tt>{1}</tt>"
286 + "</p></html>",
287 relativeHelpTopic,
288 e.toString()
289 );
290 loadTopic(message);
291 }
292
293 /**
294 * Loads a help topic given by a relative help topic name (i.e. "/Action/New")
295 *
296 * First tries to load the language specific help topic. If it is missing, tries to
297 * load the topic in English.
298 *
299 * @param relativeHelpTopic the relative help topic
300 */
301 protected void loadRelativeHelpTopic(String relativeHelpTopic) {
302 String url = getHelpTopicUrl(buildAbsoluteHelpTopic(relativeHelpTopic, LocaleType.DEFAULTNOTENGLISH));
303 String content = null;
304 try {
305 content = reader.fetchHelpTopicContent(url, true);
306 } catch (MissingHelpContentException e) {
307 Logging.trace(e);
308 url = getHelpTopicUrl(buildAbsoluteHelpTopic(relativeHelpTopic, LocaleType.BASELANGUAGE));
309 try {
310 content = reader.fetchHelpTopicContent(url, true);
311 } catch (MissingHelpContentException e1) {
312 Logging.trace(e1);
313 url = getHelpTopicUrl(buildAbsoluteHelpTopic(relativeHelpTopic, LocaleType.ENGLISH));
314 try {
315 content = reader.fetchHelpTopicContent(url, true);
316 } catch (MissingHelpContentException e2) {
317 Logging.debug(e2);
318 this.url = url;
319 handleMissingHelpContent(relativeHelpTopic);
320 return;
321 } catch (HelpContentReaderException e2) {
322 Logging.error(e2);
323 handleHelpContentReaderException(relativeHelpTopic, e2);
324 return;
325 }
326 } catch (HelpContentReaderException e1) {
327 Logging.error(e1);
328 handleHelpContentReaderException(relativeHelpTopic, e1);
329 return;
330 }
331 } catch (HelpContentReaderException e) {
332 Logging.error(e);
333 handleHelpContentReaderException(relativeHelpTopic, e);
334 return;
335 }
336 loadTopic(content);
337 history.setCurrentUrl(url);
338 this.url = url;
339 }
340
341 /**
342 * Loads a help topic given by an absolute help topic name, i.e.
343 * "/De:Help/Action/New"
344 *
345 * @param absoluteHelpTopic the absolute help topic name
346 */
347 protected void loadAbsoluteHelpTopic(String absoluteHelpTopic) {
348 String url = getHelpTopicUrl(absoluteHelpTopic);
349 String content = null;
350 try {
351 content = reader.fetchHelpTopicContent(url, true);
352 } catch (MissingHelpContentException e) {
353 Logging.debug(e);
354 this.url = url;
355 handleMissingHelpContent(absoluteHelpTopic);
356 return;
357 } catch (HelpContentReaderException e) {
358 Logging.error(e);
359 handleHelpContentReaderException(absoluteHelpTopic, e);
360 return;
361 }
362 loadTopic(content);
363 history.setCurrentUrl(url);
364 this.url = url;
365 }
366
367 @Override
368 public void openUrl(String url) {
369 if (!isVisible()) {
370 setVisible(true);
371 toFront();
372 } else {
373 toFront();
374 }
375 String helpTopic = HelpUtil.extractAbsoluteHelpTopic(url);
376 if (helpTopic == null) {
377 try {
378 this.url = url;
379 String content = reader.fetchHelpTopicContent(url, false);
380 loadTopic(content);
381 history.setCurrentUrl(url);
382 this.url = url;
383 } catch (HelpContentReaderException e) {
384 Logging.warn(e);
385 HelpAwareOptionPane.showOptionDialog(
386 Main.parent,
387 tr(
388 "<html>Failed to open help page for url {0}.<br>"
389 + "This is most likely due to a network problem, please check<br>"
390 + "your internet connection</html>",
391 url
392 ),
393 tr("Failed to open URL"),
394 JOptionPane.ERROR_MESSAGE,
395 null, /* no icon */
396 null, /* standard options, just OK button */
397 null, /* default is standard */
398 null /* no help context */
399 );
400 }
401 history.setCurrentUrl(url);
402 } else {
403 loadAbsoluteHelpTopic(helpTopic);
404 }
405 }
406
407 @Override
408 public void openHelpTopic(String relativeHelpTopic) {
409 if (!isVisible()) {
410 setVisible(true);
411 toFront();
412 } else {
413 toFront();
414 }
415 loadRelativeHelpTopic(relativeHelpTopic);
416 }
417
418 abstract static class AbstractBrowserAction extends AbstractAction {
419 protected final transient IHelpBrowser browser;
420
421 protected AbstractBrowserAction(IHelpBrowser browser) {
422 this.browser = browser;
423 }
424 }
425
426 static class OpenInBrowserAction extends AbstractBrowserAction {
427
428 /**
429 * Constructs a new {@code OpenInBrowserAction}.
430 * @param browser help browser
431 */
432 OpenInBrowserAction(IHelpBrowser browser) {
433 super(browser);
434 putValue(SHORT_DESCRIPTION, tr("Open the current help page in an external browser"));
435 new ImageProvider("help", "internet").getResource().attachImageIcon(this, true);
436 }
437
438 @Override
439 public void actionPerformed(ActionEvent e) {
440 OpenBrowser.displayUrl(browser.getUrl());
441 }
442 }
443
444 static class EditAction extends AbstractBrowserAction {
445
446 /**
447 * Constructs a new {@code EditAction}.
448 * @param browser help browser
449 */
450 EditAction(IHelpBrowser browser) {
451 super(browser);
452 putValue(SHORT_DESCRIPTION, tr("Edit the current help page"));
453 new ImageProvider("dialogs", "edit").getResource().attachImageIcon(this, true);
454 }
455
456 @Override
457 public void actionPerformed(ActionEvent e) {
458 String url = browser.getUrl();
459 if (url == null)
460 return;
461 if (!url.startsWith(HelpUtil.getWikiBaseHelpUrl())) {
462 String message = tr(
463 "<html>The current URL <tt>{0}</tt><br>"
464 + "is an external URL. Editing is only possible for help topics<br>"
465 + "on the help server <tt>{1}</tt>.</html>",
466 url,
467 HelpUtil.getWikiBaseUrl()
468 );
469 if (!GraphicsEnvironment.isHeadless()) {
470 JOptionPane.showMessageDialog(
471 Main.parent,
472 message,
473 tr("Warning"),
474 JOptionPane.WARNING_MESSAGE
475 );
476 }
477 return;
478 }
479 url = url.replaceAll("#[^#]*$", "");
480 OpenBrowser.displayUrl(url+"?action=edit");
481 }
482 }
483
484 static class ReloadAction extends AbstractBrowserAction {
485
486 /**
487 * Constructs a new {@code ReloadAction}.
488 * @param browser help browser
489 */
490 ReloadAction(IHelpBrowser browser) {
491 super(browser);
492 putValue(SHORT_DESCRIPTION, tr("Reload the current help page"));
493 new ImageProvider("dialogs", "refresh").getResource().attachImageIcon(this, true);
494 }
495
496 @Override
497 public void actionPerformed(ActionEvent e) {
498 browser.openUrl(browser.getUrl());
499 }
500 }
501
502 static class BackAction extends AbstractBrowserAction implements ChangeListener {
503
504 /**
505 * Constructs a new {@code BackAction}.
506 * @param browser help browser
507 */
508 BackAction(IHelpBrowser browser) {
509 super(browser);
510 browser.getHistory().addChangeListener(this);
511 putValue(SHORT_DESCRIPTION, tr("Go to the previous page"));
512 new ImageProvider("help", "previous").getResource().attachImageIcon(this, true);
513 setEnabled(browser.getHistory().canGoBack());
514 }
515
516 @Override
517 public void actionPerformed(ActionEvent e) {
518 browser.getHistory().back();
519 }
520
521 @Override
522 public void stateChanged(ChangeEvent e) {
523 setEnabled(browser.getHistory().canGoBack());
524 }
525 }
526
527 static class ForwardAction extends AbstractBrowserAction implements ChangeListener {
528
529 /**
530 * Constructs a new {@code ForwardAction}.
531 * @param browser help browser
532 */
533 ForwardAction(IHelpBrowser browser) {
534 super(browser);
535 browser.getHistory().addChangeListener(this);
536 putValue(SHORT_DESCRIPTION, tr("Go to the next page"));
537 new ImageProvider("help", "next").getResource().attachImageIcon(this, true);
538 setEnabled(browser.getHistory().canGoForward());
539 }
540
541 @Override
542 public void actionPerformed(ActionEvent e) {
543 browser.getHistory().forward();
544 }
545
546 @Override
547 public void stateChanged(ChangeEvent e) {
548 setEnabled(browser.getHistory().canGoForward());
549 }
550 }
551
552 static class HomeAction extends AbstractBrowserAction {
553
554 /**
555 * Constructs a new {@code HomeAction}.
556 * @param browser help browser
557 */
558 HomeAction(IHelpBrowser browser) {
559 super(browser);
560 putValue(SHORT_DESCRIPTION, tr("Go to the JOSM help home page"));
561 new ImageProvider("help", "home").getResource().attachImageIcon(this, true);
562 }
563
564 @Override
565 public void actionPerformed(ActionEvent e) {
566 browser.openHelpTopic("/");
567 }
568 }
569
570 class HyperlinkHandler implements HyperlinkListener {
571
572 /**
573 * Scrolls the help browser to the element with id <code>id</code>
574 *
575 * @param id the id
576 * @return true, if an element with this id was found and scrolling was successful; false, otherwise
577 */
578 protected boolean scrollToElementWithId(String id) {
579 Document d = help.getDocument();
580 if (d instanceof HTMLDocument) {
581 HTMLDocument doc = (HTMLDocument) d;
582 Element element = doc.getElement(id);
583 try {
584 // Deprecated API to replace only when migrating to Java 9 (replacement not available in Java 8)
585 @SuppressWarnings("deprecation")
586 Rectangle r = help.modelToView(element.getStartOffset());
587 if (r != null) {
588 Rectangle vis = help.getVisibleRect();
589 r.height = vis.height;
590 help.scrollRectToVisible(r);
591 return true;
592 }
593 } catch (BadLocationException e) {
594 Logging.warn(tr("Bad location in HTML document. Exception was: {0}", e.toString()));
595 Logging.error(e);
596 }
597 }
598 return false;
599 }
600
601 /**
602 * Checks whether the hyperlink event originated on a &lt;a ...&gt; element with
603 * a relative href consisting of a URL fragment only, i.e.
604 * &lt;a href="#thisIsALocalFragment"&gt;. If so, replies the fragment, i.e. "thisIsALocalFragment".
605 *
606 * Otherwise, replies <code>null</code>
607 *
608 * @param e the hyperlink event
609 * @return the local fragment or <code>null</code>
610 */
611 protected String getUrlFragment(HyperlinkEvent e) {
612 AttributeSet set = e.getSourceElement().getAttributes();
613 Object value = set.getAttribute(Tag.A);
614 if (!(value instanceof SimpleAttributeSet))
615 return null;
616 SimpleAttributeSet atts = (SimpleAttributeSet) value;
617 value = atts.getAttribute(javax.swing.text.html.HTML.Attribute.HREF);
618 if (value == null)
619 return null;
620 String s = (String) value;
621 Matcher m = Pattern.compile("(?:"+url+")?#(.+)").matcher(s);
622 if (m.matches())
623 return m.group(1);
624 return null;
625 }
626
627 @Override
628 public void hyperlinkUpdate(HyperlinkEvent e) {
629 if (e.getEventType() != HyperlinkEvent.EventType.ACTIVATED)
630 return;
631 if (e.getURL() == null || e.getURL().toExternalForm().startsWith(url+'#')) {
632 // Probably hyperlink event on a an A-element with a href consisting of a fragment only, i.e. "#ALocalFragment".
633 String fragment = getUrlFragment(e);
634 if (fragment != null) {
635 // first try to scroll to an element with id==fragment. This is the way
636 // table of contents are built in the JOSM wiki. If this fails, try to
637 // scroll to a <A name="..."> element.
638 //
639 if (!scrollToElementWithId(fragment)) {
640 help.scrollToReference(fragment);
641 }
642 } else {
643 HelpAwareOptionPane.showOptionDialog(
644 instance,
645 tr("Failed to open help page. The target URL is empty."),
646 tr("Failed to open help page"),
647 JOptionPane.ERROR_MESSAGE,
648 null, /* no icon */
649 null, /* standard options, just OK button */
650 null, /* default is standard */
651 null /* no help context */
652 );
653 }
654 } else if (e.getURL().toExternalForm().endsWith("action=edit")) {
655 OpenBrowser.displayUrl(e.getURL().toExternalForm());
656 } else {
657 url = e.getURL().toExternalForm();
658 if (url.startsWith(HelpUtil.getWikiBaseUrl())) {
659 openUrl(url);
660 } else {
661 OpenBrowser.displayUrl(url);
662 }
663 }
664 }
665 }
666
667 @Override
668 public HelpBrowserHistory getHistory() {
669 return history;
670 }
671}
Note: See TracBrowser for help on using the repository browser.