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

com.alee.painter.AbstractPainter Maven / Gradle / Ivy

There is a newer version: 1.2.14
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;

import com.alee.api.jdk.Objects;
import com.alee.laf.WebLookAndFeel;
import com.alee.managers.style.Bounds;
import com.alee.utils.*;
import com.alee.utils.laf.WebBorder;

import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.plaf.ComponentUI;
import java.awt.*;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * This abstract {@link Painter} implementation provides a few basic commonly used features.
 * You might want to extended this class instead of implementing {@link Painter} interface directly.
 *
 * @param  component type
 * @param  component UI type
 * @author Mikle Garin
 * @author Alexandr Zernov
 * @see Painter
 */
public abstract class AbstractPainter implements Painter
{
    /**
     * Listeners.
     */
    protected transient PropertyChangeListener propertyChangeListener;

    /**
     * Whether or not this painter is installed onto some component.
     */
    protected transient boolean installed;

    /**
     * Component reference.
     */
    protected transient C component;

    /**
     * Component UI reference.
     */
    protected transient U ui;

    /**
     * Installed section painters.
     */
    protected transient Map> sectionPainters;

    /**
     * Whether or not painted component has LTR orientation.
     */
    protected transient boolean ltr;

    @Override
    public void install ( final C c, final U ui )
    {
        // Event Dispatch Thread checkers
        WebLookAndFeel.installEventDispatchThreadCheckers ( c );

        // Saving references
        this.component = c;
        this.ui = ui;

        // Additional actions before installation
        beforeInstall ();

        // Installing section painters
        // This must always be done first, before we try installing any listeners
        installSectionPainters ();

        // Installing properties and listeners
        installPropertiesAndListeners ();

        // Additional actions after installation
        afterInstall ();
    }

    @Override
    public void uninstall ( final C c, final U ui )
    {
        // Event Dispatch Thread checkers
        WebLookAndFeel.uninstallEventDispatchThreadCheckers ( c );

        // Additional actions before uninstallation
        beforeUninstall ();

        // Uninstalling properties and listeners
        uninstallPropertiesAndListeners ();

        // Uninstalling section painters
        // This must always be done last, after we uninstall all listeners
        uninstallSectionPainters ();

        // Additional actions after uninstallation
        afterUninstall ();

        // Cleaning up references
        this.component = null;
        this.ui = null;
    }

    /**
     * Performs additional actions before installation starts.
     * This is an additional method for override convenience.
     */
    protected void beforeInstall ()
    {
        /**
         * Updating installation mark.
         */
        this.installed = true;
    }

    /**
     * Performs additional actions after installation ends.
     * This is an additional method for override convenience.
     */
    protected void afterInstall ()
    {
        /**
         * Updating initial border.
         */
        updateBorder ();
    }

    /**
     * Performs additional actions before uninstallation starts.
     * This is an additional method for override convenience.
     */
    protected void beforeUninstall ()
    {
        /**
         * Do nothing by default.
         */
    }

    /**
     * Performs additional actions after uninstallation ends.
     * This is an additional method for override convenience.
     */
    protected void afterUninstall ()
    {
        /**
         * Updating installation mark.
         */
        this.installed = false;
    }

    @Override
    public boolean isInstalled ()
    {
        return installed;
    }

    @Override
    public Boolean isOpaque ()
    {
        return null;
    }

    /**
     * Returns whether or not this painter is allowed to update component settings and visual state.
     * By default it is determined by the painter type, for example any SectionPainter should avoid updating settings.
     *
     * @return {@code true} if this painter is allowed to update component settings and visual state, {@code false} otherwise
     */
    protected boolean isSettingsUpdateAllowed ()
    {
        // Event Dispatch Thread check
        WebLookAndFeel.checkEventDispatchThread ();

        // Actual updateability check
        return isInstalled () && !isSectionPainter ();
    }

    /**
     * Returns whether or not this is a section painter.
     * Some internal behaviors might vary depending on what this method returns.
     *
     * @return {@code true} if this is a section painter, {@code false} otherwise
     */
    protected boolean isSectionPainter ()
    {
        return this instanceof SectionPainter;
    }

    /**
     * Installs {@link SectionPainter}s used by this {@link Painter}.
     */
    protected final void installSectionPainters ()
    {
        final List> sectionPainters = getSectionPainters ();
        if ( CollectionUtils.notEmpty ( sectionPainters ) )
        {
            for ( final SectionPainter sectionPainter : sectionPainters )
            {
                installSectionPainter ( sectionPainter );
            }
        }
    }

