001/*
002 * $Id: BasicStatusBarUI.java 3927 2011-02-22 16:34:11Z kleopatra $
003 *
004 * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle,
005 * Santa Clara, California 95054, U.S.A. All rights reserved.
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
015 * Lesser General Public License for more details.
016 *
017 * You should have received a copy of the GNU Lesser General Public
018 * License along with this library; if not, write to the Free Software
019 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
020 */
021
022package org.jdesktop.swingx.plaf.basic;
023
024import java.awt.Color;
025import java.awt.Component;
026import java.awt.Container;
027import java.awt.Cursor;
028import java.awt.Dimension;
029import java.awt.Graphics;
030import java.awt.Graphics2D;
031import java.awt.Insets;
032import java.awt.LayoutManager;
033import java.awt.LayoutManager2;
034import java.awt.Point;
035import java.awt.Rectangle;
036import java.awt.Window;
037import java.awt.event.MouseEvent;
038import java.awt.event.MouseListener;
039import java.awt.event.MouseMotionListener;
040import java.beans.PropertyChangeEvent;
041import java.beans.PropertyChangeListener;
042import java.util.HashMap;
043import java.util.Map;
044
045import javax.swing.BorderFactory;
046import javax.swing.JComponent;
047import javax.swing.LookAndFeel;
048import javax.swing.SwingUtilities;
049import javax.swing.border.Border;
050import javax.swing.plaf.BorderUIResource;
051import javax.swing.plaf.ComponentUI;
052import javax.swing.plaf.UIResource;
053
054import org.jdesktop.swingx.JXStatusBar;
055import org.jdesktop.swingx.JXStatusBar.Constraint;
056import org.jdesktop.swingx.plaf.StatusBarUI;
057import org.jdesktop.swingx.plaf.UIManagerExt;
058
059/**
060 *
061 * @author rbair
062 * @author Karl Schaefer
063 */
064public class BasicStatusBarUI extends StatusBarUI {
065    private class Handler implements MouseListener, MouseMotionListener, PropertyChangeListener {
066        private Window window = SwingUtilities.getWindowAncestor(statusBar);
067        private int handleBoundary = getHandleBoundary();
068        private boolean validPress = false;
069        private Point startingPoint;
070        
071        private int getHandleBoundary() {
072            Border border = statusBar.getBorder();
073            
074            if (border == null || !statusBar.isResizeHandleEnabled()) {
075                return 0;
076            }
077            
078            if (statusBar.getComponentOrientation().isLeftToRight()) {
079                return border.getBorderInsets(statusBar).right;
080            } else {
081                return border.getBorderInsets(statusBar).left;
082            }
083        }
084        
085        private boolean isHandleAreaPoint(Point point) {
086            if (window == null || window.isMaximumSizeSet()) {
087                return false;
088            }
089            
090            if (statusBar.getComponentOrientation().isLeftToRight()) {
091                return point.x >= statusBar.getWidth() - handleBoundary;
092            } else {
093                return point.x <= handleBoundary;
094            }
095        }
096        
097        /**
098         * {@inheritDoc}
099         */
100        @Override
101        public void mouseClicked(MouseEvent e) {
102            //does nothing
103        }
104
105        /**
106         * {@inheritDoc}
107         */
108        @Override
109        public void mouseEntered(MouseEvent e) {
110            if (isHandleAreaPoint(e.getPoint())) {
111                if (statusBar.getComponentOrientation().isLeftToRight()) {
112                    window.setCursor(Cursor.getPredefinedCursor(
113                            Cursor.SE_RESIZE_CURSOR));
114                } else {
115                    window.setCursor(Cursor.getPredefinedCursor(
116                            Cursor.SW_RESIZE_CURSOR));
117                }
118            } else {
119                window.setCursor(null);
120            }
121        }
122
123        /**
124         * {@inheritDoc}
125         */
126        @Override
127        public void mouseExited(MouseEvent e) {
128            if (!validPress) {
129                window.setCursor(null);
130            }
131        }
132
133        /**
134         * {@inheritDoc}
135         */
136        @Override
137        public void mousePressed(MouseEvent e) {
138            validPress = SwingUtilities.isLeftMouseButton(e) && isHandleAreaPoint(e.getPoint()); 
139            startingPoint = e.getPoint();
140            SwingUtilities.convertPointToScreen(startingPoint, statusBar);
141        }
142
143        /**
144         * {@inheritDoc}
145         */
146        @Override
147        public void mouseReleased(MouseEvent e) {
148            validPress = !SwingUtilities.isLeftMouseButton(e);
149            window.validate();
150            window.setCursor(null);
151        }
152
153        /**
154         * {@inheritDoc}
155         */
156        @Override
157        public void mouseDragged(MouseEvent e) {
158            if (validPress) {
159                Rectangle wb = window.getBounds();
160                Point p = e.getPoint();
161                SwingUtilities.convertPointToScreen(p, statusBar);
162                
163                wb.height += (p.y - startingPoint.y);
164                if (statusBar.getComponentOrientation().isLeftToRight()) {
165                    wb.width += (p.x - startingPoint.x);
166                } else {
167                    wb.x += (p.x - startingPoint.x);
168                    wb.width += (startingPoint.x - p.x);
169                }
170                
171                window.setBounds(wb);
172                window.validate();
173                startingPoint = p;
174            }
175        }
176
177        /**
178         * {@inheritDoc}
179         */
180        @Override
181        public void mouseMoved(MouseEvent e) {
182            if (isHandleAreaPoint(e.getPoint())) {
183                if (statusBar.getComponentOrientation().isLeftToRight()) {
184                    window.setCursor(Cursor.getPredefinedCursor(
185                            Cursor.SE_RESIZE_CURSOR));
186                } else {
187                    window.setCursor(Cursor.getPredefinedCursor(
188                            Cursor.SW_RESIZE_CURSOR));
189                }
190            } else {
191                window.setCursor(null);
192            }
193        }
194
195        /**
196         * {@inheritDoc}
197         */
198        @Override
199        public void propertyChange(PropertyChangeEvent evt) {
200            if ("ancestor".equals(evt.getPropertyName())) {
201                window = SwingUtilities.getWindowAncestor(statusBar);
202                
203                boolean useResizeHandle = statusBar.getParent() != null
204                        && statusBar.getRootPane() != null
205                        && (statusBar.getParent() == statusBar.getRootPane()
206                        || statusBar.getParent() == statusBar.getRootPane().getContentPane());
207                statusBar.setResizeHandleEnabled(useResizeHandle);
208            } else if ("border".equals(evt.getPropertyName())) {
209                handleBoundary = getHandleBoundary();
210            } else if ("componentOrientation".equals(evt.getPropertyName())) {
211                handleBoundary = getHandleBoundary();
212            } else if ("resizeHandleEnabled".equals(evt.getPropertyName())) {
213                //TODO disable handle display
214                handleBoundary = getHandleBoundary();
215            }
216        }
217    }
218    
219    public static final String AUTO_ADD_SEPARATOR = new StringBuffer("auto-add-separator").toString();
220    /**
221     * Used to help reduce the amount of trash being generated
222     */
223    private static Insets TEMP_INSETS;
224    /**
225     * The one and only JXStatusBar for this UI delegate
226     */
227    protected JXStatusBar statusBar;
228    
229    protected MouseListener mouseListener;
230    
231    protected MouseMotionListener mouseMotionListener;
232    
233    protected PropertyChangeListener propertyChangeListener;
234    
235    private Handler handler;
236    
237    /** Creates a new instance of BasicStatusBarUI */
238    public BasicStatusBarUI() {
239    }
240    
241    /**
242     * Returns an instance of the UI delegate for the specified component.
243     * Each subclass must provide its own static <code>createUI</code>
244     * method that returns an instance of that UI delegate subclass.
245     * If the UI delegate subclass is stateless, it may return an instance
246     * that is shared by multiple components.  If the UI delegate is
247     * stateful, then it should return a new instance per component.
248     * The default implementation of this method throws an error, as it
249     * should never be invoked.
250     */
251    public static ComponentUI createUI(JComponent c) {
252        return new BasicStatusBarUI();
253    }
254    
255    /**
256     * {@inheritDoc}
257     */
258    @Override
259    public void installUI(JComponent c) {
260        assert c instanceof JXStatusBar;
261        statusBar = (JXStatusBar)c;
262        
263        installDefaults(statusBar);
264        installListeners(statusBar);
265        
266        // only set the layout manager if the layout manager of the component is
267        // null or a UIResource. Do not replace custom layout managers.
268        LayoutManager m = statusBar.getLayout();
269        if (m == null || m instanceof UIResource) {
270            statusBar.setLayout(createLayout());
271        }
272    }
273    
274    protected void installDefaults(JXStatusBar sb) {
275        //only set the border if it is an instanceof UIResource
276        //In other words, only replace the border if it has not been
277        //set by the developer. UIResource is the flag we use to indicate whether
278        //the value was set by the UIDelegate, or by the developer.
279        Border b = statusBar.getBorder();
280        if (b == null || b instanceof UIResource) {
281            statusBar.setBorder(createBorder());
282        }
283        
284        LookAndFeel.installProperty(sb, "opaque", Boolean.TRUE);
285    }
286    
287    private Handler getHandler() {
288        if (handler == null) {
289            handler = new Handler();
290        }
291        
292        return handler;
293    }
294    
295    /**
296     * Creates a {@code MouseListener} which will be added to the 
297     * status bar. If this method returns null then it will not 
298     * be added to the status bar.
299     * <p>
300     * Subclasses may override this method to return instances of their own
301     * MouseEvent handlers.
302     *
303     * @return an instance of a {@code MouseListener} or null
304     */
305    protected MouseListener createMouseListener() {
306        return getHandler();
307    }
308    
309    /**
310     * Creates a {@code MouseMotionListener} which will be added to the 
311     * status bar. If this method returns null then it will not 
312     * be added to the status bar.
313     * <p>
314     * Subclasses may override this method to return instances of their own
315     * MouseEvent handlers.
316     *
317     * @return an instance of a {@code MouseMotionListener} or null
318     */
319    protected MouseMotionListener createMouseMotionListener() {
320        return getHandler();
321    }
322    
323    /**
324     * Creates a {@code PropertyChangeListener} which will be added to the 
325     * status bar. If this method returns null then it will not 
326     * be added to the status bar.
327     * <p>
328     * Subclasses may override this method to return instances of their own
329     * PropertyChangeEvent handlers.
330     *
331     * @return an instance of a {@code PropertyChangeListener} or null
332     */
333    protected PropertyChangeListener createPropertyChangeListener() {
334        return getHandler();
335    }
336    
337    /**
338     * Create and install the listeners for the status bar.
339     * This method is called when the UI is installed.
340     */
341    protected void installListeners(JXStatusBar sb) {
342        if ((mouseListener = createMouseListener()) != null) {
343            statusBar.addMouseListener(mouseListener);
344        }
345        
346        if ((mouseMotionListener = createMouseMotionListener()) != null) {
347            statusBar.addMouseMotionListener(mouseMotionListener);
348        }
349        
350        if ((propertyChangeListener = createPropertyChangeListener()) != null) {
351            statusBar.addPropertyChangeListener(propertyChangeListener);
352        }
353    }
354    
355    /**
356     * {@inheritDoc}
357     */
358    @Override
359    public void uninstallUI(JComponent c) {
360        assert c instanceof JXStatusBar;
361
362        uninstallDefaults(statusBar);
363        uninstallListeners(statusBar);
364        
365        if (statusBar.getLayout() instanceof UIResource) {
366            statusBar.setLayout(null);
367        }
368    }
369    
370    protected void uninstallDefaults(JXStatusBar sb) {
371        if (sb.getBorder() instanceof UIResource) {
372            sb.setBorder(null);
373        }
374    }
375    
376    /**
377     * Remove the installed listeners from the status bar.
378     * The number and types of listeners removed in this method should be
379     * the same that were added in <code>installListeners</code>
380     */
381    protected void uninstallListeners(JXStatusBar sb) {
382        if (mouseListener != null) {
383            statusBar.removeMouseListener(mouseListener);
384        }
385        
386        if (mouseMotionListener != null) {
387            statusBar.removeMouseMotionListener(mouseMotionListener);
388        }
389        
390        if (propertyChangeListener != null) {
391            statusBar.removePropertyChangeListener(propertyChangeListener);
392        }
393    }
394    
395    @Override
396    public void paint(Graphics g, JComponent c) {
397        //paint the background if opaque
398        if (statusBar.isOpaque()) {
399            Graphics2D g2 = (Graphics2D)g;
400            paintBackground(g2, statusBar);
401        }
402        
403        if (includeSeparators()) {
404            //now paint the separators
405            TEMP_INSETS = getSeparatorInsets(TEMP_INSETS);
406            for (int i=0; i<statusBar.getComponentCount()-1; i++) {
407                Component comp = statusBar.getComponent(i);
408                int x = comp.getX() + comp.getWidth() + TEMP_INSETS.left;
409                int y = TEMP_INSETS.top;
410                int w = getSeparatorWidth() - TEMP_INSETS.left - TEMP_INSETS.right;
411                int h = c.getHeight() - TEMP_INSETS.top - TEMP_INSETS.bottom;
412
413                paintSeparator((Graphics2D)g, statusBar, x, y, w, h);
414            }
415        }
416    }
417    
418    //----------------------------------------------------- Extension Points
419    protected void paintBackground(Graphics2D g, JXStatusBar bar) {
420        if (bar.isOpaque()) {
421            g.setColor(bar.getBackground());
422            g.fillRect(0, 0, bar.getWidth(), bar.getHeight());
423        }
424    }
425    
426    protected void paintSeparator(Graphics2D g, JXStatusBar bar, int x, int y, int w, int h) {
427        Color fg = UIManagerExt.getSafeColor("Separator.foreground", Color.BLACK);
428        Color bg = UIManagerExt.getSafeColor("Separator.background", Color.WHITE);
429        
430        x += w / 2;
431        g.setColor(fg);
432        g.drawLine(x, y, x, h);
433        
434        g.setColor(bg);
435        g.drawLine(x+1, y, x+1, h);
436    }
437    
438    protected Insets getSeparatorInsets(Insets insets) {
439        if (insets == null) {
440            insets = new Insets(0, 0, 0, 0);
441        }
442        
443        insets.top = 4;
444        insets.left = 4;
445        insets.bottom = 2;
446        insets.right = 4;
447        
448        return insets;
449    }
450    
451    protected int getSeparatorWidth() {
452        return 10;
453    }
454    
455    protected boolean includeSeparators() {
456        Boolean b = (Boolean)statusBar.getClientProperty(AUTO_ADD_SEPARATOR);
457        return b == null || b;
458    }
459    
460    protected BorderUIResource createBorder() {
461        return new BorderUIResource(BorderFactory.createEmptyBorder(4, 5, 4, 22));
462    }
463    
464    protected LayoutManager createLayout() {
465        //This is in the UI delegate because the layout
466        //manager takes into account spacing for the separators between components
467        return new LayoutManager2() {
468            private Map<Component,Constraint> constraints = new HashMap<Component,Constraint>();
469            
470            @Override
471            public void addLayoutComponent(String name, Component comp) {addLayoutComponent(comp, null);}
472            @Override
473            public void removeLayoutComponent(Component comp) {constraints.remove(comp);}
474            @Override
475            public Dimension minimumLayoutSize(Container parent) {return preferredLayoutSize(parent);}
476            @Override
477            public Dimension maximumLayoutSize(Container target) {return new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE);}
478            @Override
479            public float getLayoutAlignmentX(Container target) {return .5f;}
480            @Override
481            public float getLayoutAlignmentY(Container target) {return .5f;}
482            @Override
483            public void invalidateLayout(Container target) {}
484            
485            @Override
486            public void addLayoutComponent(Component comp, Object constraint) {
487                //we accept an Insets, a ResizeBehavior, or a Constraint.
488                if (constraint instanceof Insets) {
489                    constraint = new Constraint((Insets)constraint);
490                } else if (constraint instanceof Constraint.ResizeBehavior) {
491                    constraint = new Constraint((Constraint.ResizeBehavior)constraint);
492                }
493                
494                constraints.put(comp, (Constraint)constraint);
495            }
496            
497            @Override
498            public Dimension preferredLayoutSize(Container parent) {
499                Dimension prefSize = new Dimension();
500                int count = 0;
501                for (Component comp : constraints.keySet()) {
502                    Constraint c = constraints.get(comp);
503                    Dimension d = comp.getPreferredSize();
504                    int prefWidth = 0;
505                    if (c != null) {
506                        Insets i = c.getInsets();
507                        d.width += i.left + i.right;
508                        d.height += i.top + i.bottom;
509                        prefWidth = c.getFixedWidth();
510                    }
511                    prefSize.height = Math.max(prefSize.height, d.height);
512                    prefSize.width += Math.max(d.width, prefWidth);
513                    
514                    //If this is not the last component, add extra space between each
515                    //component (for the separator).
516                    count++;
517                    if (includeSeparators() && constraints.size() < count) {
518                        prefSize.width += getSeparatorWidth();
519                    }
520                }
521                
522                Insets insets = parent.getInsets();
523                prefSize.height += insets.top + insets.bottom;
524                prefSize.width += insets.left + insets.right;
525                return prefSize;
526            }
527            
528            @Override
529            public void layoutContainer(Container parent) {
530                /*
531                 * Layout algorithm:
532                 *      If the parent width is less than the sum of the preferred
533                 *      widths of the components (including separators), where
534                 *      preferred width means either the component preferred width + 
535                 *      constraint insets, or fixed width + constraint insets, then
536                 *      simply layout the container from left to right and let the
537                 *      right hand components flow off the parent.
538                 *
539                 *      Otherwise, lay out each component according to its preferred
540                 *      width except for components with a FILL constraint. For these,
541                 *      resize them evenly for each FILL constraint.
542                 */
543                
544                //the insets of the parent component.
545                Insets parentInsets = parent.getInsets();
546                //the available width for putting components.
547                int availableWidth = parent.getWidth() - parentInsets.left - parentInsets.right;
548                if (includeSeparators()) {
549                    //remove from availableWidth the amount of space the separators will take
550                    availableWidth -= (parent.getComponentCount() - 1) * getSeparatorWidth();
551                }
552                
553                //the preferred widths of all of the components -- where preferred
554                //width mean the preferred width after calculating fixed widths and
555                //constraint insets
556                int[] preferredWidths = new int[parent.getComponentCount()];
557                int sumPreferredWidths = 0;
558                for (int i=0; i<preferredWidths.length; i++) {
559                    preferredWidths[i] = getPreferredWidth(parent.getComponent(i));
560                    sumPreferredWidths += preferredWidths[i];
561                }
562                
563                //if the availableWidth is greater than the sum of preferred
564                //sizes, then adjust the preferred width of each component that
565                //has a FILL constraint, to evenly use up the extra space.
566                if (availableWidth > sumPreferredWidths) {
567                    //the number of components with a fill constraint
568                    int numFilledComponents = 0;
569                    for (Component comp : parent.getComponents()) {
570                        Constraint c = constraints.get(comp);
571                        if (c != null && c.getResizeBehavior() == Constraint.ResizeBehavior.FILL) {
572                            numFilledComponents++;
573                        }
574                    }
575                    
576                    if (numFilledComponents > 0) {
577                        //calculate the share of free space each FILL component will take
578                        availableWidth -= sumPreferredWidths;
579                        double weight = 1.0 / (double)numFilledComponents;
580                        int share = (int)(availableWidth * weight);
581                        int remaining = numFilledComponents;
582                        for (int i=0; i<parent.getComponentCount(); i++) {
583                            Component comp = parent.getComponent(i);
584                            Constraint c = constraints.get(comp);
585                            if (c != null && c.getResizeBehavior() == Constraint.ResizeBehavior.FILL) {
586                                if (remaining > 1) {
587                                    preferredWidths[i] += share;
588                                    availableWidth -= share;
589                                } else {
590                                    preferredWidths[i] += availableWidth;
591                                }
592                                remaining--;
593                            }
594                        }
595                    }
596                }
597                
598                //now lay out the components
599                int nextX = parentInsets.left;
600                int height = parent.getHeight() - parentInsets.top - parentInsets.bottom;
601                for (int i=0; i<parent.getComponentCount(); i++) {
602                    Component comp = parent.getComponent(i);
603                    Constraint c = constraints.get(comp);
604                    Insets insets = c == null ? new Insets(0,0,0,0) : c.getInsets();
605                    int width = preferredWidths[i] - (insets.left + insets.right);
606                    int x = nextX + insets.left;
607                    int y = parentInsets.top + insets.top;
608                    comp.setSize(width, height);
609                    comp.setLocation(x, y);
610                    nextX = x + width + insets.right;
611                    //If this is not the last component, add extra space
612                    //for the separator
613                    if (includeSeparators() && i < parent.getComponentCount() - 1) {
614                        nextX += getSeparatorWidth();
615                    }
616                }
617            }
618            
619            /**
620             * @return the "preferred" width, where that means either 
621             *         comp.getPreferredSize().width + constraintInsets, or
622             *         constraint.fixedWidth + constraintInsets.
623             */
624            private int getPreferredWidth(Component comp) {
625                Constraint c = constraints.get(comp);
626                if (c == null) {
627                    return comp.getPreferredSize().width;
628                } else {
629                    Insets insets = c.getInsets();
630                    assert insets != null;
631                    if (c.getFixedWidth() <= 0) {
632                        return comp.getPreferredSize().width + insets.left + insets.right;
633                    } else {
634                        return c.getFixedWidth() + insets.left + insets.right;
635                    }
636                }
637            }
638            
639        };
640    }
641}