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: 2007
18   *     The copyright to this program is held by it's authors.
19   *
20   * ID: $Id: org.eclipse.jdt.ui.prefs 1178 2006-11-06 12:48:02Z dmsmith $
21   */
22  
23  package org.crosswire.bibledesktop.display.basic;
24  
25  import java.awt.Color;
26  import java.awt.Component;
27  import java.awt.Dimension;
28  import java.awt.Point;
29  import java.awt.Toolkit;
30  import java.awt.event.ActionEvent;
31  import java.awt.event.ActionListener;
32  import java.awt.event.MouseAdapter;
33  import java.awt.event.MouseEvent;
34  import java.net.URI;
35  import java.util.Locale;
36  
37  import javax.swing.JScrollPane;
38  import javax.swing.JTextPane;
39  import javax.swing.Popup;
40  import javax.swing.PopupFactory;
41  import javax.swing.Timer;
42  import javax.swing.border.TitledBorder;
43  import javax.swing.text.html.HTMLEditorKit;
44  import javax.xml.transform.TransformerException;
45  
46  import org.crosswire.bibledesktop.book.install.BookFont;
47  import org.crosswire.bibledesktop.desktop.Desktop;
48  import org.crosswire.bibledesktop.desktop.XSLTProperty;
49  import org.crosswire.bibledesktop.display.URIEvent;
50  import org.crosswire.bibledesktop.display.URIEventListener;
51  import org.crosswire.common.swing.AntiAliasedTextPane;
52  import org.crosswire.common.swing.GuiConvert;
53  import org.crosswire.common.swing.GuiUtil;
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.book.Books;
65  import org.crosswire.jsword.book.Defaults;
66  import org.crosswire.jsword.passage.NoSuchKeyException;
67  import org.crosswire.jsword.util.ConverterFactory;
68  import org.xml.sax.SAXException;
69  
70  /**
71   * How it works:
72   * 1. It keeps track of mouse movement.
73   * 2. It registers the URI entering event and starts a timer.
74   * 3. When the timer fires, a tip pops up, the timer stopped.
75   * 4. The timer is stopped when a URI leaving event is received.
76   * 5. Whenever mouse clicked outside the tip, it disappears.
77   * 6. Click in the tip to make it sticky, double-click to close. 
78   * 7. If sticky, tip updates as the mouse enters another URI. 
79   *    
80   * @see gnu.gpl.License for license details.
81   *      The copyright to this program is held by it's authors.
82   * @author Yingjie Lan [lanyjie at yahoo dot com]
83   */
84  public class URITipMgr extends MouseAdapter implements ActionListener, URIEventListener {
85  
86      private Component owner;
87      private JTextPane txtView;
88      private JScrollPane scrView;
89      private TitledBorder title;
90      private Popup popup;
91      private boolean sticky;
92  
93      private int lastx;
94      private int lasty;
95      private URIEvent event;
96      private Timer timer;
97  
98      private Converter converter;
99  
100     public URITipMgr(Component own, Dimension dim) {
101         converter = ConverterFactory.getConverter();
102         owner = own;
103         txtView = new AntiAliasedTextPane();
104         txtView.setEditable(false);
105         txtView.setEditorKit(new HTMLEditorKit());
106         title = new TitledBorder((String) null);
107         scrView = new JScrollPane(txtView);
108         scrView.setBackground(Color.yellow);
109         scrView.setBorder(title);
110         scrView.getViewport().setPreferredSize(dim);
111         // listen to mouse events of the managed component
112         own.addMouseMotionListener(this);
113         own.addMouseListener(this);
114         // also hide popup if clicked inside popup
115         txtView.addMouseListener(this);
116         setDelay(1500);
117     }
118 
119     public void setDelay(int delay) {
120         if (timer != null) {
121             timer.stop();
122         }
123         timer = new Timer(delay, this);
124     }
125 
126     public void updateText() {
127         if (event == null) {
128             return;
129         }
130         String txt = null;
131         String protocol = event.getScheme();
132         Book book = null;
133         if (protocol.equals(Desktop.GREEK_DEF_PROTOCOL)) {
134             book = Defaults.getGreekDefinitions();
135             title.setTitle("Greek Definition");
136         } else if (protocol.equals(Desktop.HEBREW_DEF_PROTOCOL)) {
137             book = Defaults.getHebrewDefinitions();
138             title.setTitle("Hebrew Definition");
139         } else if (protocol.equals(Desktop.GREEK_MORPH_PROTOCOL)) {
140             book = Defaults.getGreekParse();
141             title.setTitle("Greek Morphology");
142         } else if (protocol.equals(Desktop.HEBREW_MORPH_PROTOCOL)) {
143             book = Defaults.getHebrewParse();
144             title.setTitle("Hebrew Morphology");
145         }
146 
147         if (book == null || Books.installed().getBook(book.getName()) == null) {
148             txtView.setText("Book Unavailable!");
149             title.setTitle("Exception");
150             return;
151         }
152 
153         BookData bdata = null;
154 
155         try {
156             bdata = new BookData(book, book.getKey(event.getURI()));
157         } catch (NoSuchKeyException ex) {
158             txtView.setText(ex.getDetailedMessage());
159             title.setTitle("Exception");
160             return;
161         }
162 
163         assert book == bdata.getFirstBook();
164 
165         BookMetaData bmd = book.getBookMetaData();
166         if (bmd == null) {
167             txtView.setText("Book Meta Data Unavailable!");
168             title.setTitle("Exception");
169             return;
170         }
171 
172         // Make sure Hebrew displays from Right to Left
173         boolean direction = bmd.isLeftToRight();
174         String fontSpec = GuiConvert.font2String(BookFont.instance().getFont(book));
175         try {
176             SAXEventProvider osissep = bdata.getSAXEventProvider();
177             TransformingSAXEventProvider htmlsep = (TransformingSAXEventProvider) converter.convert(osissep);
178             XSLTProperty.DIRECTION.setState(direction ? "ltr" : "rtl");
179 
180             URI loc = bmd.getLocation();
181             XSLTProperty.BASE_URL.setState(loc == null ? "" : loc.getPath());
182 
183             if (bmd.getBookCategory() == BookCategory.BIBLE) {
184                 XSLTProperty.setProperties(htmlsep);
185             } else {
186                 XSLTProperty.CSS.setProperty(htmlsep);
187                 XSLTProperty.BASE_URL.setProperty(htmlsep);
188                 XSLTProperty.DIRECTION.setProperty(htmlsep);
189             }
190 
191             // Override the default if needed
192             htmlsep.setParameter(XSLTProperty.FONT.getName(), fontSpec);
193 
194             txt = XMLUtil.writeToString(htmlsep);
195         } catch (SAXException e) {
196             Reporter.informUser(this, e);
197             e.printStackTrace();
198             txtView.setText(e.getMessage());
199             title.setTitle("Exception");
200         } catch (TransformerException e) {
201             Reporter.informUser(this, e);
202             e.printStackTrace();
203             txtView.setText(e.getMessage());
204             title.setTitle("Exception");
205         } catch (BookException e) {
206             Reporter.informUser(this, e);
207             e.printStackTrace();
208             txtView.setText(e.getMessage());
209             title.setTitle("Exception");
210         }
211 
212         // Set the correct direction
213         GuiUtil.applyOrientation(txtView, direction);
214 
215         // The content of the module determines how the display
216         // should behave. It should not be the user's locale.
217         // Set the correct locale
218         txtView.setLocale(new Locale(bmd.getLanguage().getCode()));
219         txtView.setText(txt);
220         txtView.setCaretPosition(0);
221     }
222 
223     void showTip() {
224         // when it is time to do so
225         updateText();
226         Point p = owner.getLocationOnScreen();
227         Dimension d = scrView.getPreferredSize();
228         Dimension s = Toolkit.getDefaultToolkit().getScreenSize();
229         int x = p.x + lastx;
230         int y = p.y + lasty;
231         int horizDist = 30; // these numbers are hard coded
232         int vertiDist = 10; // but they depend on font size.
233         // always show tips in the 'better half'
234         if (x + x > s.width) {
235             x -= horizDist + d.width;
236         } else {
237             x += horizDist;
238         }
239         if (y + y > s.height) {
240             y -= vertiDist + d.height;
241         } else {
242             y += vertiDist;
243         }
244 
245         popup = PopupFactory.getSharedInstance().getPopup(owner, scrView, x, y);
246         popup.show();
247     }
248 
249     void hideTip(boolean stick) {
250         if (popup != null) {
251             popup.hide();
252         }
253         popup = null;
254         sticky = stick;
255     }
256 
257     /**
258      * @param ev if we are interested in this event 
259      */
260     boolean interested(URIEvent ev) {
261        //tell if it is interested in ev
262         String protocol = ev.getScheme();
263         if (protocol.equals(Desktop.GREEK_DEF_PROTOCOL)) {
264             return true;
265         }
266         if (protocol.equals(Desktop.HEBREW_DEF_PROTOCOL)) {
267             return true;
268         }
269         if (protocol.equals(Desktop.GREEK_MORPH_PROTOCOL)) {
270             return true;
271         }
272         if (protocol.equals(Desktop.HEBREW_MORPH_PROTOCOL)) {
273             return true;
274         }
275         return false;
276     }
277 
278     /* (non-Javadoc)
279      * @see org.crosswire.bibledesktop.display.URIEventListener#activateURI(org.crosswire.bibledesktop.display.URIEvent)
280      */
281     public void activateURI(URIEvent ev) {
282     }
283 
284     /* (non-Javadoc)
285      * @see org.crosswire.bibledesktop.display.URIEventListener#enterURI(org.crosswire.bibledesktop.display.URIEvent)
286      */
287     public void enterURI(URIEvent ev) {
288         if (!interested(ev)) {
289             return;
290         }
291         System.out.println("entering" + ev.getURI());
292         event = ev;
293         if (sticky) {
294             hideTip(true); // remain sticky
295             showTip(); // new tip immediately
296         } else {
297             timer.start();
298         }
299     }
300 
301     /* (non-Javadoc)
302      * @see org.crosswire.bibledesktop.display.URIEventListener#leaveURI(org.crosswire.bibledesktop.display.URIEvent)
303      */
304     public void leaveURI(URIEvent ev) {
305         if (!interested(ev)) {
306             return;
307         }
308         System.out.println("leaving" + ev.getURI());
309         timer.stop();
310     }
311 
312     /* (non-Javadoc)
313      * @see java.awt.event.ActionListener#actionPerformed(java.awt.event.ActionEvent)
314      */
315     public void actionPerformed(ActionEvent e) {
316         //A timer fires, time to show the tip.
317         //A tip might have been shown
318         hideTip(false); //unset sticky
319         showTip();
320         timer.stop();
321     }
322 
323     /* (non-Javadoc)
324      * @see java.awt.event.MouseMotionListener#mouseDragged(java.awt.event.MouseEvent)
325      */
326     @Override
327     public void mouseDragged(MouseEvent e) {
328     }
329 
330     /* (non-Javadoc)
331      * @see java.awt.event.MouseMotionListener#mouseMoved(java.awt.event.MouseEvent)
332      */
333     @Override
334     public void mouseMoved(MouseEvent e) {
335         lastx = e.getX();
336         lasty = e.getY();
337     }
338 
339     @Override
340     public void mouseClicked(MouseEvent e) {
341         // System.out.println(e);
342         if (e.getSource() == txtView && e.getClickCount() == 1) {
343             sticky = !sticky;
344         } else {
345             hideTip(false); // no longer sticky
346         }
347     }
348 
349     @Override
350     public void mouseExited(MouseEvent e) {
351         // this is really a 'bug' for HTMLEditorKit:
352         // when the component having the URI is exited,
353         // yet it is not leaving the URI! We fix it here:
354         if (e.getSource() == owner) {
355             timer.stop();
356         }
357     }
358 }
359