/*
 * This file is part of muCommander, http://www.mucommander.com
 *
 * 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.dialog.pref.general;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.image.BufferedImage;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import javax.swing.BorderFactory;
import javax.swing.DefaultCellEditor;
import javax.swing.ImageIcon;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.JViewport;
import javax.swing.KeyStroke;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableModel;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.mucommander.commons.runtime.OsFamily;
import com.mucommander.commons.util.Pair;
import com.mucommander.text.Translator;
import com.mucommander.ui.action.ActionDescriptor;
import com.mucommander.ui.action.ActionKeymap;
import com.mucommander.ui.action.ActionManager;
import com.mucommander.ui.action.ActionProperties;
import com.mucommander.ui.dialog.pref.component.PrefTable;
import com.mucommander.ui.dialog.pref.general.ShortcutsPanel.TooltipBar;
import com.mucommander.ui.icon.IconManager;
import com.mucommander.ui.main.table.CellLabel;
import com.mucommander.ui.table.CenteredTableHeaderRenderer;
import com.mucommander.ui.text.KeyStrokeUtils;
import com.mucommander.ui.theme.ColorChangedEvent;
import com.mucommander.ui.theme.FontChangedEvent;
import com.mucommander.ui.theme.Theme;
import com.mucommander.ui.theme.ThemeCache;
import com.mucommander.ui.theme.ThemeListener;

/**
 * This class is the table in which the actions and their shortcuts are
 * present in the ShortcutsPanel.
 * 
 * @author Arik Hadas, Johann Schmitz (johann@j-schmitz.net)
 */
public class ShortcutsTable extends PrefTable implements KeyListener, ListSelectionListener, FocusListener {
    private static final Logger LOGGER = LoggerFactory.getLogger(ShortcutsTable.class);

    /** Base width and height of icons for a scale factor of 1 */
    private final static int BASE_ICON_DIMENSION = 16;

    /** Transparent icon used to align non-locked themes with the others. */
    private static ImageIcon transparentIcon = new ImageIcon(new BufferedImage(BASE_ICON_DIMENSION, BASE_ICON_DIMENSION, BufferedImage.TYPE_INT_ARGB));

    /** Private object used to indicate that a delete operation was made */
    public static final Object DELETE = new Object();

    private ShortcutsTableData data;

    /** Comparator of actions according to their labels */
    private static final Comparator<String> ACTIONS_COMPARATOR = new Comparator<String>() {
        public int compare(String id1, String id2) {
            String label1 = ActionProperties.getActionLabel(id1);
            if (label1 == null)
                return 1;

            String label2 = ActionProperties.getActionLabel(id2);
            if (label2 == null)
                return -1;

            return label1.compareTo(label2);
        }
    };

    /** Last selected row in the table */
    private int lastSelectedRow = -1;

    /** The bar below the table in which messages can be displayed */
    private TooltipBar tooltipBar;

    /** Number of mouse clicks required to enter cell's editing state */
    private static final int NUM_OF_CLICKS_TO_ENTER_EDITING_STATE = 2;

    /** Column indexes */
    public static final int ACTION_DESCRIPTION_COLUMN_INDEX = 0;
    public static final int ACCELERATOR_COLUMN_INDEX = 1;
    public static final int ALTERNATE_ACCELERATOR_COLUMN_INDEX = 2;

    /** Number of columns in the table */
    private static final int NUM_OF_COLUMNS = 3;

    /** After the following time (msec) that cell is being in editing state
     *  and no pressing was made, the editing state is canceled */
    private static final int CELL_EDITING_STATE_PERIOD = 3000;

    /** Thread that cancel cell's editing state after CELL_EDITING_STATE_PERIOD time */
    private CancelEditingStateThread cancelEditingStateThread;

    private ShortcutsTableCellRenderer cellRenderer;

