1/*
2 * Copyright (c) 1998, 2015, 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 */
25package javax.swing.tree;
26
27import javax.swing.*;
28import javax.swing.border.*;
29import javax.swing.event.*;
30import javax.swing.plaf.FontUIResource;
31import java.awt.*;
32import java.awt.event.*;
33import java.beans.BeanProperty;
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    protected transient 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    /**
481     * Determine the offset.
482     * @param tree      a <code>JTree</code> object
483     * @param value a value
484     * @param isSelected selection status
485     * @param expanded expansion status
486     * @param leaf leaf status
487     * @param row current row
488     */
489    protected void determineOffset(JTree tree, Object value,
490                                   boolean isSelected, boolean expanded,
491                                   boolean leaf, int row) {
492        if(renderer != null) {
493            if(leaf)
494                editingIcon = renderer.getLeafIcon();
495            else if(expanded)
496                editingIcon = renderer.getOpenIcon();
497            else
498                editingIcon = renderer.getClosedIcon();
499            if(editingIcon != null)
500                offset = renderer.getIconTextGap() +
501                         editingIcon.getIconWidth();
502            else
503                offset = renderer.getIconTextGap();
504        }
505        else {
506            editingIcon = null;
507            offset = 0;
508        }
509    }
510
511    /**
512     * Invoked just before editing is to start. Will add the
513     * <code>editingComponent</code> to the
514     * <code>editingContainer</code>.
515     */
516    protected void prepareForEditing() {
517        if (editingComponent != null) {
518            editingContainer.add(editingComponent);
519        }
520    }
521
522    /**
523     * Creates the container to manage placement of
524     * <code>editingComponent</code>.
525     *
526     * @return new Container object
527     */
528    protected Container createContainer() {
529        return new EditorContainer();
530    }
531
532    /**
533     * This is invoked if a <code>TreeCellEditor</code>
534     * is not supplied in the constructor.
535     * It returns a <code>TextField</code> editor.
536     * @return a new <code>TextField</code> editor
537     */
538    protected TreeCellEditor createTreeCellEditor() {
539        Border              aBorder = UIManager.getBorder("Tree.editorBorder");
540        @SuppressWarnings("serial") // Safe: outer class is non-serializable
541        DefaultCellEditor   editor = new DefaultCellEditor
542            (new DefaultTextField(aBorder)) {
543            public boolean shouldSelectCell(EventObject event) {
544                boolean retValue = super.shouldSelectCell(event);
545                return retValue;
546            }
547        };
548
549        // One click to edit.
550        editor.setClickCountToStart(1);
551        return editor;
552    }
553
554    /**
555     * Cleans up any state after editing has completed. Removes the
556     * <code>editingComponent</code> the <code>editingContainer</code>.
557     */
558    private void cleanupAfterEditing() {
559        if (editingComponent != null) {
560            editingContainer.remove(editingComponent);
561        }
562        editingComponent = null;
563    }
564
565    /**
566     * <code>TextField</code> used when no editor is supplied.
567     * This textfield locks into the border it is constructed with.
568     * It also prefers its parents font over its font. And if the
569     * renderer is not <code>null</code> and no font
570     * has been specified the preferred height is that of the renderer.
571     */
572    @SuppressWarnings("serial") // Safe: outer class is non-serializable
573    public class DefaultTextField extends JTextField {
574        /** Border to use. */
575        protected Border         border;
576
577        /**
578         * Constructs a
579         * <code>DefaultTreeCellEditor.DefaultTextField</code> object.
580         *
581         * @param border  a <code>Border</code> object
582         * @since 1.4
583         */
584        public DefaultTextField(Border border) {
585            setBorder(border);
586        }
587
588        /**
589         * Sets the border of this component.<p>
590         * This is a bound property.
591         *
592         * @param border the border to be rendered for this component
593         * @see Border
594         * @see CompoundBorder
595         */
596        @BeanProperty(preferred = true, visualUpdate = true, description
597                = "The component's border.")
598        public void setBorder(Border border) {
599            super.setBorder(border);
600            this.border = border;
601        }
602
603        /**
604         * Overrides <code>JComponent.getBorder</code> to
605         * returns the current border.
606         */
607        public Border getBorder() {
608            return border;
609        }
610
611        // implements java.awt.MenuContainer
612        public Font getFont() {
613            Font     font = super.getFont();
614
615            // Prefer the parent containers font if our font is a
616            // FontUIResource
617            if(font instanceof FontUIResource) {
618                Container     parent = getParent();
619
620                if(parent != null && parent.getFont() != null)
621                    font = parent.getFont();
622            }
623            return font;
624        }
625
626        /**
627         * Overrides <code>JTextField.getPreferredSize</code> to
628         * return the preferred size based on current font, if set,
629         * or else use renderer's font.
630         * @return a <code>Dimension</code> object containing
631         *   the preferred size
632         */
633        public Dimension getPreferredSize() {
634            Dimension      size = super.getPreferredSize();
635
636            // If not font has been set, prefer the renderers height.
637            if(renderer != null &&
638               DefaultTreeCellEditor.this.getFont() == null) {
639                Dimension     rSize = renderer.getPreferredSize();
640
641                size.height = rSize.height;
642            }
643            return size;
644        }
645    }
646
647
648    /**
649     * Container responsible for placing the <code>editingComponent</code>.
650     */
651    @SuppressWarnings("serial") // Safe: outer class is non-serializable
652    public class EditorContainer extends Container {
653        /**
654         * Constructs an <code>EditorContainer</code> object.
655         */
656        public EditorContainer() {
657            setLayout(null);
658        }
659
660        // This should not be used. It will be removed when new API is
661        // allowed.
662        /**
663         * Do not use.
664         */
665        public void EditorContainer() {
666            setLayout(null);
667        }
668
669        /**
670         * Overrides <code>Container.paint</code> to paint the node's
671         * icon and use the selection color for the background.
672         */
673        public void paint(Graphics g) {
674            int width = getWidth();
675            int height = getHeight();
676
677            // Then the icon.
678            if(editingIcon != null) {
679                int yLoc = calculateIconY(editingIcon);
680
681                if (getComponentOrientation().isLeftToRight()) {
682                    editingIcon.paintIcon(this, g, 0, yLoc);
683                } else {
684                    editingIcon.paintIcon(
685                            this, g, width - editingIcon.getIconWidth(),
686                            yLoc);
687                }
688            }
689
690            // Border selection color
691            Color       background = getBorderSelectionColor();
692            if(background != null) {
693                g.setColor(background);
694                g.drawRect(0, 0, width - 1, height - 1);
695            }
696            super.paint(g);
697        }
698
699        /**
700         * Lays out this <code>Container</code>.  If editing,
701         * the editor will be placed at
702         * <code>offset</code> in the x direction and 0 for y.
703         */
704        public void doLayout() {
705            if(editingComponent != null) {
706                int width = getWidth();
707                int height = getHeight();
708                if (getComponentOrientation().isLeftToRight()) {
709                    editingComponent.setBounds(
710                            offset, 0, width - offset, height);
711                } else {
712                    editingComponent.setBounds(
713                        0, 0, width - offset, height);
714                }
715            }
716        }
717
718        /**
719         * Calculate the y location for the icon.
720         */
721        private int calculateIconY(Icon icon) {
722            // To make sure the icon position matches that of the
723            // renderer, use the same algorithm as JLabel
724            // (SwingUtilities.layoutCompoundLabel).
725            int iconHeight = icon.getIconHeight();
726            int textHeight = editingComponent.getFontMetrics(
727                editingComponent.getFont()).getHeight();
728            int textY = iconHeight / 2 - textHeight / 2;
729            int totalY = Math.min(0, textY);
730            int totalHeight = Math.max(iconHeight, textY + textHeight) -
731                totalY;
732            return getHeight() / 2 - (totalY + (totalHeight / 2));
733        }
734
735        /**
736         * Returns the preferred size for the <code>Container</code>.
737         * This will be at least preferred size of the editor plus
738         * <code>offset</code>.
739         * @return a <code>Dimension</code> containing the preferred
740         *   size for the <code>Container</code>; if
741         *   <code>editingComponent</code> is <code>null</code> the
742         *   <code>Dimension</code> returned is 0, 0
743         */
744        public Dimension getPreferredSize() {
745            if(editingComponent != null) {
746                Dimension         pSize = editingComponent.getPreferredSize();
747
748                pSize.width += offset + 5;
749
750                Dimension         rSize = (renderer != null) ?
751                                          renderer.getPreferredSize() : null;
752
753                if(rSize != null)
754                    pSize.height = Math.max(pSize.height, rSize.height);
755                if(editingIcon != null)
756                    pSize.height = Math.max(pSize.height,
757                                            editingIcon.getIconHeight());
758
759                // Make sure width is at least 100.
760                pSize.width = Math.max(pSize.width, 100);
761                return pSize;
762            }
763            return new Dimension(0, 0);
764        }
765    }
766}
767