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 }