    public ShortcutsTable(TooltipBar tooltipBar) {
        super();
        this.tooltipBar = tooltipBar;

        setModel(new KeymapTableModel(data = new ShortcutsTableData()));

        cellRenderer = new ShortcutsTableCellRenderer();
        setShowGrid(false);
        setIntercellSpacing(new Dimension(0,0));
        setRowHeight(Math.max(getRowHeight(), BASE_ICON_DIMENSION + 2 * CellLabel.CELL_BORDER_HEIGHT));
        getTableHeader().setReorderingAllowed(false);
        setRowSelectionAllowed(false);
        setAutoCreateColumnsFromModel(false);
        setCellSelectionEnabled(false);
        setColumnSelectionAllowed(false);
        setDragEnabled(false);

        if (!usesTableHeaderRenderingProperties()) {
            CenteredTableHeaderRenderer renderer = new CenteredTableHeaderRenderer();
            getColumnModel().getColumn(ACTION_DESCRIPTION_COLUMN_INDEX).setHeaderRenderer(renderer);
            getColumnModel().getColumn(ACCELERATOR_COLUMN_INDEX).setHeaderRenderer(renderer);
            getColumnModel().getColumn(ALTERNATE_ACCELERATOR_COLUMN_INDEX).setHeaderRenderer(renderer);
        }

        putClientProperty("terminateEditOnFocusLost", Boolean.TRUE);

        addKeyListener(this);
        addFocusListener(this);
        getSelectionModel().addListSelectionListener(this);
        getColumnModel().getSelectionModel().addListSelectionListener(this);
    }

    /**
     * Paints a dotted border of the specified width, height and {@link Color}, and using the given {@link Graphics}
     * object.
     *
     * @param g Graphics object to use for painting
     * @param width border width
     * @param height border height
     * @param color border color
     */
    private static void paintDottedBorder(Graphics g, int width, int height, Color color) {
        Graphics2D g2 = (Graphics2D) g;
        g2.setStroke(new BasicStroke(1.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_MITER,
                2.0f, new float[]{2.0f}, 0));
        g2.setColor(color);

        g2.drawLine(0, 0, width, 0);
        g2.drawLine(0, height - 1, width, height - 1);
        g2.drawLine(0, 0, 0, height - 1);
        g2.drawLine(width-1, 0, width-1, height - 1);
    }

    private static boolean usesTableHeaderRenderingProperties() {
        return OsFamily.MAC_OS.isCurrent();
    }

    /**
     * Assumes table is contained in a JScrollPane. Scrolls the
     * cell (rowIndex, vColIndex) so that it is visible within the viewport.
     */
    public void scrollToVisible(int rowIndex, int vColIndex) {
        if (!(getParent() instanceof JViewport))
            return;

        JViewport viewport = (JViewport) getParent();

        // This rectangle is relative to the table where the
        // northwest corner of cell (0,0) is always (0,0).
        Rectangle rect = getCellRect(rowIndex, vColIndex, true);

        // The location of the viewport relative to the table
        Point pt = viewport.getViewPosition();

        // Translate the cell location so that it is relative
        // to the view, assuming the northwest corner of the
        // view is (0,0)
        rect.setLocation(rect.x-pt.x, rect.y-pt.y);

        // Scroll the area into view
        viewport.scrollRectToVisible(rect);
    }

    /**
     * Create thread that will cancel the editing state of the given TableCellEditor
     * after CELL_EDITING_STATE_PERIOD time in which with no pressing was made.
     */
    public void createCancelEditingStateThread(TableCellEditor cellEditor) {
        if (cancelEditingStateThread != null)
            cancelEditingStateThread.neutralize();
        (cancelEditingStateThread = new CancelEditingStateThread(cellEditor)).start();
    }

    @Override
    public TableCellRenderer getCellRenderer(int row, int column) {
        return cellRenderer;
    }

    @Override
    public void valueChanged(ListSelectionEvent e) {
        super.valueChanged(e);
        // Selection might be changed, update tooltip
        int selectetRow = getSelectedRow();
        if (selectetRow == -1) // no row is selected
            tooltipBar.showDefaultMessage();
        else
            tooltipBar.showActionTooltip(data.getCurrentTooltip());
    }

    public void updateModel(ActionFilter filter) {
        data.filter(filter);
    }

    /**
     * Override this method so that calls for SetModel function outside this class
     * won't get to setModel(KeymapTableModel model) function.
     */
    @Override
    public void setModel(TableModel model) {
        super.setModel(model);
    }

    public boolean hasChanged() { return data.hasChanged(); }

