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

Last change on this file since 10627 was 10627, checked in by Don-vip, 8 years ago

sonar - squid:S1166 - Exception handlers should preserve the original exceptions

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