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: BibleViewPane.java 2091 2011-03-07 04:15:31Z dmsmith $
21   */
22  package org.crosswire.bibledesktop.book;
23  
24  import java.awt.BorderLayout;
25  import java.awt.Dimension;
26  import java.io.File;
27  import java.io.FileReader;
28  import java.io.FileWriter;
29  import java.io.IOException;
30  import java.io.ObjectInputStream;
31  import java.io.Reader;
32  import java.io.Writer;
33  
34  import javax.swing.JFileChooser;
35  import javax.swing.UIManager;
36  import javax.swing.event.EventListenerList;
37  import javax.swing.filechooser.FileFilter;
38  
39  import org.crosswire.bibledesktop.BDMsg;
40  import org.crosswire.bibledesktop.display.BookDataDisplay;
41  import org.crosswire.bibledesktop.display.basic.SplitBookDataDisplay;
42  import org.crosswire.bibledesktop.display.basic.TabbedBookDataDisplay;
43  import org.crosswire.bibledesktop.passage.KeySidebar;
44  import org.crosswire.common.swing.GuiUtil;
45  import org.crosswire.common.swing.desktop.Clearable;
46  import org.crosswire.common.swing.desktop.TabbedPanePanel;
47  import org.crosswire.common.swing.desktop.Titleable;
48  import org.crosswire.common.swing.desktop.event.TitleChangedEvent;
49  import org.crosswire.common.swing.desktop.event.TitleChangedListener;
50  import org.crosswire.common.util.CWProject;
51  import org.crosswire.common.util.Logger;
52  import org.crosswire.common.util.Reporter;
53  import org.crosswire.jsword.passage.Key;
54  import org.crosswire.jsword.passage.NoSuchVerseException;
55  import org.crosswire.jsword.passage.Passage;
56  import org.crosswire.jsword.passage.PassageKeyFactory;
57  
58  /**
59   * A BibleViewPane consists of three areas for looking up passages, for
60   * navigating and manipulating parts of passage and for viewing a passage.
61   * 
62   * @see gnu.gpl.License for license details.<br>
63   *      The copyright to this program is held by it's authors.
64   * @author Joe Walker [joe at eireneh dot com]
65   */
66  public class BibleViewPane extends TabbedPanePanel implements Titleable, Clearable, TitleChangedListener {
67      /**
68       * Simple ctor
69       */
70      public BibleViewPane(boolean showSidebar) {
71          listeners = new EventListenerList();
72          pnlSelect = new DisplaySelectPane();
73          KeySidebar sidebar = new KeySidebar(pnlSelect.getBooks());
74          BookDataDisplay display = new TabbedBookDataDisplay();
75          pnlPassg = new SplitBookDataDisplay(sidebar, display);
76          pnlPassg.showSidebar(showSidebar);
77          sidebar.addKeyChangeListener(pnlSelect);
78          pnlSelect.addCommandListener(sidebar);
79          pnlSelect.addTitleChangedListener(this);
80          pnlPassg.addKeyChangeListener(sidebar);
81          init();
82  
83          // Display the text initial book/verse selection
84          pnlSelect.doInitialTextDisplay();
85      }
86  
87      /**
88       * Setup the GUI
89       */
90      private void init() {
91          try {
92              chooser = new JFileChooser(CWProject.instance().getWriteableProjectSubdir(BOOKMARK_DIR, true).getPath());
93          } catch (IOException ex) {
94              chooser = new JFileChooser(CWProject.instance().getWritableProjectDir().getPath());
95          }
96  
97          chooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
98          chooser.addChoosableFileFilter(new CustomFileFilter());
99          chooser.setMultiSelectionEnabled(false);
100 
101         pnlSelect.addCommandListener(new DisplaySelectListener() {
102             /* (non-Javadoc)
103              * @see org.crosswire.bibledesktop.book.DisplaySelectListener#passageSelected(org.crosswire.bibledesktop.book.DisplaySelectEvent)
104              */
105             public void passageSelected(DisplaySelectEvent ev) {
106                 pnlPassg.setBookData(ev.getBookProvider().getBooks(), ev.getKey());
107             }
108 
109             /* (non-Javadoc)
110              * @see org.crosswire.bibledesktop.book.DisplaySelectListener#bookChosen(org.crosswire.bibledesktop.book.DisplaySelectEvent)
111              */
112             public void bookChosen(DisplaySelectEvent ev) {
113                 pnlPassg.setBookData(ev.getBookProvider().getBooks(), ev.getKey());
114             }
115         });
116 
117         pnlSelect.setBorder(UIManager.getBorder("SelectPanel.border"));
118 
119         this.setLayout(new BorderLayout());
120         this.setMinimumSize(new Dimension(0, 0));
121         this.add(pnlSelect, BorderLayout.NORTH);
122         this.add(pnlPassg, BorderLayout.CENTER);
123         GuiUtil.applyDefaultOrientation(this);
124     }
125 
126     /**
127      * Make it as though no-one is using this view
128      */
129     public void clear() {
130         saved = null;
131         if (!pnlSelect.isClear()) {
132             pnlSelect.clear();
133         }
134     }
135 
136     /**
137      * Has anyone started using this view
138      */
139     public boolean isClear() {
140         saved = null;
141         return pnlSelect.isClear();
142     }
143 
144     /**
145      * How has this view been saved
146      */
147     public String getTitle() {
148         if (saved == null) {
149             return pnlSelect.getTitle();
150         }
151 
152         return saved.getName();
153     }
154 
155     /**
156      * Save the view to disk.
157      */
158     public void save() throws IOException {
159         Key key = getKey();
160         if (key == null) {
161             return;
162         }
163 
164         // We need a name to save against
165         if (saved == null && !querySaveFile()) {
166             return;
167         }
168 
169         saveKey(key);
170     }
171 
172     /**
173      * Save the view to disk, but ask the user where to save it first.
174      * 
175      * @throws IOException
176      */
177     public void saveAs() throws IOException {
178         Key key = getKey();
179         if (key == null) {
180             return;
181         }
182 
183         querySaveFile();
184 
185         saveKey(key);
186     }
187 
188     /**
189      * Do the real work of saving to a file
190      * 
191      * @param key
192      *            The key to save
193      * @throws IOException
194      *             If a write error happens
195      */
196     private void saveKey(Key key) throws IOException {
197         // TODO(DMS): change this to save:
198         // The version of the save file, incremented everytime this method
199         // changes.
200         // the search request,
201         // the set of bibles,
202         // and any advanced search options.
203         // perhaps by having and creating a book mark object.
204         assert saved != null;
205 
206         Writer out = null;
207         try {
208             out = new FileWriter(saved);
209             if (key instanceof Passage) {
210                 Passage ref = (Passage) key;
211                 ref.writeDescription(out);
212             } else {
213                 out.write(key.getName());
214                 out.write("\n");
215             }
216         } finally {
217             if (out != null) {
218                 out.close();
219             }
220         }
221     }
222 
223     /**
224      * Returns true if there is something to save.
225      */
226     public boolean maySave() {
227         return getKey() != null;
228     }
229 
230     /**
231      * Open a saved verse list form disk
232      * 
233      * @throws IOException
234      * @throws NoSuchVerseException
235      */
236     public void open() throws NoSuchVerseException, IOException {
237         // TODO(DMS): make this sensitive to a version marker in the file!
238         int reply = chooser.showOpenDialog(getRootPane());
239         if (reply == JFileChooser.APPROVE_OPTION) {
240             saved = chooser.getSelectedFile();
241             if (saved.length() == 0) {
242                 // TRANSLATOR: Let the user know that the file is empty. {0} is a placeholder for the filename.
243                 Reporter.informUser(getRootPane(), BDMsg.gettext("File {0} is empty", saved.getName()));
244                 return;
245             }
246 
247             Reader in = null;
248             try {
249                 in = new FileReader(saved);
250                 Passage ref = PassageKeyFactory.readPassage(in);
251                 setKey(ref);
252             } finally {
253                 if (in != null) {
254                     in.close();
255                 }
256             }
257         }
258     }
259 
260     /**
261      * Ask the user where to store the data
262      */
263     private boolean querySaveFile() {
264         if (saved == null) {
265             File guess = new File(getTitle() + EXTENSION);
266             chooser.setSelectedFile(guess);
267         } else {
268             chooser.setSelectedFile(saved);
269         }
270 
271         int reply = chooser.showSaveDialog(getRootPane());
272         if (reply == JFileChooser.APPROVE_OPTION) {
273             saved = chooser.getSelectedFile();
274             return true;
275         }
276         return false;
277     }
278 
279     /**
280      * Accessor for the current passage
281      */
282     public Key getKey() {
283         // Get it from the pnlPassg because the user may be in the middle of an
284         // edit and pnlSelect may be inconsistent.
285         return pnlPassg.getKey();
286     }
287 
288     /**
289      * Accessor for the current passage
290      */
291     public final void setKey(Key key) {
292         pnlSelect.setKey(key);
293     }
294 
295     /**
296      * Accessor for the SplitBookDataDisplay
297      */
298     public SplitBookDataDisplay getPassagePane() {
299         return pnlPassg;
300     }
301 
302     /**
303      * Accessor for the DisplaySelectPane
304      */
305     public DisplaySelectPane getSelectPane() {
306         return pnlSelect;
307     }
308 
309     /**
310      * Add a TitleChangedEvent listener
311      */
312     public synchronized void addTitleChangedListener(TitleChangedListener li) {
313         listeners.add(TitleChangedListener.class, li);
314     }
315 
316     /**
317      * Remove a TitleChangedEvent listener
318      */
319     public synchronized void removeTitleChangedListener(TitleChangedListener li) {
320         listeners.remove(TitleChangedListener.class, li);
321     }
322 
323     /**
324      * Listen for changes to the title
325      * 
326      * @param ev
327      *            the event to throw
328      */
329     protected void fireTitleChanged(TitleChangedEvent ev) {
330         // Guaranteed to return a non-null array
331         Object[] contents = listeners.getListenerList();
332 
333         // Process the listeners last to first, notifying
334         // those that are interested in this event
335         for (int i = contents.length - 2; i >= 0; i -= 2) {
336             if (contents[i] == TitleChangedListener.class) {
337                 ((TitleChangedListener) contents[i + 1]).titleChanged(ev);
338             }
339         }
340     }
341 
342     /* (non-Javadoc)
343      * @see org.crosswire.common.swing.desktop.event.TitleChangedListener#titleChanged(org.crosswire.common.swing.desktop.event.TitleChangedEvent)
344      */
345     public void titleChanged(TitleChangedEvent ev) {
346         if (saved == null) {
347             fireTitleChanged(new TitleChangedEvent(BibleViewPane.this, getTitle()));
348         }
349     }
350 
351     /**
352      * Serialization support.
353      * 
354      * @param is
355      * @throws IOException
356      * @throws ClassNotFoundException
357      */
358     private void readObject(ObjectInputStream is) throws IOException, ClassNotFoundException {
359         listeners = new EventListenerList();
360         is.defaultReadObject();
361     }
362 
363     protected File saved;
364     private transient EventListenerList listeners;
365     private DisplaySelectPane pnlSelect;
366     protected SplitBookDataDisplay pnlPassg;
367     private JFileChooser chooser;
368     private static final String BOOKMARK_DIR = "bookmarks";
369     private static final String EXTENSION = ".lst";
370 
371     /**
372      * The log stream
373      */
374     protected static final Logger log = Logger.getLogger(BibleViewPane.class);
375 
376     /**
377      * Serialization ID
378      */
379     private static final long serialVersionUID = 3258415036346282038L;
380 
381     /**
382      * Filter out verse lists
383      */
384     static final class CustomFileFilter extends FileFilter {
385         /* (non-Javadoc)
386          * @see javax.swing.filechooser.FileFilter#accept(java.io.File)
387          */
388         @Override
389         public boolean accept(File file) {
390             return file.getName().endsWith(EXTENSION);
391         }
392 
393         /* (non-Javadoc)
394          * @see javax.swing.filechooser.FileFilter#getDescription()
395          */
396         @Override
397         public String getDescription() {
398             // TRANSLATOR: This is the label for the verse list extension. {0} is a placeholder for that extension, which must be ".lst".
399             return BDMsg.gettext("Verse Lists ({0})", EXTENSION);
400         }
401     }
402 }
403