    /**
     * Uninstalls {@link SectionPainter}s used by this {@link Painter}.
     */
    protected final void uninstallSectionPainters ()
    {
        final List> sectionPainters = getInstalledSectionPainters ();
        if ( CollectionUtils.notEmpty ( sectionPainters ) )
        {
            for ( final SectionPainter sectionPainter : sectionPainters )
            {
                uninstallSectionPainter ( sectionPainter );
            }
        }
    }

    /**
     * Returns {@link SectionPainter}s used by this painter or {@code null} if none are used.
     * Do not return any {@code null} {@link SectionPainter}s here, it will cause an exception.
     * You can use {@link #asList(SectionPainter[])} method to conveniently form a list filtering out {@link null}s.
     *
     * @return {@link SectionPainter}s used by this painter or {@code null} if none are used
     */
    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 {@link SectionPainter}s installed within this painter or {@code null} if none are installed.
     * This method is used for various internal update mechanisms involving {@link SectionPainter}s.
     *
     * @return {@link SectionPainter}s installed within this painter or {@code null} if none are installed
     */
    protected final List> getInstalledSectionPainters ()
    {
        return MapUtils.notEmpty ( sectionPainters ) ? new ArrayList> ( sectionPainters.values () ) : null;
    }

    /**
     * Installs {@link SectionPainter} onto this {@link Painter}.
     *
     * @param painter {@link SectionPainter} to install
     */
    protected final void installSectionPainter ( final SectionPainter painter )
    {
        // Ensure painter exists
        if ( painter == null )
        {
            throw new PainterException ( "Installed Painter cannot be null" );
        }

        // Initializing cache map
        if ( sectionPainters == null )
        {
            sectionPainters = new HashMap> ( 3 );
        }

        // Section identifier
        final String sectionId = painter.getSectionId ();

        // Uninstalling previous section painter under same section identifier
        final SectionPainter old = sectionPainters.get ( sectionId );
        if ( old != null )
        {
            old.uninstall ( component, ui, AbstractPainter.this );
        }

        // Installing new section painter
        painter.install ( component, ui, AbstractPainter.this );

        // Caching new section painter
        sectionPainters.put ( sectionId, painter );
    }

    /**
     * Uninstalls {@link SectionPainter} from this {@link Painter}.
     *
     * @param painter {@link SectionPainter} to uninstall
     */
    protected final void uninstallSectionPainter ( final SectionPainter painter )
    {
        // Ensure painter exists
        if ( painter == null )
        {
            throw new PainterException ( "Uninstalled Painter cannot be null" );
        }

        // Section identifier
        final String sectionId = painter.getSectionId ();

        // Removing section painter cache
        sectionPainters.remove ( sectionId );

        // Uninstalling section painter
        painter.uninstall ( component, ui, AbstractPainter.this );
    }

    /**
     * Paints {@link com.alee.painter.SectionPainter} at the specified bounds.
     * This method was introduced as one of the measures to fix #401 issue appearing on Linux systems.
     *
     * @param painter {@link com.alee.painter.SectionPainter}
     * @param g2d     graphics context
     * @param bounds  section bounds relative to component coordinates system
     */
    protected void paintSection ( final SectionPainter painter, final Graphics2D g2d, final Rectangle bounds )
    {
        if ( SystemUtils.isUnix () )
        {
            /**
             * This part of code is only here until #401 issue fix for Unix systems.
             * The problem with this workaround is that it provides bounds which are only relevant within paint run.
             */

            // Translating to section coordinates
            g2d.translate ( bounds.x, bounds.y );

            // Clipping area
            final Rectangle section = new Rectangle ( 0, 0, bounds.width, bounds.height );
            final Shape oc = GraphicsUtils.intersectClip ( g2d, section );

            // Creating appropriate bounds for painter
            final Bounds componentBounds = new Bounds ( component, -bounds.x, -bounds.y );
            final Bounds sectionBounds = new Bounds ( componentBounds, section );

            // Painting section
            painter.paint ( g2d, component, ui, sectionBounds );

            // Restoring old clip
            GraphicsUtils.restoreClip ( g2d, oc );

            // Translating back
            g2d.translate ( -bounds.x, -bounds.y );
        }
        else
        {
            // Clipping area
            final Shape oc = GraphicsUtils.intersectClip ( g2d, bounds );

            // Creating appropriate bounds for painter
            final Bounds componentBounds = new Bounds ( component );
            final Bounds sectionBounds = new Bounds ( componentBounds, bounds );

            // Painting section
            painter.paint ( g2d, component, ui, sectionBounds );

            // Restoring old clip
            GraphicsUtils.restoreClip ( g2d, oc );
        }
    }

