org.nuclos.client.ui.resplan.header.JHeaderGrid.java Source code

Java tutorial

Introduction

Here is the source code for org.nuclos.client.ui.resplan.header.JHeaderGrid.java

Source

//Copyright (C) 2010  Novabit Informationssysteme GmbH
//
//This file is part of Nuclos.
//
//Nuclos is free software: you can redistribute it and/or modify
//it under the terms of the GNU Affero General Public License as published by
//the Free Software Foundation, either version 3 of the License, or
//(at your option) any later version.
//
//Nuclos is distributed in the hope that it will be useful,
//but WITHOUT ANY WARRANTY; without even the implied warranty of
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//GNU Affero General Public License for more details.
//
//You should have received a copy of the GNU Affero General Public License
//along with Nuclos.  If not, see <http://www.gnu.org/licenses/>.

package org.nuclos.client.ui.resplan.header;

import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.Paint;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.List;

import javax.swing.CellRendererPane;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.SwingConstants;
import javax.swing.ToolTipManager;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.ListDataEvent;
import javax.swing.event.ListDataListener;
import javax.swing.event.MouseInputAdapter;
import javax.swing.text.View;

import org.apache.commons.lang.ObjectUtils;
import org.jdesktop.swingx.painter.Painter;
import org.jdesktop.swingx.renderer.CellContext;
import org.jdesktop.swingx.renderer.ComponentProvider;
import org.jdesktop.swingx.renderer.DefaultVisuals;
import org.jdesktop.swingx.renderer.LabelProvider;
import org.jdesktop.swingx.renderer.PainterAware;
import org.nuclos.client.ui.UIUtils;
import org.nuclos.client.ui.util.GeomUtils;
import org.nuclos.client.ui.util.MultiActionMouseHandler;
import org.nuclos.client.ui.util.MultiActionMouseHandler.MouseActionHandler;
import org.nuclos.client.ui.util.Orientation;
import org.nuclos.client.ui.util.PainterUtils;
import org.nuclos.client.ui.util.Range;
import org.nuclos.common2.LangUtils;

public class JHeaderGrid<E> extends JComponent implements ListDataListener {

    private static final Dimension DEFAULT_CELL_EXTENT = new Dimension(75, 20);

    private static final Dimension MINIMUM_CELL_EXTENT = new Dimension(10, 10);

    public static final String MODEL_PROPERTY = "model";
    public static final String CELL_EXTENT_PROPERTY = "cellExtent";
    public static final String CELL_RESIZING_RANGE_PROPERTY = "cellResizingRange";

    private static final int GRID_SIZE = 1;

    /** Constant for horizontal orientation, i.e. works as column header. */
    public static final int COLUMN = SwingConstants.HORIZONTAL;
    /** Constant for vertical orientation, i.e. works as row header. */
    public static final int ROW = SwingConstants.VERTICAL;

    private Orientation orientation;

    private TypesafeListModel<E> model;
    private E selectedValue;

    private int cellExtent = 75;
    private boolean cellExtentSet;

    private GroupMapper<E> categoryModel;
    private CellRendererPane cellRendererPane;
    private ComponentProvider<?> cellRendererProvider;

    private CategoryView[] groupings;

    private Paint gridColor;
    private Range cellResizingRange;

    private MultiActionMouseHandler mouseActionHandler;

    private Component cornerComponent;

    public JHeaderGrid(Orientation orientation, TypesafeListModel<E> primaryModel) {
        this(orientation, primaryModel, null);
    }

    public JHeaderGrid(Orientation orientation, TypesafeListModel<E> model, GroupMapper<E> categoryModel) {
        this.orientation = orientation;
        this.cellExtent = orientation.extentFrom(DEFAULT_CELL_EXTENT);

        this.gridColor = new Color(163, 172, 187);

        this.cellRendererPane = new CellRendererPane();
        this.cellRendererProvider = createDefaultCellRendererProvider();
        this.add(cellRendererPane);

        this.setModel(model);
        this.setCategoryModel(categoryModel);

        // Enable location-sensitive tool-tips (don't fiddle with setting a dummy tooltip,
        // registering the component with the ToolTipManager is the standard Swing way to do it)
        ToolTipManager.sharedInstance().registerComponent(this);

        initializeModel();

        mouseActionHandler = new MultiActionMouseHandler(new DefaultMouseHandler());
        mouseActionHandler.install(this);
    }

