001    /* DefaultTreeCellEditor.java --
002       Copyright (C) 2002, 2004, 2005  Free Software Foundation, Inc.
003    
004    This file is part of GNU Classpath.
005    
006    GNU Classpath is free software; you can redistribute it and/or modify
007    it under the terms of the GNU General Public License as published by
008    the Free Software Foundation; either version 2, or (at your option)
009    any later version.
010    
011    GNU Classpath is distributed in the hope that it will be useful, but
012    WITHOUT ANY WARRANTY; without even the implied warranty of
013    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014    General Public License for more details.
015    
016    You should have received a copy of the GNU General Public License
017    along with GNU Classpath; see the file COPYING.  If not, write to the
018    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
019    02110-1301 USA.
020    
021    Linking this library statically or dynamically with other modules is
022    making a combined work based on this library.  Thus, the terms and
023    conditions of the GNU General Public License cover the whole
024    combination.
025    
026    As a special exception, the copyright holders of this library give you
027    permission to link this library with independent modules to produce an
028    executable, regardless of the license terms of these independent
029    modules, and to copy and distribute the resulting executable under
030    terms of your choice, provided that you also meet, for each linked
031    independent module, the terms and conditions of the license of that
032    module.  An independent module is a module which is not derived from
033    or based on this library.  If you modify this library, you may extend
034    this exception to your version of the library, but you are not
035    obligated to do so.  If you do not wish to do so, delete this
036    exception statement from your version. */
037    
038    
039    package javax.swing.tree;
040    
041    import java.awt.Color;
042    import java.awt.Component;
043    import java.awt.Container;
044    import java.awt.Dimension;
045    import java.awt.Font;
046    import java.awt.Graphics;
047    import java.awt.Rectangle;
048    import java.awt.event.ActionEvent;
049    import java.awt.event.ActionListener;
050    import java.awt.event.MouseEvent;
051    import java.io.IOException;
052    import java.io.ObjectInputStream;
053    import java.io.ObjectOutputStream;
054    import java.util.EventObject;
055    
056    import javax.swing.DefaultCellEditor;
057    import javax.swing.Icon;
058    import javax.swing.JTextField;
059    import javax.swing.JTree;
060    import javax.swing.SwingUtilities;
061    import javax.swing.Timer;
062    import javax.swing.UIManager;
063    import javax.swing.border.Border;
064    import javax.swing.event.CellEditorListener;
065    import javax.swing.event.EventListenerList;
066    import javax.swing.event.TreeSelectionEvent;
067    import javax.swing.event.TreeSelectionListener;
068    
069    /**
070     * Participates in the tree cell editing.
071     * 
072     * @author Andrew Selkirk
073     * @author Audrius Meskauskas
074     */
075    public class DefaultTreeCellEditor
076      implements ActionListener, TreeCellEditor, TreeSelectionListener
077    {
078      /**
079       * This container that appears on the tree during editing session.
080       * It contains the editing component displays various other editor - 
081       * specific parts like editing icon. 
082       */
083      public class EditorContainer extends Container
084      {
085       /**
086        * Use v 1.5 serial version UID for interoperability.
087        */
088        static final long serialVersionUID = 6470339600449699810L;
089        
090        /**
091         * Creates an <code>EditorContainer</code> object.
092         */
093        public EditorContainer()
094        {
095          setLayout(null);
096        }
097    
098        /**
099         * This method only exists for API compatibility and is useless as it does
100         * nothing. It got probably introduced by accident.
101         */
102        public void EditorContainer()
103        {
104          // Do nothing here.
105        }
106       
107        /**
108         * Overrides Container.paint to paint the node's icon and use the selection
109         * color for the background.
110         * 
111         * @param g -
112         *          the specified Graphics window
113         */
114        public void paint(Graphics g)
115        {
116          // Paint editing icon.
117          if (editingIcon != null)
118            {
119              // From the previous version, the left margin is taken as half
120              // of the icon width.
121              int y = Math.max(0, (getHeight() - editingIcon.getIconHeight()) / 2);
122              editingIcon.paintIcon(this, g, 0, y);
123            }
124          // Paint border.
125          Color c = getBorderSelectionColor();
126          if (c != null)
127            {
128              g.setColor(c);
129              g.drawRect(0, 0, getWidth() - 1, getHeight() - 1);
130            }
131          super.paint(g);
132        }
133    
134        /**
135         * Lays out this Container, moving the editor component to the left
136         * (leaving place for the icon).
137         */
138        public void doLayout()
139        {
140          if (editingComponent != null)
141            {
142              editingComponent.getPreferredSize();
143              editingComponent.setBounds(offset, 0, getWidth() - offset,
144                                         getHeight());
145            }
146          }
147    
148        public Dimension getPreferredSize()
149        {
150          Dimension dim;
151          if (editingComponent != null)
152            {
153              dim = editingComponent.getPreferredSize();
154              dim.width += offset + 5;
155              if (renderer != null)
156                {
157                  Dimension r = renderer.getPreferredSize();
158                  dim.height = Math.max(dim.height, r.height);
159                }
160              if (editingIcon != null)
161                dim.height = Math.max(dim.height, editingIcon.getIconHeight());
162              dim.width = Math.max(100, dim.width);
163            }
164          else
165            dim = new Dimension(0, 0);
166          return dim;
167        }
168      }
169    
170      /**
171       * The default text field, used in the editing sessions.
172       */
173      public class DefaultTextField extends JTextField
174      {
175       /**
176        * Use v 1.5 serial version UID for interoperability.
177        */
178        static final long serialVersionUID = -6629304544265300143L; 
179        
180        /**
181         * The border of the text field.
182         */
183        protected Border border;
184    
185        /**
186         * Creates a <code>DefaultTextField</code> object.
187         *
188         * @param aBorder the border to use
189         */
190        public DefaultTextField(Border aBorder)
191        {
192          border = aBorder;
193        }
194    
195        /**
196         * Gets the font of this component.
197         * @return this component's font; if a font has not been set for 
198         * this component, the font of its parent is returned (if the parent
199         * is not null, otherwise null is returned). 
200         */
201        public Font getFont()
202        {
203          Font font = super.getFont();
204          if (font == null)
205            {
206              Component parent = getParent();
207              if (parent != null)
208                return parent.getFont();
209              return null;
210            }
211          return font;
212        }
213    
214        /**
215         * Returns the border of the text field.
216         *
217         * @return the border
218         */
219        public Border getBorder()
220        {
221          return border;
222        }
223    
224        /**
225         * Overrides JTextField.getPreferredSize to return the preferred size 
226         * based on current font, if set, or else use renderer's font.
227         * 
228         * @return the Dimension of this textfield.
229         */
230        public Dimension getPreferredSize()
231        {
232          Dimension size = super.getPreferredSize();
233          if (renderer != null && DefaultTreeCellEditor.this.getFont() == null)
234            {
235              size.height = renderer.getPreferredSize().height;
236            }
237          return renderer.getPreferredSize();
238        }
239      }
240      
241      private EventListenerList listenerList = new EventListenerList();
242      
243      /**
244       * Editor handling the editing.
245       */
246      protected TreeCellEditor realEditor;
247    
248      /**
249       * Renderer, used to get border and offsets from.
250       */
251      protected DefaultTreeCellRenderer renderer;
252    
253      /**
254       * Editing container, will contain the editorComponent.
255       */
256      protected Container editingContainer;
257    
258      /**
259       * Component used in editing, obtained from the editingContainer.
260       */
261      protected transient Component editingComponent;
262    
263      /**
264       * As of Java 2 platform v1.4 this field should no longer be used. 
265       * If you wish to provide similar behavior you should directly 
266       * override isCellEditable.
267       */
268      protected boolean canEdit;
269    
270      /**
271       * Used in editing. Indicates x position to place editingComponent.
272       */
273      protected transient int offset;
274    
275      /**
276       * JTree instance listening too.
277       */
278      protected transient JTree tree;
279    
280      /**
281       * Last path that was selected.
282       */
283      protected transient TreePath lastPath;
284    
285      /**
286       * Used before starting the editing session.
287       */
288      protected transient javax.swing.Timer timer;
289    
290      /**
291       * Row that was last passed into getTreeCellEditorComponent.
292       */
293      protected transient int lastRow;
294    
295      /**
296       * True if the border selection color should be drawn.
297       */
298      protected Color borderSelectionColor;
299    
300      /**
301       * Icon to use when editing.
302       */
303      protected transient Icon editingIcon;
304    
305      /**
306       * Font to paint with, null indicates font of renderer is to be used.
307       */
308      protected Font font;
309      
310      /**
311       * Helper field used to save the last path seen while the timer was
312       * running.
313       */
314        private TreePath tPath;
315        
316      /**
317       * Constructs a DefaultTreeCellEditor object for a JTree using the 
318       * specified renderer and a default editor. (Use this constructor 
319       * for normal editing.)
320       * 
321       * @param tree - a JTree object
322       * @param renderer - a DefaultTreeCellRenderer object
323       */
324      public DefaultTreeCellEditor(JTree tree, DefaultTreeCellRenderer renderer)
325      {
326        this(tree, renderer, null);
327      }
328    
329      /**
330       * Constructs a DefaultTreeCellEditor  object for a JTree using the specified 
331       * renderer and the specified editor. (Use this constructor 
332       * for specialized editing.)
333       * 
334       * @param tree - a JTree object
335       * @param renderer - a DefaultTreeCellRenderer object
336       * @param editor - a TreeCellEditor object
337       */
338      public DefaultTreeCellEditor(JTree tree, DefaultTreeCellRenderer renderer,
339                                   TreeCellEditor editor)
340      {
341        this.renderer = renderer;
342        realEditor = editor;
343        if (realEditor == null)
344          realEditor = createTreeCellEditor();
345        editingContainer = createContainer();
346        setTree(tree);
347        Color c = UIManager.getColor("Tree.editorBorderSelectionColor");
348        setBorderSelectionColor(c);
349      }
350    
351      /**
352       * Configures the editing component whenever it is null.
353       * 
354       * @param tree the tree to configure to component for.
355       * @param renderer the renderer used to set up the nodes
356       * @param editor the editor used 
357       */
358      private void configureEditingComponent(JTree tree,
359                                             DefaultTreeCellRenderer renderer,
360                                             TreeCellEditor editor)
361      {    
362        if (tree != null && lastPath != null)
363          {
364            Object val = lastPath.getLastPathComponent();
365            boolean isLeaf = tree.getModel().isLeaf(val);
366            boolean expanded = tree.isExpanded(lastPath);
367            determineOffset(tree, val, true, expanded, isLeaf, lastRow);
368    
369            // set up icon
370            if (isLeaf)
371              renderer.setIcon(renderer.getLeafIcon());
372            else if (expanded)
373              renderer.setIcon(renderer.getOpenIcon());
374            else
375              renderer.setIcon(renderer.getClosedIcon());
376            editingIcon = renderer.getIcon();
377            
378            editingComponent = getTreeCellEditorComponent(tree, val, true,
379                                                          expanded, isLeaf, lastRow);
380          }
381      }
382      
383      /**
384       * writeObject
385       * 
386       * @param value0
387       *          TODO
388       * @exception IOException
389       *              TODO
390       */
391      private void writeObject(ObjectOutputStream value0) throws IOException
392      {
393        // TODO
394      }
395    
396      /**
397       * readObject
398       * @param value0 TODO
399       * @exception IOException TODO
400       * @exception ClassNotFoundException TODO
401       */
402      private void readObject(ObjectInputStream value0)
403        throws IOException, ClassNotFoundException
404      {
405        // TODO
406      }
407    
408      /**
409       * Sets the color to use for the border.
410       * @param newColor - the new border color
411       */
412      public void setBorderSelectionColor(Color newColor)
413      {
414        this.borderSelectionColor = newColor;
415      }
416    
417      /**
418       * Returns the color the border is drawn.
419       * @return Color
420       */
421      public Color getBorderSelectionColor()
422      {
423        return borderSelectionColor;
424      }
425    
426      /**
427       * Sets the font to edit with. null indicates the renderers 
428       * font should be used. This will NOT override any font you have 
429       * set in the editor the receiver was instantied with. If null for 
430       * an editor was passed in, a default editor will be created that 
431       * will pick up this font.
432       * 
433       * @param font - the editing Font
434       */
435      public void setFont(Font font)
436      {
437        if (font != null)
438          this.font = font;
439        else
440          this.font = renderer.getFont();
441      }
442    
443      /**
444       * Gets the font used for editing.
445       * 
446       * @return the editing font
447       */
448      public Font getFont()
449      {
450        return font;
451      }
452    
453      /**
454       * Configures the editor. Passed onto the realEditor.
455       * Sets an initial value for the editor. This will cause 
456       * the editor to stopEditing and lose any partially edited value 
457       * if the editor is editing when this method is called. 
458       * Returns the component that should be added to the client's Component 
459       * hierarchy. Once installed in the client's hierarchy this component will 
460       * then be able to draw and receive user input. 
461       * 
462       * @param tree - the JTree that is asking the editor to edit; this parameter can be null
463       * @param value - the value of the cell to be edited
464       * @param isSelected - true is the cell is to be rendered with selection highlighting
465       * @param expanded - true if the node is expanded
466       * @param leaf - true if the node is a leaf node
467       * @param row - the row index of the node being edited
468       * 
469       * @return the component for editing
470       */
471      public Component getTreeCellEditorComponent(JTree tree, Object value,
472                                                  boolean isSelected,
473                                                  boolean expanded,
474                                                  boolean leaf, int row)
475      {
476        setTree(tree);
477        lastRow = row;
478        determineOffset(tree, value, isSelected, expanded, leaf, row);
479        if (editingComponent != null)
480          editingContainer.remove(editingComponent);
481    
482        editingComponent = realEditor.getTreeCellEditorComponent(tree, value,
483                                                                 isSelected,
484                                                                 expanded, leaf,
485                                                                 row);
486        Font f = getFont();
487        if (f == null)
488          {
489            if (renderer != null)
490              f = renderer.getFont();
491            if (f == null)
492              f = tree.getFont();
493          }
494        editingContainer.setFont(f);
495        prepareForEditing();
496        return editingContainer;
497      }
498    
499      /**
500       * Returns the value currently being edited (requests it from the
501       * {@link #realEditor}.
502       * 
503       * @return the value currently being edited
504       */
505      public Object getCellEditorValue()
506      {
507        return realEditor.getCellEditorValue();
508      }
509      
510      /**
511       * If the realEditor returns true to this message, prepareForEditing  
512       * is messaged and true is returned.
513       * 
514       * @param event - the event the editor should use to consider whether to 
515       * begin editing or not
516       * @return true if editing can be started
517       */
518      public boolean isCellEditable(EventObject event)
519      {
520        boolean ret = false;
521        boolean ed = false;
522        if (event != null)
523          {
524            if (event.getSource() instanceof JTree)
525              {
526                setTree((JTree) event.getSource());
527                if (event instanceof MouseEvent)
528                  {
529                    MouseEvent me = (MouseEvent) event;
530                    TreePath path = tree.getPathForLocation(me.getX(), me.getY());
531                    ed = lastPath != null && path != null && lastPath.equals(path);
532                    if (path != null)
533                      {
534                        lastRow = tree.getRowForPath(path);
535                        Object val = path.getLastPathComponent();
536                        boolean isSelected = tree.isRowSelected(lastRow);
537                        boolean isExpanded = tree.isExpanded(path);
538                        TreeModel m = tree.getModel();
539                        boolean isLeaf = m.isLeaf(val);
540                        determineOffset(tree, val, isSelected, isExpanded, isLeaf,
541                                        lastRow);
542                      }
543                  }
544              }
545          }
546        if (! realEditor.isCellEditable(event))
547          ret = false;
548        else 
549          {
550            if (canEditImmediately(event))
551              ret = true;
552            else if (ed && shouldStartEditingTimer(event))
553              startEditingTimer();
554            else if (timer != null && timer.isRunning())
555              timer.stop();
556          }
557        if (ret)
558          prepareForEditing();
559        return ret;
560            
561      }
562    
563      /**
564       * Messages the realEditor for the return value.
565       * 
566       * @param event -
567       *          the event the editor should use to start editing
568       * @return true if the editor would like the editing cell to be selected;
569       *         otherwise returns false
570       */
571      public boolean shouldSelectCell(EventObject event)
572      {
573        return true;
574      }
575    
576      /**
577       * If the realEditor will allow editing to stop, the realEditor
578       * is removed and true is returned, otherwise false is returned.
579       * @return true if editing was stopped; false otherwise
580       */
581      public boolean stopCellEditing()
582      {
583        boolean ret = false;
584        if (realEditor.stopCellEditing())
585          {
586            finish();
587            ret = true;
588          }
589        return ret;
590      }
591    
592      /**
593       * Messages cancelCellEditing to the realEditor and removes it
594       * from this instance.
595       */
596      public void cancelCellEditing()
597      {
598        realEditor.cancelCellEditing();
599        finish();
600      }
601    
602      private void finish()
603      {
604        if (editingComponent != null)
605          editingContainer.remove(editingComponent);
606        editingComponent = null;
607      }
608    
609      /**
610       * Adds a <code>CellEditorListener</code> object to this editor.
611       * 
612       * @param listener
613       *          the listener to add
614       */
615      public void addCellEditorListener(CellEditorListener listener)
616      {
617        realEditor.addCellEditorListener(listener);
618      }
619    
620      /**
621       * Removes a <code>CellEditorListener</code> object.
622       *
623       * @param listener the listener to remove
624       */
625      public void removeCellEditorListener(CellEditorListener listener)
626      {
627        realEditor.removeCellEditorListener(listener);
628      }
629    
630      /**
631       * Returns all added <code>CellEditorListener</code> objects to this editor.
632       *
633       * @return an array of listeners
634       *
635       * @since 1.4
636       */
637      public CellEditorListener[] getCellEditorListeners()
638      {
639        return (CellEditorListener[]) listenerList.getListeners(CellEditorListener.class);
640      }
641    
642      /**
643       * Resets lastPath.
644       * 
645       * @param e - the event that characterizes the change.
646       */
647      public void valueChanged(TreeSelectionEvent e)
648      {
649        if (tree != null)
650          {
651            if (tree.getSelectionCount() == 1)
652              lastPath = tree.getSelectionPath();
653            else
654              lastPath = null;
655          }
656        // TODO: We really should do the following here, but can't due
657        // to buggy DefaultTreeSelectionModel. This selection model
658        // should only fire if the selection actually changes.
659    //    if (timer != null)
660    //      timer.stop();
661      }
662      
663      /**
664       * Messaged when the timer fires.
665       * 
666       * @param e the event that characterizes the action.
667       */
668      public void actionPerformed(ActionEvent e)
669      {
670        if (tree != null && lastPath != null)
671          tree.startEditingAtPath(lastPath);
672      }
673    
674      /**
675       * Sets the tree currently editing for. This is needed to add a selection
676       * listener.
677       * 
678       * @param newTree -
679       *          the new tree to be edited
680       */
681      protected void setTree(JTree newTree)
682      {
683        if (tree != newTree)
684          {
685            if (tree != null)
686              tree.removeTreeSelectionListener(this);
687            tree = newTree;
688            if (tree != null)
689              tree.addTreeSelectionListener(this);
690    
691            if (timer != null)
692              timer.stop();
693          }
694      }
695    
696      /**
697       * Returns true if event is a MouseEvent and the click count is 1.
698       * 
699       * @param event - the event being studied
700       * @return true if editing should start
701       */
702      protected boolean shouldStartEditingTimer(EventObject event)
703      {
704        boolean ret = false;
705        if (event instanceof MouseEvent)
706          {
707            MouseEvent me = (MouseEvent) event;
708            ret = SwingUtilities.isLeftMouseButton(me) && me.getClickCount() == 1
709                  && inHitRegion(me.getX(), me.getY());
710          }
711        return ret;
712      }
713    
714      /**
715       * Starts the editing timer (if one installed). 
716       */
717      protected void startEditingTimer()
718      {
719        if (timer == null)
720          {
721            timer = new Timer(1200, this);
722            timer.setRepeats(false);
723          }
724        timer.start();
725      }
726    
727      /**
728       * Returns true if event is null, or it is a MouseEvent with 
729       * a click count > 2 and inHitRegion returns true.
730       * 
731       * @param event - the event being studied
732       * @return true if event is null, or it is a MouseEvent with 
733       * a click count > 2 and inHitRegion returns true 
734       */
735      protected boolean canEditImmediately(EventObject event)
736      {
737        if (event == null || !(event instanceof MouseEvent) || (((MouseEvent) event).
738            getClickCount() > 2 && inHitRegion(((MouseEvent) event).getX(), 
739                                             ((MouseEvent) event).getY())))
740          return true;
741        return false;
742      }
743    
744      /**
745       * Returns true if the passed in location is a valid mouse location 
746       * to start editing from. This is implemented to return false if x is
747       * less than or equal to the width of the icon and icon 
748       * gap displayed by the renderer. In other words this returns true if 
749       * the user clicks over the text part displayed by the renderer, and 
750       * false otherwise.
751       * 
752       * @param x - the x-coordinate of the point
753       * @param y - the y-coordinate of the point
754       * 
755       * @return true if the passed in location is a valid mouse location
756       */
757      protected boolean inHitRegion(int x, int y)
758      {
759        Rectangle bounds = tree.getPathBounds(lastPath);
760        return bounds.contains(x, y);
761      }
762    
763      /**
764       * determineOffset
765       * @param tree -
766       * @param value - 
767       * @param isSelected - 
768       * @param expanded - 
769       * @param leaf - 
770       * @param row - 
771       */
772      protected void determineOffset(JTree tree, Object value, boolean isSelected,
773                                     boolean expanded, boolean leaf, int row)
774      {
775        if (renderer != null)
776          {
777            if (leaf)
778              editingIcon = renderer.getLeafIcon();
779            else if (expanded)
780              editingIcon = renderer.getOpenIcon();
781            else
782              editingIcon = renderer.getClosedIcon();
783            if (editingIcon != null)
784              offset = renderer.getIconTextGap() + editingIcon.getIconWidth();
785            else
786              offset = renderer.getIconTextGap();
787          }
788        else
789          {
790            editingIcon = null;
791            offset = 0;
792          }
793      }
794    
795      /**
796       * Invoked just before editing is to start. Will add the 
797       * editingComponent to the editingContainer.
798       */
799      protected void prepareForEditing()
800      {
801        if (editingComponent != null)
802          editingContainer.add(editingComponent);
803      }
804    
805      /**
806       * Creates the container to manage placement of editingComponent.
807       * 
808       * @return the container to manage the placement of the editingComponent.
809       */
810      protected Container createContainer()
811      {
812        return new DefaultTreeCellEditor.EditorContainer();
813      }
814    
815      /**
816       * This is invoked if a TreeCellEditor is not supplied in the constructor. 
817       * It returns a TextField editor.
818       * 
819       * @return a new TextField editor
820       */
821      protected TreeCellEditor createTreeCellEditor()
822      {
823        Border border = UIManager.getBorder("Tree.editorBorder");
824        JTextField tf = new DefaultTreeCellEditor.DefaultTextField(border);
825        DefaultCellEditor editor = new DefaultCellEditor(tf);
826        editor.setClickCountToStart(1);
827        realEditor = editor;
828        return editor;
829      }
830    }