View Javadoc

1   // Copyright (C) 2004 - 2012 Philip Aston
2   // All rights reserved.
3   //
4   // This file is part of The Grinder software distribution. Refer to
5   // the file LICENSE which is part of The Grinder distribution for
6   // licensing details. The Grinder distribution is available on the
7   // Internet at http://grinder.sourceforge.net/
8   //
9   // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
10  // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
11  // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
12  // FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
13  // COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
14  // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
15  // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
16  // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
17  // HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
18  // STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
19  // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
20  // OF THE POSSIBILITY OF SUCH DAMAGE.
21  
22  package net.grinder.console.swingui;
23  
24  import java.awt.Color;
25  import java.awt.Component;
26  import java.awt.Dimension;
27  import java.awt.Font;
28  import java.awt.Graphics;
29  import java.awt.SystemColor;
30  import java.awt.event.ActionEvent;
31  import java.awt.event.MouseAdapter;
32  import java.awt.event.MouseEvent;
33  import java.io.File;
34  import javax.swing.ActionMap;
35  import javax.swing.BorderFactory;
36  import javax.swing.Icon;
37  import javax.swing.ImageIcon;
38  import javax.swing.InputMap;
39  import javax.swing.JComponent;
40  import javax.swing.JLabel;
41  import javax.swing.JOptionPane;
42  import javax.swing.JPopupMenu;
43  import javax.swing.JScrollBar;
44  import javax.swing.JScrollPane;
45  import javax.swing.JTree;
46  import javax.swing.KeyStroke;
47  import javax.swing.SwingUtilities;
48  import javax.swing.event.TreeSelectionEvent;
49  import javax.swing.event.TreeSelectionListener;
50  import javax.swing.tree.DefaultTreeCellRenderer;
51  import javax.swing.tree.TreePath;
52  import javax.swing.tree.TreeSelectionModel;
53  
54  import net.grinder.console.common.ConsoleException;
55  import net.grinder.console.common.ErrorHandler;
56  import net.grinder.console.common.Resources;
57  import net.grinder.console.editor.Buffer;
58  import net.grinder.console.editor.EditorModel;
59  import net.grinder.console.model.ConsoleProperties;
60  import net.grinder.console.swingui.FileTreeModel.FileNode;
61  
62  
63  /**
64   * Panel containing buffer list and file tree.
65   *
66   * <p>
67   * Listens to the Editor Model, and updates a BufferTreeModel and FileTreeModel
68   * appropriately.
69   * </p>
70   *
71   * @author Philip Aston
72   */
73  final class FileTree {
74  
75    private final Resources m_resources;
76    private final ErrorHandler m_errorHandler;
77    private final EditorModel m_editorModel;
78    private final BufferTreeModel m_bufferTreeModel;
79    private final FileTreeModel m_fileTreeModel;
80    private final ConsoleProperties m_properties;
81  
82    private final JTree m_tree;
83    private final OpenAction m_openAction;
84    private final OpenExternalAction m_openExternalAction;
85    private final SelectPropertiesAction m_selectPropertiesAction;
86    private final DeselectPropertiesAction m_deselectPropertiesAction;
87    private final JScrollPane m_scrollPane;
88  
89    public FileTree(Resources resources,
90                    ErrorHandler errorHandler,
91                    EditorModel editorModel,
92                    BufferTreeModel bufferTreeModel,
93                    FileTreeModel fileTreeModel,
94                    Font font,
95                    JPopupMenu popupMenu,
96                    ConsoleProperties properties) {
97  
98      m_resources = resources;
99      m_errorHandler = errorHandler;
100     m_editorModel = editorModel;
101     m_bufferTreeModel = bufferTreeModel;
102     m_fileTreeModel = fileTreeModel;
103     m_properties = properties;
104 
105     final CompositeTreeModel compositeTreeModel = new CompositeTreeModel();
106 
107     compositeTreeModel.addTreeModel(m_bufferTreeModel, false);
108     compositeTreeModel.addTreeModel(m_fileTreeModel, true);
109 
110     m_tree = new JTree(compositeTreeModel) {
111         // A new CustomTreeCellRenderer needs to be set whenever the
112         // L&F changes because its superclass constructor reads the
113         // resources.
114         public void updateUI() {
115           super.updateUI();
116 
117           // Unfortunately updateUI is called from the JTree
118           // constructor and we can't use the nested
119           // CustomTreeCellRenderer until its enclosing class has been
120           // fully initialised. We hack to prevent this with the
121           // following conditional.
122           if (!isRootVisible()) {
123             // Changing LAF to metal gets JTree background wrong without this.
124             setBackground(new JLabel().getBackground());
125 
126             setCellRenderer(
127               new CustomTreeCellRenderer(getFont(), getBackground()));
128           }
129         }
130       };
131 
132     m_tree.setBackground(new JLabel().getBackground());
133     m_tree.setFont(font);
134 
135     m_tree.setRootVisible(false);
136     m_tree.setShowsRootHandles(true);
137 
138     m_tree.setCellRenderer(
139       new CustomTreeCellRenderer(m_tree.getFont(), m_tree.getBackground()));
140     m_tree.getSelectionModel().setSelectionMode(
141       TreeSelectionModel.SINGLE_TREE_SELECTION);
142 
143     m_tree.addMouseListener(new MouseListener(popupMenu));
144 
145     m_tree.addTreeSelectionListener(new TreeSelectionListener() {
146         public void valueChanged(TreeSelectionEvent e) {
147           updateActionState();
148         }
149       });
150 
151     m_openAction = new OpenAction();
152     m_openExternalAction = new OpenExternalAction();
153     m_selectPropertiesAction = new SelectPropertiesAction();
154     m_deselectPropertiesAction = new DeselectPropertiesAction();
155 
156     // J2SE 1.4 drops the mapping from "ENTER" -> "toggle"
157     // (expand/collapse) that J2SE 1.3 has. I like this mapping, so
158     // we combine the "toggle" action with our OpenFileAction and let
159     // TeeAction figure out which to call based on what's enabled.
160     final InputMap inputMap = m_tree.getInputMap();
161 
162     inputMap.put(KeyStroke.getKeyStroke("ENTER"), "activateNode");
163     inputMap.put(KeyStroke.getKeyStroke("SPACE"), "activateNode");
164 
165     final ActionMap actionMap = m_tree.getActionMap();
166     actionMap.put("activateNode",
167                   new TeeAction(actionMap.get("toggle"), m_openAction));
168 
169     m_scrollPane = new JScrollPane(m_tree);
170     m_scrollPane.setBorder(BorderFactory.createEtchedBorder());
171 
172     m_editorModel.addListener(new EditorModelListener());
173 
174     updateActionState();
175   }
176 
177   private final class MouseListener extends MouseAdapter {
178     private final JPopupMenu m_popupMenu;
179 
180     private boolean m_handledOnPress;
181 
182     private MouseListener(JPopupMenu popupMenu) {
183       m_popupMenu = popupMenu;
184     }
185 
186     public void mousePressed(MouseEvent e) {
187       m_handledOnPress = false;
188 
189       if (!e.isConsumed() && SwingUtilities.isLeftMouseButton(e)) {
190         final TreePath path = m_tree.getPathForLocation(e.getX(), e.getY());
191 
192         if (path == null) {
193           return;
194         }
195 
196         final Object selectedNode = path.getLastPathComponent();
197 
198         if (selectedNode instanceof Node) {
199           final Node node = (Node)selectedNode;
200           final int clickCount = e.getClickCount();
201 
202           final boolean hasBuffer = node.getBuffer() != null;
203 
204           if (clickCount == 2 || clickCount == 1 && hasBuffer) {
205             m_openAction.invoke(node);
206             m_handledOnPress = true;
207             e.consume();
208           }
209 
210           if (clickCount == 2 &&
211               hasBuffer &&
212               m_selectPropertiesAction.isEnabled()) {
213             m_selectPropertiesAction.invoke();
214             m_handledOnPress = true;
215             e.consume();
216           }
217         }
218       }
219 
220       if (e.isPopupTrigger()) {
221         m_popupMenu.show(e.getComponent(), e.getX(), e.getY());
222       }
223     }
224 
225     public void mouseReleased(MouseEvent e) {
226       if (m_handledOnPress) {
227         // Prevent downstream event handlers from overriding our good work.
228         e.consume();
229       }
230 
231       if (e.isPopupTrigger()) {
232         m_popupMenu.show(e.getComponent(), e.getX(), e.getY());
233       }
234     }
235   }
236 
237   private class EditorModelListener extends EditorModel.AbstractListener {
238 
239     public void bufferAdded(Buffer buffer) {
240       // When a file is opened, the new buffer causes the view to
241       // scroll down by one row. This feels wrong, so we compensate.
242       final int rowHeight = m_tree.getRowBounds(0).height;
243       final JScrollBar verticalScrollBar = m_scrollPane.getVerticalScrollBar();
244       verticalScrollBar.setValue(verticalScrollBar.getValue() + rowHeight);
245     }
246 
247     public void bufferStateChanged(Buffer buffer) {
248       final File file = buffer.getFile();
249 
250       if (file != null) {
251         final FileTreeModel.FileNode oldFileNode =
252           m_fileTreeModel.findFileNode(buffer);
253 
254         // Find a node, if its in our directory structure. This
255         // may cause parts of the tree to be refreshed.
256         final FileTreeModel.Node node = m_fileTreeModel.findNode(file);
257 
258         if (oldFileNode == null || !oldFileNode.equals(node)) {
259           // Buffer's associated file has changed.
260 
261           if (oldFileNode != null) {
262             oldFileNode.setBuffer(null);
263           }
264 
265           if (node instanceof FileTreeModel.FileNode) {
266             final FileTreeModel.FileNode fileNode =
267               (FileTreeModel.FileNode)node;
268 
269             fileNode.setBuffer(buffer);
270             m_tree.scrollPathToVisible(treePathForFileNode(fileNode));
271           }
272         }
273       }
274 
275       final FileTreeModel.Node fileNode = m_fileTreeModel.findFileNode(buffer);
276 
277       if (fileNode != null) {
278         m_fileTreeModel.valueForPathChanged(fileNode.getPath(), fileNode);
279       }
280 
281       m_bufferTreeModel.bufferChanged(buffer);
282 
283       updateActionState();
284     }
285 
286     public void bufferRemoved(Buffer buffer) {
287       final FileTreeModel.FileNode fileNode =
288         m_fileTreeModel.findFileNode(buffer);
289 
290       if (fileNode != null) {
291         fileNode.setBuffer(null);
292         m_fileTreeModel.valueForPathChanged(fileNode.getPath(), fileNode);
293       }
294     }
295   }
296 
297   public JComponent getComponent() {
298     return m_scrollPane;
299   }
300 
301   public CustomAction[] getActions() {
302     return new CustomAction[] {
303         m_openAction,
304         m_openExternalAction,
305         m_selectPropertiesAction,
306         m_deselectPropertiesAction,
307     };
308   }
309 
310   /**
311    * Action for opening the currently selected file in the tree.
312    */
313   private final class OpenAction extends CustomAction {
314     public OpenAction() {
315       super(m_resources, "open-file");
316     }
317 
318     public void actionPerformed(ActionEvent event) {
319       invoke(m_tree.getLastSelectedPathComponent());
320     }
321 
322     public void invoke(Object selectedNode) {
323       if (selectedNode instanceof BufferTreeModel.BufferNode) {
324         m_editorModel.selectBuffer(
325           ((BufferTreeModel.BufferNode)selectedNode).getBuffer());
326       }
327       else if (selectedNode instanceof FileTreeModel.FileNode) {
328         final FileNode fileNode = (FileTreeModel.FileNode)selectedNode;
329 
330         try {
331           fileNode.setBuffer(
332             m_editorModel.selectBufferForFile(fileNode.getFile()));
333 
334           // The above line can add the buffer to the editor model which
335           // causes the BufferTreeModel to fire a top level structure
336           // change, which in turn causes the selection to clear. We
337           // reselect the original node so our actions are enabled
338           // correctly.
339           m_tree.setSelectionPath(treePathForFileNode(fileNode));
340         }
341         catch (ConsoleException e) {
342           m_errorHandler.handleException(
343             e, m_resources.getString("fileError.title"));
344         }
345       }
346     }
347   }
348 
349 
350   /**
351    * Action for opening the currently selected file in the tree in an external
352    * editor.
353    */
354   private final class OpenExternalAction extends CustomAction {
355     public OpenExternalAction() {
356       super(m_resources, "open-file-external");
357     }
358 
359     public void actionPerformed(ActionEvent event) {
360       final Object selectedNode = m_tree.getLastSelectedPathComponent();
361 
362       if (selectedNode instanceof Node) {
363         final Node node = (Node)selectedNode;
364 
365         final File file = node.getFile();
366 
367         if (file != null) {
368           final Buffer buffer = node.getBuffer();
369 
370           if (buffer != null && buffer.isDirty() &&
371                 JOptionPane.showConfirmDialog(
372                   getComponent(),
373                   m_resources.getString(
374                     "externalEditModifiedBufferConfirmation.text"),
375                   file.toString(),
376                   JOptionPane.YES_NO_OPTION) == JOptionPane.NO_OPTION) {
377               return;
378           }
379 
380           try {
381             m_editorModel.openWithExternalEditor(file);
382           }
383           catch (ConsoleException e) {
384             m_errorHandler.handleException(
385               e, m_resources.getString("fileError.title"));
386           }
387         }
388       }
389     }
390   }
391 
392   private final class SelectPropertiesAction extends CustomAction {
393     public SelectPropertiesAction() {
394       super(m_resources, "select-properties");
395     }
396 
397     public void actionPerformed(ActionEvent event) {
398       invoke();
399     }
400 
401     public void invoke() {
402       final Object selectedNode = m_tree.getLastSelectedPathComponent();
403 
404       if (selectedNode instanceof Node) {
405         final Node node = (Node)selectedNode;
406 
407         final File file = node.getFile();
408 
409         if (file.isFile()) {
410           try {
411             // Editor model learns of selection through a properties listener.
412             m_properties.setAndSavePropertiesFile(file);
413           }
414           catch (ConsoleException e) {
415             m_errorHandler.handleException(e);
416             return;
417           }
418 
419           m_bufferTreeModel.valueForPathChanged(node.getPath(), node);
420           updateActionState();
421         }
422       }
423     }
424   }
425 
426   private final class DeselectPropertiesAction extends CustomAction {
427     public DeselectPropertiesAction() {
428       super(m_resources, "deselect-properties");
429     }
430 
431     public void actionPerformed(ActionEvent event) {
432       invoke();
433     }
434 
435     public void invoke() {
436       try {
437         final File previousProperties = m_properties.getPropertiesFile();
438 
439         m_properties.setAndSavePropertiesFile(null);
440         updateActionState();
441 
442         if (previousProperties != null) {
443           final FileTreeModel.Node fileNode =
444             m_fileTreeModel.findNode(previousProperties);
445 
446           if (fileNode != null) {
447             m_fileTreeModel.valueForPathChanged(fileNode.getPath(), fileNode);
448 
449             m_bufferTreeModel.bufferChanged(fileNode.getBuffer());
450           }
451         }
452       }
453       catch (ConsoleException e) {
454         m_errorHandler.handleException(e);
455       }
456     }
457   }
458 
459   private void updateActionState() {
460     m_deselectPropertiesAction.setEnabled(
461       m_editorModel.getSelectedPropertiesFile() != null);
462 
463     if (m_tree.isEnabled()) {
464       final Object selectedNode = m_tree.getLastSelectedPathComponent();
465       if (selectedNode instanceof Node) {
466         final Node node = (Node)selectedNode;
467 
468         final Buffer buffer = node.getBuffer();
469         final File file = node.getFile();
470 
471         m_openAction.setEnabled(
472           node.canOpen() &&
473           (buffer == null ||
474            !buffer.equals(m_editorModel.getSelectedBuffer())));
475         m_openAction.setRelevantToSelection(node.canOpen());
476 
477         m_openExternalAction.setEnabled(file != null && file.isFile());
478         m_openExternalAction.setRelevantToSelection(node.canOpen());
479 
480         m_selectPropertiesAction.setEnabled(
481           m_editorModel.isPropertiesFile(file) &&
482           !file.equals(m_editorModel.getSelectedPropertiesFile()));
483 
484         m_selectPropertiesAction.setRelevantToSelection(
485           m_selectPropertiesAction.isEnabled());
486 
487         m_deselectPropertiesAction.setRelevantToSelection(
488           m_editorModel.isPropertiesFile(file) &&
489           !m_selectPropertiesAction.isEnabled());
490 
491         return;
492       }
493     }
494 
495     m_openAction.setEnabled(false);
496     m_openAction.setRelevantToSelection(false);
497     m_openExternalAction.setEnabled(false);
498     m_openExternalAction.setRelevantToSelection(false);
499     m_selectPropertiesAction.setEnabled(false);
500     m_selectPropertiesAction.setRelevantToSelection(false);
501     m_deselectPropertiesAction.setRelevantToSelection(false);
502   }
503 
504   /**
505    * Custom cell renderer.
506    */
507   private final class CustomTreeCellRenderer extends DefaultTreeCellRenderer {
508     private final DefaultTreeCellRenderer m_defaultRenderer =
509       new DefaultTreeCellRenderer();
510 
511     private final Font m_boldFont;
512     private final Font m_boldItalicFont;
513     private final ImageIcon m_propertiesIcon =
514       m_resources.getImageIcon("file.properties.image");
515     private final ImageIcon m_markedPropertiesIcon =
516       m_resources.getImageIcon("file.selectedproperties.image");
517     private final ImageIcon m_scriptIcon =
518       m_resources.getImageIcon("file.script.image");
519     private final ImageIcon m_selectedScriptIcon =
520       m_resources.getImageIcon("file.selectedscript.image");
521 
522     private boolean m_active;
523 
524     CustomTreeCellRenderer(Font baseFont, Color background) {
525       m_boldFont = baseFont.deriveFont(Font.BOLD);
526       m_boldItalicFont = m_boldFont.deriveFont(Font.BOLD | Font.ITALIC);
527       m_defaultRenderer.setBackgroundNonSelectionColor(background);
528     }
529 
530     public Component getTreeCellRendererComponent(
531       JTree tree, Object value, boolean selected, boolean expanded,
532       boolean leaf, int row, boolean hasFocus) {
533 
534       if (value instanceof Node) {
535         final Node node = (Node)value;
536 
537         final File file = node.getFile();
538 
539         if (file != null && !file.isFile()) {
540           return m_defaultRenderer.getTreeCellRendererComponent(
541             tree, value, selected, expanded, leaf, row, hasFocus);
542         }
543 
544         final Icon icon;
545 
546         if (file != null &&
547             file.equals(m_editorModel.getSelectedPropertiesFile())) {
548           icon = m_markedPropertiesIcon;
549         }
550         else if (m_editorModel.isSelectedScript(file)) {
551           icon = m_selectedScriptIcon;
552         }
553         else if (m_editorModel.isPropertiesFile(file)) {
554           icon = m_propertiesIcon;
555         }
556         else if (m_editorModel.isScriptFile(file)) {
557           icon = m_scriptIcon;
558         }
559         else {
560           icon = m_defaultRenderer.getLeafIcon();
561         }
562 
563         setLeafIcon(icon);
564 
565         final Buffer buffer = node.getBuffer();
566 
567         // See note in paint().
568         setTextNonSelectionColor(
569           buffer == null && m_editorModel.isBoringFile(file) ?
570           SystemColor.textInactiveText :
571           m_defaultRenderer.getTextNonSelectionColor());
572 
573         if (buffer != null) {
574           // File has an open buffer.
575           setFont(buffer.isDirty() ? m_boldItalicFont : m_boldFont);
576           m_active = buffer.equals(m_editorModel.getSelectedBuffer());
577         }
578         else {
579           setFont(m_defaultRenderer.getFont());
580           m_active = false;
581         }
582 
583         return super.getTreeCellRendererComponent(
584           tree, value, selected, expanded, leaf, row, hasFocus);
585       }
586       else {
587         return m_defaultRenderer.getTreeCellRendererComponent(
588           tree, value, selected, expanded, leaf, row, hasFocus);
589       }
590     }
591 
592     /**
593      * Our parent overrides validate() and revalidate() for speed.
594      * This means it never resizes. Go with this, but be a few pixels
595      * wider to allow text to be italicised.
596      */
597     public Dimension getPreferredSize() {
598       final Dimension result = super.getPreferredSize();
599 
600       return result != null ?
601         new Dimension(result.width + 3, result.height) : null;
602     }
603 
604     public void paint(Graphics g) {
605 
606       final Color backgroundColour;
607 
608       // For some reason, setting the text non-selection colour doesn't
609       // work here. I've left the logic in anyway. That's why its set
610       // in getTreeCellRendererComponent().
611       if (m_active) {
612         backgroundColour = Colours.FAINT_YELLOW;
613         setTextSelectionColor(SystemColor.textText);
614         setTextNonSelectionColor(SystemColor.textText);
615       }
616       else if (selected) {
617         backgroundColour = m_defaultRenderer.getBackgroundSelectionColor();
618         setTextSelectionColor(m_defaultRenderer.getTextSelectionColor());
619       }
620       else {
621         backgroundColour = m_defaultRenderer.getBackgroundNonSelectionColor();
622         setTextNonSelectionColor(m_defaultRenderer.getTextNonSelectionColor());
623       }
624 
625       if (backgroundColour != null) {
626         g.setColor(backgroundColour);
627         g.fillRect(0, 0, getWidth() - 1, getHeight());
628       }
629 
630       // Sigh. The whole reason we override paint is that the
631       // DefaultTreeCellRenderer version is crap. We can't call
632       // super.super.paint() so we work hard to make the
633       // DefaultTreeCellRenderer version ineffectual.
634 
635       final boolean oldHasFocus = hasFocus;
636       final boolean oldSelected = selected;
637       final Color oldBackgroundNonSelectionColour =
638         getBackgroundNonSelectionColor();
639 
640       try {
641         hasFocus = false;
642         selected = false;
643         setBackgroundNonSelectionColor(backgroundColour);
644 
645         super.paint(g);
646       }
647       finally {
648         hasFocus = oldHasFocus;
649         selected = oldSelected;
650         setBackgroundNonSelectionColor(oldBackgroundNonSelectionColour);
651       }
652 
653       // Now draw our border.
654       final Color borderColour;
655 
656       if (m_active) {
657         borderColour = getTextNonSelectionColor();
658       }
659       else if (hasFocus) {
660         borderColour = getBorderSelectionColor();
661       }
662       else {
663         borderColour = null;
664       }
665 
666       if (borderColour != null) {
667         g.setColor(borderColour);
668         g.drawRect(0, 0, getWidth() - 1, getHeight() - 1);
669       }
670     }
671   }
672 
673   /**
674    * Hard to see how this could be easily incorporated into
675    * CompositeTreeModel without having the child models know about the
676    * composite model.
677    */
678   private TreePath treePathForFileNode(FileTreeModel.FileNode fileNode) {
679     final Object[] original = fileNode.getPath().getPath();
680     final Object[] result = new Object[original.length + 1];
681     System.arraycopy(original, 0, result, 1, original.length);
682 
683     result[0] = m_tree.getModel().getRoot();
684 
685     return new TreePath(result);
686   }
687 
688   /**
689    * Allows us to treat FileNodes and BufferNodes polymorphically.
690    */
691   interface Node {
692 
693     /**
694      * @return <code>null</code> if the node has no associated buffer.
695      */
696     Buffer getBuffer();
697 
698     /**
699      * @return <code>null</code> if the node has no associated file.
700      */
701     File getFile();
702 
703     TreePath getPath();
704 
705     boolean canOpen();
706   }
707 }