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

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

PMD - VariableNamingConventions

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