    public void addChangeListener(ChangeListener l) {
        listenerList.add(ChangeListener.class, l);
    }

    public void removeChangeListener(ChangeListener l) {
        listenerList.remove(ChangeListener.class, l);
    }

    public ChangeListener[] getChangeListeners() {
        return listenerList.getListeners(ChangeListener.class);
    }

    public int getCellExtent() {
        return cellExtent;
    }

    public void setCellExtent(int newExtent) {
        if (newExtent == -1) {
            setCellExtentImpl(getPreferredCellExtent(), false);
        } else if (newExtent >= 0) {
            setCellExtentImpl(Math.max(orientation.extentFrom(MINIMUM_CELL_EXTENT), newExtent), true);
        } else {
            throw new IllegalArgumentException("extent < 0");
        }
    }

    public boolean isCellExtentSet() {
        return cellExtentSet;
    }

    private void setCellExtentImpl(int newExtent, boolean extentSet) {
        int oldExtent = this.cellExtent;
        if (oldExtent != newExtent) {
            this.cellExtent = newExtent;
            this.cellExtentSet = extentSet;
            invalidate();
            if (this.categoryModel != null) {
                this.categoryModel.setCellExtent(this.orientation, newExtent);
            }
            firePropertyChange(CELL_EXTENT_PROPERTY, oldExtent, newExtent);
            fireStateChanged();
        }
    }

    public Component getCornerComponent() {
        return cornerComponent;
    }

    public void setCornerComponent(Component cornerComponent) {
        this.cornerComponent = cornerComponent;
        invalidate();
    }

    public Range getCellResizingRange() {
        return cellResizingRange;
    }

    public void setCellResizingRange(Range newRange) {
        Range oldRange = this.cellResizingRange;
        if (!ObjectUtils.equals(oldRange, newRange)) {
            repaint(oldRange);
            this.cellResizingRange = newRange;
            repaint(newRange);
            firePropertyChange(CELL_RESIZING_RANGE_PROPERTY, oldRange, newRange);
            fireStateChanged();
        }
    }

    protected void fireStateChanged() {
        ChangeEvent changeEvent = null;
        for (ChangeListener listener : listenerList.getListeners(ChangeListener.class)) {
            if (changeEvent == null) {
                changeEvent = new ChangeEvent(this);
            }
            listener.stateChanged(changeEvent);
        }
    }

    protected ComponentProvider<?> createDefaultCellRendererProvider() {
        return new LabelProvider(JLabel.CENTER) {

            @Override
            protected DefaultVisuals<JLabel> createDefaultVisuals() {
                return new DefaultVisuals<JLabel>() {

                    private final Painter<?> painter = new PainterUtils.HeaderPainter();

                    @Override
                    protected void configurePainter(JLabel renderingComponent, CellContext context) {
                        if (renderingComponent instanceof PainterAware) {
                            ((PainterAware) renderingComponent).setPainter(painter);
                        }
                    }
                };
            }
        };
    }

    public TypesafeListModel<E> getModel() {
        return model;
    }

    public void setModel(TypesafeListModel<E> newModel) {
        TypesafeListModel<E> oldModel = this.model;
        if (oldModel != null) {
            oldModel.removeListDataListener(this);
        }
        this.model = newModel;
        if (newModel != null) {
            newModel.addListDataListener(this);
        }
        initializeModel();
        firePropertyChange(MODEL_PROPERTY, oldModel, newModel);
        fireStateChanged();
    }

    public E getSelectedValue() {
        return selectedValue;
    }

    public void setSelectedValue(E newValue) {
        E oldValue = this.selectedValue;
        this.selectedValue = newValue;
        if (!LangUtils.equals(oldValue, newValue)) {
            repaint();
        }
    }

    public GroupMapper<E> getCategoryModel() {
        return categoryModel;
    }

    public void setCategoryModel(GroupMapper<E> categoryModel) {
        this.categoryModel = categoryModel;
        if (categoryModel != null && categoryModel.getCellExtent(this.orientation) > 0) {
            setCellExtent(categoryModel.getCellExtent(this.orientation));
        }

        initializeModel();
        fireStateChanged();
    }

