All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.alee.painter.decoration.AbstractDecorationPainter Maven / Gradle / Ivy

Go to download

WebLaf is a Java Swing Look and Feel and extended components library for cross-platform applications

There is a newer version: 2.2.1
Show newest version
/*
 * This file is part of WebLookAndFeel library.
 *
 * WebLookAndFeel library is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * WebLookAndFeel library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with WebLookAndFeel library.  If not, see .
 */

package com.alee.painter.decoration;

import com.alee.laf.WebLookAndFeel;
import com.alee.laf.grouping.GroupingLayout;
import com.alee.managers.focus.DefaultFocusTracker;
import com.alee.managers.focus.FocusManager;
import com.alee.managers.focus.FocusTracker;
import com.alee.managers.style.Bounds;
import com.alee.managers.style.PainterShapeProvider;
import com.alee.painter.AbstractPainter;
import com.alee.painter.SectionPainter;
import com.alee.utils.*;
import com.alee.utils.swing.AbstractHoverBehavior;

import javax.swing.*;
import javax.swing.plaf.ComponentUI;
import java.awt.*;
import java.awt.event.ContainerEvent;
import java.awt.event.ContainerListener;
import java.awt.event.HierarchyEvent;
import java.awt.event.HierarchyListener;
import java.util.*;
import java.util.List;

/**
 * Abstract decoration painter that can be used by any custom and specific painter.
 *
 * @param  component type
 * @param  component UI type
 * @param  decoration type
 * @author Mikle Garin
 */

