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

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

sonar - Immutable Field

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