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

import java.awt.*;
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.ArrayList;
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.runtime.OsVersion;
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;

    private static final Stroke DOTTED_BORDER_STROKE = new BasicStroke(1.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_MITER, 2.0f, new float[]{2.0f}, 0);

    /** 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 = (id1, 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(DOTTED_BORDER_STROKE);
        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_X.isCurrent() && OsVersion.MAC_OS_X_10_5.isCurrentOrHigher();
    }
    
    /** 
     * 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 selectedRow = getSelectedRow();
		if (selectedRow == -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 abstract class ActionFilter {
		public abstract boolean accept(String actionId);
	}
	
	/**
	 * 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 ignore) {}
			
			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 = new ArrayList<>();
            Iterator<String> iterator = ActionManager.getActionIds();
            while(iterator.hasNext())
                allActionIds.add(iterator.next());
			Collections.sort(allActionIds, ACTIONS_COMPARATOR);
			
			final int nbActions = allActionIds.size();
			db = new HashMap<>(nbActions);

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

            final Insets insets = new Insets(0, 4, 0, 4);
            for(String actionId : allActionIds) {
				ActionDescriptor actionDescriptor = ActionProperties.getActionDescriptor(actionId);
				
				HashMap<Integer, Object> actionProperties = new HashMap<>();
				
				ImageIcon actionIcon = actionDescriptor.getIcon();
				if (actionIcon == null)
					actionIcon = transparentIcon;
				String actionLabel = actionDescriptor.getLabel();
				
				/* 0 -> action's icon & name pair */
				actionProperties.put(description, new Pair<>(IconManager.getPaddedIcon(actionIcon, insets), 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<>();
            for (String actionId : actionIds) {
                // Discard actions that are parameterized, and those that are rejected by the IMAGE_FILTER
                if (!ActionProperties.getActionDescriptor(actionId).isParameterized() && filter.accept(actionId))
                    filteredActionsList.add(actionId);
            }
			return filteredActionsList;
		}
		
		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) {
			int columnId = convertColumnIndexToModel(vColIndex);
			DotBorderedCellLabel 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);
		}
	}
}
