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

Last change on this file since 14807 was 14807, checked in by Don-vip, 5 years ago

fix #17338 - NPE

  • Property svn:eol-style set to native
File size: 19.4 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.gui.help.HelpUtil.getHelpTopicUrl;
7import static org.openstreetmap.josm.tools.I18n.tr;
8
9import java.awt.BorderLayout;
10import java.awt.Dimension;
11import java.awt.event.ActionEvent;
12import java.awt.event.WindowAdapter;
13import java.awt.event.WindowEvent;
14import java.io.IOException;
15import java.io.StringReader;
16import java.nio.charset.StandardCharsets;
17import java.util.Locale;
18
19import javax.swing.AbstractAction;
20import javax.swing.JButton;
21import javax.swing.JFrame;
22import javax.swing.JMenuItem;
23import javax.swing.JOptionPane;
24import javax.swing.JPanel;
25import javax.swing.JScrollPane;
26import javax.swing.JSeparator;
27import javax.swing.JToolBar;
28import javax.swing.SwingUtilities;
29import javax.swing.event.ChangeEvent;
30import javax.swing.event.ChangeListener;
31import javax.swing.text.BadLocationException;
32import javax.swing.text.Document;
33import javax.swing.text.html.StyleSheet;
34
35import org.openstreetmap.josm.actions.JosmAction;
36import org.openstreetmap.josm.gui.HelpAwareOptionPane;
37import org.openstreetmap.josm.gui.MainApplication;
38import org.openstreetmap.josm.gui.MainMenu;
39import org.openstreetmap.josm.gui.util.WindowGeometry;
40import org.openstreetmap.josm.gui.widgets.JosmEditorPane;
41import org.openstreetmap.josm.gui.widgets.JosmHTMLEditorKit;
42import org.openstreetmap.josm.io.CachedFile;
43import org.openstreetmap.josm.tools.ImageProvider;
44import org.openstreetmap.josm.tools.InputMapUtils;
45import org.openstreetmap.josm.tools.LanguageInfo.LocaleType;
46import org.openstreetmap.josm.tools.Logging;
47import org.openstreetmap.josm.tools.OpenBrowser;
48
49/**
50 * Help browser displaying HTML pages fetched from JOSM wiki.
51 */
52public class HelpBrowser extends JFrame implements IHelpBrowser {
53
54 /** the unique instance */
55 private static HelpBrowser instance;
56
57 /** the menu item in the windows menu. Required to properly hide on dialog close */
58 private JMenuItem windowMenuItem;
59
60 /** the help browser */
61 private JosmEditorPane help;
62
63 /** the help browser history */
64 private transient HelpBrowserHistory history;
65
66 /** the currently displayed URL */
67 private String url;
68
69 private final transient HelpContentReader reader;
70
71 private static final JosmAction FOCUS_ACTION = new JosmAction(tr("JOSM Help Browser"), "help", "", null, false, false) {
72 @Override
73 public void actionPerformed(ActionEvent e) {
74 HelpBrowser.getInstance().setVisible(true);
75 }
76 };
77
78 /**
79 * Constructs a new {@code HelpBrowser}.
80 */
81 public HelpBrowser() {
82 reader = new HelpContentReader(HelpUtil.getWikiBaseUrl());
83 build();
84 }
85
86 /**
87 * Replies the unique instance of the help browser
88 *
89 * @return the unique instance of the help browser
90 */
91 public static synchronized HelpBrowser getInstance() {
92 if (instance == null) {
93 instance = new HelpBrowser();
94 }
95 return instance;
96 }
97
98 /**
99 * 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}
Note: See TracBrowser for help on using the repository browser.