    protected void initializeModel() {
        int levelCount = getLevelCount();
        groupings = new CategoryView[levelCount];
        for (int level = 0; level < levelCount; level++) {
            CategoryView categoryView = groupings[level] = new CategoryView(level);
            int count = model.getSize();
            Object levelValue = null;
            int startIndex = -1;
            // TODO_RESPLAN: grouping must be senseful
            for (int index = 0; index < count; index++) {
                if (levelValue != null && levelValue.equals(getLevelComponent(level, index))) {
                    continue;
                } else {
                    if (startIndex != -1) {
                        categoryView.add(levelValue, startIndex, index - 1);
                    }
                    startIndex = index;
                    levelValue = getLevelComponent(level, index);
                }
            }
            if (levelValue != null) {
                categoryView.add(levelValue, startIndex, count - 1);
            }
        }

        if (!isCellExtentSet()) {
            setCellExtentImpl(getPreferredCellExtent(), false);
        }
        // TODO: this triggers calculation of group sizes
        calculateGroupingExtents();

        super.invalidate();
        repaint();
    }

    private void calculateGroupingExtents() {
        final Orientation groupOrient = orientation.opposite();
        int preferredGroupingsExtent = 0;
        for (CategoryView categoryView : groupings) {
            int preferredCategorySize = 0;
            for (HeaderCell cell : categoryView.cells) {
                JComponent renderer = setupCellRenderer(cell.levelValue, false);
                Dimension preferredSize = renderer.getPreferredSize();
                int preferredExtent = groupOrient.extentFrom(preferredSize);
                if (renderer instanceof JLabel) {
                    // This is a some kind of hack for determing the preferred size of an HTML JLabel
                    // using a given width or height. See also:
                    // http://blog.nobel-joergensen.com/2009/01/18/changing-preferred-size-of-a-html-jlabel/
                    // NOTE: This seems to work for horizontal orientations (i.e. calculating the height for a
                    // given width) while for vertical orientations often the dummy value 0 is returned.
                    View view = (View) renderer.getClientProperty(javax.swing.plaf.basic.BasicHTML.propertyKey);
                    if (view != null) {
                        float w = (orientation == Orientation.HORIZONTAL) ? cellExtent : 0;
                        float h = (orientation == Orientation.VERTICAL) ? cellExtent : 0;
                        view.setSize(w, h);
                        float span = (int) Math.ceil(view.getPreferredSpan(groupOrient.swingConstant()));
                        preferredExtent = Math.max(preferredExtent, (int) Math.ceil(span));
                    }
                }
                preferredCategorySize = Math.max(preferredCategorySize, preferredExtent + 2);
            }
            categoryView.preferredSize = preferredCategorySize;
            categoryView.size = preferredCategorySize;
            preferredGroupingsExtent += preferredCategorySize + GRID_SIZE;
        }

        // Real size and preferred size may vary
        int realGroupingsExtent = groupOrient.extentFrom(getSize());
        int delta = realGroupingsExtent - preferredGroupingsExtent;
        if (delta > 0) {
            int gap = delta / groupings.length;
            for (CategoryView v : groupings) {
                v.size += gap;
            }
            int rem = delta % groupings.length;
            if (rem > 0 && groupings.length > 0) {
                groupings[groupings.length - 1].size += rem;
            }
        }
    }

    public int getPreferredCellExtent() {
        int preferredCellExtent = -1;
        for (CategoryView categoryView : groupings) {
            for (HeaderCell cell : categoryView.cells) {
                JComponent renderer = setupCellRenderer(cell.levelValue, false);
                Dimension preferredSize = renderer.getPreferredSize();
                preferredCellExtent = Math.max(preferredCellExtent, orientation.extentFrom(preferredSize) + 2);
            }
        }
        if (preferredCellExtent == -1) {
            preferredCellExtent = orientation.extentFrom(DEFAULT_CELL_EXTENT);
        }
        return preferredCellExtent;
    }

    @Override
    public void invalidate() {
        initializeModel();
        super.invalidate();
    }

    private void repaint(Range range) {
        if (range == null)
            return;
        Rectangle rect = new Rectangle(getWidth(), getHeight());
        orientation.updateRange(rect, range);
        rect.grow(GRID_SIZE + 1, GRID_SIZE + 1);
        repaint(rect);
    }

