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

com.alee.laf.tree.TreePainter 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.tree;

import com.alee.laf.WebLookAndFeel;
import com.alee.managers.style.Bounds;
import com.alee.painter.DefaultPainter;
import com.alee.painter.decoration.AbstractDecorationPainter;
import com.alee.painter.decoration.IDecoration;
import com.alee.painter.PainterSupport;
import com.alee.painter.SectionPainter;
import com.alee.utils.CollectionUtils;
import com.alee.utils.CompareUtils;
import com.alee.utils.GeometryUtils;
import com.alee.utils.SwingUtils;

import javax.swing.*;
import javax.swing.event.TreeExpansionEvent;
import javax.swing.event.TreeExpansionListener;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.tree.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.*;
import java.util.List;

/**
 * @author Alexandr Zernov
 */

public class TreePainter> extends AbstractDecorationPainter
        implements ITreePainter
{
    /**
     * Style settings.
     */
    protected boolean paintLines;
    protected boolean dashedLines;
    protected Color linesColor;

    /**
     * Tree rows background painter.
     * It can be used to provide background customization for specific tree rows.
     */
    @DefaultPainter ( AlternateTreeRowPainter.class )
    protected ITreeRowPainter rowPainter;

    /**
     * Hover node background painter.
     * It can be used to provide background for hover nodes.
     */
    @DefaultPainter ( TreeNodePainter.class )
    protected ITreeNodePainter hoverPainter;

    /**
     * Selected nodes background painter.
     * It can be used to provide background for selected nodes.
     * WebLaF uses this painter instead of cell renderer -based selection decoration.
     */
    @DefaultPainter ( TreeNodePainter.class )
    protected ITreeNodePainter selectionPainter;

    /**
     * Tree drop location painter.
     * Provides visual representation for drag-and-drop operation on tree nodes.
     */
    @DefaultPainter ( TreeDropLocationPainter.class )
    protected ITreeDropLocationPainter dropLocationPainter;

    /**
     * Tree nodes selector painter.
     * It can be provided to enable nodes multiselector.
     */
    @DefaultPainter ( TreeSelectorPainter.class )
    protected ITreeSelectorPainter selectorPainter;

    /**
     * Listeners.
     */
    protected TreeSelectionListener treeSelectionListener;
    protected TreeExpansionListener treeExpansionListener;
    protected MouseAdapter mouseAdapter;

    /**
     * Runtime variables.
     */
    protected List initialSelection = new ArrayList ();
    protected Point selectionStart = null;
    protected Point selectionEnd = null;
    protected TreePath draggablePath = null;

    /**
     * Painting variables.
     */
    protected int totalChildIndent;
    protected int depthOffset;
    protected TreeModel treeModel;
    protected AbstractLayoutCache treeState;
    protected Hashtable paintingCache;
    protected CellRendererPane rendererPane;
    protected TreeCellRenderer currentCellRenderer;
    protected int editingRow = -1;
    protected int lastSelectionRow = -1;

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

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

        // Selection listener
        // Required for proper update of complex selection
        treeSelectionListener = new TreeSelectionListener ()
        {
            @Override
            public void valueChanged ( final TreeSelectionEvent e )
            {
                // Optimized selection repaint
                repaintSelection ();

                // Tree expansion on selection
                if ( ui.isExpandSelected () && component.getSelectionCount () > 0 )
                {
                    component.expandPath ( component.getSelectionPath () );
                }
            }
        };
        component.addTreeSelectionListener ( treeSelectionListener );

        // Expansion listener
        // Required for proper update of complex selection
        treeExpansionListener = new TreeExpansionListener ()
        {
            @Override
            public void treeExpanded ( final TreeExpansionEvent event )
            {
                repaintSelection ();
            }

            @Override
            public void treeCollapsed ( final TreeExpansionEvent event )
            {
                repaintSelection ();
            }
        };
        component.addTreeExpansionListener ( treeExpansionListener );

        // Mouse events adapter
        mouseAdapter = new MouseAdapter ()
        {
            @Override
            public void mousePressed ( final MouseEvent e )
            {
                // Only left mouse button events
                if ( SwingUtilities.isLeftMouseButton ( e ) )
                {
                    // Check that mouse did not hit actual tree cell
                    if ( !SwingUtils.isCtrl ( e ) && ( !component.getDragEnabled () || component.getTransferHandler () == null ) ||
                            ui.getRowForPoint ( e.getPoint (), false ) == -1 )
                    {
                        if ( isSelectorAvailable () )
                        {
                            // Avoiding selection start when pressed on tree expand handle
                            final TreePath path = ui.getClosestPathForLocation ( component, e.getX (), e.getY () );
                            if ( path == null || !isLocationInExpandControl ( path, e.getX (), e.getY () ) &&
                                    !ui.isLocationInCheckBoxControl ( path, e.getX (), e.getY () ) )
                            {
                                // Avoid starting multiselection if row is selected and drag is possible
                                final int rowForPath = ui.getRowForPath ( component, path );
                                if ( isDragAvailable () && rowForPath != -1 &&
                                        ui.getRowBounds ( rowForPath ).contains ( e.getX (), e.getY () ) &&
                                        component.isRowSelected ( rowForPath ) )
                                {
                                    // Marking row to be dragged
                                    draggablePath = path;
                                }
                                else
                                {
                                    // Selection
                                    selectionStart = e.getPoint ();
                                    selectionEnd = selectionStart;

                                    // Initial tree selection
                                    initialSelection = getSelectedRows ();

                                    // Updating selection
                                    validateSelection ( e );

                                    // Repainting selection on the tree
                                    repaintSelector ();
                                }
                            }
                        }
                        else if ( isFullLineSelection () )
                        {
                            // todo Start DnD on selected line here
                            // Avoiding selection start when pressed on tree expand handle
                            final TreePath path = ui.getClosestPathForLocation ( component, e.getX (), e.getY () );
                            if ( path != null && !isLocationInExpandControl ( path, e.getX (), e.getY () ) &&
                                    !ui.isLocationInCheckBoxControl ( path, e.getX (), e.getY () ) )
                            {
                                // Single row selection
                                if ( component.getSelectionModel ().getSelectionMode () == TreeSelectionModel.SINGLE_TREE_SELECTION )
                                {
                                    component.setSelectionRow ( ui.getRowForPoint ( e.getPoint (), true ) );
                                }

                                // Marking row to be dragged
                                final int rowForPath = ui.getRowForPath ( component, path );
                                if ( isDragAvailable () && ui.getRowBounds ( rowForPath ).contains ( e.getX (), e.getY () ) &&
                                        component.isRowSelected ( rowForPath ) )
                                {
                                    draggablePath = path;
                                }
                            }
                        }
                    }
                    // else
                    // {
                    //     // todo Start DnD on selected row here
                    //     // todo Also collapse node expansion on double-click if it is expanded and clicked on full line
                    // }
                }
            }

            @Override
            public void mouseDragged ( final MouseEvent e )
            {
                if ( draggablePath != null )
                {
                    final TransferHandler transferHandler = component.getTransferHandler ();
                    transferHandler.exportAsDrag ( component, e, transferHandler.getSourceActions ( component ) );
                    draggablePath = null;
                }
                if ( isSelectorAvailable () && selectionStart != null )
                {
                    // Selection
                    selectionEnd = e.getPoint ();

                    // Updating selection
                    validateSelection ( e );

                    // Repainting selection on the tree
                    repaintSelector ();

                    if ( !component.getVisibleRect ().contains ( e.getPoint () ) )
                    {
                        component.scrollRectToVisible ( new Rectangle ( e.getPoint (), new Dimension ( 0, 0 ) ) );
                    }
                }
            }

            @Override
            public void mouseReleased ( final MouseEvent e )
            {
                if ( draggablePath != null )
                {
                    draggablePath = null;
                }
                if ( isSelectorAvailable () && selectionStart != null )
                {
                    // Saving selection rect to repaint
                    // Rectangle fr = GeometryUtils.getContainingRect ( selectionStart, selectionEnd );

                    // Selection
                    selectionStart = null;
                    selectionEnd = null;

                    // Repainting selection on the tree
                    repaintSelector ( /*fr*/ );
                }
            }

            /**
             * Performs selection validation and updates.
             * todo Modify selection instead of overwriting each time?
             *
             * @param e mouse event
             */
            private void validateSelection ( final MouseEvent e )
            {
                // Selection rect
                final Rectangle selection = GeometryUtils.getContainingRect ( selectionStart, selectionEnd );

                // Compute new selection
                final List newSelection = new ArrayList ();
                if ( SwingUtils.isShift ( e ) )
                {
                    for ( int row = 0; row < component.getRowCount (); row++ )
                    {
                        if ( ui.getRowBounds ( row ).intersects ( selection ) && !initialSelection.contains ( row ) )
                        {
                            newSelection.add ( row );
                        }
                    }
                    for ( final int row : initialSelection )
                    {
                        newSelection.add ( row );
                    }
                }
                else if ( SwingUtils.isCtrl ( e ) )
                {
                    final List excludedRows = new ArrayList ();
                    for ( int row = 0; row < component.getRowCount (); row++ )
                    {
                        if ( ui.getRowBounds ( row ).intersects ( selection ) )
                        {
                            if ( initialSelection.contains ( row ) )
                            {
                                excludedRows.add ( row );
                            }
                            else
                            {
                                newSelection.add ( row );
                            }
                        }
                    }
                    for ( final int row : initialSelection )
                    {
                        if ( !excludedRows.contains ( row ) )
                        {
                            newSelection.add ( row );
                        }
                    }
                }
                else
                {
                    for ( int row = 0; row < component.getRowCount (); row++ )
                    {
                        if ( ui.getRowBounds ( row ).intersects ( selection ) )
                        {
                            newSelection.add ( row );
                        }
                    }
                }

                // Change selection if it is not the same as before
                if ( !CollectionUtils.equals ( getSelectedRows (), newSelection ) )
                {
                    if ( newSelection.size () > 0 )
                    {
                        component.setSelectionRows ( CollectionUtils.toArray ( newSelection ) );
                    }
                    else
                    {
                        component.clearSelection ();
                    }
                }
            }

            /**
             * Returns selected rows list.
             *
             * @return selected rows list
             */
            private List getSelectedRows ()
            {
                final List selection = new ArrayList ();
                final int[] selectionRows = component.getSelectionRows ();
                if ( selectionRows != null )
                {
                    for ( final int row : selectionRows )
                    {
                        selection.add ( row );
                    }
                }
                return selection;
            }

            /**
             * Repaints tree selector.
             * Replaced with full repaint due to strange tree lines painting bug.
             */
            private void repaintSelector ()
            {
                component.repaint ( component.getVisibleRect () );
            }
        };
        component.addMouseListener ( mouseAdapter );
        component.addMouseMotionListener ( mouseAdapter );
    }

    @Override
    public void uninstall ( final E c, final U ui )
    {
        // Removing listeners
        component.removeTreeSelectionListener ( treeSelectionListener );
        treeSelectionListener = null;
        component.removeTreeExpansionListener ( treeExpansionListener );
        treeExpansionListener = null;
        component.removeMouseListener ( mouseAdapter );
        component.removeMouseMotionListener ( mouseAdapter );
        mouseAdapter = null;

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

        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 );

        // Update visual drop location
        if ( CompareUtils.equals ( property, WebLookAndFeel.DROP_LOCATION ) && dropLocationPainter != null )
        {
            // Repainting previous drop location
            final JTree.DropLocation oldLocation = ( JTree.DropLocation ) oldValue;
            if ( oldLocation != null )
            {
                component.repaint ( dropLocationPainter.getDropViewBounds ( oldLocation ) );
            }

            // Repainting current drop location
            final JTree.DropLocation newLocation = ( JTree.DropLocation ) newValue;
            if ( newLocation != null )
            {
                component.repaint ( dropLocationPainter.getDropViewBounds ( newLocation ) );
            }
        }
    }

    @Override
    protected List> getSectionPainters ()
    {
        return asList ( rowPainter, selectionPainter, hoverPainter );
    }

    @Override
    protected void paintContent ( final Graphics2D g2d, final Rectangle bounds, final E c, final U ui )
    {
        // Checking tree state
        treeState = ui.getTreeState ();
        if ( treeState == null )
        {
            return;
        }

        // Preparing to paint tree
        treeModel = component.getModel ();
        totalChildIndent = ui.getLeftChildIndent () + ui.getRightChildIndent ();
        rendererPane = ui.getCellRendererPane ();
        lastSelectionRow = component.getLeadSelectionRow ();
        final TreePath editingPath = component.getEditingPath ();
        editingRow = editingPath != null ? component.getRowForPath ( editingPath ) : -1;
        updateDepthOffset ();

        // Painting tree background
        paintBackground ( g2d );

        // Painting hover node background
        paintHoverNodeBackground ( g2d );

        // Painting selected nodes background
        paintSelectedNodesBackground ( g2d );

        // Painting tree
        paintTree ( g2d );

        // Painting drop location
        paintDropLocation ( g2d, bounds, c, ui );

        // Multiselector
        paintMultiselector ( g2d );

        treeModel = null;
        treeState = null;
        paintingCache = null;
        rendererPane = null;
    }

    /**
     * Paints tree background.
     *
     * @param g2d graphics context
     */
    protected void paintBackground ( final Graphics2D g2d )
    {
        // Painting row background if one is available
        if ( rowPainter != null )
        {
            final Rectangle paintBounds = g2d.getClipBounds ();
            final TreePath initialPath = ui.getClosestPathForLocation ( component, 0, paintBounds.y );
            final Enumeration paintingEnumerator = treeState.getVisiblePathsFrom ( initialPath );

            if ( initialPath != null && paintingEnumerator != null )
            {
                final Insets insets = component.getInsets ();
                final int endY = paintBounds.y + paintBounds.height;
                final Rectangle boundsBuffer = new Rectangle ();

                Rectangle bounds;
                TreePath path;
                int row = treeState.getRowForPath ( initialPath );
                while ( paintingEnumerator.hasMoreElements () )
                {
                    path = ( TreePath ) paintingEnumerator.nextElement ();
                    if ( path != null )
                    {
                        bounds = getPathBounds ( path, insets, boundsBuffer );
                        if ( bounds == null )
                        {
                            // This will only happen if the model changes out
                            // from under us (usually in another thread).
                            // Swing isn't multi-threaded, but I'll put this
                            // check in anyway.
                            return;
                        }

                        // Preparing row painter to paint row background
                        rowPainter.prepareToPaint ( row );

                        // Calculating row bounds and painting its background
                        final Rectangle rowBounds = ui.getFullRowBounds ( row );
                        final Insets padding = ui.getPadding ();
                        if ( padding != null )
                        {
                            // Increasing background by the padding sizes at left and right sides
                            // This is required to properly display full row background, not node background
                            rowBounds.x -= padding.left;
                            rowBounds.width += padding.left + padding.right;
                        }
                        rowPainter.paint ( g2d, rowBounds, component, ui );

                        if ( ( bounds.y + bounds.height ) >= endY )
                        {
                            break;
                        }
                    }
                    else
                    {
                        break;
                    }
                    row++;
                }
            }
        }
    }

    @Override
    public void prepareToPaint ( final Hashtable paintingCache, final TreeCellRenderer currentCellRenderer )
    {
        this.paintingCache = paintingCache;
        this.currentCellRenderer = currentCellRenderer;
    }

    /**
     * Paints centered icon.
     *
     * @param c    component
     * @param g2d  graphics
     * @param icon icon
     * @param x    X coordinate
     * @param y    Y coordinate
     */
    protected void paintCentered ( final Component c, final Graphics2D g2d, final Icon icon, final int x, final int y )
    {                                                                   //x-icon.getIconWidth ()/2-2
        icon.paintIcon ( c, g2d, findCenteredX ( x, icon.getIconWidth () ), y - icon.getIconHeight () / 2 );
    }

    /**
     * Returns centered x coordinate for the icon.
     *
     * @param x         X coordinate
     * @param iconWidth icon width
     * @return centered x coordinate
     */
    protected int findCenteredX ( final int x, final int iconWidth )
    {
        return ltr ? ( x - 2 - ( int ) Math.ceil ( iconWidth / 2.0 ) ) : ( x - 1 - ( int ) Math.floor ( iconWidth / 2.0 ) );
    }

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

    /**
     * Paints hover node highlight.
     *
     * @param g2d graphics context
     */
    protected void paintHoverNodeBackground ( final Graphics2D g2d )
    {
        if ( isHoverDecorationSupported () )
        {
            // Checking hover row availability
            final int hoverRow = ui.getHoverRow ();
            if ( hoverRow != -1 && !component.isRowSelected ( hoverRow ) )
            {
                // Checking hover row bounds
                final Rectangle r = isFullLineSelection () ? ui.getFullRowBounds ( hoverRow ) : component.getRowBounds ( hoverRow );
                if ( r != null )
                {
                    // Painting hover node background
                    hoverPainter.paint ( g2d, r, component, ui );
                }
            }
        }
    }

    /**
     * Paints special WebLaF tree nodes selection.
     * It is rendered separately from nodes allowing you to simplify your tree cell renderer component.
     *
     * @param g2d graphics context
     */
    protected void paintSelectedNodesBackground ( final Graphics2D g2d )
    {
        if ( selectionPainter != null && component.getSelectionCount () > 0 && ui.getSelectionStyle () != TreeSelectionStyle.none )
        {
            // Painting selections
            final List selections = getSelectionRects ();
            for ( final Rectangle rect : selections )
            {
                selectionPainter.paint ( g2d, rect, component, ui );
            }
        }
    }

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

        // Checking that selection exists
        final int[] indices = component.getSelectionRows ();
        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 );
        final Insets insets = component.getInsets ();
        Rectangle maxRect = null;
        int lastRow = -1;
        for ( final int index : indices )
        {
            if ( ui.getSelectionStyle () == TreeSelectionStyle.single )
            {
                // Required bounds
                selections.add ( component.getRowBounds ( 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.getRowBounds ( index );

                    // Increasing bounds to cover whole line
                    if ( isFullLineSelection () )
                    {
                        b.x = insets.left;
                        b.width = component.getWidth () - insets.left - insets.right;
                    }

                    // 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 tree selections.
     * This method is optimized to repaint only those area which are actually have selection in them.
     */
    protected void repaintSelection ()
    {
        if ( component.getSelectionCount () > 0 )
        {
            for ( final Rectangle rect : getSelectionRects () )
            {
                component.repaint ( rect );
            }
        }
    }

    /**
     * Paints all base tree elements.
     * This is almost the same method as in BasicTreeUI but it doesn't paint drop line.
     *
     * @param g2d graphics context
     */
    protected void paintTree ( final Graphics2D g2d )
    {
        final Rectangle paintBounds = g2d.getClipBounds ();
        final Insets insets = component.getInsets ();
        final TreePath initialPath = ui.getClosestPathForLocation ( component, 0, paintBounds.y );
        final Enumeration paintingEnumerator = treeState.getVisiblePathsFrom ( initialPath );
        final int endY = paintBounds.y + paintBounds.height;
        int row = treeState.getRowForPath ( initialPath );

        paintingCache.clear ();

        if ( initialPath != null && paintingEnumerator != null )
        {
            TreePath parentPath = initialPath;

            // Paint the lines, knobs, and rows

            // Find each parent and have them paint a line to their last child
            parentPath = parentPath.getParentPath ();
            while ( parentPath != null )
            {
                paintVerticalPartOfLeg ( g2d, paintBounds, insets, parentPath );
                paintingCache.put ( parentPath, Boolean.TRUE );
                parentPath = parentPath.getParentPath ();
            }

            // Information for the node being rendered.
            final Rectangle boundsBuffer = new Rectangle ();
            final boolean rootVisible = isRootVisible ();
            boolean isExpanded;
            boolean hasBeenExpanded;
            boolean isLeaf;
            Rectangle bounds;
            TreePath path;

            while ( paintingEnumerator.hasMoreElements () )
            {
                path = ( TreePath ) paintingEnumerator.nextElement ();
                if ( path != null )
                {
                    isLeaf = treeModel.isLeaf ( path.getLastPathComponent () );
                    if ( isLeaf )
                    {
                        isExpanded = hasBeenExpanded = false;
                    }
                    else
                    {
                        isExpanded = treeState.getExpandedState ( path );
                        hasBeenExpanded = component.hasBeenExpanded ( path );
                    }

                    bounds = getPathBounds ( path, insets, boundsBuffer );
                    if ( bounds == null )
                    {
                        // This will only happen if the model changes out from under us (usually in another thread).
                        // Swing isn't multi-threaded, but I'll put this check in anyway.
                        return;
                    }

                    // See if the vertical line to the parent has been painted
                    parentPath = path.getParentPath ();
                    if ( parentPath != null )
                    {
                        if ( paintingCache.get ( parentPath ) == null )
                        {
                            paintVerticalPartOfLeg ( g2d, paintBounds, insets, parentPath );
                            paintingCache.put ( parentPath, Boolean.TRUE );
                        }
                        paintHorizontalPartOfLeg ( g2d, paintBounds, insets, bounds, path, row, isExpanded, hasBeenExpanded, isLeaf );
                    }
                    else if ( rootVisible && row == 0 )
                    {
                        paintHorizontalPartOfLeg ( g2d, paintBounds, insets, bounds, path, row, isExpanded, hasBeenExpanded, isLeaf );
                    }
                    if ( shouldPaintExpandControl ( path, row, isExpanded, hasBeenExpanded, isLeaf ) )
                    {
                        paintExpandControl ( g2d, paintBounds, insets, bounds, path, row, isExpanded, hasBeenExpanded, isLeaf );
                    }
                    paintRow ( g2d, paintBounds, insets, bounds, path, row, isExpanded, hasBeenExpanded, isLeaf );
                    if ( ( bounds.y + bounds.height ) >= endY )
                    {
                        break;
                    }
                }
                else
                {
                    break;
                }
                row++;
            }
        }

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

    /**
     * Returns whether or not {@code mouseX} and {@code mouseY} fall in the area of row that is used to expand/collapse the node and the
     * node at {@code row} does not represent a leaf.
     *
     * @param path   tree path
     * @param mouseX mouse X location
     * @param mouseY mouse Y location
     * @return true if {@code mouseX} and {@code mouseY} fall in the area of row that is used to expand/collapse the node and the node at
     * {@code row} does not represent a leaf, false otherwise
     */
    @SuppressWarnings ( "UnusedParameters" )
    protected boolean isLocationInExpandControl ( final TreePath path, final int mouseX, final int mouseY )
    {
        if ( path != null && !component.getModel ().isLeaf ( path.getLastPathComponent () ) )
        {
            final int boxWidth = ui.getExpandedIcon () != null ? ui.getExpandedIcon ().getIconWidth () : 8;
            final Insets i = component.getInsets ();

            int boxLeftX = getRowX ( component.getRowForPath ( path ), path.getPathCount () - 1 );

            boxLeftX = ltr ? boxLeftX + i.left - ui.getRightChildIndent () + 1 :
                    component.getWidth () - boxLeftX - i.right + ui.getRightChildIndent () - 1;

            boxLeftX = findCenteredX ( boxLeftX, boxWidth );

            return mouseX >= boxLeftX && mouseX < ( boxLeftX + boxWidth );
        }
        return false;
    }

    /**
     * Paints the expand (toggle) part of a row.
     * The receiver should NOT modify {@code clipBounds}, or {@code insets}.
     *
     * @param g2d             graphics context
     * @param clipBounds      clip bounds
     * @param insets          tree insets
     * @param bounds          tree path bounds
     * @param path            tree path
     * @param row             row index
     * @param isExpanded      whether row is expanded or not
     * @param hasBeenExpanded whether row has been expanded once before or not
     * @param isLeaf          whether node is leaf or not
     */
    @SuppressWarnings ( "UnusedParameters" )
    protected void paintExpandControl ( final Graphics2D g2d, final Rectangle clipBounds, final Insets insets, final Rectangle bounds,
                                        final TreePath path, final int row, final boolean isExpanded, final boolean hasBeenExpanded,
                                        final boolean isLeaf )
    {
        final Object value = path.getLastPathComponent ();

        // Paint icons if not a leaf and either hasn't been loaded,
        // or the model child count is > 0.
        if ( !isLeaf && ( !hasBeenExpanded || treeModel.getChildCount ( value ) > 0 ) )
        {
            final int middleXOfKnob;
            if ( ltr )
            {
                middleXOfKnob = bounds.x - ui.getRightChildIndent () + 1;
            }
            else
            {
                middleXOfKnob = bounds.x + bounds.width + ui.getRightChildIndent () - 1;
            }
            final int middleYOfKnob = bounds.y + ( bounds.height / 2 );

            if ( isExpanded )
            {
                final Icon expandedIcon = ui.getExpandedIcon ();
                if ( expandedIcon != null )
                {
                    paintCentered ( component, g2d, expandedIcon, middleXOfKnob, middleYOfKnob );
                }
            }
            else
            {
                final Icon collapsedIcon = ui.getCollapsedIcon ();
                if ( collapsedIcon != null )
                {
                    paintCentered ( component, g2d, collapsedIcon, middleXOfKnob, middleYOfKnob );
                }
            }
        }
    }

    /**
     * Paints the renderer part of a row.
     * The receiver should NOT modify {@code clipBounds}, or {@code insets}.
     *
     * @param g2d             graphics context
     * @param clipBounds      clip bounds
     * @param insets          tree insets
     * @param bounds          tree path bounds
     * @param path            tree path
     * @param row             row index
     * @param isExpanded      whether row is expanded or not
     * @param hasBeenExpanded whether row has been expanded once before or not
     * @param isLeaf          whether node is leaf or not
     */
    @SuppressWarnings ( "UnusedParameters" )
    protected void paintRow ( final Graphics2D g2d, final Rectangle clipBounds, final Insets insets, final Rectangle bounds,
                              final TreePath path, final int row, final boolean isExpanded, final boolean hasBeenExpanded,
                              final boolean isLeaf )
    {
        // Don't paint the renderer if editing this row.
        if ( editingRow == row )
        {
            return;
        }

        // Retrieving row cell renderer
        final Object value = path.getLastPathComponent ();
        final boolean hasFocus = ( component.hasFocus () ? lastSelectionRow : -1 ) == row;
        final boolean selected = component.isRowSelected ( row );
        final Component rowComponent =
                currentCellRenderer.getTreeCellRendererComponent ( component, value, selected, isExpanded, isLeaf, row, hasFocus );

        // Painting cell renderer
        rendererPane.paintComponent ( g2d, rowComponent, component, bounds.x, bounds.y, bounds.width, bounds.height, true );
    }

    /**
     * Returns whether or not the expand (toggle) control should be painted for the specified row.
     *
     * @param path            tree path
     * @param row             row index
     * @param isExpanded      whether row is expanded or not
     * @param hasBeenExpanded whether row has been expanded once before or not
     * @param isLeaf          whether node is leaf or not
     * @return true if the expand (toggle) control should be painted for the specified row, false otherwise
     */
    @SuppressWarnings ( "UnusedParameters" )
    protected boolean shouldPaintExpandControl ( final TreePath path, final int row, final boolean isExpanded,
                                                 final boolean hasBeenExpanded, final boolean isLeaf )
    {
        if ( isLeaf )
        {
            return false;
        }

        final int depth = path.getPathCount () - 1;
        return !( ( depth == 0 || ( depth == 1 && !isRootVisible () ) ) && !getShowsRootHandles () );
    }

    /**
     * Returns whether or not root is visible.
     *
     * @return true if root is visible, false otherwise
     */
    protected boolean isRootVisible ()
    {
        return component != null && component.isRootVisible ();
    }

    /**
     * Returns whether or not root handles should be displayed.
     *
     * @return true if root handles should be displayed, false otherwise
     */
    protected boolean getShowsRootHandles ()
    {
        return component != null && component.getShowsRootHandles ();
    }

    /**
     * Paints the horizontal part of the leg.
     *
     * @param g2d             graphics
     * @param clipBounds      clip bounds
     * @param insets          tree insets
     * @param bounds          tree path bounds
     * @param path            tree path
     * @param row             row index
     * @param isExpanded      whether row is expanded or not
     * @param hasBeenExpanded whether row has been expanded once before or not
     * @param isLeaf          whether node is leaf or not
     */
    @SuppressWarnings ( "UnusedParameters" )
    protected void paintHorizontalPartOfLeg ( final Graphics2D g2d, final Rectangle clipBounds, final Insets insets, final Rectangle bounds,
                                              final TreePath path, final int row, final boolean isExpanded, final boolean hasBeenExpanded,
                                              final boolean isLeaf )
    {
        if ( !paintLines )
        {
            return;
        }

        // Don't paint the legs for the root node if the
        final int depth = path.getPathCount () - 1;
        if ( ( depth == 0 || ( depth == 1 && !isRootVisible () ) ) && !getShowsRootHandles () )
        {
            return;
        }

        final int clipLeft = clipBounds.x;
        final int clipRight = clipBounds.x + clipBounds.width;
        final int clipTop = clipBounds.y;
        final int clipBottom = clipBounds.y + clipBounds.height;
        final int lineY = bounds.y + bounds.height / 2;

        if ( ltr )
        {
            final int leftX = bounds.x - ui.getRightChildIndent ();
            final int nodeX = bounds.x - getHorizontalLegIndent ();

            if ( lineY >= clipTop && lineY < clipBottom && nodeX >= clipLeft && leftX < clipRight && leftX < nodeX )
            {

                g2d.setPaint ( linesColor );
                paintHorizontalLine ( g2d, lineY, leftX, nodeX - 1 );
            }
        }
        else
        {
            final int nodeX = bounds.x + bounds.width + getHorizontalLegIndent ();
            final int rightX = bounds.x + bounds.width + ui.getRightChildIndent ();

            if ( lineY >= clipTop && lineY < clipBottom && rightX >= clipLeft && nodeX < clipRight && nodeX < rightX )
            {

                g2d.setPaint ( linesColor );
                paintHorizontalLine ( g2d, lineY, nodeX, rightX - 1 );
            }
        }
    }

    /**
     * Returns horizontal leg indent.
     *
     * @return horizontal leg indent
     */
    protected int getHorizontalLegIndent ()
    {
        return -2;
    }

    /**
     * Paints the vertical part of the leg.
     *
     * @param g2d        graphics
     * @param clipBounds clip bounds
     * @param insets     tree insets
     * @param path       tree path
     */
    protected void paintVerticalPartOfLeg ( final Graphics2D g2d, final Rectangle clipBounds, final Insets insets, final TreePath path )
    {
        if ( !paintLines )
        {
            return;
        }

        final int depth = path.getPathCount () - 1;
        if ( depth == 0 && !getShowsRootHandles () && !isRootVisible () )
        {
            return;
        }
        int lineX = getRowX ( -1, depth + 1 );
        if ( ltr )
        {
            lineX = lineX - ui.getRightChildIndent () + insets.left;
        }
        else
        {
            lineX = component.getWidth () - lineX - insets.right + ui.getRightChildIndent () - 1;
        }
        final int clipLeft = clipBounds.x;
        final int clipRight = clipBounds.x + ( clipBounds.width - 1 );

        if ( lineX >= clipLeft && lineX <= clipRight )
        {
            final int clipTop = clipBounds.y;
            final int clipBottom = clipBounds.y + clipBounds.height;
            Rectangle parentBounds = getPathBounds ( path );
            final Rectangle lastChildBounds = getPathBounds ( getLastChildPath ( path ) );

            if ( lastChildBounds == null )
            // This shouldn't happen, but if the model is modified
            // in another thread it is possible for this to happen.
            // Swing isn't multi-threaded, but I'll add this check in
            // anyway.
            {
                return;
            }

            int top;

            if ( parentBounds == null )
            {
                top = Math.max ( insets.top + getVerticalLegIndent (), clipTop );
            }
            else
            {
                top = Math.max ( parentBounds.y + parentBounds.height +
                        getVerticalLegIndent (), clipTop );
            }
            if ( depth == 0 && !isRootVisible () )
            {
                if ( treeModel != null )
                {
                    final Object root = treeModel.getRoot ();

                    if ( treeModel.getChildCount ( root ) > 0 )
                    {
                        parentBounds = getPathBounds ( path.pathByAddingChild ( treeModel.getChild ( root, 0 ) ) );
                        if ( parentBounds != null )
                        {
                            top = Math.max ( insets.top + getVerticalLegIndent (), parentBounds.y + parentBounds.height / 2 );
                        }
                    }
                }
            }

            final int bottom = Math.min ( lastChildBounds.y + ( lastChildBounds.height / 2 ), clipBottom );

            if ( top <= bottom )
            {
                g2d.setPaint ( linesColor );
                paintVerticalLine ( g2d, lineX, top, bottom );
            }
        }
    }

    /**
     * Returns a path to the last child of {@code parent}.
     *
     * @param parent parent tree path
     * @return path to the last child of {@code parent}
     */
    protected TreePath getLastChildPath ( final TreePath parent )
    {
        if ( treeModel != null )
        {
            final int childCount = treeModel.getChildCount ( parent.getLastPathComponent () );
            if ( childCount > 0 )
            {
                return parent.pathByAddingChild ( treeModel.getChild ( parent.getLastPathComponent (), childCount - 1 ) );
            }
        }
        return null;
    }

    /**
     * Paints a vertical line.
     *
     * @param g2d graphics context
     * @param x   X coordinate
     * @param y1  start Y coordinate
     * @param y2  end Y coordinate
     */
    protected void paintVerticalLine ( final Graphics2D g2d, final int x, final int y1, final int y2 )
    {
        if ( dashedLines )
        {
            paintDashedVerticalLine ( g2d, x, y1, y2 );
        }
        else
        {
            g2d.drawLine ( x, y1, x, y2 );
        }
    }

    /**
     * Paints dashed vertical line.
     * This method assumes that y1 <= y2 always.
     * todo Change to proper stroke usage instead as this implementation is slow
     *
     * @param g2d graphics context
     * @param x   X coordinate
     * @param y1  start Y coordinate
     * @param y2  end Y coordinate
     */
    protected void paintDashedVerticalLine ( final Graphics2D g2d, final int x, int y1, final int y2 )
    {
        // Painting only even coordinates helps join line segments so they appear as one line
        // This can be defeated by translating the Graphics2D by an odd amount
        y1 += y1 % 2;

        // Painting dashed line
        for ( int y = y1; y <= y2; y += 2 )
        {
            g2d.drawLine ( x, y, x, y );
        }
    }

    /**
     * Paints a horizontal line.
     *
     * @param g2d graphics context
     * @param y   Y coordinate
     * @param x1  start X coordinate
     * @param x2  end X coordinate
     */
    protected void paintHorizontalLine ( final Graphics2D g2d, final int y, final int x1, final int x2 )
    {
        if ( dashedLines )
        {
            paintDashedHorizontalLine ( g2d, y, x1, x2 );
        }
        else
        {
            g2d.drawLine ( x1, y, x2, y );
        }
    }

    /**
     * Paints dashed horizontal line.
     * This method assumes that x1 <= x2 always.
     * todo Change to proper stroke usage instead as this implementation is slow
     *
     * @param g2d graphics context
     * @param y   Y coordinate
     * @param x1  start X coordinate
     * @param x2  end X coordinate
     */
    protected void paintDashedHorizontalLine ( final Graphics2D g2d, final int y, int x1, final int x2 )
    {
        // Painting only even coordinates helps join line segments so they appear as one line
        // This can be defeated by translating the Graphics2D by an odd amount
        x1 += x1 % 2;

        // Painting dashed line
        for ( int x = x1; x <= x2; x += 2 )
        {
            g2d.drawLine ( x, y, x, y );
        }
    }

    /**
     * Returns the location, along the x-axis, to render a particular row at. The return value does not include any Insets specified on the
     * JTree. This does not check for the validity of the row or depth, it is assumed to be correct and will not throw an Exception if the
     * row or depth doesn't match that of the tree.
     *
     * @param row   Row to return x location for
     * @param depth Depth of the row
     * @return amount to indent the given row.
     */
    @SuppressWarnings ( "UnusedParameters" )
    protected int getRowX ( final int row, final int depth )
    {
        return totalChildIndent * ( depth + depthOffset );
    }

    /**
     * Updates how much each depth should be offset by.
     */
    protected void updateDepthOffset ()
    {
        if ( isRootVisible () )
        {
            if ( getShowsRootHandles () )
            {
                depthOffset = 1;
            }
            else
            {
                depthOffset = 0;
            }
        }
        else if ( !getShowsRootHandles () )
        {
            depthOffset = -1;
        }
        else
        {
            depthOffset = 0;
        }
    }

    /**
     * The vertical element of legs between nodes starts at the bottom of the parent node by default.
     * This method makes the leg start below that.
     *
     * @return vertical leg indent
     */
    protected int getVerticalLegIndent ()
    {
        return 0;
    }

    /**
     * Paints drop location if it is available.
     *
     * @param g2d    graphics context
     * @param bounds painting bounds
     * @param c      painted component
     * @param ui     painted component UI
     */
    protected void paintDropLocation ( final Graphics2D g2d, final Rectangle bounds, final E c, final U ui )
    {
        if ( isDropLocationAvailable () )
        {
            dropLocationPainter.prepareToPaint ( component.getDropLocation () );
            dropLocationPainter.paint ( g2d, bounds, c, ui );
        }
    }

    /**
     * Returns whether or not drop location is available.
     *
     * @return true if drop location is available, false otherwise
     */
    protected boolean isDropLocationAvailable ()
    {
        return dropLocationPainter != null && component.getDropLocation () != null;
    }

    /**
     * Paints custom WebLaF multiselector.
     *
     * @param g2d graphics context
     */
    protected void paintMultiselector ( final Graphics2D g2d )
    {
        if ( isSelectorAvailable () && selectionStart != null && selectionEnd != null )
        {
            final Rectangle sb = GeometryUtils.getContainingRect ( selectionStart, selectionEnd );
            final Rectangle fsb = sb.intersection ( Bounds.component.of ( component ) );
            fsb.width -= 1;
            fsb.height -= 1;
            selectorPainter.paint ( g2d, fsb, component, ui );
        }
    }

    /**
     * Returns whether selector is available for current tree or not.
     *
     * @return true if selector is available for current tree, false otherwise
     */
    protected boolean isSelectorAvailable ()
    {
        return selectorPainter != null && component != null && component.isEnabled () &&
                component.getSelectionModel ().getSelectionMode () != TreeSelectionModel.SINGLE_TREE_SELECTION;
    }

    /**
     * Returns the Rectangle enclosing the label portion that the last item in path will be painted into.
     * Will return null if any component in path is currently valid.
     *
     * @param path tree path
     * @return Rectangle enclosing the label portion that the last item in path will be painted into
     */
    protected Rectangle getPathBounds ( final TreePath path )
    {
        if ( component != null && treeState != null )
        {
            return getPathBounds ( path, component.getInsets (), new Rectangle () );
        }
        return null;
    }

    /**
     * Returns path bounds used for painting.
     *
     * @param path   tree path
     * @param insets tree insets
     * @param bounds bounds buffer
     * @return path bounds
     */
    protected Rectangle getPathBounds ( final TreePath path, final Insets insets, Rectangle bounds )
    {
        bounds = treeState.getBounds ( path, bounds );
        if ( bounds != null )
        {
            if ( ltr )
            {
                bounds.x += insets.left;
            }
            else
            {
                bounds.x = component.getWidth () - ( bounds.x + bounds.width ) - insets.right;
            }
            bounds.y += insets.top;
        }
        return bounds;
    }

    /**
     * Returns whether tree selection style points that the whole line is a single cell or not.
     *
     * @return true if tree selection style points that the whole line is a single cell, false otherwise
     */
    protected boolean isFullLineSelection ()
    {
        return ui.getSelectionStyle () == TreeSelectionStyle.line;
    }

    /**
     * Returns whether tree nodes drag available or not.
     *
     * @return true if tree nodes drag available, false otherwise
     */
    protected boolean isDragAvailable ()
    {
        return component != null && component.isEnabled () && component.getDragEnabled () && component.getTransferHandler () != null &&
                component.getTransferHandler ().getSourceActions ( component ) > 0;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy