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 Pub
5   import org.crosswire.jsword.passage.KeyUtil;
6   lic License, version 2 as published by
7    * the Free Software Foundation. This program is distributed in the hope
8    * that it will be useful, but WITHOUT ANY WARRANTY; without even the
9    * implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
10   * See the GNU General Public License for more details.
11   *
12   * The License is available on the internet at:
13   *       http://www.gnu.org/copyleft/gpl.html
14   * or by writing to:
15   *      Free Software Foundation, Inc.
16   *      59 Temple Place - Suite 330
17   *      Boston, MA 02111-1307, USA
18   *
19   * Copyright: 2007
20   *     The copyright to this program is held by it's authors.
21   *
22   * ID: $Id: DisplaySelectPane.java 2223 2012-01-26 21:28:02Z dmsmith $
23   */
24  package org.crosswire.bibledesktop.book;
25  
26  import java.awt.FlowLayout;
27  import java.awt.GridBagConstraints;
28  import java.awt.GridBagLayout;
29  import java.awt.Insets;
30  import java.awt.SystemColor;
31  import java.awt.Toolkit;
32  import java.awt.event.ActionEvent;
33  import java.awt.event.ActionListener;
34  import java.awt.event.KeyAdapter;
35  import java.awt.event.KeyEvent;
36  import java.io.IOException;
37  import java.io.ObjectInputStream;
38  
39  import javax.swing.JButton;
40  import javax.swing.JComboBox;
41  import javax.swing.JLabel;
42  import javax.swing.JOptionPane;
43  import javax.swing.JPanel;
44  import javax.swing.JTextField;
45  import javax.swing.event.EventListenerList;
46  
47  import org.crosswire.bibledesktop.BDMsg;
48  import org.crosswire.bibledesktop.book.install.IndexResolver;
49  import org.crosswire.bibledesktop.passage.KeyChangeEvent;
50  import org.crosswire.bibledesktop.passage.KeyChangeListener;
51  import org.crosswire.common.swing.ActionFactory;
52  import org.crosswire.common.swing.CWAction;
53  import org.crosswire.common.swing.CWLabel;
54  import org.crosswire.common.swing.CWOptionPane;
55  import org.crosswire.common.swing.GuiUtil;
56  import org.crosswire.common.swing.QuickHelpDialog;
57  import org.crosswire.common.swing.desktop.event.TitleChangedEvent;
58  import org.crosswire.common.swing.desktop.event.TitleChangedListener;
59  import org.crosswire.common.util.Reporter;
60  import org.crosswire.jsword.book.Book;
61  import org.crosswire.jsword.book.BookComparators;
62  import org.crosswire.jsword.book.BookException;
63  import org.crosswire.jsword.book.BookFilters;
64  import org.crosswire.jsword.book.BookProvider;
65  import org.crosswire.jsword.index.IndexStatus;
66  import org.crosswire.jsword.index.IndexStatusEvent;
67  import org.crosswire.jsword.index.IndexStatusListener;
68  import org.crosswire.jsword.index.search.DefaultSearchModifier;
69  import org.crosswire.jsword.index.search.DefaultSearchRequest;
70  import org.crosswire.jsword.passage.Key;
71  import org.crosswire.jsword.passage.KeyUtil;
72  import org.crosswire.jsword.passage.NoSuchKeyException;
73  import org.crosswire.jsword.passage.PassageTally;
74  import org.crosswire.jsword.passage.RocketPassage;
75  import org.crosswire.jsword.passage.Verse;
76  import org.crosswire.jsword.passage.VerseRange;
77  import org.crosswire.jsword.versification.BibleBook;
78  import org.crosswire.jsword.versification.Versification;
79  import org.crosswire.jsword.versification.system.Versifications;
80  
81  /**
82   * Passage Selection area.
83   * 
84   * @see gnu.gpl.License for license details.<br>
85   *      The copyright to this program is held by it's authors.
86   * @author Joe Walker [joe at eireneh dot com]
87   * @author DM Smith [dmsmith555 at yahoo dot com]
88   */
89  public class DisplaySelectPane extends JPanel implements KeyChangeListener, BookSelectListener, BookProvider {
90      /**
91       * General constructor
92       */
93      public DisplaySelectPane() {
94          initialize();
95      }
96  
97      /**
98       * Initialize the GUI
99       */
100     private void initialize() {
101         listeners = new EventListenerList();
102 
103         advanced = new AdvancedSearchPane();
104 
105         // TRANSLATOR: This is the initial title of a Bible View. {0} is a placeholder for a number that uniquely identifies the Bible View.
106         title = BDMsg.gettext("Untitled {0}", Integer.valueOf(base++));
107 
108         actions = new ActionFactory(this);
109 
110         isl = new IndexStatusListener() {
111             public void statusChanged(IndexStatusEvent ev) {
112                 enableComponents();
113             }
114         };
115 
116         // search() and version() rely on this returning only Books indexed by verses
117         biblePicker = new ParallelBookPicker(BookFilters.getBibles(), BookComparators.getInitialComparator());
118         biblePicker.addBookListener(this);
119         selected = biblePicker.getBooks();
120         if (selected != null && selected.length > 0) {
121             selected[0].addIndexStatusListener(isl);
122             key = selected[0].createEmptyKeyList();
123         } else {
124             // The application has started and there are no installed bibles.
125             // Should always get a key from book, unless we need a PassageTally
126             // But here we don't have a book yet.
127             // AV11N(DMS): Is this right?
128             key = new RocketPassage(Versifications.instance().getDefaultVersification());
129         }
130 
131         JComboBox cboBooks = new JComboBox();
132         JComboBox cboChaps = new JComboBox();
133         quickSet = new BibleComboBoxModelSet(cboBooks, cboChaps, null);
134         quickSet.addActionListener(new ActionListener() {
135             public void actionPerformed(ActionEvent ev) {
136                 BibleComboBoxModelSet set = (BibleComboBoxModelSet) ev.getSource();
137                 Verse start = set.getVerse();
138                 BibleBook book = start.getBook();
139                 int chapter = start.getChapter();
140                 Versification v11n = KeyUtil.getPassage(key).getVersification();
141                 VerseRange range = new VerseRange(v11n, start, new Verse(book, chapter, v11n.getLastVerse(book, chapter)));
142                 txtSearch.setText("");
143                 txtKey.setText(range.getName());
144                 doGoPassage();
145             }
146         });
147 
148         JPanel quickPicker = new JPanel();
149         quickPicker.setLayout(new FlowLayout());
150         quickPicker.add(cboBooks);
151         quickPicker.add(cboChaps);
152 
153         // TRANSLATOR: This is the label for the Bible pickers
154         JLabel lblBible = CWLabel.createJLabel(BDMsg.gettext("Bible:"));
155         lblBible.setLabelFor(biblePicker);
156 
157         // TRANSLATOR: This is the label for the text box that show the passage
158         JLabel lblKey = CWLabel.createJLabel(BDMsg.gettext("Show Passage:"));
159 
160         CWAction action = actions.addAction("PassageAction");
161         // TRANSLATOR: This is the tooltip for the Show Passage text box
162         action.setTooltip(BDMsg.gettext("Enter a passage to display."));
163         txtKey = new JTextField();
164         txtKey.setAction(action);
165         txtKey.addKeyListener(new KeyAdapter() {
166             /* (non-Javadoc)
167              * @see java.awt.event.KeyListener#keyTyped(java.awt.event.KeyEvent)
168              */
169             @Override
170             public void keyTyped(KeyEvent ev) {
171                 if (ev.getKeyChar() == '\n' && ev.getModifiers() == Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()) {
172                     showSelectDialog();
173                 }
174             }
175         });
176         // TRANSLATOR: This labels a button that brings up a dialog box
177         // to select Bible book, chapter and verses
178         action = actions.addAction("More", BDMsg.gettext("Select"));
179         action.setTooltip(BDMsg.gettext("Pick a passage to display"));
180         btnKey = new JButton(action);
181 
182         // TRANSLATOR: The label of the button tied to the Show Passage text box
183         // Note: The " (passage)" is not to be translated.
184         // It is here because some languages translate "Go" differently depending on context.
185         action = actions.addAction("GoPassage", BDMsg.gettext("Go (passage)"));
186         action.setTooltip(BDMsg.gettext("Display the passage"));
187         btnKeyGo = new JButton(action);
188 
189         // FIXME(DMS): remove "Search (text)" from translations
190         action = actions.addAction("SearchAction"); // , BDMsg.gettext("Search (text)"));
191         // TRANSLATOR: The tooltip for the Search text box.
192         action.setTooltip(BDMsg.gettext("Search for a passage."));
193         txtSearch = new JTextField();
194         txtSearch.setAction(action);
195 
196         // TRANSLATOR: The label for the Search text box.
197         JLabel lblSearch = CWLabel.createJLabel(BDMsg.gettext("Search:"));
198         lblSearch.setLabelFor(txtSearch);
199 
200         // TRANSLATOR: The label of the button tied to the Search text box
201         // Note: The " (search)" is not to be translated.
202         // It is here because some languages translate "Go" differently depending on context.
203         action = actions.addAction("GoSearch", BDMsg.gettext("Go (search)"));
204         action.setTooltip(BDMsg.gettext("Search for a passage."));
205         btnSearch = new JButton(action);
206 
207         action = actions.addAction("HelpAction");
208         // TRANSLATOR: The tooltip for the help button that brings up Quick Search Tips
209         action.setTooltip(BDMsg.gettext("Quick Search Help"));
210         action.setSmallIcon("toolbarButtonGraphics/general/ContextualHelp16.gif");
211         JButton btnHelp = GuiUtil.flatten(new JButton(action));
212         // TRANSLATOR: Title to the dialog that shows search tips.
213         String dialogTitle = BDMsg.gettext("Search Quick Help");
214 
215         StringBuilder buf = new StringBuilder(200);
216         buf.append("<html><b>");
217         // TRANSLATOR: Label for search tips.
218         buf.append(BDMsg.gettext("Search Tips"));
219         buf.append("</b><br>");
220         // TRANSLATOR: Tip for using search keywords ||, OR. You can use any example you want.
221         // You may use balanced HTML markup.
222         buf.append(BDMsg.gettext("You can use || to join phrases, for example <code>balaam || balak</code> finds passages containing Balak OR Balaam"));
223         buf.append("<br>");
224         // TRANSLATOR: Tip for using search keywords &&, AND. You can use any example you want. 
225         // You may use balanced HTML markup.
226         buf.append(BDMsg.gettext("Using && requires both words, e.g. <code>aaron && moses</code> finds passages containing both Aaron AND Moses"));
227         buf.append("<br>");
228         // TRANSLATOR: Tip for using search keywords !, BUT NOT. You can use any example you want. 
229         // You may use balanced HTML markup.
230         buf.append(BDMsg.gettext("Using a ! removes words from the result e.g. <code>lord ! jesus</code> is passages containing Lord BUT NOT Jesus"));
231         buf.append("<br>");
232         // TRANSLATOR: Tip for using search keyword ~n. You can use any example you want. 
233         // You may use balanced HTML markup.
234         buf.append(BDMsg.gettext("Using ~2 widens the passage by 2 verses either side on any match. So <code>amminadab ~1 perez</code> finds verses containting Amminadab within 1 verse of mention of Perez."));
235         buf.append("<br>");
236         // TRANSLATOR: Tip for using search keyword +[...]. You can use any example you want. 
237         // You may use balanced HTML markup.
238         buf.append(BDMsg.gettext("Using +[Gen-Exo] at the beginning will restrict a search to that range of verses."));
239         dlgHelp = new QuickHelpDialog(GuiUtil.getFrame(this), dialogTitle, buf.toString());
240 
241         // TRANSLATOR: The label for the button that brings up the Advanced Search dialog
242         action = actions.addAction("Advanced", BDMsg.gettext("Advanced"));
243         // TRANSLATOR: The tooltip for the button that brings up the Advanced Search dialog
244         action.setTooltip(BDMsg.gettext("Advanced Search"));
245         btnAdvanced = new JButton(action);
246 
247         // TRANSLATOR: The label for the button that creates a search index for the selected book.
248         action = actions.addAction("Index", BDMsg.gettext("Enable Search"));
249         // TRANSLATOR: The tooltip for the button that creates a search index for the selected book.
250         action.setTooltip(BDMsg.gettext("Create a search index"));
251         btnIndex = new JButton(action);
252 
253         this.setLayout(new GridBagLayout());
254         this.add(lblBible,    new GridBagConstraints(0, 0, 2, 1, 0.0, 0.0, GridBagConstraints.LINE_END,   GridBagConstraints.VERTICAL,   new Insets(0, 0, 0, 5), 0, 0));
255         this.add(biblePicker, new GridBagConstraints(2, 0, 2, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0));
256         this.add(quickPicker, new GridBagConstraints(4, 0, 2, 1, 0.0, 0.0, GridBagConstraints.LINE_END,   GridBagConstraints.NONE,       new Insets(0, 0, 0, 0), 0, 0));
257 
258         this.add(lblKey,      new GridBagConstraints(0, 1, 2, 1, 0.0, 0.0, GridBagConstraints.LINE_END,   GridBagConstraints.NONE,       new Insets(0, 0, 0, 5), 0, 0));
259         this.add(txtKey,      new GridBagConstraints(2, 1, 2, 1, 1.0, 0.0, GridBagConstraints.CENTER,     GridBagConstraints.HORIZONTAL, new Insets(2, 0, 1, 2), 0, 0));
260         this.add(btnKeyGo,    new GridBagConstraints(4, 1, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER,     GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0));
261         this.add(btnKey,      new GridBagConstraints(5, 1, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_END,   GridBagConstraints.HORIZONTAL, new Insets(2, 0, 2, 2), 0, 0));
262 
263         this.add(btnHelp,     new GridBagConstraints(0, 2, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE,       new Insets(0, 0, 0, 0), 0, 0));
264         this.add(lblSearch,   new GridBagConstraints(1, 2, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_END,   GridBagConstraints.NONE,       new Insets(0, 0, 0, 5), 0, 0));
265         this.add(btnIndex,    new GridBagConstraints(2, 2, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE,       new Insets(2, 0, 2, 2), 0, 0));
266         this.add(txtSearch,   new GridBagConstraints(2, 2, 1, 1, 1.0, 0.0, GridBagConstraints.CENTER,     GridBagConstraints.HORIZONTAL, new Insets(2, 0, 3, 2), 0, 0));
267         this.add(btnSearch,   new GridBagConstraints(4, 2, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER,     GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0));
268         this.add(btnAdvanced, new GridBagConstraints(5, 2, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_END,   GridBagConstraints.HORIZONTAL, new Insets(2, 0, 2, 2), 0, 0));
269 
270         enableComponents();
271         GuiUtil.applyDefaultOrientation(this);
272 
273     }
274 
275     /**
276      * During view creation, allow firing off an event to display the initial
277      * book/chapter. This is copied from quickSet.addActionListener().
278      */
279     public void doInitialTextDisplay() {
280         Verse start = quickSet.getVerse();
281         BibleBook book = start.getBook();
282         int chapter = start.getChapter();
283         // AV11N(DMS): Is this right?
284         Versification v11n = Versifications.instance().getDefaultVersification();
285         VerseRange range = new VerseRange(v11n, start, new Verse(book, chapter, v11n.getLastVerse(book, chapter)));
286         txtSearch.setText("");
287         txtKey.setText(range.getName());
288         doGoPassage();
289     }
290 
291     /**
292      * What are the currently selected Books?
293      */
294     public Book[] getBooks() {
295         return selected.clone();
296     }
297 
298     /**
299      * What is the first currently selected book?
300      */
301     public Book getFirstBook() {
302         return selected != null && selected.length > 0 ? selected[0] : null;
303     }
304 
305     /**
306      * Clear the contents
307      */
308     public void clear() {
309         // AV11N(DMS): Is this right?
310         setKey(selected == null || selected.length == 0 ? new RocketPassage(Versifications.instance().getDefaultVersification()) : selected[0].createEmptyKeyList());
311         setTitle(Mode.CLEAR);
312     }
313 
314     /**
315      * Determine whether there is content
316      */
317     public boolean isClear() {
318         // TRANSLATOR: This must match the word that is used for "Untitled {0}".
319         // This is used to determine whether a tab is unused or not.
320         return title.indexOf(BDMsg.gettext("Untitled")) != -1;
321     }
322 
323     /**
324      * More (...) button was clicked
325      */
326     public void doMore() {
327         showSelectDialog();
328     }
329 
330     /**
331      * Go button was clicked
332      */
333     public void doGoPassage() {
334         doPassageAction();
335     }
336 
337     /**
338      * Go button was clicked
339      */
340     public void doGoSearch() {
341         doSearchAction();
342     }
343 
344     /**
345      * Someone pressed return in the passage area
346      */
347     public void doPassageAction() {
348         setKey(txtKey.getText());
349         if (!key.isEmpty()) {
350             txtSearch.setText("");
351             setTitle(Mode.PASSAGE);
352         }
353     }
354 
355     /**
356      * Someone pressed return in the search area
357      */
358     public void doSearchAction() {
359         if (selected == null || selected.length == 0) {
360             noBookInstalled();
361             return;
362         }
363 
364         try {
365             String param = txtSearch.getText();
366             if (param == null || param.length() == 0) {
367                 return;
368             }
369 
370             boolean rank = advanced.isRanked();
371 
372             DefaultSearchModifier modifier = new DefaultSearchModifier();
373             modifier.setRanked(rank);
374 
375             // If ranking see if the results are being limited.
376             int rankCount = getNumRankedVerses();
377             if (rank && rankCount != 0) {
378                 modifier.setMaxResults(rankCount);
379             }
380 
381             Key results = selected[0].find(new DefaultSearchRequest(param, modifier));
382             int partial = results.getCardinality();
383             int total = partial;
384 
385             // we should get PassageTallys for rank searches
386             if (results instanceof PassageTally) {
387                 PassageTally tally = (PassageTally) results;
388                 total = tally.getTotal();
389                 tally.setOrdering(PassageTally.Order.TALLY);
390             }
391 
392             if (total == 0) {
393                 // TRANSLATOR: There were no verses that satisfied the search request.
394                 // {0} is a placeholder for the search request.
395                 Reporter.informUser(this, BDMsg.gettext("Could not find verses with: {0}", param));
396             } else {
397                 if (total == partial) {
398                     // TRANSLATOR: There were verses that satisfied the search request. This tells the user how many.
399                     // {0} is a placeholder for the search request.
400                     // {1} is a placeholder for the number of verses that satisfied the search request.
401                     // I18N(DMS): This needs support for singular/plural and to show internationalized numbers.
402                     Reporter.informUser(this, BDMsg.gettext("There are {1} verses with: {0}", param, Integer.valueOf(total)));
403                 } else {
404                     // TRANSLATOR: The user has done a prioritized search and there are more hits that the user has requested.
405                     // {0} is a placeholder for the search request.
406                     // {1} is a placeholder for the number of verses that is being given back to the user. This is the number of prioritized verses that the user requested.
407                     // {2} is a placeholder for the number of verses that satisfied the search request.
408                     // I18N(DMS): This needs support for singular/plural and to show internationalized numbers.
409                     Reporter.informUser(this, BDMsg.gettext("Showing {1} of {2} verses with: {0}", param, Integer.toString(partial), Integer.toString(total)));
410                 }
411                 setTitle(Mode.SEARCH);
412                 setKey(results);
413             }
414         } catch (BookException ex) {
415             Reporter.informUser(this, ex);
416         }
417     }
418 
419     /**
420      * Someone has clicked on the advanced search button
421      */
422     public void doAdvanced() {
423         // TRANSLATOR: This is the title for the Advanced Search dialog.
424         String reply = advanced.showInDialog(this, BDMsg.gettext("Advanced Search"), true, txtSearch.getText());
425         if (reply != null) {
426             txtSearch.setText(reply);
427             doSearchAction();
428         }
429     }
430 
431     /**
432      * Rank is an action, but we don't need to do anything because rank is only
433      * used when search is clicked. But ActionFactory will complain if we leave
434      * it out.
435      */
436     public void doRank() {
437         // Do nothing
438     }
439 
440     /**
441      * Someone clicked help
442      */
443     public void doHelpAction() {
444         dlgHelp.setVisible(true);
445     }
446 
447     /**
448      * Someone clicked one the index button
449      */
450     public void doIndex() {
451         if (selected == null || selected.length == 0) {
452             noBookInstalled();
453             return;
454         }
455 
456         IndexResolver.scheduleIndex(selected[0], this);
457         enableComponents();
458     }
459 
460     /**
461      * Sync the viewed passage with the passage text box
462      */
463     private void updateDisplay() {
464         if (selected == null || selected.length == 0) {
465             noBookInstalled();
466             return;
467         }
468 
469         fireCommandMade(new DisplaySelectEvent(this, key));
470     }
471 
472     /**
473      * Accessor for the default name
474      */
475     public String getTitle() {
476         return title;
477     }
478 
479     /**
480      * @return the picker
481      */
482     public ParallelBookPicker getBiblePicker() {
483         return biblePicker;
484     }
485 
486     /**
487      * Set the key
488      * @param newKey the new key
489      */
490     public void setKey(String newKey) {
491         if (selected == null || selected.length == 0) {
492             return;
493         }
494 
495         try {
496             setKey(selected[0].getKey(newKey));
497         } catch (NoSuchKeyException e) {
498             Reporter.informUser(this, e);
499         }
500     }
501 
502     /**
503      * Set the key
504      * @param newKey the new key
505      */
506     public void setKey(Key newKey) {
507         if (newKey == null || newKey.isEmpty()) {
508             if (!key.isEmpty()) {
509                 key = selected[0].createEmptyKeyList();
510                 txtKey.setText("");
511                 txtSearch.setText("");
512 
513                 updateDisplay();
514                 setTitle(Mode.CLEAR);
515             }
516         } else if (!newKey.equals(key)) {
517             key = newKey;
518             String text = key.getName();
519             txtKey.setText(text);
520             updateDisplay();
521             if (isClear()) {
522                 setTitle(Mode.PASSAGE);
523                 txtSearch.setText("");
524             }
525         }
526     }
527 
528     /**
529      * Gets the number of verses that should be shown when a search result is
530      * ranked. A value of 0 means show all.
531      * 
532      * @return Returns the numRankedVerses.
533      */
534     public static int getNumRankedVerses() {
535         return numRankedVerses;
536     }
537 
538     /**
539      * Sets the number of verses that should be shown when a search result is
540      * ranked. This can be a value in the range of 0 to maxNumRankedVerses.
541      * Values outside this range are silently constrained to the range.
542      * 
543      * @param newNumRankedVerses
544      *            The numRankedVerses to set.
545      */
546     public static void setNumRankedVerses(int newNumRankedVerses) {
547         int count = newNumRankedVerses;
548         if (count < 0) {
549             count = 0;
550         } else if (count > maxNumRankedVerses) {
551             count = maxNumRankedVerses;
552         }
553         numRankedVerses = count;
554     }
555 
556     /**
557      * @return Returns the maxNumRankedVerses.
558      */
559     public static int getMaxNumRankedVerses() {
560         return maxNumRankedVerses;
561     }
562 
563     /**
564      * @param newMaxNumRankedVerses
565      *            The maxNumRankedVerses to set.
566      */
567     public static void setMaxNumRankedVerses(int newMaxNumRankedVerses) {
568         int count = newMaxNumRankedVerses;
569         if (count < numRankedVerses) {
570             count = numRankedVerses;
571         }
572         maxNumRankedVerses = count;
573     }
574 
575     private void setTitle(Mode clear) {
576         mode = clear;
577         switch (mode) {
578         case CLEAR:
579             // TRANSLATOR: This is the initial title of a Bible View. {0} is a placeholder for a number that uniquely identifies the Bible View.
580             title = BDMsg.gettext("Untitled {0}", Integer.valueOf(base++));
581             break;
582         case PASSAGE:
583             title = key.getName();
584             break;
585         case SEARCH:
586             title = txtSearch.getText();
587             break;
588         default:
589             assert false;
590         }
591         if (title.length() == 0) {
592             setTitle(Mode.CLEAR);
593         } else {
594             fireTitleChanged(new TitleChangedEvent(this, title));
595         }
596     }
597 
598     /**
599      * Display a dialog indicating that no Bible is installed.
600      */
601     private void noBookInstalled() {
602         // TRANSLATOR: The user is trying to do something that requires at least one Bible to be installed.
603         // There are a variety of common reasons that this can happen:
604         //     The user has chosen to not install a Bible when starting the program for the first time.
605         //     The user has never installed a Bible.
606         //     The user has deleted the last installed Bible.
607         //     The books are on a CD, USB or some other removable media and are not available.
608         String noBible = BDMsg.gettext("No Bible is installed");
609         CWOptionPane.showMessageDialog(this, noBible, noBible, JOptionPane.WARNING_MESSAGE);
610     }
611 
612     /**
613      * Ensure that the right components are enabled
614      */
615     /*private*/final void enableComponents() {
616         boolean readable = selected != null && selected.length > 0;
617         boolean searchable = readable && selected[0].getIndexStatus().equals(IndexStatus.DONE);
618         boolean indexable = readable && selected[0].getIndexStatus().equals(IndexStatus.UNDONE);
619 
620         txtSearch.setEnabled(searchable);
621         txtSearch.setBackground(searchable ? SystemColor.text : SystemColor.control);
622         txtSearch.setVisible(searchable);
623         btnAdvanced.setEnabled(searchable);
624         btnSearch.setEnabled(searchable);
625         txtKey.setEnabled(readable);
626         txtKey.setBackground(readable ? SystemColor.text : SystemColor.control);
627         btnKey.setEnabled(readable);
628         btnKeyGo.setEnabled(readable);
629         btnIndex.setVisible(indexable);
630         btnIndex.setEnabled(indexable);
631     }
632 
633     /**
634      * Someone clicked the "..." button
635      */
636     /*private*/final void showSelectDialog() {
637         if (dlgSelect == null) {
638             dlgSelect = new PassageSelectionPane();
639         }
640 
641         // TRANSLATOR: The title to the "Select Passage" dialog.
642         String passg = dlgSelect.showInDialog(this, BDMsg.gettext("Select Passage"), true, txtKey.getText());
643         if (passg != null) {
644             txtKey.setText(passg);
645             doPassageAction();
646         }
647     }
648 
649     /* (non-Javadoc)
650      * @see org.crosswire.bibledesktop.book.BookSelectListener#booksChosen(org.crosswire.bibledesktop.book.BookSelectEvent)
651      */
652     public void booksChosen(BookSelectEvent ev) {
653         Book[] books = ev.getBookProvider().getBooks();
654         assert books.length > 0;
655 
656         Book newSelected = ev.getBookProvider().getFirstBook();
657 
658         if (selected.length > 0 && selected[0] != newSelected) {
659             selected[0].removeIndexStatusListener(isl);
660             newSelected.addIndexStatusListener(isl);
661         }
662 
663         selected = books;
664 
665         enableComponents();
666 
667         if (selected == null || selected.length == 0) {
668             noBookInstalled();
669             return;
670         }
671 
672         fireVersionChanged(new DisplaySelectEvent(this, key));
673     }
674 
675     /* (non-Javadoc)
676      * @see org.crosswire.bibledesktop.book.KeyChangeListener#keyChanged(org.crosswire.bibledesktop.book.KeyChangeEvent)
677      */
678     public void keyChanged(KeyChangeEvent ev) {
679         setKey(ev.getKey());
680     }
681 
682     /**
683      * Add a TitleChangedEvent listener
684      */
685     public synchronized void addTitleChangedListener(TitleChangedListener li) {
686         listeners.add(TitleChangedListener.class, li);
687     }
688 
689     /**
690      * Remove a TitleChangedEvent listener
691      */
692     public synchronized void removeTitleChangedListener(TitleChangedListener li) {
693         listeners.remove(TitleChangedListener.class, li);
694     }
695 
696     /**
697      * Listen for changes to the title
698      * 
699      * @param ev
700      *            the event to throw
701      */
702     protected void fireTitleChanged(TitleChangedEvent ev) {
703         // Guaranteed to return a non-null array
704         Object[] contents = listeners.getListenerList();
705 
706         // Process the listeners last to first, notifying
707         // those that are interested in this event
708         for (int i = contents.length - 2; i >= 0; i -= 2) {
709             if (contents[i] == TitleChangedListener.class) {
710                 ((TitleChangedListener) contents[i + 1]).titleChanged(ev);
711             }
712         }
713     }
714 
715     /**
716      * Add a DisplaySelectEvent listener
717      */
718     public synchronized void addCommandListener(DisplaySelectListener li) {
719         listeners.add(DisplaySelectListener.class, li);
720     }
721 
722     /**
723      * Remove a DisplaySelectEvent listener
724      */
725     public synchronized void removeCommandListener(DisplaySelectListener li) {
726         listeners.remove(DisplaySelectListener.class, li);
727     }
728 
729     /**
730      * Inform the command listeners
731      */
732     protected void fireCommandMade(DisplaySelectEvent ev) {
733         // Guaranteed to return a non-null array
734         Object[] contents = listeners.getListenerList();
735 
736         // Process the listeners last to first, notifying
737         // those that are interested in this event
738         for (int i = contents.length - 2; i >= 0; i -= 2) {
739             if (contents[i] == DisplaySelectListener.class) {
740                 ((DisplaySelectListener) contents[i + 1]).passageSelected(ev);
741             }
742         }
743     }
744 
745     /**
746      * Inform the version listeners
747      */
748     protected void fireVersionChanged(DisplaySelectEvent ev) {
749         // Guaranteed to return a non-null array
750         Object[] contents = listeners.getListenerList();
751 
752         // Process the listeners last to first, notifying
753         // those that are interested in this event
754         for (int i = contents.length - 2; i >= 0; i -= 2) {
755             if (contents[i] == DisplaySelectListener.class) {
756                 ((DisplaySelectListener) contents[i + 1]).bookChosen(ev);
757             }
758         }
759     }
760 
761     /**
762      * Serialization support.
763      * 
764      * @param is
765      * @throws IOException
766      * @throws ClassNotFoundException
767      */
768     private void readObject(ObjectInputStream is) throws IOException, ClassNotFoundException {
769         // We don't serialize views
770         selected = null;
771 
772         listeners = new EventListenerList();
773 
774         actions = new ActionFactory(this);
775 
776         isl = new IndexStatusListener() {
777             public void statusChanged(IndexStatusEvent ev) {
778                 enableComponents();
779             }
780         };
781         is.defaultReadObject();
782     }
783 
784     /**
785      * Keep the selection up to date with indexing.
786      */
787     private transient IndexStatusListener isl;
788 
789     private static int base = 1;
790 
791     private String title;
792 
793     private QuickHelpDialog dlgHelp;
794 
795     private transient ActionFactory actions;
796 
797     private transient Book[] selected;
798     /*
799      * GUI Components
800      */
801     private BibleComboBoxModelSet quickSet;
802     private PassageSelectionPane dlgSelect;
803     private ParallelBookPicker biblePicker;
804     protected JTextField txtKey;
805     protected JTextField txtSearch;
806     private JButton btnAdvanced;
807     private JButton btnSearch;
808     private JButton btnKey;
809     private JButton btnKeyGo;
810     private AdvancedSearchPane advanced;
811     private JButton btnIndex;
812 
813     /**
814      * Defines the state of this DisplaySelectPane
815      */
816     private enum Mode {
817         CLEAR,
818         PASSAGE,
819         SEARCH,
820     }
821     /**
822      * The current state of the display: SEARCH, PASSAGE, CLEAR
823      */
824     private Mode mode;
825 
826     /**
827      * The current passage.
828      */
829     private Key key;
830 
831     /**
832      * Who is interested in things this DisplaySelectPane does
833      */
834     private transient EventListenerList listeners;
835 
836     /**
837      * How may hits to show when the search results are ranked.
838      */
839     private static int numRankedVerses = 20;
840 
841     /**
842      * What is the limit to which numRankedVerses can be set.
843      */
844     private static int maxNumRankedVerses = 200;
845 
846     /**
847      * Serialization ID
848      */
849     private static final long serialVersionUID = 3256446910616057650L;
850 }
851