    /**
     * Installs properties and listeners used by this {@link Painter} implementation.
     * Override this method instead of {@link #install(JComponent, ComponentUI)} to install additional properties and listeners.
     */
    protected void installPropertiesAndListeners ()
    {
        // Updating orientation
        updateOrientation ();
        saveOrientation ();

        // Install property change listener
        installPropertyChangeListener ();
    }

    /**
     * Uninstalls properties and listeners used by this {@link Painter} implementation.
     * Override this method instead of {@link #uninstall(JComponent, ComponentUI)} to uninstall additional properties and listeners.
     */
    protected void uninstallPropertiesAndListeners ()
    {
        // Uninstall property change listener
        uninstallPropertyChangeListener ();
    }

    /**
     * Installs listener that will inform about component property changes.
     */
    protected void installPropertyChangeListener ()
    {
        // Property change listener
        propertyChangeListener = new PropertyChangeListener ()
        {
            @Override
            public void propertyChange ( final PropertyChangeEvent evt )
            {
                // Event Dispatch Thread check
                WebLookAndFeel.checkEventDispatchThread ();

                // Ensure component is still available
                // This might happen if painter is replaced from another PropertyChangeListener
                if ( component != null )
                {
                    // Inform about property change event
                    AbstractPainter.this.propertyChanged ( evt.getPropertyName (), evt.getOldValue (), evt.getNewValue () );
                }
            }
        };
        component.addPropertyChangeListener ( propertyChangeListener );
    }

    /**
     * Informs about {@link #component} property change.
     *
     * @param property modified property
     * @param oldValue old property value
     * @param newValue new property value
     */
    protected void propertyChanged ( final String property, final Object oldValue, final Object newValue )
    {
        // Forcing orientation visual updates
        if ( Objects.equals ( property, WebLookAndFeel.COMPONENT_ORIENTATION_PROPERTY ) )
        {
            orientationChange ();
        }

        // Tracking component border changes
        if ( Objects.equals ( property, WebLookAndFeel.BORDER_PROPERTY ) )
        {
            borderChange ( ( Border ) newValue );
        }

        // Tracking component margin and padding changes
        if ( Objects.equals ( property, WebLookAndFeel.LAF_MARGIN_PROPERTY, WebLookAndFeel.LAF_PADDING_PROPERTY ) )
        {
            updateBorder ();
        }
    }

    /**
     * Uninstalls listener that is informing about component property changes.
     */
    protected void uninstallPropertyChangeListener ()
    {
        component.removePropertyChangeListener ( propertyChangeListener );
        propertyChangeListener = null;
    }

    /**
     * Performs various updates on orientation change.
     */
    protected void orientationChange ()
    {
        // Saving new orientation
        saveOrientation ();

        // Updating component view
        // Revalidate includes border update so we don't need to call it separately
        revalidate ();
        repaint ();
    }

    /**
     * Saves current component orientation state.
     */
    protected void saveOrientation ()
    {
        ltr = component.getComponentOrientation ().isLeftToRight ();
    }

    /**
     * Updates component orientation based on global orientation.
     */
    protected void updateOrientation ()
    {
        if ( isSettingsUpdateAllowed () )
        {
            SwingUtils.setOrientation ( component );
        }
    }

    /**
     * Performs various border-related operations.
     *
     * @param border new border
     */
    protected void borderChange ( final Border border )
    {
        /**
         * Managing {@link JComponent}'s client property to preserve customized component {@link Border}s.
         * Any {@link Border} that is not {@code null} and not a {@link javax.swing.plaf.UIResource} is considered to be custom.
         * This is necessary to prevent WebLaF from overwriting those custom borders.
         */
        final boolean oldHonor = SwingUtils.getHonorUserBorders ( component );
        final boolean newHonor = !SwingUtils.isUIResource ( border );
        if ( oldHonor != newHonor )
        {
            // Updating client property
            SwingUtils.setHonorUserBorders ( component, newHonor );
        }
        if ( !newHonor && !( border instanceof WebBorder ) )
        {
            // Restoring WebLaF's border
            updateBorder ();
        }
    }