    public E getValue(int index) {
        return model.getElementAt(index);
    }

    public E getValueAt(Point pt) {
        int index = stripAtPoint(pt);
        return (index != -1) ? getValue(index) : null;
    }

    public String getStringAt(int index) {
        JComponent renderer = setupCellRenderer(model.getElementAt(index), false);
        return UIUtils.getRendererText(renderer);
    }

    public int getCount() {
        return model.getSize();
    }

    public ComponentProvider<?> getCellRendererProvider() {
        return cellRendererProvider;
    }

    public void setCellRendererProvider(ComponentProvider<?> cellRendererProvider) {
        this.cellRendererProvider = cellRendererProvider;
        initializeModel();
    }

    public Orientation getOrientation() {
        return orientation;
    }

    public void setOrientation(Orientation orientation) {
        this.orientation = orientation;
        initializeModel();
    }

    public int stripAtPoint(Point pt) {
        int coord = orientation.coordFrom(pt);
        return stripAtCoord(coord);
    }

    public int stripAtCoord(int coord) {
        int index = coord / (this.cellExtent + GRID_SIZE);
        if (index >= 0 && index < model.getSize()) {
            return index;
        }
        return -1;
    }

    public int[] stripRangeForRect(Rectangle rect) {
        if (rect == null) {
            return new int[] { 0, model.getSize() };
        }
        Range range = orientation.rangeFrom(rect);
        int index1 = stripAtCoord(range.getCoord());
        int index2 = stripAtCoord(range.getCoord() + range.getExtent());
        return new int[] { index1 != -1 ? index1 : 0, index2 != -1 ? index2 + 1 : model.getSize() };
    }

    public Rectangle getStrip(int index) {
        return getStrip(index, index + 1);
    }

    /**
     * Returns the "strip" rectangle for the given index range.
     */
    public Rectangle getStrip(int startIndex, int endIndex) {
        return getStrip(startIndex, endIndex, null);
    }

    public Rectangle getStrip(int startIndex, int endIndex, Rectangle rect) {
        if (rect == null)
            rect = new Rectangle(0, 0, Integer.MAX_VALUE, Integer.MAX_VALUE);
        int startLine = getGridLine(startIndex);
        int endLine = getGridLine(endIndex);
        orientation.updateCoordExtent(rect, startLine + GRID_SIZE, endLine - startLine - GRID_SIZE);
        return rect;
    }

    public Rectangle getRect(int index) {
        return getStrip(index, index + 1, new Rectangle(getWidth(), getHeight()));
    }

    // TODO_RESPLAN: rounding issues
    public Rectangle getRect(int level, int index) {
        Rectangle rect = getRect(index);
        switch (orientation) {
        case HORIZONTAL:
            for (int i = 0; i < level; i++) {
                rect.y += groupings[i].size + GRID_SIZE;
            }
            rect.height = groupings[level].size;
            return rect;
        case VERTICAL:
            for (int i = 0; i < level; i++) {
                rect.x += groupings[i].size + GRID_SIZE;
            }
            rect.width = groupings[level].size;
            return rect;
        default:
            throw new IllegalStateException();
        }
    }

    // TODO_RESPLAN: rounding issues
    public Rectangle getRect(int level, int start, int end) {
        return getRect(level, start).union(getRect(level, end));
    }

    @Override
    public Dimension getPreferredSize() {
        if (isPreferredSizeSet())
            return super.getPreferredSize();
        int preferredGroupingsExtent = 0;
        for (CategoryView v : groupings) {
            preferredGroupingsExtent += v.preferredSize + GRID_SIZE;
        }
        if (cornerComponent != null) {
            int cornerExtent = orientation.opposite().extentFrom(cornerComponent.getPreferredSize());
            preferredGroupingsExtent = Math.max(preferredGroupingsExtent, cornerExtent);
        }
        switch (orientation) {
        case HORIZONTAL:
            return new Dimension(getPreferredExtent() + GRID_SIZE, preferredGroupingsExtent);
        case VERTICAL:
            return new Dimension(preferredGroupingsExtent, getPreferredExtent() + GRID_SIZE);
        default:
            throw new IllegalStateException();
        }
    }

    public int getPreferredExtent() {
        return getGridLine(model.getSize()) + GRID_SIZE;
    }

