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

com.alee.laf.list.ListPainter 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
package com.alee.laf.list;

import com.alee.painter.DefaultPainter;
import com.alee.painter.PainterSupport;
import com.alee.painter.decoration.AbstractDecorationPainter;
import com.alee.painter.decoration.IDecoration;
import com.alee.utils.GeometryUtils;

import javax.swing.*;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import java.awt.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

/**
 * Basic painter for JList component.
 * It is used as WebListUI default painter.
 *
 * @param  component type
 * @param  component UI type
 * @param  decoration type
 * @author Alexandr Zernov
 */

public class ListPainter> extends AbstractDecorationPainter
        implements IListPainter
{
    /**
     * Hover list item decoration painter.
     */
    @DefaultPainter (ListItemPainter.class)
    protected IListItemPainter hoverPainter;

    /**
     * Selected list items decoration painter.
     */
    @DefaultPainter (ListItemPainter.class)
    protected IListItemPainter selectionPainter;

    /**
     * Listeners.
     */
    protected ListSelectionListener listSelectionListener;

    /**
     * Painting variables.
     */
    protected Integer layoutOrientation;
    protected CellRendererPane rendererPane;
    protected Integer listHeight = -1;
    protected Integer listWidth = -1;
    protected Integer columnCount;
    protected Integer preferredHeight;
    protected Integer rowsPerColumn;
    protected int cellWidth = -1;
    protected int cellHeight = -1;
    protected int[] cellHeights = null;

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

        // Properly installing section painters
        this.hoverPainter = PainterSupport.installSectionPainter ( this, hoverPainter, null, c, ui );
        this.selectionPainter = PainterSupport.installSectionPainter ( this, selectionPainter, null, c, ui );

        // Selection listener
        listSelectionListener = new ListSelectionListener ()
        {
            @Override
            public void valueChanged ( final ListSelectionEvent e )
            {
                // Optimized selection repaint
                repaintSelection ();
            }
        };
        component.addListSelectionListener ( listSelectionListener );
    }

    @Override
    public void uninstall ( final E c, final U ui )
    {
        // Removing listeners
        component.removeListSelectionListener ( listSelectionListener );
        listSelectionListener = null;

        // Clearing painting variables
        cellHeights = null;

        // Properly uninstalling section painters
        this.selectionPainter = PainterSupport.uninstallSectionPainter ( selectionPainter, c, ui );
        this.hoverPainter = PainterSupport.uninstallSectionPainter ( hoverPainter, c, ui );

        super.uninstall ( c, ui );
    }

    @Override
    public void prepareToPaint ( final Integer layoutOrientation, final Integer listHeight, final Integer listWidth,
                                 final Integer columnCount, final Integer rowsPerColumn, final Integer preferredHeight, final int cellWidth,
                                 final int cellHeight, final int[] cellHeights )
    {
        this.layoutOrientation = layoutOrientation;
        this.listHeight = listHeight;
        this.listWidth = listWidth;
        this.columnCount = columnCount;
        this.rowsPerColumn = rowsPerColumn;
        this.preferredHeight = preferredHeight;
        this.cellWidth = cellWidth;
        this.cellHeight = cellHeight;
        this.cellHeights = cellHeights;
    }

    @Override
    protected void paintContent ( final Graphics2D g2d, final Rectangle bounds, final E c, final U ui )
    {
        // Painting hover cell background
        paintHoverCellBackground ( g2d );

        // Painting selected cells background
        paintSelectedCellsBackground ( g2d );

        // Painting list
        paintList ( g2d );

        // Painting drop location
        paintDropLocation ( g2d );
    }

    /**
     * Paints existing list items.
     *
     * @param g2d graphics context
     */
    protected void paintList ( final Graphics2D g2d )
    {
        // Saving initial clip
        final Shape clip = g2d.getClip ();

        // Retrieving paint settings
        layoutOrientation = component.getLayoutOrientation ();
        rendererPane = ui.getCellRendererPane ();

        final ListCellRenderer renderer = component.getCellRenderer ();
        final ListModel dataModel = component.getModel ();
        final int size = dataModel.getSize ();
        if ( renderer != null && size > 0 )
        {
            // Determine how many columns we need to paint
            final Rectangle paintBounds = g2d.getClipBounds ();
            final int startColumn;
            final int endColumn;
            if ( ltr )
            {
                startColumn = convertLocationToColumn ( paintBounds.x, paintBounds.y );
                endColumn = convertLocationToColumn ( paintBounds.x + paintBounds.width, paintBounds.y );
            }
            else
            {
                startColumn = convertLocationToColumn ( paintBounds.x + paintBounds.width, paintBounds.y );
                endColumn = convertLocationToColumn ( paintBounds.x, paintBounds.y );
            }
            final int maxY = paintBounds.y + paintBounds.height;
            final int leadIndex = adjustIndex ( component.getLeadSelectionIndex (), component );
            final int rowIncrement = ( layoutOrientation == JList.HORIZONTAL_WRAP ) ? columnCount : 1;

            final ListSelectionModel selModel = component.getSelectionModel ();
            for ( int colCounter = startColumn; colCounter <= endColumn; colCounter++ )
            {
                // And then how many rows in this column
                int row = convertLocationToRowInColumn ( paintBounds.y, colCounter );
                final int rowCount = getRowCount ( colCounter );
                int index = getModelIndex ( colCounter, row );
                final Rectangle rowBounds = ui.getCellBounds ( component, index, index );
                if ( rowBounds == null )
                {
                    // Not valid, bail!
                    return;
                }
                while ( row < rowCount && rowBounds.y < maxY &&
                        index < size )
                {
                    rowBounds.height = getHeight ( colCounter, row );
                    g2d.setClip ( rowBounds.x, rowBounds.y, rowBounds.width, rowBounds.height );
                    g2d.clipRect ( paintBounds.x, paintBounds.y, paintBounds.width, paintBounds.height );
                    paintCell ( g2d, index, rowBounds, renderer, dataModel, selModel, leadIndex );
                    rowBounds.y += rowBounds.height;
                    index += rowIncrement;
                    row++;
                }
            }
        }

        // Empty out the renderer pane, allowing renderers to be gc'ed.
        rendererPane.removeAll ();
        rendererPane = null;

        // Restoring initial clip
        g2d.setClip ( clip );
    }

    /**
     * Returns the height of the cell at the passed in location.
     *
     * @param column list column
     * @param row    list row
     * @return height of the cell at the passed in location
     */
    protected int getHeight ( final int column, final int row )
    {
        if ( column < 0 || column > columnCount || row < 0 )
        {
            return -1;
        }
        if ( layoutOrientation != JList.VERTICAL )
        {
            return cellHeight;
        }
        if ( row >= component.getModel ().getSize () )
        {
            return -1;
        }
        return ( cellHeights == null ) ? cellHeight : row < cellHeights.length ? cellHeights[ row ] : -1;
    }

    /**
     * Returns the model index for the specified display location.
     * If {@code column} x {@code row} is beyond the length of the model, this will return the model size - 1.
     *
     * @param column list column
     * @param row    list row
     * @return model index for the specified display location
     */
    protected int getModelIndex ( final int column, final int row )
    {
        switch ( layoutOrientation )
        {
            case JList.VERTICAL_WRAP:
                return Math.min ( component.getModel ().getSize () - 1, rowsPerColumn * column + Math.min ( row, rowsPerColumn - 1 ) );
            case JList.HORIZONTAL_WRAP:
                return Math.min ( component.getModel ().getSize () - 1, row * columnCount + column );
            default:
                return row;
        }
    }

    /**
     * Returns the number of rows in the given column.
     *
     * @param column column index
     * @return number of rows in the given column
     */
    protected int getRowCount ( final int column )
    {
        if ( column < 0 || column >= columnCount )
        {
            return -1;
        }
        if ( layoutOrientation == JList.VERTICAL || ( column == 0 && columnCount == 1 ) )
        {
            return component.getModel ().getSize ();
        }
        if ( column >= columnCount )
        {
            return -1;
        }
        if ( layoutOrientation == JList.VERTICAL_WRAP )
        {
            if ( column < ( columnCount - 1 ) )
            {
                return rowsPerColumn;
            }
            return component.getModel ().getSize () - ( columnCount - 1 ) * rowsPerColumn;
        }
        // JList.HORIZONTAL_WRAP
        final int diff = columnCount - ( columnCount * rowsPerColumn - component.getModel ().getSize () );

        if ( column >= diff )
        {
            return Math.max ( 0, rowsPerColumn - 1 );
        }
        return rowsPerColumn;
    }

    /**
     * Returns the closest row that starts at the specified y-location in the passed in column.
     *
     * @param y      Y location
     * @param column column index
     * @return closest row that starts at the specified y-location in the passed in column
     */
    protected int convertLocationToRowInColumn ( final int y, final int column )
    {
        int x = 0;

        if ( layoutOrientation != JList.VERTICAL )
        {
            if ( ltr )
            {
                x = column * cellWidth;
            }
            else
            {
                x = component.getWidth () - ( column + 1 ) * cellWidth - component.getInsets ().right;
            }
        }
        return convertLocationToRow ( x, y, true );
    }

    /**
     * Returns the row at the location specified by X and Y coordinates.
     * If {@code closest} is {@code true} and the location doesn't exactly match a particular location, closest row will be returned.
     *
     * @param x       X location
     * @param y0      Y location
     * @param closest whether or not should try finding closest row if exact location doesn't match any
     * @return row at the location specified by X and Y coordinates
     */
    @SuppressWarnings ("UnusedParameters")
    protected int convertLocationToRow ( final int x, final int y0, final boolean closest )
    {
        final int size = component.getModel ().getSize ();
        if ( size <= 0 )
        {
            return -1;
        }

        final Insets insets = component.getInsets ();
        if ( cellHeights == null )
        {
            int row = ( cellHeight == 0 ) ? 0 : ( ( y0 - insets.top ) / cellHeight );
            if ( closest )
            {
                if ( row < 0 )
                {
                    row = 0;
                }
                else if ( row >= size )
                {
                    row = size - 1;
                }
            }
            return row;
        }
        else if ( size > cellHeights.length )
        {
            return -1;
        }
        else
        {
            int y = insets.top;
            int row = 0;
            if ( closest && y0 < y )
            {
                return 0;
            }

            int i;
            for ( i = 0; i < size; i++ )
            {
                if ( ( y0 >= y ) && ( y0 < y + cellHeights[ i ] ) )
                {
                    return row;
                }
                y += cellHeights[ i ];
                row += 1;
            }
            return i - 1;
        }
    }

    /**
     * Returns the column at the location specified by X and Y coordinates.
     *
     * @param x X location
     * @param y Y location
     * @return column at the location specified by X and Y coordinates
     */
    @SuppressWarnings ("UnusedParameters")
    protected int convertLocationToColumn ( final int x, final int y )
    {
        if ( cellWidth > 0 )
        {
            if ( layoutOrientation == JList.VERTICAL )
            {
                return 0;
            }
            final Insets insets = component.getInsets ();
            final int col;
            if ( ltr )
            {
                col = ( x - insets.left ) / cellWidth;
            }
            else
            {
                col = ( component.getWidth () - x - insets.right - 1 ) / cellWidth;
            }
            if ( col < 0 )
            {
                return 0;
            }
            else if ( col >= columnCount )
            {
                return columnCount - 1;
            }
            return col;
        }
        return 0;
    }

    /**
     * Returns corrected item index.
     *
     * @param index list item index
     * @param list  painted list
     * @return corrected item index
     */
    protected int adjustIndex ( final int index, final E list )
    {
        return index < list.getModel ().getSize () ? index : -1;
    }

    /**
     * Paints list drop location.
     *
     * @param g2d graphics context
     */
    @SuppressWarnings ("UnusedParameters")
    protected void paintDropLocation ( final Graphics2D g2d )
    {
        final JList.DropLocation loc = component.getDropLocation ();
        if ( loc == null || !loc.isInsert () )
        {
            //noinspection UnnecessaryReturnStatement
            return;
        }

        // todo check needs. Maybe move to the style
        //        final Color c = DefaultLookup.getColor ( list, this, "List.dropLineColor", null );
        //        if ( c != null )
        //        {
        //            g.setPaint ( c );
        //            final Rectangle rect = getDropLineRect ( loc );
        //            g.fillRect ( rect.x, rect.y, rect.width, rect.height );
        //        }
    }

    //    private Rectangle getDropLineRect ( final JList.DropLocation loc )
    //    {
    //        final int size = list.getModel ().getSize ();
    //
    //        if ( size == 0 )
    //        {
    //            final Insets insets = list.getInsets ();
    //            if ( layoutOrientation == JList.HORIZONTAL_WRAP )
    //            {
    //                if ( ltr )
    //                {
    //                    return new Rectangle ( insets.left, insets.top, DROP_LINE_THICKNESS, 20 );
    //                }
    //                else
    //                {
    //                    return new Rectangle ( list.getWidth () - DROP_LINE_THICKNESS - insets.right, insets.top, DROP_LINE_THICKNESS, 20 );
    //                }
    //            }
    //            else
    //            {
    //                return new Rectangle ( insets.left, insets.top, list.getWidth () - insets.left - insets.right, DROP_LINE_THICKNESS );
    //            }
    //        }
    //
    //        Rectangle rect = null;
    //        int index = loc.getIndex ();
    //        boolean decr = false;
    //
    //        if ( layoutOrientation == JList.HORIZONTAL_WRAP )
    //        {
    //            if ( index == size )
    //            {
    //                decr = true;
    //            }
    //            else if ( index != 0 && convertModelToRow ( index ) != convertModelToRow ( index - 1 ) )
    //            {
    //                final Rectangle prev = getCellBounds ( list, index - 1 );
    //                final Rectangle me = getCellBounds ( list, index );
    //                final Point p = loc.getDropPoint ();
    //
    //                if ( ltr )
    //                {
    //                    decr = Point2D.distance ( prev.x + prev.width, prev.y + ( int ) ( prev.height / 2.0 ), p.x, p.y ) <
    //                            Point2D.distance ( me.x, me.y + ( int ) ( me.height / 2.0 ), p.x, p.y );
    //                }
    //                else
    //                {
    //                    decr = Point2D.distance ( prev.x, prev.y + ( int ) ( prev.height / 2.0 ), p.x, p.y ) <
    //                            Point2D.distance ( me.x + me.width, me.y + ( int ) ( prev.height / 2.0 ), p.x, p.y );
    //                }
    //            }
    //
    //            if ( decr )
    //            {
    //                index--;
    //                rect = getCellBounds ( list, index );
    //                if ( ltr )
    //                {
    //                    rect.x += rect.width;
    //                }
    //                else
    //                {
    //                    rect.x -= DROP_LINE_THICKNESS;
    //                }
    //            }
    //            else
    //            {
    //                rect = getCellBounds ( list, index );
    //                if ( !ltr )
    //                {
    //                    rect.x += rect.width - DROP_LINE_THICKNESS;
    //                }
    //            }
    //
    //            if ( rect.x >= list.getWidth () )
    //            {
    //                rect.x = list.getWidth () - DROP_LINE_THICKNESS;
    //            }
    //            else if ( rect.x < 0 )
    //            {
    //                rect.x = 0;
    //            }
    //
    //            rect.width = DROP_LINE_THICKNESS;
    //        }
    //        else if ( layoutOrientation == JList.VERTICAL_WRAP )
    //        {
    //            if ( index == size )
    //            {
    //                index--;
    //                rect = getCellBounds ( list, index );
    //                rect.y += rect.height;
    //            }
    //            else if ( index != 0 && convertModelToColumn ( index ) != convertModelToColumn ( index - 1 ) )
    //            {
    //
    //                final Rectangle prev = getCellBounds ( list, index - 1 );
    //                final Rectangle me = getCellBounds ( list, index );
    //                final Point p = loc.getDropPoint ();
    //                if ( Point2D.distance ( prev.x + ( int ) ( prev.width / 2.0 ), prev.y + prev.height, p.x, p.y ) <
    //                        Point2D.distance ( me.x + ( int ) ( me.width / 2.0 ), me.y, p.x, p.y ) )
    //                {
    //                    index--;
    //                    rect = getCellBounds ( list, index );
    //                    rect.y += rect.height;
    //                }
    //                else
    //                {
    //                    rect = getCellBounds ( list, index );
    //                }
    //            }
    //            else
    //            {
    //                rect = getCellBounds ( list, index );
    //            }
    //
    //            if ( rect.y >= list.getHeight () )
    //            {
    //                rect.y = list.getHeight () - DROP_LINE_THICKNESS;
    //            }
    //
    //            rect.height = DROP_LINE_THICKNESS;
    //        }
    //        else
    //        {
    //            if ( index == size )
    //            {
    //                index--;
    //                rect = getCellBounds ( list, index );
    //                rect.y += rect.height;
    //            }
    //            else
    //            {
    //                rect = getCellBounds ( list, index );
    //            }
    //
    //            if ( rect.y >= list.getHeight () )
    //            {
    //                rect.y = list.getHeight () - DROP_LINE_THICKNESS;
    //            }
    //
    //            rect.height = DROP_LINE_THICKNESS;
    //        }
    //
    //        return rect;
    //    }

    //    /**
    //     * Returns the row that the model index index will be displayed in..
    //     */
    //    protected int convertModelToRow ( final int index )
    //    {
    //        final int size = list.getModel ().getSize ();
    //
    //        if ( ( index < 0 ) || ( index >= size ) )
    //        {
    //            return -1;
    //        }
    //
    //        if ( layoutOrientation != JList.VERTICAL && columnCount > 1 &&
    //                rowsPerColumn > 0 )
    //        {
    //            if ( layoutOrientation == JList.VERTICAL_WRAP )
    //            {
    //                return index % rowsPerColumn;
    //            }
    //            return index / columnCount;
    //        }
    //        return index;
    //    }

    //    /**
    //     * Returns the column that the model index index will be displayed in.
    //     */
    //    protected int convertModelToColumn ( final int index )
    //    {
    //        final int size = list.getModel ().getSize ();
    //
    //        if ( ( index < 0 ) || ( index >= size ) )
    //        {
    //            return -1;
    //        }
    //
    //        if ( layoutOrientation != JList.VERTICAL && rowsPerColumn > 0 &&
    //                columnCount > 1 )
    //        {
    //            if ( layoutOrientation == JList.VERTICAL_WRAP )
    //            {
    //                return index / rowsPerColumn;
    //            }
    //            return index % columnCount;
    //        }
    //        return 0;
    //    }
    //
    //    /**
    //     * Gets the bounds of the specified model index, returning the resulting
    //     * bounds, or null if index is not valid.
    //     */
    //    protected Rectangle getCellBounds ( final JList list, final int index )
    //    {
    //        return ui.getCellBounds ( list, index, index );
    //    }

    /**
     * Paint one List cell: compute the relevant state, get the "rubber stamp" cell renderer component, and then use the CellRendererPane
     * to paint it. Subclasses may want to override this method rather than paint().
     *
     * @param g2d          graphics context
     * @param index        cell index
     * @param rowBounds    cell bounds
     * @param cellRenderer cell renderer
     * @param dataModel    list model
     * @param selModel     list selection model
     * @param leadIndex    lead cell index
     * @see #paint
     */
    protected void paintCell ( final Graphics2D g2d, final int index, final Rectangle rowBounds, final ListCellRenderer cellRenderer,
                               final ListModel dataModel, final ListSelectionModel selModel, final int leadIndex )
    {
        final Object value = dataModel.getElementAt ( index );
        final boolean isSelected = selModel.isSelectedIndex ( index );
        final boolean cellHasFocus = component.hasFocus () && ( index == leadIndex );
        final Component renderer = cellRenderer.getListCellRendererComponent ( component, value, index, isSelected, cellHasFocus );
        rendererPane.paintComponent ( g2d, renderer, component, rowBounds.x, rowBounds.y, rowBounds.width, rowBounds.height, true );
    }

    @Override
    public boolean isHoverDecorationSupported ()
    {
        return hoverPainter != null && component != null && component.isEnabled ();
    }

    /**
     * Paints hover cell highlight.
     *
     * @param g2d graphics context
     */
    protected void paintHoverCellBackground ( final Graphics2D g2d )
    {
        if ( isHoverDecorationSupported () )
        {
            // Checking hover cell availability
            final int hoverIndex = ui.getHoverIndex ();
            if ( hoverIndex != -1 && !component.isSelectedIndex ( hoverIndex ) )
            {
                // Checking hover cell bounds
                final Rectangle r = ui.getCellBounds ( component, hoverIndex, hoverIndex );
                if ( r != null )
                {
                    // Painting hover cell background
                    hoverPainter.paint ( g2d, r, component, ui );
                }
            }
        }
    }

    /**
     * Paints special WebLaF list cells selection.
     * It is rendered separately from cells allowing you to simplify your list cell renderer component.
     *
     * @param g2d graphics context
     */
    protected void paintSelectedCellsBackground ( final Graphics2D g2d )
    {
        if ( selectionPainter != null && component.getSelectedIndex () != -1 && ui.getSelectionStyle () != ListSelectionStyle.none )
        {
            // Painting selections
            final List selections = getSelectionRects ();
            for ( final Rectangle rect : selections )
            {
                selectionPainter.paint ( g2d, rect, component, ui );
            }
        }
    }

    /**
     * Returns list of list selections bounds.
     * This method takes selection style into account.
     *
     * @return list of list selections bounds
     */
    protected List getSelectionRects ()
    {
        // Return empty selection rects when custom selection painting is disabled
        if ( ui.getSelectionStyle () == ListSelectionStyle.none )
        {
            return Collections.emptyList ();
        }

        // Checking that selection exists
        final int[] indices = component.getSelectedIndices ();
        if ( indices == null || indices.length == 0 )
        {
            return Collections.emptyList ();
        }

        // Sorting selected rows
        Arrays.sort ( indices );

        // Calculating selection rects
        final List selections = new ArrayList ( indices.length );
        Rectangle maxRect = null;
        int lastRow = -1;
        for ( final int index : indices )
        {
            if ( ui.getSelectionStyle () == ListSelectionStyle.single )
            {
                // Required bounds
                selections.add ( component.getCellBounds ( index, index ) );
            }
            else
            {
                if ( lastRow != -1 && lastRow + 1 != index )
                {
                    // Save determined group
                    selections.add ( maxRect );

                    // Reset counting
                    maxRect = null;
                    lastRow = -1;
                }
                if ( lastRow == -1 || lastRow + 1 == index )
                {
                    // Required bounds
                    final Rectangle b = component.getCellBounds ( index, index );

                    // Increase rect
                    maxRect = lastRow == -1 ? b : GeometryUtils.getContainingRect ( maxRect, b );

                    // Remember last row
                    lastRow = index;
                }
            }
        }
        if ( maxRect != null )
        {
            selections.add ( maxRect );
        }
        return selections;
    }

    /**
     * Repaints all rectangles containing list selections.
     * This method is optimized to repaint only those area which are actually have selection in them.
     */
    protected void repaintSelection ()
    {
        if ( component.getSelectedIndex () != -1 )
        {
            for ( final Rectangle rect : getSelectionRects () )
            {
                component.repaint ( rect );
            }
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy