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.gui.help.HelpUtil.getHelpTopicUrl; 007import static org.openstreetmap.josm.tools.I18n.tr; 008 009import java.awt.BorderLayout; 010import java.awt.Dimension; 011import java.awt.event.ActionEvent; 012import java.awt.event.WindowAdapter; 013import java.awt.event.WindowEvent; 014import java.io.IOException; 015import java.io.StringReader; 016import java.nio.charset.StandardCharsets; 017import java.util.Locale; 018 019import javax.swing.AbstractAction; 020import javax.swing.JButton; 021import javax.swing.JFrame; 022import javax.swing.JMenuItem; 023import javax.swing.JOptionPane; 024import javax.swing.JPanel; 025import javax.swing.JScrollPane; 026import javax.swing.JSeparator; 027import javax.swing.JToolBar; 028import javax.swing.SwingUtilities; 029import javax.swing.event.ChangeEvent; 030import javax.swing.event.ChangeListener; 031import javax.swing.text.BadLocationException; 032import javax.swing.text.Document; 033import javax.swing.text.html.StyleSheet; 034 035import org.openstreetmap.josm.actions.JosmAction; 036import org.openstreetmap.josm.gui.HelpAwareOptionPane; 037import org.openstreetmap.josm.gui.MainApplication; 038import org.openstreetmap.josm.gui.MainMenu; 039import org.openstreetmap.josm.gui.util.WindowGeometry; 040import org.openstreetmap.josm.gui.widgets.JosmEditorPane; 041import org.openstreetmap.josm.gui.widgets.JosmHTMLEditorKit; 042import org.openstreetmap.josm.io.CachedFile; 043import org.openstreetmap.josm.tools.ImageProvider; 044import org.openstreetmap.josm.tools.InputMapUtils; 045import org.openstreetmap.josm.tools.LanguageInfo.LocaleType; 046import org.openstreetmap.josm.tools.Logging; 047import org.openstreetmap.josm.tools.OpenBrowser; 048 049/** 050 * Help browser displaying HTML pages fetched from JOSM wiki. 051 */ 052public class HelpBrowser extends JFrame implements IHelpBrowser { 053 054 /** the unique instance */ 055 private static HelpBrowser instance; 056 057 /** the menu item in the windows menu. Required to properly hide on dialog close */ 058 private JMenuItem windowMenuItem; 059 060 /** the help browser */ 061 private JosmEditorPane help; 062 063 /** the help browser history */ 064 private transient HelpBrowserHistory history; 065 066 /** the currently displayed URL */ 067 private String url; 068 069 private final transient HelpContentReader reader; 070 071 private static final JosmAction FOCUS_ACTION = new JosmAction(tr("JOSM Help Browser"), "help", "", null, false, false) { 072 @Override 073 public void actionPerformed(ActionEvent e) { 074 HelpBrowser.getInstance().setVisible(true); 075 } 076 }; 077 078 /** 079 * Constructs a new {@code HelpBrowser}. 080 */ 081 public HelpBrowser() { 082 reader = new HelpContentReader(HelpUtil.getWikiBaseUrl()); 083 build(); 084 } 085 086 /** 087 * Replies the unique instance of the help browser 088 * 089 * @return the unique instance of the help browser 090 */ 091 public static synchronized HelpBrowser getInstance() { 092 if (instance == null) { 093 instance = new HelpBrowser(); 094 } 095 return instance; 096 } 097 098 /** 099 * Show the help page for help topic <code>helpTopic</code>. 100 * 101 * @param helpTopic the help topic 102 */ 103 public static void setUrlForHelpTopic(final String helpTopic) { 104 final HelpBrowser browser = getInstance(); 105 SwingUtilities.invokeLater(() -> { 106 browser.openHelpTopic(helpTopic); 107 browser.setVisible(true); 108 browser.toFront(); 109 }); 110 } 111 112 /** 113 * Launches the internal help browser and directs it to the help page for 114 * <code>helpTopic</code>. 115 * 116 * @param helpTopic the help topic 117 */ 118 public static void launchBrowser(String helpTopic) { 119 HelpBrowser browser = getInstance(); 120 browser.openHelpTopic(helpTopic); 121 browser.setVisible(true); 122 browser.toFront(); 123 } 124 125 /** 126 * Builds the style sheet used in the internal help browser 127 * 128 * @return the style sheet 129 */ 130 protected StyleSheet buildStyleSheet() { 131 StyleSheet ss = new StyleSheet(); 132 final String css; 133 try (CachedFile cf = new CachedFile("resource://data/help-browser.css")) { 134 css = new String(cf.getByteContent(), StandardCharsets.ISO_8859_1); 135 } catch (IOException e) { 136 Logging.error(tr("Failed to read CSS file ''help-browser.css''. Exception is: {0}", e.toString())); 137 Logging.error(e); 138 return ss; 139 } 140 ss.addRule(css); 141 return ss; 142 } 143 144 /** 145 * Builds toolbar. 146 * @return the toolbar 147 */ 148 protected JToolBar buildToolBar() { 149 JToolBar tb = new JToolBar(); 150 tb.add(new JButton(new HomeAction(this))); 151 tb.add(new JButton(new BackAction(this))); 152 tb.add(new JButton(new ForwardAction(this))); 153 tb.add(new JButton(new ReloadAction(this))); 154 tb.add(new JSeparator()); 155 tb.add(new JButton(new OpenInBrowserAction(this))); 156 tb.add(new JButton(new EditAction(this))); 157 return tb; 158 } 159 160 /** 161 * Builds GUI. 162 */ 163 protected final void build() { 164 help = new JosmEditorPane(); 165 JosmHTMLEditorKit kit = new JosmHTMLEditorKit(); 166 kit.setStyleSheet(buildStyleSheet()); 167 help.setEditorKit(kit); 168 help.setEditable(false); 169 help.addHyperlinkListener(new HyperlinkHandler(this, help)); 170 help.setContentType("text/html"); 171 history = new HelpBrowserHistory(this); 172 173 JPanel p = new JPanel(new BorderLayout()); 174 setContentPane(p); 175 176 p.add(new JScrollPane(help), BorderLayout.CENTER); 177 178 addWindowListener(new WindowAdapter() { 179 @Override public void windowClosing(WindowEvent e) { 180 setVisible(false); 181 } 182 }); 183 184 p.add(buildToolBar(), BorderLayout.NORTH); 185 InputMapUtils.addEscapeAction(getRootPane(), new AbstractAction() { 186 @Override 187 public void actionPerformed(ActionEvent e) { 188 setVisible(false); 189 } 190 }); 191 192 setMinimumSize(new Dimension(400, 200)); 193 setTitle(tr("JOSM Help Browser")); 194 } 195 196 @Override 197 public void setVisible(boolean visible) { 198 if (visible) { 199 new WindowGeometry( 200 getClass().getName() + ".geometry", 201 WindowGeometry.centerInWindow( 202 getParent(), 203 new Dimension(600, 400) 204 ) 205 ).applySafe(this); 206 } else if (isShowing()) { // Avoid IllegalComponentStateException like in #8775 207 new WindowGeometry(this).remember(getClass().getName() + ".geometry"); 208 } 209 MainMenu menu = MainApplication.getMenu(); 210 if (menu != null && menu.windowMenu != null) { 211 if (windowMenuItem != null && !visible) { 212 menu.windowMenu.remove(windowMenuItem); 213 windowMenuItem = null; 214 } 215 if (windowMenuItem == null && visible) { 216 windowMenuItem = MainMenu.add(menu.windowMenu, FOCUS_ACTION, MainMenu.WINDOW_MENU_GROUP.VOLATILE); 217 } 218 } 219 super.setVisible(visible); 220 } 221 222 /** 223 * Load help topic. 224 * @param content topic contents 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 (IOException | BadLocationException e) { 231 Logging.error(e); 232 } 233 help.setDocument(document); 234 } 235 236 @Override 237 public String getUrl() { 238 return url; 239 } 240 241 @Override 242 public void setUrl(String url) { 243 this.url = 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, LocaleType.DEFAULT)), 263 getHelpTopicEditUrl(buildAbsoluteHelpTopic(relativeHelpTopic, LocaleType.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 = getHelpTopicUrl(buildAbsoluteHelpTopic(relativeHelpTopic, LocaleType.DEFAULTNOTENGLISH)); 296 String content = null; 297 try { 298 content = reader.fetchHelpTopicContent(url, true); 299 } catch (MissingHelpContentException e) { 300 Logging.trace(e); 301 url = getHelpTopicUrl(buildAbsoluteHelpTopic(relativeHelpTopic, LocaleType.BASELANGUAGE)); 302 try { 303 content = reader.fetchHelpTopicContent(url, true); 304 } catch (MissingHelpContentException e1) { 305 Logging.trace(e1); 306 url = getHelpTopicUrl(buildAbsoluteHelpTopic(relativeHelpTopic, LocaleType.ENGLISH)); 307 try { 308 content = reader.fetchHelpTopicContent(url, true); 309 } catch (MissingHelpContentException e2) { 310 Logging.debug(e2); 311 this.url = url; 312 handleMissingHelpContent(relativeHelpTopic); 313 return; 314 } catch (HelpContentReaderException e2) { 315 Logging.error(e2); 316 handleHelpContentReaderException(relativeHelpTopic, e2); 317 return; 318 } 319 } catch (HelpContentReaderException e1) { 320 Logging.error(e1); 321 handleHelpContentReaderException(relativeHelpTopic, e1); 322 return; 323 } 324 } catch (HelpContentReaderException e) { 325 Logging.error(e); 326 handleHelpContentReaderException(relativeHelpTopic, e); 327 return; 328 } 329 loadTopic(content); 330 history.setCurrentUrl(url); 331 this.url = url; 332 } 333 334 /** 335 * Loads a help topic given by an absolute help topic name, i.e. 336 * "/De:Help/Action/New" 337 * 338 * @param absoluteHelpTopic the absolute help topic name 339 */ 340 protected void loadAbsoluteHelpTopic(String absoluteHelpTopic) { 341 String url = getHelpTopicUrl(absoluteHelpTopic); 342 String content = null; 343 try { 344 content = reader.fetchHelpTopicContent(url, true); 345 } catch (MissingHelpContentException e) { 346 Logging.debug(e); 347 this.url = url; 348 handleMissingHelpContent(absoluteHelpTopic); 349 return; 350 } catch (HelpContentReaderException e) { 351 Logging.error(e); 352 handleHelpContentReaderException(absoluteHelpTopic, e); 353 return; 354 } 355 loadTopic(content); 356 history.setCurrentUrl(url); 357 this.url = url; 358 } 359 360 @Override 361 public void openUrl(String url) { 362 if (!isVisible()) { 363 setVisible(true); 364 toFront(); 365 } else { 366 toFront(); 367 } 368 String helpTopic = HelpUtil.extractAbsoluteHelpTopic(url); 369 if (helpTopic == null) { 370 try { 371 this.url = url; 372 String content = reader.fetchHelpTopicContent(url, false); 373 loadTopic(content); 374 history.setCurrentUrl(url); 375 this.url = url; 376 } catch (HelpContentReaderException e) { 377 Logging.warn(e); 378 HelpAwareOptionPane.showOptionDialog( 379 MainApplication.getMainFrame(), 380 tr( 381 "<html>Failed to open help page for url {0}.<br>" 382 + "This is most likely due to a network problem, please check<br>" 383 + "your internet connection</html>", 384 url 385 ), 386 tr("Failed to open URL"), 387 JOptionPane.ERROR_MESSAGE, 388 null, /* no icon */ 389 null, /* standard options, just OK button */ 390 null, /* default is standard */ 391 null /* no help context */ 392 ); 393 } 394 history.setCurrentUrl(url); 395 } else { 396 loadAbsoluteHelpTopic(helpTopic); 397 } 398 } 399 400 @Override 401 public void openHelpTopic(String relativeHelpTopic) { 402 if (!isVisible()) { 403 setVisible(true); 404 toFront(); 405 } else { 406 toFront(); 407 } 408 loadRelativeHelpTopic(relativeHelpTopic); 409 } 410 411 abstract static class AbstractBrowserAction extends AbstractAction { 412 protected final transient IHelpBrowser browser; 413 414 protected AbstractBrowserAction(IHelpBrowser browser) { 415 this.browser = browser; 416 } 417 } 418 419 static class OpenInBrowserAction extends AbstractBrowserAction { 420 421 /** 422 * Constructs a new {@code OpenInBrowserAction}. 423 * @param browser help browser 424 */ 425 OpenInBrowserAction(IHelpBrowser browser) { 426 super(browser); 427 putValue(SHORT_DESCRIPTION, tr("Open the current help page in an external browser")); 428 new ImageProvider("help", "internet").getResource().attachImageIcon(this, true); 429 } 430 431 @Override 432 public void actionPerformed(ActionEvent e) { 433 OpenBrowser.displayUrl(browser.getUrl()); 434 } 435 } 436 437 static class EditAction extends AbstractBrowserAction { 438 439 /** 440 * Constructs a new {@code EditAction}. 441 * @param browser help browser 442 */ 443 EditAction(IHelpBrowser browser) { 444 super(browser); 445 putValue(SHORT_DESCRIPTION, tr("Edit the current help page")); 446 new ImageProvider("dialogs", "edit").getResource().attachImageIcon(this, true); 447 } 448 449 @Override 450 public void actionPerformed(ActionEvent e) { 451 String url = browser.getUrl(); 452 if (url == null) 453 return; 454 if (!url.startsWith(HelpUtil.getWikiBaseHelpUrl())) { 455 String message = tr( 456 "<html>The current URL <tt>{0}</tt><br>" 457 + "is an external URL. Editing is only possible for help topics<br>" 458 + "on the help server <tt>{1}</tt>.</html>", 459 url, 460 HelpUtil.getWikiBaseUrl() 461 ); 462 JOptionPane.showMessageDialog( 463 MainApplication.getMainFrame(), 464 message, 465 tr("Warning"), 466 JOptionPane.WARNING_MESSAGE 467 ); 468 return; 469 } 470 url = url.replaceAll("#[^#]*$", ""); 471 OpenBrowser.displayUrl(url+"?action=edit"); 472 } 473 } 474 475 static class ReloadAction extends AbstractBrowserAction { 476 477 /** 478 * Constructs a new {@code ReloadAction}. 479 * @param browser help browser 480 */ 481 ReloadAction(IHelpBrowser browser) { 482 super(browser); 483 putValue(SHORT_DESCRIPTION, tr("Reload the current help page")); 484 new ImageProvider("dialogs", "refresh").getResource().attachImageIcon(this, true); 485 } 486 487 @Override 488 public void actionPerformed(ActionEvent e) { 489 browser.openUrl(browser.getUrl()); 490 } 491 } 492 493 static class BackAction extends AbstractBrowserAction implements ChangeListener { 494 495 /** 496 * Constructs a new {@code BackAction}. 497 * @param browser help browser 498 */ 499 BackAction(IHelpBrowser browser) { 500 super(browser); 501 browser.getHistory().addChangeListener(this); 502 putValue(SHORT_DESCRIPTION, tr("Go to the previous page")); 503 new ImageProvider("dialogs", "previous").getResource().attachImageIcon(this, true); 504 setEnabled(browser.getHistory().canGoBack()); 505 } 506 507 @Override 508 public void actionPerformed(ActionEvent e) { 509 browser.getHistory().back(); 510 } 511 512 @Override 513 public void stateChanged(ChangeEvent e) { 514 setEnabled(browser.getHistory().canGoBack()); 515 } 516 } 517 518 static class ForwardAction extends AbstractBrowserAction implements ChangeListener { 519 520 /** 521 * Constructs a new {@code ForwardAction}. 522 * @param browser help browser 523 */ 524 ForwardAction(IHelpBrowser browser) { 525 super(browser); 526 browser.getHistory().addChangeListener(this); 527 putValue(SHORT_DESCRIPTION, tr("Go to the next page")); 528 new ImageProvider("dialogs", "next").getResource().attachImageIcon(this, true); 529 setEnabled(browser.getHistory().canGoForward()); 530 } 531 532 @Override 533 public void actionPerformed(ActionEvent e) { 534 browser.getHistory().forward(); 535 } 536 537 @Override 538 public void stateChanged(ChangeEvent e) { 539 setEnabled(browser.getHistory().canGoForward()); 540 } 541 } 542 543 static class HomeAction extends AbstractBrowserAction { 544 545 /** 546 * Constructs a new {@code HomeAction}. 547 * @param browser help browser 548 */ 549 HomeAction(IHelpBrowser browser) { 550 super(browser); 551 putValue(SHORT_DESCRIPTION, tr("Go to the JOSM help home page")); 552 new ImageProvider("help", "home").getResource().attachImageIcon(this, true); 553 } 554 555 @Override 556 public void actionPerformed(ActionEvent e) { 557 browser.openHelpTopic("/"); 558 } 559 } 560 561 @Override 562 public HelpBrowserHistory getHistory() { 563 return history; 564 } 565}