001/*
002 * $Id: JXPanel.java 4158 2012-02-03 18:29:40Z kschaefe $
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;
023
024import java.awt.AlphaComposite;
025import java.awt.Color;
026import java.awt.Component;
027import java.awt.Composite;
028import java.awt.Dimension;
029import java.awt.Graphics;
030import java.awt.Graphics2D;
031import java.awt.LayoutManager;
032import java.awt.Rectangle;
033import java.awt.image.BufferedImage;
034import java.beans.PropertyChangeEvent;
035import java.beans.PropertyChangeListener;
036
037import javax.swing.JPanel;
038import javax.swing.RepaintManager;
039import javax.swing.Scrollable;
040import javax.swing.SwingConstants;
041
042import org.jdesktop.beans.JavaBean;
043import org.jdesktop.swingx.painter.AbstractPainter;
044import org.jdesktop.swingx.painter.Painter;
045import org.jdesktop.swingx.util.GraphicsUtilities;
046
047/**
048 * <p>
049 * An extended {@code JPanel} that provides additional features. First, the
050 * component is {@code Scrollable}, using reasonable defaults. Second, the
051 * component is alpha-channel enabled. This means that the {@code JXPanel} can
052 * be made fully or partially transparent. Finally, {@code JXPanel} has support
053 * for {@linkplain Painter painters}.
054 * </p>
055 * <p>
056 * A transparency example, this following code will show the black background of
057 * the parent:
058 * 
059 * <pre>
060 * JXPanel panel = new JXPanel();
061 * panel.add(new JButton(&quot;Push Me&quot;));
062 * panel.setAlpha(.5f);
063 * 
064 * container.setBackground(Color.BLACK);
065 * container.add(panel);
066 * </pre>
067 * 
068 * </p>
069 * <p>
070 * A painter example, this following code will show how to add a simple painter:
071 * 
072 * <pre>
073 * JXPanel panel = new JXPanel();
074 * panel.setBackgroundPainter(new PinstripePainter());
075 * </pre>
076 * 
077 * </p>
078 * 
079 * @author rbair
080 * @see Scrollable
081 * @see Painter
082 */
083@JavaBean
084@SuppressWarnings("nls")
085public class JXPanel extends JPanel implements AlphaPaintable, BackgroundPaintable, Scrollable {
086//    private boolean scrollableTracksViewportHeight = true;
087//    private boolean scrollableTracksViewportWidth = true;
088    
089    private ScrollableSizeHint scrollableWidthHint = ScrollableSizeHint.FIT;
090    private ScrollableSizeHint scrollableHeightHint = ScrollableSizeHint.FIT;
091    
092    /**
093     * The alpha level for this component.
094     */
095    private float alpha = 1.0f;
096    /**
097     * If the old alpha value was 1.0, I keep track of the opaque setting because
098     * a translucent component is not opaque, but I want to be able to restore
099     * opacity to its default setting if the alpha is 1.0. Honestly, I don't know
100     * if this is necessary or not, but it sounded good on paper :)
101     * <p>TODO: Check whether this variable is necessary or not</p>
102     */
103    private boolean oldOpaque;
104    /**
105     * Indicates whether this component should inherit its parent alpha value
106     */
107    private boolean inheritAlpha = true;
108    /**
109     * Specifies the Painter to use for painting the background of this panel.
110     * If no painter is specified, the normal painting routine for JPanel
111     * is called. Old behavior is also honored for the time being if no
112     * backgroundPainter is specified
113     */
114    @SuppressWarnings("rawtypes")
115    private Painter backgroundPainter;
116    
117    private boolean paintBorderInsets = true;
118
119    /**
120     * The listener installed on the current backgroundPainter, if any.
121     */
122    private PropertyChangeListener painterChangeListener;
123    
124    /**
125     * Creates a new <code>JXPanel</code> with a double buffer
126     * and a flow layout.
127     */
128    public JXPanel() {
129    }
130    
131    /**
132     * Creates a new <code>JXPanel</code> with <code>FlowLayout</code>
133     * and the specified buffering strategy.
134     * If <code>isDoubleBuffered</code> is true, the <code>JXPanel</code>
135     * will use a double buffer.
136     *
137     * @param isDoubleBuffered  a boolean, true for double-buffering, which
138     *        uses additional memory space to achieve fast, flicker-free 
139     *        updates
140     */
141    public JXPanel(boolean isDoubleBuffered) {
142        super(isDoubleBuffered);
143    }
144    
145    /**
146     * Create a new buffered JXPanel with the specified layout manager
147     *
148     * @param layout  the LayoutManager to use
149     */
150    public JXPanel(LayoutManager layout) {
151        super(layout);
152    }
153    
154    /**
155     * Creates a new JXPanel with the specified layout manager and buffering
156     * strategy.
157     *
158     * @param layout  the LayoutManager to use
159     * @param isDoubleBuffered  a boolean, true for double-buffering, which
160     *        uses additional memory space to achieve fast, flicker-free 
161     *        updates
162     */
163    public JXPanel(LayoutManager layout, boolean isDoubleBuffered) {
164        super(layout, isDoubleBuffered);
165    }
166    
167    /**
168     * {@inheritDoc}
169     */
170    @Override
171    public float getAlpha() {
172        return alpha;
173    }
174
175    /**
176     * {@inheritDoc}
177     */
178    @Override
179    public void setAlpha(float alpha) {
180        if (alpha < 0f || alpha > 1f) {
181            throw new IllegalArgumentException("invalid alpha value " + alpha);
182        }
183        
184        float oldValue = getAlpha();
185        this.alpha = alpha;
186        
187        if (getAlpha() < 1f) {
188            if (oldValue == 1) {
189                //it used to be 1, but now is not. Save the oldOpaque
190                oldOpaque = isOpaque();
191                setOpaque(false);
192            }
193            
194            installRepaintManager();
195        } else {
196            uninstallRepaintManager();
197            
198            //restore the oldOpaque if it was true (since opaque is false now)
199            if (oldOpaque) {
200                setOpaque(true);
201            }
202        }
203        
204        firePropertyChange("alpha", oldValue, getAlpha());
205        repaint();
206    }
207    
208    void installRepaintManager() {
209        RepaintManager manager = RepaintManager.currentManager(this);
210        RepaintManager trm = SwingXUtilities.getTranslucentRepaintManager(manager);
211        RepaintManager.setCurrentManager(trm);
212    }
213    
214    void uninstallRepaintManager() {
215        //TODO uninstall TranslucentRepaintManager when no more non-opaque JXPanel's exist
216    }
217    
218    /**
219     * {@inheritDoc}
220     */
221    @Override
222    public float getEffectiveAlpha() {
223        float a = getAlpha();
224        
225        if (isInheritAlpha()) {
226            for (Component c = getParent(); c != null; c = c.getParent()) {
227                if (c instanceof AlphaPaintable) {
228                    a = Math.min(((AlphaPaintable) c).getEffectiveAlpha(), a);
229                    break;
230                }
231            }
232        }
233        
234        return a;
235    }
236
237    /**
238     * {@inheritDoc}
239     */
240    @Override
241    public boolean isInheritAlpha() {
242        return inheritAlpha;
243    }
244
245    /**
246     * {@inheritDoc}
247     */
248    @Override
249    public void setInheritAlpha(boolean val) {
250        boolean oldValue = isInheritAlpha();
251        inheritAlpha = val;
252        firePropertyChange("inheritAlpha", oldValue, isInheritAlpha());
253    }
254    
255    /**
256     * Sets the horizontal sizing hint. The hint is used by the Scrollable implementation
257     * to service the getScrollableTracksWidth.
258     * 
259     * @param hint the horizontal sizing hint, must not be null
260     *   and must be vertical.
261     * 
262     * @throws IllegalArgumentException if track not horizontal 
263     * @throws NullPointerException if null
264     * 
265     * @see #setScrollableHeightHint(ScrollableSizeHint)
266     * @see ScrollableSizeHint
267     */
268    public final void setScrollableWidthHint(ScrollableSizeHint hint) {
269        if (!hint.isHorizontalCompatible()) throw 
270           new IllegalArgumentException("track must be horizontal, but was " + hint);
271        ScrollableSizeHint oldValue = getScrollableWidthHint();
272        if (oldValue == hint) return;
273        this.scrollableWidthHint = hint;
274        revalidate();
275        firePropertyChange("scrollableWidthHint", oldValue, getScrollableWidthHint());
276    }
277    
278    
279    /**
280     * Sets the vertical sizing hint. The hint is used by the Scrollable implementation
281     * to service the getScrollableTracksHeight.
282     * 
283     * @param hint the vertical sizing hint, must not be null
284     *   and must be vertical.
285     * 
286     * @throws IllegalArgumentException if track not vertical 
287     * @throws NullPointerException if null
288     * 
289     * @see #setScrollableWidthHint(ScrollableSizeHint)
290     * @see ScrollableSizeHint
291     */
292    public final void setScrollableHeightHint(ScrollableSizeHint hint) {
293        if (!hint.isVerticalCompatible()) throw 
294            new IllegalArgumentException("track must be vertical, but was " + hint);
295        ScrollableSizeHint oldValue = getScrollableHeightHint();
296        if (oldValue == hint) return;
297        this.scrollableHeightHint = hint;
298        revalidate();
299        firePropertyChange("scrollableHeightHint", oldValue, getScrollableHeightHint());
300    }
301    
302    protected ScrollableSizeHint getScrollableWidthHint() {
303        return scrollableWidthHint;
304    }
305    
306    protected ScrollableSizeHint getScrollableHeightHint() {
307        return scrollableHeightHint;
308        
309    }
310    
311    
312    /**
313     * {@inheritDoc}
314     */
315    @Override
316    public boolean getScrollableTracksViewportHeight() {
317        return scrollableHeightHint.getTracksParentSize(this, SwingConstants.VERTICAL);
318    }
319    
320    /**
321     * {@inheritDoc}
322     */
323    @Override
324    public boolean getScrollableTracksViewportWidth() {
325        return scrollableWidthHint.getTracksParentSize(this, SwingConstants.HORIZONTAL);
326    }
327    
328    /**
329     * {@inheritDoc}
330     */
331    @Override
332    public Dimension getPreferredScrollableViewportSize() {
333        return getPreferredSize();
334    }
335    
336    /**
337     * {@inheritDoc}
338     */
339    @Override
340    public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
341        if (orientation == SwingConstants.VERTICAL) {
342            return visibleRect.height;
343        } else if (orientation == SwingConstants.HORIZONTAL) {
344            return visibleRect.width;
345        } else {
346            throw new IllegalArgumentException("invalid orientation"); //$NON-NLS-1$
347        }
348    }
349    
350    /**
351     * {@inheritDoc}
352     */
353    @Override
354    public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
355        return getScrollableBlockIncrement(visibleRect, orientation, direction) / 10;
356    }
357    
358    /**
359     * 
360     * Sets the vertical size tracking to either ScrollableSizeTrack.FIT or NONE, if the
361     * boolean parameter is true or false, respectively.<p>
362     * 
363     * <b>NOTE</b>: this method is kept for backward compatibility only, for full 
364     * control use setScrollableHeightHint.
365     * 
366     * @param scrollableTracksViewportHeight The scrollableTracksViewportHeight to set.
367     * 
368     * @see #setScrollableHeightHint(ScrollableSizeHint)
369     */
370    public void setScrollableTracksViewportHeight(boolean scrollableTracksViewportHeight) {
371        setScrollableHeightHint(scrollableTracksViewportHeight ? 
372                ScrollableSizeHint.FIT : ScrollableSizeHint.NONE);
373    }
374    /**
375     * Sets the horizontal size tracking to either ScrollableSizeTrack.FIT or NONE, if the
376     * boolean parameter is true or false, respectively.<p>
377     * 
378     * <b>NOTE</b>: this method is kept for backward compatibility only, for full 
379     * control use setScrollableWidthHint.
380     * 
381     * 
382     * @param scrollableTracksViewportWidth The scrollableTracksViewportWidth to set.
383     * 
384     * @see #setScrollableWidthHint(ScrollableSizeHint)
385     */
386    public void setScrollableTracksViewportWidth(boolean scrollableTracksViewportWidth) {
387        setScrollableWidthHint(scrollableTracksViewportWidth ? 
388                ScrollableSizeHint.FIT : ScrollableSizeHint.NONE);
389    }
390
391    /**
392     * Sets the background color for this component by
393     * 
394     * @param bg
395     *            the desired background <code>Color</code>
396     * @see javax.swing.JComponent#getBackground()
397     * @see #setOpaque(boolean)
398     * 
399    * @beaninfo
400    *    preferred: true
401    *        bound: true
402    *    attribute: visualUpdate true
403    *  description: The background color of the component.
404     */
405    @Override
406    public void setBackground(Color bg) {
407        super.setBackground(bg);
408        
409        SwingXUtilities.installBackground(this, bg);
410    }
411    
412    /**
413     * Sets a Painter to use to paint the background of this JXPanel.
414     * 
415     * @param p the new painter
416     * @see #getBackgroundPainter()
417     */
418    @Override
419    public void setBackgroundPainter(Painter p) {
420        Painter old = getBackgroundPainter();
421        if (old instanceof AbstractPainter) {
422            ((AbstractPainter<?>) old).removePropertyChangeListener(painterChangeListener);
423        }
424        backgroundPainter = p;
425        if (backgroundPainter instanceof AbstractPainter) {
426            ((AbstractPainter<?>) backgroundPainter).addPropertyChangeListener(getPainterChangeListener());
427        }
428        firePropertyChange("backgroundPainter", old, getBackgroundPainter());
429        repaint();
430    }
431    
432    /**
433     * @return a listener for painter change events
434     */
435    protected PropertyChangeListener getPainterChangeListener() {
436        if (painterChangeListener == null) {
437            painterChangeListener = new PropertyChangeListener() {
438                
439                @Override
440                public void propertyChange(PropertyChangeEvent evt) {
441                    repaint();
442                }
443            };
444        }
445        return painterChangeListener;
446    }
447
448    /**
449     * Returns the current background painter. The default value of this property 
450     * is a painter which draws the normal JPanel background according to the current look and feel.
451     * @return the current painter
452     * @see #setBackgroundPainter(Painter)
453     * @see #isPaintBorderInsets()
454     */
455    @Override
456    public Painter getBackgroundPainter() {
457        return backgroundPainter;
458    }
459    
460    /**
461     * Returns true if the background painter should paint where the border is
462     * or false if it should only paint inside the border. This property is 
463     * true by default. This property affects the width, height,
464     * and initial transform passed to the background painter.
465     */
466    @Override
467    public boolean isPaintBorderInsets() {
468        return paintBorderInsets;
469    }
470    
471    /**
472     * Sets the paintBorderInsets property.
473     * Set to true if the background painter should paint where the border is
474     * or false if it should only paint inside the border. This property is true by default.
475     * This property affects the width, height,
476     * and initial transform passed to the background painter.
477     * 
478     * This is a bound property.
479     */
480    @Override
481    public void setPaintBorderInsets(boolean paintBorderInsets) {
482        boolean old = this.isPaintBorderInsets();
483        this.paintBorderInsets = paintBorderInsets;
484        firePropertyChange("paintBorderInsets", old, isPaintBorderInsets());
485    }
486
487    /**
488     * Overridden paint method to take into account the alpha setting.
489     * 
490     * @param g
491     *            the <code>Graphics</code> context in which to paint
492     */
493    @Override
494    public void paint(Graphics g) {
495        //short circuit painting if no transparency
496        if (getAlpha() == 1f) {
497            super.paint(g);
498        } else {
499            //the component is translucent, so we need to render to
500            //an intermediate image before painting
501            // TODO should we cache this image? repaint to same image unless size changes?
502            BufferedImage img = GraphicsUtilities.createCompatibleTranslucentImage(getWidth(), getHeight());
503            Graphics2D gfx = img.createGraphics();
504            
505            try {
506                super.paint(gfx);
507            } finally {
508                gfx.dispose();
509            }
510            
511            Graphics2D g2d = (Graphics2D) g;
512            Composite oldComp = g2d.getComposite();
513            
514            try {
515                Composite alphaComp = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, getEffectiveAlpha());
516                g2d.setComposite(alphaComp);
517                //TODO should we cache the image?
518                g2d.drawImage(img, null, 0, 0);
519            } finally {
520                g2d.setComposite(oldComp);
521            }
522        }
523    }
524    
525    /**
526     * Overridden to provide Painter support. It will call backgroundPainter.paint()
527     * if it is not null, else it will call super.paintComponent().
528     * 
529     * @param g
530     *            the <code>Graphics</code> context in which to paint
531     */
532    @Override
533    protected void paintComponent(Graphics g) {
534        if (backgroundPainter == null) {
535            super.paintComponent(g);
536        } else {
537            if (isOpaque()) {
538                super.paintComponent(g);
539            }
540            
541            Graphics2D g2 = (Graphics2D) g.create();
542            
543            try {
544                SwingXUtilities.paintBackground(this, g2);
545            } finally {
546                g2.dispose();
547            }
548            
549            getUI().paint(g, this);
550        }
551    }
552}