    @Override
    public TableCellEditor getCellEditor(int row, int column) {
        return new KeyStrokeCellEditor(new RecordingKeyStrokeField((KeyStroke) getValueAt(row, column)));
    }

    /**
     * This method updates ActionKeymap with the modified shortcuts.
     */
    public void commitChanges() {
        data.submitChanges();
    }

    public void restoreDefaults() {
        data.restoreDefaultAccelerators();
    }

    ///////////////////////////
    // FocusListener methods //
    ///////////////////////////

    public void focusGained(FocusEvent e) {
        int currentSelectedRow = getSelectedRow();
        if (lastSelectedRow != currentSelectedRow)
            tooltipBar.showActionTooltip(data.getCurrentTooltip());
        lastSelectedRow = currentSelectedRow;
    }

    public void focusLost(FocusEvent e) { }

    /////////////////////////////
    //// KeyListener methods ////
    /////////////////////////////

    public void keyPressed(KeyEvent e) {
        int keyCode = e.getKeyCode();
        if (keyCode == KeyEvent.VK_ENTER) {
            if (editCellAt(getSelectedRow(), getSelectedColumn()))
                getEditorComponent().requestFocusInWindow();
            e.consume();
        }
        else if (keyCode == KeyEvent.VK_DELETE || keyCode == KeyEvent.VK_BACK_SPACE) {
            setValueAt(DELETE, getSelectedRow(), getSelectedColumn());
            repaint();
            e.consume();
        }
        else if (keyCode != KeyEvent.VK_LEFT && keyCode != KeyEvent.VK_RIGHT && keyCode != KeyEvent.VK_UP &&
                keyCode != KeyEvent.VK_DOWN && keyCode != KeyEvent.VK_HOME  && keyCode != KeyEvent.VK_END &&
                keyCode != KeyEvent.VK_F2   && keyCode != KeyEvent.VK_ESCAPE)
            e.consume();
    }

    public void keyReleased(KeyEvent e) {}

    public void keyTyped(KeyEvent e) {}

    public static interface ActionFilter {
        boolean accept(String actionId, String rowAsText);
    }

    /**
     * Helper Classes
     */
    private class KeyStrokeCellEditor extends DefaultCellEditor implements TableCellEditor {

        RecordingKeyStrokeField rec;

        public KeyStrokeCellEditor(RecordingKeyStrokeField rec) {
            super(rec);
            this.rec = rec;
            rec.setSelectionColor(rec.getBackground());
            rec.setSelectedTextColor(rec.getForeground());
            rec.getDocument().addDocumentListener(new DocumentListener() {
                public void insertUpdate(DocumentEvent e) {
                    // quit editing state after text is written to the text field.
                    stopCellEditing();
                }
                public void changedUpdate(DocumentEvent e) {}
                public void removeUpdate(DocumentEvent e) {}
            });

            setClickCountToStart(NUM_OF_CLICKS_TO_ENTER_EDITING_STATE);

            createCancelEditingStateThread(this);
        }

        @Override
        public Component getTableCellEditorComponent(JTable table, Object value,
                boolean isSelected, int row, int col) {
            return rec;
        }

        @Override
        public Object getCellEditorValue() {
            return rec.getLastKeyStroke();
        }
    }

    private class CancelEditingStateThread extends Thread {
        private boolean stopped = false;
        private TableCellEditor cellEditor;

        public CancelEditingStateThread(TableCellEditor cellEditor) {
            this.cellEditor = cellEditor;
        }

        public void neutralize() {
            stopped = true;
        }

        @Override
        public void run() {
            try {
                Thread.sleep(CELL_EDITING_STATE_PERIOD);
            } catch (InterruptedException e) {}

            if (!stopped && cellEditor != null)
                cellEditor.stopCellEditing();
        }
    }

    private class KeymapTableModel extends DefaultTableModel {	
        private ShortcutsTableData tableData = null;

        private KeymapTableModel(ShortcutsTableData data) {
            super(data.getTableData(), new String[] {Translator.get("shortcuts_table.action_description"),
                    Translator.get("shortcuts_table.shortcut"),
                    Translator.get("shortcuts_table.alternate_shortcut")});
            this.tableData = data;
        }

