/* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2016 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.mucommander.ui.list; import com.mucommander.commons.collections.AlteredVector; import com.mucommander.commons.collections.VectorChangeListener; import com.mucommander.text.Translator; import javax.swing.*; import java.awt.event.ActionEvent; import java.awt.event.KeyEvent; /** * DynamicList extends JList to work with an {@link AlteredVector} of items which values can be dynamically modified * and automatically reflected in the list, also keeping the current selection consistent. * * <p>It also provides actions to: * <ul> * <li>move the currently selected item up (mapped to 'Shift+UP' and 'Shift+LEFT') * <li>move the currently selected item down (mapped to 'Shift+DOWN' and 'Shift+RIGHT') * <li>remove the currently selected item and selects the previous one (if any) (mapped to 'DELETE' and 'BACKSPACE') * </ul> * * <p>This list only works in 'single selection mode', that means only one item can be selected at a time. * * @author Maxence Bernard */ public class DynamicList<E> extends JList { /** Items displayed in the JList */ private AlteredVector<E> items; /** Custom ListModel that handles modifications made to the AlteredVector */ private DynamicListModel model; /** Action instance which moves the currently selected item up when triggered */ private MoveUpAction moveUpAction; /** Action instance which moves the currently selected item down when triggered */ private MoveDownAction moveDownAction; /** Action instance which, when triggered, removes the currently selected item from the list * and selects the previous item (if any). */ private RemoveAction removeAction; /** * Custom ListModel that handles modifications made to the AlteredVector and reflect them changes in the JList. */ private class DynamicListModel extends AbstractListModel implements VectorChangeListener { public int getSize() { return items.size(); } public Object getElementAt(int i) { if(i<0 || i>=items.size()) return null; return items.elementAt(i); } private void notifyAdded(int fromIndex, int toIndex) { fireIntervalAdded(this, fromIndex, toIndex); } private void notifyRemoved(int fromIndex, int toIndex) { fireIntervalRemoved(this, fromIndex, toIndex); } private void notifyModified(int index) { fireContentsChanged(this, index, index); } ////////////////////////// // VectorChangeListener // ////////////////////////// public void elementsAdded(int startIndex, int nbAdded) { model.notifyAdded(startIndex, startIndex+nbAdded-1); } public void elementsRemoved(int startIndex, int nbRemoved) { model.notifyRemoved(startIndex, startIndex+nbRemoved-1); } public void elementChanged(int index) { model.notifyModified(index); } } /** * Action which moves the currently selected item up when triggered. */ private class MoveUpAction extends AbstractAction { private MoveUpAction() { } public void actionPerformed(ActionEvent actionEvent) { moveItem(getSelectedIndex(), true); // Request focus back on the list requestFocus(); } } /** * Action which moves the currently selected item down when triggered. */ private class MoveDownAction extends AbstractAction { private MoveDownAction() { } public void actionPerformed(ActionEvent actionEvent) { moveItem(getSelectedIndex(), false); // Request focus back on the list requestFocus(); } } /** * Action which, when triggered, removes the currently selected item from the list and selects the previous item (if any). */ private class RemoveAction extends AbstractAction { private RemoveAction() { putValue(Action.NAME, Translator.get("delete")); } public void actionPerformed(ActionEvent actionEvent) { int selectedIndex = getSelectedIndex(); if(!isIndexValid(selectedIndex)) return; items.removeElementAt(selectedIndex); // Select previous item (if there is one) and make sure it is visible. int nbItems = items.size(); if(nbItems>0) selectAndScroll(Math.min(selectedIndex, nbItems-1)); // Request focus back on the list requestFocus(); } } /** * Creates a new DynamicList using the items stored in the given {@link AlteredVector}. * These items (if any) will be visible whenever this list is visible, and the first item (if any) will be selected. * * <p>Any change made to the AlteredVector will be automatically reflected in the list, except for changes * made to the item instances themselves for which {@link #itemModified(int, boolean)} will need to * be called explicitely. * * @param items items to add to the list */ public DynamicList(AlteredVector<E> items) { this.items = items; // Use a custom ListModel this.model = new DynamicListModel(); setModel(model); // Listen to changes made to the Vector this.items.addVectorChangeListener(model); // Allow only one item to be selected at a time setSelectionMode(ListSelectionModel.SINGLE_SELECTION); // Select first item, if there is at least one if(items.size()>0) setSelectedIndex(0); // Create action instances this.moveUpAction = new MoveUpAction(); this.moveDownAction = new MoveDownAction(); this.removeAction = new RemoveAction(); InputMap inputMap = getInputMap(); ActionMap actionMap = getActionMap(); // Map 'Delete' and 'Backspace' to RemoveAction Class<? extends Action> actionClass = removeAction.getClass(); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), actionClass); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_BACK_SPACE, 0), actionClass); actionMap.put(actionClass, removeAction); // Map 'Shift+Up'/'Meta+Up' and 'Shift+Left'/'Meta+Left' to MoveUpAction actionClass = moveUpAction.getClass(); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, KeyEvent.SHIFT_MASK), actionClass); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, KeyEvent.META_MASK), actionClass); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, KeyEvent.SHIFT_MASK), actionClass); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, KeyEvent.META_MASK), actionClass); actionMap.put(actionClass, moveUpAction); // Map 'Shift+Down'/'Meta+Down' and 'Shift+Right'/'Meta+Right' to MoveDownAction actionClass = moveDownAction.getClass(); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, KeyEvent.SHIFT_MASK), actionClass); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, KeyEvent.META_MASK), actionClass); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, KeyEvent.SHIFT_MASK), actionClass); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, KeyEvent.META_MASK), actionClass); actionMap.put(actionClass, moveDownAction); } /** * Returns the items displayed by this DynamicList. */ public AlteredVector<E> getItems() { return items; } /** * Selects the item located at the given index and if necessary scrolls the list to make sure * that the new selection is visible within the viewport. * * @param index index of the item to select */ public void selectAndScroll(int index) { setSelectedIndex(index); ensureIndexIsVisible(index); } /** * Returns true if the given index is without the bounds of the items Vector. * * @param index index to test * @return true if the given index is without the bounds of the items. */ public boolean isIndexValid(int index) { return index>=0 && index<items.size(); } /** * This method should be called whenever an item in the items vector has been modified in order to properly * repaint the list and reflect the change. * * @param index index of the item in the Vector that has been modified * @param selectItem if true, the modified item will be selected */ public void itemModified(int index, boolean selectItem) { // Make sure that the given index is not out of bounds if(!isIndexValid(index)) return; // Notify ListModel in order to properly repaint list model.notifyModified(index); } /** * Moves the item located at the given index up or down, swapping its place with the previous or next item. * * @param index the item to move * @param moveUp if true the item at the given index will be moved up, if not moved down */ public void moveItem(int index, boolean moveUp) { // Make sure that the given index is not out of bounds if(!isIndexValid(index)) return; int newIndex; // Calculate the new index for the item to move if (moveUp) { // Item is already at the top, do nothing if(index<1) return; newIndex = index-1; } else { // Item is already at the bottom, do nothing if(index>=items.size()-1) return; newIndex = index+1; } // Swap values in the Vector E tmp = items.elementAt(index); items.setElementAt(items.elementAt(newIndex), index); items.setElementAt(tmp, newIndex); // Select moved item and make sure it is visible selectAndScroll(newIndex); } /** * Returns an Action that can be used for instance in a JButton to * move the item currently selected item up, swapping it with the previous item. * * @return an Action that moves the currently selected item up. */ public Action getMoveUpAction() { return moveUpAction; } /** * Returns an Action that can be used for instance in a JButton to * move the item currently selected item down, swapping it with the following item. * * @return an Action that moves the currently selected item down. */ public Action getMoveDownAction() { return moveDownAction; } /** * Returns an Action that can be used for instance in a JButton to * remove the currently selected item. * * @return an Action that removes the currently selected item. */ public Action getRemoveAction() { return removeAction; } }