package com.limegroup.gnutella.gui.search;

import java.awt.Insets;
import java.awt.GridBagLayout;
import java.awt.GridBagConstraints;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.GridLayout;
import java.awt.Point;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.Iterator;

import javax.swing.AbstractListModel;
import javax.swing.BorderFactory;
import javax.swing.DefaultListCellRenderer;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.ListCellRenderer;
import javax.swing.ListSelectionModel;
import javax.swing.UIManager;
import javax.swing.SwingConstants;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.ListDataEvent;
import javax.swing.event.ListDataListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;

import com.limegroup.gnutella.gui.GUIMediator;
import com.limegroup.gnutella.gui.GUIUtils;
import com.limegroup.gnutella.gui.tables.CircularIcon;
import com.limegroup.gnutella.gui.tables.IconAndNameHolder;
import com.limegroup.gnutella.gui.tables.SortArrowIcon;
import com.limegroup.gnutella.gui.themes.ThemeFileHandler;
import com.limegroup.gnutella.settings.BooleanSetting;
import com.limegroup.gnutella.util.StringUtils;

/**
 * A listbox with a header.
 *
 * Except for the header, all backgrounds are opaque.
 */
class FilterBox extends JPanel {    
    /**
     * The renderer to use on all lists.
     */
    private static final ListCellRenderer RENDERER = new Renderer();
    
    /**
     * The listener that ensures the selected row is always visible.
     */
    private static final ListSelectionListener MOVER = new Mover();
    
    /**
     * The setting that controls row striping.
     *
     * (Ideally we wouldn't reference ResultPanel, but it's easiest.)
     */
    private static final BooleanSetting STRIPE_ROWS = 
        ResultPanel.SEARCH_SETTINGS.ROWSTRIPE;
    
    /**
     * The string to use for 'Options'.
     */
    private static final String OPTIONS =
        GUIMediator.getStringResource("SEARCH_FILTER_OPTIONS");
        
    /**
     * The string to use for 'Option'.
     */
    private static final String OPTION =
        GUIMediator.getStringResource("SEARCH_FILTER_OPTION");
    
    /**
     * The property name stored within the JList that keeps the currently
     * selected value.
     *
     * This is used when the contents of the model change so that we can
     * reselect the old value.
     */
    private static final String SELECTED = "SELECTION";
    
    /**
     * The property named stored within the JList that keeps the current
     * matching value.
     */
    private static final String MATCH = "MATCH";
    
    /**
     * The property name stored within the JList that keeps the current
     * matching index.
     */
    private static final String MATCH_IDX = "MATCH_IDX";
    
    /**
     * The ditherer drawing the title of the box.
     */
    private final Ditherer DITHERER =
            new Ditherer(10,
                        ThemeFileHandler.FILTER_TITLE_TOP_COLOR.getValue(), 
                        ThemeFileHandler.FILTER_TITLE_COLOR.getValue()
                        );    
    
    
    /**
     * The JLabel with the title.
     */
    protected final JLabel TITLE;
    
    /**
     * The panel with the title in it.
     */
    protected final JPanel TITLE_PANEL;
    
    /**
     * The JList with the list choices.
     */
    protected final JList LIST;
    
    /**
     * The panel the list is contained in.
     */
    protected final JPanel LIST_PANEL;    
    
    /**
     * The delegate model for our JList.
     */
    protected final ListModelDelegator DELEGATOR;
    
    /**
     * The label containing the icon to restore, minimize or maximize.
     */
    protected final JLabel CONTROLS;
    
    /**
     * The MetadataModel from which the selectors should
     * extract their values.
     */
    protected final MetadataModel MODEL;
    
    /**
     * The only ChangeEvent to ever use.
     */
    protected final ChangeEvent EVENT = new ChangeEvent(this);
    
    /**
     The selector that this FilterBox is acting on.
     */
    protected Selector _selector;
    
    /**
     * The ChangeListener for selectors.
     *
     * TODO: Allow more than one.
     */
    protected ChangeListener _selectorChangeListener;
    
    /**
     * The listener for the state of this filter box (minimized/maximized)
     *
     * TODO: Allow more than one.
     */
    protected ChangeListener _stateChangeListener;
    