        @Override
        public boolean isCellEditable(int row, int column) {
            switch (column) {
            case ACTION_DESCRIPTION_COLUMN_INDEX:
                return false;
            case ACCELERATOR_COLUMN_INDEX:
            case ALTERNATE_ACCELERATOR_COLUMN_INDEX:
                return true;
            default:
                return false;
            }
        }

        @Override
        public Object getValueAt(int row, int column) {
            return tableData.getTableData(row, column);
        }

        @Override
        public void setValueAt(Object value, int row, int column) {
            // if no keystroke was pressed
            if (value == null)
                return;
            // if the user pressed a keystroke that is used to indicate a delete operation should be made
            else if (value == DELETE)
                value = null;

            KeyStroke typedKeyStroke = (KeyStroke) value;
            switch (column){
            case ACCELERATOR_COLUMN_INDEX:
                tableData.setAccelerator(typedKeyStroke, row);
                break;
            case ALTERNATE_ACCELERATOR_COLUMN_INDEX:
                tableData.setAlternativeAccelerator(typedKeyStroke, row);
                break;
            default:
                LOGGER.debug("Unexpected column index: " + column);
            }

            fireTableCellUpdated(row, column);

            LOGGER.trace("Value: " + value + ", row: " + row + ", col: " + column);
        }
    }

    private class RecordingKeyStrokeField extends JTextField implements KeyListener {

        // The last KeyStroke that was entered to the field.
        // Before any keystroke is entered, it contains the keystroke appearing in the cell before entering the editing state.
        private KeyStroke lastKeyStroke;

        public RecordingKeyStrokeField(KeyStroke currentKeyStroke) {
            super(Translator.get("shortcuts_table.type_in_a_shortcut"));

            lastKeyStroke = currentKeyStroke;
            setBorder(BorderFactory.createEmptyBorder());
            setHorizontalAlignment(JTextField.CENTER);
            setEditable(false);
            setBackground(ThemeCache.backgroundColors[ThemeCache.ACTIVE][ThemeCache.SELECTED]);
            setForeground(ThemeCache.foregroundColors[ThemeCache.ACTIVE][ThemeCache.SELECTED][ThemeCache.PLAIN_FILE]);
            addKeyListener(this);
            // It is required to disable the traversal keys in order to support keys combination that include the TAB key
            setFocusTraversalKeysEnabled(false);
        }

        /**
         *
         * @return the last KeyStroke the user entered to the field.
         */
        public KeyStroke getLastKeyStroke() { return lastKeyStroke; }


        ////////////////////////
        // Overridden methods //
        ////////////////////////

        @Override
        protected void paintBorder(Graphics g) {
            paintDottedBorder(g, getWidth(), getHeight(), ThemeCache.backgroundColors[ThemeCache.ACTIVE][ThemeCache.NORMAL]);
        }

        /////////////////////////////
        //// KeyListener methods ////
        /////////////////////////////

        public void keyPressed(KeyEvent keyEvent) {
            LOGGER.trace("keyModifiers="+keyEvent.getModifiers()+" keyCode="+keyEvent.getKeyCode());

            int keyCode = keyEvent.getKeyCode();
            if(keyCode==KeyEvent.VK_SHIFT || keyCode==KeyEvent.VK_CONTROL || keyCode==KeyEvent.VK_ALT || keyCode==KeyEvent.VK_META)
                return;

            KeyStroke pressedKeyStroke = KeyStroke.getKeyStrokeForEvent(keyEvent);

            if (pressedKeyStroke.equals(lastKeyStroke)) {
                TableCellEditor activeCellEditor = getCellEditor();
                if (activeCellEditor!= null)
                    activeCellEditor.stopCellEditing();
            }
            else {
                String actionId;
                if ((actionId = data.contains(pressedKeyStroke)) != null) {
                    String errorMessage = "The shortcut [" + KeyStrokeUtils.getKeyStrokeDisplayableRepresentation(pressedKeyStroke)
                    + "] is already assigned to '" + ActionProperties.getActionDescription(actionId) + "'";
                    tooltipBar.showErrorMessage(errorMessage);
                    createCancelEditingStateThread(getCellEditor());
                }
                else {
                    lastKeyStroke = pressedKeyStroke;
                    setText(KeyStrokeUtils.getKeyStrokeDisplayableRepresentation(lastKeyStroke));
                }
            }

            keyEvent.consume();
        }

