com.android.tools.idea.wizard.ASGallery.java Source code

Java tutorial

Introduction

Here is the source code for com.android.tools.idea.wizard.ASGallery.java

Source

/*
 * Copyright (C) 2014 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.tools.idea.wizard;

import com.android.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.base.Functions;
import com.google.common.base.Objects;
import com.google.common.base.Optional;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.ui.GraphicsUtil;
import com.intellij.util.ui.UIUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import javax.accessibility.*;
import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.event.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.font.LineMetrics;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.concurrent.ExecutionException;

/**
 * A gallery widget for displaying a collection of images.
 * <p/>
 * This widget obtains its model from {@link javax.swing.ListModel} and
 * relies on two functions to obtain image and lable for model object.
 * It does not support notions of "renderer" or "editor"
 */
public class ASGallery<E> extends JComponent implements Accessible, Scrollable {
    /**
     * Default insets around the cell contents.
     */
    private static final Insets DEFAULT_CELL_MARGIN = new Insets(1, 1, 1, 1);
    /**
     * Insets around cell content (image and title).
     */
    @NotNull
    private Insets myCellMargin = DEFAULT_CELL_MARGIN;
    /**
     * Timeout in ms when incremental search is reset
     */
    private static final int INCSEARCH_TIMEOUT_MS = 500; // ms
    /**
     * Listeners for events other then property event
     */
    private final EventListenerList myListeners = new EventListenerList();
    /**
     * Listens to changes in the model data
     */
    private final ListDataListener myListDataListener = new InternalListDataListener();
    /**
     * Size of the image. Currently all images will be scaled to this size, this
     * may change as we get more requirements.
     */
    @NotNull
    private Dimension myThumbnailSize = new Dimension(128, 128);
    /**
     * Index of the selected item or -1 if none
     */
    private int mySelectedIndex = -1;
    /**
     * Data shown in this component
     */
    private ListModel myModel;
    /**
     * Filter string for the incremental search
     */
    private String myFilterString = "";
    /**
     * Timestamp of the last keypress used for incremental search.
     */
    private long myPreviousKeypressTimestamp = 0;
    /**
     * Caches item images, is reset if different image provider is supplied.
     */
    @NotNull
    private LoadingCache<E, Optional<Image>> myImagesCache;
    /**
     * Obtains string label for the model object.
     */
    @NotNull
    private Function<? super E, String> myLabelProvider = Functions.toStringFunction();

    public ASGallery() {
        this(new DefaultListModel(), Functions.<Image>constant(null), Functions.toStringFunction(),
                new Dimension(0, 0));
    }

    public ASGallery(@NotNull ListModel model, @NotNull Function<? super E, Image> imageProvider,
            @NotNull Function<? super E, String> labelProvider, @NotNull Dimension thumbnailSize) {
        Font listFont = UIUtil.getListFont();
        if (listFont != null) {
            setFont(listFont);
        }

        setThumbnailSize(thumbnailSize);
        setImageProvider(imageProvider);
        setLabelProvider(labelProvider);
        setModel(model);

        setOpaque(true);
        setFocusable(true);
        addMouseListener(new MouseAdapter() {
            @Override
            public void mousePressed(MouseEvent e) {
                requestFocusInWindow();
                int cell = getCellAt(e.getPoint());
                if (cell >= 0) {
                    setSelectedIndex(cell);
                }
            }
        });
        addFocusListener(new FocusListener() {
            @Override
            public void focusGained(FocusEvent e) {
                reveal();
                updateFocusRectangle();
            }

            @Override
            public void focusLost(FocusEvent e) {
                updateFocusRectangle();
            }
        });
        addKeyListener(new KeyAdapter() {
            @Override
            public void keyTyped(KeyEvent e) {
                char keyChar = e.getKeyChar();
                if (keyChar != KeyEvent.CHAR_UNDEFINED) {
                    incrementalSearch(keyChar);
                }
            }
        });
        setBackground(UIUtil.getListBackground());
        InputMap inputMap = getInputMap(JComponent.WHEN_FOCUSED);
        ActionMap actionMap = getActionMap();

        ImmutableMap<Integer, Action> keysToActions = ImmutableMap.<Integer, Action>builder()
                .put(KeyEvent.VK_DOWN, new MoveSelectionAction(1, 0))
                .put(KeyEvent.VK_UP, new MoveSelectionAction(-1, 0))
                .put(KeyEvent.VK_LEFT, new MoveSelectionAction(0, -1))
                .put(KeyEvent.VK_RIGHT, new MoveSelectionAction(0, 1)).put(KeyEvent.VK_HOME, new JumpSelection() {
                    @Override
                    public int getIndex() {
                        return 0;
                    }
                }).put(KeyEvent.VK_END, new JumpSelection() {
                    @Override
                    public int getIndex() {
                        return myModel.getSize() - 1;
                    }
                }).build();
        for (Map.Entry<Integer, Action> entry : keysToActions.entrySet()) {
            String key = "selection_move_" + entry.getKey();
            inputMap.put(KeyStroke.getKeyStroke(entry.getKey(), 0), key);
            actionMap.put(key, entry.getValue());
        }
    }