public abstract class AbstractDecorationPainter>
        extends AbstractPainter implements PainterShapeProvider
{
    /**
     * Decoratable states property.
     */
    public static final String DECORATION_STATES_PROPERTY = "decorationStates";

    /**
     * Decoration states.
     */
    protected List decorations;

    /**
     * Listeners.
     */
    protected transient FocusTracker focusStateTracker;
    protected transient AbstractHoverBehavior hoverStateTracker;
    protected transient HierarchyListener hierarchyTracker;
    protected transient ContainerListener neighboursTracker;

    /**
     * Runtime variables.
     */
    protected transient List states;
    protected transient Map decorationCache;
    protected transient boolean focused = false;
    protected transient boolean hover = false;
    protected transient Container ancestor;

    @Override
    public void install ( final E c, final U ui )
    {
        super.install ( c, ui );

        // Determining initial decoration state
        this.states = collectDecorationStates ();

        // Installing listeners
        installFocusListener ();
        installHoverListener ();
        installHierarchyListener ();
    }

    @Override
    public void uninstall ( final E c, final U ui )
    {
        // Uninstalling listeners
        uninstallHierarchyListener ();
        uninstallHoverListener ();
        uninstallFocusListener ();

        // Cleaning up variables
        this.decorationCache = null;
        this.states = null;

        super.uninstall ( c, ui );
    }

    @Override
    protected void propertyChange ( final String property, final Object oldValue, final Object newValue )
    {
        // Perform basic actions on property changes
        super.propertyChange ( property, oldValue, newValue );

        // Updating decoration state
        if ( isSettingsUpdateAllowed () )
        {
            // Updating enabled state
            if ( CompareUtils.equals ( property, WebLookAndFeel.ENABLED_PROPERTY ) )
            {
                if ( usesState ( DecorationState.enabled ) || usesState ( DecorationState.disabled ) )
                {
                    updateDecorationState ();
                }
            }

            // Updating custom decoration states
            if ( CompareUtils.equals ( property, DECORATION_STATES_PROPERTY ) )
            {
                updateDecorationState ();
            }
        }
    }

    /**
     * Installs listener that will perform decoration updates on focus state change.
     */
    protected void installFocusListener ()
    {
        if ( usesState ( DecorationState.focused ) )
        {
            focusStateTracker = new DefaultFocusTracker ()
            {
                @Override
                public void focusChanged ( final boolean focused )
                {
                    // Updating focus state
                    AbstractDecorationPainter.this.focused = focused;

                    // Updating decoration
                    if ( isSettingsUpdateAllowed () )
                    {
                        updateDecorationState ();
                    }
                }
            };
            FocusManager.addFocusTracker ( component, focusStateTracker );
        }
    }

    /**
     * Uninstalls listener that performs decoration updates on focus state change.
     */
    protected void uninstallFocusListener ()
    {
        if ( focusStateTracker != null )
        {
            FocusManager.removeFocusTracker ( focusStateTracker );
            focusStateTracker = null;
        }
    }

    /**
     * Returns whether or not component has focus.
     *
     * @return true if component has focus, false otherwise
     */
    protected boolean isFocused ()
    {
        return focused;
    }

    /**
     * Installs listener that will perform decoration updates on hover state change.
     */
    protected void installHoverListener ()
    {
        if ( usesState ( DecorationState.hover ) )
        {
            hoverStateTracker = new AbstractHoverBehavior ( component, false )
            {
                @Override
                public void hoverChanged ( final boolean hover )
                {
                    // Updating hover state
                    AbstractDecorationPainter.this.hover = hover;

                    // Updating decoration
                    if ( isSettingsUpdateAllowed () )
                    {
                        updateDecorationState ();
                    }
                }
            };
            hoverStateTracker.install ();
        }
    }

    /**
     * Uninstalls listener that performs decoration updates on hover state change.
     */
    protected void uninstallHoverListener ()
    {
        if ( hoverStateTracker != null )
        {
            hoverStateTracker.uninstall ();
            hoverStateTracker = null;
        }
    }

    /**
     * Installs listener that will perform border updates on component hierarchy changes.
     * This is required to properly update decoration borders in case it was moved from or into container with grouping layout.
     * It also tracks neighbour components addition and removal to update this component border accordingly.
     */
    protected void installHierarchyListener ()
    {
        neighboursTracker = new ContainerListener ()
        {
            @Override
            public void componentAdded ( final ContainerEvent e )
            {
                // Updating border when a child was added nearby
                if ( ancestor != null && ancestor.getLayout () instanceof GroupingLayout && e.getChild () != component )
                {
                    updateBorder ();
                }
            }

            @Override
            public void componentRemoved ( final ContainerEvent e )
            {
                // Updating border when a child was removed nearby
                if ( ancestor != null && ancestor.getLayout () instanceof GroupingLayout && e.getChild () != component )
                {
                    updateBorder ();
                }
            }
        };
        hierarchyTracker = new HierarchyListener ()
        {
            @Override
            public void hierarchyChanged ( final HierarchyEvent e )
            {
                // Listening only for parent change event
                // It will inform us when parent container for this component changes
                // Ancestor listener is not really reliable because it might inform about consequent parent changes
                if ( ( e.getChangeFlags () & HierarchyEvent.PARENT_CHANGED ) == HierarchyEvent.PARENT_CHANGED )
                {
                    // If there was a previous container...
                    if ( ancestor != null )
                    {
                        // Stop tracking neighbours
                        ancestor.removeContainerListener ( neighboursTracker );
                    }

                    // Updating ancestor
                    ancestor = component.getParent ();

                    // If there is a new container...
                    if ( ancestor != null )
                    {
                        // Start tracking neighbours
                        ancestor.addContainerListener ( neighboursTracker );

                        // Updating border
                        updateBorder ();
                    }
                }
            }
        };
        component.addHierarchyListener ( hierarchyTracker );
    }

    /**
     * Uninstalls listener that performs border updates on component hierarchy changes.
     */
    protected void uninstallHierarchyListener ()
    {
        component.removeHierarchyListener ( hierarchyTracker );
        hierarchyTracker = null;
        if ( ancestor != null )
        {
            ancestor.removeContainerListener ( neighboursTracker );
            ancestor = null;
        }
        neighboursTracker = null;
    }

    /**
     * Returns whether or not component is in hover state.
     *
     * @return true if component is in hover state, false otherwise
     */
    protected boolean isHover ()
    {
        return hover;
    }

    /**
     * Returns section painters used within this painter.
     * Might also return {@code null} in case no section painters are used within this one.
     * This method is used for various internal update mechanisms involving section painters.
     *
     * @return section painters used within this painter
     */
    protected List> getSectionPainters ()
    {
        return null;
    }

    /**
     * Returns section painters list in a most optimal way.
     * Utility method for usage inside of classed extending this one.
     *
     * @param sections section painters, some or all of them can be {@code null}
     * @return section painters list in a most optimal way
     */
    protected final List> asList ( final SectionPainter... sections )
    {
        ArrayList> list = null;
        if ( sections != null )
        {
            for ( final SectionPainter section : sections )
            {
                if ( section != null )
                {
                    if ( list == null )
                    {
                        list = new ArrayList> ( sections.length );
                    }
                    list.add ( section );
                }
            }
        }
        return list;
    }

    /**
     * Returns whether or not component is in enabled state.
     *
     * @return true if component is in enabled state, false otherwise
     */
    protected boolean isEnabled ()
    {
        return component != null && component.isEnabled ();
    }

    /**
     * Returns properly sorted current component decoration states.
     *
     * @return properly sorted current component decoration states
     */
    protected final List collectDecorationStates ()
    {
        // Retrieving current decoration states
        final List states = getDecorationStates ();

        // Adding custom UI decoration states
        if ( ui instanceof Stateful )
        {
            final List uiStates = ( ( Stateful ) ui ).getStates ();
            if ( !CollectionUtils.isEmpty ( uiStates ) )
            {
                states.addAll ( uiStates );
            }
        }

        // Adding custom component decoration states
        if ( component instanceof Stateful )
        {
            final List componentStates = ( ( Stateful ) component ).getStates ();
            if ( !CollectionUtils.isEmpty ( componentStates ) )
            {
                states.addAll ( componentStates );
            }
        }

        // Sorting states to always keep the same order
        Collections.sort ( states );

        return states;
    }

    /**
     * Returns current component decoration states.
     *
     * @return current component decoration states
     */
    protected List getDecorationStates ()
    {
        final List states = new ArrayList ();
        states.add ( SystemUtils.getShortOsName () );
        states.add ( isEnabled () ? DecorationState.enabled : DecorationState.disabled );
        if ( isFocused () )
        {
            states.add ( DecorationState.focused );
        }
        if ( isHover () )
        {
            states.add ( DecorationState.hover );
        }
        return states;
    }

    /**
     * Returns whether component has decoration associated with specified state.
     *
     * @param state decoration state
     * @return true if component has decoration associated with specified state, false otherwise
     */
    protected boolean usesState ( final String state )
    {
        // Checking whether or not this painter uses this decoration state
        boolean usesState = usesState ( decorations, state );

        // Checking whether or not section painters used by this painter use it
        if ( !usesState )
        {
            final List> sectionPainters = getSectionPainters ();
            if ( !CollectionUtils.isEmpty ( sectionPainters ) )
            {
                for ( final SectionPainter section : sectionPainters )
                {
                    if ( section instanceof AbstractDecorationPainter )
                    {
                        if ( usesState ( ( ( AbstractDecorationPainter ) section ).decorations, state ) )
                        {
                            usesState = true;
                            break;
                        }
                    }
                }
            }
        }

        return usesState;
    }

    /**
     * Returns whether specified decorations are associated with specified state.
     *
     * @param decorations decorations
     * @param state       decoration state
     * @return true if specified decorations are associated with specified state, false otherwise
     */
    protected final boolean usesState ( final List decorations, final String state )
    {
        if ( !CollectionUtils.isEmpty ( decorations ) )
        {
            for ( final D decoration : decorations )
            {
                final List states = decoration.getStates ();
                if ( states != null && states.contains ( state ) )
                {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Returns decorations for the specified states.
     *
     * @param forStates decoration states to retrieve decoration for
     * @return decorations for the specified states
     */
    protected List getDecorations ( final List forStates )
    {
        if ( !CollectionUtils.isEmpty ( decorations ) )
        {
            final List d = new ArrayList ( 1 );
            for ( final D decoration : decorations )
            {
                final List states = decoration.getStates ();
                if ( !CollectionUtils.isEmpty ( states ) )
                {
                    boolean containsAll = true;
                    for ( final String state : states )
                    {
                        if ( CollectionUtils.isEmpty ( forStates ) || !forStates.contains ( state ) )
                        {
                            containsAll = false;
                            break;
                        }
                    }
                    if ( containsAll )
                    {
                        d.add ( decoration );
                    }
                }
                else
                {
                    d.add ( decoration );
                }
            }
            return d;
        }
        else
        {
            return null;
        }
    }

    /**
     * Returns decoration matching current states.
     * Decorations returned here are cached copies of the data presented in skins.
     * This was made to avoid corrupting inital data and to increase the decoration retrieval speed.
     *
     * @return decoration matching current states
     */
    protected D getDecoration ()
    {
        // Optimization for painter without decorations
        if ( !CollectionUtils.isEmpty ( decorations ) )
        {
            // Decoration key
            // States are properly sorted, so their order is always the same
            final String key = TextUtils.listToString ( states, "," );

            // Creating decorations cache
            if ( decorationCache == null )
            {
                decorationCache = new HashMap ( decorations.size () );
            }

            // Resolving decoration if it wasn't cached yet
            if ( !decorationCache.containsKey ( key ) )
            {
                // Retrieving all decorations fitting current states
                final List decorations = getDecorations ( states );

                // Resolving resulting decoration
                final D decoration;
                if ( CollectionUtils.isEmpty ( decorations ) )
                {
                    // No decoration for the states available
                    decoration = null;
                }
                else if ( decorations.size () == 1 )
                {
                    // Single existing decoration for the states
                    decoration = MergeUtils.clone ( decorations.get ( 0 ) );
                }
                else
                {
                    // Merging multiple decorations together
                    decoration = MergeUtils.clone ( decorations.get ( 0 ) );
                    for ( int i = 1; i < decorations.size (); i++ )
                    {
                        decoration.merge ( decorations.get ( i ) );
                    }
                }

                // Updating section mark
                if ( decoration != null )
                {
                    decoration.setSection ( isSectionPainter () );
                }

                // Caching resulting decoration
                decorationCache.put ( key, decoration );
            }

            return decorationCache.get ( key );
        }
        else
        {
            return null;
        }
    }

    /**
     * Performs current decoration state update.
     */
    protected void updateDecorationState ()
    {
        final List states = collectDecorationStates ();
        if ( !CollectionUtils.equals ( this.states, states ) )
        {
            // Saving new decoration states
            this.states = states;

            // Updating section painters decoration states
            // This is required to provide state changes into section painters used within this painter
            final List> sectionPainters = getSectionPainters ();
            if ( !CollectionUtils.isEmpty ( sectionPainters ) )
            {
                for ( final SectionPainter section : sectionPainters )
                {
                    if ( section instanceof AbstractDecorationPainter )
                    {
                        ( ( AbstractDecorationPainter ) section ).updateDecorationState ();
                    }
                }
            }

            // Updating state if allowed
            if ( isSettingsUpdateAllowed () )
            {
                // States debug message
                // System.out.println ( ReflectUtils.getClassName ( getClass () ) + ": " + TextUtils.listToString ( states, "," ) );

                // Updating component visual state
                revalidate ();
                repaint ();
            }
        }
    }

    /**
     * Returns whether or not decoration is available.
     *
     * @return true if decoration is available, false otherwise
     */
    public boolean isDecorated ()
    {
        final D decoration = getDecoration ();
        return decoration != null && decoration.isVisible ();
    }

    @Override
    public Insets getBorders ()
    {
        final IDecoration decoration = getDecoration ();
        return decoration != null ? decoration.getBorderInsets ( component ) : null;
    }

    @Override
    public Shape provideShape ( final E component, final Rectangle bounds )
    {
        final IDecoration decoration = getDecoration ();
        return decoration != null ? decoration.provideShape ( component, bounds ) : bounds;
    }

    @Override
    public Boolean isOpaque ()
    {
        // Calculates opacity state based on provided decorations
        // In case there is an active visible decoration component should not be opaque
        // This might be changed in future and moved into decoration to provide specific setting
        final IDecoration decoration = getDecoration ();
        return decoration != null ? isOpaqueDecorated () : isOpaqueUndecorated ();
    }

    /**
     * Returns opacity state for decorated component.
     * This is separated from base opacity state method to allow deep customization.
     *
     * @return opacity state for decorated component
     */
    protected Boolean isOpaqueDecorated ()
    {
        // Returns {@code false} to make component non-opaque when decoration is available
        // This is convenient because almost any custom decoration might have transparent spots in it
        // And if it has any it cannot be opaque or it will cause major painting glitches
        return false;
    }

    /**
     * Returns opacity state for undecorated component.
     * This is separated from base opacity state method to allow deep customization.
     *
     * @return opacity state for undecorated component
     */
    protected Boolean isOpaqueUndecorated ()
    {
        // Returns {@code null} to disable automatic opacity changes by default
        // You may still provide a non-null opacity in your own painter implementations
        return null;
    }

    @Override
    public void paint ( final Graphics2D g2d, final Rectangle bounds, final E c, final U ui )
    {
        final Rectangle b = adjustBounds ( bounds );

        // Paint simple background if opaque
        // Otherwise component might cause various visual glitches
        if ( isPlainBackgroundPaintAllowed ( c ) )
        {
            g2d.setPaint ( c.getBackground () );
            g2d.fill ( isSectionPainter () ? b : Bounds.component.of ( c, b ) );
        }

        // Painting current decoration state
        final D decoration = getDecoration ();
        if ( isDecorationPaintAllowed ( decoration ) )
        {
            decoration.paint ( g2d, isSectionPainter () ? Bounds.margin.of ( c, decoration, b ) : Bounds.margin.of ( c, b ), c );
        }

        // Painting content
        paintContent ( g2d, b, c, ui );
    }

    /**
     * Returns adjusted painting bounds.
     *
     * @param bounds painting bounds to adjust
     * @return adjusted painting bounds
     */
    protected Rectangle adjustBounds ( final Rectangle bounds )
    {
        return bounds;
    }

    /**
     * Paints content decorated by this painter.
     * todo This method should eventually be removed since all content will be painted by IContent implementations
     *
     * @param g2d    graphics context
     * @param bounds painting bounds
     * @param c      painted component
     * @param ui     painted component UI
     */
    protected void paintContent ( final Graphics2D g2d, final Rectangle bounds, final E c, final U ui )
    {
        // No content by default
    }

    /**
     * Returns whether or not painting plain component background is allowed.
     * Moved into separated method for convenient background painting blocking using additional conditions.
     * 

* By default this condition is limited to component being opaque. * When component is opaque we must fill every single pixel in its bounds with something to avoid issues. * * @param c component to paint background for * @return true if painting plain component background is allowed, false otherwise */ protected boolean isPlainBackgroundPaintAllowed ( final E c ) { return c.isOpaque (); } /** * Returns whether or not painting specified decoration is allowed. * Moved into separated method for convenient decorationg painting blocking using additional conditions. *

* By default this condition is limited to decoration existance and visibility. * * @param decoration decoration to be painted * @return true if painting specified decoration is allowed, false otherwise */ protected boolean isDecorationPaintAllowed ( final D decoration ) { return decoration != null && decoration.isVisible (); } @Override public Dimension getPreferredSize () { return SwingUtils.max ( getDecorationSize (), super.getPreferredSize () ); } /** * Returns preferred decoration size. * * @return preferred decoration size */ protected Dimension getDecorationSize () { final D decoration = getDecoration (); return decoration != null ? decoration.getPreferredSize () : null; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy