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

Last change on this file since 2448 was 2448, checked in by Gubaer, 14 years ago

fixed #3352: History doesn't get invalidated on upload?
fixed #3912: Extend history dialog to contain the currently modified version
new: zoom to node in list of nodes in history dialog (popup menu)
new: load history of node from node list in history dialog (popup menu or double click)
fixed: close all history dialogs when the number of layers drop to 0
fixed: implemented equals() and hashCode() on SimplePrimitiveId
fixed: history features now usePrimitiveId instead of long.

File size: 20.0 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.tools.I18n.tr;
7
8import java.awt.BorderLayout;
9import java.awt.Rectangle;
10import java.awt.event.ActionEvent;
11import java.awt.event.KeyEvent;
12import java.awt.event.WindowAdapter;
13import java.awt.event.WindowEvent;
14import java.io.BufferedReader;
15import java.io.IOException;
16import java.io.InputStreamReader;
17import java.util.Locale;
18import java.util.Observable;
19import java.util.Observer;
20import java.util.logging.Logger;
21
22import javax.swing.AbstractAction;
23import javax.swing.JButton;
24import javax.swing.JComponent;
25import javax.swing.JEditorPane;
26import javax.swing.JFrame;
27import javax.swing.JOptionPane;
28import javax.swing.JPanel;
29import javax.swing.JScrollBar;
30import javax.swing.JScrollPane;
31import javax.swing.JSeparator;
32import javax.swing.JToolBar;
33import javax.swing.KeyStroke;
34import javax.swing.event.HyperlinkEvent;
35import javax.swing.event.HyperlinkListener;
36import javax.swing.text.AttributeSet;
37import javax.swing.text.BadLocationException;
38import javax.swing.text.Document;
39import javax.swing.text.Element;
40import javax.swing.text.SimpleAttributeSet;
41import javax.swing.text.html.HTMLDocument;
42import javax.swing.text.html.HTMLEditorKit;
43import javax.swing.text.html.StyleSheet;
44import javax.swing.text.html.HTML.Tag;
45
46import org.openstreetmap.josm.Main;
47import org.openstreetmap.josm.gui.HelpAwareOptionPane;
48import org.openstreetmap.josm.tools.ImageProvider;
49import org.openstreetmap.josm.tools.OpenBrowser;
50
51public class HelpBrowser extends JFrame {
52 static private final Logger logger = Logger.getLogger(HelpBrowser.class.getName());
53
54 /** the unique instance */
55 private static HelpBrowser instance;
56
57 /**
58 * Replies the unique instance of the help browser
59 *
60 * @return the unique instance of the help browser
61 */
62 static public HelpBrowser getInstance() {
63 if (instance == null) {
64 instance = new HelpBrowser();
65 }
66 return instance;
67 }
68
69 /**
70 * Launches the internal help browser and directs it to the help page for
71 * <code>helpTopic</code>.
72 *
73 * @param helpTopic the help topic
74 */
75 static public void launchBrowser(String helpTopic) {
76 HelpBrowser browser = getInstance();
77 browser.openHelpTopic(helpTopic);
78 browser.setVisible(true);
79 browser.toFront();
80 }
81
82 /** the help browser */
83 private JEditorPane help;
84 private JScrollPane spHelp;
85
86 /** the help browser history */
87 private HelpBrowserHistory history;
88
89 /** the currently displayed URL */
90 private String url;
91
92 private HelpContentReader reader;
93
94 /**
95 * Builds the style sheet used in the internal help browser
96 *
97 * @return the style sheet
98 */
99 protected StyleSheet buildStyleSheet() {
100 StyleSheet ss = new StyleSheet();
101 BufferedReader reader = new BufferedReader(
102 new InputStreamReader(
103 getClass().getResourceAsStream("help-browser.css")
104 )
105 );
106 StringBuffer css = new StringBuffer();
107 try {
108 String line = null;
109 while ((line = reader.readLine()) != null) {
110 css.append(line);
111 css.append("\n");
112 }
113 reader.close();
114 } catch(Exception e) {
115 System.err.println(tr("Failed to read CSS file ''help-browser.css''. Exception is: {0}", e.toString()));
116 e.printStackTrace();
117 return ss;
118 }
119 ss.addRule(css.toString());
120 return ss;
121 }
122
123 protected JToolBar buildToolBar() {
124 JToolBar tb = new JToolBar();
125 tb.add(new JButton(new HomeAction()));
126 tb.add(new JButton(new BackAction(history)));
127 tb.add(new JButton(new ForwardAction(history)));
128 tb.add(new JButton(new ReloadAction()));
129 tb.add(new JSeparator());
130 tb.add(new JButton(new OpenInBrowserAction()));
131 tb.add(new JButton(new EditAction()));
132 return tb;
133 }
134
135 protected void build() {
136 help = new JEditorPane();
137 HTMLEditorKit kit = new HTMLEditorKit();
138 kit.setStyleSheet(buildStyleSheet());
139 help.setEditorKit(kit);
140 help.setEditable(false);
141 help.addHyperlinkListener(new HyperlinkHandler());
142 help.setContentType("text/html");
143 history = new HelpBrowserHistory(this);
144
145 JPanel p = new JPanel(new BorderLayout());
146 setContentPane(p);
147
148 p.add(spHelp = new JScrollPane(help), BorderLayout.CENTER);
149
150 addWindowListener(new WindowAdapter(){
151 @Override public void windowClosing(WindowEvent e) {
152 setVisible(false);
153 }
154 });
155
156 p.add(buildToolBar(), BorderLayout.NORTH);
157 help.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "Close");
158 help.getActionMap().put("Close", new AbstractAction(){
159 public void actionPerformed(ActionEvent e) {
160 setVisible(false);
161 }
162 });
163
164 setTitle(tr("JOSM Help Browser"));
165 }
166
167 public HelpBrowser() {
168 reader = new HelpContentReader(HelpUtil.getWikiBaseUrl());
169 build();
170 }
171
172 /**
173 * Replies the current URL
174 *
175 * @return the current URL
176 */
177
178
179 public String getUrl() {
180 return url;
181 }
182
183 /**
184 * Displays a warning page when a help topic doesn't exist yet.
185 *
186 * @param relativeHelpTopic the help topic
187 */
188 protected void handleMissingHelpContent(String relativeHelpTopic) {
189 String message = tr("<html><p class=\"warning-header\">Help content for help topic missing</p>"
190 + "<p class=\"warning-body\">Help content for the help topic <strong>{0}</strong> is "
191 + "not available yet. It is missing both in your local language ({1}) and in english.<br><br>"
192 + "Please help to improve the JOSM help system and fill in the missing information."
193 + "You can both edit the <a href=\"{2}\">help topic in your local language ({1})</a> and "
194 + "the <a href=\"{3}\">help topic in english</a>."
195 + "</p></html>",
196 relativeHelpTopic,
197 Locale.getDefault().getDisplayName(),
198 getHelpTopicEditUrl(buildAbsoluteHelpTopic(relativeHelpTopic)),
199 getHelpTopicEditUrl(buildAbsoluteHelpTopic(relativeHelpTopic, Locale.ENGLISH))
200 );
201 help.setText(message);
202 }
203
204 /**
205 * Displays a error page if a help topic couldn't be loaded because of network or IO error.
206 *
207 * @param relativeHelpTopic the help topic
208 * @param e the exception
209 */
210 protected void handleHelpContentReaderException(String relativeHelpTopic, HelpContentReaderException e) {
211 String message = tr("<html><p class=\"error-header\">Error when retrieving help information</p>"
212 + "<p class=\"error-body\">The content for the help topic <strong>{0}</strong> could "
213 + "not be loaded. The error message is (untranslated):<br>"
214 + "<tt>{1}</tt>"
215 + "</p></html>",
216 relativeHelpTopic,
217 e.toString()
218 );
219 help.setText(message);
220 }
221
222 protected void scrollToTop() {
223 JScrollBar sb = spHelp.getVerticalScrollBar();
224 sb.setValue(sb.getMinimum());
225 }
226
227 /**
228 * Loads a help topic given by a relative help topic name (i.e. "/Action/New")
229 *
230 * First tries to load the language specific help topic. If it is missing, tries to
231 * load the topic in english.
232 *
233 * @param relativeHelpTopic the relative help topic
234 */
235 protected void loadRelativeHelpTopic(String relativeHelpTopic) {
236 String url = HelpUtil.getHelpTopicUrl(HelpUtil.buildAbsoluteHelpTopic(relativeHelpTopic));
237 String content = null;
238 try {
239 content = reader.fetchHelpTopicContent(url);
240 } catch(MissingHelpContentException e) {
241 url = HelpUtil.getHelpTopicUrl(HelpUtil.buildAbsoluteHelpTopic(relativeHelpTopic, Locale.ENGLISH));
242 try {
243 logger.info("fetching url: " + url);
244 content = reader.fetchHelpTopicContent(url);
245 } catch(MissingHelpContentException e1) {
246 handleMissingHelpContent(relativeHelpTopic);
247 return;
248 } catch(HelpContentReaderException e1) {
249 e1.printStackTrace();
250 handleHelpContentReaderException(relativeHelpTopic,e1);
251 return;
252 }
253 } catch(HelpContentReaderException e) {
254 e.printStackTrace();
255 handleHelpContentReaderException(relativeHelpTopic, e);
256 return;
257 }
258 help.setText(content);
259 history.setCurrentUrl(url);
260 this.url = url;
261 scrollToTop();
262 }
263
264 /**
265 * Loads a help topic given by an absolute help topic name, i.e.
266 * "/De:Help/Action/New"
267 *
268 * @param absoluteHelpTopic the absolute help topic name
269 */
270 protected void loadAbsoluteHelpTopic(String absoluteHelpTopic) {
271 String url = HelpUtil.getHelpTopicUrl(absoluteHelpTopic);
272 String content = null;
273 try {
274 content = reader.fetchHelpTopicContent(url);
275 } catch(MissingHelpContentException e) {
276 handleMissingHelpContent(absoluteHelpTopic);
277 return;
278 } catch(HelpContentReaderException e) {
279 e.printStackTrace();
280 handleHelpContentReaderException(absoluteHelpTopic, e);
281 return;
282 }
283 help.setText(content);
284 history.setCurrentUrl(url);
285 this.url = url;
286 scrollToTop();
287 }
288
289 /**
290 * Opens an URL and displays the content.
291 *
292 * If the URL is the locator of an absolute help topic, help content is loaded from
293 * the JOSM wiki. Otherwise, the help browser loads the page from the given URL
294 *
295 * @param url the url
296 */
297 public void openUrl(String url) {
298 if (!isVisible()) {
299 setVisible(true);
300 toFront();
301 } else {
302 toFront();
303 }
304 String helpTopic = HelpUtil.extractAbsoluteHelpTopic(url);
305 if (helpTopic == null) {
306 try {
307 this.url = url;
308 help.setPage(url);
309 } catch(IOException e) {
310 HelpAwareOptionPane.showOptionDialog(
311 Main.parent,
312 tr(
313 "<html>Failed to open help page for url {0}.<br>"
314 + "This is most likely due to a network problem, please check your<br>"
315 + "your internet connection</html>",
316 url.toString()
317 ),
318 tr("Failed to open URL"),
319 JOptionPane.ERROR_MESSAGE,
320 null, /* no icon */
321 null, /* standard options, just OK button */
322 null, /* default is standard */
323 null /* no help context */
324 );
325 }
326 history.setCurrentUrl(url);
327 } else {
328 loadAbsoluteHelpTopic(helpTopic);
329 }
330 }
331
332 /**
333 * Loads and displays the help information for a help topic given
334 * by a relative help topic name, i.e. "/Action/New"
335 *
336 * @param relativeHelpTopic the relative help topic
337 */
338 public void openHelpTopic(String relativeHelpTopic) {
339 if (!isVisible()) {
340 setVisible(true);
341 toFront();
342 } else {
343 toFront();
344 }
345 loadRelativeHelpTopic(relativeHelpTopic);
346 }
347
348 class OpenInBrowserAction extends AbstractAction {
349 public OpenInBrowserAction() {
350 //putValue(NAME, tr("Open in Browser"));
351 putValue(SHORT_DESCRIPTION, tr("Open the current help page in an external browser"));
352 putValue(SMALL_ICON, ImageProvider.get("help", "internet"));
353 }
354
355 public void actionPerformed(ActionEvent e) {
356 OpenBrowser.displayUrl(getUrl());
357 }
358 }
359
360 class EditAction extends AbstractAction {
361 public EditAction() {
362 // putValue(NAME, tr("Edit"));
363 putValue(SHORT_DESCRIPTION, tr("Edit the current help page"));
364 putValue(SMALL_ICON,ImageProvider.get("dialogs", "edit"));
365 }
366
367 public void actionPerformed(ActionEvent e) {
368 if (!getUrl().startsWith(HelpUtil.getWikiBaseHelpUrl())) {
369 String message = tr(
370 "<html>The current URL <tt>{0}</tt><br>"
371 + "is an external URL. Editing is only possible for help topics<br>"
372 + "on the help server <tt>{1}</tt>.</html>",
373 getUrl(),
374 HelpUtil.getWikiBaseUrl()
375 );
376 JOptionPane.showMessageDialog(
377 Main.parent,
378 message,
379 tr("Warning"),
380 JOptionPane.WARNING_MESSAGE
381 );
382 return;
383 }
384 String url = getUrl();
385 url = url.replaceAll("#[^#]*$", "");
386 OpenBrowser.displayUrl(url+"?action=edit");
387 }
388 }
389
390 class ReloadAction extends AbstractAction {
391 public ReloadAction() {
392 //putValue(NAME, tr("Reload"));
393 putValue(SHORT_DESCRIPTION, tr("Reload the current help page"));
394 putValue(SMALL_ICON, ImageProvider.get("dialogs", "refresh"));
395 }
396
397 public void actionPerformed(ActionEvent e) {
398 openUrl(getUrl());
399 }
400 }
401
402 class BackAction extends AbstractAction implements Observer {
403 private HelpBrowserHistory history;
404 public BackAction(HelpBrowserHistory history) {
405 this.history = history;
406 history.addObserver(this);
407 //putValue(NAME, tr("Back"));
408 putValue(SHORT_DESCRIPTION, tr("Go to the previous page"));
409 putValue(SMALL_ICON, ImageProvider.get("help", "previous"));
410 setEnabled(history.canGoBack());
411 }
412
413 public void actionPerformed(ActionEvent e) {
414 history.back();
415 }
416 public void update(Observable o, Object arg) {
417 System.out.println("BackAction: canGoBoack=" + history.canGoBack() );
418 setEnabled(history.canGoBack());
419 }
420 }
421
422 class ForwardAction extends AbstractAction implements Observer {
423 private HelpBrowserHistory history;
424 public ForwardAction(HelpBrowserHistory history) {
425 this.history = history;
426 history.addObserver(this);
427 //putValue(NAME, tr("Forward"));
428 putValue(SHORT_DESCRIPTION, tr("Go to the next page"));
429 putValue(SMALL_ICON, ImageProvider.get("help", "next"));
430 setEnabled(history.canGoForward());
431 }
432
433 public void actionPerformed(ActionEvent e) {
434 history.forward();
435 }
436 public void update(Observable o, Object arg) {
437 setEnabled(history.canGoForward());
438 }
439 }
440
441 class HomeAction extends AbstractAction {
442 public HomeAction() {
443 //putValue(NAME, tr("Home"));
444 putValue(SHORT_DESCRIPTION, tr("Go to the JOSM help home page"));
445 putValue(SMALL_ICON, ImageProvider.get("help", "home"));
446 }
447
448 public void actionPerformed(ActionEvent e) {
449 openHelpTopic("/");
450 }
451 }
452
453 class HyperlinkHandler implements HyperlinkListener {
454
455 /**
456 * Scrolls the help browser to the element with id <code>id</code>
457 *
458 * @param id the id
459 * @return true, if an element with this id was found and scrolling was successful; false, otherwise
460 */
461 protected boolean scrollToElementWithId(String id) {
462 Document d = help.getDocument();
463 if (d instanceof HTMLDocument) {
464 HTMLDocument doc = (HTMLDocument) d;
465 Element element = doc.getElement(id);
466 try {
467 Rectangle r = help.modelToView(element.getStartOffset());
468 if (r != null) {
469 Rectangle vis = help.getVisibleRect();
470 r.height = vis.height;
471 help.scrollRectToVisible(r);
472 return true;
473 }
474 } catch(BadLocationException e) {
475 System.err.println(tr("Warning: bad location in HTML document. Exception was: " + e.toString()));
476 e.printStackTrace();
477 }
478 }
479 return false;
480 }
481
482 /**
483 * Checks whether the hyperlink event originated on a <a ...> element with
484 * a relative href consisting of a URL fragment only, i.e.
485 * <a href="#thisIsALocalFragment">. If so, replies the fragment, i.e.
486 * "thisIsALocalFragment".
487 *
488 * Otherwise, replies null
489 *
490 * @param e the hyperlink event
491 * @return the local fragment
492 */
493 protected String getUrlFragment(HyperlinkEvent e) {
494 AttributeSet set = e.getSourceElement().getAttributes();
495 Object value = set.getAttribute(Tag.A);
496 if (value == null || ! (value instanceof SimpleAttributeSet)) return null;
497 SimpleAttributeSet atts = (SimpleAttributeSet)value;
498 value = atts.getAttribute(javax.swing.text.html.HTML.Attribute.HREF);
499 if (value == null) return null;
500 String s = (String)value;
501 if (s.matches("#.*"))
502 return s.substring(1);
503 return null;
504 }
505
506 public void hyperlinkUpdate(HyperlinkEvent e) {
507 if (e.getEventType() != HyperlinkEvent.EventType.ACTIVATED)
508 return;
509 if (e.getURL() == null) {
510 // Probably hyperlink event on a an A-element with a href consisting of
511 // a fragment only, i.e. "#ALocalFragment".
512 //
513 String fragment = getUrlFragment(e);
514 if (fragment != null) {
515 // first try to scroll to an element with id==fragment. This is the way
516 // table of contents are built in the JOSM wiki. If this fails, try to
517 // scroll to a <A name="..."> element.
518 //
519 if (!scrollToElementWithId(fragment)) {
520 help.scrollToReference(fragment);
521 }
522 } else {
523 HelpAwareOptionPane.showOptionDialog(
524 Main.parent,
525 tr("Failed to open help page. The target URL is empty."),
526 tr("Failed to open help page"),
527 JOptionPane.ERROR_MESSAGE,
528 null, /* no icon */
529 null, /* standard options, just OK button */
530 null, /* default is standard */
531 null /* no help context */
532 );
533 }
534 } else if (e.getURL().toString().endsWith("action=edit")) {
535 OpenBrowser.displayUrl(e.getURL().toString());
536 } else {
537 url = e.getURL().toString();
538 openUrl(e.getURL().toString());
539 }
540 }
541 }
542}
Note: See TracBrowser for help on using the repository browser.