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

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

see #8902 - Small performance enhancements / coding style (patch by shinigami):

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