    /**
     * The current state of this filter box (whether or not it is minimized)
     */
    private boolean _minimized = false;
    
    /**
     * Whether or not we are allowed to minimize.
     */
    private boolean _canMinimize = true;
    
    /**
     * Whether or not the mouse has been clicked on the box's selection area.
     * If it has, we stop doing 'point scoring' to select the closest matching
     * value.
     */
    private boolean _mouseClicked = false;
    
    /**
     * The currently requested value (full string).
     */
    private String _requestedValue;
    
    /**
     * The currently requested value split into tokens.
     */
    private String[] _requestedValues;
    
    /**
     * Constructs a new FilterBox with the specified model & selector.
     */
    FilterBox(MetadataModel model, Selector selector) {
        super();
        setLayout(new BorderLayout());
        if(model == null)
            throw new NullPointerException("no model");
        if(selector == null)
            throw new NullPointerException("no selector");

        CONTROLS = new JLabel();
        TITLE = new JLabel();
        TITLE.setFont(UIManager.getFont("Table.font.bold"));        
        TITLE_PANEL = createTitlePanel(TITLE, CONTROLS);
        LIST = new JList();
        DELEGATOR = new ListModelDelegator();
        JScrollPane pane = new JScrollPane(LIST);
        LIST_PANEL = addToPanel(pane, false);
        MODEL = model;
        
        add(TITLE_PANEL, BorderLayout.NORTH);
        add(LIST_PANEL, BorderLayout.CENTER);

        LIST.setBackground(ThemeFileHandler.TABLE_BACKGROUND_COLOR.getValue());
        LIST.setCellRenderer(RENDERER);
        LIST.addListSelectionListener(MOVER);
        LIST.setModel(DELEGATOR);
        LIST.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
        LIST.addMouseListener(new MouseListener() {
            public void mouseClicked(MouseEvent e) {}
            public void mouseEntered(MouseEvent e) {}
            public void mouseExited(MouseEvent e) {}
            public void mousePressed(MouseEvent e) {
                _mouseClicked = true;
                LIST.removeMouseListener(this);
            }
            public void mouseReleased(MouseEvent e) {}
        });

        setBorder(BorderFactory.createCompoundBorder(
            BorderFactory.createRaisedBevelBorder(),
            BorderFactory.createLoweredBevelBorder())
        );

        pane.setHorizontalScrollBarPolicy(
            JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
            
        setSelector(selector);
    }
    
    /**
     * Returns the list used in this box.
     */
    JList getList() {
        return LIST;
    }
    
    /**
     * Gets the component that should display.
     * If minimized, only the TITLE_PANEL.
     * Otherwise, both title & list.
     */
    JComponent getComponent() {
        if(_minimized) {
            return TITLE_PANEL;
        } else {
            removeAll();
            add(TITLE_PANEL, BorderLayout.NORTH);
            add(LIST_PANEL, BorderLayout.CENTER);
            return this;
        }
    }
   
    
    /**
     * Minimizes the list.
     */
    void minimize() {
        _minimized = true;
        _selector.setMinimized(true);
        TITLE.setFont(UIManager.getFont("Table.font"));
        TITLE_PANEL.setBorder(getBorder());
        invalidate();
        CONTROLS.setIcon(SortArrowIcon.getAscendingIcon());
        updateTitle();
        revalidate();
        if(_stateChangeListener != null)
            _stateChangeListener.stateChanged(EVENT);
    }
    
    /**
     * Restores the list.
     */
    void restore() {
        _minimized = false;
        _selector.setMinimized(false);
        TITLE.setFont(UIManager.getFont("Table.font.bold"));
        TITLE_PANEL.setBorder(null);
        updateTitle();
        CONTROLS.setIcon(SortArrowIcon.getDescendingIcon());        
        revalidate();
        if(_stateChangeListener != null)
            _stateChangeListener.stateChanged(EVENT);
    }
    
    /**
     * Determines whether or not minimizing is allowed.
     */
    void setCanMinimize(boolean allowed) {
        if(_minimized)
            return;
        
        if(allowed) {
            CONTROLS.setIcon(SortArrowIcon.getDescendingIcon());
        } else {
            CONTROLS.setIcon(null);
        }
        _canMinimize = allowed;
    }
    
    /**
     * Returns whether or not this box is minimized.
     */
    boolean isMinimized() {
        return _minimized;
    }
    
    /**
     * Returns the MetadataModel used to build the lists.
     */
    MetadataModel getMetadataModel() {
        return MODEL;
    }
    
    /**
     * Returns the active selector.
     */
    Selector getSelector() {
        return _selector;
    }
    
    /**
     * Sets the new ChangeListener for selector-changing events.
     */
    void setSelectorChangeListener(ChangeListener listener) {
        _selectorChangeListener = listener;
    }
    
    /**
     * Sets the new ChangeListener for state-changing events.
     */
    void setStateChangeListener(ChangeListener listener) {
        _stateChangeListener = listener;
    }
    
    /**
     * Sets a new active selector.
     */
    void setSelector(Selector selector) {
        if(selector == null)
            throw new NullPointerException("no selector");
    
        // erase selections & matching values.
        LIST.putClientProperty(MATCH, null);
        LIST.putClientProperty(MATCH_IDX, null);
        LIST.putClientProperty(SELECTED, null);

        ListModelMap oldModel = 
                    _selector==null ? null : MODEL.getListModelMap(_selector);
        _selector = selector;
        ListModelMap newModel = MODEL.getListModelMap(selector);
        setModel(newModel);
        DELEGATOR.changeListener(oldModel, newModel);

        if(selector.isMinimized())
            minimize();
        
        if(_selectorChangeListener != null)
            _selectorChangeListener.stateChanged(EVENT);

        updateTitle();
    }
    
    /**
     * Updates the text in the title.
     */
    void updateTitle() {
        Object sel = getSelectedValue();
        String title = getTitle(_selector);
        String oldTitle = TITLE.getText();
        if(!_minimized) {
            TITLE.setText(title);
        } else {
            String extra;
            if(sel == null || MetadataModel.isAll(sel)) {
                int size = DELEGATOR.getSize() -1;
                if(size == 1)
                    extra = size + " " + OPTION;
                else
                    extra = size + " " + OPTIONS;
            } else {
                extra = sel.toString();
            }
            TITLE.setText(title + " (" + extra + ")");
        }
        if(!oldTitle.equals(TITLE.getText()))
            TITLE.setPreferredSize(new Dimension(GUIUtils.width(TITLE), 13));
    }    
    
    /**
     * Returns the currently selected item.
     */
    Object getSelectedValue() {
        int idx = LIST.getSelectedIndex();
        if(idx < 0 || idx >= DELEGATOR.getSize())
            return null;
        else
            return LIST.getSelectedValue();
    }
    
    /**
     * Sets the value that we want to be selected if it arrives.
     */
    void setRequestedValue(String value) {
        _mouseClicked = false;
        _requestedValue = value.trim().toLowerCase();
        _requestedValues = StringUtils.split(_requestedValue, ' ');
        selectValueFromScore();
    }
    
    /**
     * Clears the selected value on the box.
     */
    void clearSelection() {
        LIST.putClientProperty(SELECTED, null);
        LIST.clearSelection();
    }
    
    /**
     * Sets the model of the underlying JList.
     */
	void setModel(ListModelMap view) {
        Object selected = LIST.getClientProperty(SELECTED);
	    DELEGATOR.setDelegate(view);
	    if(selected != null) {
	        int index = indexOf(selected);
	        if(index != -1) {
	            setSelectedIndex(index, true);
	            selectMatchingValue(false);
            } else {
                LIST.clearSelection();
                selectMatchingValue(true);
            }
        } else {
            // Make sure nothing is set.
            LIST.clearSelection();
            selectMatchingValue(true);            
        }
        updateTitle();
    }
    
    /**
     * Retrieves the model of the underlying list.
     */
    ListModelMap getModel() {
        return DELEGATOR.getDelegate();
    }    
    
    /**
     * Adds a ListSelectionListener to the underlying JList & 
     * A ListDataListener to our model delegator.
     */
    void addSelectionListener(ListDataListener ldl, ListSelectionListener lsl) {
        LIST.addListSelectionListener(lsl);
        DELEGATOR.addListDataListener(ldl);
    }
    
    /**
     * Creates the title JPanel, including the 'title' label
     * and the 'controls' label.
     */
    protected JPanel createTitlePanel(JLabel title, JLabel controls) {
        title.setHorizontalAlignment(SwingConstants.CENTER);
        
        JLabel button = new JLabel(CircularIcon.instance());
        button.addMouseListener(new MouseAdapter() {
            public void mouseClicked(MouseEvent e) {
                Point p = e.getPoint();
                JComponent source = (JComponent)e.getSource();
                SelectorMenu menu = new SelectorMenu(FilterBox.this);
                menu.getComponent().show(source, p.x+1, p.y-6);
            }
        });
        
        controls.setIcon(SortArrowIcon.getDescendingIcon());
        controls.addMouseListener(new MouseAdapter() {
            public void mouseClicked(MouseEvent e) {
                if(_minimized)
                    restore();
                else if(_canMinimize)
                    minimize();
                    
            }
        });
        
        JPanel panel = new DitherPanel(DITHERER);
        panel.setBackground(ThemeFileHandler.FILTER_TITLE_COLOR.getValue());        
        panel.setLayout(new GridBagLayout());
        GridBagConstraints c = new GridBagConstraints();
        c.anchor = GridBagConstraints.WEST;
        c.insets = new Insets(0, 5, 0, 0);
        panel.add(button, c);
        c.anchor = GridBagConstraints.CENTER;
        c.weightx = 1;
        c.fill = GridBagConstraints.BOTH;
        c.insets = new Insets(0, 1, 0, 1);
        panel.add(title, c);
        c.anchor = GridBagConstraints.EAST;
        c.weightx = 0;
        c.fill = GridBagConstraints.NONE;
        c.insets = new Insets(0, 0, 0, 5);
        panel.add(controls, c);
        
        panel.setMaximumSize(new Dimension(9999999, CircularIcon.instance().getIconHeight()+4));

        return panel;
    }
        
    /**
     * Adds the specified component within a possibly opaque JPanel.
     */
    protected JPanel addToPanel(JComponent comp, boolean opaque) {
        JPanel panel = new JPanel(new GridLayout());
        panel.add(comp);
        panel.setMaximumSize(comp.getMaximumSize());
        panel.setPreferredSize(comp.getPreferredSize());
        return panel;
    }
    
    /**
     * Selects a possible value based on the scores of all possible choices.
     */
    private void selectValueFromScore() {
        if(_requestedValue == null)
            return;
            
        ListModelMap map = DELEGATOR.getDelegate();
        int highScore = 0;
        int index = -1;
        String matchingValue = null;
        int i = 1; // start at one because of the 'All' option we're ignoring.
        for(Iterator iter = map.iterator(); iter.hasNext(); i++) {
            Object next = iter.next();
            if(!(next instanceof String))
                continue;

            String val = (String)next;
            int score = score(val, highScore);
            if(score > highScore) {
                highScore = score;
                index = i;
                matchingValue = val;
            }
            // If we had a perfect match, fake a mouse click so we don't
            // score any more.
            if(highScore == 100) {
                _mouseClicked = true;
                break;
            }
        }
        if(index != -1) {
            LIST.putClientProperty(MATCH, matchingValue);
            LIST.putClientProperty(MATCH_IDX, new Integer(index));
            LIST.ensureIndexIsVisible(index);
        }
    }
    
    /**
     * Ensures that the matching value is visible.
     */
    private void selectMatchingValue(boolean scroll) {
        String match = (String)LIST.getClientProperty(MATCH);
        if(match != null) {
            int idx = indexOf(match);
            if(idx != -1) {
                LIST.putClientProperty(MATCH_IDX, new Integer(idx));
                if(scroll)
                     LIST.ensureIndexIsVisible(idx);
            } else {
                LIST.putClientProperty(MATCH_IDX, null);
            }
        } 
    }
    
    /** 
     * Returns the index of the value in the list's model.
     */
    private int indexOf(Object value) {
        ListModelMap view = DELEGATOR.getDelegate();
        if(view != null) {
            return view.indexOf(value);
        } else {
            return -1;
        }
    }
    
    /**
     * Sets the given index to be the selected index & optionally
     * scrolls to make it visible.
     */
    private void setSelectedIndex(int index, boolean scroll) {
        LIST.setSelectedIndex(index);
        if(scroll)
            LIST.ensureIndexIsVisible(index);
        LIST.repaint();
    }
    
    /**
     * Scores how close the given value matches the requested value.
     *
     * The scoring works the following way:
     *  - 100 points for exact matches.
     *  -  99 points for matches containing the substring
     *  -  +1 points for each word that matches
     */
    private int score(String value, int oldScore) {
        value = value.toLowerCase();
        
        // Exact match.
        if(_requestedValue.equals(value.trim()))
            return 100;
        
        // no point in trying any more.
        if(oldScore > 99)
            return 0;
        
        // Exact substring match
        if(value.indexOf(_requestedValue) > -1)
            return 99;
            
        // no point in trying anymore.
        if(oldScore > 98)
            return 0;
            
        // If more than one token, iterate for matches.
        if(_requestedValues.length == 1) {
            return 0;
        } else if(_requestedValues.length == oldScore) {
            // already have the highest possible score?
            return 0;
        } else {
            int matches = 0;
            for(int i = 0; i < _requestedValues.length; i++) {
                if(value.indexOf(_requestedValues[i]) > -1)
                    matches++;
            }
            return matches;
        }
    }

    /**
     * Determines the title of the specified selector.
     */
    private static String getTitle(Selector selector) {
        return selector.getTitle();
    }
    
    /**
     * A delegate list model so that we can register for change events on a
     * single model and just change the delegate model, without losing any
     * registered listeners.
     *
     * Also ensures that the list's selection is maintained when the contents
     * of the model change.
     */
    private class ListModelDelegator extends AbstractListModel 
                                            implements ListDataListener {
        /**
         * The delegate model.
         */
        private ListModelMap _delegate = null;
        
        /**
         * Sets a new delegate model, and calls for refresh
         */
        void setDelegate(ListModelMap delegate) {
            if(_delegate == delegate)
                return;
            _delegate = delegate;
            fireContentsChanged(this, 0, getSize());
        }
        
        /**
         * Unregisters this from listening for events on the old model, and
         * registers for events on the new model
         */
        void changeListener(ListModelMap oldModel, ListModelMap newModel) {
            // remove our old listener.
            if(oldModel != null)
                oldModel.removeListDataListener(this);
            // add a new listener.
            newModel.addListDataListener(this);
        }

        /**
         * Retrieves the delegate model.
         */
        ListModelMap getDelegate() {
            return _delegate;
        }
        
        /////////////////////////////////////////////////////////////////////
        // ListModel methods.
        // These delegate to the underlying model.
        
        /**
         * Returns the size of the delegate model.
         */
        public int getSize() {
            if(_delegate != null)
                return _delegate.getSize();
            else
                return 0;
        }
        
        /**
         * Returns the element at the delegate's index.
         */
        public Object getElementAt(int idx) {
            if(_delegate != null) 
                return _delegate.getElementAt(idx);
            else
                return null;
        }
        
        /////////////////////////////////////////////////////////////////////
        // Forwarding of ListDataEvents.
        // Note that these methods use the listenerList variable,
        // a protected variable from AbstractListModel, containing
        // the list of listeners.
        
        /**
         * Forwards interval added events from the delegate list.
         */
        public void intervalAdded(ListDataEvent e) {
            e = new ListDataEvent(this, e.getType(), 
                                  e.getIndex0(), e.getIndex1());
	        Object[] listeners = listenerList.getListenerList();
        	for (int i = listeners.length - 2; i >= 0; i -= 2)
        	    if (listeners[i] == ListDataListener.class)
        	        ((ListDataListener)listeners[i+1]).intervalAdded(e);
        }
        
        /**
         * Forwards interval removed events from the delegate list.
         */
        public void intervalRemoved(ListDataEvent e) {
            e = new ListDataEvent(this, e.getType(), 
                                  e.getIndex0(), e.getIndex1());
	        Object[] listeners = listenerList.getListenerList();
        	for (int i = listeners.length - 2; i >= 0; i -= 2)
        	    if (listeners[i] == ListDataListener.class)
        	        ((ListDataListener)listeners[i+1]).intervalRemoved(e);
        }
        
        /**
         * Forwards contents changed events from the delegate list.
         */
        public void contentsChanged(ListDataEvent e) {
            e = new ListDataEvent(this, e.getType(), 
                                  e.getIndex0(), e.getIndex1());
	        Object[] listeners = listenerList.getListenerList();
        	for (int i = listeners.length - 2; i >= 0; i -= 2)
        	    if (listeners[i] == ListDataListener.class)
        	        ((ListDataListener)listeners[i+1]).contentsChanged(e);
            
            // If the user hasn't manually selected anything on the list,
            // select a value based on the score of the wanted-selection
            // and the possible choices.
            // Otherwise (the user has selected something) maintain the
            // selection.
            if(!_mouseClicked && _requestedValue != null) {
                selectValueFromScore();
            } else {
                boolean matching = LIST.getClientProperty(MATCH) != null;
                Object selected = LIST.getClientProperty(SELECTED);
                if(selected != null) {
                    setSelectedIndex(indexOf(selected), true);
                    if(matching)
                        selectMatchingValue(false);
                } else if(matching) {
                    selectMatchingValue(true);
                }
            }
            updateTitle();
        }
    }
    
    /**
     * Removes the Renderer from it's parent (a CellRendererPane) to ensure
     * that the search is fully erased from memory.
     */
    public static void clearRenderer() {
        Container parent = ((Component)RENDERER).getParent();
        if(parent != null)
            parent.remove((Component)RENDERER);
    }   
    
    /**
     * The renderer for Filter Boxes.
     *
     * Supports drawing an icon & text if the value is an IconAndNameHolder,
     * or just the text (using the toString method) if anything else.
     *
     * Draws the line transparent unless it is selected.
     */
    private static class Renderer extends DefaultListCellRenderer {
        Renderer() {
            super();
        }
        
    	/**
    	 * Returns the <tt>Component</tt> that displays the icons & names
    	 * based on the <tt>IconAndNameHolder</tt> object.
    	 */
    	public Component getListCellRendererComponent(JList list, Object value,
                                                   int idx, boolean isSelected,
                                                   boolean cellHasFocus) {
            Integer matchIdx = (Integer)list.getClientProperty(MATCH_IDX);
            boolean match = matchIdx != null && idx == matchIdx.intValue();
            setComponentOrientation(list.getComponentOrientation());

            if (isSelected) {
				if (match) {
					setFont(UIManager.getFont("Table.font.bold"));
				} else {
					setFont(UIManager.getFont("Table.font"));
				}
                setOpaque(true);
                setBackground(list.getSelectionBackground());
                setForeground(list.getSelectionForeground());
            } else {
                if(match) {
                    setFont(UIManager.getFont("Table.font.bold"));
                    setForeground(list.getForeground());
                    //TODO: ideally we would change the color also,
                    //      but what color should we use?
                    //setForeground(
                    //  ThemeFileHandler.FILTER_TITLE_COLOR.getValue());
                } else {
                    setFont(UIManager.getFont("Table.font"));                
                    setForeground(list.getForeground());
                }
                if(idx % 2 == 0 && STRIPE_ROWS.getValue()) {
                    setOpaque(true);
                    setBackground(ThemeFileHandler.TABLE_ALTERNATE_COLOR.getValue());
                } else {
                    setOpaque(false);
                }
            }
    	    
    	    if(value instanceof IconAndNameHolder) {
    	        IconAndNameHolder in = (IconAndNameHolder)value;
                setIcon(in.getIcon());
                setText((value == null) ? "" : in.getName());
            } else {
                setIcon(null);
                setText((value == null) ? "" : value.toString());
            }
            setEnabled(list.isEnabled());
            setBorder((cellHasFocus) ? 
                UIManager.getBorder("List.focusCellHighlightBorder") : noFocusBorder);
            return this;
        }
    }
    
    /**
     * The listener for list selection events, scrolls to the selected row.
     * A single one is used for all filter boxes.
     */
    private static class Mover implements ListSelectionListener {
        public void valueChanged(ListSelectionEvent event) {
            if(event.getValueIsAdjusting())
                return;
            
            JList source = (JList)event.getSource();
            int selIndex = source.getSelectedIndex();
            if(selIndex != -1) {
                source.ensureIndexIsVisible(selIndex);
                source.putClientProperty(SELECTED, source.getSelectedValue());
            }
        }
    }
}

