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

org.pushingpixels.radiance.theming.internal.ui.RadianceTableUI Maven / Gradle / Ivy

/*
 * Copyright (c) 2005-2021 Radiance Kirill Grouchnikov. All Rights Reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *  o Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 *  o Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 *  o Neither the name of the copyright holder nor the names of
 *    its contributors may be used to endorse or promote products derived
 *    from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package org.pushingpixels.radiance.theming.internal.ui;

//import org.pushingpixels.radiance.animation.api.Timeline.TimelineState;
//import org.pushingpixels.radiance.animation.api.swing.EventDispatchThreadTimelineCallbackAdapter;
import org.pushingpixels.trident.api.Timeline.TimelineState;
import org.pushingpixels.trident.api.swing.EventDispatchThreadTimelineCallbackAdapter;

import org.pushingpixels.radiance.common.api.RadianceCommonCortex;
import org.pushingpixels.radiance.theming.api.ComponentState;
import org.pushingpixels.radiance.theming.api.RadianceThemingSlices;
import org.pushingpixels.radiance.theming.api.colorscheme.RadianceColorScheme;
import org.pushingpixels.radiance.theming.api.renderer.RadianceDefaultTableCellRenderer;
import org.pushingpixels.radiance.theming.api.renderer.RadianceDefaultTableHeaderCellRenderer;
import org.pushingpixels.radiance.theming.internal.AnimationConfigurationManager;
import org.pushingpixels.radiance.theming.internal.animation.StateTransitionMultiTracker;
import org.pushingpixels.radiance.theming.internal.animation.StateTransitionTracker;
import org.pushingpixels.radiance.theming.internal.painter.BackgroundPaintingUtils;
import org.pushingpixels.radiance.theming.internal.painter.DecorationPainterUtils;
import org.pushingpixels.radiance.theming.internal.painter.HighlightPainterUtils;
import org.pushingpixels.radiance.theming.internal.utils.*;

import javax.swing.*;
import javax.swing.RowSorter.SortKey;
import javax.swing.border.Border;
import javax.swing.event.*;
import javax.swing.plaf.ComponentUI;
import javax.swing.plaf.TableHeaderUI;
import javax.swing.plaf.UIResource;
import javax.swing.plaf.basic.BasicTableUI;
import javax.swing.table.*;
import java.awt.*;
import java.awt.event.*;
import java.beans.PropertyChangeListener;
import java.util.List;
import java.util.*;

/**
 * UI for tables in Radiance look and feel.
 *
 * @author Kirill Grouchnikov
 */
public class RadianceTableUI extends BasicTableUI implements UpdateOptimizationAware {
    /**
     * Holds the list of currently selected row-column indexes.
     */
    private Map selectedIndices;

    /**
     * Holds the currently rolled-over row-column index, or null if none such.
     */
    private Set rolledOverIndices;

    private TableCellId focusedCellId;

    /**
     * Holds the currently rolled-over column index, or -1 if none such. This is used
     * for the table header animations.
     */
    private int rolledOverColumn;

    private int rolledOverRow;

    /**
     * Map of default renderers.
     */
    private Map, TableCellRenderer> defaultRenderers;

    /**
     * Map of default editors.
     */
    private Map, TableCellEditor> defaultEditors;

    /**
     * Listener that listens to changes on table properties.
     */
    private PropertyChangeListener radiancePropertyChangeListener;

    /**
     * Listener for transition animations on list selections.
     */
    private TableStateListener radianceTableStateListener;

    /**
     * Listener for transition animations on table rollovers.
     */
    private RolloverFadeListener radianceFadeRolloverListener;

    private FocusListener radianceFocusListener;

    private StateTransitionMultiTracker stateTransitionMultiTracker;

    /**
     * Cell renderer insets. Is computed in {@link #installDefaults()} and reused in
     * {@link RadianceDefaultTableCellRenderer#getTableCellRendererComponent(JTable, Object, boolean, boolean, int, int)}
     * and
     * {@link RadianceDefaultTableHeaderCellRenderer#getTableCellRendererComponent(JTable, Object, boolean, boolean, int, int)}
     * for performance optimizations.
     */
    private Insets cellRendererInsets;

    public static ComponentUI createUI(JComponent comp) {
        RadianceCoreUtilities.testComponentCreationThreadingViolation(comp);
        return new RadianceTableUI();
    }

    /**
     * Creates a UI delegate for table.
     */
    private RadianceTableUI() {
        super();
        this.selectedIndices = new HashMap<>();
        this.rolledOverIndices = new HashSet<>();
        this.stateTransitionMultiTracker = new StateTransitionMultiTracker<>();
        this.rolledOverColumn = -1;
        this.rolledOverRow = -1;

        this.cellId = new TableCellId(-1, -1);
    }

    static class BooleanEditor extends DefaultCellEditor {
        private static class RadianceEditorCheckBox extends JCheckBox {
            @Override
            public void setOpaque(boolean isOpaque) {
                if (!isOpaque) {
                    super.setOpaque(isOpaque);
                }
            }

            @Override
            public boolean isOpaque() {
                return false;
            }

            @Override
            public void setBorder(Border border) {
            }
        }

        public BooleanEditor() {
            super(new RadianceEditorCheckBox());
            JCheckBox checkBox = (JCheckBox) getComponent();
            checkBox.setOpaque(false);
            checkBox.setHorizontalAlignment(JCheckBox.CENTER);
        }
    }