    public Dimension getPreferredExtent(Dimension dimension) {
        if (dimension == null)
            dimension = new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE);
        orientation.updateExtent(dimension, getPreferredExtent());
        return dimension;
    }

    public int getGridSize() {
        return GRID_SIZE;
    }

    public int getGridLine(int index) {
        return index * (this.cellExtent + GRID_SIZE) - GRID_SIZE;
    }

    public int getGridLineLevel(int index) {
        if (groupings.length <= 1)
            return 0;
        int n = groupings.length - 1;
        for (int i = 0; i < n; i++) {
            if (groupings[i].isBoundary(index)) {
                return i;
            }
        }
        return n;
    }

    @Override
    public String getToolTipText(MouseEvent event) {
        int index = stripAtPoint(event.getPoint());
        if (index != -1) {
            JComponent renderer = setupCellRenderer(model.getElementAt(index), false);
            return renderer.getToolTipText();
        } else {
            return getToolTipText();
        }
    }

    @Override
    public void contentsChanged(ListDataEvent e) {
        initializeModel();
    }

    @Override
    public void intervalAdded(ListDataEvent e) {
        initializeModel();
    }

    @Override
    public void intervalRemoved(ListDataEvent e) {
        initializeModel();
    }

    @Override
    protected void paintComponent(Graphics g) {
        Graphics2D g2d = (Graphics2D) g;

        super.paintComponent(g2d);

        g2d.setPaint(gridColor);

        Rectangle clipBounds = g.getClipBounds();

        int[] indices = stripRangeForRect(clipBounds);
        int startIndex = indices[0];
        int endIndex = indices[1];

        Point p1, p2;
        p1 = new Point();
        p2 = new Point(getWidth(), getHeight());
        for (int i = startIndex; i <= endIndex; i++) {
            int gridLine = getGridLine(i);
            orientation.updateCoord(p1, gridLine);
            orientation.updateCoord(p2, gridLine);
            g2d.drawLine(p1.x, p1.y, p2.x, p2.y);
        }

        p1.setLocation(0, 0);
        p2.setLocation(getWidth(), getHeight());
        int groupGridLine = 0;
        for (CategoryView v : groupings) {
            groupGridLine += v.size;
            orientation.opposite().updateCoord(p1, groupGridLine);
            orientation.opposite().updateCoord(p2, groupGridLine);
            g2d.drawLine(p1.x, p1.y, p2.x, p2.y);
            groupGridLine += GRID_SIZE;
        }

        for (int level = 0, levelCount = groupings.length; level < levelCount; level++) {
            for (HeaderCell cell : groupings[level].cells) {
                if (cell.endIndex < startIndex || cell.startIndex >= endIndex)
                    continue;
                Rectangle rect = getRect(level, cell.startIndex, cell.endIndex);
                if (clipBounds != null && !clipBounds.intersects(rect))
                    continue;
                paintHeaderCell(g2d, rect, cell.levelValue);
            }
        }

        if (cellResizingRange != null) {
            Rectangle rect = new Rectangle(getWidth(), getHeight());
            orientation.updateRange(rect, cellResizingRange);
            rect.grow(orientation.select(GRID_SIZE, 0), orientation.select(0, GRID_SIZE));
            g2d.setColor(new Color(0x9999ff, false));
            g2d.draw(rect);
            g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, 0.5f));
            g2d.fill(rect);
        }
    }

    private void paintHeaderCell(Graphics2D g2d, Rectangle rect, Object value) {
        Rectangle clipBounds = g2d.getClipBounds();
        if (clipBounds != null && !clipBounds.intersects(rect))
            return;
        boolean selected = (selectedValue != null && selectedValue.equals(value));
        JComponent renderer = setupCellRenderer(value, selected);
        cellRendererPane.paintComponent(g2d, renderer, this, rect);
        cellRendererPane.remove(renderer);
    }

    private JComponent setupCellRenderer(Object value, boolean selected) {
        GridHeaderContext context = new GridHeaderContext();
        context.installContext(this, value, 0, 0, selected, false, false, false);
        return cellRendererProvider.getRendererComponent(context);
    }

    public int getLevelCount() {
        return (categoryModel != null ? categoryModel.getCategoryCount() : 1);
    }

    // TODO_RESPLAN:
    protected Object getLevelComponent(int level, int index) {
        E value = model.getElementAt(index);
        if (categoryModel != null) {
            return categoryModel.getCategoryValue(level, value);
        } else {
            return value;
        }
    }

    private static class CategoryView {

        private List<HeaderCell> cells;
        private int preferredSize = 50;
        private int size = 50;

        public CategoryView(int level) {
            cells = new ArrayList<HeaderCell>();
        }

        public void add(Object levelValue, int startIndex, int endIndex) {
            cells.add(new HeaderCell(levelValue, startIndex, endIndex));
        }

        public boolean isBoundary(int index) {
            for (HeaderCell cell : cells) {
                if (index == cell.startIndex || index == cell.endIndex + 1)
                    return true;
            }
            return false;
        }
    }

    private static class HeaderCell {
        Object levelValue;
        int startIndex;
        int endIndex;

        public HeaderCell(Object levelValue, int startIndex, int endIndex) {
            this.levelValue = levelValue;
            this.startIndex = startIndex;
            this.endIndex = endIndex;
        }
    }

    public static class GridHeaderContext extends CellContext {

        @Override
        public JHeaderGrid<?> getComponent() {
            return (JHeaderGrid<?>) super.getComponent();
        }

        public void installContext(JHeaderGrid<?> component, Object value, int row, int column, boolean selected,
                boolean focused, boolean expanded, boolean leaf) {
            this.component = component;
            installState(value, row, column, selected, focused, expanded, leaf);
        }
    }

    private class DefaultMouseHandler extends MouseInputAdapter {

        private final Insets INSETS = new Insets(5, 5, 5, 5);

        private int resizingIndex;

        @Override
        public void mouseMoved(MouseEvent evt) {
            resizingIndex = -1;
            int stripAt = stripAtPoint(evt.getPoint());
            Cursor cursor = null;
            if (stripAt != -1) {
                Rectangle rect = getRect(stripAt);
                if (rect.contains(evt.getPoint())) {
                    int compass = GeomUtils.findInsetDirection(rect, evt.getPoint(), INSETS);
                    compass = orientation.normalizeCompassDirection(compass);
                    switch (orientation.testCompassDirection(compass)) {
                    case -1:
                        resizingIndex = stripAt - 1;
                        break;
                    case 1:
                        resizingIndex = stripAt;
                        break;
                    }
                    if (resizingIndex != -1) {
                        cursor = Cursor.getPredefinedCursor(GeomUtils.getResizeCursor(compass));
                    }
                }
            }
            JHeaderGrid.this.setCursor(cursor);
        }

        @Override
        public void mouseExited(MouseEvent evt) {
            JHeaderGrid.this.setCursor(null);
        }

        @Override
        public void mousePressed(MouseEvent evt) {
            if (evt.getButton() == MouseEvent.BUTTON1) {
                if (resizingIndex != -1) {
                    Rectangle rect = getRect(resizingIndex);
                    mouseActionHandler.startHandler(new CellResizingHandler(orientation.rangeFrom(rect)), evt);
                }
            }
        }
    }

    private class CellResizingHandler extends MouseActionHandler {

        private final int anchor;
        private int extent;

        public CellResizingHandler(Range range) {
            this.anchor = range.getCoord();
            this.extent = range.getExtent();
        }

        @Override
        protected void start(MouseEvent evt) {
            super.start(evt);
            setCellResizingRange(new Range(anchor, extent));
        }

        @Override
        public void mouseDragged(MouseEvent evt) {
            int lead = orientation.coordFrom(evt.getPoint());
            extent = Math.max(orientation.extentFrom(MINIMUM_CELL_EXTENT), lead - anchor);
            setCellResizingRange(new Range(anchor, extent));
        }

        @Override
        public void mouseReleased(MouseEvent evt) {
            final Rectangle visibleRect = getVisibleRect();
            int visibleIndex = stripAtPoint(visibleRect.getLocation());
            int visibleOffset = orientation.coordFrom(getVisibleRect()) - getGridLine(visibleIndex);

            setCellResizingRange(null);
            setCellExtent(extent);

            orientation.updateCoord(visibleRect, getGridLine(visibleIndex) + visibleOffset);
            scrollRectToVisible(visibleRect);

            evt.consume();
            stop();
        }
    }
}