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

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

see #15182 - deprecate all Main logging methods and introduce suitable replacements in Logging for most of them

  • 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.WindowAdapter;
15import java.awt.event.WindowEvent;
16import java.io.IOException;
17import java.io.StringReader;
18import java.nio.charset.StandardCharsets;
19import java.util.Locale;
20
21import javax.swing.AbstractAction;
22import javax.swing.JButton;
23import javax.swing.JFrame;
24import javax.swing.JMenuItem;
25import javax.swing.JOptionPane;
26import javax.swing.JPanel;
27import javax.swing.JScrollPane;
28import javax.swing.JSeparator;
29import javax.swing.JToolBar;
30import javax.swing.SwingUtilities;
31import javax.swing.event.ChangeEvent;
32import javax.swing.event.ChangeListener;
33import javax.swing.event.HyperlinkEvent;
34import javax.swing.event.HyperlinkListener;
35import javax.swing.text.AttributeSet;
36import javax.swing.text.BadLocationException;
37import javax.swing.text.Document;
38import javax.swing.text.Element;
39import javax.swing.text.SimpleAttributeSet;
40import javax.swing.text.html.HTML.Tag;
41import javax.swing.text.html.HTMLDocument;
42import javax.swing.text.html.StyleSheet;
43
44import org.openstreetmap.josm.Main;
45import org.openstreetmap.josm.actions.JosmAction;
46import org.openstreetmap.josm.gui.HelpAwareOptionPane;
47import org.openstreetmap.josm.gui.MainMenu;
48import org.openstreetmap.josm.gui.widgets.JosmEditorPane;
49import org.openstreetmap.josm.gui.widgets.JosmHTMLEditorKit;
50import org.openstreetmap.josm.io.CachedFile;
51import org.openstreetmap.josm.tools.ImageProvider;
52import org.openstreetmap.josm.tools.InputMapUtils;
53import org.openstreetmap.josm.tools.LanguageInfo.LocaleType;
54import org.openstreetmap.josm.tools.Logging;
55import org.openstreetmap.josm.tools.OpenBrowser;
56import org.openstreetmap.josm.tools.WindowGeometry;
57
58/**
59 * Help browser displaying HTML pages fetched from JOSM wiki.
60 */
61public class HelpBrowser extends JFrame implements IHelpBrowser {
62
63 /** the unique instance */
64 private static HelpBrowser instance;
65
66 /** the menu item in the windows menu. Required to properly hide on dialog close */
67 private JMenuItem windowMenuItem;
68
69 /** the help browser */
70 private JosmEditorPane help;
71
72 /** the help browser history */
73 private transient HelpBrowserHistory history;
74
75 /** the currently displayed URL */
76 private String url;
77
78 private final transient HelpContentReader reader;
79
80 private static final JosmAction FOCUS_ACTION = new JosmAction(tr("JOSM Help Browser"), "help", "", null, false, false) {
81 @Override
82 public void actionPerformed(ActionEvent e) {
83 HelpBrowser.getInstance().setVisible(true);
84 }
85 };
86
87 /**
88 * Constructs a new {@code HelpBrowser}.
89 */
90 public HelpBrowser() {
91 reader = new HelpContentReader(HelpUtil.getWikiBaseUrl());
92 build();
93 }
94
95 /**
96 * Replies the unique instance of the help browser
97 *
98 * @return the unique instance of the help browser
99 */
100 public static synchronized HelpBrowser getInstance() {
101 if (instance == null) {
102 instance = new HelpBrowser();
103 }
104 return instance;
105 }
106
107 /**
108 * Show the help page for help topic <code>helpTopic</code>.
109 *
110 * @param helpTopic the help topic
111 */
112 public static void setUrlForHelpTopic(final String helpTopic) {
113 final HelpBrowser browser = getInstance();
114 SwingUtilities.invokeLater(() -> {
115 browser.openHelpTopic(helpTopic);
116 browser.setVisible(true);
117 browser.toFront();
118 });
119 }
120
121 /**
122 * Launches the internal help browser and directs it to the help page for
123 * <code>helpTopic</code>.
124 *
125 * @param helpTopic the help topic
126 */
127 public static void launchBrowser(String helpTopic) {
128 HelpBrowser browser = getInstance();
129 browser.openHelpTopic(helpTopic);
130 browser.setVisible(true);
131 browser.toFront();
132 }
133
134 /**
135 * Builds the style sheet used in the internal help browser
136 *
137 * @return the style sheet
138 */
139 protected StyleSheet buildStyleSheet() {
140 StyleSheet ss = new StyleSheet();
141 final String css;
142 try (CachedFile cf = new CachedFile("resource://data/help-browser.css")) {
143 css = new String(cf.getByteContent(), StandardCharsets.ISO_8859_1);
144 } catch (IOException e) {
145 Logging.error(tr("Failed to read CSS file ''help-browser.css''. Exception is: {0}", e.toString()));
146 Logging.error(e);
147 return ss;
148 }
149 ss.addRule(css);
150 return ss;
151 }
152
153 /**
154 * Builds toolbar.
155 * @return the toolbar
156 */
157 protected JToolBar buildToolBar() {
158 JToolBar tb = new JToolBar();
159 tb.add(new JButton(new HomeAction(this)));
160 tb.add(new JButton(new BackAction(this)));
161 tb.add(new JButton(new ForwardAction(this)));
162 tb.add(new JButton(new ReloadAction(this)));
163 tb.add(new JSeparator());
164 tb.add(new JButton(new OpenInBrowserAction(this)));
165 tb.add(new JButton(new EditAction(this)));
166 return tb;
167 }
168
169 /**
170 * Builds GUI.
171 */
172 protected final void build() {
173 help = new JosmEditorPane();
174 JosmHTMLEditorKit kit = new JosmHTMLEditorKit();
175 kit.setStyleSheet(buildStyleSheet());
176 help.setEditorKit(kit);
177 help.setEditable(false);
178 help.addHyperlinkListener(new HyperlinkHandler());
179 help.setContentType("text/html");
180 history = new HelpBrowserHistory(this);
181
182 JPanel p = new JPanel(new BorderLayout());
183 setContentPane(p);
184
185 p.add(new JScrollPane(help), BorderLayout.CENTER);
186
187 addWindowListener(new WindowAdapter() {
188 @Override public void windowClosing(WindowEvent e) {
189 setVisible(false);
190 }
191 });
192
193 p.add(buildToolBar(), BorderLayout.NORTH);
194 InputMapUtils.addEscapeAction(getRootPane(), new AbstractAction() {
195 @Override
196 public void actionPerformed(ActionEvent e) {
197 setVisible(false);
198 }
199 });
200
201 setMinimumSize(new Dimension(400, 200));
202 setTitle(tr("JOSM Help Browser"));
203 }
204
205 @Override
206 public void setVisible(boolean visible) {
207 if (visible) {
208 new WindowGeometry(
209 getClass().getName() + ".geometry",
210 WindowGeometry.centerInWindow(
211 getParent(),
212 new Dimension(600, 400)
213 )
214 ).applySafe(this);
215 } else if (isShowing()) { // Avoid IllegalComponentStateException like in #8775
216 new WindowGeometry(this).remember(getClass().getName() + ".geometry");
217 }
218 if (Main.main != null && Main.main.menu != null && Main.main.menu.windowMenu != null) {
219 if (windowMenuItem != null && !visible) {
220 Main.main.menu.windowMenu.remove(windowMenuItem);
221 windowMenuItem = null;
222 }
223 if (windowMenuItem == null && visible) {
224 windowMenuItem = MainMenu.add(Main.main.menu.windowMenu, FOCUS_ACTION, MainMenu.WINDOW_MENU_GROUP.VOLATILE);
225 }
226 }
227 super.setVisible(visible);
228 }
229
230 /**
231 * Load help topic.
232 * @param content topic contents
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 Logging.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 Logging.trace(e);
304 url = getHelpTopicUrl(buildAbsoluteHelpTopic(relativeHelpTopic, LocaleType.BASELANGUAGE));
305 try {
306 content = reader.fetchHelpTopicContent(url, true);
307 } catch (MissingHelpContentException e1) {
308 Logging.trace(e1);
309 url = getHelpTopicUrl(buildAbsoluteHelpTopic(relativeHelpTopic, LocaleType.ENGLISH));
310 try {
311 content = reader.fetchHelpTopicContent(url, true);
312 } catch (MissingHelpContentException e2) {
313 Logging.debug(e2);
314 this.url = url;
315 handleMissingHelpContent(relativeHelpTopic);
316 return;
317 } catch (HelpContentReaderException e2) {
318 Logging.error(e2);
319 handleHelpContentReaderException(relativeHelpTopic, e2);
320 return;
321 }
322 } catch (HelpContentReaderException e1) {
323 Logging.error(e1);
324 handleHelpContentReaderException(relativeHelpTopic, e1);
325 return;
326 }
327 } catch (HelpContentReaderException e) {
328 Logging.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 Logging.debug(e);
350 this.url = url;
351 handleMissingHelpContent(absoluteHelpTopic);
352 return;
353 } catch (HelpContentReaderException e) {
354 Logging.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 Logging.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 // Deprecated API to replace only when migrating to Java 9 (replacement not available in Java 8)
581 @SuppressWarnings("deprecation")
582 Rectangle r = help.modelToView(element.getStartOffset());
583 if (r != null) {
584 Rectangle vis = help.getVisibleRect();
585 r.height = vis.height;
586 help.scrollRectToVisible(r);
587 return true;
588 }
589 } catch (BadLocationException e) {
590 Logging.warn(tr("Bad location in HTML document. Exception was: {0}", e.toString()));
591 Logging.error(e);
592 }
593 }
594 return false;
595 }
596
597 /**
598 * Checks whether the hyperlink event originated on a &lt;a ...&gt; element with
599 * a relative href consisting of a URL fragment only, i.e.
600 * &lt;a href="#thisIsALocalFragment"&gt;. If so, replies the fragment, i.e. "thisIsALocalFragment".
601 *
602 * Otherwise, replies <code>null</code>
603 *
604 * @param e the hyperlink event
605 * @return the local fragment or <code>null</code>
606 */
607 protected String getUrlFragment(HyperlinkEvent e) {
608 AttributeSet set = e.getSourceElement().getAttributes();
609 Object value = set.getAttribute(Tag.A);
610 if (!(value instanceof SimpleAttributeSet))
611 return null;
612 SimpleAttributeSet atts = (SimpleAttributeSet) value;
613 value = atts.getAttribute(javax.swing.text.html.HTML.Attribute.HREF);
614 if (value == null)
615 return null;
616 String s = (String) value;
617 if (s.matches("#.*"))
618 return s.substring(1);
619 return null;
620 }
621
622 @Override
623 public void hyperlinkUpdate(HyperlinkEvent e) {
624 if (e.getEventType() != HyperlinkEvent.EventType.ACTIVATED)
625 return;
626 if (e.getURL() == null || e.getURL().toExternalForm().startsWith(url+'#')) {
627 // Probably hyperlink event on a an A-element with a href consisting of a fragment only, i.e. "#ALocalFragment".
628 String fragment = getUrlFragment(e);
629 if (fragment != null) {
630 // first try to scroll to an element with id==fragment. This is the way
631 // table of contents are built in the JOSM wiki. If this fails, try to
632 // scroll to a <A name="..."> element.
633 //
634 if (!scrollToElementWithId(fragment)) {
635 help.scrollToReference(fragment);
636 }
637 } else {
638 HelpAwareOptionPane.showOptionDialog(
639 Main.parent,
640 tr("Failed to open help page. The target URL is empty."),
641 tr("Failed to open help page"),
642 JOptionPane.ERROR_MESSAGE,
643 null, /* no icon */
644 null, /* standard options, just OK button */
645 null, /* default is standard */
646 null /* no help context */
647 );
648 }
649 } else if (e.getURL().toExternalForm().endsWith("action=edit")) {
650 OpenBrowser.displayUrl(e.getURL().toExternalForm());
651 } else {
652 url = e.getURL().toExternalForm();
653 openUrl(url);
654 }
655 }
656 }
657
658 @Override
659 public HelpBrowserHistory getHistory() {
660 return history;
661 }
662}
Note: See TracBrowser for help on using the repository browser.