    /**
     * Returns {@link Painter} border according to component's margin, padding and {@link Painter}'s borders.
     * It is used to update component's border within {@link #updateBorder()} and to calculated default preferred size.
     *
     * @return {@link Painter} border according to component's margin, padding and {@link Painter}'s borders
     */
    protected Insets getCompleteBorder ()
    {
        final Insets border;
        if ( component != null && !SwingUtils.isPreserveBorders ( component ) )
        {
            // Initializing empty border
            border = new Insets ( 0, 0, 0, 0 );

            // Adding margin size
            if ( !isSectionPainter () )
            {
                final Insets margin = PainterSupport.getMargin ( component, true );
                SwingUtils.increase ( border, margin );
            }

            // Adding painter border size
            final Insets borders = getBorder ();
            if ( borders != null )
            {
                border.top += borders.top;
                border.left += ltr ? borders.left : borders.right;
                border.bottom += borders.bottom;
                border.right += ltr ? borders.right : borders.left;
            }

            // Adding padding size
            if ( !isSectionPainter () )
            {
                final Insets padding = PainterSupport.getPadding ( component, true );
                SwingUtils.increase ( border, padding );
            }
        }
        else
        {
            // Null border to prevent updates
            border = null;
        }
        return border;
    }

    /**
     * Returns border required for the view provided by this {@link Painter} or {@code null} in case it is not needed.     *
     * This border should not include possible component margin and padding, but only border provided by painter.
     * This border is added to component's margin and padding in {@link #getCompleteBorder()} calculations.
     * This border should not take component orientation into account, painter will take care of it later.
     *
     * @return border required for the view provided by this {@link Painter} or {@code null} in case it is not needed
     */
    protected Insets getBorder ()
    {
        return null;
    }

    /**
     * Custom check is written to avoid {@link Component#inside(int, int)} deprecated method usage.
     * Default implementation simply checks that point is within component bounds.
     */
    @Override
    public boolean contains ( final C c, final U ui, final Bounds bounds, final int x, final int y )
    {
        return bounds.get ().contains ( x, y );
    }

    @Override
    public int getBaseline ( final C c, final U ui, final Bounds bounds )
    {
        return -1;
    }

    @Override
    public Component.BaselineResizeBehavior getBaselineResizeBehavior ( final C c, final U ui )
    {
        return Component.BaselineResizeBehavior.OTHER;
    }

    @Override
    public Dimension getPreferredSize ()
    {
        return SwingUtils.increase ( new Dimension ( 0, 0 ), getCompleteBorder () );
    }

    /**
     * Updates component with complete border.
     * This border takes painter borders and component margin and padding into account.
     */
    protected void updateBorder ()
    {
        if ( isSettingsUpdateAllowed () )
        {
            final Insets border = getCompleteBorder ();
            if ( border != null )
            {
                final Border old = component.getBorder ();
                if ( !( old instanceof WebBorder ) || Objects.notEquals ( ( ( WebBorder ) old ).getBorderInsets (), border ) )
                {
                    component.setBorder ( new WebBorder ( border ) );
                }
            }
        }
    }

    /**
     * Should be called when whole painter visual representation changes.
     */
    protected void repaint ()
    {
        repaint ( 0, 0, component.getWidth (), component.getHeight () );
    }

    /**
     * Should be called when part of painter visual representation changes.
     *
     * @param bounds part bounds
     */
    protected void repaint ( final Rectangle bounds )
    {
        repaint ( bounds.x, bounds.y, bounds.width, bounds.height );
    }

    /**
     * Should be called when part of painter visual representation changes.
     *
     * @param x      part bounds X coordinate
     * @param y      part bounds Y coordinate
     * @param width  part bounds width
     * @param height part bounds height
     */
    protected void repaint ( final int x, final int y, final int width, final int height )
    {
        if ( isSettingsUpdateAllowed () && component.isShowing () )
        {
            component.repaint ( x, y, width, height );
        }
    }

    /**
     * Should be called when painter size or border changes.
     */
    protected void revalidate ()
    {
        updateBorder ();
        if ( isSettingsUpdateAllowed () )
        {
            component.revalidate ();
        }
    }

    /**
     * Should be called when painter opacity changes.
     * todo Use this instead of the outer border updates?
     */
    protected void updateOpacity ()
    {
        if ( isSettingsUpdateAllowed () )
        {
            final Boolean opaque = isOpaque ();
            if ( opaque != null )
            {
                LookAndFeel.installProperty ( component, WebLookAndFeel.OPAQUE_PROPERTY, opaque ? Boolean.TRUE : Boolean.FALSE );
            }
        }
    }

    /**
     * Should be called when painter size, border and visual representation changes.
     * Makes sure that everything in the component view is up to date.
     */
    protected void updateAll ()
    {
        updateOpacity ();
        revalidate ();
        repaint ();
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy