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

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

see #18514 - disable Window menu when empty

  • 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.WindowMenu;
40import org.openstreetmap.josm.gui.util.WindowGeometry;
41import org.openstreetmap.josm.gui.widgets.JosmEditorPane;
42import org.openstreetmap.josm.gui.widgets.JosmHTMLEditorKit;
43import org.openstreetmap.josm.io.CachedFile;
44import org.openstreetmap.josm.tools.ImageProvider;
45import org.openstreetmap.josm.tools.InputMapUtils;
46import org.openstreetmap.josm.tools.LanguageInfo.LocaleType;
47import org.openstreetmap.josm.tools.Logging;
48import org.openstreetmap.josm.tools.OpenBrowser;
49
50/**
51 * Help browser displaying HTML pages fetched from JOSM wiki.
52 */
53public class HelpBrowser extends JFrame implements IHelpBrowser {
54
55 /** the unique instance */
56 private static HelpBrowser instance;
57
58 /** the menu item in the windows menu. Required to properly hide on dialog close */
59 private JMenuItem windowMenuItem;
60
61 /** the help browser */
62 private JosmEditorPane help;
63
64 /** the help browser history */
65 private transient HelpBrowserHistory history;
66
67 /** the currently displayed URL */
68 private String url;
69
70 private final transient HelpContentReader reader;
71
72 private static final JosmAction FOCUS_ACTION = new JosmAction(tr("JOSM Help Browser"), "help", "", null, false, false) {
73 @Override
74 public void actionPerformed(ActionEvent e) {
75 HelpBrowser.getInstance().setVisible(true);
76 }
77 };
78
79 /**
80 * Constructs a new {@code HelpBrowser}.
81 */
82 public HelpBrowser() {
83 reader = new HelpContentReader(HelpUtil.getWikiBaseUrl());
84 build();
85 }
86
87 /**
88 * Replies the unique instance of the help browser
89 *
90 * @return the unique instance of the help browser
91 */
92 public static synchronized HelpBrowser getInstance() {
93 if (instance == null) {
94 instance = new HelpBrowser();
95 }
96 return instance;
97 }
98
99 /**
100 * Show the help page for help topic <code>helpTopic</code>.
101 *
102 * @param helpTopic the help topic
103 */
104 public static void setUrlForHelpTopic(final String helpTopic) {
105 final HelpBrowser browser = getInstance();
106 SwingUtilities.invokeLater(() -> {
107 browser.openHelpTopic(helpTopic);
108 browser.setVisible(true);
109 browser.toFront();
110 });
111 }
112
113 /**
114 * Launches the internal help browser and directs it to the help page for
115 * <code>helpTopic</code>.
116 *
117 * @param helpTopic the help topic
118 */
119 public static void launchBrowser(String helpTopic) {
120 HelpBrowser browser = getInstance();
121 browser.openHelpTopic(helpTopic);
122 browser.setVisible(true);
123 browser.toFront();
124 }
125
126 /**
127 * Builds the style sheet used in the internal help browser
128 *
129 * @return the style sheet
130 */
131 protected StyleSheet buildStyleSheet() {
132 StyleSheet ss = new StyleSheet();
133 final String css;
134 try (CachedFile cf = new CachedFile("resource://data/help-browser.css")) {
135 css = new String(cf.getByteContent(), StandardCharsets.ISO_8859_1);
136 } catch (IOException e) {
137 Logging.error(tr("Failed to read CSS file ''help-browser.css''. Exception is: {0}", e.toString()));
138 Logging.error(e);
139 return ss;
140 }
141 ss.addRule(css);
142 return ss;
143 }
144
145 /**
146 * Builds toolbar.
147 * @return the toolbar
148 */
149 protected JToolBar buildToolBar() {
150 JToolBar tb = new JToolBar();
151 tb.add(new JButton(new HomeAction(this)));
152 tb.add(new JButton(new BackAction(this)));
153 tb.add(new JButton(new ForwardAction(this)));
154 tb.add(new JButton(new ReloadAction(this)));
155 tb.add(new JSeparator());
156 tb.add(new JButton(new OpenInBrowserAction(this)));
157 tb.add(new JButton(new EditAction(this)));
158 return tb;
159 }
160
161 /**
162 * Builds GUI.
163 */
164 protected final void build() {
165 help = new JosmEditorPane();
166 JosmHTMLEditorKit kit = new JosmHTMLEditorKit();
167 kit.setStyleSheet(buildStyleSheet());
168 help.setEditorKit(kit);
169 help.setEditable(false);
170 help.addHyperlinkListener(new HyperlinkHandler(this, help));
171 help.setContentType("text/html");
172 history = new HelpBrowserHistory(this);
173
174 JPanel p = new JPanel(new BorderLayout());
175 setContentPane(p);
176
177 p.add(new JScrollPane(help), BorderLayout.CENTER);
178
179 addWindowListener(new WindowAdapter() {
180 @Override public void windowClosing(WindowEvent e) {
181 setVisible(false);
182 }
183 });
184
185 p.add(buildToolBar(), BorderLayout.NORTH);
186 InputMapUtils.addEscapeAction(getRootPane(), new AbstractAction() {
187 @Override
188 public void actionPerformed(ActionEvent e) {
189 setVisible(false);
190 }
191 });
192
193 setMinimumSize(new Dimension(400, 200));
194 setTitle(tr("JOSM Help Browser"));
195 }
196
197 @Override
198 public void setVisible(boolean visible) {
199 if (visible) {
200 new WindowGeometry(
201 getClass().getName() + ".geometry",
202 WindowGeometry.centerInWindow(
203 getParent(),
204 new Dimension(600, 400)
205 )
206 ).applySafe(this);
207 } else if (isShowing()) { // Avoid IllegalComponentStateException like in #8775
208 new WindowGeometry(this).remember(getClass().getName() + ".geometry");
209 }
210 MainMenu menu = MainApplication.getMenu();
211 if (menu != null && menu.windowMenu != null) {
212 if (windowMenuItem != null && !visible) {
213 menu.windowMenu.remove(windowMenuItem);
214 windowMenuItem = null;
215 }
216 if (windowMenuItem == null && visible) {
217 windowMenuItem = MainMenu.add(menu.windowMenu, FOCUS_ACTION, WindowMenu.WINDOW_MENU_GROUP.VOLATILE);
218 }
219 }
220 super.setVisible(visible);
221 }
222
223 /**
224 * Load help topic.
225 * @param content topic contents
226 */
227 protected void loadTopic(String content) {
228 Document document = help.getEditorKit().createDefaultDocument();
229 try {
230 help.getEditorKit().read(new StringReader(content), document, 0);
231 } catch (IOException | BadLocationException e) {
232 Logging.error(e);
233 }
234 help.setDocument(document);
235 }
236
237 @Override
238 public String getUrl() {
239 return url;
240 }
241
242 @Override
243 public void setUrl(String url) {
244 this.url = url;
245 }
246
247 /**
248 * Displays a warning page when a help topic doesn't exist yet.
249 *
250 * @param relativeHelpTopic the help topic
251 */
252 protected void handleMissingHelpContent(String relativeHelpTopic) {
253 // i18n: do not translate "warning-header" and "warning-body"
254 String message = tr("<html><p class=\"warning-header\">Help content for help topic missing</p>"
255 + "<p class=\"warning-body\">Help content for the help topic <strong>{0}</strong> is "
256 + "not available yet. It is missing both in your local language ({1}) and in English.<br><br>"
257 + "Please help to improve the JOSM help system and fill in the missing information. "
258 + "You can both edit the <a href=\"{2}\">help topic in your local language ({1})</a> and "
259 + "the <a href=\"{3}\">help topic in English</a>."
260 + "</p></html>",
261 relativeHelpTopic,
262 Locale.getDefault().getDisplayName(),
263 getHelpTopicEditUrl(buildAbsoluteHelpTopic(relativeHelpTopic, LocaleType.DEFAULT)),
264 getHelpTopicEditUrl(buildAbsoluteHelpTopic(relativeHelpTopic, LocaleType.ENGLISH))
265 );
266 loadTopic(message);
267 }
268
269 /**
270 * Displays a error page if a help topic couldn't be loaded because of network or IO error.
271 *
272 * @param relativeHelpTopic the help topic
273 * @param e the exception
274 */
275 protected void handleHelpContentReaderException(String relativeHelpTopic, HelpContentReaderException e) {
276 String message = tr("<html><p class=\"error-header\">Error when retrieving help information</p>"
277 + "<p class=\"error-body\">The content for the help topic <strong>{0}</strong> could "
278 + "not be loaded. The error message is (untranslated):<br>"
279 + "<tt>{1}</tt>"
280 + "</p></html>",
281 relativeHelpTopic,
282 e.toString()
283 );
284 loadTopic(message);
285 }
286
287 /**
288 * Loads a help topic given by a relative help topic name (i.e. "/Action/New")
289 *
290 * First tries to load the language specific help topic. If it is missing, tries to
291 * load the topic in English.
292 *
293 * @param relativeHelpTopic the relative help topic
294 */
295 protected void loadRelativeHelpTopic(String relativeHelpTopic) {
296 String url = getHelpTopicUrl(buildAbsoluteHelpTopic(relativeHelpTopic, LocaleType.DEFAULTNOTENGLISH));
297 String content = null;
298 try {
299 content = reader.fetchHelpTopicContent(url, true);
300 } catch (MissingHelpContentException e) {
301 Logging.trace(e);
302 url = getHelpTopicUrl(buildAbsoluteHelpTopic(relativeHelpTopic, LocaleType.BASELANGUAGE));
303 try {
304 content = reader.fetchHelpTopicContent(url, true);
305 } catch (MissingHelpContentException e1) {
306 Logging.trace(e1);
307 url = getHelpTopicUrl(buildAbsoluteHelpTopic(relativeHelpTopic, LocaleType.ENGLISH));
308 try {
309 content = reader.fetchHelpTopicContent(url, true);
310 } catch (MissingHelpContentException e2) {
311 Logging.debug(e2);
312 this.url = url;
313 handleMissingHelpContent(relativeHelpTopic);
314 return;
315 } catch (HelpContentReaderException e2) {
316 Logging.error(e2);
317 handleHelpContentReaderException(relativeHelpTopic, e2);
318 return;
319 }
320 } catch (HelpContentReaderException e1) {
321 Logging.error(e1);
322 handleHelpContentReaderException(relativeHelpTopic, e1);
323 return;
324 }
325 } catch (HelpContentReaderException e) {
326 Logging.error(e);
327 handleHelpContentReaderException(relativeHelpTopic, e);
328 return;
329 }
330 loadTopic(content);
331 history.setCurrentUrl(url);
332 this.url = url;
333 }
334
335 /**
336 * Loads a help topic given by an absolute help topic name, i.e.
337 * "/De:Help/Action/New"
338 *
339 * @param absoluteHelpTopic the absolute help topic name
340 */
341 protected void loadAbsoluteHelpTopic(String absoluteHelpTopic) {
342 String url = getHelpTopicUrl(absoluteHelpTopic);
343 String content = null;
344 try {
345 content = reader.fetchHelpTopicContent(url, true);
346 } catch (MissingHelpContentException e) {
347 Logging.debug(e);
348 this.url = url;
349 handleMissingHelpContent(absoluteHelpTopic);
350 return;
351 } catch (HelpContentReaderException e) {
352 Logging.error(e);
353 handleHelpContentReaderException(absoluteHelpTopic, e);
354 return;
355 }
356 loadTopic(content);
357 history.setCurrentUrl(url);
358 this.url = url;
359 }
360
361 @Override
362 public void openUrl(String url) {
363 if (!isVisible()) {
364 setVisible(true);
365 toFront();
366 } else {
367 toFront();
368 }
369 String helpTopic = HelpUtil.extractAbsoluteHelpTopic(url);
370 if (helpTopic == null) {
371 try {
372 this.url = url;
373 String content = reader.fetchHelpTopicContent(url, false);
374 loadTopic(content);
375 history.setCurrentUrl(url);
376 this.url = url;
377 } catch (HelpContentReaderException e) {
378 Logging.warn(e);
379 HelpAwareOptionPane.showOptionDialog(
380 MainApplication.getMainFrame(),
381 tr(
382 "<html>Failed to open help page for url {0}.<br>"
383 + "This is most likely due to a network problem, please check<br>"
384 + "your internet connection</html>",
385 url
386 ),
387 tr("Failed to open URL"),
388 JOptionPane.ERROR_MESSAGE,
389 null, /* no icon */
390 null, /* standard options, just OK button */
391 null, /* default is standard */
392 null /* no help context */
393 );
394 }
395 history.setCurrentUrl(url);
396 } else {
397 loadAbsoluteHelpTopic(helpTopic);
398 }
399 }
400
401 @Override
402 public void openHelpTopic(String relativeHelpTopic) {
403 if (!isVisible()) {
404 setVisible(true);
405 toFront();
406 } else {
407 toFront();
408 }
409 loadRelativeHelpTopic(relativeHelpTopic);
410 }
411
412 abstract static class AbstractBrowserAction extends AbstractAction {
413 protected final transient IHelpBrowser browser;
414
415 protected AbstractBrowserAction(IHelpBrowser browser) {
416 this.browser = browser;
417 }
418 }
419
420 static class OpenInBrowserAction extends AbstractBrowserAction {
421
422 /**
423 * Constructs a new {@code OpenInBrowserAction}.
424 * @param browser help browser
425 */
426 OpenInBrowserAction(IHelpBrowser browser) {
427 super(browser);
428 putValue(SHORT_DESCRIPTION, tr("Open the current help page in an external browser"));
429 new ImageProvider("help", "internet").getResource().attachImageIcon(this, true);
430 }
431
432 @Override
433 public void actionPerformed(ActionEvent e) {
434 OpenBrowser.displayUrl(browser.getUrl());
435 }
436 }
437
438 static class EditAction extends AbstractBrowserAction {
439
440 /**
441 * Constructs a new {@code EditAction}.
442 * @param browser help browser
443 */
444 EditAction(IHelpBrowser browser) {
445 super(browser);
446 putValue(SHORT_DESCRIPTION, tr("Edit the current help page"));
447 new ImageProvider("dialogs", "edit").getResource().attachImageIcon(this, true);
448 }
449
450 @Override
451 public void actionPerformed(ActionEvent e) {
452 String url = browser.getUrl();
453 if (url == null)
454 return;
455 if (!url.startsWith(HelpUtil.getWikiBaseHelpUrl())) {
456 String message = tr(
457 "<html>The current URL <tt>{0}</tt><br>"
458 + "is an external URL. Editing is only possible for help topics<br>"
459 + "on the help server <tt>{1}</tt>.</html>",
460 url,
461 HelpUtil.getWikiBaseUrl()
462 );
463 JOptionPane.showMessageDialog(
464 MainApplication.getMainFrame(),
465 message,
466 tr("Warning"),
467 JOptionPane.WARNING_MESSAGE
468 );
469 return;
470 }
471 url = url.replaceAll("#[^#]*$", "");
472 OpenBrowser.displayUrl(url+"?action=edit");
473 }
474 }
475
476 static class ReloadAction extends AbstractBrowserAction {
477
478 /**
479 * Constructs a new {@code ReloadAction}.
480 * @param browser help browser
481 */
482 ReloadAction(IHelpBrowser browser) {
483 super(browser);
484 putValue(SHORT_DESCRIPTION, tr("Reload the current help page"));
485 new ImageProvider("dialogs", "refresh").getResource().attachImageIcon(this, true);
486 }
487
488 @Override
489 public void actionPerformed(ActionEvent e) {
490 browser.openUrl(browser.getUrl());
491 }
492 }
493
494 static class BackAction extends AbstractBrowserAction implements ChangeListener {
495
496 /**
497 * Constructs a new {@code BackAction}.
498 * @param browser help browser
499 */
500 BackAction(IHelpBrowser browser) {
501 super(browser);
502 browser.getHistory().addChangeListener(this);
503 putValue(SHORT_DESCRIPTION, tr("Go to the previous page"));
504 new ImageProvider("dialogs", "previous").getResource().attachImageIcon(this, true);
505 setEnabled(browser.getHistory().canGoBack());
506 }
507
508 @Override
509 public void actionPerformed(ActionEvent e) {
510 browser.getHistory().back();
511 }
512
513 @Override
514 public void stateChanged(ChangeEvent e) {
515 setEnabled(browser.getHistory().canGoBack());
516 }
517 }
518
519 static class ForwardAction extends AbstractBrowserAction implements ChangeListener {
520
521 /**
522 * Constructs a new {@code ForwardAction}.
523 * @param browser help browser
524 */
525 ForwardAction(IHelpBrowser browser) {
526 super(browser);
527 browser.getHistory().addChangeListener(this);
528 putValue(SHORT_DESCRIPTION, tr("Go to the next page"));
529 new ImageProvider("dialogs", "next").getResource().attachImageIcon(this, true);
530 setEnabled(browser.getHistory().canGoForward());
531 }
532
533 @Override
534 public void actionPerformed(ActionEvent e) {
535 browser.getHistory().forward();
536 }
537
538 @Override
539 public void stateChanged(ChangeEvent e) {
540 setEnabled(browser.getHistory().canGoForward());
541 }
542 }
543
544 static class HomeAction extends AbstractBrowserAction {
545
546 /**
547 * Constructs a new {@code HomeAction}.
548 * @param browser help browser
549 */
550 HomeAction(IHelpBrowser browser) {
551 super(browser);
552 putValue(SHORT_DESCRIPTION, tr("Go to the JOSM help home page"));
553 new ImageProvider("help", "home").getResource().attachImageIcon(this, true);
554 }
555
556 @Override
557 public void actionPerformed(ActionEvent e) {
558 browser.openHelpTopic("/");
559 }
560 }
561
562 @Override
563 public HelpBrowserHistory getHistory() {
564 return history;
565 }
566}
Note: See TracBrowser for help on using the repository browser.