    @Override
    protected void installDefaults() {
        super.installDefaults();

        // fix for defect 117 - need to restore default table cell
        // renderers when Radiance is unset
        this.defaultRenderers = new HashMap<>();

        Class[] defClasses = new Class[] {Object.class, Icon.class, ImageIcon.class,
                Number.class, Float.class, Double.class, Date.class, Boolean.class};
        for (Class clazz : defClasses) {
            this.defaultRenderers.put(clazz, this.table.getDefaultRenderer(clazz));
        }

        // Override default renderers - note fix for issue 194
        // that doesn't override user-specific renderers (those that don't come
        // from JTable class).
        this.installRendererIfNecessary(Object.class, new RadianceDefaultTableCellRenderer());
        this.installRendererIfNecessary(Icon.class,
                new RadianceDefaultTableCellRenderer.IconRenderer());
        this.installRendererIfNecessary(ImageIcon.class,
                new RadianceDefaultTableCellRenderer.IconRenderer());
        this.installRendererIfNecessary(Number.class,
                new RadianceDefaultTableCellRenderer.NumberRenderer());
        this.installRendererIfNecessary(Float.class,
                new RadianceDefaultTableCellRenderer.DoubleRenderer());
        this.installRendererIfNecessary(Double.class,
                new RadianceDefaultTableCellRenderer.DoubleRenderer());
        this.installRendererIfNecessary(Date.class,
                new RadianceDefaultTableCellRenderer.DateRenderer());

        // fix for bug 56 - making default renderer for Boolean a check box.
        this.installRendererIfNecessary(Boolean.class,
                new RadianceDefaultTableCellRenderer.BooleanRenderer());

        this.defaultEditors = new HashMap<>();

        Class[] defEditorClasses = new Class[] {Boolean.class};
        for (Class clazz : defEditorClasses) {
            this.defaultEditors.put(clazz, this.table.getDefaultEditor(clazz));
        }
        this.installEditorIfNecessary(Boolean.class, new BooleanEditor());

        int rows = this.table.getRowCount();
        int cols = this.table.getColumnCount();
        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                if (this.table.isCellSelected(i, j)) {
                    TableCellId cellId = new TableCellId(i, j);
                    this.selectedIndices.put(cellId, this.table.getValueAt(i, j));
                }
            }
        }

        // This is a little tricky, and hopefully will not
        // interfere with existing applications. The row height in tables
        // is computed differently from trees and lists. While lists
        // trees respect the current renderers and their insets, the
        // JTable uses hard-code value of 16 pixels as the default
        // row height. This, obviously, doesn't sit well with the support
        // for custom fonts and high-DPI monitors.
        //
        // The current solution first checks whether all the renderers
        // come from Radiance. If not, it does nothing. If they do, it
        // creates a placeholder label, computes its preferred height and apply
        // insets. There's no need to go over each cell and compute its
        // preferred height - since at this moment the cell renderers
        // *are* Radiance labels.
        boolean areAllRenderersFromRadiance = true;
        TableColumnModel columnModel = table.getColumnModel();
        for (int i = 0; i < columnModel.getColumnCount(); i++) {
            TableColumn column = columnModel.getColumn(i);
            TableCellRenderer renderer = column.getCellRenderer();
            if (renderer == null) {
                renderer = table.getDefaultRenderer(table.getColumnClass(i));
            }
            if ((renderer instanceof RadianceDefaultTableCellRenderer)
                    || (renderer instanceof RadianceDefaultTableCellRenderer.BooleanRenderer))
                continue;
            areAllRenderersFromRadiance = false;
            break;
        }
        if (areAllRenderersFromRadiance) {
            Insets rendererInsets = RadianceSizeUtils
                    .getTableCellRendererInsets(RadianceSizeUtils.getComponentFontSize(table));
            JLabel forSizing = new JLabel("Text");
            forSizing.setFont(table.getFont());
            int rowHeight = forSizing.getPreferredSize().height + rendererInsets.bottom + rendererInsets.top;
            table.setRowHeight(rowHeight);
        }

        // instead of computing the cell renderer insets on
        // every cell rendering, compute it once and expose to the
        // RadianceDefaultTableCellRenderer
        this.cellRendererInsets = RadianceSizeUtils
                .getTableCellRendererInsets(RadianceSizeUtils.getComponentFontSize(table));
    }

    /**
     * Installs Radiance-specific renderers for column classes that don't have application-specific
     * renderers installed by the user code.
     *
     * @param clazz    Column class.
     * @param renderer Default renderer for the specified column class.
     */
    protected void installRendererIfNecessary(Class clazz, TableCellRenderer renderer) {
        TableCellRenderer currRenderer = this.table.getDefaultRenderer(clazz);
        if (currRenderer != null) {
            boolean isCore = (currRenderer instanceof DefaultTableCellRenderer.UIResource)
                    || (currRenderer.getClass().getName().startsWith("javax.swing.JTable"));
            if (!isCore)
                return;
        }
        this.table.setDefaultRenderer(clazz, renderer);
    }

    /**
     * Installs Radiance-specific renderers for column classes that don't have application-specific
     * renderers installed by the user code.
     *
     * @param clazz  Column class.
     * @param editor Default renderer for the specified column class.
     */
    protected void installEditorIfNecessary(Class clazz, TableCellEditor editor) {
        TableCellEditor currEditor = this.table.getDefaultEditor(clazz);
        if (currEditor != null) {
            boolean isCore = currEditor.getClass().getName().startsWith("javax.swing.JTable");
            if (!isCore)
                return;
        }
        this.table.setDefaultEditor(clazz, editor);
    }

    @Override
    protected void uninstallDefaults() {
        // fix for defect 117 - need to restore default table cell
        // renderers when Radiance is unset
        for (Map.Entry, TableCellRenderer> entry : this.defaultRenderers.entrySet()) {
            // fix for issue 194 - restore only those renderers that were
            // overriden by Radiance.
            this.uninstallRendererIfNecessary(entry.getKey(), entry.getValue());
        }

        for (Map.Entry, TableCellEditor> entry : this.defaultEditors.entrySet()) {
            this.uninstallEditorIfNecessary(entry.getKey(), entry.getValue());
        }

        this.selectedIndices.clear();

        super.uninstallDefaults();
    }

    /**
     * Uninstalls default Radiance renderers that were installed in
     * {@link #installRendererIfNecessary(Class, TableCellRenderer)}.
     *
     * @param clazz    Column class.
     * @param renderer Renderer to restore.
     */
    protected void uninstallRendererIfNecessary(Class clazz, TableCellRenderer renderer) {
        TableCellRenderer currRenderer = this.table.getDefaultRenderer(clazz);
        if (currRenderer != null) {
            boolean isRadianceRenderer = isRadianceDefaultRenderer(currRenderer);
            if (!isRadianceRenderer)
                return;
        }
        if (renderer instanceof Component)
            SwingUtilities.updateComponentTreeUI((Component) renderer);
        this.table.setDefaultRenderer(clazz, renderer);
    }

    /**
     * Uninstalls default Radiance editors that were installed in
     * {@link #installEditorIfNecessary(Class, TableCellEditor)}.
     *
     * @param clazz  Column class.
     * @param editor Editor to restore.
     */
    protected void uninstallEditorIfNecessary(Class clazz, TableCellEditor editor) {
        TableCellEditor currEditor = this.table.getDefaultEditor(clazz);
        if (currEditor != null) {
            boolean isRadianceEditor = isRadianceDefaultEditor(currEditor);
            if (!isRadianceEditor)
                return;
        }
        if (editor instanceof Component)
            SwingUtilities.updateComponentTreeUI((Component) editor);
        this.table.setDefaultEditor(clazz, editor);
    }

    @Override
    protected void installListeners() {
        super.installListeners();
        this.radiancePropertyChangeListener = propertyChangeEvent -> {
            if ("columnSelectionAllowed".equals(propertyChangeEvent.getPropertyName())
                    || "rowSelectionAllowed".equals(propertyChangeEvent.getPropertyName())) {
                RadianceTableUI.this.syncSelection(true);
            }

            if ("model".equals(propertyChangeEvent.getPropertyName())) {
                TableModel old = (TableModel) propertyChangeEvent.getOldValue();
                if (old != null) {
                    old.removeTableModelListener(radianceTableStateListener);
                }
                // fix for defect 291 - track changes to the table.
                table.getModel().addTableModelListener(radianceTableStateListener);
                selectedIndices.clear();
                stateTransitionMultiTracker.clear();
                RadianceTableUI.this.syncSelection(true);
            }

            if ("columnModel".equals(propertyChangeEvent.getPropertyName())) {
                TableColumnModel old = (TableColumnModel) propertyChangeEvent.getOldValue();
                if (old != null) {
                    old.getSelectionModel()
                            .removeListSelectionListener(radianceTableStateListener);
                }
                table.getColumnModel().getSelectionModel()
                        .addListSelectionListener(radianceTableStateListener);
                selectedIndices.clear();
                stateTransitionMultiTracker.clear();
                RadianceTableUI.this.syncSelection(true);

                JTableHeader tableHeader = table.getTableHeader();
                // fix for issue 408 - table header may be null.
                if (tableHeader != null) {
                    // fix for issue 309 - syncing animations on tables
                    // and table headers.
                    RadianceTableHeaderUI headerUI = (RadianceTableHeaderUI) tableHeader.getUI();
                    headerUI.processColumnModelChangeEvent((TableColumnModel) propertyChangeEvent.getOldValue(),
                            (TableColumnModel) propertyChangeEvent.getNewValue());
                }
            }

            // fix for defect 243 - not tracking changes to selection
            // model results in incorrect selection painting on JXTreeTable
            // component from SwingX.
            if ("selectionModel".equals(propertyChangeEvent.getPropertyName())) {
                ListSelectionModel old = (ListSelectionModel) propertyChangeEvent.getOldValue();
                if (old != null) {
                    old.removeListSelectionListener(radianceTableStateListener);
                }
                table.getSelectionModel().addListSelectionListener(radianceTableStateListener);
                selectedIndices.clear();
                stateTransitionMultiTracker.clear();
                RadianceTableUI.this.syncSelection(true);
            }

            // fix for issue 479 - tracking sort / filter changes and
            // canceling selection animations
            if ("rowSorter".equals(propertyChangeEvent.getPropertyName())) {
                RowSorter old = (RowSorter) propertyChangeEvent.getOldValue();
                if (old != null) {
                    old.removeRowSorterListener(radianceTableStateListener);
                }
                RowSorter newSorter = (RowSorter) propertyChangeEvent.getNewValue();
                if (newSorter != null) {
                    newSorter.addRowSorterListener(radianceTableStateListener);
                }
                selectedIndices.clear();
                stateTransitionMultiTracker.clear();
                RadianceTableUI.this.syncSelection(true);
            }

            if ("font".equals(propertyChangeEvent.getPropertyName())) {
                SwingUtilities.invokeLater(() -> {
                    // fix for bug 341
                    if (table == null)
                        return;
                    table.updateUI();
                });
            }

            if ("background".equals(propertyChangeEvent.getPropertyName())) {
                // propagate application-specific background color to the
                // header.
                Color newBackgr = (Color) propertyChangeEvent.getNewValue();
                JTableHeader header = table.getTableHeader();
                if (header != null) {
                    Color headerBackground = header.getBackground();
                    if (RadianceCoreUtilities.canReplaceChildBackgroundColor(headerBackground)) {
                        if (!(newBackgr instanceof UIResource)) {
                            if (newBackgr == null) {
                                header.setBackground(null);
                            } else {
                                // Issue 450 - wrap the color in
                                // RadianceColorResource to
                                // mark that it can be replaced.
                                header.setBackground(new RadianceColorResource(newBackgr));
                            }
                        } else {
                            header.setBackground(newBackgr);
                        }
                    }
                }
            }

            // fix for issue 361 - track enabled status of the table
            // and propagate to the table header
            if ("enabled".equals(propertyChangeEvent.getPropertyName())) {
                JTableHeader header = table.getTableHeader();
                if (header != null) {
                    header.setEnabled(table.isEnabled());
                    header.repaint();
                }
            }

            if ("dropLocation".equals(propertyChangeEvent.getPropertyName())) {
                JTable.DropLocation oldValue = (JTable.DropLocation) propertyChangeEvent.getOldValue();
                if (oldValue != null) {
                    Rectangle oldRect = getCellRectangleForRepaint(oldValue.getRow(),
                            oldValue.getColumn());
                    table.repaint(oldRect);
                }
                JTable.DropLocation newValue = table.getDropLocation();
                if (newValue != null) {
                    Rectangle newRect = getCellRectangleForRepaint(table.getDropLocation().getRow(),
                            table.getDropLocation().getColumn());
                    table.repaint(newRect);
                }
            }

            if ("tableCellEditor".equals(propertyChangeEvent.getPropertyName())) {
                // fix for issue 481 - rollovers on cell editing
                TableCellEditor newEditor = (TableCellEditor) propertyChangeEvent.getNewValue();
                TableCellEditor oldEditor = (TableCellEditor) propertyChangeEvent.getOldValue();
                if (oldEditor != null) {
                    table.getEditorComponent().removeMouseListener(radianceFadeRolloverListener);
                }
                if (newEditor != null) {
                    table.getEditorComponent().addMouseListener(radianceFadeRolloverListener);
                }
            }
        };
        this.table.addPropertyChangeListener(this.radiancePropertyChangeListener);

        // Add listener for the selection animation
        this.radianceTableStateListener = new TableStateListener();
        this.table.getSelectionModel().addListSelectionListener(this.radianceTableStateListener);
        TableColumnModel columnModel = this.table.getColumnModel();
        columnModel.getSelectionModel().addListSelectionListener(this.radianceTableStateListener);
        this.table.getModel().addTableModelListener(this.radianceTableStateListener);
        if (this.table.getRowSorter() != null) {
            this.table.getRowSorter().addRowSorterListener(this.radianceTableStateListener);
        }

        // Add listener for the transition animation
        this.radianceFadeRolloverListener = new RolloverFadeListener();
        this.table.addMouseMotionListener(this.radianceFadeRolloverListener);
        this.table.addMouseListener(this.radianceFadeRolloverListener);

        // fix for issue 481 - tracking focus events on the table
        this.radianceFocusListener = new FocusListener() {
            @Override
            public void focusLost(FocusEvent e) {
                if (focusedCellId == null)
                    return;

                ComponentState cellState = getCellState(focusedCellId);
                StateTransitionTracker tracker = getTracker(focusedCellId,
                        cellState.isFacetActive(RadianceThemingSlices.ComponentStateFacet.ROLLOVER),
                        cellState.isFacetActive(RadianceThemingSlices.ComponentStateFacet.SELECTION));
                tracker.setFocusState(false);

                focusedCellId = null;
            }

            @Override
            public void focusGained(FocusEvent e) {
                int rowLead = table.getSelectionModel().getLeadSelectionIndex();
                int colLead = table.getColumnModel().getSelectionModel().getLeadSelectionIndex();
                if ((rowLead >= 0) && (colLead >= 0)) {
                    TableCellId toFocus = new TableCellId(rowLead, colLead);
                    if (toFocus.equals(focusedCellId))
                        return;
                    ComponentState cellState = getCellState(toFocus);
                    StateTransitionTracker tracker = getTracker(toFocus,
                            cellState.isFacetActive(RadianceThemingSlices.ComponentStateFacet.ROLLOVER),
                            cellState.isFacetActive(RadianceThemingSlices.ComponentStateFacet.SELECTION));
                    tracker.setFocusState(true);

                    focusedCellId = toFocus;
                }
            }
        };
        this.table.addFocusListener(this.radianceFocusListener);
    }

    @Override
    protected void uninstallListeners() {
        this.table.removePropertyChangeListener(this.radiancePropertyChangeListener);
        this.radiancePropertyChangeListener = null;

        this.table.getSelectionModel()
                .removeListSelectionListener(this.radianceTableStateListener);
        this.table.getColumnModel().getSelectionModel()
                .removeListSelectionListener(this.radianceTableStateListener);
        this.table.getModel().removeTableModelListener(this.radianceTableStateListener);
        if (this.table.getRowSorter() != null) {
            this.table.getRowSorter().removeRowSorterListener(this.radianceTableStateListener);
        }
        this.radianceTableStateListener = null;

        // Remove listener for the fade animation
        this.table.removeMouseMotionListener(this.radianceFadeRolloverListener);
        this.table.removeMouseListener(this.radianceFadeRolloverListener);
        this.radianceFadeRolloverListener = null;

        this.table.removeFocusListener(this.radianceFocusListener);
        this.radianceFocusListener = null;

        super.uninstallListeners();
    }

    /**
     * Paint a representation of the table instance that was set in installUI().
     */
    @Override
    public void paint(Graphics g, JComponent c) {
        Rectangle clip = g.getClipBounds();

        Rectangle bounds = this.table.getBounds();
        // account for the fact that the graphics has already been translated
        // into the table's bounds
        bounds.x = bounds.y = 0;

        if (this.table.getRowCount() <= 0 || this.table.getColumnCount() <= 0 ||
                // this check prevents us from painting the entire table
                // when the clip doesn't intersect our bounds at all
                !bounds.intersects(clip)) {

            return;
        }

        Point upperLeft = clip.getLocation();
        Point lowerRight = new Point(clip.x + clip.width - 1, clip.y + clip.height - 1);
        int rMin = this.table.rowAtPoint(upperLeft);
        int rMax = this.table.rowAtPoint(lowerRight);
        // This should never happen (as long as our bounds intersect the clip,
        // which is why we bail above if that is the case).
        if (rMin == -1) {
            rMin = 0;
        }
        // If the table does not have enough rows to fill the view we'll get -1.
        // (We could also get -1 if our bounds don't intersect the clip,
        // which is why we bail above if that is the case).
        // Replace this with the index of the last row.
        if (rMax == -1) {
            rMax = this.table.getRowCount() - 1;
        }

        boolean ltr = this.table.getComponentOrientation().isLeftToRight();
        int cMin = this.table.columnAtPoint(ltr ? upperLeft : lowerRight);
        int cMax = this.table.columnAtPoint(ltr ? lowerRight : upperLeft);
        // This should never happen.
        if (cMin == -1) {
            cMin = 0;
        }
        // If the table does not have enough columns to fill the view we'll get
        // -1.
        // Replace this with the index of the last column.
        if (cMax == -1) {
            cMax = this.table.getColumnCount() - 1;
        }

        // Paint the cells.
        this.paintCells(g, rMin, rMax, cMin, cMax);

        // Paint the grid.
        this.paintGrid(g, rMin, rMax, cMin, cMax);

        // Paint the drop lines
        this.paintDropLines(g);
    }

    /**
     * Paints the grid lines within aRect, using the grid color set with setGridColor.
     * Paints vertical lines if getShowVerticalLines() returns true and paints
     * horizontal lines if getShowHorizontalLines() returns true.
     */
    protected void paintGrid(Graphics g, int rMin, int rMax, int cMin, int cMax) {
        Graphics2D g2d = (Graphics2D) g.create();
        ComponentState currState = this.table.isEnabled() ? ComponentState.ENABLED
                : ComponentState.DISABLED_UNSELECTED;
        float alpha = RadianceColorSchemeUtilities.getAlpha(this.table, currState);
        g2d.setComposite(WidgetUtilities.getAlphaComposite(this.table, alpha, g));

        Color gridColor = this.table.getGridColor();
        if (gridColor instanceof UIResource) {
            gridColor = RadianceCoreUtilities.getSkin(this.table).getOverlayColor(
                    RadianceThemingSlices.ColorOverlayType.LINE,
                    DecorationPainterUtils.getDecorationType(this.table), currState);
            if (gridColor == null) {
                RadianceColorScheme scheme = RadianceColorSchemeUtilities.getColorScheme(this.table,
                        RadianceThemingSlices.ColorSchemeAssociationKind.BORDER,
                        this.table.isEnabled() ? ComponentState.ENABLED
                                : ComponentState.DISABLED_UNSELECTED);
                gridColor = scheme.getLineColor();
            }
        }
        g2d.setColor(gridColor);

        Rectangle minCell = this.table.getCellRect(rMin, cMin, true);
        Rectangle maxCell = this.table.getCellRect(rMax, cMax, true);
        Rectangle damagedArea = minCell.union(maxCell);

        float strokeWidth = RadianceSizeUtils.getBorderStrokeWidth(this.table);
        g2d.setStroke(new BasicStroke(strokeWidth, BasicStroke.CAP_ROUND, BasicStroke.JOIN_BEVEL));
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

        if (this.table.getShowHorizontalLines()) {
            int tableWidth = damagedArea.x + damagedArea.width;
            int y = damagedArea.y;
            for (int row = rMin; row <= rMax; row++) {
                y += this.table.getRowHeight(row);
                g2d.drawLine(damagedArea.x, y - 1, tableWidth - 1, y - 1);
            }
        }
        if (this.table.getShowVerticalLines()) {
            TableColumnModel cm = this.table.getColumnModel();
            int tableHeight = damagedArea.y + damagedArea.height;
            int x;
            if (this.table.getComponentOrientation().isLeftToRight()) {
                x = damagedArea.x;
                for (int column = cMin; column <= cMax; column++) {
                    int w = cm.getColumn(column).getWidth();
                    if (hasLeadingVerticalGridLine(cm, column)) {
                        g2d.drawLine(x, 0, x, tableHeight - 1);
                    }
                    x += w;
                    if (hasTrailingVerticalGridLine(cm, column)) {
                        g2d.drawLine(x - 1, 0, x - 1, tableHeight - 1);
                    }
                }
            } else {
                x = damagedArea.x + damagedArea.width;
                // fix for defect 196 - proper grid painting on RTL tables
                for (int column = cMin; column <= cMax; column++) {
                    int w = cm.getColumn(column).getWidth();
                    if (hasLeadingVerticalGridLine(cm, column)) {
                        g2d.drawLine(x - 1, 0, x - 1, tableHeight - 1);
                    }
                    x -= w;
                    if (hasTrailingVerticalGridLine(cm, column)) {
                        g2d.drawLine(x, 0, x, tableHeight - 1);
                    }
                }
            }
        }
        g2d.dispose();
    }

    private boolean hasTrailingVerticalGridLine(TableColumnModel cm, int column) {
        boolean toDrawLine = (column != (cm.getColumnCount() - 1));
        if (!toDrawLine) {
            Container parent = this.table.getParent();
            toDrawLine = (parent != null) && (parent.getWidth() > this.table.getWidth());
        }
        return toDrawLine;
    }

    private boolean hasLeadingVerticalGridLine(TableColumnModel cm, int column) {
        if (column != 0)
            return false;
        Container parent = this.table.getParent();
        if (parent instanceof JViewport) {
            Container grand = parent.getParent();
            if (grand instanceof JScrollPane) {
                return (((JScrollPane) grand).getRowHeader() != null);
            }
        }
        return false;
    }

    private int viewIndexForColumn(TableColumn aColumn) {
        TableColumnModel cm = this.table.getColumnModel();
        for (int column = 0; column < cm.getColumnCount(); column++) {
            if (cm.getColumn(column) == aColumn) {
                return column;
            }
        }
        return -1;
    }

    protected void paintCells(Graphics g, int rMin, int rMax, int cMin, int cMax) {
        JTableHeader header = this.table.getTableHeader();
        TableColumn draggedColumn = (header == null) ? null : header.getDraggedColumn();

        TableColumnModel cm = this.table.getColumnModel();
        int columnMargin = cm.getColumnMargin();
        int rowMargin = this.table.getRowMargin();

        Rectangle cellRect;
        Rectangle highlightCellRect;
        TableColumn aColumn;
        int columnWidth;
        if (this.table.getComponentOrientation().isLeftToRight()) {
            for (int row = rMin; row <= rMax; row++) {
                cellRect = this.table.getCellRect(row, cMin, false);

                highlightCellRect = new Rectangle(cellRect);
                highlightCellRect.y -= rowMargin / 2;
                highlightCellRect.height += rowMargin;

                for (int column = cMin; column <= cMax; column++) {
                    aColumn = cm.getColumn(column);
                    columnWidth = aColumn.getWidth();

                    cellRect.width = columnWidth - columnMargin;
                    highlightCellRect.x = cellRect.x - columnMargin / 2;
                    highlightCellRect.width = columnWidth;
                    if (!hasTrailingVerticalGridLine(cm, column)) {
                        cellRect.width++;
                        highlightCellRect.width++;
                    }

                    if (aColumn != draggedColumn) {
                        this.paintCell(g, cellRect, highlightCellRect, row, column);
                    }
                    cellRect.x += columnWidth;
                }
            }
        } else {
            for (int row = rMin; row <= rMax; row++) {
                cellRect = this.table.getCellRect(row, cMin, false);
                highlightCellRect = new Rectangle(cellRect);
                highlightCellRect.y -= rowMargin / 2;
                highlightCellRect.height += rowMargin;

                for (int column = cMin; column <= cMax; column++) {
                    aColumn = cm.getColumn(column);
                    columnWidth = aColumn.getWidth();
                    cellRect.width = columnWidth - columnMargin;

                    highlightCellRect.x = cellRect.x - columnMargin / 2;
                    highlightCellRect.width = columnWidth;
                    if (aColumn != draggedColumn) {
                        this.paintCell(g, cellRect, highlightCellRect, row, column);
                    }
                    cellRect.x -= columnWidth;
                }
            }
        }

        // Paint the dragged column if we are dragging.
        if (draggedColumn != null) {
            Graphics2D g2d = (Graphics2D) g.create();
            // enhancement 331 - translucent dragged column
            g2d.setComposite(WidgetUtilities.getAlphaComposite(this.table, 0.65f, g));
            this.paintDraggedArea(g2d, rMin, rMax, draggedColumn, header.getDraggedDistance());
            g2d.dispose();
        }

        // Remove any renderers that may be left in the rendererPane.
        this.rendererPane.removeAll();
    }

    protected void paintDraggedArea(Graphics g, int rMin, int rMax, TableColumn draggedColumn,
            int distance) {
        int draggedColumnIndex = this.viewIndexForColumn(draggedColumn);

        Rectangle minCell = this.table.getCellRect(rMin, draggedColumnIndex, true);
        Rectangle maxCell = this.table.getCellRect(rMax, draggedColumnIndex, true);

        Rectangle vacatedColumnRect = minCell.union(maxCell);

        // Paint a gray well in place of the moving column.
        g.setColor(this.table.getParent().getBackground());
        g.fillRect(vacatedColumnRect.x, vacatedColumnRect.y, vacatedColumnRect.width,
                vacatedColumnRect.height);

        // Move to the where the cell has been dragged.
        vacatedColumnRect.x += distance;

        // Fill the background.
        g.setColor(this.table.getBackground());
        g.fillRect(vacatedColumnRect.x, vacatedColumnRect.y, vacatedColumnRect.width,
                vacatedColumnRect.height);

        // Paint the vertical grid lines if necessary.
        if (this.table.getShowVerticalLines()) {
            g.setColor(this.table.getGridColor());
            int x1 = vacatedColumnRect.x;
            int y1 = vacatedColumnRect.y;
            int x2 = x1 + vacatedColumnRect.width - 1;
            int y2 = y1 + vacatedColumnRect.height - 1;
            // Left
            g.drawLine(x1 - 1, y1, x1 - 1, y2);
            // Right
            g.drawLine(x2, y1, x2, y2);
        }

        // Don't paint the dragged cell if dragged column index is negative (otherwise
        // it will crash in ColumnModel.getColumn)
        if (draggedColumnIndex >= 0) {
            for (int row = rMin; row <= rMax; row++) {
                // Render the cell value
                Rectangle r = this.table.getCellRect(row, draggedColumnIndex, false);
                r.x += distance;
                this.paintCell(g, r, r, row, draggedColumnIndex);

                // Paint the (lower) horizontal grid line if necessary.
                if (this.table.getShowHorizontalLines()) {
                    g.setColor(this.table.getGridColor());
                    Rectangle rcr = this.table.getCellRect(row, draggedColumnIndex, true);
                    rcr.x += distance;
                    int x1 = rcr.x;
                    int y1 = rcr.y;
                    int x2 = x1 + rcr.width - 1;
                    int y2 = y1 + rcr.height - 1;
                    g.drawLine(x1, y2, x2, y2);
                }
            }
        }
    }

    protected void paintCell(Graphics g, Rectangle cellRect, Rectangle highlightCellRect, int row,
            int column) {
        // System.out.println("Painting " + row + ":" + column);
        Component rendererComponent = null;
        if (!this.table.isEditing() || this.table.getEditingRow() != row
                || this.table.getEditingColumn() != column) {
            TableCellRenderer renderer = this.table.getCellRenderer(row, column);
            boolean isRadianceRenderer = isRadianceDefaultRenderer(renderer);
            rendererComponent = this.table.prepareRenderer(renderer, row, column);
            boolean isRadianceRendererComponent = isRadianceDefaultRenderer(rendererComponent);
            if (isRadianceRenderer && !isRadianceRendererComponent) {
                throw new IllegalArgumentException(
                        "Renderer extends the RadianceDefaultTableCellRenderer but does not return one in its getTableCellRendererComponent() method");
            }

            if (!isRadianceRenderer) {
                rendererPane.paintComponent(g, rendererComponent, table, cellRect.x, cellRect.y,
                        cellRect.width, cellRect.height, true);
                return;
            }
        }

        Graphics2D g2d = (Graphics2D) g.create();
        // fix for issue 183 - passing the original Graphics context
        // to compute the alpha composite. If the table is in a JXPanel
        // (component from SwingX) and it has custom alpha value set,
        // then the original graphics context will have a SRC_OVER
        // alpha composite applied to it.
        g2d.setComposite(WidgetUtilities.getAlphaComposite(this.table, g));

        TableCellId cellId = new TableCellId(row, column);

        StateTransitionTracker.ModelStateInfo modelStateInfo = this.getModelStateInfo(cellId);
        Map activeStates = ((modelStateInfo == null)
                ? null
                : modelStateInfo.getStateContributionMap());
        // optimize for tables that don't initiate rollover
        // or selection animations
        ComponentState currState = null;
        if (!updateInfo.hasRolloverAnimations && !updateInfo.hasSelectionAnimations) {
            activeStates = null;
            boolean isRollover = false;
            if (table.getRowSelectionAllowed()) {
                if (table.getColumnSelectionAllowed()) {
                    isRollover = (row == rolledOverRow) && (column == rolledOverColumn);
                } else {
                    isRollover = (row == rolledOverRow);
                }
            } else if (table.getColumnSelectionAllowed()) {
                isRollover = (column == rolledOverColumn);
            }
            boolean isSelected = table.isCellSelected(row, column);
            currState = isRollover
                    ? (isSelected ? ComponentState.ROLLOVER_SELECTED : ComponentState.ROLLOVER_UNSELECTED)
                    : (isSelected ? ComponentState.SELECTED : ComponentState.ENABLED);
        } else {
            currState = ((modelStateInfo == null) ? this.getCellState(cellId)
                    : modelStateInfo.getCurrModelState());
        }

        boolean hasHighlights = (currState != ComponentState.ENABLED) || (activeStates != null);
        if (activeStates != null) {
            for (Map.Entry stateEntry : activeStates
                    .entrySet()) {
                hasHighlights = (this.updateInfo.getHighlightAlpha(stateEntry.getKey())
                        * stateEntry.getValue().getContribution() > 0.0f);
                if (hasHighlights)
                    break;
            }
        } else {
            hasHighlights = (this.updateInfo.getHighlightAlpha(currState) > 0.0f);
        }

        Set highlightOpenSides = null;
        float highlightBorderAlpha = 0.0f;

        if (hasHighlights) {
            // compute the highlight visuals, but only if there are
            // highlights on this cell (optimization)
            highlightOpenSides = EnumSet.noneOf(RadianceThemingSlices.Side.class);
            // show highlight border only when the table grid is not shown
            highlightBorderAlpha = (table.getShowHorizontalLines() || table.getShowVerticalLines())
                    ? 0.0f
                    : 0.8f;
            if (!table.getColumnSelectionAllowed() && table.getRowSelectionAllowed()) {
                // if row selection is on and column selection is off, we
                // will show the highlight for the entire row

                // all cells have open left side
                highlightOpenSides.add(RadianceThemingSlices.Side.LEFT);
                // all cells have open right side
                highlightOpenSides.add(RadianceThemingSlices.Side.RIGHT);
            }
            if (table.getColumnSelectionAllowed() && !table.getRowSelectionAllowed()) {
                // if row selection is off and column selection is on, we
                // will show the highlight for the entire column

                // the top side is open for all rows except the
                // first, or when the table header is visible
                highlightOpenSides.add(RadianceThemingSlices.Side.TOP);
                // all cells but the last have open bottom side
                highlightOpenSides.add(RadianceThemingSlices.Side.BOTTOM);
            }
            if (row > 1) {
                ComponentState upperNeighbourState = this
                        .getCellState(new TableCellId(row - 1, column));
                if (currState == upperNeighbourState) {
                    // the cell above it is in the same state
                    highlightOpenSides.add(RadianceThemingSlices.Side.TOP);
                }
            }
            if (column > 1) {
                ComponentState leftNeighbourState = this
                        .getCellState(new TableCellId(row, column - 1));
                if (currState == leftNeighbourState) {
                    // the cell to the left is in the same state
                    highlightOpenSides.add(RadianceThemingSlices.Side.LEFT);
                }
            }
            if (row == 0) {
                highlightOpenSides.add(RadianceThemingSlices.Side.TOP);
            }
            if (row == (table.getRowCount() - 1)) {
                highlightOpenSides.add(RadianceThemingSlices.Side.BOTTOM);
            }
            if (column == 0) {
                highlightOpenSides.add(RadianceThemingSlices.Side.LEFT);
            }
            if (column == (table.getColumnCount() - 1)) {
                highlightOpenSides.add(RadianceThemingSlices.Side.RIGHT);
            }
        }

        boolean isRollover = this.rolledOverIndices.contains(cellId);
        if (this.table.isEditing() && this.table.getEditingRow() == row
                && this.table.getEditingColumn() == column) {
            Component component = this.table.getEditorComponent();
            component.applyComponentOrientation(this.table.getComponentOrientation());

            if (hasHighlights) {
                float extra = RadianceSizeUtils.getBorderStrokeWidth(this.table);
                float extraWidth = highlightOpenSides.contains(RadianceThemingSlices.Side.LEFT) ? 0.0f
                        : extra;
                float extraHeight = highlightOpenSides.contains(RadianceThemingSlices.Side.TOP) ? 0.0f
                        : extra;
                Rectangle highlightRect = new Rectangle(highlightCellRect.x - (int) extraWidth,
                        highlightCellRect.y - (int) extraHeight,
                        highlightCellRect.width + (int) extraWidth,
                        highlightCellRect.height + (int) extraHeight);
                if (activeStates == null) {
                    float alpha = this.updateInfo.getHighlightAlpha(currState);
                    if (alpha > 0.0f) {
                        RadianceColorScheme fillScheme = this.updateInfo
                                .getHighlightColorScheme(currState);
                        RadianceColorScheme borderScheme = this.updateInfo
                                .getHighlightBorderColorScheme(currState);
                        g2d.setComposite(WidgetUtilities.getAlphaComposite(this.table, alpha, g));
                        HighlightPainterUtils.paintHighlight(g2d, this.rendererPane, component,
                                highlightRect, highlightBorderAlpha, highlightOpenSides, fillScheme,
                                borderScheme);
                        g2d.setComposite(WidgetUtilities.getAlphaComposite(this.table, g));
                    }
                } else {
                    for (Map.Entry stateEntry : activeStates
                            .entrySet()) {
                        ComponentState activeState = stateEntry.getKey();
                        float alpha = this.updateInfo.getHighlightAlpha(activeState)
                                * stateEntry.getValue().getContribution();
                        if (alpha == 0.0f)
                            continue;
                        RadianceColorScheme fillScheme = this.updateInfo
                                .getHighlightColorScheme(activeState);
                        RadianceColorScheme borderScheme = this.updateInfo
                                .getHighlightBorderColorScheme(activeState);
                        g2d.setComposite(WidgetUtilities.getAlphaComposite(this.table, alpha, g));
                        HighlightPainterUtils.paintHighlight(g2d, this.rendererPane, component,
                                highlightRect, highlightBorderAlpha, highlightOpenSides, fillScheme,
                                borderScheme);
                        g2d.setComposite(WidgetUtilities.getAlphaComposite(this.table, g));
                    }
                }
            }

            component.setBounds(cellRect);
            component.validate();
        } else {
            if (rendererComponent != null) {
                Color background = rendererComponent.getBackground();
                // optimization - only render background if it's different
                // from the table background
                if ((background != null) && (!table.getBackground().equals(background)
                        || this.updateInfo.isInDecorationArea)) {
                    // fill with the renderer background color
                    g2d.setColor(background);
                    g2d.fillRect(highlightCellRect.x, highlightCellRect.y,
                            highlightCellRect.width, highlightCellRect.height);
                }
            }

            if (hasHighlights) {
                JTable.DropLocation dropLocation = table.getDropLocation();
                if (dropLocation != null && !dropLocation.isInsertRow()
                        && !dropLocation.isInsertColumn() && dropLocation.getRow() == row
                        && dropLocation.getColumn() == column) {
                    // mark drop location
                    RadianceColorScheme scheme = RadianceColorSchemeUtilities.getColorScheme(
                            table, RadianceThemingSlices.ColorSchemeAssociationKind.HIGHLIGHT, currState);
                    RadianceColorScheme borderScheme = RadianceColorSchemeUtilities
                            .getColorScheme(table, RadianceThemingSlices.ColorSchemeAssociationKind.HIGHLIGHT_BORDER,
                                    currState);
                    float extra = RadianceSizeUtils.getBorderStrokeWidth(table);
                    HighlightPainterUtils.paintHighlight(g2d, this.rendererPane, rendererComponent,
                            new Rectangle(highlightCellRect.x - (int) extra,
                                    highlightCellRect.y - (int) extra,
                                    highlightCellRect.width + (int) extra,
                                    highlightCellRect.height + (int) extra),
                            0.8f, null, scheme, borderScheme);
                } else {
                    float extra = RadianceSizeUtils.getBorderStrokeWidth(table);
                    float extraWidth = highlightOpenSides.contains(RadianceThemingSlices.Side.LEFT) ? 0.0f
                            : extra;
                    float extraHeight = highlightOpenSides.contains(RadianceThemingSlices.Side.TOP) ? 0.0f
                            : extra;
                    Rectangle highlightRect = new Rectangle(highlightCellRect.x - (int) extraWidth,
                            highlightCellRect.y - (int) extraHeight,
                            highlightCellRect.width + (int) extraWidth,
                            highlightCellRect.height + (int) extraHeight);
                    if (activeStates == null) {
                        RadianceColorScheme fillScheme = this.updateInfo
                                .getHighlightColorScheme(currState);
                        RadianceColorScheme borderScheme = this.updateInfo
                                .getHighlightBorderColorScheme(currState);
                        float alpha = this.updateInfo.getHighlightAlpha(currState);
                        if (alpha > 0.0f) {
                            g2d.setComposite(
                                    WidgetUtilities.getAlphaComposite(this.table, alpha, g));
                            HighlightPainterUtils.paintHighlight(g2d, this.rendererPane,
                                    rendererComponent, highlightRect, highlightBorderAlpha,
                                    highlightOpenSides, fillScheme, borderScheme);
                            g2d.setComposite(WidgetUtilities.getAlphaComposite(this.table, g));
                        }
                    } else {
                        for (Map.Entry stateEntry : activeStates
                                .entrySet()) {
                            ComponentState activeState = stateEntry.getKey();
                            RadianceColorScheme fillScheme = this.updateInfo
                                    .getHighlightColorScheme(activeState);
                            RadianceColorScheme borderScheme = this.updateInfo
                                    .getHighlightBorderColorScheme(activeState);
                            float alpha = this.updateInfo.getHighlightAlpha(activeState)
                                    * stateEntry.getValue().getContribution();
                            if (alpha > 0.0f) {
                                g2d.setComposite(
                                        WidgetUtilities.getAlphaComposite(this.table, alpha, g));
                                HighlightPainterUtils.paintHighlight(g2d, this.rendererPane,
                                        rendererComponent, highlightRect, highlightBorderAlpha,
                                        highlightOpenSides, fillScheme, borderScheme);
                                g2d.setComposite(WidgetUtilities.getAlphaComposite(this.table, g));
                            }
                        }
                    }
                }
            }

            rendererComponent.applyComponentOrientation(this.table.getComponentOrientation());
            if (rendererComponent instanceof JComponent) {
                // Play with opacity to make our own gradient background
                // on selected elements to show.
                JComponent jRenderer = (JComponent) rendererComponent;
                // Compute the selection status to prevent flicker - JTable
                // registers a listener on selection changes and repaints
                // the relevant cell before our listener (in TableUI) gets
                // the chance to start the fade sequence. The result is that
                // the first frame uses full opacity, and the next frame
                // starts the fade sequence. So, we use the UI delegate to
                // compute the selection status.
                boolean isSelected = updateInfo.hasSelectionAnimations
                        ? this.selectedIndices.containsKey(cellId)
                        : this.table.isCellSelected(row, column);
                boolean newOpaque = !(isSelected || isRollover || hasHighlights);

                Map opacity = new HashMap<>();
                if (!newOpaque)
                    RadianceCoreUtilities.makeNonOpaque(jRenderer, opacity);
                this.rendererPane.paintComponent(g2d, rendererComponent, this.table, cellRect.x,
                        cellRect.y, cellRect.width, cellRect.height, true);
                if (!newOpaque)
                    RadianceCoreUtilities.restoreOpaque(jRenderer, opacity);
            } else {
                this.rendererPane.paintComponent(g2d, rendererComponent, this.table, cellRect.x,
                        cellRect.y, cellRect.width, cellRect.height, true);
            }
        }
        g2d.dispose();
    }

    protected void paintDropLines(Graphics g) {
        JTable.DropLocation loc = table.getDropLocation();
        if (loc == null) {
            return;
        }

        Color color = UIManager.getColor("Table.dropLineColor");
        Color shortColor = UIManager.getColor("Table.dropLineShortColor");
        if (color == null && shortColor == null) {
            return;
        }

        Rectangle rect;

        rect = getHDropLineRect(loc);
        if (rect != null) {
            int x = rect.x;
            int w = rect.width;
            if (color != null) {
                extendRect(rect, true);
                g.setColor(color);
                g.fillRect(rect.x, rect.y, rect.width, rect.height);
            }
            if (!loc.isInsertColumn() && shortColor != null) {
                g.setColor(shortColor);
                g.fillRect(x, rect.y, w, rect.height);
            }
        }

        rect = getVDropLineRect(loc);
        if (rect != null) {
            int y = rect.y;
            int h = rect.height;
            if (color != null) {
                extendRect(rect, false);
                g.setColor(color);
                g.fillRect(rect.x, rect.y, rect.width, rect.height);
            }
            if (!loc.isInsertRow() && shortColor != null) {
                g.setColor(shortColor);
                g.fillRect(rect.x, y, rect.width, h);
            }
        }
    }

    private Rectangle getHDropLineRect(JTable.DropLocation loc) {
        if (!loc.isInsertRow()) {
            return null;
        }

        int row = loc.getRow();
        int col = loc.getColumn();
        if (col >= table.getColumnCount()) {
            col--;
        }

        Rectangle rect = table.getCellRect(row, col, true);

        if (row >= table.getRowCount()) {
            row--;
            Rectangle prevRect = table.getCellRect(row, col, true);
            rect.y = prevRect.y + prevRect.height;
        }

        if (rect.y == 0) {
            rect.y = -1;
        } else {
            rect.y -= 2;
        }

        rect.height = 3;

        return rect;
    }

    private Rectangle getVDropLineRect(JTable.DropLocation loc) {
        if (!loc.isInsertColumn()) {
            return null;
        }

        boolean ltr = table.getComponentOrientation().isLeftToRight();
        int col = loc.getColumn();
        Rectangle rect = table.getCellRect(loc.getRow(), col, true);

        if (col >= table.getColumnCount()) {
            col--;
            rect = table.getCellRect(loc.getRow(), col, true);
            if (ltr) {
                rect.x = rect.x + rect.width;
            }
        } else if (!ltr) {
            rect.x = rect.x + rect.width;
        }

        if (rect.x == 0) {
            rect.x = -1;
        } else {
            rect.x -= 2;
        }

        rect.width = 3;

        return rect;
    }

    private Rectangle extendRect(Rectangle rect, boolean horizontal) {
        if (rect == null) {
            return rect;
        }

        if (horizontal) {
            rect.x = 0;
            rect.width = table.getWidth();
        } else {
            rect.y = 0;

            if (table.getRowCount() != 0) {
                Rectangle lastRect = table.getCellRect(table.getRowCount() - 1, 0, true);
                rect.height = lastRect.y + lastRect.height;
            } else {
                rect.height = table.getHeight();
            }
        }

        return rect;
    }

    /**
     * Repaints a single cell during the fade animation cycle.
     *
     * @author Kirill Grouchnikov
     */
    protected class CellRepaintCallback extends EventDispatchThreadTimelineCallbackAdapter {
        /**
         * Associated table.
         */
        protected JTable table;

        /**
         * Associated (animated) row index.
         */
        protected int rowIndex;

        /**
         * Associated (animated) column index.
         */
        protected int columnIndex;

        /**
         * Creates a new animation repaint callback.
         *
         * @param table       Associated table.
         * @param rowIndex    Associated (animated) row index.
         * @param columnIndex Associated (animated) column index.
         */
        public CellRepaintCallback(JTable table, int rowIndex, int columnIndex) {
            super();
            this.table = table;
            this.rowIndex = rowIndex;
            this.columnIndex = columnIndex;
        }

        @Override
        public void onTimelinePulse(float durationFraction, float timelinePosition) {
            this.repaintCell();
        }

        @Override
        public void onTimelineStateChanged(TimelineState oldState, TimelineState newState,
                float durationFraction, float timelinePosition) {
            this.repaintCell();
        }

        /**
         * Repaints the associated cell.
         */
        private void repaintCell() {
            SwingUtilities.invokeLater(() -> {
                if (RadianceTableUI.this.table == null) {
                    // may happen if the LAF was switched in the meantime
                    return;
                }
                int rowCount = table.getRowCount();
                int colCount = table.getColumnCount();
                if ((rowCount > 0) && (rowIndex < rowCount) && (colCount > 0)
                        && (columnIndex < colCount)) {
                    // need to retrieve the cell rectangle since the cells
                    // can be moved while animating
                    Rectangle rect = getCellRectangleForRepaint(rowIndex, columnIndex);
                    // System.out.println("Cell Repainting " + rowIndex +
                    // ":"
                    // + columnIndex + ":" + rect);
                    CellRepaintCallback.this.table.repaint(rect);
                }
            });
        }
    }

    /**
     * Repaints a single row during the fade animation cycle.
     *
     * @author Kirill Grouchnikov
     */
    protected class RowRepaintCallback extends EventDispatchThreadTimelineCallbackAdapter {
        /**
         * Associated table.
         */
        protected JTable table;

        /**
         * Associated (animated) row index.
         */
        protected int rowIndex;

        /**
         * Creates a new animation repaint callback.
         *
         * @param table    Associated table.
         * @param rowIndex Associated (animated) row index.
         */
        public RowRepaintCallback(JTable table, int rowIndex) {
            super();
            this.table = table;
            this.rowIndex = rowIndex;
        }

        @Override
        public void onTimelinePulse(float durationFraction, float timelinePosition) {
            this.repaintRow();
        }

        @Override
        public void onTimelineStateChanged(TimelineState oldState, TimelineState newState,
                float durationFraction, float timelinePosition) {
            this.repaintRow();
        }

        /**
         * Repaints the associated row.
         */
        private void repaintRow() {
            SwingUtilities.invokeLater(() -> {
                if (RadianceTableUI.this.table == null) {
                    // may happen if the LAF was switched in the meantime
                    return;
                }
                int rowCount = RowRepaintCallback.this.table.getRowCount();
                if ((rowCount > 0) && (RowRepaintCallback.this.rowIndex < rowCount)) {
                    // need to retrieve the cell rectangle since the cells
                    // can be moved while animating
                    Rectangle rect = RowRepaintCallback.this.table
                            .getCellRect(RowRepaintCallback.this.rowIndex, 0, true);
                    for (int i = 1; i < RowRepaintCallback.this.table.getColumnCount(); i++) {
                        rect = rect.union(RowRepaintCallback.this.table
                                .getCellRect(RowRepaintCallback.this.rowIndex, i, true));
                    }
                    if (!table.getShowHorizontalLines() && !table.getShowVerticalLines()) {
                        float extra = RadianceSizeUtils.getBorderStrokeWidth(this.table);
                        rect.y -= (int) extra;
                        rect.height += 2 * (int) extra;
                    }
                    // System.out.println("Repainting row " + rowIndex
                    // + " at " + rect);
                    RowRepaintCallback.this.table.repaint(rect);
                }
            });
        }
    }

    /**
     * Repaints a single column during the fade animation cycle.
     *
     * @author Kirill Grouchnikov
     */
    protected class ColumnRepaintCallback extends EventDispatchThreadTimelineCallbackAdapter {
        /**
         * Associated table.
         */
        protected JTable table;

        /**
         * Associated (animated) column index.
         */
        protected int columnIndex;

        /**
         * Creates a new animation repaint callback.
         *
         * @param table       Associated table.
         * @param columnIndex Associated (animated) column index.
         */
        public ColumnRepaintCallback(JTable table, int columnIndex) {
            super();
            this.table = table;
            this.columnIndex = columnIndex;
        }

        @Override
        public void onTimelinePulse(float durationFraction, float timelinePosition) {
            this.repaintColumn();
        }

        @Override
        public void onTimelineStateChanged(TimelineState oldState, TimelineState newState,
                float durationFraction, float timelinePosition) {
            this.repaintColumn();
        }

        /**
         * Repaints the associated row.
         */
        private void repaintColumn() {
            SwingUtilities.invokeLater(() -> {
                if (RadianceTableUI.this.table == null) {
                    // may happen if the LAF was switched in the meantime
                    return;
                }
                int columnCount = ColumnRepaintCallback.this.table.getColumnCount();
                if ((columnCount > 0) && (ColumnRepaintCallback.this.columnIndex < columnCount)) {
                    // need to retrieve the cell rectangle since the cells
                    // can be moved while animating
                    Rectangle rect = ColumnRepaintCallback.this.table.getCellRect(0,
                            ColumnRepaintCallback.this.columnIndex, true);
                    for (int i = 1; i < ColumnRepaintCallback.this.table.getRowCount(); i++) {
                        rect = rect.union(ColumnRepaintCallback.this.table.getCellRect(i,
                                ColumnRepaintCallback.this.columnIndex, true));
                    }
                    if (!table.getShowHorizontalLines() && !table.getShowVerticalLines()) {
                        float extra = RadianceSizeUtils.getBorderStrokeWidth(this.table);
                        rect.x -= (int) extra;
                        rect.width += 2 * (int) extra;
                    }
                    ColumnRepaintCallback.this.table.repaint(rect);
                }
            });
        }
    }

    /**
     * ID of a single table cell.
     *
     * @author Kirill Grouchnikov
     */
    public static class TableCellId implements Comparable {
        /**
         * Cell row.
         */
        protected int row;

        /**
         * Cell column.
         */
        protected int column;

        /**
         * Creates a new cell ID.
         *
         * @param row    Cell row.
         * @param column Cell column.
         */
        public TableCellId(int row, int column) {
            this.row = row;
            this.column = column;
        }

        @Override
        public int compareTo(TableCellId o) {
            if (row == o.row) {
                return Integer.compare(column, o.column);
            } else {
                return Integer.compare(row, o.row);
            }
        }

        @Override
        public boolean equals(Object obj) {
            if (obj instanceof TableCellId) {
                return this.compareTo((TableCellId) obj) == 0;
            }
            return false;
        }

        @Override
        public int hashCode() {
            return (this.row ^ (this.row << 16)) & (this.column ^ (this.column << 16));
        }

        @Override
        public String toString() {
            return "Row " + this.row + ", Column " + this.column;
        }
    }

    /**
     * State listener for tracking the selection changes.
     *
     * @author Kirill Grouchnikov
     */
    protected class TableStateListener
            implements ListSelectionListener, TableModelListener, RowSorterListener {
        List oldSortKeys = null;

        private boolean isSameSorter(List sortKeys1,
                List sortKeys2) {
            int size1 = (sortKeys1 == null) ? 0 : sortKeys1.size();
            int size2 = (sortKeys2 == null) ? 0 : sortKeys2.size();
            if ((size1 == 0) && (size2 == 0)) {
                return true;
            }
            if ((sortKeys1 == null) && (sortKeys2 == null))
                return true;
            if ((sortKeys1 == null) || (sortKeys2 == null))
                return false;
            if (size1 != size2)
                return false;
            for (int i = 0; i < size1; i++) {
                SortKey sortKey1 = sortKeys1.get(i);
                SortKey sortKey2 = sortKeys2.get(i);
                if ((sortKey1.getColumn() != sortKey2.getColumn())
                        || (sortKey1.getSortOrder() != sortKey2.getSortOrder())) {
                    return false;
                }
            }
            return true;
        }

        @Override
        public void valueChanged(final ListSelectionEvent e) {
            // fix for issue 478 - no animations when sorter has changed
            List sortKeys = (table.getRowSorter() == null) ? null
                    : table.getRowSorter().getSortKeys();
            boolean isDifferentSorter = !isSameSorter(sortKeys, oldSortKeys);
            if (e.getValueIsAdjusting() && isDifferentSorter)
                return;
            if (sortKeys == null) {
                oldSortKeys = null;
            } else {
                oldSortKeys = new ArrayList<>();
                for (SortKey sortKey : sortKeys) {
                    SortKey copy = new SortKey(sortKey.getColumn(), sortKey.getSortOrder());
                    oldSortKeys.add(copy);
                }
            }
            syncSelection(isDifferentSorter);
        }

        @Override
        public void tableChanged(final TableModelEvent e) {
            // fix for defect 291 - tracking changes to the table.
            SwingUtilities.invokeLater(() -> {
                // fix for defect 350 - font might have been
                // switched in the middle of update
                if (table == null)
                    return;

                // fix for defect 328 - do not clear the
                // internal selection and focus tracking
                // when the event is table update.
                if (e.getType() != TableModelEvent.UPDATE) {
                    selectedIndices.clear();
                    stateTransitionMultiTracker.clear();
                    focusedCellId = null;
                }
                syncSelection(true);
                table.repaint();
            });
        }

        @Override
        public void sorterChanged(RowSorterEvent e) {
            // fix for issue 479 - cancel selection animations
            // that are happening due to changes in sorter
            SwingUtilities.invokeLater(stateTransitionMultiTracker::clear);
        }
    }

    /**
     * Listener for fade animations on table rollovers.
     *
     * @author Kirill Grouchnikov
     */
    private class RolloverFadeListener implements MouseListener, MouseMotionListener {
        private int lastRow = -1;
        private int lastColumn = -1;

        public void mouseClicked(MouseEvent e) {
        }

        public void mouseEntered(MouseEvent e) {
        }

        public void mousePressed(MouseEvent e) {
        }

        public void mouseReleased(MouseEvent e) {
        }

        public void mouseExited(MouseEvent e) {
            if (table == null)
                return;

            if (!table.isEnabled())
                return;

            // check the mouse location. If the cell editor has been shown
            // or hidden, we will get a mouseExited() event, but shouldn't
            // be changing the rollover indication if the mouse is still
            // over the table
            PointerInfo pi = MouseInfo.getPointerInfo();
            Point mouseLoc = pi != null ? pi.getLocation() : null;
            Window windowAncestor = SwingUtilities.getWindowAncestor(table);
            if ((mouseLoc != null) && (windowAncestor != null)) {
                SwingUtilities.convertPointFromScreen(mouseLoc, windowAncestor);
                Component deepest = SwingUtilities.getDeepestComponentAt(windowAncestor, mouseLoc.x,
                        mouseLoc.y);

                while (deepest != null) {
                    if (deepest == table) {
                        // still in table
                        return;
                    }
                    deepest = deepest.getParent();
                }
            }

            fadeOutAllRollovers();
            this.fadeOutTableHeader();
            rolledOverIndices.clear();
            rolledOverColumn = -1;
            rolledOverRow = -1;
            lastRow = -1;
            lastColumn = -1;

            if (!_hasRolloverAnimations()) {
                table.repaint();
            }
        }

        public void mouseMoved(MouseEvent e) {
            if (!RadianceTableUI.this.table.isEnabled())
                return;
            handleMouseMove(e.getPoint());
            this.handleMoveForHeader(e);
        }

        public void mouseDragged(MouseEvent e) {
            if (!RadianceTableUI.this.table.isEnabled())
                return;
            handleMouseMove(e.getPoint());
            this.handleMoveForHeader(e);
        }

        /**
         * Handles various mouse move events and initiates the fade animation if necessary.
         *
         * @param e Mouse event.
         */
        private void handleMoveForHeader(MouseEvent e) {
            if (!RadianceTableUI.this.table.getColumnSelectionAllowed())
                return;
            JTableHeader header = RadianceTableUI.this.table.getTableHeader();
            if ((header == null) || (!header.isVisible()))
                return;

            TableHeaderUI ui = header.getUI();
            if (!(ui instanceof RadianceTableHeaderUI))
                return;

            RadianceTableHeaderUI sthui = (RadianceTableHeaderUI) ui;

            int row = RadianceTableUI.this.table.rowAtPoint(e.getPoint());
            int column = RadianceTableUI.this.table.columnAtPoint(e.getPoint());
            if ((row < 0) || (row >= RadianceTableUI.this.table.getRowCount()) || (column < 0)
                    || (column >= RadianceTableUI.this.table.getColumnCount())) {
                this.fadeOutTableHeader();
                // System.out.println("Nulling RO column index");
                RadianceTableUI.this.rolledOverColumn = -1;
            } else {
                // check if this is the same column
                if (RadianceTableUI.this.rolledOverColumn == column)
                    return;

                this.fadeOutTableHeader();

                TableColumnModel columnModel = header.getColumnModel();
                StateTransitionTracker columnTransitionTracker = sthui.getTracker(column, false,
                        columnModel.getColumnSelectionAllowed()
                                && columnModel.getSelectionModel().isSelectedIndex(column));
                columnTransitionTracker.getModel().setRollover(true);

                RadianceTableUI.this.rolledOverColumn = column;
            }
            // }
        }

        /**
         * Initiates the fade out effect.
         */
        private void fadeOutTableHeader() {
            if (RadianceTableUI.this.rolledOverColumn >= 0) {
                JTableHeader header = RadianceTableUI.this.table.getTableHeader();
                if ((header == null) || (!header.isVisible()))
                    return;
                RadianceTableHeaderUI ui = (RadianceTableHeaderUI) header.getUI();

                TableColumnModel columnModel = header.getColumnModel();
                StateTransitionTracker columnTransitionTracker = ui.getTracker(rolledOverColumn,
                        true, columnModel.getColumnSelectionAllowed() && columnModel
                                .getSelectionModel().isSelectedIndex(rolledOverColumn));
                columnTransitionTracker.getModel().setRollover(false);
            }
        }

        /**
         * Handles various mouse move events and initiates the fade animation if necessary.
         *
         * @param mousePoint Mouse event location.
         */
        private void handleMouseMove(Point mousePoint) {
            int row = table.rowAtPoint(mousePoint);
            int column = table.columnAtPoint(mousePoint);

            if ((row == this.lastRow) && (column == this.lastColumn)) {
                return;
            }
            this.lastRow = row;
            this.lastColumn = column;

            rolledOverRow = row;

            boolean hasRolloverAnimations = _hasRolloverAnimations();
            if (!hasRolloverAnimations) {
                table.repaint();
                return;
            }

            if ((row < 0) || (row >= table.getRowCount()) || (column < 0)
                    || (column >= table.getColumnCount())) {
                this.fadeOutAllRollovers();
                rolledOverIndices.clear();
            } else {
                // check if this is the same index
                boolean hasRowSelection = table.getRowSelectionAllowed();
                boolean hasColumnSelection = table.getColumnSelectionAllowed();
                int startRolloverRow = row;
                int endRolloverRow = row;
                int startRolloverColumn = column;
                int endRolloverColumn = column;
                if (hasRowSelection && !hasColumnSelection) {
                    startRolloverColumn = 0;
                    endRolloverColumn = table.getColumnCount() - 1;
                }
                if (!hasRowSelection && hasColumnSelection) {
                    startRolloverRow = 0;
                    endRolloverRow = table.getRowCount() - 1;
                }
                Set toRemove = new HashSet<>();
                for (TableCellId currRolloverId : rolledOverIndices) {
                    if ((currRolloverId.row < startRolloverRow)
                            || (currRolloverId.row > endRolloverRow)
                            || (currRolloverId.column < startRolloverColumn)
                            || (currRolloverId.column > endRolloverColumn)) {
                        fadeOutRollover(currRolloverId);
                        toRemove.add(currRolloverId);
                    }
                }
                for (TableCellId id : toRemove) {
                    rolledOverIndices.remove(id);
                }

                int totalRolloverCount = (endRolloverRow - startRolloverRow + 1)
                        * (endRolloverColumn - startRolloverColumn + 1);
                if (totalRolloverCount > 20) {
                    for (int i = startRolloverRow; i <= endRolloverRow; i++) {
                        for (int j = startRolloverColumn; j <= endRolloverColumn; j++) {
                            rolledOverIndices.add(new TableCellId(i, j));
                        }
                    }
                    table.repaint();
                } else {
                    for (int i = startRolloverRow; i <= endRolloverRow; i++) {
                        for (int j = startRolloverColumn; j <= endRolloverColumn; j++) {
                            TableCellId currCellId = new TableCellId(i, j);
                            if (rolledOverIndices.contains(currCellId))
                                continue;
                            // System.out
                            // .println("Getting rollover/in tracker for "
                            // + currCellId);
                            StateTransitionTracker tracker = getTracker(currCellId, false,
                                    getCellState(currCellId)
                                            .isFacetActive(RadianceThemingSlices.ComponentStateFacet.SELECTION));
                            tracker.getModel().setRollover(true);

                            rolledOverIndices.add(currCellId);
                        }
                    }
                }
            }
        }

        /**
         * Initiates the fade out effect.
         */
        private void fadeOutRollover(TableCellId tableCellId) {
            if (rolledOverIndices.contains(tableCellId)) {
                // System.out
                // .println("Getting rollover/out tracker for " + cellId);
                StateTransitionTracker tracker = getTracker(tableCellId, true,
                        getCellState(tableCellId).isFacetActive(RadianceThemingSlices.ComponentStateFacet.SELECTION));
                tracker.getModel().setRollover(false);
            }
        }

        private void fadeOutAllRollovers() {
            if (rolledOverIndices.size() < 20) {
                for (TableCellId tcid : rolledOverIndices) {
                    fadeOutRollover(tcid);
                }
            }
        }
    }

    /**
     * Returns a comparable ID for the specified location.
     *
     * @param row    Row index.
     * @param column Column index.
     * @return Comparable ID for the specified location.
     */
    public TableCellId getId(int row, int column) {
        cellId.column = column;
        cellId.row = row;
        return cellId;
    }

    TableCellId cellId;

    /**
     * Synchronizes the current selection state.
     */
    // @SuppressWarnings("unchecked")
    protected void syncSelection(boolean enforceNoAnimations) {
        if (this.table == null) {
            // fix for defect 270 - if the UI delegate is updated
            // by another selection listener, ignore this
            return;
        }

        int rows = this.table.getRowCount();
        int cols = this.table.getColumnCount();

        int rowLeadIndex = this.table.getSelectionModel().getLeadSelectionIndex();
        int colLeadIndex = this.table.getColumnModel().getSelectionModel().getLeadSelectionIndex();
        boolean isFocusOwner = this.table.isFocusOwner();

        // fix for defect 209 - selection very slow on large tables with
        // column selection set to true and row selection set to false.
        // Solution - no selection animations on tables with more than 1000
        // cells.
        if (!this._hasSelectionAnimations()) {
            stateTransitionMultiTracker.clear();
            // this.prevStateMap.clear();
            table.repaint();

            // fix for issue 414 - track focus on tables
            // without selection animations
            if (isFocusOwner) {
                this.focusedCellId = new TableCellId(rowLeadIndex, colLeadIndex);
            }
            return;
        }

        Set initiatedTrackers = new HashSet<>();

        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                TableCellId cellId = new TableCellId(i, j);
                if (this.table.isCellSelected(i, j)) {
                    // check if was selected before
                    if (!this.selectedIndices.containsKey(cellId)) {
                        // start fading in
                        if (!enforceNoAnimations) {
                            // System.out
                            // .println("Getting selection/in tracker for "
                            // + cellId);
                            StateTransitionTracker tracker = getTracker(cellId, getCellState(cellId)
                                    .isFacetActive(RadianceThemingSlices.ComponentStateFacet.ROLLOVER), false);
                            tracker.getModel().setSelected(true);
                            // System.out
                            // .println("Selecting previously unselected "
                            // + i + ":" + j);
                            initiatedTrackers.add(tracker);
                            if (initiatedTrackers.size() > 20) {
                                stateTransitionMultiTracker.clear();
                                initiatedTrackers.clear();
                                enforceNoAnimations = true;
                            }
                        }

                        this.selectedIndices.put(cellId, this.table.getValueAt(i, j));
                    }
                } else {
                    // check if was selected before and still points
                    // to the same element
                    if (this.selectedIndices.containsKey(cellId)) {
                        // corner case when the model returns null
                        Object oldValue = this.selectedIndices.get(cellId);
                        if ((i >= this.table.getModel().getRowCount())
                                || (j >= this.table.getModel().getColumnCount())) {
                            // not only the content changed, but the model
                            // dimensions as well
                            continue;
                        }
                        Object currValue = this.table.getValueAt(i, j);
                        boolean isSame = false;
                        if (oldValue == null) {
                            isSame = (currValue == null);
                        } else {
                            isSame = oldValue.equals(currValue);
                        }
                        if (isSame) {
                            // start fading out
                            if (!enforceNoAnimations) {
                                // System.out
                                // .println("Getting selection/out tracker for "
                                // + cellId);
                                StateTransitionTracker tracker = getTracker(cellId,
                                        getCellState(cellId)
                                                .isFacetActive(RadianceThemingSlices.ComponentStateFacet.ROLLOVER),
                                        true);
                                tracker.getModel().setSelected(false);
                                // System.out
                                // .println("Unselecting previously selected "
                                // + i + ":" + j);

                                initiatedTrackers.add(tracker);
                                if (initiatedTrackers.size() > 20) {
                                    stateTransitionMultiTracker.clear();
                                    initiatedTrackers.clear();
                                    enforceNoAnimations = true;
                                }
                            }
                        }
                        this.selectedIndices.remove(cellId);
                    }
                }

                // handle focus animations
                boolean cellHasFocus = isFocusOwner && (i == rowLeadIndex) && (j == colLeadIndex);
                if (cellHasFocus) {
                    // check if it's a different cell
                    if ((this.focusedCellId == null) || !this.focusedCellId.equals(cellId)) {
                        if (!enforceNoAnimations) {
                            if (this.focusedCellId != null) {
                                // fade out the previous focus holder

                                ComponentState cellState = getCellState(this.focusedCellId);
                                // System.out.println("Getting focus/out tracker for "
                                // + cellId);
                                StateTransitionTracker tracker = getTracker(this.focusedCellId,
                                        cellState.isFacetActive(RadianceThemingSlices.ComponentStateFacet.ROLLOVER),
                                        cellState.isFacetActive(RadianceThemingSlices.ComponentStateFacet.SELECTION));
                                tracker.setFocusState(false);
                            }

                            // fade in the current cell (new focus holder)
                            ComponentState cellState = getCellState(cellId);

                            // System.out.println("Getting focus/in tracker for "
                            // + currId);
                            StateTransitionTracker tracker = getTracker(cellId,
                                    cellState.isFacetActive(RadianceThemingSlices.ComponentStateFacet.ROLLOVER),
                                    cellState.isFacetActive(RadianceThemingSlices.ComponentStateFacet.SELECTION));
                            tracker.setFocusState(true);
                        }

                        if (AnimationConfigurationManager.getInstance()
                                .isAnimationAllowed(RadianceThemingSlices.AnimationFacet.FOCUS, this.table)) {
                            // and store it for future checks
                            this.focusedCellId = new TableCellId(i, j);
                        }
                    }
                } else {
                    // check if previously it held focus
                    if (cellId.equals(this.focusedCellId)) {
                        if (!enforceNoAnimations) {
                            // fade it out
                            ComponentState cellState = getCellState(cellId);
                            // System.out.println("Getting focus/out tracker for "
                            // + cellId);
                            StateTransitionTracker tracker = getTracker(cellId,
                                    cellState.isFacetActive(RadianceThemingSlices.ComponentStateFacet.ROLLOVER),
                                    cellState.isFacetActive(RadianceThemingSlices.ComponentStateFacet.SELECTION));
                            tracker.setFocusState(false);
                        }

                        this.focusedCellId = null;
                    }
                }
            }
        }
    }

    /**
     * Returns the current state for the specified cell.
     *
     * @param cellIndex Cell index.
     * @return The current state for the specified cell.
     */
    public ComponentState getCellState(TableCellId cellIndex) {
        boolean isEnabled = this.table.isEnabled();

        StateTransitionTracker tracker = this.stateTransitionMultiTracker.getTracker(cellIndex);
        if (tracker == null) {
            int row = cellIndex.row;
            int column = cellIndex.column;
            TableCellId cellId = this.getId(row, column);

            boolean isRollover = false;
            if (_hasRolloverAnimations()) {
                isRollover = rolledOverIndices.contains(cellId);
            } else if (table.getRowSelectionAllowed()) {
                if (table.getColumnSelectionAllowed()) {
                    isRollover = (row == rolledOverRow) && (column == rolledOverColumn);
                } else {
                    isRollover = (row == rolledOverRow);
                }
            } else if (table.getColumnSelectionAllowed()) {
                isRollover = (column == rolledOverColumn);
            }

            boolean isSelected = false;
            boolean hasSelectionAnimations = (this.updateInfo != null)
                    ? this.updateInfo.hasSelectionAnimations
                    : this._hasSelectionAnimations();
            if (hasSelectionAnimations && AnimationConfigurationManager.getInstance()
                    .isAnimationAllowed(RadianceThemingSlices.AnimationFacet.SELECTION, table))
                isSelected = this.selectedIndices.containsKey(cellId);
            else {
                isSelected = this.table.isCellSelected(row, column);
            }
            return ComponentState.getState(isEnabled, isRollover, isSelected);
        } else {
            ComponentState fromTracker = tracker.getModelStateInfo().getCurrModelState();
            return ComponentState.getState(isEnabled,
                    fromTracker.isFacetActive(RadianceThemingSlices.ComponentStateFacet.ROLLOVER),
                    fromTracker.isFacetActive(RadianceThemingSlices.ComponentStateFacet.SELECTION));
        }
    }

    /**
     * Returns the current state for the specified cell.
     *
     * @param cellId Cell index.
     * @return The current state for the specified cell.
     */
    public StateTransitionTracker.ModelStateInfo getModelStateInfo(TableCellId cellId) {
        if (this.stateTransitionMultiTracker.size() == 0)
            return null;
        StateTransitionTracker tracker = this.stateTransitionMultiTracker.getTracker(cellId);
        if (tracker == null) {
            return null;
        } else {
            return tracker.getModelStateInfo();
        }
    }

    /**
     * Checks whether the table has animations.
     *
     * @return true if the table has animations, false otherwise.
     */
    protected boolean _hasAnimations() {
        // fix for defects 164 and 209 - selection
        // and deletion are very slow on large tables.
        int rowCount = this.table.getRowCount();
        int colCount = this.table.getColumnCount();
        if (rowCount * colCount >= 500)
            return false;
        if (this.table.getColumnSelectionAllowed() && !this.table.getRowSelectionAllowed()) {
            if (!this.table.getShowHorizontalLines() && !this.table.getShowVerticalLines())
                return rowCount <= 10;
            return rowCount <= 25;
        }
        if (!this.table.getColumnSelectionAllowed() && this.table.getRowSelectionAllowed()) {
            if (!this.table.getShowHorizontalLines() && !this.table.getShowVerticalLines())
                return colCount <= 10;
            return colCount <= 25;
        }
        return true;
    }

    /**
     * Checks whether the table has selection animations.
     *
     * @return true if the table has selection animations, false
     * otherwise.
     */
    protected boolean _hasSelectionAnimations() {
        return this._hasAnimations()
                && !WidgetUtilities.hasNoAnimations(this.table, RadianceThemingSlices.AnimationFacet.SELECTION);
    }

    /**
     * Checks whether the table has rollover animations.
     *
     * @return true if the table has rollover animations, false otherwise.
     */
    protected boolean _hasRolloverAnimations() {
        return this._hasAnimations()
                && !WidgetUtilities.hasNoAnimations(this.table, RadianceThemingSlices.AnimationFacet.ROLLOVER);
    }

    /**
     * Returns the index of the rollover column.
     *
     * @return The index of the rollover column.
     */
    public int getRolloverColumnIndex() {
        return this.rolledOverColumn;
    }

    /**
     * Returns indication whether the specified cell has focus.
     *
     * @param row    Cell row index.
     * @param column Cell column index.
     * @return true If the focus is on the specified cell, false
     * otherwise.
     */
    public boolean isFocusedCell(int row, int column) {
        return (this.focusedCellId != null) && (this.focusedCellId.row == row)
                && (this.focusedCellId.column == column);
    }

    @Override
    public void update(Graphics g, JComponent c) {
        BackgroundPaintingUtils.updateIfOpaque(g, c);
        Graphics2D g2d = (Graphics2D) g.create();
        RadianceCommonCortex.installDesktopHints(g2d, c.getFont());
        RadianceStripingUtils.setup(c);
        this.updateInfo = new TableUpdateOptimizationInfo();
        this.paint(g2d, c);
        RadianceStripingUtils.tearDown(c);
        g2d.dispose();
        this.updateInfo = null;
    }

    /**
     * Returns the cell renderer insets of this table. Is for internal use only.
     *
     * @return The cell renderer insets of this table.
     */
    public Insets getCellRendererInsets() {
        return this.cellRendererInsets;
    }

    public boolean hasSelectionAnimations() {
        if (this.updateInfo != null)
            return this.updateInfo.hasSelectionAnimations;
        return this._hasSelectionAnimations();
    }

    public boolean hasRolloverAnimations() {
        if (this.updateInfo != null)
            return this.updateInfo.hasRolloverAnimations;
        return this._hasRolloverAnimations();
    }

    private TableUpdateOptimizationInfo updateInfo;

    private class TableUpdateOptimizationInfo extends UpdateOptimizationInfo {
        public boolean hasSelectionAnimations;

        public boolean hasRolloverAnimations;

        public TableUpdateOptimizationInfo() {
            super(table);
            this.hasSelectionAnimations = _hasSelectionAnimations();
            this.hasRolloverAnimations = _hasRolloverAnimations();
        }
    }

    @Override
    public UpdateOptimizationInfo getUpdateOptimizationInfo() {
        return this.updateInfo;
    }

    private boolean isRadianceDefaultRenderer(Object instance) {
        return (instance instanceof RadianceDefaultTableCellRenderer)
                || (instance instanceof RadianceDefaultTableCellRenderer.BooleanRenderer);
    }

    private boolean isRadianceDefaultEditor(TableCellEditor editor) {
        return (editor instanceof BooleanEditor);
    }

    private Rectangle getCellRectangleForRepaint(int row, int column) {
        Rectangle rect = this.table.getCellRect(row, column, true);

        if (!table.getShowHorizontalLines() && !table.getShowVerticalLines()) {
            float extra = RadianceSizeUtils.getBorderStrokeWidth(table);
            rect.x -= (int) extra;
            rect.width += 2 * (int) extra;
            rect.y -= (int) extra;
            rect.height += 2 * (int) extra;
        }
        return rect;
    }

    private StateTransitionTracker getTracker(final TableCellId tableCellId,
            boolean initialRollover, boolean initialSelected) {
        StateTransitionTracker tracker = stateTransitionMultiTracker.getTracker(tableCellId);
        // System.out.println("TableID " + tableCellId + " has tracker "
        // + ((tracker == null) ? "null" : ("@" + tracker.hashCode())));
        if (tracker == null) {
            ButtonModel model = new DefaultButtonModel();
            model.setSelected(initialSelected);
            model.setRollover(initialRollover);
            tracker = new StateTransitionTracker(table, model);
            tracker.registerModelListeners();
            tracker.setRepaintCallback(
                    () -> new CellRepaintCallback(table, tableCellId.row, tableCellId.column));
            tracker.setName("row " + tableCellId.row + ", col " + tableCellId.column);
            // System.out.println("TableID " + tableCellId +
            // " has new tracker @"
            // + tracker.hashCode());
            stateTransitionMultiTracker.addTracker(tableCellId, tracker);
        }
        return tracker;
    }

    public StateTransitionTracker getStateTransitionTracker(TableCellId tableId) {
        return this.stateTransitionMultiTracker.getTracker(tableId);
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy