001package org.jdesktop.swingx.plaf;
002
003import java.awt.Component;
004import java.awt.Container;
005import java.awt.Dimension;
006import java.awt.Graphics;
007import java.awt.Insets;
008import java.awt.LayoutManager;
009import java.awt.Rectangle;
010import java.beans.PropertyChangeEvent;
011import java.beans.PropertyChangeListener;
012
013import javax.swing.JTextField;
014import javax.swing.SwingUtilities;
015import javax.swing.border.Border;
016import javax.swing.plaf.UIResource;
017import javax.swing.plaf.basic.BasicBorders.MarginBorder;
018
019import org.jdesktop.swingx.prompt.BuddySupport;
020import org.jdesktop.swingx.prompt.BuddySupport.Position;
021
022public class BuddyLayoutAndBorder implements LayoutManager, Border, PropertyChangeListener, UIResource {
023        private JTextField textField;
024
025        private Border borderDelegate;
026
027        /**
028         * Installs a {@link BuddyLayoutAndBorder} as a layout and border of the
029         * given text field. Registers a {@link PropertyChangeListener} to wrap any
030         * subsequently set border on the text field.
031         */
032        protected void install(JTextField textField) {
033                uninstall();
034                this.textField = textField;
035
036                textField.setLayout(this);
037
038                replaceBorderIfNecessary();
039                textField.addPropertyChangeListener("border", this);
040        }
041
042        public Border getBorderDelegate() {
043                return borderDelegate;
044        }
045
046        /**
047         * Wraps and replaces the text fields default border with this object, to
048         * honor the button margins and sizes of the search, clear and popup buttons
049         * and the layout style.
050         */
051        protected void replaceBorderIfNecessary() {
052                Border original = textField.getBorder();
053
054                if (!(original instanceof BuddyLayoutAndBorder)) {
055                        borderDelegate = original;
056                        textField.setBorder(this);
057                }
058        }
059
060        /**
061         * Does nothing.
062         * 
063         * @see BuddySupport#add(javax.swing.JComponent, Position, JTextField)
064         */
065        @Override
066    public void addLayoutComponent(String name, Component comp) {
067        }
068
069        @Override
070    public Dimension minimumLayoutSize(Container parent) {
071                return preferredLayoutSize(parent);
072        }
073
074        @Override
075    public Dimension preferredLayoutSize(Container parent) {
076                Dimension d = new Dimension();
077
078                // height of highest buddy.
079                for (Component c : BuddySupport.getLeft(textField)) {
080                        d.height = Math.max(d.height, c.getPreferredSize().height);
081                }
082                for (Component c : BuddySupport.getRight(textField)) {
083                        d.height = Math.max(d.height, c.getPreferredSize().height);
084                }
085
086                Insets insets = getRealBorderInsets();
087                d.height += insets.top + insets.bottom;
088                d.width += insets.left + insets.right;
089
090                Insets outerMargin = BuddySupport.getOuterMargin(textField);
091                if (outerMargin != null) {
092                        d.width += outerMargin.left + outerMargin.right;
093                        d.height += outerMargin.bottom + outerMargin.top;
094                }
095
096                return d;
097        }
098
099        /**
100         * Does nothing.
101         * 
102         * @see BuddySupport#remove(javax.swing.JComponent, JTextField)
103         */
104        @Override
105    public void removeLayoutComponent(Component comp) {
106        }
107
108        @Override
109    public void layoutContainer(Container parent) {
110                Rectangle visibleRect = getVisibleRect();
111                Dimension size;
112
113                for (Component comp : BuddySupport.getLeft(textField)) {
114                        if (!comp.isVisible()) {
115                                continue;
116                        }
117                        size = comp.getPreferredSize();
118                        comp.setBounds(visibleRect.x, centerY(visibleRect, size), size.width, size.height);
119
120                        visibleRect.x += size.width;
121                        visibleRect.width -= size.width;
122                }
123
124                for (Component comp : BuddySupport.getRight(textField)) {
125                        if (!comp.isVisible()) {
126                                continue;
127                        }
128
129                        size = comp.getPreferredSize();
130                        comp.setBounds(visibleRect.x + visibleRect.width - size.width, centerY(visibleRect, size), size.width,
131                                        size.height);
132                        visibleRect.width -= size.width;
133                }
134        }
135
136        protected int centerY(Rectangle rect, Dimension size) {
137                return (int) (rect.getCenterY() - (size.height / 2));
138        }
139
140        /**
141         * @return the rectangle allocated by the text field, including the space
142         *         allocated by the child components left and right, the text fields
143         *         original border insets and the outer margin.
144         * 
145         */
146        protected Rectangle getVisibleRect() {
147                Rectangle alloc = SwingUtilities.getLocalBounds(textField);
148
149                substractInsets(alloc, getRealBorderInsets());
150                substractInsets(alloc, BuddySupport.getOuterMargin(textField));
151
152                return alloc;
153        }
154
155        private void substractInsets(Rectangle alloc, Insets insets) {
156                if (insets != null) {
157                        alloc.x += insets.left;
158                        alloc.y += insets.top;
159                        alloc.width -= insets.left + insets.right;
160                        alloc.height -= insets.top + insets.bottom;
161                }
162        }
163
164        /**
165         * Returns the {@link Insets} of the original {@link Border} plus the space
166         * required by the child components.
167         * 
168         * @see javax.swing.border.Border#getBorderInsets(java.awt.Component)
169         */
170        @Override
171    public Insets getBorderInsets(Component c) {
172                Insets insets = null;
173                if (borderDelegate != null) {
174                        // Original insets are cloned to make it work in Mac OS X Aqua LnF.
175                        // Seems that this LnF uses a shared insets instance which should
176                        // not be modified.
177                        // Include margin here
178                        insets = (Insets) borderDelegate.getBorderInsets(textField).clone();
179                } else {
180                        insets = new Insets(0, 0, 0, 0);
181                }
182                //somehow this happens sometimes
183                if (textField == null) {
184                        return insets;
185                }
186                
187                for (Component comp : BuddySupport.getLeft(textField)) {
188                        insets.left += comp.isVisible() ? comp.getPreferredSize().width : 0;
189                }
190                for (Component comp : BuddySupport.getRight(textField)) {
191                        insets.right += comp.isVisible() ? comp.getPreferredSize().width : 0;
192                }
193
194                Insets outerMargin = BuddySupport.getOuterMargin(textField);
195                if (outerMargin != null) {
196                        insets.left += outerMargin.left;
197                        insets.right += outerMargin.right;
198                        insets.top += outerMargin.top;
199                        insets.bottom += outerMargin.bottom;
200                }
201
202                return insets;
203        }
204
205        /**
206         * Returns the insets of the original border (without the margin! Beware of
207         * {@link MarginBorder}!).
208         * 
209         * @return the insets of the border delegate
210         */
211        public Insets getRealBorderInsets() {
212                if (borderDelegate == null) {
213                    //SwingX 1287: null borders are possible and give no insets
214                        return new Insets(0, 0, 0, 0);
215                }
216
217                Insets insets = borderDelegate.getBorderInsets(textField);
218
219                // for some reason, all LnFs add the margin to the insets.
220                // we want the insets without the margin, so substract the margin here!!
221                // TODO: consider checking, if the current border really includes the
222                // margin. Consider:
223                // 1. Not only MarginBorder adds margin
224                // 2. Calling getBorderInsets(null) is not appropriate, since some
225                // Borders can't handle null values.
226                Insets margin = textField.getMargin();
227                if (margin != null) {
228                        insets.left -= margin.left;
229                        insets.right -= margin.right;
230                        insets.top -= margin.top;
231                        insets.bottom -= margin.bottom;
232                }
233
234                return insets;
235        }
236
237        @Override
238    public boolean isBorderOpaque() {
239                if (borderDelegate == null) {
240                        return false;
241                }
242                return borderDelegate.isBorderOpaque();
243        }
244
245        @Override
246    public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) {
247                if (borderDelegate != null) {
248                        borderDelegate.paintBorder(c, g, x, y, width, height);
249                }
250        }
251
252        @Override
253    public void propertyChange(PropertyChangeEvent evt) {
254                replaceBorderIfNecessary();
255        }
256
257        public void uninstall() {
258                if (textField != null) {
259                        textField.removePropertyChangeListener("border", this);
260                        if (textField.getBorder() == this) {
261                                textField.setBorder(borderDelegate);
262                        }
263                        textField.setLayout(null);
264                        textField = null;
265                }
266        }
267
268        @Override
269        public String toString() {
270                return String.format("%s (%s): %s", getClass().getName(), getBorderInsets(null), borderDelegate);
271        }
272}