    @Override
    public Dimension getMinimumSize() {
        Dimension size = new Dimension(computeCellSize());
        Insets insets = getInsets();
        size.setSize(size.getWidth() + insets.left + insets.right, size.getHeight() + insets.top + insets.bottom);
        return computeCellSize();
    }

    private static int intDivideRoundUp(int divident, int divisor) {
        return (divident + divisor - 1) / divisor;
    }

    private static int getElementIndex(@NotNull ListModel model, @Nullable Object element) {
        if (element == null) {
            return -1;
        }
        for (int i = 0; i < model.getSize(); i++) {
            Object modelElement = model.getElementAt(i);
            if (Objects.equal(element, modelElement)) {
                return i;
            }
        }
        return -1;
    }

    public void setLabelProvider(@NotNull Function<? super E, String> labelProvider) {
        myLabelProvider = labelProvider;
        repaint(getVisibleRect());
    }

    public void setThumbnailSize(@NotNull Dimension thumbnailSize) {
        if (!Objects.equal(thumbnailSize, myThumbnailSize)) {
            myThumbnailSize = thumbnailSize;
            invalidate();
            repaint(getVisibleRect());
        }
    }

    /**
     * Set the function that obtains the image for the item.
     * <p/>
     * Values are cached. We may need to provide a way to force value update if
     * it is needed at a later time.
     * (Implementation detail) Cache uses identity (==) comparison and does not
     * use {@link Object#equals(Object)}. Please do not rely on this behaviour
     * as it may change without prior notice.
     */
    public void setImageProvider(@NotNull Function<? super E, Image> imageProvider) {
        CacheLoader<? super E, Optional<Image>> cacheLoader = CacheLoader
                .from(ToOptionalFunction.wrap(imageProvider));
        myImagesCache = CacheBuilder.newBuilder().weakKeys().build(cacheLoader);
        repaint(getVisibleRect());
    }

    private void incrementalSearch(char keyChar) {
        final long timestamp = System.currentTimeMillis();
        if (timestamp - myPreviousKeypressTimestamp > INCSEARCH_TIMEOUT_MS) {
            myFilterString = String.valueOf(keyChar);
        } else {
            myFilterString += keyChar;
        }
        final int ind = findMatchingItem();
        myPreviousKeypressTimestamp = timestamp;
        if (ind < 0) {
            boolean resumedSearch = myFilterString.length() > 1;
            myFilterString = "";
            if (resumedSearch) {
                incrementalSearch(keyChar);
            }
        } else {
            setSelectedIndex(ind);
        }
    }

