001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.help; 003 004import static org.openstreetmap.josm.gui.help.HelpUtil.buildAbsoluteHelpTopic; 005import static org.openstreetmap.josm.gui.help.HelpUtil.getHelpTopicEditUrl; 006import static org.openstreetmap.josm.tools.I18n.tr; 007 008import java.awt.BorderLayout; 009import java.awt.Dimension; 010import java.awt.Rectangle; 011import java.awt.event.ActionEvent; 012import java.awt.event.KeyEvent; 013import java.awt.event.WindowAdapter; 014import java.awt.event.WindowEvent; 015import java.io.BufferedReader; 016import java.io.InputStreamReader; 017import java.io.StringReader; 018import java.util.Locale; 019import java.util.Observable; 020import java.util.Observer; 021 022import javax.swing.AbstractAction; 023import javax.swing.JButton; 024import javax.swing.JComponent; 025import javax.swing.JDialog; 026import javax.swing.JMenuItem; 027import javax.swing.JOptionPane; 028import javax.swing.JPanel; 029import javax.swing.JScrollPane; 030import javax.swing.JSeparator; 031import javax.swing.JToolBar; 032import javax.swing.KeyStroke; 033import javax.swing.SwingUtilities; 034import javax.swing.event.HyperlinkEvent; 035import javax.swing.event.HyperlinkListener; 036import javax.swing.text.AttributeSet; 037import javax.swing.text.BadLocationException; 038import javax.swing.text.Document; 039import javax.swing.text.Element; 040import javax.swing.text.SimpleAttributeSet; 041import javax.swing.text.html.HTML.Tag; 042import javax.swing.text.html.HTMLDocument; 043import javax.swing.text.html.StyleSheet; 044 045import org.openstreetmap.josm.Main; 046import org.openstreetmap.josm.actions.JosmAction; 047import org.openstreetmap.josm.gui.HelpAwareOptionPane; 048import org.openstreetmap.josm.gui.MainMenu; 049import org.openstreetmap.josm.gui.widgets.JosmEditorPane; 050import org.openstreetmap.josm.gui.widgets.JosmHTMLEditorKit; 051import org.openstreetmap.josm.tools.ImageProvider; 052import org.openstreetmap.josm.tools.LanguageInfo.LocaleType; 053import org.openstreetmap.josm.tools.OpenBrowser; 054import org.openstreetmap.josm.tools.Utils; 055import org.openstreetmap.josm.tools.WindowGeometry; 056 057public class HelpBrowser extends JDialog { 058 /** the unique instance */ 059 private static HelpBrowser instance; 060 061 /** the menu item in the windows menu. Required to properly 062 * hide on dialog close. 063 */ 064 private JMenuItem windowMenuItem; 065 066 /** 067 * Replies the unique instance of the help browser 068 * 069 * @return the unique instance of the help browser 070 */ 071 static public HelpBrowser getInstance() { 072 if (instance == null) { 073 instance = new HelpBrowser(); 074 } 075 return instance; 076 } 077 078 /** 079 * Show the help page for help topic <code>helpTopic</code>. 080 * 081 * @param helpTopic the help topic 082 */ 083 public static void setUrlForHelpTopic(final String helpTopic) { 084 final HelpBrowser browser = getInstance(); 085 Runnable r = new Runnable() { 086 @Override 087 public void run() { 088 browser.openHelpTopic(helpTopic); 089 browser.setVisible(true); 090 browser.toFront(); 091 } 092 }; 093 SwingUtilities.invokeLater(r); 094 } 095 096 /** 097 * Launches the internal help browser and directs it to the help page for 098 * <code>helpTopic</code>. 099 * 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 Main.error(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(SHORT_DESCRIPTION, tr("Reload the current help page")); 474 putValue(SMALL_ICON, ImageProvider.get("dialogs", "refresh")); 475 } 476 477 @Override 478 public void actionPerformed(ActionEvent e) { 479 openUrl(getUrl()); 480 } 481 } 482 483 static class BackAction extends AbstractAction implements Observer { 484 private HelpBrowserHistory history; 485 public BackAction(HelpBrowserHistory history) { 486 this.history = history; 487 history.addObserver(this); 488 putValue(SHORT_DESCRIPTION, tr("Go to the previous page")); 489 putValue(SMALL_ICON, ImageProvider.get("help", "previous")); 490 setEnabled(history.canGoBack()); 491 } 492 493 @Override 494 public void actionPerformed(ActionEvent e) { 495 history.back(); 496 } 497 @Override 498 public void update(Observable o, Object arg) { 499 setEnabled(history.canGoBack()); 500 } 501 } 502 503 static class ForwardAction extends AbstractAction implements Observer { 504 private HelpBrowserHistory history; 505 public ForwardAction(HelpBrowserHistory history) { 506 this.history = history; 507 history.addObserver(this); 508 putValue(SHORT_DESCRIPTION, tr("Go to the next page")); 509 putValue(SMALL_ICON, ImageProvider.get("help", "next")); 510 setEnabled(history.canGoForward()); 511 } 512 513 @Override 514 public void actionPerformed(ActionEvent e) { 515 history.forward(); 516 } 517 @Override 518 public void update(Observable o, Object arg) { 519 setEnabled(history.canGoForward()); 520 } 521 } 522 523 class HomeAction extends AbstractAction { 524 public HomeAction() { 525 putValue(SHORT_DESCRIPTION, tr("Go to the JOSM help home page")); 526 putValue(SMALL_ICON, ImageProvider.get("help", "home")); 527 } 528 529 @Override 530 public void actionPerformed(ActionEvent e) { 531 openHelpTopic("/"); 532 } 533 } 534 535 class HyperlinkHandler implements HyperlinkListener { 536 537 /** 538 * Scrolls the help browser to the element with id <code>id</code> 539 * 540 * @param id the id 541 * @return true, if an element with this id was found and scrolling was successful; false, otherwise 542 */ 543 protected boolean scrollToElementWithId(String id) { 544 Document d = help.getDocument(); 545 if (d instanceof HTMLDocument) { 546 HTMLDocument doc = (HTMLDocument) d; 547 Element element = doc.getElement(id); 548 try { 549 Rectangle r = help.modelToView(element.getStartOffset()); 550 if (r != null) { 551 Rectangle vis = help.getVisibleRect(); 552 r.height = vis.height; 553 help.scrollRectToVisible(r); 554 return true; 555 } 556 } catch (BadLocationException e) { 557 Main.warn(tr("Bad location in HTML document. Exception was: {0}", e.toString())); 558 e.printStackTrace(); 559 } 560 } 561 return false; 562 } 563 564 /** 565 * Checks whether the hyperlink event originated on a <a ...> element with 566 * a relative href consisting of a URL fragment only, i.e. 567 * <a href="#thisIsALocalFragment">. If so, replies the fragment, i.e. 568 * "thisIsALocalFragment". 569 * 570 * Otherwise, replies <code>null</code> 571 * 572 * @param e the hyperlink event 573 * @return the local fragment or <code>null</code> 574 */ 575 protected String getUrlFragment(HyperlinkEvent e) { 576 AttributeSet set = e.getSourceElement().getAttributes(); 577 Object value = set.getAttribute(Tag.A); 578 if (!(value instanceof SimpleAttributeSet)) return null; 579 SimpleAttributeSet atts = (SimpleAttributeSet)value; 580 value = atts.getAttribute(javax.swing.text.html.HTML.Attribute.HREF); 581 if (value == null) return null; 582 String s = (String)value; 583 if (s.matches("#.*")) 584 return s.substring(1); 585 return null; 586 } 587 588 @Override 589 public void hyperlinkUpdate(HyperlinkEvent e) { 590 if (e.getEventType() != HyperlinkEvent.EventType.ACTIVATED) 591 return; 592 if (e.getURL() == null || e.getURL().toString().startsWith(url+"#")) { 593 // Probably hyperlink event on a an A-element with a href consisting of 594 // a fragment only, i.e. "#ALocalFragment". 595 // 596 String fragment = getUrlFragment(e); 597 if (fragment != null) { 598 // first try to scroll to an element with id==fragment. This is the way 599 // table of contents are built in the JOSM wiki. If this fails, try to 600 // scroll to a <A name="..."> element. 601 // 602 if (!scrollToElementWithId(fragment)) { 603 help.scrollToReference(fragment); 604 } 605 } else { 606 HelpAwareOptionPane.showOptionDialog( 607 Main.parent, 608 tr("Failed to open help page. The target URL is empty."), 609 tr("Failed to open help page"), 610 JOptionPane.ERROR_MESSAGE, 611 null, /* no icon */ 612 null, /* standard options, just OK button */ 613 null, /* default is standard */ 614 null /* no help context */ 615 ); 616 } 617 } else if (e.getURL().toString().endsWith("action=edit")) { 618 OpenBrowser.displayUrl(e.getURL().toString()); 619 } else { 620 url = e.getURL().toString(); 621 openUrl(e.getURL().toString()); 622 } 623 } 624 } 625}