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.versification.system.Versifications;
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: 2005
20   *     The copyright to this program is held by it's authors.
21   *
22   * ID: $Id: PassageSelectionPane.java 2223 2012-01-26 21:28:02Z dmsmith $
23   */
24  package org.crosswire.bibledesktop.book;
25  
26  import java.awt.BorderLayout;
27  import java.awt.Component;
28  import java.awt.Frame;
29  import java.awt.GridBagConstraints;
30  import java.awt.GridBagLayout;
31  import java.awt.Insets;
32  import java.awt.event.ActionEvent;
33  import java.awt.event.ActionListener;
34  import java.awt.event.KeyEvent;
35  import java.io.IOException;
36  import java.io.ObjectInputStream;
37  
38  import javax.swing.BorderFactory;
39  import javax.swing.Icon;
40  import javax.swing.JButton;
41  import javax.swing.JComponent;
42  import javax.swing.JDialog;
43  import javax.swing.JLabel;
44  import javax.swing.JList;
45  import javax.swing.JOptionPane;
46  import javax.swing.JPanel;
47  import javax.swing.JTextField;
48  import javax.swing.JTree;
49  import javax.swing.KeyStroke;
50  import javax.swing.WindowConstants;
51  import javax.swing.event.DocumentEvent;
52  import javax.swing.event.DocumentListener;
53  import javax.swing.event.ListSelectionEvent;
54  import javax.swing.event.ListSelectionListener;
55  import javax.swing.event.TreeSelectionEvent;
56  import javax.swing.event.TreeSelectionListener;
57  import javax.swing.tree.TreePath;
58  
59  import org.crosswire.bibledesktop.BDMsg;
60  import org.crosswire.bibledesktop.passage.RangeListModel;
61  import org.crosswire.bibledesktop.passage.WholeBibleTreeModel;
62  import org.crosswire.bibledesktop.passage.WholeBibleTreeNode;
63  import org.crosswire.common.swing.ActionFactory;
64  import org.crosswire.common.swing.CWAction;
65  import org.crosswire.common.swing.CWLabel;
66  import org.crosswire.common.swing.CWScrollPane;
67  import org.crosswire.common.swing.GuiUtil;
68  import org.crosswire.jsword.passage.NoSuchKeyException;
69  import org.crosswire.jsword.passage.Passage;
70  import org.crosswire.jsword.passage.PassageEvent;
71  import org.crosswire.jsword.passage.PassageKeyFactory;
72  import org.crosswire.jsword.passage.PassageListener;
73  import org.crosswire.jsword.passage.RestrictionType;
74  import org.crosswire.jsword.passage.VerseRange;
75  import org.crosswire.jsword.versification.Versification;
76  import org.crosswire.jsword.versification.system.Versifications;
77  
78  /**
79   * A JPanel (or dialog) that presents a interactive GUI way to select passages.
80   * 
81   * @see gnu.gpl.License for license details.<br>
82   *      The copyright to this program is held by it's authors.
83   * @author Joe Walker [joe at eireneh dot com]
84   */
85  public class PassageSelectionPane extends JPanel {
86      /**
87       * Constructor for PassageSelectionPane.
88       */
89      public PassageSelectionPane() {
90          icoGood = GuiUtil.getIcon(GOOD_ICON);
91          icoBad = GuiUtil.getIcon(BAD_ICON);
92  
93          init();
94      }
95  
96      /**
97       * GUI init
98       */
99      private void init() {
100         actions = new ActionFactory(this);
101         CWAction action;
102 
103         // TRANSLATOR: This is the label of the Book/Chapter/Verse tree of the entire Bible
104         JLabel lblAll = CWLabel.createJLabel(BDMsg.gettext("All Verses"));
105         // TRANSLATOR: This is the label of the Book/Chapter/Verse tree of the chosen verses
106         JLabel lblSel = CWLabel.createJLabel(BDMsg.gettext("Selected Verses"));
107         // TRANSLATOR: This is the label for the button that moves Books/Chapter/Verses from the Selected Verses tree
108         action = actions.addAction("DeleteVerse", BDMsg.gettext("Remove <"));
109         // TRANSLATOR: This is the tooltip for the button that moves Books/Chapter/Verses from the Selected Verses tree
110         action.setTooltip(BDMsg.gettext("Delete verses from the list selected."));
111         JButton deleteButton = new JButton(action);
112         // TRANSLATOR: This is the label for the button that moves Books/Chapter/Verses to the Selected Verses tree
113         action = actions.addAction("AddVerse", BDMsg.gettext("Add >"));
114         // TRANSLATOR: This is the tooltip for the button that moves Books/Chapter/Verses to the Selected Verses tree
115         action.setTooltip(BDMsg.gettext("Add verses to list selected."));
116         JButton addButton = new JButton(action);
117 
118         this.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
119         this.setLayout(new GridBagLayout());
120         this.add(lblAll, new GridBagConstraints(0, 0, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(5, 10, 5, 5), 0, 0));
121         this.add(createScrolledTree(lblAll), new GridBagConstraints(0, 1, 1, 4, 0.5, 0.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 10, 10, 2), 0, 0));
122         this.add(new JPanel(), new GridBagConstraints(1, 1, 1, 1, 0.0, 0.5, GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));
123         this.add(deleteButton, new GridBagConstraints(1, 2, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, new Insets(5, 5, 5, 5), 0, 0));
124         this.add(addButton, new GridBagConstraints(1, 3, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, new Insets(5, 5, 5, 5), 0, 0));
125         this.add(new JPanel(), new GridBagConstraints(1, 4, 1, 1, 0.0, 0.5, GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));
126         this.add(lblSel, new GridBagConstraints(2, 0, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(5, 5, 5, 10), 0, 0));
127         this.add(createScrolledList(lblSel), new GridBagConstraints(2, 1, 1, 4, 0.5, 0.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 2, 10, 10), 0, 0));
128         this.add(createMessageLabel(), new GridBagConstraints(0, 5, 3, 1, 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, new Insets(0, 10, 5, 10), 0, 0));
129         this.add(createDisplayPanel(), new GridBagConstraints(0, 6, 3, 1, 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, new Insets(0, 10, 0, 10), 0, 0));
130         GuiUtil.applyDefaultOrientation(this);
131     }
132 
133     /**
134      *
135      */
136     private Component createScrolledTree(JLabel label) {
137         treAll = new JTree();
138         treAll.setModel(new WholeBibleTreeModel());
139         treAll.setShowsRootHandles(true);
140         treAll.setRootVisible(false);
141         treAll.putClientProperty("JTree.lineStyle", "Angled");
142         treAll.addTreeSelectionListener(new TreeSelectionListener() {
143             public void valueChanged(TreeSelectionEvent ev) {
144                 treeSelected();
145             }
146         });
147 
148         label.setLabelFor(treAll);
149 
150         return new CWScrollPane(treAll);
151     }
152 
153     /**
154      *
155      */
156     private Component createScrolledList(JLabel label) {
157         model = new RangeListModel(RestrictionType.CHAPTER);
158         lstSel = new JList(model);
159         lstSel.addListSelectionListener(new ListSelectionListener() {
160             public void valueChanged(ListSelectionEvent ev) {
161                 listSelected();
162             }
163         });
164 
165         label.setLabelFor(lstSel);
166 
167         return new CWScrollPane(lstSel);
168     }
169 
170     /**
171      *
172      */
173     private Component createDisplayPanel() {
174         txtDisplay = new JTextField();
175         txtDisplay.getDocument().addDocumentListener(new CustomDocumentEvent());
176 
177         // TRANSLATOR: This is the label for the text box showing a compact representation
178         // of the verses in the Selected Verses tree
179         JLabel lblDisplay = CWLabel.createJLabel(BDMsg.gettext("Verses"));
180         lblDisplay.setLabelFor(txtDisplay);
181 
182         JPanel panel = new JPanel();
183         panel.setLayout(new BorderLayout());
184         panel.add(txtDisplay, BorderLayout.CENTER);
185         panel.add(lblDisplay, BorderLayout.LINE_START);
186         return panel;
187     }
188 
189     /**
190      *
191      */
192     private Component createMessageLabel() {
193         lblMessage = new JLabel();
194 
195         return lblMessage;
196     }
197 
198     /**
199      * Called whenever the passage changes to update the text box.
200      */
201     protected void copyListToText() {
202         if (changing) {
203             return;
204         }
205 
206         changing = true;
207         txtDisplay.setText(ref.getName());
208         updateMessageSummary();
209         changing = false;
210     }
211 
212     /**
213      * Called whenever the text box changes to update the list
214      */
215     protected void copyTextToList() {
216         if (changing) {
217             return;
218         }
219 
220         changing = true;
221         String refstr = txtDisplay.getText();
222 
223         try {
224             // AV11N(DMS): Is this right?
225             Versification v11n = Versifications.instance().getDefaultVersification();
226             Passage temp = (Passage) keyf.getKey(v11n, refstr);
227             ref.clear();
228             ref.addAll(temp);
229             model.setPassage(ref);
230 
231             setValidPassage(true);
232             updateMessageSummary();
233         } catch (NoSuchKeyException ex) {
234             setValidPassage(false);
235             updateMessage(ex);
236         }
237         changing = false;
238     }
239 
240     /**
241      * Update the UI when the validity of the passage changes
242      * 
243      * @param valid
244      */
245     private void setValidPassage(boolean valid) {
246         lstSel.setEnabled(valid);
247         treAll.setEnabled(valid);
248         actions.findAction("AddVerse").setEnabled(valid);
249         actions.findAction("DeleteVerse").setEnabled(valid);
250     }
251 
252     /**
253      * Write out an error message to the message label
254      * 
255      * @param ex
256      */
257     private void updateMessage(NoSuchKeyException ex) {
258         // TRANSLATOR: Error condition: An unexpected unknown error occurred.
259         // Tell the user about it. {0} is a placeholder for the error that occurred.
260         lblMessage.setText(BDMsg.gettext("Error: {0}", ex.getMessage()));
261         lblMessage.setIcon(icoBad);
262     }
263 
264     /**
265      * Write out an summary message to the message label
266      */
267     private void updateMessageSummary() {
268         // TRANSLATOR: Output the Summary label followed by the passage
269         // that the user has built using the Select Passage Wizard.
270         // {0} is the placeholder for the passage reference.
271         lblMessage.setText(BDMsg.gettext("Summary: {0}", ref.getOverview()));
272         lblMessage.setIcon(icoGood);
273     }
274 
275     /**
276      * Open us in a new (optionally modal) dialog window
277      * 
278      * @param parent
279      *            The component to which to attach the new dialog
280      * @param title
281      *            The title for the new dialog
282      * @param modal
283      */
284     public String showInDialog(Component parent, String title, boolean modal, String refstr) {
285         try {
286             // AV11N(DMS): Is this right?
287             Versification v11n = Versifications.instance().getDefaultVersification();
288             ref = (Passage) keyf.getKey(v11n, refstr);
289 
290             txtDisplay.setText(refstr);
291 
292             ref.addPassageListener(new CustomPassageListener());
293             updateMessageSummary();
294         } catch (NoSuchKeyException ex) {
295             setValidPassage(false);
296             updateMessage(ex);
297         }
298 
299         // Make sure the add/delete buttons start right
300         treeSelected();
301         listSelected();
302 
303         Frame root = JOptionPane.getFrameForComponent(parent);
304         dlgMain = new JDialog(root);
305 
306         JPanel pnlAction = new JPanel();
307         KeyStroke esc = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0);
308         bailout = true;
309 
310         // TRANSLATOR: This is the label for the button that closes the window and returns the selected verses
311         CWAction action = actions.addAction("Done", BDMsg.gettext("OK"));
312         // TRANSLATOR: This is the tooltip for the button that closes the window and returns the selected verses
313         action.setTooltip(BDMsg.gettext("Close this window."));
314         JButton btnGo = new JButton(action);
315 
316         pnlAction.setLayout(new BorderLayout());
317         pnlAction.setBorder(BorderFactory.createEmptyBorder(5, 5, 15, 20));
318         pnlAction.add(btnGo, BorderLayout.LINE_END);
319 
320         ActionListener closer = new ActionListener() {
321             public void actionPerformed(ActionEvent ev) {
322                 dlgMain.dispose();
323             }
324         };
325 
326         dlgMain.getContentPane().setLayout(new BorderLayout());
327         dlgMain.getContentPane().add(this, BorderLayout.CENTER);
328         dlgMain.getContentPane().add(pnlAction, BorderLayout.SOUTH);
329         dlgMain.getRootPane().setDefaultButton(btnGo);
330         dlgMain.getRootPane().registerKeyboardAction(closer, esc, JComponent.WHEN_IN_FOCUSED_WINDOW);
331         dlgMain.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
332         dlgMain.setTitle(title);
333         dlgMain.setModal(modal);
334 
335         GuiUtil.applyDefaultOrientation(dlgMain);
336         GuiUtil.restrainedPack(dlgMain, 0.5f, 0.75f);
337         GuiUtil.centerOnScreen(dlgMain);
338         dlgMain.setVisible(true);
339 
340         if (bailout) {
341             return null;
342         }
343         return txtDisplay.getText();
344     }
345 
346     /**
347      * Add from the tree to the list
348      */
349     public void doAddVerse() {
350         TreePath[] selected = treAll.getSelectionPaths();
351         if (selected != null) {
352             for (int i = 0; i < selected.length; i++) {
353                 WholeBibleTreeNode node = (WholeBibleTreeNode) selected[i].getLastPathComponent();
354                 VerseRange range = node.getVerseRange();
355                 ref.add(range);
356             }
357             model.setPassage(ref);
358         }
359     }
360 
361     /**
362      * Remove the selected items from the list
363      */
364     public void doDeleteVerse() {
365         Object[] selected = lstSel.getSelectedValues();
366         if (selected != null) {
367             for (int i = 0; i < selected.length; i++) {
368                 VerseRange range = (VerseRange) selected[i];
369                 ref.remove(range);
370             }
371             model.setPassage(ref);
372         }
373     }
374 
375     /**
376      * Someone clicked on OK
377      */
378     public void doDone() {
379         bailout = false;
380         dlgMain.dispose();
381     }
382 
383     /**
384      * The tree selection has changed
385      */
386     /*private*/final void treeSelected() {
387         TreePath[] selected = treAll.getSelectionPaths();
388         actions.findAction("AddVerse").setEnabled(selected != null && selected.length > 0);
389     }
390 
391     /**
392      * List selection has changed
393      */
394     /*private*/final void listSelected() {
395         Object[] selected = lstSel.getSelectedValues();
396         actions.findAction("DeleteVerse").setEnabled(selected != null && selected.length > 0);
397     }
398 
399     /**
400      * Serialization support.
401      * 
402      * @param is
403      * @throws IOException
404      * @throws ClassNotFoundException
405      */
406     private void readObject(ObjectInputStream is) throws IOException, ClassNotFoundException {
407         // We don't serialize views
408         icoGood = GuiUtil.getIcon(GOOD_ICON);
409         icoBad = GuiUtil.getIcon(BAD_ICON);
410         keyf = PassageKeyFactory.instance();
411         actions = new ActionFactory(this);
412         is.defaultReadObject();
413     }
414 
415     private static final String GOOD_ICON = "toolbarButtonGraphics/general/About24.gif";
416     private static final String BAD_ICON = "toolbarButtonGraphics/general/Stop24.gif";
417 
418     /**
419      * To convert strings into Biblical keys
420      */
421     protected transient PassageKeyFactory keyf = PassageKeyFactory.instance();
422 
423     /**
424      * If escape was pressed we don't want to update the parent
425      */
426     protected boolean bailout;
427 
428     /**
429      * Prevent us getting in an event cascade loop
430      */
431     private boolean changing;
432 
433     /**
434      * The passage we are editing
435      */
436     private Passage ref;
437 
438     /**
439      * The ActionFactory holding the actions used by this Component.
440      */
441     private transient ActionFactory actions;
442 
443     /*
444      * GUI Components
445      */
446     private transient Icon icoGood;
447     private transient Icon icoBad;
448     private JTree treAll;
449     private JList lstSel;
450     private RangeListModel model;
451     private JTextField txtDisplay;
452     private JLabel lblMessage;
453     protected JDialog dlgMain;
454 
455     /**
456      * Serialization ID
457      */
458     private static final long serialVersionUID = 3546920298944673072L;
459 
460     /**
461      * Update the list whenever the textbox changes
462      */
463     class CustomDocumentEvent implements DocumentListener {
464         /* (non-Javadoc)
465          * @see javax.swing.event.DocumentListener#insertUpdate(javax.swing.event.DocumentEvent)
466          */
467         public void insertUpdate(DocumentEvent ev) {
468             copyTextToList();
469         }
470 
471         /* (non-Javadoc)
472          * @see javax.swing.event.DocumentListener#removeUpdate(javax.swing.event.DocumentEvent)
473          */
474         public void removeUpdate(DocumentEvent ev) {
475             copyTextToList();
476         }
477 
478         /* (non-Javadoc)
479          * @see javax.swing.event.DocumentListener#changedUpdate(javax.swing.event.DocumentEvent)
480          */
481         public void changedUpdate(DocumentEvent ev) {
482             copyTextToList();
483         }
484     }
485 
486     /**
487      * To update the textbox when the passage changes
488      */
489     class CustomPassageListener implements PassageListener {
490         /* (non-Javadoc)
491          * @see org.crosswire.jsword.passage.PassageListener#versesAdded(org.crosswire.jsword.passage.PassageEvent)
492          */
493         public void versesAdded(PassageEvent ev) {
494             copyListToText();
495         }
496 
497         /* (non-Javadoc)
498          * @see org.crosswire.jsword.passage.PassageListener#versesRemoved(org.crosswire.jsword.passage.PassageEvent)
499          */
500         public void versesRemoved(PassageEvent ev) {
501             copyListToText();
502         }
503 
504         /* (non-Javadoc)
505          * @see org.crosswire.jsword.passage.PassageListener#versesChanged(org.crosswire.jsword.passage.PassageEvent)
506          */
507         public void versesChanged(PassageEvent ev) {
508             copyListToText();
509         }
510     }
511 }
512