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

Last change on this file since 11044 was 11044, checked in by simon04, 8 years ago

fix #13687 - Use CachedFile class for /data/help-browser.css in HelpBrowser class (patch by sebastic, modified)

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