    private int findMatchingItem() {
        int itemCount = myModel.getSize();
        int startingIndex = Math.max(0, mySelectedIndex);
        // Ideal match starts with the search string. Otherwise we try to match
        // words (e.g. "maps" should match Google maps template)
        int secondBest = -1;
        final String normalizedFilterString = StringUtil.toLowerCase(myFilterString);

        for (int i = startingIndex; i < itemCount + startingIndex; i++) {
            // We only should to wrap search if there's no matches "under" the cursor
            final int index = i % itemCount;
            String title = getLabel(index);
            if (!StringUtil.isEmpty(title)) {
                String normalizedTitle = StringUtil.toLowerCase(title);
                if (normalizedTitle.startsWith(normalizedFilterString)) {
                    return index;
                } else if (secondBest < 0 && normalizedTitle.contains(" " + normalizedFilterString)) {
                    secondBest = index;
                }
            }
        }
        return secondBest;
    }

    @Nullable
    private String getLabel(int index) {
        Object element = myModel.getElementAt(index);
        if (element == null) {
            return null;
        } else {
            //noinspection unchecked
            return myLabelProvider.apply((E) element);
        }
    }

    @VisibleForTesting
    protected int getCellAt(@NotNull Point point) {
        Insets borderInsets = getInsets();
        int columnCount = getColumnCount();
        Dimension cellDimensions = computeCellSize();
        int galleryWidth = getClientWidth(borderInsets);

        int offsetX = point.x - borderInsets.left;
        int offsetY = point.y - borderInsets.top;
        if (offsetX >= galleryWidth || offsetX < 0) {
            return -1;
        }
        int column = 0;
        // We may have columns of (slightly) varied width due to rounding errors...
        while (getColumnOffset(column + 1, columnCount, galleryWidth) <= offsetX) {
            column++;
        }

        int row = offsetY / cellDimensions.height;
        if (row < 0 || row > myModel.getSize() / columnCount) {
            return -1;
        }
        int selection = column + row * columnCount;
        return selection >= 0 && selection < myModel.getSize() ? selection : -1;
    }

    private int getClientWidth(Insets borderInsets) {
        return getWidth() - borderInsets.left - borderInsets.right;
    }

    private int getNewSelectionIndex(int vdirection, int hdirection) {
        if (mySelectedIndex < 0) {
            return 0;
        }
        int columnCount = getColumnCount();
        int column = mySelectedIndex % columnCount + hdirection;
        int row = mySelectedIndex / columnCount + vdirection;
        if (column >= 0 && column < columnCount) {
            int newSelection = column + row * columnCount;
            if (newSelection < 0) {
                return 0;
            } else {
                int itemCount = myModel.getSize();
                if (newSelection >= itemCount) {
                    return itemCount - 1;
                } else {
                    return newSelection;
                }
            }
        } else {
            return mySelectedIndex;
        }
    }

    private void updateFocusRectangle() {
        if (mySelectedIndex >= 0) {
            repaint(getVisibleRect());
        }
    }

    @NotNull
    public Insets getCellMargin() {
        return myCellMargin;
    }

    /**
     * Set cell margin value.
     */
    public void setCellMargin(@Nullable Insets cellMargin) {
        cellMargin = cellMargin == null ? DEFAULT_CELL_MARGIN : cellMargin;
        if (!Objects.equal(cellMargin, myCellMargin)) {
            Insets oldInsets = myCellMargin;
            myCellMargin = cellMargin;
            firePropertyChange("cellMargin", oldInsets, cellMargin);
        }
    }

    @Nullable
    public E getSelectedElement() {
        if (mySelectedIndex < 0) {
            return null;
        }
        //noinspection unchecked
        return (E) myModel.getElementAt(mySelectedIndex);
    }

    public void setSelectedElement(@Nullable E element) {
        final int index;
        if (element == null) {
            index = -1;
        } else {
            index = getElementIndex(getModel(), element);
            if (index < 0) {
                throw new NoSuchElementException(element.toString());
            }
        }
        setSelectedIndex(index);
    }

