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: TabbedBookDataDisplay.java 2091 2011-03-07 04:15:31Z dmsmith $
21   */
22  package org.crosswire.bibledesktop.display.basic;
23  
24  import java.awt.BorderLayout;
25  import java.awt.Component;
26  import java.beans.PropertyChangeEvent;
27  import java.util.ArrayList;
28  import java.util.HashMap;
29  import java.util.List;
30  import java.util.Map;
31  
32  import javax.swing.JPanel;
33  import javax.swing.JScrollPane;
34  import javax.swing.JTabbedPane;
35  import javax.swing.SwingConstants;
36  import javax.swing.event.ChangeEvent;
37  import javax.swing.event.ChangeListener;
38  
39  import org.crosswire.bibledesktop.BDMsg;
40  import org.crosswire.bibledesktop.display.BookDataDisplay;
41  import org.crosswire.bibledesktop.display.BookDataDisplayFactory;
42  import org.crosswire.bibledesktop.display.URIEventListener;
43  import org.crosswire.bibledesktop.passage.KeyChangeListener;
44  import org.crosswire.common.swing.CWScrollPane;
45  import org.crosswire.common.swing.GuiUtil;
46  import org.crosswire.jsword.book.Book;
47  import org.crosswire.jsword.passage.Key;
48  import org.crosswire.jsword.passage.KeyUtil;
49  import org.crosswire.jsword.passage.Passage;
50  
51  /**
52   * An inner component of Passage pane that can't show the list.
53   * <p>
54   * At some stage we should convert this code to remove Passage so it will work
55   * with all Books and not just Bibles. Code is included (commented out) on how
56   * this could be done.
57   * 
58   * @see gnu.gpl.License for license details.<br>
59   *      The copyright to this program is held by it's authors.
60   * @author Joe Walker [joe at eireneh dot com]
61   */
62  public class TabbedBookDataDisplay implements BookDataDisplay {
63      /**
64       * Simple Constructor
65       */
66      public TabbedBookDataDisplay() {
67          views = new HashMap<JScrollPane, BookDataDisplay>();
68          displays = new ArrayList<BookDataDisplay>();
69          pnlMore = new JPanel();
70          pnlMain = new JPanel();
71  
72          bookDataDisplay = createInnerDisplayPane();
73  
74          tabMain = new JTabbedPane();
75          tabMain.setTabPlacement(SwingConstants.BOTTOM);
76          tabMain.addChangeListener(new ChangeListener() {
77              public void stateChanged(ChangeEvent ev) {
78                  tabChanged();
79              }
80          });
81  
82          pnlMain.setLayout(new BorderLayout());
83  
84          center = bookDataDisplay.getComponent();
85          scrMain = new CWScrollPane(center);
86          pnlMain.add(scrMain, BorderLayout.CENTER);
87      }
88  
89      /* (non-Javadoc)
90       * @see org.crosswire.bibledesktop.display.BookDataDisplay#getComponent()
91       */
92      public Component getComponent() {
93          return pnlMain;
94      }
95  
96      /* (non-Javadoc)
97       * @see org.crosswire.bibledesktop.display.BookDataDisplay#clearBookData()
98       */
99      public void clearBookData() {
100         setBookData(null, null);
101     }
102 
103     /* (non-Javadoc)
104      * @see org.crosswire.bibledesktop.display.BookDataDisplay#setBookData(org.crosswire.jsword.book.Book[], org.crosswire.jsword.passage.Key)
105      */
106     public void setBookData(Book[] books, Key newkey) {
107         this.books = books == null ? null : (Book[]) books.clone();
108         this.key = KeyUtil.getPassage(newkey);
109 
110         // Tabbed view or not we should clear out the old tabs
111         tabMain.removeAll();
112         views.clear();
113         displays.clear();
114         displays.add(bookDataDisplay);
115 
116         // So use purely Keys and not Passage, create a utility to cut up
117         // a key into a number of keys.
118         // private Key keys;
119         // private Passage waiting;
120         // ...
121         // keys = null; // OSISUtil.pagenate(key, pagesize * 10);
122         // tabs = (keys.size() > 1);
123         // And then inside the if:
124         // Key first = (Key) keys.get(0);
125         // in place of the first/waiting code.
126         // Then down in tabChanged()
127         // // What do we display next
128         // int countTabs = tabMain.getTabCount();
129         // Key next = (Key) keys.get(countTabs);
130         // And a bit lower:
131         // // Do we need a new more tab
132         // if (countTabs >= keys.size())
133 
134         // Do we need a tabbed view
135         tabs = key != null && key.countVerses() > pageSize;
136         if (tabs) {
137             // Calc the verses to display in this tab
138             Passage first = (Passage) key.clone();
139             waiting = first.trimVerses(pageSize);
140 
141             // Create the first tab
142             BookDataDisplay pnlNew = createInnerDisplayPane();
143             pnlNew.setBookData(books, first);
144 
145             Component display = pnlNew.getComponent();
146             JScrollPane scrView = new CWScrollPane(display);
147             views.put(scrView, pnlNew);
148 
149             tabMain.add(getTabName(first), scrView);
150             // TRANSLATOR: Extra bottom tabs are created when there is too much to display in one.
151             // Rather than figuring out how many tabs there should be, we label one "More..."
152             // When the user clicks on it, it is filled with what remains. And if it is filled
153             // to overflowing, another "More..." tab is created.
154             tabMain.add(BDMsg.gettext("More ..."), pnlMore);
155 
156             setCenterComponent(tabMain);
157         } else {
158             bookDataDisplay.setBookData(books, key);
159 
160             setCenterComponent(bookDataDisplay.getComponent());
161         }
162 
163         // Since we changed the contents of the page we need to cause it to
164         // repaint
165         GuiUtil.refresh(pnlMain);
166     }
167 
168     /* (non-Javadoc)
169      * @see org.crosswire.bibledesktop.display.BookDataDisplay#setCompareBooks(boolean)
170      */
171     public void setCompareBooks(boolean compare) {
172         // Now go through all the known tabs and refresh each
173         for (BookDataDisplay bdd : displays) {
174             bdd.setCompareBooks(compare);
175         }
176     }
177 
178     /* (non-Javadoc)
179      * @see org.crosswire.bibledesktop.display.BookDataDisplay#refresh()
180      */
181     public void refresh() {
182         // Now go through all the known tabs and refresh each
183         for (BookDataDisplay bdd : displays) {
184             bdd.refresh();
185         }
186     }
187 
188     /* (non-Javadoc)
189      * @see org.crosswire.bibledesktop.display.BookDataDisplay#getKey()
190      */
191     public Key getKey() {
192         return key;
193     }
194 
195     /* (non-Javadoc)
196      * @see org.crosswire.bibledesktop.display.BookDataDisplay#getBook()
197      */
198     public Book[] getBooks() {
199         return books == null ? null : (Book[]) books.clone();
200     }
201 
202     /* (non-Javadoc)
203      * @see org.crosswire.bibledesktop.display.BookDataDisplay#getFirstBook()
204      */
205     public Book getFirstBook() {
206         return books != null && books.length > 0 ? books[0] : null;
207     }
208 
209     /* (non-Javadoc)
210      * @see org.crosswire.bibledesktop.display.BookDataDisplay#copy()
211      */
212     public void copy() {
213         getInnerDisplayPane().copy();
214     }
215 
216     /* (non-Javadoc)
217      * @see org.crosswire.bibledesktop.display.BookDataDisplay#addKeyChangeListener(org.crosswire.bibledesktop.passage.KeyChangeListener)
218      */
219     public void addKeyChangeListener(KeyChangeListener listener) {
220         // First add to our list of listeners so when we add a new tab
221         // we can add this new listener to the new tab
222         List<KeyChangeListener> temp = new ArrayList<KeyChangeListener>();
223         if (keyEventListeners == null) {
224             temp.add(listener);
225             keyEventListeners = temp;
226         } else {
227             temp.addAll(keyEventListeners);
228 
229             if (!temp.contains(listener)) {
230                 temp.add(listener);
231                 keyEventListeners = temp;
232             }
233         }
234 
235         for (BookDataDisplay bdd : displays) {
236             bdd.addKeyChangeListener(listener);
237         }
238     }
239 
240     /* (non-Javadoc)
241      * @see org.crosswire.bibledesktop.display.BookDataDisplay#removeKeyChangeListener(org.crosswire.bibledesktop.passage.KeyChangeListener)
242      */
243     public void removeKeyChangeListener(KeyChangeListener listener) {
244         // First remove from the list of listeners
245         if (keyEventListeners != null && keyEventListeners.contains(listener)) {
246             List<KeyChangeListener> temp = new ArrayList<KeyChangeListener>();
247             temp.addAll(keyEventListeners);
248             temp.remove(listener);
249             keyEventListeners = temp;
250         }
251 
252         for (BookDataDisplay bdd : displays) {
253             bdd.removeKeyChangeListener(listener);
254         }
255     }
256 
257     /* (non-Javadoc)
258      * @see java.beans.PropertyChangeListener#propertyChange(java.beans.PropertyChangeEvent)
259      */
260     public void propertyChange(PropertyChangeEvent evt) {
261         // Now go through all the known syncs and add this one in
262         for (BookDataDisplay bdd : displays) {
263             bdd.propertyChange(evt);
264         }
265     }
266 
267     /* (non-Javadoc)
268      * @see org.crosswire.bibledesktop.display.BookDataDisplay#addURIEventListener(org.crosswire.bibledesktop.display.URIEventListener)
269      */
270     public synchronized void addURIEventListener(URIEventListener listener) {
271         // First add to our list of listeners so when we add a new tab
272         // we can add this new listener to the new tab
273         List<URIEventListener> temp = new ArrayList<URIEventListener>();
274         if (uriEventListeners == null) {
275             temp.add(listener);
276             uriEventListeners = temp;
277         } else {
278             temp.addAll(uriEventListeners);
279 
280             if (!temp.contains(listener)) {
281                 temp.add(listener);
282                 uriEventListeners = temp;
283             }
284         }
285 
286         // Now go through all the known syncs and add this one in
287         for (BookDataDisplay bdd : displays) {
288             bdd.addURIEventListener(listener);
289         }
290     }
291 
292     /* (non-Javadoc)
293      * @see org.crosswire.bibledesktop.display.BookDataDisplay#removeURIEventListener(org.crosswire.bibledesktop.display.URIEventListener)
294      */
295     public synchronized void removeURIEventListener(URIEventListener listener) {
296         // First remove from the list of listeners
297         if (uriEventListeners != null && uriEventListeners.contains(listener)) {
298             List<URIEventListener> temp = new ArrayList<URIEventListener>();
299             temp.addAll(uriEventListeners);
300             temp.remove(listener);
301             uriEventListeners = temp;
302         }
303 
304         // Now remove from all the known syncs
305         for (BookDataDisplay bdd : displays) {
306             bdd.removeURIEventListener(listener);
307         }
308     }
309 
310     /**
311      * Make a new component reside in the center of this panel
312      */
313     private void setCenterComponent(Component comp) {
314         // We are currently viewing either a set of tabs (tabMain)
315         // or a single page (bookDataDisplay.getComponent()).
316         // The new center component is either tabMain
317         // or something that should be wrapped with a scroller.
318         // 
319         // So when we go from tabMain, we need to remove center
320         // and when we go from a single page we need to remove the scroller
321         //
322 
323         if (center != comp) {
324             pnlMain.removeAll();
325             center = comp;
326             if (center == tabMain) {
327                 pnlMain.add(tabMain, BorderLayout.CENTER);
328             } else {
329                 pnlMain.add(scrMain, BorderLayout.CENTER);
330                 // scrMain.setViewportView(center);
331             }
332         }
333     }
334 
335     /**
336      * Tabs changed, generate some stuff
337      */
338     /*private*/final void tabChanged() {
339         // This is someone clicking on more isnt it?
340         if (tabMain.getSelectedComponent() != pnlMore) {
341             return;
342         }
343 
344         // First remove the old more ... tab that the user has just selected
345         tabMain.remove(pnlMore);
346 
347         // What do we display next
348         Passage next = waiting;
349         waiting = next.trimVerses(pageSize);
350 
351         // Create a new tab
352         BookDataDisplay pnlNew = createInnerDisplayPane();
353         pnlNew.setBookData(books, next);
354 
355         JScrollPane scrView = new CWScrollPane(pnlNew.getComponent());
356         views.put(scrView, pnlNew);
357 
358         tabMain.add(getTabName(next), scrView);
359 
360         // Do we need a new more tab
361         if (waiting != null) {
362             // TRANSLATOR: Extra bottom tabs are created when there is too much to display in one.
363             // Rather than figuring out how many tabs there should be, we label one "More..."
364             // When the user clicks on it, it is filled with what remains. And if it is filled
365             // to overflowing, another "More..." tab is created.
366             tabMain.add(BDMsg.gettext("More ..."), pnlMore);
367         }
368 
369         // Select the real new tab in place of any more tabs
370         tabMain.setSelectedComponent(scrView);
371     }
372 
373     /**
374      * Accessor for the current TextComponent
375      */
376     public BookDataDisplay getInnerDisplayPane() {
377         if (tabs) {
378             Object o = tabMain.getSelectedComponent();
379             JScrollPane sp = (JScrollPane) o;
380             return views.get(sp);
381         }
382         return bookDataDisplay;
383     }
384 
385     /**
386      * Tab creation helper
387      */
388     private synchronized BookDataDisplay createInnerDisplayPane() {
389         BookDataDisplay display = BookDataDisplayFactory.createBookDataDisplay();
390         displays.add(display);
391 
392         // Add all the known listeners to this new BookDataDisplay
393         if (uriEventListeners != null) {
394             for (URIEventListener li : uriEventListeners) {
395                 display.addURIEventListener(li);
396             }
397         }
398 
399         // Add all the known listeners to this new BookDataDisplay
400         if (keyEventListeners != null) {
401             for (KeyChangeListener li : keyEventListeners) {
402                 display.addKeyChangeListener(li);
403             }
404         }
405 
406         return display;
407     }
408 
409     /**
410      * Accessor for the page size
411      */
412     public static void setPageSize(int pageSize) {
413         TabbedBookDataDisplay.pageSize = pageSize;
414     }
415 
416     /**
417      * Accessor for the page size
418      */
419     public static int getPageSize() {
420         return pageSize;
421     }
422 
423     /**
424      * Ensure that the tab names are not too long - 25 chars max
425      * 
426      * @param key
427      *            The key to get a short name from
428      * @return The first 9 chars followed by ... followed by the last 9
429      */
430     private static String getTabName(Key key) {
431         String tabname = key.getName();
432         int len = tabname.length();
433         if (len > TITLE_LENGTH) {
434             tabname = tabname.substring(0, 9) + " ... " + tabname.substring(len - 9, len);
435         }
436 
437         return tabname;
438     }
439 
440     /**
441      * What is the max length for a tab title
442      */
443     private static final int TITLE_LENGTH = 25;
444 
445     /**
446      * How many verses on a tab.
447      */
448     private static int pageSize = 200; // There are 176 in Ps 119.
449 
450     /**
451      * A list of all the URIEventListeners
452      */
453     private List<URIEventListener> uriEventListeners;
454 
455     /**
456      * A list of all the keyEventListeners
457      */
458     private List<KeyChangeListener> keyEventListeners;
459 
460     /**
461      * The passage that we are displaying (in one or more tabs)
462      */
463     private Passage key;
464 
465     /**
466      * The verses that we have not created tabs for yet
467      */
468     private Passage waiting;
469 
470     /**
471      * The version used for display
472      */
473     private Book[] books;
474 
475     /**
476      * Are we using tabs?
477      */
478     private boolean tabs;
479 
480     /**
481      * If we are using tabs, this is the main view
482      */
483     private JTabbedPane tabMain;
484 
485     /**
486      * If we are not using tabs, this is the main view
487      */
488     private BookDataDisplay bookDataDisplay;
489 
490     /**
491      * An map of components to their views
492      */
493     private Map<JScrollPane, BookDataDisplay> views;
494 
495     /**
496      * A list of all the InnerDisplayPanes so we can control listeners
497      */
498     private List<BookDataDisplay> displays;
499 
500     /**
501      * Pointer to whichever of the above is currently in use
502      */
503     private Component center;
504 
505     /**
506      * Blank thing for the "More..." button
507      */
508     private JPanel pnlMore;
509 
510     /**
511      * The top level component
512      */
513     private JPanel pnlMain;
514 
515     /**
516      * The top level component
517      */
518     private JScrollPane scrMain;
519 }
520