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       * Constructs a DefaultTreeCellEditor object for a JTree using the
312       * specified renderer and a default editor. (Use this constructor
313       * for normal editing.)
314       *
315       * @param tree - a JTree object
316       * @param renderer - a DefaultTreeCellRenderer object
317       */
318      public DefaultTreeCellEditor(JTree tree, DefaultTreeCellRenderer renderer)
319      {
320        this(tree, renderer, null);
321      }
322    
323      /**
324       * Constructs a DefaultTreeCellEditor  object for a JTree using the specified
325       * renderer and the specified editor. (Use this constructor
326       * for specialized editing.)
327       *
328       * @param tree - a JTree object
329       * @param renderer - a DefaultTreeCellRenderer object
330       * @param editor - a TreeCellEditor object
331       */
332      public DefaultTreeCellEditor(JTree tree, DefaultTreeCellRenderer renderer,
333                                   TreeCellEditor editor)
334      {
335        this.renderer = renderer;
336        realEditor = editor;
337        if (realEditor == null)
338          realEditor = createTreeCellEditor();
339        editingContainer = createContainer();
340        setTree(tree);
341        Color c = UIManager.getColor("Tree.editorBorderSelectionColor");
342        setBorderSelectionColor(c);
343      }
344    
345      /**
346       * writeObject
347       *
348       * @param value0
349       *          TODO
350       * @exception IOException
351       *              TODO
352       */
353      private void writeObject(ObjectOutputStream value0) throws IOException
354      {
355        // TODO
356      }
357    
358      /**
359       * readObject
360       * @param value0 TODO
361       * @exception IOException TODO
362       * @exception ClassNotFoundException TODO
363       */
364      private void readObject(ObjectInputStream value0)
365        throws IOException, ClassNotFoundException
366      {
367        // TODO
368      }
369    
370      /**
371       * Sets the color to use for the border.
372       * @param newColor - the new border color
373       */
374      public void setBorderSelectionColor(Color newColor)
375      {
376        this.borderSelectionColor = newColor;
377      }
378    
379      /**
380       * Returns the color the border is drawn.
381       * @return Color
382       */
383      public Color getBorderSelectionColor()
384      {
385        return borderSelectionColor;
386      }
387    
388      /**
389       * Sets the font to edit with. null indicates the renderers
390       * font should be used. This will NOT override any font you have
391       * set in the editor the receiver was instantied with. If null for
392       * an editor was passed in, a default editor will be created that
393       * will pick up this font.
394       *
395       * @param font - the editing Font
396       */
397      public void setFont(Font font)
398      {
399        if (font != null)
400          this.font = font;
401        else
402          this.font = renderer.getFont();
403      }
404    
405      /**
406       * Gets the font used for editing.
407       *
408       * @return the editing font
409       */
410      public Font getFont()
411      {
412        return font;
413      }
414    
415      /**
416       * Configures the editor. Passed onto the realEditor.
417       * Sets an initial value for the editor. This will cause
418       * the editor to stopEditing and lose any partially edited value
419       * if the editor is editing when this method is called.
420       * Returns the component that should be added to the client's Component
421       * hierarchy. Once installed in the client's hierarchy this component will
422       * then be able to draw and receive user input.
423       *
424       * @param tree - the JTree that is asking the editor to edit; this parameter can be null
425       * @param value - the value of the cell to be edited
426       * @param isSelected - true is the cell is to be rendered with selection highlighting
427       * @param expanded - true if the node is expanded
428       * @param leaf - true if the node is a leaf node
429       * @param row - the row index of the node being edited
430       *
431       * @return the component for editing
432       */
433      public Component getTreeCellEditorComponent(JTree tree, Object value,
434                                                  boolean isSelected,
435                                                  boolean expanded,
436                                                  boolean leaf, int row)
437      {
438        setTree(tree);
439        lastRow = row;
440        determineOffset(tree, value, isSelected, expanded, leaf, row);
441        if (editingComponent != null)
442          editingContainer.remove(editingComponent);
443    
444        editingComponent = realEditor.getTreeCellEditorComponent(tree, value,
445                                                                 isSelected,
446                                                                 expanded, leaf,
447                                                                 row);
448        Font f = getFont();
449        if (f == null)
450          {
451            if (renderer != null)
452              f = renderer.getFont();
453            if (f == null)
454              f = tree.getFont();
455          }
456        editingContainer.setFont(f);
457        prepareForEditing();
458        return editingContainer;
459      }
460    
461      /**
462       * Returns the value currently being edited (requests it from the
463       * {@link #realEditor}.
464       *
465       * @return the value currently being edited
466       */
467      public Object getCellEditorValue()
468      {
469        return realEditor.getCellEditorValue();
470      }
471    
472      /**
473       * If the realEditor returns true to this message, prepareForEditing
474       * is messaged and true is returned.
475       *
476       * @param event - the event the editor should use to consider whether to
477       * begin editing or not
478       * @return true if editing can be started
479       */
480      public boolean isCellEditable(EventObject event)
481      {
482        boolean ret = false;
483        boolean ed = false;
484        if (event != null)
485          {
486            if (event.getSource() instanceof JTree)
487              {
488                setTree((JTree) event.getSource());
489                if (event instanceof MouseEvent)
490                  {
491                    MouseEvent me = (MouseEvent) event;
492                    TreePath path = tree.getPathForLocation(me.getX(), me.getY());
493                    ed = lastPath != null && path != null && lastPath.equals(path);
494                    if (path != null)
495                      {
496                        lastRow = tree.getRowForPath(path);
497                        Object val = path.getLastPathComponent();
498                        boolean isSelected = tree.isRowSelected(lastRow);
499                        boolean isExpanded = tree.isExpanded(path);
500                        TreeModel m = tree.getModel();
501                        boolean isLeaf = m.isLeaf(val);
502                        determineOffset(tree, val, isSelected, isExpanded, isLeaf,
503                                        lastRow);
504                      }
505                  }
506              }
507          }
508        if (! realEditor.isCellEditable(event))
509          ret = false;
510        else
511          {
512            if (canEditImmediately(event))
513              ret = true;
514            else if (ed && shouldStartEditingTimer(event))
515              startEditingTimer();
516            else if (timer != null && timer.isRunning())
517              timer.stop();
518          }
519        if (ret)
520          prepareForEditing();
521        return ret;
522    
523      }
524    
525      /**
526       * Messages the realEditor for the return value.
527       *
528       * @param event -
529       *          the event the editor should use to start editing
530       * @return true if the editor would like the editing cell to be selected;
531       *         otherwise returns false
532       */
533      public boolean shouldSelectCell(EventObject event)
534      {
535        return true;
536      }
537    
538      /**
539       * If the realEditor will allow editing to stop, the realEditor
540       * is removed and true is returned, otherwise false is returned.
541       * @return true if editing was stopped; false otherwise
542       */
543      public boolean stopCellEditing()
544      {
545        boolean ret = false;
546        if (realEditor.stopCellEditing())
547          {
548            finish();
549            ret = true;
550          }
551        return ret;
552      }
553    
554      /**
555       * Messages cancelCellEditing to the realEditor and removes it
556       * from this instance.
557       */
558      public void cancelCellEditing()
559      {
560        realEditor.cancelCellEditing();
561        finish();
562      }
563    
564      private void finish()
565      {
566        if (editingComponent != null)
567          editingContainer.remove(editingComponent);
568        editingComponent = null;
569      }
570    
571      /**
572       * Adds a <code>CellEditorListener</code> object to this editor.
573       *
574       * @param listener
575       *          the listener to add
576       */
577      public void addCellEditorListener(CellEditorListener listener)
578      {
579        realEditor.addCellEditorListener(listener);
580      }
581    
582      /**
583       * Removes a <code>CellEditorListener</code> object.
584       *
585       * @param listener the listener to remove
586       */
587      public void removeCellEditorListener(CellEditorListener listener)
588      {
589        realEditor.removeCellEditorListener(listener);
590      }
591    
592      /**
593       * Returns all added <code>CellEditorListener</code> objects to this editor.
594       *
595       * @return an array of listeners
596       *
597       * @since 1.4
598       */
599      public CellEditorListener[] getCellEditorListeners()
600      {
601        return (CellEditorListener[]) listenerList.getListeners(CellEditorListener.class);
602      }
603    
604      /**
605       * Resets lastPath.
606       *
607       * @param e - the event that characterizes the change.
608       */
609      public void valueChanged(TreeSelectionEvent e)
610      {
611        if (tree != null)
612          {
613            if (tree.getSelectionCount() == 1)
614              lastPath = tree.getSelectionPath();
615            else
616              lastPath = null;
617          }
618        // TODO: We really should do the following here, but can't due
619        // to buggy DefaultTreeSelectionModel. This selection model
620        // should only fire if the selection actually changes.
621    //    if (timer != null)
622    //      timer.stop();
623      }
624    
625      /**
626       * Messaged when the timer fires.
627       *
628       * @param e the event that characterizes the action.
629       */
630      public void actionPerformed(ActionEvent e)
631      {
632        if (tree != null && lastPath != null)
633          tree.startEditingAtPath(lastPath);
634      }
635    
636      /**
637       * Sets the tree currently editing for. This is needed to add a selection
638       * listener.
639       *
640       * @param newTree -
641       *          the new tree to be edited
642       */
643      protected void setTree(JTree newTree)
644      {
645        if (tree != newTree)
646          {
647            if (tree != null)
648              tree.removeTreeSelectionListener(this);
649            tree = newTree;
650            if (tree != null)
651              tree.addTreeSelectionListener(this);
652    
653            if (timer != null)
654              timer.stop();
655          }
656      }
657    
658      /**
659       * Returns true if event is a MouseEvent and the click count is 1.
660       *
661       * @param event - the event being studied
662       * @return true if editing should start
663       */
664      protected boolean shouldStartEditingTimer(EventObject event)
665      {
666        boolean ret = false;
667        if (event instanceof MouseEvent)
668          {
669            MouseEvent me = (MouseEvent) event;
670            ret = SwingUtilities.isLeftMouseButton(me) && me.getClickCount() == 1
671                  && inHitRegion(me.getX(), me.getY());
672          }
673        return ret;
674      }
675    
676      /**
677       * Starts the editing timer (if one installed).
678       */
679      protected void startEditingTimer()
680      {
681        if (timer == null)
682          {
683            timer = new Timer(1200, this);
684            timer.setRepeats(false);
685          }
686        timer.start();
687      }
688    
689      /**
690       * Returns true if event is null, or it is a MouseEvent with
691       * a click count > 2 and inHitRegion returns true.
692       *
693       * @param event - the event being studied
694       * @return true if event is null, or it is a MouseEvent with
695       * a click count > 2 and inHitRegion returns true
696       */
697      protected boolean canEditImmediately(EventObject event)
698      {
699        if (event == null || !(event instanceof MouseEvent) || (((MouseEvent) event).
700            getClickCount() > 2 && inHitRegion(((MouseEvent) event).getX(),
701                                             ((MouseEvent) event).getY())))
702          return true;
703        return false;
704      }
705    
706      /**
707       * Returns true if the passed in location is a valid mouse location
708       * to start editing from. This is implemented to return false if x is
709       * less than or equal to the width of the icon and icon
710       * gap displayed by the renderer. In other words this returns true if
711       * the user clicks over the text part displayed by the renderer, and
712       * false otherwise.
713       *
714       * @param x - the x-coordinate of the point
715       * @param y - the y-coordinate of the point
716       *
717       * @return true if the passed in location is a valid mouse location
718       */
719      protected boolean inHitRegion(int x, int y)
720      {
721        Rectangle bounds = tree.getPathBounds(lastPath);
722        return bounds.contains(x, y);
723      }
724    
725      /**
726       * determineOffset
727       * @param tree -
728       * @param value -
729       * @param isSelected -
730       * @param expanded -
731       * @param leaf -
732       * @param row -
733       */
734      protected void determineOffset(JTree tree, Object value, boolean isSelected,
735                                     boolean expanded, boolean leaf, int row)
736      {
737        if (renderer != null)
738          {
739            if (leaf)
740              editingIcon = renderer.getLeafIcon();
741            else if (expanded)
742              editingIcon = renderer.getOpenIcon();
743            else
744              editingIcon = renderer.getClosedIcon();
745            if (editingIcon != null)
746              offset = renderer.getIconTextGap() + editingIcon.getIconWidth();
747            else
748              offset = renderer.getIconTextGap();
749          }
750        else
751          {
752            editingIcon = null;
753            offset = 0;
754          }
755      }
756    
757      /**
758       * Invoked just before editing is to start. Will add the
759       * editingComponent to the editingContainer.
760       */
761      protected void prepareForEditing()
762      {
763        if (editingComponent != null)
764          editingContainer.add(editingComponent);
765      }
766    
767      /**
768       * Creates the container to manage placement of editingComponent.
769       *
770       * @return the container to manage the placement of the editingComponent.
771       */
772      protected Container createContainer()
773      {
774        return new DefaultTreeCellEditor.EditorContainer();
775      }
776    
777      /**
778       * This is invoked if a TreeCellEditor is not supplied in the constructor.
779       * It returns a TextField editor.
780       *
781       * @return a new TextField editor
782       */
783      protected TreeCellEditor createTreeCellEditor()
784      {
785        Border border = UIManager.getBorder("Tree.editorBorder");
786        JTextField tf = new DefaultTreeCellEditor.DefaultTextField(border);
787        DefaultCellEditor editor = new DefaultCellEditor(tf);
788        editor.setClickCountToStart(1);
789        realEditor = editor;
790        return editor;
791      }
792    }