    public int getSelectedIndex() {
        return mySelectedIndex;
    }

    public void setSelectedIndex(int selectedIndex) {
        setSelectedIndex(selectedIndex, true);
    }

    private void setSelectedIndex(int selectedIndex, boolean notifyListeners) {
        assert selectedIndex < myModel.getSize() && selectedIndex >= -1;
        if (selectedIndex != mySelectedIndex) {
            mySelectedIndex = selectedIndex;
            repaint(getVisibleRect());
            revealCell(mySelectedIndex);
            if (notifyListeners) {
                fireSelectionChanged(mySelectedIndex);
            }
        }
    }

    private void fireSelectionChanged(int newSelection) {
        boolean isSelectionListener = false;
        ListSelectionEvent event = new ListSelectionEvent(this, newSelection, newSelection, false);
        for (Object object : myListeners.getListenerList()) {
            if (isSelectionListener) {
                ((ListSelectionListener) object).valueChanged(event);
                isSelectionListener = false;
            } else {
                isSelectionListener = object == ListSelectionListener.class;
            }
        }
    }

    /**
     * @return data model
     */
    @NotNull
    public ListModel getModel() {
        return myModel;
    }

    public void setModel(@NotNull ListModel model) {
        if (!Objects.equal(myModel, model)) {
            final Object element;
            //noinspection ConstantConditions
            if (myModel != null) {
                myModel.removeListDataListener(myListDataListener);
                element = mySelectedIndex < 0 ? null : myModel.getElementAt(mySelectedIndex);
            } else {
                element = null;
            }
            myModel = model;
            myModel.addListDataListener(myListDataListener);
            invalidate();
            setSelectedIndex(getElementIndex(myModel, element));
        }
    }

    @Override
    public Dimension getPreferredSize() {
        Dimension preferredSize = super.getPreferredSize();
        int itemCount = myModel == null ? 0 : myModel.getSize();
        if (isPreferredSizeSet() || itemCount == 0) {
            return preferredSize;
        }
        Insets insets = getInsets();
        int insetsWidth = insets.left + insets.right;
        int insetsHeight = insets.top + insets.bottom;
        Dimension cellSize = computeCellSize();
        final int width = getWidth();
        if (width == 0) {
            return new Dimension(cellSize.width + insetsWidth, cellSize.height + insetsHeight);
        } else { // Avoid horizontal scroll
            int rows = intDivideRoundUp(itemCount, getColumnCount());
            int height = rows * cellSize.height + insetsHeight;
            return new Dimension(Math.max(width, cellSize.width + insetsWidth), height);
        }
    }

    @VisibleForTesting
    protected int getColumnCount() {
        Dimension cellSize = computeCellSize();
        int width = getClientWidth(getInsets());
        int columnCount = Math.max(width / cellSize.width, 1);
        if (myModel != null) {
            int entries = myModel.getSize();
            // If one row, spread out the entries - but don't increase the entry width more then 2x, doesn't look right then
            if (columnCount > entries && columnCount < entries * 2) {
                return entries;
            }
        }
        return columnCount;
    }