        public void keyReleased(KeyEvent e) {e.consume();}

        public void keyTyped(KeyEvent e) {e.consume();}
    }

    private class ShortcutsTableData {
        private Object[][] data;
        private String[] actionIds;
        private String[] descriptions;

        private final Integer description = 0;
        private final Integer accelerator = 1;
        private final Integer alt_accelerator = 2;
        private final Integer tooltips = 3;

        private List<String> allActionIds;
        private HashMap<String, HashMap<Integer, Object>> db;

        public ShortcutsTableData() {
            allActionIds = ActionManager.getActionIds();
            Collections.sort(allActionIds, ACTIONS_COMPARATOR);

            final int nbActions = allActionIds.size();
            db = new HashMap<String, HashMap<Integer, Object>>(nbActions);

            int nbRows = allActionIds.size();
            data = new Object[nbRows][NUM_OF_COLUMNS];

            for(String actionId : allActionIds) {
                ActionDescriptor actionDescriptor = ActionProperties.getActionDescriptor(actionId);

                HashMap<Integer, Object> actionProperties = new HashMap<Integer, Object>();

                ImageIcon actionIcon = actionDescriptor.getIcon();
                if (actionIcon == null)
                    actionIcon = transparentIcon;
                String actionLabel = actionDescriptor.getLabel();

                /* 0 -> action's icon & name pair */
                actionProperties.put(description, new Pair<ImageIcon, String>(IconManager.getPaddedIcon(actionIcon, new Insets(0, 4, 0, 4)), actionLabel));
                /* 1 -> action's accelerator */
                actionProperties.put(accelerator, ActionKeymap.getAccelerator(actionId));
                /* 2 -> action's alternate accelerator */
                actionProperties.put(alt_accelerator, ActionKeymap.getAlternateAccelerator(actionId));
                /* 3 -> action's description */
                actionProperties.put(tooltips, actionDescriptor.getDescription());

                db.put(actionId, actionProperties);
            }
        }

        public void filter(ActionFilter filter) {
            List<String> filteredActionIds = filter(allActionIds, filter);

            // Build the table data
            int nbRows = filteredActionIds.size();

            actionIds = new String[nbRows];
            descriptions = new String[nbRows];
            data = new Object[nbRows][NUM_OF_COLUMNS];

            for (int i = 0; i < nbRows; ++i) {
                String actionId = filteredActionIds.get(i);
                actionIds[i] = actionId;
                ActionDescriptor actionDescriptor = ActionProperties.getActionDescriptor(actionId);

                data[i][ACTION_DESCRIPTION_COLUMN_INDEX] = db.get(actionId).get(this.description);

                KeyStroke accelerator = (KeyStroke) db.get(actionId).get(this.accelerator);
                setAccelerator(accelerator, i);

                KeyStroke alternativeAccelerator = (KeyStroke) db.get(actionId).get(this.alt_accelerator);
                setAlternativeAccelerator(alternativeAccelerator, i);

                descriptions[i] = actionDescriptor.getDescription();
            }

            ShortcutsTable.this.clearSelection();
            ((DefaultTableModel) getModel()).setRowCount(data.length);
            ShortcutsTable.this.repaint();
            ShortcutsTable.this.scrollToVisible(0, 0);
        }

        public Object[][] getTableData() { return data; }

        public Object getTableData(int row, int col) { return data[row][col]; }

        public String getCurrentTooltip() { return descriptions[getSelectedRow()]; }

        public String getActionId(int row) { return actionIds[row]; }

        public boolean hasChanged() {
            for (String actionId : db.keySet()) {
                HashMap<Integer, Object> actionProperties = db.get(actionId);
                if (!equals(actionProperties.get(this.accelerator), ActionKeymap.getAccelerator(actionId)) ||
                        !equals(actionProperties.get(this.alt_accelerator), ActionKeymap.getAlternateAccelerator(actionId)))
                    return true;
            }
            return false;
        }

