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

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

fix #15572 - use ImageProvider attach API for all JOSM actions to ensure proper icon size everywhere

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