    @VisibleForTesting
    protected Dimension computeCellSize() {
        Dimension imageSize = myThumbnailSize;
        int width = imageSize.width + myCellMargin.left + myCellMargin.right;
        int textHeight = getFont().getSize();
        int height = imageSize.height + myCellMargin.top + myCellMargin.bottom + 2 * textHeight;
        return new Dimension(width, height);
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        Rectangle clipRectangle = g.getClipBounds();
        if (isOpaque()) {
            g.setColor(getBackground());
            g.fillRect(clipRectangle.x, clipRectangle.y, clipRectangle.width, clipRectangle.height);
        }

        Dimension cellBounds = computeCellSize();
        int firstColumn = clipRectangle.x / cellBounds.width;
        int lastColumn = (clipRectangle.x + clipRectangle.width) / cellBounds.width;
        int firstRow = clipRectangle.y / cellBounds.height;
        int lastRow = intDivideRoundUp(clipRectangle.y + clipRectangle.height, cellBounds.height);
        Insets borderInsets = getInsets();
        int componentWidth = getClientWidth(borderInsets);
        int columns = getColumnCount();

        for (int row = firstRow; row <= lastRow; row++) {
            for (int column = firstColumn; column <= lastColumn; column++) {
                int cell = row * columns + column;
                if (cell >= myModel.getSize()) {
                    break;
                }
                // Intermediate values like componentWidth/columns are not cached
                // as we are doing integer math here and rounding errors might
                // accumulate and cause "holes" in control we are painting.
                final int cellX = getColumnOffset(column, columns, componentWidth);
                final int width = getColumnOffset(column + 1, columns, componentWidth) - cellX;
                int cellY = row * cellBounds.height + borderInsets.top;
                int cellHeight = cellBounds.height - 1;
                Rectangle bounds = new Rectangle(cellX + borderInsets.left, cellY, width, cellHeight);
                paintCell(g, cell, bounds);
            }
        }
    }

    private void paintCell(Graphics g, int cell, Rectangle cellBounds) {
        String label = getLabel(cell);
        Image thumbnail = getImage(cell);
        drawSelection(g, cell, cellBounds, !StringUtil.isEmpty(label) && thumbnail != null);
        final int thumbnailHeight;
        if (thumbnail != null) {
            Dimension thumbnailSize = myThumbnailSize;
            int imageX = cellBounds.x + (cellBounds.width - thumbnailSize.width) / 2;
            int imageY = cellBounds.y + myCellMargin.top;
            g.drawImage(thumbnail, imageX, imageY, thumbnailSize.width, thumbnailSize.height, null);
            thumbnailHeight = thumbnailSize.height;
        } else {
            thumbnailHeight = 0;
        }
        paintLabel(g, cell, cellBounds, label, thumbnailHeight);
    }

    private void paintLabel(Graphics g, int cell, Rectangle cellBounds, @Nullable String label,
            int thumbnailHeight) {
        if (!StringUtil.isEmpty(label)) {
            final Color fg;
            if (hasFocus() && cell == mySelectedIndex && (getImage(cell) != null || UIUtil.isUnderDarcula())) {
                fg = UIUtil.getTreeSelectionForeground();
            } else {
                fg = UIUtil.getTreeForeground();
            }
            GraphicsUtil.setupAntialiasing(g);
            g.setColor(fg);
            FontMetrics fontMetrics = g.getFontMetrics();
            LineMetrics metrics = fontMetrics.getLineMetrics(label, g);
            int width = fontMetrics.stringWidth(label);

            int textBoxTop = myCellMargin.top + thumbnailHeight;
            int cellBottom = cellBounds.height - myCellMargin.bottom;

            int textY = cellBounds.y
                    + (cellBottom + textBoxTop + (int) (metrics.getHeight() - metrics.getDescent())) / 2;
            int textX = (cellBounds.width - myCellMargin.left - myCellMargin.right - width) / 2 + cellBounds.x
                    + myCellMargin.left;
            g.drawString(label, textX, textY);
        }
    }

    private void drawSelection(Graphics g, int cell, Rectangle cellBounds, boolean paintLabelBackground) {
        if (cell == mySelectedIndex) {
            Color currentColor = g.getColor();
            Color bg = UIUtil.getTreeSelectionBackground(hasFocus());
            g.setColor(bg);
            g.drawRect(cellBounds.x, cellBounds.y, cellBounds.width - 1, cellBounds.height - 1);
            if (paintLabelBackground) {
                int textBoxTop = myThumbnailSize.height + myCellMargin.top;
                g.fillRect(cellBounds.x, cellBounds.y + textBoxTop, cellBounds.width - 1,
                        cellBounds.height - textBoxTop);
            }
            if (hasFocus()) {
                Border border = UIUtil.getTableFocusCellHighlightBorder();
                border.paintBorder(this, g, cellBounds.x, cellBounds.y, cellBounds.width, cellBounds.height);
            }
            g.setColor(currentColor);
        }
    }

