DefaultTreeCellEditor.java revision 10517:e50dfa1c0902
1/*
2 * Copyright (c) 1998, 2014, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.  Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26package javax.swing.tree;
27
28import javax.swing.*;
29import javax.swing.border.*;
30import javax.swing.event.*;
31import javax.swing.plaf.FontUIResource;
32import java.awt.*;
33import java.awt.event.*;
34import java.util.EventObject;
35
36/**
37 * A <code>TreeCellEditor</code>. You need to supply an
38 * instance of <code>DefaultTreeCellRenderer</code>
39 * so that the icons can be obtained. You can optionally supply
40 * a <code>TreeCellEditor</code> that will be layed out according
41 * to the icon in the <code>DefaultTreeCellRenderer</code>.
42 * If you do not supply a <code>TreeCellEditor</code>,
43 * a <code>TextField</code> will be used. Editing is started
44 * on a triple mouse click, or after a click, pause, click and
45 * a delay of 1200 milliseconds.
46 *<p>
47 * <strong>Warning:</strong>
48 * Serialized objects of this class will not be compatible with
49 * future Swing releases. The current serialization support is
50 * appropriate for short term storage or RMI between applications running
51 * the same version of Swing.  As of 1.4, support for long term storage
52 * of all JavaBeans&trade;
53 * has been added to the <code>java.beans</code> package.
54 * Please see {@link java.beans.XMLEncoder}.
55 *
56 * @see javax.swing.JTree
57 *
58 * @author Scott Violet
59 */
60public class DefaultTreeCellEditor implements ActionListener, TreeCellEditor,
61            TreeSelectionListener {
62    /** Editor handling the editing. */
63    protected TreeCellEditor               realEditor;
64
65    /** Renderer, used to get border and offsets from. */
66    protected DefaultTreeCellRenderer      renderer;
67
68    /** Editing container, will contain the <code>editorComponent</code>. */
69    protected Container                    editingContainer;
70
71    /**
72     * Component used in editing, obtained from the
73     * <code>editingContainer</code>.
74     */
75    transient protected Component          editingComponent;
76
77    /**
78     * As of Java 2 platform v1.4 this field should no longer be used. If
79     * you wish to provide similar behavior you should directly override
80     * <code>isCellEditable</code>.
81     */
82    protected boolean                      canEdit;
83
84    /**
85     * Used in editing. Indicates x position to place
86     * <code>editingComponent</code>.
87     */
88    protected transient int                offset;
89
90    /** <code>JTree</code> instance listening too. */
91    protected transient JTree              tree;
92
93    /** Last path that was selected. */
94    protected transient TreePath           lastPath;
95
96    /** Used before starting the editing session. */
97    protected transient Timer              timer;
98
99    /**
100     * Row that was last passed into
101     * <code>getTreeCellEditorComponent</code>.
102     */
103    protected transient int                lastRow;
104
105    /** True if the border selection color should be drawn. */
106    protected Color                        borderSelectionColor;
107
108    /** Icon to use when editing. */
109    protected transient Icon               editingIcon;
110
111    /**
112     * Font to paint with, <code>null</code> indicates
113     * font of renderer is to be used.
114     */
115    protected Font                         font;
116
117
118    /**
119     * Constructs a <code>DefaultTreeCellEditor</code>
120     * object for a JTree using the specified renderer and
121     * a default editor. (Use this constructor for normal editing.)
122     *
123     * @param tree      a <code>JTree</code> object
124     * @param renderer  a <code>DefaultTreeCellRenderer</code> object
125     */
126    public DefaultTreeCellEditor(JTree tree,
127                                 DefaultTreeCellRenderer renderer) {
128        this(tree, renderer, null);
129    }
130
131    /**
132     * Constructs a <code>DefaultTreeCellEditor</code>
133     * object for a <code>JTree</code> using the
134     * specified renderer and the specified editor. (Use this constructor
135     * for specialized editing.)
136     *
137     * @param tree      a <code>JTree</code> object
138     * @param renderer  a <code>DefaultTreeCellRenderer</code> object
139     * @param editor    a <code>TreeCellEditor</code> object
140     */
141    public DefaultTreeCellEditor(JTree tree, DefaultTreeCellRenderer renderer,
142                                 TreeCellEditor editor) {
143        this.renderer = renderer;
144        realEditor = editor;
145        if(realEditor == null)
146            realEditor = createTreeCellEditor();
147        editingContainer = createContainer();
148        setTree(tree);
149        setBorderSelectionColor(UIManager.getColor
150                                ("Tree.editorBorderSelectionColor"));
151    }
152
153    /**
154      * Sets the color to use for the border.
155      * @param newColor the new border color
156      */
157    public void setBorderSelectionColor(Color newColor) {
158        borderSelectionColor = newColor;
159    }
160
161    /**
162      * Returns the color the border is drawn.
163      * @return the border selection color
164      */
165    public Color getBorderSelectionColor() {
166        return borderSelectionColor;
167    }
168
169    /**
170     * Sets the font to edit with. <code>null</code> indicates
171     * the renderers font should be used. This will NOT
172     * override any font you have set in the editor
173     * the receiver was instantiated with. If <code>null</code>
174     * for an editor was passed in a default editor will be
175     * created that will pick up this font.
176     *
177     * @param font  the editing <code>Font</code>
178     * @see #getFont
179     */
180    public void setFont(Font font) {
181        this.font = font;
182    }
183
184    /**
185     * Gets the font used for editing.
186     *
187     * @return the editing <code>Font</code>
188     * @see #setFont
189     */
190    public Font getFont() {
191        return font;
192    }
193
194    //
195    // TreeCellEditor
196    //
197
198    /**
199     * Configures the editor.  Passed onto the <code>realEditor</code>.
200     */
201    public Component getTreeCellEditorComponent(JTree tree, Object value,
202                                                boolean isSelected,
203                                                boolean expanded,
204                                                boolean leaf, int row) {
205        setTree(tree);
206        lastRow = row;
207        determineOffset(tree, value, isSelected, expanded, leaf, row);
208
209        if (editingComponent != null) {
210            editingContainer.remove(editingComponent);
211        }
212        editingComponent = realEditor.getTreeCellEditorComponent(tree, value,
213                                        isSelected, expanded,leaf, row);
214
215
216        // this is kept for backwards compatibility but isn't really needed
217        // with the current BasicTreeUI implementation.
218        TreePath        newPath = tree.getPathForRow(row);
219
220        canEdit = (lastPath != null && newPath != null &&
221                   lastPath.equals(newPath));
222
223        Font            font = getFont();
224
225        if(font == null) {
226            if(renderer != null)
227                font = renderer.getFont();
228            if(font == null)
229                font = tree.getFont();
230        }
231        editingContainer.setFont(font);
232        prepareForEditing();
233        return editingContainer;
234    }
235
236    /**
237     * Returns the value currently being edited.
238     * @return the value currently being edited
239     */
240    public Object getCellEditorValue() {
241        return realEditor.getCellEditorValue();
242    }
243
244    /**
245     * If the <code>realEditor</code> returns true to this
246     * message, <code>prepareForEditing</code>
247     * is messaged and true is returned.
248     */
249    public boolean isCellEditable(EventObject event) {
250        boolean            retValue = false;
251        boolean            editable = false;
252
253        if (event != null) {
254            if (event.getSource() instanceof JTree) {
255                setTree((JTree)event.getSource());
256                if (event instanceof MouseEvent) {
257                    TreePath path = tree.getPathForLocation(
258                                         ((MouseEvent)event).getX(),
259                                         ((MouseEvent)event).getY());
260                    editable = (lastPath != null && path != null &&
261                               lastPath.equals(path));
262                    if (path!=null) {
263                        lastRow = tree.getRowForPath(path);
264                        Object value = path.getLastPathComponent();
265                        boolean isSelected = tree.isRowSelected(lastRow);
266                        boolean expanded = tree.isExpanded(path);
267                        TreeModel treeModel = tree.getModel();
268                        boolean leaf = treeModel.isLeaf(value);
269                        determineOffset(tree, value, isSelected,
270                                        expanded, leaf, lastRow);
271                    }
272                }
273            }
274        }
275        if(!realEditor.isCellEditable(event))
276            return false;
277        if(canEditImmediately(event))
278            retValue = true;
279        else if(editable && shouldStartEditingTimer(event)) {
280            startEditingTimer();
281        }
282        else if(timer != null && timer.isRunning())
283            timer.stop();
284        if(retValue)
285            prepareForEditing();
286        return retValue;
287    }
288
289    /**
290     * Messages the <code>realEditor</code> for the return value.
291     */
292    public boolean shouldSelectCell(EventObject event) {
293        return realEditor.shouldSelectCell(event);
294    }
295
296    /**
297     * If the <code>realEditor</code> will allow editing to stop,
298     * the <code>realEditor</code> is removed and true is returned,
299     * otherwise false is returned.
300     */
301    public boolean stopCellEditing() {
302        if(realEditor.stopCellEditing()) {
303            cleanupAfterEditing();
304            return true;
305        }
306        return false;
307    }
308
309    /**
310     * Messages <code>cancelCellEditing</code> to the
311     * <code>realEditor</code> and removes it from this instance.
312     */
313    public void cancelCellEditing() {
314        realEditor.cancelCellEditing();
315        cleanupAfterEditing();
316    }
317
318    /**
319     * Adds the <code>CellEditorListener</code>.
320     * @param l the listener to be added
321     */
322    public void addCellEditorListener(CellEditorListener l) {
323        realEditor.addCellEditorListener(l);
324    }
325
326    /**
327      * Removes the previously added <code>CellEditorListener</code>.
328      * @param l the listener to be removed
329      */
330    public void removeCellEditorListener(CellEditorListener l) {
331        realEditor.removeCellEditorListener(l);
332    }
333
334    /**
335     * Returns an array of all the <code>CellEditorListener</code>s added
336     * to this DefaultTreeCellEditor with addCellEditorListener().
337     *
338     * @return all of the <code>CellEditorListener</code>s added or an empty
339     *         array if no listeners have been added
340     * @since 1.4
341     */
342    public CellEditorListener[] getCellEditorListeners() {
343        return ((DefaultCellEditor)realEditor).getCellEditorListeners();
344    }
345
346    //
347    // TreeSelectionListener
348    //
349
350    /**
351     * Resets <code>lastPath</code>.
352     */
353    public void valueChanged(TreeSelectionEvent e) {
354        if(tree != null) {
355            if(tree.getSelectionCount() == 1)
356                lastPath = tree.getSelectionPath();
357            else
358                lastPath = null;
359        }
360        if(timer != null) {
361            timer.stop();
362        }
363    }
364
365    //
366    // ActionListener (for Timer).
367    //
368
369    /**
370     * Messaged when the timer fires, this will start the editing
371     * session.
372     */
373    public void actionPerformed(ActionEvent e) {
374        if(tree != null && lastPath != null) {
375            tree.startEditingAtPath(lastPath);
376        }
377    }
378
379    //
380    // Local methods
381    //
382
383    /**
384     * Sets the tree currently editing for. This is needed to add
385     * a selection listener.
386     * @param newTree the new tree to be edited
387     */
388    protected void setTree(JTree newTree) {
389        if(tree != newTree) {
390            if(tree != null)
391                tree.removeTreeSelectionListener(this);
392            tree = newTree;
393            if(tree != null)
394                tree.addTreeSelectionListener(this);
395            if(timer != null) {
396                timer.stop();
397            }
398        }
399    }
400
401    /**
402     * Returns true if <code>event</code> is a <code>MouseEvent</code>
403     * and the click count is 1.
404     *
405     * @param event the event being studied
406     * @return whether {@code event} should starts the editing timer
407     */
408    protected boolean shouldStartEditingTimer(EventObject event) {
409        if((event instanceof MouseEvent) &&
410            SwingUtilities.isLeftMouseButton((MouseEvent)event)) {
411            MouseEvent        me = (MouseEvent)event;
412
413            return (me.getClickCount() == 1 &&
414                    inHitRegion(me.getX(), me.getY()));
415        }
416        return false;
417    }
418
419    /**
420     * Starts the editing timer.
421     */
422    protected void startEditingTimer() {
423        if(timer == null) {
424            timer = new Timer(1200, this);
425            timer.setRepeats(false);
426        }
427        timer.start();
428    }
429
430    /**
431     * Returns true if <code>event</code> is <code>null</code>,
432     * or it is a <code>MouseEvent</code> with a click count &gt; 2
433     * and <code>inHitRegion</code> returns true.
434     *
435     * @param event the event being studied
436     * @return whether editing can be started for the given {@code event}
437     */
438    protected boolean canEditImmediately(EventObject event) {
439        if((event instanceof MouseEvent) &&
440           SwingUtilities.isLeftMouseButton((MouseEvent)event)) {
441            MouseEvent       me = (MouseEvent)event;
442
443            return ((me.getClickCount() > 2) &&
444                    inHitRegion(me.getX(), me.getY()));
445        }
446        return (event == null);
447    }
448
449    /**
450     * Returns true if the passed in location is a valid mouse location
451     * to start editing from. This is implemented to return false if
452     * <code>x</code> is &lt;= the width of the icon and icon gap displayed
453     * by the renderer. In other words this returns true if the user
454     * clicks over the text part displayed by the renderer, and false
455     * otherwise.
456     * @param x the x-coordinate of the point
457     * @param y the y-coordinate of the point
458     * @return true if the passed in location is a valid mouse location
459     */
460    protected boolean inHitRegion(int x, int y) {
461        if(lastRow != -1 && tree != null) {
462            Rectangle bounds = tree.getRowBounds(lastRow);
463            ComponentOrientation treeOrientation = tree.getComponentOrientation();
464
465            if ( treeOrientation.isLeftToRight() ) {
466                if (bounds != null && x <= (bounds.x + offset) &&
467                    offset < (bounds.width - 5)) {
468                    return false;
469                }
470            } else if ( bounds != null &&
471                        ( x >= (bounds.x+bounds.width-offset+5) ||
472                          x <= (bounds.x + 5) ) &&
473                        offset < (bounds.width - 5) ) {
474                return false;
475            }
476        }
477        return true;
478    }
479
480    protected void determineOffset(JTree tree, Object value,
481                                   boolean isSelected, boolean expanded,
482                                   boolean leaf, int row) {
483        if(renderer != null) {
484            if(leaf)
485                editingIcon = renderer.getLeafIcon();
486            else if(expanded)
487                editingIcon = renderer.getOpenIcon();
488            else
489                editingIcon = renderer.getClosedIcon();
490            if(editingIcon != null)
491                offset = renderer.getIconTextGap() +
492                         editingIcon.getIconWidth();
493            else
494                offset = renderer.getIconTextGap();
495        }
496        else {
497            editingIcon = null;
498            offset = 0;
499        }
500    }
501
502    /**
503     * Invoked just before editing is to start. Will add the
504     * <code>editingComponent</code> to the
505     * <code>editingContainer</code>.
506     */
507    protected void prepareForEditing() {
508        if (editingComponent != null) {
509            editingContainer.add(editingComponent);
510        }
511    }
512
513    /**
514     * Creates the container to manage placement of
515     * <code>editingComponent</code>.
516     *
517     * @return new Container object
518     */
519    protected Container createContainer() {
520        return new EditorContainer();
521    }
522
523    /**
524     * This is invoked if a <code>TreeCellEditor</code>
525     * is not supplied in the constructor.
526     * It returns a <code>TextField</code> editor.
527     * @return a new <code>TextField</code> editor
528     */
529    protected TreeCellEditor createTreeCellEditor() {
530        Border              aBorder = UIManager.getBorder("Tree.editorBorder");
531        @SuppressWarnings("serial") // Safe: outer class is non-serializable
532        DefaultCellEditor   editor = new DefaultCellEditor
533            (new DefaultTextField(aBorder)) {
534            public boolean shouldSelectCell(EventObject event) {
535                boolean retValue = super.shouldSelectCell(event);
536                return retValue;
537            }
538        };
539
540        // One click to edit.
541        editor.setClickCountToStart(1);
542        return editor;
543    }
544
545    /**
546     * Cleans up any state after editing has completed. Removes the
547     * <code>editingComponent</code> the <code>editingContainer</code>.
548     */
549    private void cleanupAfterEditing() {
550        if (editingComponent != null) {
551            editingContainer.remove(editingComponent);
552        }
553        editingComponent = null;
554    }
555
556    /**
557     * <code>TextField</code> used when no editor is supplied.
558     * This textfield locks into the border it is constructed with.
559     * It also prefers its parents font over its font. And if the
560     * renderer is not <code>null</code> and no font
561     * has been specified the preferred height is that of the renderer.
562     */
563    @SuppressWarnings("serial") // Safe: outer class is non-serializable
564    public class DefaultTextField extends JTextField {
565        /** Border to use. */
566        protected Border         border;
567
568        /**
569         * Constructs a
570         * <code>DefaultTreeCellEditor.DefaultTextField</code> object.
571         *
572         * @param border  a <code>Border</code> object
573         * @since 1.4
574         */
575        public DefaultTextField(Border border) {
576            setBorder(border);
577        }
578
579        /**
580         * Sets the border of this component.<p>
581         * This is a bound property.
582         *
583         * @param border the border to be rendered for this component
584         * @see Border
585         * @see CompoundBorder
586         * @beaninfo
587         *        bound: true
588         *    preferred: true
589         *    attribute: visualUpdate true
590         *  description: The component's border.
591         */
592        public void setBorder(Border border) {
593            super.setBorder(border);
594            this.border = border;
595        }
596
597        /**
598         * Overrides <code>JComponent.getBorder</code> to
599         * returns the current border.
600         */
601        public Border getBorder() {
602            return border;
603        }
604
605        // implements java.awt.MenuContainer
606        public Font getFont() {
607            Font     font = super.getFont();
608
609            // Prefer the parent containers font if our font is a
610            // FontUIResource
611            if(font instanceof FontUIResource) {
612                Container     parent = getParent();
613
614                if(parent != null && parent.getFont() != null)
615                    font = parent.getFont();
616            }
617            return font;
618        }
619
620        /**
621         * Overrides <code>JTextField.getPreferredSize</code> to
622         * return the preferred size based on current font, if set,
623         * or else use renderer's font.
624         * @return a <code>Dimension</code> object containing
625         *   the preferred size
626         */
627        public Dimension getPreferredSize() {
628            Dimension      size = super.getPreferredSize();
629
630            // If not font has been set, prefer the renderers height.
631            if(renderer != null &&
632               DefaultTreeCellEditor.this.getFont() == null) {
633                Dimension     rSize = renderer.getPreferredSize();
634
635                size.height = rSize.height;
636            }
637            return size;
638        }
639    }
640
641
642    /**
643     * Container responsible for placing the <code>editingComponent</code>.
644     */
645    @SuppressWarnings("serial") // Safe: outer class is non-serializable
646    public class EditorContainer extends Container {
647        /**
648         * Constructs an <code>EditorContainer</code> object.
649         */
650        public EditorContainer() {
651            setLayout(null);
652        }
653
654        // This should not be used. It will be removed when new API is
655        // allowed.
656        public void EditorContainer() {
657            setLayout(null);
658        }
659
660        /**
661         * Overrides <code>Container.paint</code> to paint the node's
662         * icon and use the selection color for the background.
663         */
664        public void paint(Graphics g) {
665            int width = getWidth();
666            int height = getHeight();
667
668            // Then the icon.
669            if(editingIcon != null) {
670                int yLoc = calculateIconY(editingIcon);
671
672                if (getComponentOrientation().isLeftToRight()) {
673                    editingIcon.paintIcon(this, g, 0, yLoc);
674                } else {
675                    editingIcon.paintIcon(
676                            this, g, width - editingIcon.getIconWidth(),
677                            yLoc);
678                }
679            }
680
681            // Border selection color
682            Color       background = getBorderSelectionColor();
683            if(background != null) {
684                g.setColor(background);
685                g.drawRect(0, 0, width - 1, height - 1);
686            }
687            super.paint(g);
688        }
689
690        /**
691         * Lays out this <code>Container</code>.  If editing,
692         * the editor will be placed at
693         * <code>offset</code> in the x direction and 0 for y.
694         */
695        public void doLayout() {
696            if(editingComponent != null) {
697                int width = getWidth();
698                int height = getHeight();
699                if (getComponentOrientation().isLeftToRight()) {
700                    editingComponent.setBounds(
701                            offset, 0, width - offset, height);
702                } else {
703                    editingComponent.setBounds(
704                        0, 0, width - offset, height);
705                }
706            }
707        }
708
709        /**
710         * Calculate the y location for the icon.
711         */
712        private int calculateIconY(Icon icon) {
713            // To make sure the icon position matches that of the
714            // renderer, use the same algorithm as JLabel
715            // (SwingUtilities.layoutCompoundLabel).
716            int iconHeight = icon.getIconHeight();
717            int textHeight = editingComponent.getFontMetrics(
718                editingComponent.getFont()).getHeight();
719            int textY = iconHeight / 2 - textHeight / 2;
720            int totalY = Math.min(0, textY);
721            int totalHeight = Math.max(iconHeight, textY + textHeight) -
722                totalY;
723            return getHeight() / 2 - (totalY + (totalHeight / 2));
724        }
725
726        /**
727         * Returns the preferred size for the <code>Container</code>.
728         * This will be at least preferred size of the editor plus
729         * <code>offset</code>.
730         * @return a <code>Dimension</code> containing the preferred
731         *   size for the <code>Container</code>; if
732         *   <code>editingComponent</code> is <code>null</code> the
733         *   <code>Dimension</code> returned is 0, 0
734         */
735        public Dimension getPreferredSize() {
736            if(editingComponent != null) {
737                Dimension         pSize = editingComponent.getPreferredSize();
738
739                pSize.width += offset + 5;
740
741                Dimension         rSize = (renderer != null) ?
742                                          renderer.getPreferredSize() : null;
743
744                if(rSize != null)
745                    pSize.height = Math.max(pSize.height, rSize.height);
746                if(editingIcon != null)
747                    pSize.height = Math.max(pSize.height,
748                                            editingIcon.getIconHeight());
749
750                // Make sure width is at least 100.
751                pSize.width = Math.max(pSize.width, 100);
752                return pSize;
753            }
754            return new Dimension(0, 0);
755        }
756    }
757}
758