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

Last change on this file since 5915 was 5915, checked in by stoecker, 11 years ago

use 3 step wiki loading fallback, cleanup handling of language fallbacks

  • Property svn:eol-style set to native
File size: 22.5 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.HTMLEditorKit;
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.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 public void run() {
87 browser.openHelpTopic(helpTopic);
88 browser.setVisible(true);
89 browser.toFront();
90 }
91 };
92 SwingUtilities.invokeLater(r);
93 }
94
95 /**
96 * Launches the internal help browser and directs it to the help page for
97 * <code>helpTopic</code>.
98 *
99 * @param helpTopic the help topic
100 */
101 static public void launchBrowser(String helpTopic) {
102 HelpBrowser browser = getInstance();
103 browser.openHelpTopic(helpTopic);
104 browser.setVisible(true);
105 browser.toFront();
106 }
107
108 /** the help browser */
109 private JosmEditorPane help;
110
111 /** the help browser history */
112 private HelpBrowserHistory history;
113
114 /** the currently displayed URL */
115 private String url;
116
117 private HelpContentReader reader;
118
119 private static final JosmAction focusAction = new JosmAction(tr("JOSM Help Browser"), "help", "", null, false, false) {
120 @Override
121 public void actionPerformed(ActionEvent e) {
122 HelpBrowser.getInstance().setVisible(true);
123 }
124 };
125
126 /**
127 * Builds the style sheet used in the internal help browser
128 *
129 * @return the style sheet
130 */
131 protected StyleSheet buildStyleSheet() {
132 StyleSheet ss = new StyleSheet();
133 BufferedReader reader = new BufferedReader(
134 new InputStreamReader(
135 getClass().getResourceAsStream("/data/help-browser.css")
136 )
137 );
138 StringBuffer css = new StringBuffer();
139 try {
140 String line = null;
141 while ((line = reader.readLine()) != null) {
142 css.append(line);
143 css.append("\n");
144 }
145 } catch(Exception e) {
146 System.err.println(tr("Failed to read CSS file ''help-browser.css''. Exception is: {0}", e.toString()));
147 e.printStackTrace();
148 return ss;
149 } finally {
150 Utils.close(reader);
151 }
152 ss.addRule(css.toString());
153 return ss;
154 }
155
156 protected JToolBar buildToolBar() {
157 JToolBar tb = new JToolBar();
158 tb.add(new JButton(new HomeAction()));
159 tb.add(new JButton(new BackAction(history)));
160 tb.add(new JButton(new ForwardAction(history)));
161 tb.add(new JButton(new ReloadAction()));
162 tb.add(new JSeparator());
163 tb.add(new JButton(new OpenInBrowserAction()));
164 tb.add(new JButton(new EditAction()));
165 return tb;
166 }
167
168 protected void build() {
169 help = new JosmEditorPane();
170 HTMLEditorKit kit = new HTMLEditorKit();
171 kit.setStyleSheet(buildStyleSheet());
172 help.setEditorKit(kit);
173 help.setEditable(false);
174 help.addHyperlinkListener(new HyperlinkHandler());
175 help.setContentType("text/html");
176 history = new HelpBrowserHistory(this);
177
178 JPanel p = new JPanel(new BorderLayout());
179 setContentPane(p);
180
181 p.add(new JScrollPane(help), BorderLayout.CENTER);
182
183 addWindowListener(new WindowAdapter(){
184 @Override public void windowClosing(WindowEvent e) {
185 setVisible(false);
186 }
187 });
188
189 p.add(buildToolBar(), BorderLayout.NORTH);
190 help.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "Close");
191 help.getActionMap().put("Close", new AbstractAction(){
192 public void actionPerformed(ActionEvent e) {
193 setVisible(false);
194 }
195 });
196
197 setMinimumSize(new Dimension(400, 200));
198 setTitle(tr("JOSM Help Browser"));
199 }
200
201 @Override
202 public void setVisible(boolean visible) {
203 if (visible) {
204 new WindowGeometry(
205 getClass().getName() + ".geometry",
206 WindowGeometry.centerInWindow(
207 getParent(),
208 new Dimension(600,400)
209 )
210 ).applySafe(this);
211 } else if (!visible && isShowing()){
212 new WindowGeometry(this).remember(getClass().getName() + ".geometry");
213 }
214 if(windowMenuItem != null && !visible) {
215 Main.main.menu.windowMenu.remove(windowMenuItem);
216 windowMenuItem = null;
217 }
218 if(windowMenuItem == null && visible) {
219 windowMenuItem = MainMenu.add(Main.main.menu.windowMenu, focusAction, MainMenu.WINDOW_MENU_GROUP.VOLATILE);
220 }
221 super.setVisible(visible);
222 }
223
224 public HelpBrowser() {
225 reader = new HelpContentReader(HelpUtil.getWikiBaseUrl());
226 build();
227 }
228
229 protected void loadTopic(String content) {
230 Document document = help.getEditorKit().createDefaultDocument();
231 try {
232 help.getEditorKit().read(new StringReader(content), document, 0);
233 } catch (Exception e) {
234 e.printStackTrace();
235 }
236 help.setDocument(document);
237 }
238
239 /**
240 * Replies the current URL
241 *
242 * @return the current URL
243 */
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 = HelpUtil.getHelpTopicUrl(HelpUtil.buildAbsoluteHelpTopic(relativeHelpTopic, LocaleType.DEFAULTNOTENGLISH));
298 String content = null;
299 try {
300 content = reader.fetchHelpTopicContent(url, true);
301 } catch(MissingHelpContentException e) {
302 url = HelpUtil.getHelpTopicUrl(HelpUtil.buildAbsoluteHelpTopic(relativeHelpTopic, LocaleType.BASELANGUAGE));
303 try {
304 content = reader.fetchHelpTopicContent(url, true);
305 } catch(MissingHelpContentException e1) {
306 url = HelpUtil.getHelpTopicUrl(HelpUtil.buildAbsoluteHelpTopic(relativeHelpTopic, LocaleType.ENGLISH));
307 try {
308 content = reader.fetchHelpTopicContent(url, true);
309 } catch(MissingHelpContentException e2) {
310 this.url = url;
311 handleMissingHelpContent(relativeHelpTopic);
312 return;
313 } catch(HelpContentReaderException e2) {
314 e2.printStackTrace();
315 handleHelpContentReaderException(relativeHelpTopic, e2);
316 return;
317 }
318 } catch(HelpContentReaderException e1) {
319 e1.printStackTrace();
320 handleHelpContentReaderException(relativeHelpTopic, e1);
321 return;
322 }
323 } catch(HelpContentReaderException e) {
324 e.printStackTrace();
325 handleHelpContentReaderException(relativeHelpTopic, e);
326 return;
327 }
328 loadTopic(content);
329 history.setCurrentUrl(url);
330 this.url = url;
331 }
332
333 /**
334 * Loads a help topic given by an absolute help topic name, i.e.
335 * "/De:Help/Action/New"
336 *
337 * @param absoluteHelpTopic the absolute help topic name
338 */
339 protected void loadAbsoluteHelpTopic(String absoluteHelpTopic) {
340 String url = HelpUtil.getHelpTopicUrl(absoluteHelpTopic);
341 String content = null;
342 try {
343 content = reader.fetchHelpTopicContent(url, true);
344 } catch(MissingHelpContentException e) {
345 this.url = url;
346 handleMissingHelpContent(absoluteHelpTopic);
347 return;
348 } catch(HelpContentReaderException e) {
349 e.printStackTrace();
350 handleHelpContentReaderException(absoluteHelpTopic, e);
351 return;
352 }
353 loadTopic(content);
354 history.setCurrentUrl(url);
355 this.url = url;
356 }
357
358 /**
359 * Opens an URL and displays the content.
360 *
361 * If the URL is the locator of an absolute help topic, help content is loaded from
362 * the JOSM wiki. Otherwise, the help browser loads the page from the given URL
363 *
364 * @param url the url
365 */
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(Exception e) {
382 HelpAwareOptionPane.showOptionDialog(
383 Main.parent,
384 tr(
385 "<html>Failed to open help page for url {0}.<br>"
386 + "This is most likely due to a network problem, please check<br>"
387 + "your internet connection</html>",
388 url.toString()
389 ),
390 tr("Failed to open URL"),
391 JOptionPane.ERROR_MESSAGE,
392 null, /* no icon */
393 null, /* standard options, just OK button */
394 null, /* default is standard */
395 null /* no help context */
396 );
397 }
398 history.setCurrentUrl(url);
399 } else {
400 loadAbsoluteHelpTopic(helpTopic);
401 }
402 }
403
404 /**
405 * Loads and displays the help information for a help topic given
406 * by a relative help topic name, i.e. "/Action/New"
407 *
408 * @param relativeHelpTopic the relative help topic
409 */
410 public void openHelpTopic(String relativeHelpTopic) {
411 if (!isVisible()) {
412 setVisible(true);
413 toFront();
414 } else {
415 toFront();
416 }
417 loadRelativeHelpTopic(relativeHelpTopic);
418 }
419
420 class OpenInBrowserAction extends AbstractAction {
421 public OpenInBrowserAction() {
422 //putValue(NAME, tr("Open in Browser"));
423 putValue(SHORT_DESCRIPTION, tr("Open the current help page in an external browser"));
424 putValue(SMALL_ICON, ImageProvider.get("help", "internet"));
425 }
426
427 public void actionPerformed(ActionEvent e) {
428 OpenBrowser.displayUrl(getUrl());
429 }
430 }
431
432 class EditAction extends AbstractAction {
433 public EditAction() {
434 // putValue(NAME, tr("Edit"));
435 putValue(SHORT_DESCRIPTION, tr("Edit the current help page"));
436 putValue(SMALL_ICON,ImageProvider.get("dialogs", "edit"));
437 }
438
439 public void actionPerformed(ActionEvent e) {
440 String url = getUrl();
441 if(url == null)
442 return;
443 if (!url.startsWith(HelpUtil.getWikiBaseHelpUrl())) {
444 String message = tr(
445 "<html>The current URL <tt>{0}</tt><br>"
446 + "is an external URL. Editing is only possible for help topics<br>"
447 + "on the help server <tt>{1}</tt>.</html>",
448 getUrl(),
449 HelpUtil.getWikiBaseUrl()
450 );
451 JOptionPane.showMessageDialog(
452 Main.parent,
453 message,
454 tr("Warning"),
455 JOptionPane.WARNING_MESSAGE
456 );
457 return;
458 }
459 url = url.replaceAll("#[^#]*$", "");
460 OpenBrowser.displayUrl(url+"?action=edit");
461 }
462 }
463
464 class ReloadAction extends AbstractAction {
465 public ReloadAction() {
466 //putValue(NAME, tr("Reload"));
467 putValue(SHORT_DESCRIPTION, tr("Reload the current help page"));
468 putValue(SMALL_ICON, ImageProvider.get("dialogs", "refresh"));
469 }
470
471 public void actionPerformed(ActionEvent e) {
472 openUrl(getUrl());
473 }
474 }
475
476 static class BackAction extends AbstractAction implements Observer {
477 private HelpBrowserHistory history;
478 public BackAction(HelpBrowserHistory history) {
479 this.history = history;
480 history.addObserver(this);
481 //putValue(NAME, tr("Back"));
482 putValue(SHORT_DESCRIPTION, tr("Go to the previous page"));
483 putValue(SMALL_ICON, ImageProvider.get("help", "previous"));
484 setEnabled(history.canGoBack());
485 }
486
487 public void actionPerformed(ActionEvent e) {
488 history.back();
489 }
490 public void update(Observable o, Object arg) {
491 //System.out.println("BackAction: canGoBoack=" + history.canGoBack() );
492 setEnabled(history.canGoBack());
493 }
494 }
495
496 static class ForwardAction extends AbstractAction implements Observer {
497 private HelpBrowserHistory history;
498 public ForwardAction(HelpBrowserHistory history) {
499 this.history = history;
500 history.addObserver(this);
501 //putValue(NAME, tr("Forward"));
502 putValue(SHORT_DESCRIPTION, tr("Go to the next page"));
503 putValue(SMALL_ICON, ImageProvider.get("help", "next"));
504 setEnabled(history.canGoForward());
505 }
506
507 public void actionPerformed(ActionEvent e) {
508 history.forward();
509 }
510 public void update(Observable o, Object arg) {
511 setEnabled(history.canGoForward());
512 }
513 }
514
515 class HomeAction extends AbstractAction {
516 public HomeAction() {
517 //putValue(NAME, tr("Home"));
518 putValue(SHORT_DESCRIPTION, tr("Go to the JOSM help home page"));
519 putValue(SMALL_ICON, ImageProvider.get("help", "home"));
520 }
521
522 public void actionPerformed(ActionEvent e) {
523 openHelpTopic("/");
524 }
525 }
526
527 class HyperlinkHandler implements HyperlinkListener {
528
529 /**
530 * Scrolls the help browser to the element with id <code>id</code>
531 *
532 * @param id the id
533 * @return true, if an element with this id was found and scrolling was successful; false, otherwise
534 */
535 protected boolean scrollToElementWithId(String id) {
536 Document d = help.getDocument();
537 if (d instanceof HTMLDocument) {
538 HTMLDocument doc = (HTMLDocument) d;
539 Element element = doc.getElement(id);
540 try {
541 Rectangle r = help.modelToView(element.getStartOffset());
542 if (r != null) {
543 Rectangle vis = help.getVisibleRect();
544 r.height = vis.height;
545 help.scrollRectToVisible(r);
546 return true;
547 }
548 } catch(BadLocationException e) {
549 System.err.println(tr("Warning: bad location in HTML document. Exception was: {0}", e.toString()));
550 e.printStackTrace();
551 }
552 }
553 return false;
554 }
555
556 /**
557 * Checks whether the hyperlink event originated on a &lt;a ...&gt; element with
558 * a relative href consisting of a URL fragment only, i.e.
559 * &lt;a href="#thisIsALocalFragment"&gt;. If so, replies the fragment, i.e.
560 * "thisIsALocalFragment".
561 *
562 * Otherwise, replies <code>null</code>
563 *
564 * @param e the hyperlink event
565 * @return the local fragment or <code>null</code>
566 */
567 protected String getUrlFragment(HyperlinkEvent e) {
568 AttributeSet set = e.getSourceElement().getAttributes();
569 Object value = set.getAttribute(Tag.A);
570 if (value == null || ! (value instanceof SimpleAttributeSet)) return null;
571 SimpleAttributeSet atts = (SimpleAttributeSet)value;
572 value = atts.getAttribute(javax.swing.text.html.HTML.Attribute.HREF);
573 if (value == null) return null;
574 String s = (String)value;
575 if (s.matches("#.*"))
576 return s.substring(1);
577 return null;
578 }
579
580 public void hyperlinkUpdate(HyperlinkEvent e) {
581 if (e.getEventType() != HyperlinkEvent.EventType.ACTIVATED)
582 return;
583 if (e.getURL() == null) {
584 // Probably hyperlink event on a an A-element with a href consisting of
585 // a fragment only, i.e. "#ALocalFragment".
586 //
587 String fragment = getUrlFragment(e);
588 if (fragment != null) {
589 // first try to scroll to an element with id==fragment. This is the way
590 // table of contents are built in the JOSM wiki. If this fails, try to
591 // scroll to a <A name="..."> element.
592 //
593 if (!scrollToElementWithId(fragment)) {
594 help.scrollToReference(fragment);
595 }
596 } else {
597 HelpAwareOptionPane.showOptionDialog(
598 Main.parent,
599 tr("Failed to open help page. The target URL is empty."),
600 tr("Failed to open help page"),
601 JOptionPane.ERROR_MESSAGE,
602 null, /* no icon */
603 null, /* standard options, just OK button */
604 null, /* default is standard */
605 null /* no help context */
606 );
607 }
608 } else if (e.getURL().toString().endsWith("action=edit")) {
609 OpenBrowser.displayUrl(e.getURL().toString());
610 } else {
611 url = e.getURL().toString();
612 openUrl(e.getURL().toString());
613 }
614 }
615 }
616}
Note: See TracBrowser for help on using the repository browser.