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: SitePane.java 2125 2011-03-14 21:37:06Z dmsmith $
21   */
22  package org.crosswire.bibledesktop.book.install;
23  
24  import java.awt.BorderLayout;
25  import java.awt.Component;
26  import java.awt.Font;
27  import java.awt.GridLayout;
28  import java.io.IOException;
29  import java.io.ObjectInputStream;
30  
31  import javax.swing.BorderFactory;
32  import javax.swing.JButton;
33  import javax.swing.JLabel;
34  import javax.swing.JOptionPane;
35  import javax.swing.JPanel;
36  import javax.swing.JScrollPane;
37  import javax.swing.JSplitPane;
38  import javax.swing.JTree;
39  import javax.swing.ToolTipManager;
40  import javax.swing.event.TreeSelectionEvent;
41  import javax.swing.event.TreeSelectionListener;
42  import javax.swing.tree.DefaultMutableTreeNode;
43  import javax.swing.tree.DefaultTreeModel;
44  import javax.swing.tree.TreeModel;
45  import javax.swing.tree.TreeNode;
46  import javax.swing.tree.TreePath;
47  import javax.swing.tree.TreeSelectionModel;
48  
49  import org.crosswire.bibledesktop.BDMsg;
50  import org.crosswire.common.swing.ActionFactory;
51  import org.crosswire.common.swing.CWAction;
52  import org.crosswire.common.swing.CWLabel;
53  import org.crosswire.common.swing.CWOptionPane;
54  import org.crosswire.common.swing.CWScrollPane;
55  import org.crosswire.common.swing.FixedSplitPane;
56  import org.crosswire.common.swing.FontChooser;
57  import org.crosswire.common.util.Language;
58  import org.crosswire.common.util.Reporter;
59  import org.crosswire.jsword.book.Book;
60  import org.crosswire.jsword.book.BookCategory;
61  import org.crosswire.jsword.book.BookException;
62  import org.crosswire.jsword.book.BookList;
63  import org.crosswire.jsword.book.BookMetaData;
64  import org.crosswire.jsword.book.BookSet;
65  import org.crosswire.jsword.book.Books;
66  import org.crosswire.jsword.book.BooksEvent;
67  import org.crosswire.jsword.book.BooksListener;
68  import org.crosswire.jsword.book.install.InstallException;
69  import org.crosswire.jsword.book.install.Installer;
70  import org.crosswire.jsword.index.IndexManager;
71  import org.crosswire.jsword.index.IndexManagerFactory;
72  import org.crosswire.jsword.util.WebWarning;
73  
74  /**
75   * A panel for use within a SitesPane to display one set of Books that are
76   * installed or could be installed.
77   * 
78   * @see gnu.gpl.License for license details.<br>
79   *      The copyright to this program is held by it's authors.
80   * @author Joe Walker [joe at eireneh dot com]
81   * @author DM Smith [dmsmith555 at yahoo dot com]
82   */
83  public class SitePane extends JPanel {
84      /**
85       * For local installations
86       */
87      public SitePane() {
88          // TRANSLATOR: This is the label for a list of installed books
89          this(null, BDMsg.gettext("Installed Books:"));
90      }
91  
92      /**
93       * For remote installations
94       */
95      public SitePane(Installer bookListInstaller) {
96          // TRANSLATOR: This is the label for a list of available books
97          this(bookListInstaller, BDMsg.gettext("Available Books:"));
98      }
99  
100     /**
101      * Internal ctor
102      */
103     private SitePane(Installer bookListInstaller, String labelAcronymn) {
104         installer = bookListInstaller;
105 
106         actions = new ActionFactory(this);
107 
108         BookList bl = installer;
109         if (bl == null) {
110             bl = Books.installed();
111             bl.addBooksListener(new CustomBooksListener());
112         }
113 
114         initialize(labelAcronymn, bl);
115     }
116 
117     /**
118      * Build the GUI components
119      */
120     private void initialize(String labelAcronymn, BookList books) {
121         lblDesc = new JLabel();
122         lblDesc.setBorder(BorderFactory.createEmptyBorder(5, 5, 0, 0));
123 
124         Component left = createAvailablePanel(labelAcronymn, books);
125         Component right = createSelectedPanel();
126         this.setLayout(new BorderLayout());
127         this.add(lblDesc, BorderLayout.NORTH);
128         this.add(createSplitPane(left, right), BorderLayout.CENTER);
129 
130         updateDescription();
131     }
132 
133     /**
134      *
135      */
136     private void updateDescription() {
137         String desc = "#ERROR#";
138 
139         if (installer == null) {
140             int bookCount = Books.installed().getBooks().size();
141             // TRANSLATOR: This label give the number of books that are installed. {0} is a placeholder for the number.
142             desc = BDMsg.gettext("{0} books installed.", Integer.valueOf(bookCount));
143         } else {
144             int bookCount = installer.getBooks().size();
145             if (bookCount == 0) {
146                 StringBuilder buf = new StringBuilder(200);
147                 buf.append("<html><b>");
148                 // TRANSLATOR: This label shows up when the list of available books for a download site is missing.
149                 buf.append(BDMsg.gettext("Click 'Update Available Books' to download an up to date book list."));
150                 buf.append("</b>");
151                 desc = buf.toString();
152             } else {
153                 // TRANSLATOR: This label gives the number of books available at a download site. {0} is a placeholder for the number.
154                 desc = BDMsg.gettext("{0} books available for download.", Integer.valueOf(bookCount));
155             }
156         }
157 
158         lblDesc.setText(desc);
159     }
160 
161     /**
162      *
163      */
164     private Component createSplitPane(Component left, Component right) {
165         JSplitPane split = new FixedSplitPane();
166         split.setDividerLocation(0.3D);
167         split.setResizeWeight(0.3D);
168         split.setOrientation(JSplitPane.HORIZONTAL_SPLIT);
169         split.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
170         split.setDividerSize(10);
171         split.setLeftComponent(left);
172         split.setRightComponent(right);
173         return split;
174     }
175 
176     /**
177      *
178      */
179     private Component createAvailablePanel(String labelAcronymn, BookList books) {
180         JLabel lblAvailable = CWLabel.createJLabel(labelAcronymn);
181 
182         JPanel panel = new JPanel();
183         panel.setLayout(new BorderLayout());
184         panel.add(lblAvailable, BorderLayout.PAGE_START);
185         panel.add(createScrolledTree(books), BorderLayout.CENTER);
186         panel.add(createPanelActions(), BorderLayout.PAGE_END);
187 
188         // Tie the label's mnemonic to the tree
189         lblAvailable.setLabelFor(treAvailable);
190 
191         return panel;
192     }
193 
194     /**
195      *
196      */
197     private Component createSelectedPanel() {
198 
199         // TRANSLATOR: This is the label for the display of information about the selected book
200         JLabel lblSelected = CWLabel.createJLabel(BDMsg.gettext("Selected Book:"));
201         display = new TextPaneBookMetaDataDisplay();
202         lblSelected.setLabelFor(display.getComponent());
203 
204         JScrollPane scrSelected = new CWScrollPane();
205         JPanel panel = new JPanel();
206         panel.setLayout(new BorderLayout());
207         panel.add(lblSelected, BorderLayout.PAGE_START);
208         panel.add(scrSelected, BorderLayout.CENTER);
209         scrSelected.getViewport().add(display.getComponent());
210         return panel;
211     }
212 
213     /**
214      *
215      */
216     private Component createScrolledTree(BookList books) {
217         treAvailable = new JTree();
218         // Turn on tooltips so that they will show
219         ToolTipManager.sharedInstance().registerComponent(treAvailable);
220         treAvailable.setCellRenderer(new BookTreeCellRenderer());
221 
222         setTreeModel(books);
223         // Add lines if viewed in Java Look & Feel
224         treAvailable.putClientProperty("JTree.lineStyle", "Angled");
225         treAvailable.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
226         treAvailable.setCellEditor(null);
227         treAvailable.setRootVisible(false);
228         treAvailable.setShowsRootHandles(true);
229         treAvailable.addTreeSelectionListener(new TreeSelectionListener() {
230             public void valueChanged(TreeSelectionEvent ev) {
231                 selected();
232             }
233         });
234 
235         return new CWScrollPane(treAvailable);
236     }
237 
238     private TreeModel createTreeModel(BookList books) {
239         // return new BooksTreeModel(books);
240         BookSet bmds = new BookSet(books.getBooks());
241         TreeNode bookRoot = new BookNode("root", bmds, 0, BookMetaData.KEY_CATEGORY, BookMetaData.KEY_XML_LANG);
242         return new DefaultTreeModel(bookRoot);
243     }
244 
245     private Book getBook(Object anObj) {
246         Object obj = anObj;
247         if (obj instanceof DefaultMutableTreeNode) {
248             obj = ((DefaultMutableTreeNode) obj).getUserObject();
249         }
250         if (obj instanceof Book) {
251             return (Book) obj;
252         }
253         return null;
254     }
255 
256     private Language getLanguage(Object anObj) {
257         Object obj = anObj;
258         if (obj instanceof DefaultMutableTreeNode) {
259             obj = ((DefaultMutableTreeNode) obj).getUserObject();
260         }
261         if (obj instanceof Language) {
262             return (Language) obj;
263         }
264         return null;
265     }
266 
267     /**
268      *
269      */
270     private Component createPanelActions() {
271         JPanel panel = new JPanel();
272         CWAction action;
273         if (installer != null) {
274             panel.setLayout(new GridLayout(1, 2, 3, 3));
275 
276             // TRANSLATOR: This is the text on an "Install" button.
277             action = actions.addAction("Install", BDMsg.gettext("Install"));
278             // TRANSLATOR: This is the tooltip for an "Install" button.
279             action.setTooltip(BDMsg.gettext("Install the selected book"));
280             action.enable(false);
281             panel.add(new JButton(action));
282 
283             // LATER(DMS): Put back when this works
284             // action = actions.addAction("InstallSearch", UserMsg.gettext("Install with Search"));
285             // action.setTooltip(UserMsg.gettext("Install the selected book along with a search index."));
286             // action.enable(false);
287             // panel.add(new JButton(action));
288 
289             // TRANSLATOR: This is the text on a button that will refresh the list of available books
290             // from a download site
291             action = actions.addAction("Refresh", BDMsg.gettext("Update Available Books"));
292             action.setTooltip(BDMsg.gettext("Download a current listing of books."));
293             panel.add(new JButton(action));
294         } else {
295             panel.setLayout(new GridLayout(3, 2, 3, 3));
296 
297             // TRANSLATOR: This is the text on a "Delete Book" button.
298             action = actions.addAction("Delete", BDMsg.gettext("Delete Book"));
299             // TRANSLATOR: This is the tooltip for a "Delete Book" button.
300             action.setTooltip(BDMsg.gettext("Delete the selected book"));
301             action.enable(false);
302             panel.add(new JButton(action));
303 
304             // TRANSLATOR: This is the text on a "Remove Search Index" button.
305             action = actions.addAction("Unindex", BDMsg.gettext("Remove Search Index"));
306             // TRANSLATOR: This is the tooltip for a "Remove Search Index" button.
307             action.setTooltip(BDMsg.gettext("Remove the search index of the selected book"));
308             action.enable(false);
309             panel.add(new JButton(action));
310 
311             // TRANSLATOR: This is the text on a "Font..." button that brings up a font selection dialog.
312             action = actions.addAction("ChooseFont", BDMsg.gettext("Font..."));
313             // TRANSLATOR: This is the tooltip for a "Font..." button that brings up a font selection dialog.
314             action.setTooltip(BDMsg.gettext("Choose a font for the language or book"));
315             action.enable(false);
316             panel.add(new JButton(action));
317 
318             // TRANSLATOR: This is the text on an "Unlock" button that brings up a dialog box to enter an unlock key.
319             action = actions.addAction("Unlock", BDMsg.gettext("Unlock"));
320             // TRANSLATOR: This is the tooltip for an "Unlock" button that brings up a dialog box to enter an unlock key.
321             action.setTooltip(BDMsg.gettext("Unlock the selected book"));
322             action.enable(false);
323             panel.add(new JButton(action));
324 
325             // TRANSLATOR: This is the text on a "Reset Font" button.
326             // Clicking on this button will restore the original font for the language or book
327             action = actions.addAction("ResetFont", BDMsg.gettext("Reset Font"));
328             // TRANSLATOR: This is the tooltip for a "Reset Font" button.
329             action.setTooltip(BDMsg.gettext("Reset the custom font set for this language or book"));
330             action.enable(false);
331             panel.add(new JButton(action));
332 
333         }
334         return panel;
335     }
336 
337     /**
338      * Delete the current book
339      */
340     public void doDelete() {
341         TreePath path = treAvailable.getSelectionPath();
342         if (path == null) {
343             return;
344         }
345 
346         Object last = path.getLastPathComponent();
347         Book book = getBook(last);
348 
349         try {
350             // TRANSLATOR: Message asking for confirmation of a delete of a book.
351             String msg = BDMsg.gettext("Are you sure you want to delete {0}?", book.getName());
352             // TRANSLATOR: Title of a dialog that asks whether the book should be deleted.
353             if (CWOptionPane.showConfirmDialog(this, msg, BDMsg.gettext("Delete Book"), JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION) {
354                 book.getDriver().delete(book);
355 
356                 IndexManager imanager = IndexManagerFactory.getIndexManager();
357                 if (imanager.isIndexed(book)) {
358                     imanager.deleteIndex(book);
359                 }
360             }
361         } catch (BookException e) {
362             Reporter.informUser(this, e);
363         }
364     }
365 
366     /**
367      * Unlock the current book
368      */
369     public void doUnlock() {
370         TreePath path = treAvailable.getSelectionPath();
371         if (path == null) {
372             return;
373         }
374 
375         Object last = path.getLastPathComponent();
376         Book book = getBook(last);
377 
378         // TRANSLATOR: Title to a dialog asking the user to provide an unlock key.
379         String title = BDMsg.gettext("Unlock Book");
380         StringBuilder msg = new StringBuilder(200);
381         // TRANSLATOR: Message asking the user to provide an unlock key.
382         // The unlock key is typically a string like AbCd8364efGH8472.
383         msg.append(BDMsg.gettext("Please enter the unlock key for:"));
384         // To allow for long book names, put the name on the next line.
385         msg.append('\n');
386         msg.append(book.getName());
387         String unlockKey = (String) CWOptionPane.showInputDialog(this, msg.toString(), title, JOptionPane.QUESTION_MESSAGE, null, null, book.getUnlockKey());
388 
389         if (unlockKey != null && unlockKey.length() > 0) {
390             book.unlock(unlockKey);
391             Books.installed().addBook(book);
392         }
393     }
394 
395     /**
396      * Delete the current book
397      */
398     public void doUnindex() {
399         TreePath path = treAvailable.getSelectionPath();
400         if (path == null) {
401             return;
402         }
403 
404         Object last = path.getLastPathComponent();
405         Book book = getBook(last);
406 
407         try {
408             IndexManager imanager = IndexManagerFactory.getIndexManager();
409             if (imanager.isIndexed(book)) {
410                 // TRANSLATOR: Message asking the user to confirm the delete of a search index for a book.
411                 // {0} is a placeholder for the name of the book.
412                 String formattedMsg = BDMsg.gettext("Are you sure you want to remove the index for {0}?", book.getName());
413                 // TRANSLATOR: Title to the dialog that asks for confirmation of the deletion 
414                 // of a book's search index.
415                 if (CWOptionPane.showConfirmDialog(this, formattedMsg, BDMsg.gettext("Remove Index for Book"), JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION) {
416                     imanager.deleteIndex(book);
417                 }
418             }
419             actions.findAction("Unindex").setEnabled(imanager.isIndexed(book));
420         } catch (BookException e) {
421             Reporter.informUser(this, e);
422         }
423     }
424 
425     /**
426      * Reload and redisplay the list of books
427      */
428     public void doRefresh() {
429         if (installer != null) {
430             try {
431                 int webAccess = InternetWarning.GRANTED;
432                 if (WebWarning.instance().isShown()) {
433                     webAccess = InternetWarning.showDialog(this, "?");
434                 }
435 
436                 if (webAccess == InternetWarning.GRANTED) {
437                     installer.reloadBookList();
438                     setTreeModel(installer);
439                 }
440             } catch (InstallException ex) {
441                 Reporter.informUser(this, ex);
442             }
443         }
444     }
445 
446     /**
447      * Kick off the installer
448      */
449     public void doInstall() {
450         if (installer == null) {
451             return;
452         }
453 
454         TreePath path = treAvailable.getSelectionPath();
455         if (path == null) {
456             return;
457         }
458 
459         int webAccess = InternetWarning.GRANTED;
460         if (WebWarning.instance().isShown()) {
461             webAccess = InternetWarning.showDialog(this, "?");
462         }
463 
464         if (webAccess != InternetWarning.GRANTED) {
465             return;
466         }
467 
468         Object last = path.getLastPathComponent();
469         Book name = getBook(last);
470 
471         try {
472             // Is the book already installed? Then nothing to do.
473             Book book = Books.installed().getBook(name.getName());
474             if (book != null && !installer.isNewer(name)) {
475                 // TRANSLATOR: Popup message indicating that the book is already installed.
476                 // {0} is a placeholder for the name of the book.
477                 Reporter.informUser(this, BDMsg.gettext("Book already installed: {0}", name.getName()));
478                 return;
479             }
480 
481             float size = installer.getSize(name) / 1024.0F;
482 
483             String formattedMsg = "";
484             if (size > 1024.0F) {
485                 size /= 1024.0F;
486                 // TRANSLATOR: The size of the book is provided so that the user can decide whether to continue a download.
487                 // {0} is a placeholder for the name of the book.
488                 // {1,number,###,###,###.#} is a placeholder for the size of the download in megabytes.
489                 // The pattern ###,###,###.# says to separate the number at every third digit and
490                 //    to show one digit of fractional part.
491                 // The , and . will automatically be converted into the user's proper separators.
492                 formattedMsg = BDMsg.gettext("{0} is {1,number,###,###,###.#}MB. Continue?", name.getName(), Float.valueOf(size));
493             } else {
494                 // TRANSLATOR: The size of the book is provided so that the user can decide whether to continue a download.
495                 // {0} is a placeholder for the name of the book.
496                 // {1,number,###,###,###.#} is a placeholder for the size of the download in kilobytes.
497                 // The pattern ###,###,###.# says to separate the number at every third digit and
498                 //    to show one digit of fractional part.
499                 // The , and . will automatically be converted into the user's proper separators.
500                 formattedMsg = BDMsg.gettext("{0} is {1,number,###,###,###.#}KB. Continue?", name.getName(), Float.valueOf(size));
501             }
502 
503             // TRANSLATOR: Title to a dialog asking whether the user should download the book based on it's size.
504             if (CWOptionPane.showConfirmDialog(this, formattedMsg, BDMsg.gettext("Download Book"), JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION) {
505                 installer.install(name);
506             }
507         } catch (InstallException ex) {
508             Reporter.informUser(this, ex);
509         }
510     }
511 
512     /**
513      * Kick off the installer
514      */
515     public void doInstallSearch() {
516         doInstall();
517 
518         TreePath path = treAvailable.getSelectionPath();
519         if (path != null) {
520             Object last = path.getLastPathComponent();
521             Book book = getBook(last);
522             IndexResolver.scheduleIndex(book, this);
523         }
524     }
525 
526     /**
527      * Get a font for the current selection
528      */
529     public void doChooseFont() {
530         TreePath path = treAvailable.getSelectionPath();
531         if (path == null) {
532             return;
533         }
534 
535         Object last = path.getLastPathComponent();
536         Book book = getBook(last);
537         if (book != null) {
538             // TRANSLATOR: Title to a dialog allowing the user to choose a font face, size and style.
539             Font picked = FontChooser.showDialog(this, BDMsg.gettext("Choose Font"), BookFont.instance().getFont(book));
540             BookFont.instance().setFont(book, picked);
541         }
542 
543         Language language = getLanguage(last);
544         if (language != null) {
545             // TRANSLATOR: Title to a dialog allowing the user to choose a font face, size and style.
546             Font picked = FontChooser.showDialog(this, BDMsg.gettext("Choose Font"), BookFont.instance().getFont(language));
547             BookFont.instance().setFont(language, picked);
548         }
549         actions.findAction("ResetFont").setEnabled(BookFont.instance().isSet(book, language));
550     }
551 
552     /**
553      * Resets any font specifically set for this Book / Language
554      */
555     public void doResetFont() {
556         TreePath path = treAvailable.getSelectionPath();
557         if (path == null) {
558             return;
559         }
560 
561         Object last = path.getLastPathComponent();
562         Book book = getBook(last);
563         Language language = getLanguage(last);
564         BookFont.instance().resetFont(book, language);
565         actions.findAction("ResetFont").setEnabled(false);
566     }
567 
568     /**
569      * Something has been (un)selected in the tree
570      */
571     protected void selected() {
572         TreePath path = treAvailable.getSelectionPath();
573 
574         Book book = null;
575         Language lang = null;
576         if (path != null) {
577             Object last = path.getLastPathComponent();
578             book = getBook(last);
579             lang = getLanguage(last);
580         }
581 
582         display.setBook(book);
583 
584         actions.findAction("Delete").setEnabled(book != null && book.getDriver().isDeletable(book));
585         actions.findAction("Unlock").setEnabled(book != null && book.isEnciphered());
586         actions.findAction("Unindex").setEnabled(book != null && IndexManagerFactory.getIndexManager().isIndexed(book));
587         actions.findAction("Install").setEnabled(book != null && book.isSupported());
588         actions.findAction("InstallSearch").setEnabled(book != null && book.isSupported() && book.getBookCategory() == BookCategory.BIBLE);
589         actions.findAction("ChooseFont").setEnabled(book != null || lang != null);
590         actions.findAction("ResetFont").setEnabled(BookFont.instance().isSet(book, lang));
591     }
592 
593     public void setTreeModel(BookList books) {
594         treAvailable.setModel(createTreeModel(books));
595     }
596 
597     /**
598      * When new books are added we need to reflect the change in this tree.
599      */
600     final class CustomBooksListener implements BooksListener {
601         /* (non-Javadoc)
602          * @see org.crosswire.jsword.book.BooksListener#bookAdded(org.crosswire.jsword.book.BooksEvent)
603          */
604         public void bookAdded(BooksEvent ev) {
605             setTreeModel((BookList) ev.getSource());
606         }
607 
608         /* (non-Javadoc)
609          * @see org.crosswire.jsword.book.BooksListener#bookRemoved(org.crosswire.jsword.book.BooksEvent)
610          */
611         public void bookRemoved(BooksEvent ev) {
612             setTreeModel((BookList) ev.getSource());
613         }
614     }
615 
616     /**
617      * Serialization support.
618      * 
619      * @param is
620      * @throws IOException
621      * @throws ClassNotFoundException
622      */
623     private void readObject(ObjectInputStream is) throws IOException, ClassNotFoundException {
624         // Broken but we don't serialize views
625         installer = null;
626         display = null;
627         actions = new ActionFactory(this);
628         is.defaultReadObject();
629     }
630 
631 
632     /**
633      * From which we get our list of installable books
634      */
635     protected transient Installer installer;
636 
637     /**
638      * actions are held by this ActionFactory
639      */
640     private transient ActionFactory actions;
641 
642     /*
643      * GUI Components
644      */
645     private JTree treAvailable;
646     private transient TextPaneBookMetaDataDisplay display;
647     private JLabel lblDesc;
648 
649     /**
650      * Serialization ID
651      */
652     private static final long serialVersionUID = 3616445692051075634L;
653 }
654