1   /**
2    * Distribution License:
3    * JSword is free software; you can redistribute it and/or modify it under
4    * the terms of the GNU Lesser General Public License, version 2.1 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 Lesser General Public License for more details.
9    *
10   * The License is available on the internet at:
11   *       http://www.gnu.org/copyleft/lgpl.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: TreeConfigEditor.java 2091 2011-03-07 04:15:31Z dmsmith $
21   */
22  package org.crosswire.common.config.swing;
23  
24  import java.awt.BorderLayout;
25  import java.awt.CardLayout;
26  import java.awt.Color;
27  import java.awt.Dimension;
28  import java.awt.Font;
29  import java.util.ArrayList;
30  import java.util.Iterator;
31  import java.util.List;
32  
33  import javax.swing.BorderFactory;
34  import javax.swing.JLabel;
35  import javax.swing.JPanel;
36  import javax.swing.JScrollPane;
37  import javax.swing.JSplitPane;
38  import javax.swing.JTree;
39  import javax.swing.SwingConstants;
40  import javax.swing.event.EventListenerList;
41  import javax.swing.event.TreeModelEvent;
42  import javax.swing.event.TreeModelListener;
43  import javax.swing.event.TreeSelectionEvent;
44  import javax.swing.event.TreeSelectionListener;
45  import javax.swing.tree.DefaultTreeCellRenderer;
46  import javax.swing.tree.TreeModel;
47  import javax.swing.tree.TreePath;
48  
49  import org.crosswire.common.config.Choice;
50  import org.crosswire.common.config.Config;
51  import org.crosswire.common.swing.CWScrollPane;
52  import org.crosswire.common.swing.FixedSplitPane;
53  import org.crosswire.common.swing.FormPane;
54  import org.crosswire.common.swing.GuiUtil;
55  import org.crosswire.common.swing.CWMsg;
56  
57  /**
58   * A Configuration Editor that provides a tree for navigating to options.
59   * 
60   * @see gnu.lgpl.License for license details.<br>
61   *      The copyright to this program is held by it's authors.
62   * @author Joe Walker [joe at eireneh dot com]
63   */
64  public class TreeConfigEditor extends AbstractConfigEditor {
65      /**
66       * <br />
67       * Danger - this method is not called by the TreeConfigEditor constructor,
68       * it is called by the AbstractConfigEditor constructor so any field
69       * initializers will be called AFTER THIS METHOD EXECUTES so don't use field
70       * initializers.
71       */
72      @Override
73      protected void initializeGUI() {
74          JPanel panel = new JPanel();
75          JPanel blank = new JPanel();
76          DefaultTreeCellRenderer dtcr = new DefaultTreeCellRenderer();
77          // prevent truncation (This is a hack!)
78          dtcr.setPreferredSize(new Dimension(1200, 100));
79          ctm = new ConfigureTreeModel();
80          tree = new JTree();
81          title = new JLabel();
82          deck = new JPanel();
83          layout = new CardLayout();
84  
85          // TRANSLATOR: Label indicating that the user should select a preference category.
86          blank.add(new JLabel(CWMsg.gettext("Select a preference category")));
87  
88          deck.setLayout(layout);
89          deck.add(blank, BLANK);
90  
91          dtcr.setLeafIcon(TASK_ICON_SMALL);
92  
93          // These settings will need to change if we have a true tree.
94          tree.setModel(ctm);
95          tree.setCellRenderer(dtcr);
96          tree.setShowsRootHandles(false);
97          tree.setRootVisible(false);
98          tree.putClientProperty("JTree.lineStyle", "None");
99          tree.setSelectionRow(0);
100         tree.addTreeSelectionListener(new TreeSelectionListener() {
101             public void valueChanged(TreeSelectionEvent ev) {
102                 selectCard();
103             }
104         });
105 
106         title.setIcon(TASK_ICON_LARGE);
107         title.setFont(getFont().deriveFont(Font.PLAIN, 16));
108         title.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
109         title.setBackground(Color.gray);
110         title.setForeground(Color.white);
111         title.setOpaque(true);
112         // TRANSLATOR: This is the label for the banner when one opens Options/Preferences.
113         title.setText(CWMsg.gettext("Preferences"));
114         title.setAlignmentX(SwingConstants.LEADING);
115 
116         panel.setLayout(new BorderLayout());
117         panel.setBorder(BorderFactory.createEtchedBorder());
118         panel.add(BorderLayout.NORTH, title);
119         panel.add(BorderLayout.CENTER, deck);
120 
121         setLayout(new BorderLayout());
122         setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
123 
124         JSplitPane sptMain = new FixedSplitPane();
125         sptMain.setOrientation(JSplitPane.HORIZONTAL_SPLIT);
126         sptMain.setDividerLocation(0.25D);
127         sptMain.setResizeWeight(0.25D);
128         sptMain.setLeftComponent(new CWScrollPane(tree));
129         sptMain.setRightComponent(panel);
130 
131         add(BorderLayout.CENTER, sptMain);
132         add(BorderLayout.SOUTH, new ButtonPane(this));
133         GuiUtil.applyDefaultOrientation(this);
134     }
135 
136     /**
137      * Updates to the tree that we need to do on any change
138      */
139     @Override
140     protected void updateTree() {
141         // expand the tree
142         int row = 0;
143         while (row < tree.getRowCount()) {
144             tree.expandRow(row++);
145         }
146 
147         ctm.fireTreeStructureChanged(this);
148     }
149 
150     /**
151      * Add a Choice to our set of panels
152      */
153     @Override
154     protected void addChoice(Choice model) {
155         if (model.isHidden()) {
156             return;
157         }
158 
159         super.addChoice(model);
160 
161         // Sort the tree out
162         String path = Config.getPath(model.getFullPath());
163         FormPane card = decks.get(path);
164         if (card != null && card.getParent() == null) {
165             JScrollPane scroll = new CWScrollPane(card);
166             scroll.setBorder(BorderFactory.createEmptyBorder());
167             deck.add(scroll, path);
168         }
169     }
170 
171     /**
172      * Add a Choice to our set of panels
173      */
174     @Override
175     protected void removeChoice(Choice model) {
176         super.removeChoice(model);
177 
178         // Sort the tree out
179         String path = Config.getPath(model.getFullPath());
180         FormPane card = decks.get(path);
181         if (card != null && card.isEmpty()) {
182             deck.remove(card.getParent());
183         }
184     }
185 
186     /**
187      * Used to update the configuration panel whenever someone selects a
188      * different item form the tree on the LHS of the configuration dialog.
189      */
190     public void selectCard() {
191         Object obj = tree.getLastSelectedPathComponent();
192         if (obj == null) {
193             return;
194         }
195 
196         // TRANSLATOR: This is the label for the banner when one opens a type of Options/Preferences.
197         // {0} is the type of preference, e.g. Bible Display
198         title.setText(CWMsg.gettext("{0} Preferences", obj));
199 
200         // Get the name of the current deck
201         Object[] list = tree.getSelectionPath().getPath();
202         StringBuilder path = new StringBuilder();
203 
204         for (int i = 1; i < list.length; i++) {
205             if (i > 1) {
206                 path.append('.');
207             }
208 
209             path.append(list[i].toString());
210         }
211 
212         String key = path.toString();
213         GuiUtil.applyDefaultOrientation(deck);
214         if (decks.containsKey(key)) {
215             layout.show(deck, key);
216         } else {
217             layout.show(deck, BLANK);
218         }
219 
220         deck.repaint();
221     }
222 
223     /**
224      * The name of the blank tab
225      */
226     protected static final String BLANK = "$$BLANK$$";
227 
228     /**
229      * The tree containing the Field sets
230      */
231     protected JTree tree;
232 
233     /**
234      * The custom tree model for the tree
235      */
236     protected transient ConfigureTreeModel ctm;
237 
238     /**
239      * The title for the config panels
240      */
241     protected JLabel title;
242 
243     /**
244      * Contains the configuration panels
245      */
246     protected JPanel deck;
247 
248     /**
249      * Layout for the config panels
250      */
251     protected CardLayout layout;
252 
253     /**
254      * Serialization ID
255      */
256     private static final long serialVersionUID = 3256720688860576049L;
257 
258     /**
259      * A custom data model for the TreeConfig Tree
260      * 
261      * @author Claude Duguay
262      * @author Joe Walker
263      */
264     protected class ConfigureTreeModel implements TreeModel {
265         /*
266          * (non-Javadoc)
267          * 
268          * @see javax.swing.tree.TreeModel#getRoot()
269          */
270         public Object getRoot() {
271             return root;
272         }
273 
274         /**
275          * Get a List of the children rooted at path
276          */
277         protected List<String> getChildren(String path) {
278             List<String> retcode = new ArrayList<String>();
279 
280             Iterator<Choice> it = config.iterator();
281             while (it.hasNext()) {
282                 Choice choice = it.next();
283                 if (choice.isHidden()) {
284                     continue;
285                 }
286 
287                 String temp = choice.getFullPath();
288 
289                 if (temp.startsWith(path) && !temp.equals(path)) {
290                     // Chop off the similar start
291                     temp = temp.substring(path.length());
292                     if (temp.charAt(0) == '.') {
293                         temp = temp.substring(1);
294                     }
295 
296                     // Chop off all after the first dot
297                     int dot_pos = temp.indexOf('.');
298                     if (dot_pos == -1) {
299                         continue;
300                     }
301                     temp = temp.substring(0, dot_pos);
302 
303                     // Add it to the list if needed
304                     if (temp.length() > 0 && !retcode.contains(temp)) {
305                         retcode.add(temp);
306                     }
307                 }
308             }
309 
310             return retcode;
311         }
312 
313         /*
314          * (non-Javadoc)
315          * 
316          * @see javax.swing.tree.TreeModel#getChild(java.lang.Object, int)
317          */
318         public Object getChild(Object parent, int index) {
319             String path = ((Node) parent).getFullName();
320             String name = getChildren(path).get(index);
321             return new Node(path, name);
322         }
323 
324         /*
325          * (non-Javadoc)
326          * 
327          * @see javax.swing.tree.TreeModel#getChildCount(java.lang.Object)
328          */
329         public int getChildCount(Object parent) {
330             String path = ((Node) parent).getFullName();
331             return getChildren(path).size();
332         }
333 
334         /*
335          * (non-Javadoc)
336          * 
337          * @see javax.swing.tree.TreeModel#isLeaf(java.lang.Object)
338          */
339         public boolean isLeaf(Object node) {
340             String path = ((Node) node).getFullName();
341             return getChildren(path).size() == 0;
342         }
343 
344         /*
345          * (non-Javadoc)
346          * 
347          * @see
348          * javax.swing.tree.TreeModel#valueForPathChanged(javax.swing.tree.TreePath
349          * , java.lang.Object)
350          */
351         public void valueForPathChanged(TreePath path, Object value) {
352         }
353 
354         /*
355          * (non-Javadoc)
356          * 
357          * @see javax.swing.tree.TreeModel#getIndexOfChild(java.lang.Object,
358          * java.lang.Object)
359          */
360         public int getIndexOfChild(Object parent, Object child) {
361             String path = ((Node) parent).getFullName();
362             List<String> children = getChildren(path);
363             return children.indexOf(child);
364         }
365 
366         /*
367          * (non-Javadoc)
368          * 
369          * @see
370          * javax.swing.tree.TreeModel#addTreeModelListener(javax.swing.event
371          * .TreeModelListener)
372          */
373         public void addTreeModelListener(TreeModelListener li) {
374             listeners.add(TreeModelListener.class, li);
375         }
376 
377         /*
378          * (non-Javadoc)
379          * 
380          * @see
381          * javax.swing.tree.TreeModel#removeTreeModelListener(javax.swing.event
382          * .TreeModelListener)
383          */
384         public void removeTreeModelListener(TreeModelListener li) {
385             listeners.remove(TreeModelListener.class, li);
386         }
387 
388         /**
389          * Notify all listeners that have registered interest for notification
390          * on this event type. The event instance is lazily created using the
391          * parameters passed into the fire method.
392          * 
393          * @see EventListenerList
394          */
395         protected void fireTreeStructureChanged(Object source) {
396             fireTreeStructureChanged(source, root);
397         }
398 
399         /**
400          * Notify all listeners that have registered interest for notification
401          * on this event type. The event instance is lazily created using the
402          * parameters passed into the fire method.
403          * 
404          * @see EventListenerList
405          */
406         protected void fireTreeStructureChanged(Object source, Object... path) {
407             // Guaranteed to return a non-null array
408             Object[] array = listeners.getListenerList();
409             TreeModelEvent ev = null;
410 
411             // Process the listeners last to first, notifying
412             // those that are interested in this event
413             for (int i = array.length - 2; i >= 0; i -= 2) {
414                 if (array[i] == TreeModelListener.class) {
415                     // Lazily create the event:
416                     if (ev == null) {
417                         ev = new TreeModelEvent(source, path);
418                     }
419 
420                     ((TreeModelListener) array[i + 1]).treeStructureChanged(ev);
421                 }
422             }
423         }
424 
425         /**
426          * The Listeners.
427          */
428         protected EventListenerList listeners = new EventListenerList();
429 
430         /**
431          * The root node
432          */
433         private Node root = new Node("", "");
434     }
435 
436     /**
437      * Simple Tree Node
438      */
439     protected static class Node {
440         /**
441          * Create a node with a name and path
442          */
443         protected Node(String path, String name) {
444             this.path = path;
445             this.name = name;
446         }
447 
448         /*
449          * (non-Javadoc)
450          * 
451          * @see java.lang.Object#toString()
452          */
453         @Override
454         public String toString() {
455             return name;
456         }
457 
458         /**
459          * The path to us
460          */
461         public String getFullName() {
462             if (path.length() == 0 || name.length() == 0) {
463                 return path + name;
464             }
465             return path + "." + name;
466         }
467 
468         /**
469          * The displayed string
470          */
471         private String name;
472 
473         /**
474          * The path to us
475          */
476         private String path;
477     }
478 }
479