1   /**
2    * Distribution License:
3    * BibleDesktop is free software; you can redistribute it and/or modify it under
4    * the terms of the GNU General Public License, version 2 as published by
5    * the Free Software Foundation. This program is distributed in the hope
6    * that it will be useful, but WITHOUT ANY WARRANTY; without even the
7    * implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
8    * See the GNU General Public License for more details.
9    *
10   * The License is available on the internet at:
11   *       http://www.gnu.org/copyleft/gpl.html
12   * or by writing to:
13   *      Free Software Foundation, Inc.
14   *      59 Temple Place - Suite 330
15   *      Boston, MA 02111-1307, USA
16   *
17   * Copyright: 2005
18   *     The copyright to this program is held by it's authors.
19   *
20   * ID: $Id: TextPaneBookDataDisplay.java 2140 2011-04-03 02:07:01Z dmsmith $
21   */
22  package org.crosswire.bibledesktop.display.basic;
23  
24  import java.awt.Component;
25  import java.awt.Dimension;
26  import java.awt.event.MouseListener;
27  import java.beans.PropertyChangeEvent;
28  import java.net.MalformedURLException;
29  import java.net.URI;
30  import java.text.MessageFormat;
31  import java.util.Arrays;
32  import java.util.Locale;
33  
34  import javax.swing.JTextPane;
35  import javax.swing.event.EventListenerList;
36  import javax.swing.event.HyperlinkEvent;
37  import javax.swing.event.HyperlinkListener;
38  import javax.swing.text.Style;
39  import javax.swing.text.StyleConstants;
40  import javax.swing.text.StyledDocument;
41  import javax.xml.transform.TransformerException;
42  
43  import org.crosswire.bibledesktop.BDMsg;
44  import org.crosswire.bibledesktop.book.install.BookFont;
45  import org.crosswire.bibledesktop.desktop.XSLTProperty;
46  import org.crosswire.bibledesktop.display.BookDataDisplay;
47  import org.crosswire.bibledesktop.display.URIEvent;
48  import org.crosswire.bibledesktop.display.URIEventListener;
49  import org.crosswire.bibledesktop.passage.KeyChangeListener;
50  import org.crosswire.common.swing.AntiAliasedTextPane;
51  import org.crosswire.common.swing.GuiConvert;
52  import org.crosswire.common.swing.GuiUtil;
53  import org.crosswire.common.util.Logger;
54  import org.crosswire.common.util.Reporter;
55  import org.crosswire.common.xml.Converter;
56  import org.crosswire.common.xml.SAXEventProvider;
57  import org.crosswire.common.xml.TransformingSAXEventProvider;
58  import org.crosswire.common.xml.XMLUtil;
59  import org.crosswire.jsword.book.Book;
60  import org.crosswire.jsword.book.BookCategory;
61  import org.crosswire.jsword.book.BookData;
62  import org.crosswire.jsword.book.BookException;
63  import org.crosswire.jsword.book.BookMetaData;
64  import org.crosswire.jsword.passage.Key;
65  import org.crosswire.jsword.util.ConverterFactory;
66  import org.xml.sax.SAXException;
67  
68  /**
69   * A JTextPane implementation of an OSIS displayer.
70   * 
71   * @see gnu.gpl.License for license details.<br>
72   *      The copyright to this program is held by it's authors.
73   * @author Joe Walker [joe at eireneh dot com]
74   * @author DM Smith [dmsmith555 at yahoo dot com]
75   */
76  public class TextPaneBookDataDisplay implements BookDataDisplay, HyperlinkListener {
77  
78  
79  
80      /**
81       * Simple ctor
82       */
83      public TextPaneBookDataDisplay() {
84          converter = ConverterFactory.getConverter();
85          txtView = new AntiAliasedTextPane();
86          txtView.setEditable(false);
87          txtView.setEditorKit(new LazyHTMLEditorKit());
88          txtView.addHyperlinkListener(this);
89          style = txtView.addStyle(HYPERLINK_STYLE, null);
90          styledDoc = txtView.getStyledDocument();
91          lastStart = -1;
92          lastLength = -1;
93  
94          this.addURIEventListener(
95                  new ActiveURITip(txtView,
96                        new Dimension(400, 300)));
97      }
98  
99      /* (non-Javadoc)
100      * @see org.crosswire.bibledesktop.display.BookDataDisplay#clearBookData()
101      */
102     public void clearBookData() {
103         setBookData(null, null);
104     }
105 
106     /* (non-Javadoc)
107      * @see org.crosswire.bibledesktop.display.BookDataDisplay#setBookData(org.crosswire.jsword.book.Book[], org.crosswire.jsword.passage.Key)
108      */
109     public void setBookData(Book[] books, Key key) {
110         if (books == null || books.length == 0 || books[0] == null || key == null) {
111             bdata = null;
112         } else if (bdata == null || !Arrays.equals(books, bdata.getBooks()) || !key.equals(bdata.getKey())) {
113             bdata = new BookData(books, key, compareBooks);
114         }
115 
116         refresh();
117     }
118 
119     /* (non-Javadoc)
120      * @see org.crosswire.bibledesktop.display.BookDataDisplay#setCompareBooks(boolean)
121      */
122     public void setCompareBooks(boolean compare) {
123         compareBooks = compare;
124         if (bdata != null) {
125             bdata = new BookData(bdata.getBooks(), bdata.getKey(), compareBooks);
126             refresh();
127         }
128     }
129 
130     /* (non-Javadoc)
131      * @see org.crosswire.bibledesktop.display.BookDataDisplay#refresh()
132      */
133     public void refresh() {
134         if (bdata == null) {
135             txtView.setText("");
136             return;
137         }
138 
139         // Make sure Hebrew displays from Right to Left
140         BookMetaData bmd = getFirstBook().getBookMetaData();
141         if (bmd == null) {
142             txtView.setText("");
143             return;
144         }
145 
146         // The content of the module determines how the display
147         // should behave. It should not be the user's locale.
148         // Set the correct direction
149         boolean direction = bmd.isLeftToRight();
150         GuiUtil.applyOrientation(txtView, direction);
151         // Set the correct locale
152         txtView.setLocale(new Locale(bmd.getLanguage().getCode()));
153 
154         String fontSpec = GuiConvert.font2String(BookFont.instance().getFont(getFirstBook()));
155         try {
156             SAXEventProvider osissep = bdata.getSAXEventProvider();
157             TransformingSAXEventProvider htmlsep = (TransformingSAXEventProvider) converter.convert(osissep);
158 
159             XSLTProperty.DIRECTION.setState(direction ? "ltr" : "rtl");
160 
161             URI loc = bmd.getLocation();
162             XSLTProperty.BASE_URL.setState(loc == null ? "" : loc.getPath());
163 
164             if (bmd.getBookCategory() == BookCategory.BIBLE) {
165                 XSLTProperty.setProperties(htmlsep);
166             } else {
167                 XSLTProperty.CSS.setProperty(htmlsep);
168                 XSLTProperty.BASE_URL.setProperty(htmlsep);
169                 XSLTProperty.DIRECTION.setProperty(htmlsep);
170             }
171             // Override the default if needed
172             htmlsep.setParameter(XSLTProperty.FONT.getName(), fontSpec);
173 
174             String text = XMLUtil.writeToString(htmlsep);
175             txtView.setText(text);
176             txtView.select(0, 0);
177         } catch (SAXException e) {
178             Reporter.informUser(this, e);
179         } catch (BookException e) {
180             Reporter.informUser(this, e);
181         } catch (TransformerException e) {
182             Reporter.informUser(this, e);
183         }
184     }
185 
186     /* (non-Javadoc)
187      * @see javax.swing.event.HyperlinkListener#hyperlinkUpdate(javax.swing.event.HyperlinkEvent)
188      */
189     public void hyperlinkUpdate(HyperlinkEvent ev) {
190         // SPEEDUP(DMS): This needs to be optimized. It takes too much CPU
191         try {
192             HyperlinkEvent.EventType type = ev.getEventType();
193             JTextPane pane = (JTextPane) ev.getSource();
194 
195             String uri = ev.getDescription();
196             String[] parts = getParts(uri);
197             if (type == HyperlinkEvent.EventType.ACTIVATED) {
198                 // There are some errors which make an empty url
199                 if (parts[1].length() > 0) {
200                     if (parts[1].charAt(0) == '#') {
201                         log.debug(MessageFormat.format(SCROLL_TO_URI, uri));
202                         // This must be relative to the current document
203                         // in which case we assume that it is an in page
204                         // reference.
205                         // We ignore the frame case (example code within
206                         // JEditorPane
207                         // JavaDoc).
208                         // Remove the leading #
209                         uri = uri.substring(1);
210                         pane.scrollToReference(uri);
211                     } else {
212                         // Fully formed, so we hand it off to be processed
213                         fireActivateURI(new URIEvent(this, parts[0], parts[1]));
214                     }
215                 }
216             } else {
217                 // Must be either an enter or an exit event
218                 // simulate a link rollover effect, a CSS style not supported in
219                 // JDK 1.4
220 
221                 boolean isEnter = type == HyperlinkEvent.EventType.ENTERED;
222 
223                 int start = lastStart;
224                 int length = lastLength;
225                 if (isEnter) {
226                     javax.swing.text.Element textElement = ev.getSourceElement();
227                     start = textElement.getStartOffset();
228                     length = textElement.getEndOffset() - start;
229                     lastStart = start;
230                     lastLength = length;
231                 }
232 
233                 StyleConstants.setUnderline(style, isEnter);
234                 styledDoc.setCharacterAttributes(start, length, style, false);
235 
236                 if (isEnter) {
237                     fireEnterURI(new URIEvent(this, parts[0], parts[1]));
238                 } else {
239                     fireLeaveURI(new URIEvent(this, parts[0], parts[1]));
240                 }
241             }
242         } catch (MalformedURLException ex) {
243             Reporter.informUser(this, ex);
244         }
245     }
246 
247     /* (non-Javadoc)
248      * @see java.beans.PropertyChangeListener#propertyChange(java.beans.PropertyChangeEvent)
249      */
250     public void propertyChange(PropertyChangeEvent evt) {
251         if (evt.getPropertyName().equals(BookDataDisplay.COMPARE_BOOKS)) {
252             setCompareBooks(Boolean.valueOf(evt.getNewValue().toString()).booleanValue());
253         }
254     }
255 
256     private String[] getParts(String reference) throws MalformedURLException {
257         String protocol = RELATIVE_URI_PROTOCOL;
258         String data = reference;
259         int match = data.indexOf(':');
260         if (match == -1) {
261             // So there is no protocol, this must be relative to the current
262             // in which case we assume that it is an in page reference.
263             // We ignore the frame case (example code within JEditorPane
264             // JavaDoc).
265             if (data.charAt(0) != '#') {
266                 // TRANSLATOR: Unexpected error condition: the cross reference was bad.
267                 // {0} is a placeholder for the bad URL.
268                 throw new MalformedURLException(BDMsg.gettext("Missing : in {0}", data));
269             }
270         } else {
271             protocol = data.substring(0, match);
272             data = data.substring(match + 1);
273         }
274 
275         if (data.startsWith(DOUBLE_SLASH)) {
276             data = data.substring(2);
277         }
278 
279         return new String[] {
280                 protocol, data
281         };
282     }
283 
284     /* (non-Javadoc)
285      * @see org.crosswire.bibledesktop.display.BookDataDisplay#getComponent()
286      */
287     public Component getComponent() {
288         return txtView;
289     }
290 
291     /* (non-Javadoc)
292      * @see org.crosswire.bibledesktop.display.BookDataDisplay#copy()
293      */
294     public void copy() {
295         txtView.copy();
296     }
297 
298     /* (non-Javadoc)
299      * @see org.crosswire.bibledesktop.display.BookDataDisplay#addKeyChangeListener(org.crosswire.bibledesktop.passage.KeyChangeListener)
300      */
301     public synchronized void addKeyChangeListener(KeyChangeListener listener) {
302         listenerList.add(KeyChangeListener.class, listener);
303     }
304 
305     /* (non-Javadoc)
306      * @see org.crosswire.bibledesktop.display.BookDataDisplay#removeKeyChangeListener(org.crosswire.bibledesktop.passage.KeyChangeListener)
307      */
308     public synchronized void removeKeyChangeListener(KeyChangeListener listener) {
309         listenerList.remove(KeyChangeListener.class, listener);
310     }
311 
312     /* (non-Javadoc)
313      * @see org.crosswire.bibledesktop.display.BookDataDisplay#addURIEventListener(org.crosswire.bibledesktop.display.URIEventListener)
314      */
315     public synchronized void addURIEventListener(URIEventListener listener) {
316         listenerList.add(URIEventListener.class, listener);
317     }
318 
319     /* (non-Javadoc)
320      * @see org.crosswire.bibledesktop.display.BookDataDisplay#removeURIEventListener(org.crosswire.bibledesktop.display.URIEventListener)
321      */
322     public synchronized void removeURIEventListener(URIEventListener listener) {
323         listenerList.remove(URIEventListener.class, listener);
324     }
325 
326     /**
327      * Notify the listeners that the hyperlink (URI) has been activated.
328      * 
329      * @param e
330      *            the event
331      * @see EventListenerList
332      */
333     public void fireActivateURI(URIEvent e) {
334         // Guaranteed to return a non-null array
335         Object[] listeners = listenerList.getListenerList();
336         // Process the listeners last to first, notifying
337         // those that are interested in this event
338         for (int i = listeners.length - 2; i >= 0; i -= 2) {
339             if (listeners[i] == URIEventListener.class) {
340                 ((URIEventListener) listeners[i + 1]).activateURI(e);
341             }
342         }
343     }
344 
345     /**
346      * Notify the listeners that the hyperlink (URI) has been entered.
347      * 
348      * @param e
349      *            the event
350      * @see EventListenerList
351      */
352     public void fireEnterURI(URIEvent e) {
353         // Guaranteed to return a non-null array
354         Object[] listeners = listenerList.getListenerList();
355         // Process the listeners last to first, notifying
356         // those that are interested in this event
357         for (int i = listeners.length - 2; i >= 0; i -= 2) {
358             if (listeners[i] == URIEventListener.class) {
359                 ((URIEventListener) listeners[i + 1]).enterURI(e);
360             }
361         }
362     }
363 
364     /**
365      * Notify the listeners that the hyperlink (URI) has been left.
366      * 
367      * @param e
368      *            the event
369      * @see EventListenerList
370      */
371     public void fireLeaveURI(URIEvent e) {
372         // Guaranteed to return a non-null array
373         Object[] listeners = listenerList.getListenerList();
374         // Process the listeners last to first, notifying
375         // those that are interested in this event
376         for (int i = listeners.length - 2; i >= 0; i -= 2) {
377             if (listeners[i] == URIEventListener.class) {
378                 ((URIEventListener) listeners[i + 1]).leaveURI(e);
379             }
380         }
381     }
382 
383     /**
384      * Forward the mouse listener to our child components
385      */
386     public void removeMouseListener(MouseListener li) {
387         txtView.removeMouseListener(li);
388     }
389 
390     /**
391      * Forward the mouse listener to our child components
392      */
393     public void addMouseListener(MouseListener li) {
394         txtView.addMouseListener(li);
395     }
396 
397     /* (non-Javadoc)
398      * @see org.crosswire.bibledesktop.display.BookDataDisplay#getKey()
399      */
400     public Key getKey() {
401         return bdata == null ? null : bdata.getKey();
402     }
403 
404     /* (non-Javadoc)
405      * @see org.crosswire.bibledesktop.display.BookDataDisplay#getBook()
406      */
407     public Book[] getBooks() {
408         return bdata.getBooks();
409     }
410 
411     /* (non-Javadoc)
412      * @see org.crosswire.bibledesktop.display.BookDataDisplay#getFirstBook()
413      */
414     public Book getFirstBook() {
415         return bdata.getFirstBook();
416     }
417 
418     // Strings for hyperlinks
419     private static final String HYPERLINK_STYLE = "Hyperlink";
420     private static final String DOUBLE_SLASH = "//";
421     private static final String SCROLL_TO_URI = "scrolling to: {0}";
422     private static final String RELATIVE_URI_PROTOCOL = "";
423 
424     /**
425      * The log stream
426      */
427     protected static final Logger log = Logger.getLogger(TextPaneBookDataDisplay.class);
428 
429     /**
430      * The book data being shown.
431      */
432     private BookData bdata;
433 
434     /**
435      * Whether the books should be compared.
436      */
437     private boolean compareBooks;
438 
439     /**
440      * To convert OSIS to HTML
441      */
442     private Converter converter;
443 
444     /**
445      * The display component
446      */
447     private JTextPane txtView;
448 
449     /**
450      * A sytle used to underline a hyperlink
451      */
452     private Style style;
453 
454     /**
455      * location of last enter event
456      */
457     private int lastStart;
458 
459     /**
460      * length of last enter event
461      */
462     private int lastLength;
463 
464     /**
465      * The styled document of the JTextPane.
466      */
467     private StyledDocument styledDoc;
468 
469     /**
470      * The listeners for handling URIs
471      */
472     private EventListenerList listenerList = new EventListenerList();
473 
474 }
475