        public void restoreDefaultAccelerators() {
            for (String actionId : allActionIds) {
                (db.get(actionId)).put(this.accelerator, ActionProperties.getDefaultAccelerator(actionId));
                (db.get(actionId)).put(this.alt_accelerator, ActionProperties.getDefaultAlternativeAccelerator(actionId));
            }

            int nbRows = actionIds.length;
            for (int i=0; i<nbRows; ++i) {
                data[i][ACCELERATOR_COLUMN_INDEX] = db.get(actionIds[i]).get(this.accelerator);
                data[i][ALTERNATE_ACCELERATOR_COLUMN_INDEX] = db.get(actionIds[i]).get(this.alt_accelerator);
            }

            ((DefaultTableModel) getModel()).fireTableDataChanged();
        }

        public void submitChanges() {
            for (String actionId : db.keySet()) {
                HashMap<Integer, Object> actionProperties = db.get(actionId);
                KeyStroke accelerator = (KeyStroke) actionProperties.get(this.accelerator);
                KeyStroke alternateAccelerator = (KeyStroke) actionProperties.get(this.alt_accelerator);

                // If action's accelerators differ from its saved accelerators, register them.
                if (!equals(accelerator, ActionKeymap.getAccelerator(actionId)) ||
                        !equals(alternateAccelerator, ActionKeymap.getAlternateAccelerator(actionId)))
                    ActionKeymap.changeActionAccelerators(actionId, accelerator, alternateAccelerator);
            }
        }

        public String contains(KeyStroke accelerator) {
            if (accelerator != null) {
                for (String actionId : db.keySet()) {
                    if (accelerator.equals(db.get(actionId).get(this.accelerator)) ||
                            accelerator.equals(db.get(actionId).get(this.alt_accelerator)))
                        return actionId;
                }
            }
            return null;
        }

        private void setAccelerator(KeyStroke accelerator, int row) {
            data[row][ACCELERATOR_COLUMN_INDEX] = accelerator;
            db.get(getActionId(row)).put(this.accelerator, accelerator);
        }

        private void setAlternativeAccelerator(KeyStroke altAccelerator, int row) {
            data[row][ALTERNATE_ACCELERATOR_COLUMN_INDEX] = altAccelerator;
            db.get(getActionId(row)).put(this.alt_accelerator, altAccelerator);
        }

        private List<String> filter(List<String> actionIds, ActionFilter filter) {
            List<String> filteredActionsList = new LinkedList<String>();
            for (String actionId : actionIds) {
                // Discard actions that are parameterized, and those that are rejected by the filter
                if (!ActionProperties.getActionDescriptor(actionId).isParameterized() && filter.accept(actionId, getRowAsTextForFilter(actionId)))
                    filteredActionsList.add(actionId);
            }
            return filteredActionsList;
        }

        private String getRowAsTextForFilter(String actionId) {
            if (actionIds == null) {
                return "";
            }
            StringBuilder rowText = new StringBuilder();
            Object[] rowValues = db.get(actionId).values().toArray();

            for (int i = 0; i < rowValues.length; i++) {
                Object colValue = rowValues[i];
                String rowTextValue = "";
                switch (i) {
                case ACTION_DESCRIPTION_COLUMN_INDEX:
                    Pair<ImageIcon, String> description = (Pair<ImageIcon, String>) colValue;
                    rowTextValue = description.second;
                    break;
                case ACCELERATOR_COLUMN_INDEX:
                case ALTERNATE_ACCELERATOR_COLUMN_INDEX:
                    final KeyStroke key = (KeyStroke) colValue;
                    rowTextValue = key == null ? "" : KeyStrokeUtils.getKeyStrokeDisplayableRepresentation(key);
                    break;
                }
                rowText.append(" ");
                rowText.append(rowTextValue.trim().toLowerCase());
            }

            return rowText.toString().trim();
        }

        private boolean equals(Object obj1, Object obj2) {
            if (obj1 == null)
                return obj2 == null;
            return obj1.equals(obj2);
        }
    }

    private class ShortcutsTableCellRenderer implements TableCellRenderer, ThemeListener {
        /** Custom JLabel that render specific column cells */
        private DotBorderedCellLabel[] cellLabels = new DotBorderedCellLabel[NUM_OF_COLUMNS];