    @Nullable
    private Image getImage(int cell) {
        Object elementAt = myModel.getElementAt(cell);
        if (elementAt == null) {
            return null;
        } else {
            try {
                @SuppressWarnings("unchecked")
                Optional<Image> image = myImagesCache.get((E) elementAt);
                return image.orNull();
            } catch (ExecutionException e) {
                Logger.getInstance(getClass()).error(e);
                return null;
            }
        }
    }

    @Override
    public Dimension getPreferredScrollableViewportSize() {
        return getPreferredSize();
    }

    @Override
    public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
        return 10;
    }

    @Override
    public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
        return computeCellSize().height;
    }

    @Override
    public boolean getScrollableTracksViewportWidth() {
        return true;
    }

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

    public void addListSelectionListener(ListSelectionListener listener) {
        myListeners.add(ListSelectionListener.class, listener);
    }

    public void removeListSelectionListener(ListSelectionListener listener) {
        myListeners.remove(ListSelectionListener.class, listener);
    }

    private void reveal() {
        int selectedIndex = getSelectedIndex();
        if (selectedIndex > 0) {
            revealCell(selectedIndex);
        }
    }

    private void revealCell(int selectedIndex) {
        int columnCount = getColumnCount();
        Insets borderInsets = getInsets();

        int width = getClientWidth(borderInsets);
        int height = computeCellSize().height;

        int column = selectedIndex % columnCount;
        int x = getColumnOffset(column, columnCount, width) + borderInsets.left;
        int y = (selectedIndex / columnCount) * height + borderInsets.top;

        scrollRectToVisible(new Rectangle(x, y, width, height));
    }

    @VisibleForTesting
    protected int getColumnOffset(int column, int columnCount, int galleryWidth) {
        if (columnCount <= myModel.getSize()) {
            return column * galleryWidth / columnCount;
        } else {
            return column * computeCellSize().width;
        }
    }

    @Override
    public AccessibleContext getAccessibleContext() {
        if (accessibleContext == null) {
            accessibleContext = new AccessibleASGallery();
        }
        return accessibleContext;
    }

    /**
     * Guava containers do not like <code>null</code> values. This function
     * wraps such values into {@link com.google.common.base.Optional}.
     */
    private static final class ToOptionalFunction<P, R> implements Function<P, Optional<R>> {
        private final Function<P, R> myFunction;

        public ToOptionalFunction(Function<P, R> function) {
            myFunction = function;
        }

        public static <P, R> Function<P, Optional<R>> wrap(Function<P, R> function) {
            return new ToOptionalFunction<P, R>(function);
        }

        @Override
        public Optional<R> apply(P input) {
            R result = myFunction.apply(input);
            return Optional.fromNullable(result);
        }
    }

    private class MoveSelectionAction extends AbstractAction {
        private final int myVdirection;
        private final int myHdirection;

        public MoveSelectionAction(int vdirection, int hdirection) {
            myVdirection = vdirection;
            myHdirection = hdirection;
        }

        @Override
        public boolean isEnabled() {
            return getNewSelectionIndex(myVdirection, myHdirection) != mySelectedIndex;
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            setSelectedIndex(getNewSelectionIndex(myVdirection, myHdirection));
        }
    }

    private abstract class JumpSelection extends AbstractAction {
        public abstract int getIndex();

        @Override
        public boolean isEnabled() {
            return getIndex() >= 0;
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            setSelectedIndex(getIndex());
        }
    }

    private class InternalListDataListener implements ListDataListener {
        @Override
        public void intervalAdded(ListDataEvent e) {
            if (e.getIndex0() <= mySelectedIndex) {
                final int newSelection = mySelectedIndex + e.getIndex1() - e.getIndex0() - 1;
                setSelectedIndex(newSelection, false);
            }
            invalidate();
        }

        @Override
        public void intervalRemoved(ListDataEvent e) {
            int firstRemoved = e.getIndex0();
            if (firstRemoved <= mySelectedIndex) {
                final int lastRemoved = e.getIndex1();
                // Retain selection if this element was not deleted.
                // Move selection down if the element was deleted if there are elements after selected
                // Move selection up otherwise
                // Remove selection if the list is empty
                final int index = mySelectedIndex - (lastRemoved - firstRemoved + 1);
                final int newSelectionIndex = Math.min(Math.max(index, e.getIndex0()), myModel.getSize() - 1);
                // Notify if selected element was deleted
                setSelectedIndex(newSelectionIndex, mySelectedIndex <= lastRemoved);
            }
            invalidate();
        }

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

    private final class AccessibleASGallery extends AccessibleJComponent
            implements PropertyChangeListener, ListSelectionListener {
        private Map<Integer, Accessible> children = Maps.newHashMap();

        public AccessibleASGallery() {
            setAccessibleName(ASGallery.this.getName());
            ASGallery.this.addPropertyChangeListener(this);
            ASGallery.this.addListSelectionListener(this);
        }

        @Override
        public void propertyChange(PropertyChangeEvent evt) {
            if ("name".equals(evt.getPropertyName())) {
                setAccessibleName((String) evt.getNewValue());
            } else if ("model".equals(evt.getPropertyName())) {
                firePropertyChange(AccessibleContext.ACCESSIBLE_INVALIDATE_CHILDREN, null, ASGallery.this);
            }
        }

        @Override
        public int getAccessibleChildrenCount() {
            return getModel().getSize();
        }

        @SuppressWarnings("unchecked")
        @Override
        public Accessible getAccessibleChild(int i) {
            if (!children.containsKey(i)) {
                children.put(i, new AccessibleCell(i));
            }
            return children.get(i);
        }

        @Override
        public AccessibleRole getAccessibleRole() {
            return AccessibleRole.LIST;
        }

        @Override
        public void valueChanged(ListSelectionEvent e) {
            firePropertyChange(AccessibleContext.ACCESSIBLE_ACTIVE_DESCENDANT_PROPERTY, false, true);
            firePropertyChange(AccessibleContext.ACCESSIBLE_SELECTION_PROPERTY, false, true);
        }
    }

    private final class AccessibleCell extends AccessibleJComponent
            implements Accessible, AccessibleComponent, AccessibleAction {
        private final int myIndex;

        public AccessibleCell(int index) {
            myIndex = index;
        }

        @Override
        public AccessibleRole getAccessibleRole() {
            return AccessibleRole.LABEL;
        }

        @Override
        public AccessibleStateSet getAccessibleStateSet() {
            final AccessibleState[] state = { AccessibleState.SELECTABLE, AccessibleState.SINGLE_LINE,
                    AccessibleState.ACTIVE };
            return new AccessibleStateSet(state);
        }

        @Override
        public Accessible getAccessibleParent() {
            return ASGallery.this;
        }

        @Override
        public String getAccessibleName() {
            final String label = getLabel(myIndex);
            return StringUtil.isEmpty(label) ? "No Label" : label;
        }

        @Override
        public int getAccessibleIndexInParent() {
            return myIndex;
        }

        @Override
        public AccessibleContext getAccessibleContext() {
            return this;
        }

        @Override
        public int getAccessibleActionCount() {
            return 1;
        }

        @Override
        public String getAccessibleActionDescription(int i) {
            return AccessibleAction.CLICK;
        }

        @Override
        public boolean doAccessibleAction(int i) {
            setSelectedIndex(myIndex);
            return true;
        }
    }
}