001 // License: GPL. For details, see LICENSE file.
002 package org.openstreetmap.josm.gui.help;
003
004 import static org.openstreetmap.josm.gui.help.HelpUtil.buildAbsoluteHelpTopic;
005 import static org.openstreetmap.josm.gui.help.HelpUtil.getHelpTopicEditUrl;
006 import static org.openstreetmap.josm.tools.I18n.tr;
007
008 import java.awt.BorderLayout;
009 import java.awt.Dimension;
010 import java.awt.Rectangle;
011 import java.awt.event.ActionEvent;
012 import java.awt.event.KeyEvent;
013 import java.awt.event.WindowAdapter;
014 import java.awt.event.WindowEvent;
015 import java.io.BufferedReader;
016 import java.io.InputStreamReader;
017 import java.io.StringReader;
018 import java.util.Locale;
019 import java.util.Observable;
020 import java.util.Observer;
021
022 import javax.swing.AbstractAction;
023 import javax.swing.JButton;
024 import javax.swing.JComponent;
025 import javax.swing.JDialog;
026 import javax.swing.JEditorPane;
027 import javax.swing.JMenuItem;
028 import javax.swing.JOptionPane;
029 import javax.swing.JPanel;
030 import javax.swing.JScrollPane;
031 import javax.swing.JSeparator;
032 import javax.swing.JToolBar;
033 import javax.swing.KeyStroke;
034 import javax.swing.SwingUtilities;
035 import javax.swing.event.HyperlinkEvent;
036 import javax.swing.event.HyperlinkListener;
037 import javax.swing.text.AttributeSet;
038 import javax.swing.text.BadLocationException;
039 import javax.swing.text.Document;
040 import javax.swing.text.Element;
041 import javax.swing.text.SimpleAttributeSet;
042 import javax.swing.text.html.HTMLDocument;
043 import javax.swing.text.html.HTMLEditorKit;
044 import javax.swing.text.html.StyleSheet;
045 import javax.swing.text.html.HTML.Tag;
046
047 import org.openstreetmap.josm.Main;
048 import org.openstreetmap.josm.actions.JosmAction;
049 import org.openstreetmap.josm.gui.HelpAwareOptionPane;
050 import org.openstreetmap.josm.gui.MainMenu;
051 import org.openstreetmap.josm.tools.ImageProvider;
052 import org.openstreetmap.josm.tools.OpenBrowser;
053 import org.openstreetmap.josm.tools.WindowGeometry;
054
055 public class HelpBrowser extends JDialog {
056 /** the unique instance */
057 private static HelpBrowser instance;
058
059 /** the menu item in the windows menu. Required to properly
060 * hide on dialog close.
061 */
062 private JMenuItem windowMenuItem;
063
064 /**
065 * Replies the unique instance of the help browser
066 *
067 * @return the unique instance of the help browser
068 */
069 static public HelpBrowser getInstance() {
070 if (instance == null) {
071 instance = new HelpBrowser();
072 }
073 return instance;
074 }
075
076 /**
077 * Show the help page for help topic <code>helpTopic</code>.
078 *
079 * @param helpTopic the help topic
080 */
081 public static void setUrlForHelpTopic(final String helpTopic) {
082 final HelpBrowser browser = getInstance();
083 Runnable r = new Runnable() {
084 public void run() {
085 browser.openHelpTopic(helpTopic);
086 browser.setVisible(true);
087 browser.toFront();
088 }
089 };
090 SwingUtilities.invokeLater(r);
091 }
092
093 /**
094 * Launches the internal help browser and directs it to the help page for
095 * <code>helpTopic</code>.
096 *
097 * @param helpTopic the help topic
098 */
099 static public void launchBrowser(String helpTopic) {
100 HelpBrowser browser = getInstance();
101 browser.openHelpTopic(helpTopic);
102 browser.setVisible(true);
103 browser.toFront();
104 }
105
106 /** the help browser */
107 private JEditorPane help;
108
109 /** the help browser history */
110 private HelpBrowserHistory history;
111
112 /** the currently displayed URL */
113 private String url;
114
115 private HelpContentReader reader;
116
117 private static final JosmAction focusAction = new JosmAction(tr("JOSM Help Browser"), "help", "", null, false, false) {
118 @Override
119 public void actionPerformed(ActionEvent e) {
120 HelpBrowser.getInstance().setVisible(true);
121 }
122 };
123
124 /**
125 * Builds the style sheet used in the internal help browser
126 *
127 * @return the style sheet
128 */
129 protected StyleSheet buildStyleSheet() {
130 StyleSheet ss = new StyleSheet();
131 BufferedReader reader = new BufferedReader(
132 new InputStreamReader(
133 getClass().getResourceAsStream("/data/help-browser.css")
134 )
135 );
136 StringBuffer css = new StringBuffer();
137 try {
138 String line = null;
139 while ((line = reader.readLine()) != null) {
140 css.append(line);
141 css.append("\n");
142 }
143 reader.close();
144 } catch(Exception e) {
145 System.err.println(tr("Failed to read CSS file ''help-browser.css''. Exception is: {0}", e.toString()));
146 e.printStackTrace();
147 return ss;
148 }
149 ss.addRule(css.toString());
150 return ss;
151 }
152
153 protected JToolBar buildToolBar() {
154 JToolBar tb = new JToolBar();
155 tb.add(new JButton(new HomeAction()));
156 tb.add(new JButton(new BackAction(history)));
157 tb.add(new JButton(new ForwardAction(history)));
158 tb.add(new JButton(new ReloadAction()));
159 tb.add(new JSeparator());
160 tb.add(new JButton(new OpenInBrowserAction()));
161 tb.add(new JButton(new EditAction()));
162 return tb;
163 }
164
165 protected void build() {
166 help = new JEditorPane();
167 HTMLEditorKit kit = new HTMLEditorKit();
168 kit.setStyleSheet(buildStyleSheet());
169 help.setEditorKit(kit);
170 help.setEditable(false);
171 help.addHyperlinkListener(new HyperlinkHandler());
172 help.setContentType("text/html");
173 history = new HelpBrowserHistory(this);
174
175 JPanel p = new JPanel(new BorderLayout());
176 setContentPane(p);
177
178 p.add(new JScrollPane(help), BorderLayout.CENTER);
179
180 addWindowListener(new WindowAdapter(){
181 @Override public void windowClosing(WindowEvent e) {
182 setVisible(false);
183 }
184 });
185
186 p.add(buildToolBar(), BorderLayout.NORTH);
187 help.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "Close");
188 help.getActionMap().put("Close", new AbstractAction(){
189 public void actionPerformed(ActionEvent e) {
190 setVisible(false);
191 }
192 });
193
194 setMinimumSize(new Dimension(400, 200));
195 setTitle(tr("JOSM Help Browser"));
196 }
197
198 @Override
199 public void setVisible(boolean visible) {
200 if (visible) {
201 new WindowGeometry(
202 getClass().getName() + ".geometry",
203 WindowGeometry.centerInWindow(
204 getParent(),
205 new Dimension(600,400)
206 )
207 ).applySafe(this);
208 } else if (!visible && isShowing()){
209 new WindowGeometry(this).remember(getClass().getName() + ".geometry");
210 }
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 super.setVisible(visible);
219 }
220
221 public HelpBrowser() {
222 reader = new HelpContentReader(HelpUtil.getWikiBaseUrl());
223 build();
224 }
225
226 protected void loadTopic(String content) {
227 Document document = help.getEditorKit().createDefaultDocument();
228 try {
229 help.getEditorKit().read(new StringReader(content), document, 0);
230 } catch (Exception e) {
231 e.printStackTrace();
232 }
233 help.setDocument(document);
234 }
235
236 /**
237 * Replies the current URL
238 *
239 * @return the current URL
240 */
241
242 public String getUrl() {
243 return url;
244 }
245
246 /**
247 * Displays a warning page when a help topic doesn't exist yet.
248 *
249 * @param relativeHelpTopic the help topic
250 */
251 protected void handleMissingHelpContent(String relativeHelpTopic) {
252 // i18n: do not translate "warning-header" and "warning-body"
253 String message = tr("<html><p class=\"warning-header\">Help content for help topic missing</p>"
254 + "<p class=\"warning-body\">Help content for the help topic <strong>{0}</strong> is "
255 + "not available yet. It is missing both in your local language ({1}) and in English.<br><br>"
256 + "Please help to improve the JOSM help system and fill in the missing information. "
257 + "You can both edit the <a href=\"{2}\">help topic in your local language ({1})</a> and "
258 + "the <a href=\"{3}\">help topic in English</a>."
259 + "</p></html>",
260 relativeHelpTopic,
261 Locale.getDefault().getDisplayName(),
262 getHelpTopicEditUrl(buildAbsoluteHelpTopic(relativeHelpTopic)),
263 getHelpTopicEditUrl(buildAbsoluteHelpTopic(relativeHelpTopic, Locale.ENGLISH))
264 );
265 loadTopic(message);
266 }
267
268 /**
269 * Displays a error page if a help topic couldn't be loaded because of network or IO error.
270 *
271 * @param relativeHelpTopic the help topic
272 * @param e the exception
273 */
274 protected void handleHelpContentReaderException(String relativeHelpTopic, HelpContentReaderException e) {
275 String message = tr("<html><p class=\"error-header\">Error when retrieving help information</p>"
276 + "<p class=\"error-body\">The content for the help topic <strong>{0}</strong> could "
277 + "not be loaded. The error message is (untranslated):<br>"
278 + "<tt>{1}</tt>"
279 + "</p></html>",
280 relativeHelpTopic,
281 e.toString()
282 );
283 loadTopic(message);
284 }
285
286 /**
287 * Loads a help topic given by a relative help topic name (i.e. "/Action/New")
288 *
289 * First tries to load the language specific help topic. If it is missing, tries to
290 * load the topic in English.
291 *
292 * @param relativeHelpTopic the relative help topic
293 */
294 protected void loadRelativeHelpTopic(String relativeHelpTopic) {
295 String url = HelpUtil.getHelpTopicUrl(HelpUtil.buildAbsoluteHelpTopic(relativeHelpTopic));
296 String content = null;
297 try {
298 content = reader.fetchHelpTopicContent(url, true);
299 } catch(MissingHelpContentException e) {
300 url = HelpUtil.getHelpTopicUrl(HelpUtil.buildAbsoluteHelpTopic(relativeHelpTopic, Locale.ENGLISH));
301 try {
302 content = reader.fetchHelpTopicContent(url, true);
303 } catch(MissingHelpContentException e1) {
304 this.url = url;
305 handleMissingHelpContent(relativeHelpTopic);
306 return;
307 } catch(HelpContentReaderException e1) {
308 e1.printStackTrace();
309 handleHelpContentReaderException(relativeHelpTopic,e1);
310 return;
311 }
312 } catch(HelpContentReaderException e) {
313 e.printStackTrace();
314 handleHelpContentReaderException(relativeHelpTopic, e);
315 return;
316 }
317 loadTopic(content);
318 history.setCurrentUrl(url);
319 this.url = url;
320 }
321
322 /**
323 * Loads a help topic given by an absolute help topic name, i.e.
324 * "/De:Help/Action/New"
325 *
326 * @param absoluteHelpTopic the absolute help topic name
327 */
328 protected void loadAbsoluteHelpTopic(String absoluteHelpTopic) {
329 String url = HelpUtil.getHelpTopicUrl(absoluteHelpTopic);
330 String content = null;
331 try {
332 content = reader.fetchHelpTopicContent(url, true);
333 } catch(MissingHelpContentException e) {
334 this.url = url;
335 handleMissingHelpContent(absoluteHelpTopic);
336 return;
337 } catch(HelpContentReaderException e) {
338 e.printStackTrace();
339 handleHelpContentReaderException(absoluteHelpTopic, e);
340 return;
341 }
342 loadTopic(content);
343 history.setCurrentUrl(url);
344 this.url = url;
345 }
346
347 /**
348 * Opens an URL and displays the content.
349 *
350 * If the URL is the locator of an absolute help topic, help content is loaded from
351 * the JOSM wiki. Otherwise, the help browser loads the page from the given URL
352 *
353 * @param url the url
354 */
355 public void openUrl(String url) {
356 if (!isVisible()) {
357 setVisible(true);
358 toFront();
359 } else {
360 toFront();
361 }
362 String helpTopic = HelpUtil.extractAbsoluteHelpTopic(url);
363 if (helpTopic == null) {
364 try {
365 this.url = url;
366 String content = reader.fetchHelpTopicContent(url, false);
367 loadTopic(content);
368 history.setCurrentUrl(url);
369 this.url = url;
370 } catch(Exception e) {
371 HelpAwareOptionPane.showOptionDialog(
372 Main.parent,
373 tr(
374 "<html>Failed to open help page for url {0}.<br>"
375 + "This is most likely due to a network problem, please check<br>"
376 + "your internet connection</html>",
377 url.toString()
378 ),
379 tr("Failed to open URL"),
380 JOptionPane.ERROR_MESSAGE,
381 null, /* no icon */
382 null, /* standard options, just OK button */
383 null, /* default is standard */
384 null /* no help context */
385 );
386 }
387 history.setCurrentUrl(url);
388 } else {
389 loadAbsoluteHelpTopic(helpTopic);
390 }
391 }
392
393 /**
394 * Loads and displays the help information for a help topic given
395 * by a relative help topic name, i.e. "/Action/New"
396 *
397 * @param relativeHelpTopic the relative help topic
398 */
399 public void openHelpTopic(String relativeHelpTopic) {
400 if (!isVisible()) {
401 setVisible(true);
402 toFront();
403 } else {
404 toFront();
405 }
406 loadRelativeHelpTopic(relativeHelpTopic);
407 }
408
409 class OpenInBrowserAction extends AbstractAction {
410 public OpenInBrowserAction() {
411 //putValue(NAME, tr("Open in Browser"));
412 putValue(SHORT_DESCRIPTION, tr("Open the current help page in an external browser"));
413 putValue(SMALL_ICON, ImageProvider.get("help", "internet"));
414 }
415
416 public void actionPerformed(ActionEvent e) {
417 OpenBrowser.displayUrl(getUrl());
418 }
419 }
420
421 class EditAction extends AbstractAction {
422 public EditAction() {
423 // putValue(NAME, tr("Edit"));
424 putValue(SHORT_DESCRIPTION, tr("Edit the current help page"));
425 putValue(SMALL_ICON,ImageProvider.get("dialogs", "edit"));
426 }
427
428 public void actionPerformed(ActionEvent e) {
429 String url = getUrl();
430 if(url == null)
431 return;
432 if (!url.startsWith(HelpUtil.getWikiBaseHelpUrl())) {
433 String message = tr(
434 "<html>The current URL <tt>{0}</tt><br>"
435 + "is an external URL. Editing is only possible for help topics<br>"
436 + "on the help server <tt>{1}</tt>.</html>",
437 getUrl(),
438 HelpUtil.getWikiBaseUrl()
439 );
440 JOptionPane.showMessageDialog(
441 Main.parent,
442 message,
443 tr("Warning"),
444 JOptionPane.WARNING_MESSAGE
445 );
446 return;
447 }
448 url = url.replaceAll("#[^#]*$", "");
449 OpenBrowser.displayUrl(url+"?action=edit");
450 }
451 }
452
453 class ReloadAction extends AbstractAction {
454 public ReloadAction() {
455 //putValue(NAME, tr("Reload"));
456 putValue(SHORT_DESCRIPTION, tr("Reload the current help page"));
457 putValue(SMALL_ICON, ImageProvider.get("dialogs", "refresh"));
458 }
459
460 public void actionPerformed(ActionEvent e) {
461 openUrl(getUrl());
462 }
463 }
464
465 static class BackAction extends AbstractAction implements Observer {
466 private HelpBrowserHistory history;
467 public BackAction(HelpBrowserHistory history) {
468 this.history = history;
469 history.addObserver(this);
470 //putValue(NAME, tr("Back"));
471 putValue(SHORT_DESCRIPTION, tr("Go to the previous page"));
472 putValue(SMALL_ICON, ImageProvider.get("help", "previous"));
473 setEnabled(history.canGoBack());
474 }
475
476 public void actionPerformed(ActionEvent e) {
477 history.back();
478 }
479 public void update(Observable o, Object arg) {
480 //System.out.println("BackAction: canGoBoack=" + history.canGoBack() );
481 setEnabled(history.canGoBack());
482 }
483 }
484
485 static class ForwardAction extends AbstractAction implements Observer {
486 private HelpBrowserHistory history;
487 public ForwardAction(HelpBrowserHistory history) {
488 this.history = history;
489 history.addObserver(this);
490 //putValue(NAME, tr("Forward"));
491 putValue(SHORT_DESCRIPTION, tr("Go to the next page"));
492 putValue(SMALL_ICON, ImageProvider.get("help", "next"));
493 setEnabled(history.canGoForward());
494 }
495
496 public void actionPerformed(ActionEvent e) {
497 history.forward();
498 }
499 public void update(Observable o, Object arg) {
500 setEnabled(history.canGoForward());
501 }
502 }
503
504 class HomeAction extends AbstractAction {
505 public HomeAction() {
506 //putValue(NAME, tr("Home"));
507 putValue(SHORT_DESCRIPTION, tr("Go to the JOSM help home page"));
508 putValue(SMALL_ICON, ImageProvider.get("help", "home"));
509 }
510
511 public void actionPerformed(ActionEvent e) {
512 openHelpTopic("/");
513 }
514 }
515
516 class HyperlinkHandler implements HyperlinkListener {
517
518 /**
519 * Scrolls the help browser to the element with id <code>id</code>
520 *
521 * @param id the id
522 * @return true, if an element with this id was found and scrolling was successful; false, otherwise
523 */
524 protected boolean scrollToElementWithId(String id) {
525 Document d = help.getDocument();
526 if (d instanceof HTMLDocument) {
527 HTMLDocument doc = (HTMLDocument) d;
528 Element element = doc.getElement(id);
529 try {
530 Rectangle r = help.modelToView(element.getStartOffset());
531 if (r != null) {
532 Rectangle vis = help.getVisibleRect();
533 r.height = vis.height;
534 help.scrollRectToVisible(r);
535 return true;
536 }
537 } catch(BadLocationException e) {
538 System.err.println(tr("Warning: bad location in HTML document. Exception was: {0}", e.toString()));
539 e.printStackTrace();
540 }
541 }
542 return false;
543 }
544
545 /**
546 * Checks whether the hyperlink event originated on a <a ...> element with
547 * a relative href consisting of a URL fragment only, i.e.
548 * <a href="#thisIsALocalFragment">. If so, replies the fragment, i.e.
549 * "thisIsALocalFragment".
550 *
551 * Otherwise, replies null
552 *
553 * @param e the hyperlink event
554 * @return the local fragment
555 */
556 protected String getUrlFragment(HyperlinkEvent e) {
557 AttributeSet set = e.getSourceElement().getAttributes();
558 Object value = set.getAttribute(Tag.A);
559 if (value == null || ! (value instanceof SimpleAttributeSet)) return null;
560 SimpleAttributeSet atts = (SimpleAttributeSet)value;
561 value = atts.getAttribute(javax.swing.text.html.HTML.Attribute.HREF);
562 if (value == null) return null;
563 String s = (String)value;
564 if (s.matches("#.*"))
565 return s.substring(1);
566 return null;
567 }
568
569 public void hyperlinkUpdate(HyperlinkEvent e) {
570 if (e.getEventType() != HyperlinkEvent.EventType.ACTIVATED)
571 return;
572 if (e.getURL() == null) {
573 // Probably hyperlink event on a an A-element with a href consisting of
574 // a fragment only, i.e. "#ALocalFragment".
575 //
576 String fragment = getUrlFragment(e);
577 if (fragment != null) {
578 // first try to scroll to an element with id==fragment. This is the way
579 // table of contents are built in the JOSM wiki. If this fails, try to
580 // scroll to a <A name="..."> element.
581 //
582 if (!scrollToElementWithId(fragment)) {
583 help.scrollToReference(fragment);
584 }
585 } else {
586 HelpAwareOptionPane.showOptionDialog(
587 Main.parent,
588 tr("Failed to open help page. The target URL is empty."),
589 tr("Failed to open help page"),
590 JOptionPane.ERROR_MESSAGE,
591 null, /* no icon */
592 null, /* standard options, just OK button */
593 null, /* default is standard */
594 null /* no help context */
595 );
596 }
597 } else if (e.getURL().toString().endsWith("action=edit")) {
598 OpenBrowser.displayUrl(e.getURL().toString());
599 } else {
600 url = e.getURL().toString();
601 openUrl(e.getURL().toString());
602 }
603 }
604 }
605 }