        public ShortcutsTableCellRenderer() {
            for(int i=0; i<NUM_OF_COLUMNS; ++i)
                cellLabels[i] = new DotBorderedCellLabel();

            // Set labels' font.
            setCellLabelsFont(ThemeCache.tableFont);

            cellLabels[ACTION_DESCRIPTION_COLUMN_INDEX].setHorizontalAlignment(CellLabel.LEFT);
            cellLabels[ACCELERATOR_COLUMN_INDEX].setHorizontalAlignment(CellLabel.CENTER);
            cellLabels[ALTERNATE_ACCELERATOR_COLUMN_INDEX].setHorizontalAlignment(CellLabel.CENTER);

            // Listens to certain configuration variables
            ThemeCache.addThemeListener(this);
        }

        /**
         * Sets CellLabels' font to the current one.
         */
        private void setCellLabelsFont(Font newFont) {
            // Set custom font
            for(int i=0; i<NUM_OF_COLUMNS; ++i)
                cellLabels[i].setFont(newFont);
        }

        public Component getTableCellRendererComponent(JTable table, Object value,
                boolean isSelected, boolean hasFocus, int rowIndex, int vColIndex) {
            DotBorderedCellLabel label;
            int       columnId;

            columnId = convertColumnIndexToModel(vColIndex);
            label = cellLabels[columnId];

            // action's icon column: return ImageIcon instance
            if(columnId == ACTION_DESCRIPTION_COLUMN_INDEX) {
                Pair<ImageIcon, String> description = (Pair<ImageIcon, String>) value;
                label.setIcon(description.first);
                label.setText(description.second);

                // set cell's foreground color
                label.setForeground(ThemeCache.foregroundColors[ThemeCache.ACTIVE][ThemeCache.NORMAL][ThemeCache.PLAIN_FILE]);
            }
            // Any other column
            else {
                final KeyStroke key = (KeyStroke) value;
                String text = key == null ? "" : KeyStrokeUtils.getKeyStrokeDisplayableRepresentation(key);

                // If component's preferred width is bigger than column width then the component is not entirely
                // visible so we set a tooltip text that will display the whole text when mouse is over the component
                if (table.getColumnModel().getColumn(vColIndex).getWidth() < label.getPreferredSize().getWidth())
                    label.setToolTipText(text);
                // Have to set it to null otherwise the defaultRender sets the tooltip text to the last one specified
                else
                    label.setToolTipText(null);

                // Set label's text
                label.setText(text);
                // set cell's foreground color
                if (key != null) {
                    boolean customized;
                    switch (columnId) {
                    case ACCELERATOR_COLUMN_INDEX:
                        customized = !key.equals(ActionProperties.getDefaultAccelerator(data.getActionId(rowIndex)));
                        break;
                    case ALTERNATE_ACCELERATOR_COLUMN_INDEX:
                        customized = !key.equals(ActionProperties.getDefaultAlternativeAccelerator(data.getActionId(rowIndex)));
                        break;
                    default:
                        customized = false;
                    }

                    label.setForeground(ThemeCache.foregroundColors[ThemeCache.ACTIVE][ThemeCache.NORMAL][customized ? ThemeCache.PLAIN_FILE : ThemeCache.HIDDEN_FILE]);
                }
            }

            // set outline for the focused cell
            label.setOutline(hasFocus ? ThemeCache.backgroundColors[ThemeCache.ACTIVE][ThemeCache.SELECTED] : null);
            // set cell's background color
            label.setBackground(ThemeCache.backgroundColors[ThemeCache.ACTIVE][rowIndex % 2 == 0 ? ThemeCache.NORMAL : ThemeCache.ALTERNATE]);

            return label;
        }

        // - Theme listening -------------------------------------------------------------
        // -------------------------------------------------------------------------------
        /**
         * Receives theme color changes notifications.
         */
        public void colorChanged(ColorChangedEvent event) { }

        /**
         * Receives theme font changes notifications.
         */
        public void fontChanged(FontChangedEvent event) {
            if(event.getFontId() == Theme.FILE_TABLE_FONT) {
                setCellLabelsFont(ThemeCache.tableFont);
            }
        }
    }

    /**
     * CellLabel with a dotted outline.
     */
    private class DotBorderedCellLabel extends CellLabel {

        @Override
        protected void paintOutline(Graphics g) {
            paintDottedBorder(g, getWidth(), getHeight(), outlineColor);
        }
    }
}
