org.eclipse.ui.internal.keys.KeysPreferencePage.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.ui.internal.keys.KeysPreferencePage.java

Source

/*******************************************************************************
 * Copyright (c) 2000, 2009 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/

package org.eclipse.ui.internal.keys;

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.Set;

import org.eclipse.core.commands.Category;
import org.eclipse.core.commands.Command;
import org.eclipse.core.commands.CommandManager;
import org.eclipse.core.commands.ParameterizedCommand;
import org.eclipse.core.commands.common.NotDefinedException;
import org.eclipse.core.commands.contexts.Context;
import org.eclipse.core.commands.contexts.ContextManager;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.SafeRunner;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.bindings.Binding;
import org.eclipse.jface.bindings.BindingManager;
import org.eclipse.jface.bindings.Scheme;
import org.eclipse.jface.bindings.TriggerSequence;
import org.eclipse.jface.bindings.keys.KeyBinding;
import org.eclipse.jface.bindings.keys.KeySequence;
import org.eclipse.jface.bindings.keys.KeySequenceText;
import org.eclipse.jface.bindings.keys.KeyStroke;
import org.eclipse.jface.contexts.IContextIds;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.preference.PreferencePage;
import org.eclipse.jface.util.SafeRunnable;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.FocusEvent;
import org.eclipse.swt.events.FocusListener;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.FileDialog;
import org.eclipse.swt.widgets.Group;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.swt.widgets.TabFolder;
import org.eclipse.swt.widgets.TabItem;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableColumn;
import org.eclipse.swt.widgets.TableItem;
import org.eclipse.swt.widgets.Text;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.IWorkbenchPreferencePage;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.activities.IActivityManager;
import org.eclipse.ui.commands.ICommandService;
import org.eclipse.ui.contexts.IContextService;
import org.eclipse.ui.internal.IPreferenceConstants;
import org.eclipse.ui.internal.IWorkbenchHelpContextIds;
import org.eclipse.ui.internal.WorkbenchPlugin;
import org.eclipse.ui.internal.misc.StatusUtil;
import org.eclipse.ui.internal.util.PrefUtil;
import org.eclipse.ui.internal.util.Util;
import org.eclipse.ui.keys.IBindingService;
import org.eclipse.ui.statushandlers.StatusManager;

import com.ibm.icu.text.Collator;
import com.ibm.icu.text.MessageFormat;

/**
 * The preference page for defining keyboard shortcuts. While some of its
 * underpinning have been made generic to "bindings" rather than "key bindings",
 * it will still take some work to remove the link entirely.
 * 
 * @since 3.0
 */
public final class KeysPreferencePage extends PreferencePage implements IWorkbenchPreferencePage {

    /**
     * A selection listener to be used on the columns in the table on the view
     * tab. This selection listener modifies the sort order so that the
     * appropriate column is in the first position.
     * 
     * @since 3.1
     */
    private class SortOrderSelectionListener extends SelectionAdapter {

        /**
         * The column to be put in the first position. This value should be one
         * of the constants defined by <code>SORT_COLUMN_</code>.
         */
        private final int columnSelected;

        /**
         * Constructs a new instance of <code>SortOrderSelectionListener</code>.
         * 
         * @param columnSelected
         *            The column to be given first priority in the sort order;
         *            this value should be one of the constants defined as
         *            <code>SORT_COLUMN_</code>.
         */
        private SortOrderSelectionListener(final int columnSelected) {
            this.columnSelected = columnSelected;
        }

        /*
         * (non-Javadoc)
         * 
         * @see org.eclipse.swt.events.SelectionListener#widgetSelected(org.eclipse.swt.events.SelectionEvent)
         */
        public void widgetSelected(SelectionEvent e) {
            // Change the column titles.
            final int oldSortIndex = sortOrder[0];
            final TableColumn oldSortColumn = tableBindings.getColumn(oldSortIndex);
            oldSortColumn.setText(UNSORTED_COLUMN_NAMES[oldSortIndex]);
            final TableColumn newSortColumn = tableBindings.getColumn(columnSelected);
            newSortColumn.setText(SORTED_COLUMN_NAMES[columnSelected]);

            // Change the sort order.
            boolean columnPlaced = false;
            boolean enoughRoom = false;
            int bumpedColumn = -1;
            for (int i = 0; i < sortOrder.length; i++) {
                if (sortOrder[i] == columnSelected) {
                    /*
                     * We've found the place where the column existing in the
                     * old sort order. No matter what at this point, we have
                     * completed the reshuffling.
                     */
                    enoughRoom = true;
                    if (bumpedColumn != -1) {
                        // We have already started bumping things around, so
                        // drop the last bumped column here.
                        sortOrder[i] = bumpedColumn;
                    } else {
                        // The order has not changed.
                        columnPlaced = true;
                    }
                    break;

                } else if (columnPlaced) {
                    // We are currently bumping, so just bump another.
                    int temp = sortOrder[i];
                    sortOrder[i] = bumpedColumn;
                    bumpedColumn = temp;

                } else {
                    /*
                     * We are not currently bumping, so drop the column and
                     * start bumping.
                     */
                    bumpedColumn = sortOrder[i];
                    sortOrder[i] = columnSelected;
                    columnPlaced = true;
                }
            }

            // Grow the sort order.
            if (!enoughRoom) {
                final int[] newSortOrder = new int[sortOrder.length + 1];
                System.arraycopy(sortOrder, 0, newSortOrder, 0, sortOrder.length);
                newSortOrder[sortOrder.length] = bumpedColumn;
                sortOrder = newSortOrder;
            }

            // Update the view tab.
            updateViewTab();
        }
    }

    /**
     * The data key for the binding stored on an SWT widget. The key is a
     * fully-qualified name, but in reverse order. This is so that the equals
     * method will detect misses faster.
     */
    private static final String BINDING_KEY = "Binding.bindings.jface.eclipse.org"; //$NON-NLS-1$

    /**
     * The image associate with a binding that exists as part of the system
     * definition.
     */
    private static final Image IMAGE_BLANK = ImageFactory.getImage("blank"); //$NON-NLS-1$

    /**
     * The image associated with a binding changed by the user.
     */
    private static final Image IMAGE_CHANGE = ImageFactory.getImage("change"); //$NON-NLS-1$

    /**
     * The data key at which the <code>Binding</code> instance for a table
     * item is stored.
     */
    private static final String ITEM_DATA_KEY = "org.eclipse.jface.bindings"; //$NON-NLS-1$

    /**
     * The number of items to show in the combo boxes.
     */
    private static final int ITEMS_TO_SHOW = 9;

    /**
     * The resource bundle from which translations can be retrieved.
     */
    private static final ResourceBundle RESOURCE_BUNDLE = ResourceBundle
            .getBundle(KeysPreferencePage.class.getName());

    /**
     * The total number of columns on the view tab.
     */
    private static final int VIEW_TOTAL_COLUMNS = 4;

    /**
     * The translated names for the columns when they are the primary sort key
     * (e.g., ">Category<").
     */
    private static final String[] SORTED_COLUMN_NAMES = new String[VIEW_TOTAL_COLUMNS];

    /**
     * The index of the modify tab.
     * 
     * @since 3.1
     */
    private static final int TAB_INDEX_MODIFY = 1;

    /**
     * The translated names for the columns when they are not the primary sort
     * key (e.g., "Category").
     */
    private static final String[] UNSORTED_COLUMN_NAMES = new String[VIEW_TOTAL_COLUMNS];

    /**
     * The index of the column on the view tab containing the category name.
     */
    private static final int VIEW_CATEGORY_COLUMN_INDEX = 0;

    /**
     * The index of the column on the view tab containing the command name.
     */
    private static final int VIEW_COMMAND_COLUMN_INDEX = 1;

    /**
     * The index of the column on the view tab containing the context name.
     */
    private static final int VIEW_CONTEXT_COLUMN_INDEX = 3;

    /**
     * The index of the column on the view tab containing the key sequence.
     */
    private static final int VIEW_KEY_SEQUENCE_COLUMN_INDEX = 2;

    static {
        UNSORTED_COLUMN_NAMES[VIEW_CATEGORY_COLUMN_INDEX] = Util.translateString(RESOURCE_BUNDLE,
                "tableColumnCategory"); //$NON-NLS-1$
        UNSORTED_COLUMN_NAMES[VIEW_COMMAND_COLUMN_INDEX] = Util.translateString(RESOURCE_BUNDLE,
                "tableColumnCommand"); //$NON-NLS-1$
        UNSORTED_COLUMN_NAMES[VIEW_KEY_SEQUENCE_COLUMN_INDEX] = Util.translateString(RESOURCE_BUNDLE,
                "tableColumnKeySequence"); //$NON-NLS-1$
        UNSORTED_COLUMN_NAMES[VIEW_CONTEXT_COLUMN_INDEX] = Util.translateString(RESOURCE_BUNDLE,
                "tableColumnContext"); //$NON-NLS-1$

        SORTED_COLUMN_NAMES[VIEW_CATEGORY_COLUMN_INDEX] = Util.translateString(RESOURCE_BUNDLE,
                "tableColumnCategorySorted"); //$NON-NLS-1$
        SORTED_COLUMN_NAMES[VIEW_COMMAND_COLUMN_INDEX] = Util.translateString(RESOURCE_BUNDLE,
                "tableColumnCommandSorted"); //$NON-NLS-1$
        SORTED_COLUMN_NAMES[VIEW_KEY_SEQUENCE_COLUMN_INDEX] = Util.translateString(RESOURCE_BUNDLE,
                "tableColumnKeySequenceSorted"); //$NON-NLS-1$
        SORTED_COLUMN_NAMES[VIEW_CONTEXT_COLUMN_INDEX] = Util.translateString(RESOURCE_BUNDLE,
                "tableColumnContextSorted"); //$NON-NLS-1$
    }

    /**
     * The workbench's activity manager. This activity manager is used to see if
     * certain commands should be filtered from the user interface.
     */
    private IActivityManager activityManager;

    /**
     * The workbench's binding service. This binding service is used to access
     * the current set of bindings, and to persist changes.
     */
    private IBindingService bindingService;

    /**
     * The add button located on the bottom left of the preference page. This
     * button adds the current trigger sequence to the currently selected
     * command.
     */
    private Button buttonAdd;

    /**
     * The remove button located on the bottom left of the preference page. This
     * button removes the current trigger sequence from the current command.
     */
    private Button buttonRemove;

    /**
     * The restore button located on the bottom left of the preference page.
     * This button attempts to restore the currently trigger sequence to its
     * initial (i.e., Binding.SYSTEM) state -- undoing all user modifications.
     */
    private Button buttonRestore;

    /**
     * A map of all the category identifiers indexed by the names that appear in
     * the user interface. This look-up table is built during initialization.
     */
    private Map categoryIdsByUniqueName;

    /**
     * A map of all the category names in the user interface indexed by their
     * identifiers. This look-up table is built during initialization.
     */
    private Map categoryUniqueNamesById;

    /**
     * The combo box containing the list of all categories for commands.
     */
    private Combo comboCategory;

    /**
     * The combo box containing the list of commands relevent for the currently
     * selected category.
     */
    private Combo comboCommand;

    /**
     * The combo box containing the list of contexts in the system.
     */
    private Combo comboContext;

    /**
     * The combo box containing the list of schemes in the system.
     */
    private Combo comboScheme;

    /**
     * A map of all the command identifiers indexed by the categories to which
     * they belong. This look-up table is built during initialization.
     */
    private Map commandIdsByCategoryId;

    /**
     * The parameterized commands corresponding to the current contents of
     * <code>comboCommand</code>. The commands in this array are in the same
     * order as in the combo. This value can be <code>null</code> if nothing
     * is selected in the combo.
     */
    private ParameterizedCommand[] commands = null;

    /**
     * The workbench's command service. This command service is used to access
     * the list of commands.
     */
    private ICommandService commandService;

    /**
     * A map of all the context identifiers indexed by the names that appear in
     * the user interface. This look-up table is built during initialization.
     */
    private Map contextIdsByUniqueName;

    /**
     * The workbench's context service. This context service is used to access
     * the list of contexts.
     */
    private IContextService contextService;

    /**
     * A map of all the category names in the user interface indexed by their
     * identifiers. This look-up table is built during initialization.
     */
    private Map contextUniqueNamesById;

    /**
     * The workbench's help system. This is used to register the page with the
     * help system.
     * 
     * TODO Add a help context
     */
    // private IWorkbenchHelpSystem helpSystem;
    /**
     * This is the label next to the table showing the bindings matching a
     * particular command. The label is disabled if there isn't a selected
     * command identifier.
     */
    private Label labelBindingsForCommand;

    /**
     * This is the label next to the table showing the bindings matching a
     * particular trigger sequence. The label is disabled if there isn't a
     * current key sequence.
     */
    private Label labelBindingsForTriggerSequence;

    /**
     * The label next to the context combo box. This label indicates whether the
     * context is a child of another context. If the current context is not a
     * child, then this label is blank.
     */
    private Label labelContextExtends;

    /**
     * The label next to the scheme combo box. This label indicates whether the
     * scheme is a child of another scheme. If the current scheme is not a
     * child, then this label is blank.
     */
    private Label labelSchemeExtends;

    /**
     * A binding manager local to this preference page. When the page is
     * initialized, the current bindings are read out from the binding service
     * and placed in this manager. This manager is then updated as the user
     * makes changes. When the user has finished, the contents of this manager
     * are compared with the contents of the binding service. The changes are
     * then persisted.
     */
    private final BindingManager localChangeManager = new BindingManager(new ContextManager(),
            new CommandManager());

    /**
     * A map of all the scheme identifiers indexed by the names that appear in
     * the user interface. This look-up table is built during initialization.
     */
    private Map schemeIdsByUniqueName;

    /**
     * A map of all the scheme names in the user interface indexed by their
     * identifiers. This look-up table is built during initialization.
     */
    private Map schemeUniqueNamesById;

    /**
     * The sort order to be used on the view tab to display all of the key
     * bindings. This sort order can be changed by the user. This array is never
     * <code>null</code>, but may be empty.
     */
    private int[] sortOrder = { VIEW_CATEGORY_COLUMN_INDEX, VIEW_COMMAND_COLUMN_INDEX,
            VIEW_KEY_SEQUENCE_COLUMN_INDEX, VIEW_CONTEXT_COLUMN_INDEX };

    /**
     * The top-most tab folder for the preference page -- containing a view and
     * a modify tab.
     */
    private TabFolder tabFolder;

    /**
     * A table of the key bindings currently defined. This table appears on the
     * view tab; it is intended to be an easy way for users to learn the key
     * bindings in Eclipse. This value is only <code>null</code> until the
     * controls are first created.
     */
    private Table tableBindings;

    /**
     * The table containing all of the bindings matching the selected command.
     */
    private Table tableBindingsForCommand;

    /**
     * The table containing all of the bindings matching the current trigger
     * sequence.
     */
    private Table tableBindingsForTriggerSequence;

    /**
     * The text widget where keys are entered. This widget is managed by
     * <code>textTriggerSequenceManager</code>, which provides its special
     * behaviour.
     */
    private Text textTriggerSequence;

    /**
     * The manager for the text widget that traps incoming key events. This
     * manager should be used to access the widget, rather than accessing the
     * widget directly.
     */
    private KeySequenceText textTriggerSequenceManager;

    /* (non-Javadoc)
     * @see org.eclipse.jface.preference.PreferencePage#applyData(java.lang.Object)
     */
    public void applyData(Object data) {
        if (data instanceof Binding) {
            editBinding((Binding) data);
        }
    }

    protected final Control createContents(final Composite parent) {

        PlatformUI.getWorkbench().getHelpSystem().setHelp(parent, IWorkbenchHelpContextIds.KEYS_PREFERENCE_PAGE);

        tabFolder = new TabFolder(parent, SWT.NULL);

        // View tab
        final TabItem viewTab = new TabItem(tabFolder, SWT.NULL);
        viewTab.setText(Util.translateString(RESOURCE_BUNDLE, "viewTab.Text")); //$NON-NLS-1$
        viewTab.setControl(createViewTab(tabFolder));

        // Modify tab
        final TabItem modifyTab = new TabItem(tabFolder, SWT.NULL);
        modifyTab.setText(Util.translateString(RESOURCE_BUNDLE, "modifyTab.Text")); //$NON-NLS-1$
        modifyTab.setControl(createModifyTab(tabFolder));

        // Do some fancy stuff.
        applyDialogFont(tabFolder);
        final IPreferenceStore store = getPreferenceStore();
        final int selectedTab = store.getInt(IPreferenceConstants.KEYS_PREFERENCE_SELECTED_TAB);
        if ((tabFolder.getItemCount() > selectedTab) && (selectedTab > 0)) {
            tabFolder.setSelection(selectedTab);
        }

        return tabFolder;
    }

    /**
     * Creates the tab that allows the user to change the keyboard shortcuts.
     * 
     * @param parent
     *            The tab folder in which the tab should be created; must not be
     *            <code>null</code>.
     * @return The composite which represents the contents of the tab; never
     *         <code>null</code>.
     */
    private final Composite createModifyTab(final TabFolder parent) {
        final Composite composite = new Composite(parent, SWT.NULL);
        composite.setLayout(new GridLayout());
        GridData gridData = new GridData(GridData.FILL_BOTH);
        composite.setLayoutData(gridData);
        final Composite compositeKeyConfiguration = new Composite(composite, SWT.NULL);
        GridLayout gridLayout = new GridLayout();
        gridLayout.numColumns = 3;
        compositeKeyConfiguration.setLayout(gridLayout);
        gridData = new GridData(GridData.FILL_HORIZONTAL);
        compositeKeyConfiguration.setLayoutData(gridData);
        final Label labelKeyConfiguration = new Label(compositeKeyConfiguration, SWT.LEFT);
        labelKeyConfiguration.setText(Util.translateString(RESOURCE_BUNDLE, "labelScheme")); //$NON-NLS-1$
        comboScheme = new Combo(compositeKeyConfiguration, SWT.READ_ONLY);
        gridData = new GridData();
        gridData.widthHint = 200;
        comboScheme.setLayoutData(gridData);
        comboScheme.setVisibleItemCount(ITEMS_TO_SHOW);

        comboScheme.addSelectionListener(new SelectionAdapter() {
            public final void widgetSelected(final SelectionEvent e) {
                selectedComboScheme();
            }
        });

        labelSchemeExtends = new Label(compositeKeyConfiguration, SWT.LEFT);
        gridData = new GridData(GridData.FILL_HORIZONTAL);
        labelSchemeExtends.setLayoutData(gridData);
        final Control spacer = new Composite(composite, SWT.NULL);
        gridData = new GridData();
        gridData.heightHint = 10;
        gridData.widthHint = 10;
        spacer.setLayoutData(gridData);
        final Group groupCommand = new Group(composite, SWT.SHADOW_NONE);
        gridLayout = new GridLayout();
        gridLayout.numColumns = 3;
        groupCommand.setLayout(gridLayout);
        gridData = new GridData(GridData.FILL_BOTH);
        groupCommand.setLayoutData(gridData);
        groupCommand.setText(Util.translateString(RESOURCE_BUNDLE, "groupCommand")); //$NON-NLS-1$   
        final Label labelCategory = new Label(groupCommand, SWT.LEFT);
        gridData = new GridData();
        labelCategory.setLayoutData(gridData);
        labelCategory.setText(Util.translateString(RESOURCE_BUNDLE, "labelCategory")); //$NON-NLS-1$
        comboCategory = new Combo(groupCommand, SWT.READ_ONLY);
        gridData = new GridData();
        gridData.horizontalSpan = 2;
        gridData.widthHint = 200;
        comboCategory.setLayoutData(gridData);
        comboCategory.setVisibleItemCount(ITEMS_TO_SHOW);

        comboCategory.addSelectionListener(new SelectionAdapter() {
            public final void widgetSelected(final SelectionEvent e) {
                update();
            }
        });

        final Label labelCommand = new Label(groupCommand, SWT.LEFT);
        gridData = new GridData();
        labelCommand.setLayoutData(gridData);
        labelCommand.setText(Util.translateString(RESOURCE_BUNDLE, "labelCommand")); //$NON-NLS-1$
        comboCommand = new Combo(groupCommand, SWT.READ_ONLY);
        gridData = new GridData();
        gridData.horizontalSpan = 2;
        gridData.widthHint = 300;
        comboCommand.setLayoutData(gridData);
        comboCommand.setVisibleItemCount(9);

        comboCommand.addSelectionListener(new SelectionAdapter() {
            public final void widgetSelected(final SelectionEvent e) {
                update();
            }
        });

        labelBindingsForCommand = new Label(groupCommand, SWT.LEFT);
        gridData = new GridData(GridData.VERTICAL_ALIGN_BEGINNING);
        gridData.verticalAlignment = GridData.FILL_VERTICAL;
        labelBindingsForCommand.setLayoutData(gridData);
        labelBindingsForCommand.setText(Util.translateString(RESOURCE_BUNDLE, "labelAssignmentsForCommand")); //$NON-NLS-1$
        tableBindingsForCommand = new Table(groupCommand,
                SWT.BORDER | SWT.FULL_SELECTION | SWT.H_SCROLL | SWT.V_SCROLL);
        tableBindingsForCommand.setHeaderVisible(true);
        gridData = new GridData(GridData.FILL_BOTH);
        gridData.heightHint = 60;
        gridData.horizontalSpan = 2;
        boolean isMac = org.eclipse.jface.util.Util.isMac();
        gridData.widthHint = isMac ? 620 : 520;
        tableBindingsForCommand.setLayoutData(gridData);
        TableColumn tableColumnDelta = new TableColumn(tableBindingsForCommand, SWT.NULL, 0);
        tableColumnDelta.setResizable(false);
        tableColumnDelta.setText(Util.ZERO_LENGTH_STRING);
        tableColumnDelta.setWidth(20);
        TableColumn tableColumnContext = new TableColumn(tableBindingsForCommand, SWT.NULL, 1);
        tableColumnContext.setResizable(true);
        tableColumnContext.setText(Util.translateString(RESOURCE_BUNDLE, "tableColumnContext")); //$NON-NLS-1$
        tableColumnContext.pack();
        tableColumnContext.setWidth(200);
        final TableColumn tableColumnKeySequence = new TableColumn(tableBindingsForCommand, SWT.NULL, 2);
        tableColumnKeySequence.setResizable(true);
        tableColumnKeySequence.setText(Util.translateString(RESOURCE_BUNDLE, "tableColumnKeySequence")); //$NON-NLS-1$
        tableColumnKeySequence.pack();
        tableColumnKeySequence.setWidth(300);

        tableBindingsForCommand.addMouseListener(new MouseAdapter() {

            public void mouseDoubleClick(MouseEvent mouseEvent) {
                update();
            }
        });

        tableBindingsForCommand.addSelectionListener(new SelectionAdapter() {

            public void widgetSelected(SelectionEvent selectionEvent) {
                selectedTableBindingsForCommand();
            }
        });

        final Group groupKeySequence = new Group(composite, SWT.SHADOW_NONE);
        gridLayout = new GridLayout();
        gridLayout.numColumns = 4;
        groupKeySequence.setLayout(gridLayout);
        gridData = new GridData(GridData.FILL_BOTH);
        groupKeySequence.setLayoutData(gridData);
        groupKeySequence.setText(Util.translateString(RESOURCE_BUNDLE, "groupKeySequence")); //$NON-NLS-1$   
        final Label labelKeySequence = new Label(groupKeySequence, SWT.LEFT);
        gridData = new GridData();
        labelKeySequence.setLayoutData(gridData);
        labelKeySequence.setText(Util.translateString(RESOURCE_BUNDLE, "labelKeySequence")); //$NON-NLS-1$

        // The text widget into which the key strokes will be entered.
        textTriggerSequence = new Text(groupKeySequence, SWT.BORDER);
        // On MacOS X, this font will be changed by KeySequenceText
        textTriggerSequence.setFont(groupKeySequence.getFont());
        gridData = new GridData();
        gridData.horizontalSpan = 2;
        gridData.widthHint = 300;
        textTriggerSequence.setLayoutData(gridData);
        textTriggerSequence.addModifyListener(new ModifyListener() {
            public void modifyText(ModifyEvent e) {
                update();
            }
        });
        textTriggerSequence.addFocusListener(new FocusListener() {
            public void focusGained(FocusEvent e) {
                bindingService.setKeyFilterEnabled(false);
            }

            public void focusLost(FocusEvent e) {
                bindingService.setKeyFilterEnabled(true);
            }
        });
        textTriggerSequence.addDisposeListener(new DisposeListener() {
            public void widgetDisposed(DisposeEvent e) {
                if (!bindingService.isKeyFilterEnabled()) {
                    bindingService.setKeyFilterEnabled(true);
                }
            }
        });

        // The manager for the key sequence text widget.
        textTriggerSequenceManager = new KeySequenceText(textTriggerSequence);
        textTriggerSequenceManager.setKeyStrokeLimit(4);

        // Button for adding trapped key strokes
        final Button buttonAddKey = new Button(groupKeySequence, SWT.LEFT | SWT.ARROW);
        buttonAddKey.setToolTipText(Util.translateString(RESOURCE_BUNDLE, "buttonAddKey.ToolTipText")); //$NON-NLS-1$
        gridData = new GridData();
        gridData.heightHint = comboCategory.getTextHeight();
        buttonAddKey.setLayoutData(gridData);

        // Arrow buttons aren't normally added to the tab list. Let's fix that.
        final Control[] tabStops = groupKeySequence.getTabList();
        final ArrayList newTabStops = new ArrayList();
        for (int i = 0; i < tabStops.length; i++) {
            Control tabStop = tabStops[i];
            newTabStops.add(tabStop);
            if (textTriggerSequence.equals(tabStop)) {
                newTabStops.add(buttonAddKey);
            }
        }
        final Control[] newTabStopArray = (Control[]) newTabStops.toArray(new Control[newTabStops.size()]);
        groupKeySequence.setTabList(newTabStopArray);

        // Construct the menu to attach to the above button.
        final Menu menuButtonAddKey = new Menu(buttonAddKey);
        final Iterator trappedKeyItr = KeySequenceText.TRAPPED_KEYS.iterator();
        while (trappedKeyItr.hasNext()) {
            final KeyStroke trappedKey = (KeyStroke) trappedKeyItr.next();
            final MenuItem menuItem = new MenuItem(menuButtonAddKey, SWT.PUSH);
            menuItem.setText(trappedKey.format());
            menuItem.addSelectionListener(new SelectionAdapter() {

                public void widgetSelected(SelectionEvent e) {
                    textTriggerSequenceManager.insert(trappedKey);
                    textTriggerSequence.setFocus();
                    textTriggerSequence.setSelection(textTriggerSequence.getTextLimit());
                }
            });
        }
        buttonAddKey.addSelectionListener(new SelectionAdapter() {

            public void widgetSelected(SelectionEvent selectionEvent) {
                Point buttonLocation = buttonAddKey.getLocation();
                buttonLocation = groupKeySequence.toDisplay(buttonLocation.x, buttonLocation.y);
                Point buttonSize = buttonAddKey.getSize();
                menuButtonAddKey.setLocation(buttonLocation.x, buttonLocation.y + buttonSize.y);
                menuButtonAddKey.setVisible(true);
            }
        });

        labelBindingsForTriggerSequence = new Label(groupKeySequence, SWT.LEFT);
        gridData = new GridData(GridData.VERTICAL_ALIGN_BEGINNING);
        gridData.verticalAlignment = GridData.FILL_VERTICAL;
        labelBindingsForTriggerSequence.setLayoutData(gridData);
        labelBindingsForTriggerSequence
                .setText(Util.translateString(RESOURCE_BUNDLE, "labelAssignmentsForKeySequence")); //$NON-NLS-1$
        tableBindingsForTriggerSequence = new Table(groupKeySequence,
                SWT.BORDER | SWT.FULL_SELECTION | SWT.H_SCROLL | SWT.V_SCROLL);
        tableBindingsForTriggerSequence.setHeaderVisible(true);
        gridData = new GridData(GridData.FILL_BOTH);
        gridData.heightHint = 60;
        gridData.horizontalSpan = 3;
        gridData.widthHint = isMac ? 620 : 520;
        tableBindingsForTriggerSequence.setLayoutData(gridData);
        tableColumnDelta = new TableColumn(tableBindingsForTriggerSequence, SWT.NULL, 0);
        tableColumnDelta.setResizable(false);
        tableColumnDelta.setText(Util.ZERO_LENGTH_STRING);
        tableColumnDelta.setWidth(20);
        tableColumnContext = new TableColumn(tableBindingsForTriggerSequence, SWT.NULL, 1);
        tableColumnContext.setResizable(true);
        tableColumnContext.setText(Util.translateString(RESOURCE_BUNDLE, "tableColumnContext")); //$NON-NLS-1$
        tableColumnContext.pack();
        tableColumnContext.setWidth(200);
        final TableColumn tableColumnCommand = new TableColumn(tableBindingsForTriggerSequence, SWT.NULL, 2);
        tableColumnCommand.setResizable(true);
        tableColumnCommand.setText(Util.translateString(RESOURCE_BUNDLE, "tableColumnCommand")); //$NON-NLS-1$
        tableColumnCommand.pack();
        tableColumnCommand.setWidth(300);

        tableBindingsForTriggerSequence.addMouseListener(new MouseAdapter() {

            public void mouseDoubleClick(MouseEvent mouseEvent) {
                update();
            }
        });

        tableBindingsForTriggerSequence.addSelectionListener(new SelectionAdapter() {

            public void widgetSelected(SelectionEvent selectionEvent) {
                selectedTableBindingsForTriggerSequence();
            }
        });

        final Composite compositeContext = new Composite(composite, SWT.NULL);
        gridLayout = new GridLayout();
        gridLayout.numColumns = 3;
        compositeContext.setLayout(gridLayout);
        gridData = new GridData(GridData.FILL_HORIZONTAL);
        compositeContext.setLayoutData(gridData);
        final Label labelContext = new Label(compositeContext, SWT.LEFT);
        labelContext.setText(Util.translateString(RESOURCE_BUNDLE, "labelContext")); //$NON-NLS-1$
        comboContext = new Combo(compositeContext, SWT.READ_ONLY);
        gridData = new GridData();
        gridData.widthHint = 250;
        comboContext.setLayoutData(gridData);
        comboContext.setVisibleItemCount(ITEMS_TO_SHOW);

        comboContext.addSelectionListener(new SelectionAdapter() {
            public final void widgetSelected(final SelectionEvent e) {
                update();
            }
        });

        labelContextExtends = new Label(compositeContext, SWT.LEFT);
        gridData = new GridData(GridData.FILL_HORIZONTAL);
        labelContextExtends.setLayoutData(gridData);
        final Composite compositeButton = new Composite(composite, SWT.NULL);
        gridLayout = new GridLayout();
        gridLayout.marginHeight = 20;
        gridLayout.marginWidth = 0;
        gridLayout.numColumns = 3;
        compositeButton.setLayout(gridLayout);
        gridData = new GridData();
        compositeButton.setLayoutData(gridData);
        buttonAdd = new Button(compositeButton, SWT.CENTER | SWT.PUSH);
        gridData = new GridData();
        int widthHint = convertHorizontalDLUsToPixels(IDialogConstants.BUTTON_WIDTH);
        buttonAdd.setText(Util.translateString(RESOURCE_BUNDLE, "buttonAdd")); //$NON-NLS-1$
        gridData.widthHint = Math.max(widthHint, buttonAdd.computeSize(SWT.DEFAULT, SWT.DEFAULT, true).x) + 5;
        buttonAdd.setLayoutData(gridData);

        buttonAdd.addSelectionListener(new SelectionAdapter() {

            public void widgetSelected(SelectionEvent selectionEvent) {
                selectedButtonAdd();
            }
        });

        buttonRemove = new Button(compositeButton, SWT.CENTER | SWT.PUSH);
        gridData = new GridData();
        widthHint = convertHorizontalDLUsToPixels(IDialogConstants.BUTTON_WIDTH);
        buttonRemove.setText(Util.translateString(RESOURCE_BUNDLE, "buttonRemove")); //$NON-NLS-1$
        gridData.widthHint = Math.max(widthHint, buttonRemove.computeSize(SWT.DEFAULT, SWT.DEFAULT, true).x) + 5;
        buttonRemove.setLayoutData(gridData);

        buttonRemove.addSelectionListener(new SelectionAdapter() {

            public void widgetSelected(SelectionEvent selectionEvent) {
                selectedButtonRemove();
            }
        });

        buttonRestore = new Button(compositeButton, SWT.CENTER | SWT.PUSH);
        gridData = new GridData();
        widthHint = convertHorizontalDLUsToPixels(IDialogConstants.BUTTON_WIDTH);
        buttonRestore.setText(Util.translateString(RESOURCE_BUNDLE, "buttonRestore")); //$NON-NLS-1$
        gridData.widthHint = Math.max(widthHint, buttonRestore.computeSize(SWT.DEFAULT, SWT.DEFAULT, true).x) + 5;
        buttonRestore.setLayoutData(gridData);

        buttonRestore.addSelectionListener(new SelectionAdapter() {

            public void widgetSelected(SelectionEvent selectionEvent) {
                selectedButtonRestore();
            }
        });

        return composite;
    }

    /**
     * Creates a tab on the main page for displaying an uneditable list of the
     * current key bindings. This is intended as a discovery tool for new users.
     * It shows all of the key bindings for the current key configuration,
     * platform and locale.
     * 
     * @param parent
     *            The tab folder in which the tab should be created; must not be
     *            <code>null</code>.
     * @return The newly created composite containing all of the controls; never
     *         <code>null</code>.
     * @since 3.1
     */
    private final Composite createViewTab(final TabFolder parent) {
        GridData gridData = null;
        int widthHint;

        // Create the composite for the tab.
        final Composite composite = new Composite(parent, SWT.NONE);
        composite.setLayoutData(new GridData(GridData.FILL_BOTH));
        composite.setLayout(new GridLayout());

        // Place a table inside the tab.
        tableBindings = new Table(composite, SWT.BORDER | SWT.FULL_SELECTION | SWT.H_SCROLL | SWT.V_SCROLL);
        tableBindings.setHeaderVisible(true);
        gridData = new GridData(GridData.FILL_BOTH);
        gridData.heightHint = 400;
        gridData.horizontalSpan = 2;
        tableBindings.setLayoutData(gridData);
        final TableColumn tableColumnCategory = new TableColumn(tableBindings, SWT.NONE,
                VIEW_CATEGORY_COLUMN_INDEX);
        tableColumnCategory.setText(SORTED_COLUMN_NAMES[VIEW_CATEGORY_COLUMN_INDEX]);
        tableColumnCategory.addSelectionListener(new SortOrderSelectionListener(VIEW_CATEGORY_COLUMN_INDEX));
        final TableColumn tableColumnCommand = new TableColumn(tableBindings, SWT.NONE, VIEW_COMMAND_COLUMN_INDEX);
        tableColumnCommand.setText(UNSORTED_COLUMN_NAMES[VIEW_COMMAND_COLUMN_INDEX]);
        tableColumnCommand.addSelectionListener(new SortOrderSelectionListener(VIEW_COMMAND_COLUMN_INDEX));
        final TableColumn tableColumnKeySequence = new TableColumn(tableBindings, SWT.NONE,
                VIEW_KEY_SEQUENCE_COLUMN_INDEX);
        tableColumnKeySequence.setText(UNSORTED_COLUMN_NAMES[VIEW_KEY_SEQUENCE_COLUMN_INDEX]);
        tableColumnKeySequence.addSelectionListener(new SortOrderSelectionListener(VIEW_KEY_SEQUENCE_COLUMN_INDEX));
        final TableColumn tableColumnContext = new TableColumn(tableBindings, SWT.NONE, VIEW_CONTEXT_COLUMN_INDEX);
        tableColumnContext.setText(UNSORTED_COLUMN_NAMES[VIEW_CONTEXT_COLUMN_INDEX]);
        tableColumnContext.addSelectionListener(new SortOrderSelectionListener(VIEW_CONTEXT_COLUMN_INDEX));
        tableBindings.addSelectionListener(new SelectionAdapter() {
            public final void widgetDefaultSelected(final SelectionEvent e) {
                selectedTableKeyBindings();
            }
        });

        // A composite for the buttons.
        final Composite buttonBar = new Composite(composite, SWT.NONE);
        buttonBar.setLayout(new GridLayout(2, false));
        gridData = new GridData();
        gridData.horizontalAlignment = GridData.END;
        buttonBar.setLayoutData(gridData);

        // A button for editing the current selection.
        final Button editButton = new Button(buttonBar, SWT.PUSH);
        gridData = new GridData();
        widthHint = convertHorizontalDLUsToPixels(IDialogConstants.BUTTON_WIDTH);
        editButton.setText(Util.translateString(RESOURCE_BUNDLE, "buttonEdit")); //$NON-NLS-1$
        gridData.widthHint = Math.max(widthHint, editButton.computeSize(SWT.DEFAULT, SWT.DEFAULT, true).x) + 5;
        editButton.setLayoutData(gridData);
        editButton.addSelectionListener(new SelectionListener() {

            /*
             * (non-Javadoc)
             * 
             * @see org.eclipse.swt.events.SelectionListener#widgetDefaultSelected(org.eclipse.swt.events.SelectionEvent)
             */
            public final void widgetDefaultSelected(final SelectionEvent event) {
                selectedTableKeyBindings();
            }

            /*
             * (non-Javadoc)
             * 
             * @see org.eclipse.swt.events.SelectionListener#widgetSelected(org.eclipse.swt.events.SelectionEvent)
             */
            public void widgetSelected(SelectionEvent e) {
                widgetDefaultSelected(e);
            }
        });

        // A button for exporting the contents to a file.
        final Button buttonExport = new Button(buttonBar, SWT.PUSH);
        gridData = new GridData();
        widthHint = convertHorizontalDLUsToPixels(IDialogConstants.BUTTON_WIDTH);
        buttonExport.setText(Util.translateString(RESOURCE_BUNDLE, "buttonExport")); //$NON-NLS-1$
        gridData.widthHint = Math.max(widthHint, buttonExport.computeSize(SWT.DEFAULT, SWT.DEFAULT, true).x) + 5;
        buttonExport.setLayoutData(gridData);
        buttonExport.addSelectionListener(new SelectionListener() {

            /*
             * (non-Javadoc)
             * 
             * @see org.eclipse.swt.events.SelectionListener#widgetDefaultSelected(org.eclipse.swt.events.SelectionEvent)
             */
            public final void widgetDefaultSelected(final SelectionEvent event) {
                selectedButtonExport();
            }

            /*
             * (non-Javadoc)
             * 
             * @see org.eclipse.swt.events.SelectionListener#widgetSelected(org.eclipse.swt.events.SelectionEvent)
             */
            public void widgetSelected(SelectionEvent e) {
                widgetDefaultSelected(e);
            }
        });

        return composite;
    }

    protected IPreferenceStore doGetPreferenceStore() {
        return PrefUtil.getInternalPreferenceStore();
    }

    /**
     * Allows the user to change the key bindings for a particular command.
     * Switches the tab to the modify tab, and then selects the category and
     * command that corresponds with the given command name. It then selects the
     * given key sequence and gives focus to the key sequence text widget.
     * 
     * @param binding
     *            The binding to be edited; if <code>null</code>, then just
     *            switch to the modify tab. If the <code>binding</code> does
     *            not correspond to anything in the keys preference page, then
     *            this also just switches to the modify tab.
     * @since 3.1
     */
    public final void editBinding(final Binding binding) {
        // Switch to the modify tab.
        tabFolder.setSelection(TAB_INDEX_MODIFY);

        // If there is no command name, stop here.
        if (binding == null) {
            return;
        }

        /*
         * Get the corresponding category and command names. If either is
         * undefined, then we can just stop now. We won't be able to find their
         * name.
         */
        final ParameterizedCommand command = binding.getParameterizedCommand();
        String categoryName = null;
        String commandName = null;
        try {
            categoryName = command.getCommand().getCategory().getName();
            commandName = command.getName();
        } catch (final NotDefinedException e) {
            return; // no name
        }

        // Update the category combo box.
        final String[] categoryNames = comboCategory.getItems();
        int i = 0;
        for (; i < categoryNames.length; i++) {
            if (categoryName.equals(categoryNames[i])) {
                break;
            }
        }
        if (i >= comboCategory.getItemCount()) {
            // Couldn't find the category, so abort.
            return;
        }
        comboCategory.select(i);

        // Update the commands combo box.
        updateComboCommand();

        // Update the command combo box.
        final String[] commandNames = comboCommand.getItems();
        int j = 0;
        for (; j < commandNames.length; j++) {
            if (commandName.equals(commandNames[j])) {
                if (comboCommand.getSelectionIndex() != j) {
                    comboCommand.select(j);
                }
                break;
            }
        }
        if (j >= comboCommand.getItemCount()) {
            // Couldn't find the command, so just select the first and then stop
            if (comboCommand.getSelectionIndex() != 0) {
                comboCommand.select(0);
            }
            update();
            return;
        }

        /*
         * Update and validate the state of the modify tab in response to these
         * selection changes.
         */
        update();

        // Select the right key binding, if possible.
        final TableItem[] items = tableBindingsForCommand.getItems();
        int k = 0;
        for (; k < items.length; k++) {
            final String currentKeySequence = items[k].getText(2);
            if (binding.getTriggerSequence().format().equals(currentKeySequence)) {
                break;
            }
        }
        if (k < tableBindingsForCommand.getItemCount()) {
            tableBindingsForCommand.select(k);
            tableBindingsForCommand.notifyListeners(SWT.Selection, null);
            textTriggerSequence.setFocus();
        }
    }

    /**
     * Returns the identifier for the currently selected category.
     * 
     * @return The selected category; <code>null</code> if none.
     */
    private final String getCategoryId() {
        return !commandIdsByCategoryId.containsKey(null) || comboCategory.getSelectionIndex() > 0
                ? (String) categoryIdsByUniqueName.get(comboCategory.getText())
                : null;
    }

    /**
     * Returns the identifier for the currently selected context.
     * 
     * @return The selected context; <code>null</code> if none.
     */
    private final String getContextId() {
        return comboContext.getSelectionIndex() >= 0 ? (String) contextIdsByUniqueName.get(comboContext.getText())
                : null;
    }

    /**
     * Returns the current trigger sequence.
     * 
     * @return The trigger sequence; may be empty, but never <code>null</code>.
     */
    private final KeySequence getKeySequence() {
        return textTriggerSequenceManager.getKeySequence();
    }

    /**
     * Returns the currently-selected fully-parameterized command.
     * 
     * @return The selected fully-parameterized command; <code>null</code> if
     *         none.
     */
    private final ParameterizedCommand getParameterizedCommand() {
        final int selectionIndex = comboCommand.getSelectionIndex();
        if ((selectionIndex >= 0) && (commands != null) && (selectionIndex < commands.length)) {
            return commands[selectionIndex];
        }

        return null;
    }

    /**
     * Returns the identifier for the currently selected scheme.
     * 
     * @return The selected scheme; <code>null</code> if none.
     */
    private final String getSchemeId() {
        return comboScheme.getSelectionIndex() >= 0 ? (String) schemeIdsByUniqueName.get(comboScheme.getText())
                : null;
    }

    public final void init(final IWorkbench workbench) {
        activityManager = workbench.getActivitySupport().getActivityManager();
        bindingService = (IBindingService) workbench.getService(IBindingService.class);
        commandService = (ICommandService) workbench.getService(ICommandService.class);
        contextService = (IContextService) workbench.getService(IContextService.class);
    }

    /**
     * Checks whether the activity manager knows anything about this command
     * identifier. If the activity manager is currently filtering this command,
     * then it does not appear in the user interface.
     * 
     * @param command
     *            The command which should be checked against the activities;
     *            must not be <code>null</code>.
     * @return <code>true</code> if the command identifier is not filtered;
     *         <code>false</code> if it is
     */
    private final boolean isActive(final Command command) {
        return activityManager.getIdentifier(command.getId()).isEnabled();
    }

    /**
     * Logs the given exception, and opens an error dialog saying that something
     * went wrong. The exception is assumed to have something to do with the
     * preference store.
     * 
     * @param exception
     *            The exception to be logged; must not be <code>null</code>.
     */
    private final void logPreferenceStoreException(final Throwable exception) {
        final String message = Util.translateString(RESOURCE_BUNDLE, "PreferenceStoreError.Message"); //$NON-NLS-1$
        String exceptionMessage = exception.getMessage();
        if (exceptionMessage == null) {
            exceptionMessage = message;
        }
        final IStatus status = new Status(IStatus.ERROR, WorkbenchPlugin.PI_WORKBENCH, 0, exceptionMessage,
                exception);
        WorkbenchPlugin.log(message, status);
        StatusUtil.handleStatus(message, exception, StatusManager.SHOW);
    }

    public final boolean performCancel() {
        // Save the selected tab for future reference.
        persistSelectedTab();

        return super.performCancel();
    }

    protected final void performDefaults() {
        // Ask the user to confirm
        final String title = Util.translateString(RESOURCE_BUNDLE, "restoreDefaultsMessageBoxText"); //$NON-NLS-1$
        final String message = Util.translateString(RESOURCE_BUNDLE, "restoreDefaultsMessageBoxMessage"); //$NON-NLS-1$
        final boolean confirmed = MessageDialog.open(MessageDialog.CONFIRM, getShell(), title, message, SWT.SHEET);

        if (confirmed) {
            // Fix the scheme in the local changes.
            final String defaultSchemeId = bindingService.getDefaultSchemeId();
            final Scheme defaultScheme = localChangeManager.getScheme(defaultSchemeId);
            try {
                localChangeManager.setActiveScheme(defaultScheme);
            } catch (final NotDefinedException e) {
                // At least we tried....
            }

            // Fix the bindings in the local changes.
            final Binding[] currentBindings = localChangeManager.getBindings();
            final int currentBindingsLength = currentBindings.length;
            final Set trimmedBindings = new HashSet();
            for (int i = 0; i < currentBindingsLength; i++) {
                final Binding binding = currentBindings[i];
                if (binding.getType() != Binding.USER) {
                    trimmedBindings.add(binding);
                }
            }
            final Binding[] trimmedBindingArray = (Binding[]) trimmedBindings
                    .toArray(new Binding[trimmedBindings.size()]);
            localChangeManager.setBindings(trimmedBindingArray);

            // Apply the changes.
            try {
                bindingService.savePreferences(defaultScheme, trimmedBindingArray);
            } catch (final IOException e) {
                logPreferenceStoreException(e);
            }
        }

        setScheme(localChangeManager.getActiveScheme()); // update the scheme
        update(true);
        super.performDefaults();
    }

    public final boolean performOk() {
        // Save the preferences.
        try {
            bindingService.savePreferences(localChangeManager.getActiveScheme(), localChangeManager.getBindings());
        } catch (final IOException e) {
            logPreferenceStoreException(e);
        }

        // Save the selected tab for future reference.
        persistSelectedTab();

        return super.performOk();
    }

    /**
     * Remembers the currently selected tab for when the preference page next
     * opens.
     */
    private final void persistSelectedTab() {
        final IPreferenceStore store = getPreferenceStore();
        store.setValue(IPreferenceConstants.KEYS_PREFERENCE_SELECTED_TAB, tabFolder.getSelectionIndex());
    }

    /**
     * Handles the selection event on the add button. This removes all
     * user-defined bindings matching the given key sequence, scheme and
     * context. It then adds a new binding with the current selections.
     */
    private final void selectedButtonAdd() {
        final ParameterizedCommand command = getParameterizedCommand();
        final String contextId = getContextId();
        final String schemeId = getSchemeId();
        final KeySequence keySequence = getKeySequence();
        localChangeManager.removeBindings(keySequence, schemeId, contextId, null, null, null, Binding.USER);
        localChangeManager.addBinding(
                new KeyBinding(keySequence, command, schemeId, contextId, null, null, null, Binding.USER));
        update(true);
    }

    /**
     * Provides a facility for exporting the viewable list of key bindings to a
     * file. Currently, this only supports exporting to a list of
     * comma-separated values. The user is prompted for which file should
     * receive our bounty.
     * 
     * @since 3.1
     */
    private final void selectedButtonExport() {
        final FileDialog fileDialog = new FileDialog(getShell(), SWT.SAVE | SWT.SHEET);
        fileDialog.setFilterExtensions(new String[] { "*.csv" }); //$NON-NLS-1$
        fileDialog.setFilterNames(new String[] { Util.translateString(RESOURCE_BUNDLE, "csvFilterName") }); //$NON-NLS-1$
        final String filePath = fileDialog.open();
        if (filePath == null) {
            return;
        }

        final SafeRunnable runnable = new SafeRunnable() {
            public final void run() throws IOException {
                Writer fileWriter = null;
                try {
                    fileWriter = new BufferedWriter(new FileWriter(filePath));
                    final TableItem[] items = tableBindings.getItems();
                    final int numColumns = tableBindings.getColumnCount();
                    for (int i = 0; i < items.length; i++) {
                        final TableItem item = items[i];
                        for (int j = 0; j < numColumns; j++) {
                            String buf = Util.replaceAll(item.getText(j), "\"", //$NON-NLS-1$
                                    "\"\""); //$NON-NLS-1$
                            fileWriter.write("\"" + buf + "\""); //$NON-NLS-1$//$NON-NLS-2$
                            if (j < numColumns - 1) {
                                fileWriter.write(',');
                            }
                        }
                        fileWriter.write(System.getProperty("line.separator")); //$NON-NLS-1$
                    }

                } finally {
                    if (fileWriter != null) {
                        try {
                            fileWriter.close();
                        } catch (final IOException e) {
                            // At least I tried.
                        }
                    }

                }
            }
        };
        SafeRunner.run(runnable);
    }

    /**
     * Handles the selection event on the remove button. This removes all
     * user-defined bindings matching the given key sequence, scheme and
     * context. It then adds a new deletion binding for the selected trigger
     * sequence.
     */
    private final void selectedButtonRemove() {
        final String contextId = getContextId();
        final String schemeId = getSchemeId();
        final KeySequence keySequence = getKeySequence();
        localChangeManager.removeBindings(keySequence, schemeId, contextId, null, null, null, Binding.USER);
        localChangeManager
                .addBinding(new KeyBinding(keySequence, null, schemeId, contextId, null, null, null, Binding.USER));
        update(true);
    }

    /**
     * Handles the selection event on the restore button. This removes all
     * user-defined bindings matching the given key sequence, scheme and
     * context.
     */
    private final void selectedButtonRestore() {
        String contextId = getContextId();
        String schemeId = getSchemeId();
        KeySequence keySequence = getKeySequence();
        localChangeManager.removeBindings(keySequence, schemeId, contextId, null, null, null, Binding.USER);
        update(true);
    }

    /**
     * Updates the local managers active scheme, and then updates the interface.
     */
    private final void selectedComboScheme() {
        final String activeSchemeId = getSchemeId();
        final Scheme activeScheme = localChangeManager.getScheme(activeSchemeId);
        try {
            localChangeManager.setActiveScheme(activeScheme);
        } catch (final NotDefinedException e) {
            // Oh, well.
        }
        update(true);
    }

    /**
     * Handles the selection event on the table containing the bindings for a
     * particular command. This updates the context and trigger sequence based
     * on the selected binding.
     */
    private final void selectedTableBindingsForCommand() {
        final int selection = tableBindingsForCommand.getSelectionIndex();
        if ((selection >= 0) && (selection < tableBindingsForCommand.getItemCount())) {
            final TableItem item = tableBindingsForCommand.getItem(selection);
            final KeyBinding binding = (KeyBinding) item.getData(ITEM_DATA_KEY);
            setContextId(binding.getContextId());
            setKeySequence(binding.getKeySequence());
        }

        update();
    }

    /**
     * Handles the selection event on the table containing the bindings for a
     * particular trigger sequence. This updates the context based on the
     * selected binding.
     */
    private final void selectedTableBindingsForTriggerSequence() {
        final int selection = tableBindingsForTriggerSequence.getSelectionIndex();
        if ((selection >= 0) && (selection < tableBindingsForTriggerSequence.getItemCount())) {
            final TableItem item = tableBindingsForTriggerSequence.getItem(selection);
            final Binding binding = (Binding) item.getData(ITEM_DATA_KEY);
            setContextId(binding.getContextId());
        }

        update();
    }

    /**
     * Responds to some kind of trigger on the View tab by taking the current
     * selection on the key bindings table and selecting the appropriate items
     * in the Modify tab.
     * 
     * @since 3.1
     */
    private final void selectedTableKeyBindings() {
        final int selectionIndex = tableBindings.getSelectionIndex();
        if (selectionIndex != -1) {
            final TableItem item = tableBindings.getItem(selectionIndex);
            final Binding binding = (Binding) item.getData(BINDING_KEY);
            editBinding(binding);

        } else {
            editBinding(null);
        }
    }

    /**
     * Changes the selected context name in the context combo box. The context
     * selected is either the one matching the identifier provided (if
     * possible), or the default context identifier. If no matching name can be
     * found in the combo, then the first item is selected.
     * 
     * @param contextId
     *            The context identifier for the context to be selected in the
     *            combo box; may be <code>null</code>.
     */
    private final void setContextId(final String contextId) {
        // Clear the current selection.
        comboContext.clearSelection();
        comboContext.deselectAll();

        // Figure out which name to look for.
        String contextName = (String) contextUniqueNamesById.get(contextId);
        if (contextName == null) {
            contextName = (String) contextUniqueNamesById.get(IContextIds.CONTEXT_ID_WINDOW);
        }
        if (contextName == null) {
            contextName = Util.ZERO_LENGTH_STRING;
        }

        // Scan the list for the selection we're looking for.
        final String[] items = comboContext.getItems();
        boolean found = false;
        for (int i = 0; i < items.length; i++) {
            if (contextName.equals(items[i])) {
                comboContext.select(i);
                found = true;
                break;
            }
        }

        // If we didn't find an item, then set the first item as selected.
        if ((!found) && (items.length > 0)) {
            comboContext.select(0);
        }
    }

    /**
     * Sets the current trigger sequence.
     * 
     * @param keySequence
     *            The trigger sequence; may be <code>null</code>.
     */
    private final void setKeySequence(final KeySequence keySequence) {
        textTriggerSequenceManager.setKeySequence(keySequence);
    }

    /**
     * Changes the selection in the command combo box.
     * 
     * @param command
     *            The fully-parameterized command to select; may be
     *            <code>null</code>.
     */
    private final void setParameterizedCommand(final ParameterizedCommand command) {
        int i = 0;
        if (commands != null) {
            final int commandCount = commands.length;
            for (; i < commandCount; i++) {
                if (commands[i].equals(command)) {
                    if ((comboCommand.getSelectionIndex() != i) && (i < comboCommand.getItemCount())) {
                        comboCommand.select(i);
                    }
                    break;
                }
            }
            if ((i >= comboCommand.getItemCount()) && (comboCommand.getSelectionIndex() != 0)) {
                comboCommand.select(0);
            }
        }
    }

    /**
     * Sets the currently selected scheme
     * 
     * @param scheme
     *            The scheme to select; may be <code>null</code>.
     */
    private final void setScheme(final Scheme scheme) {
        comboScheme.clearSelection();
        comboScheme.deselectAll();
        final String schemeUniqueName = (String) schemeUniqueNamesById.get(scheme.getId());

        if (schemeUniqueName != null) {
            final String items[] = comboScheme.getItems();

            for (int i = 0; i < items.length; i++) {
                if (schemeUniqueName.equals(items[i])) {
                    comboScheme.select(i);
                    break;
                }
            }
        }
    }

    /**
     * Builds the internal look-up tables before allowing the page to become
     * visible.
     */
    public final void setVisible(final boolean visible) {
        if (visible == true) {
            Map contextsByName = new HashMap();

            for (Iterator iterator = contextService.getDefinedContextIds().iterator(); iterator.hasNext();) {
                Context context = contextService.getContext((String) iterator.next());
                try {
                    String name = context.getName();
                    Collection contexts = (Collection) contextsByName.get(name);

                    if (contexts == null) {
                        contexts = new HashSet();
                        contextsByName.put(name, contexts);
                    }

                    contexts.add(context);
                } catch (final NotDefinedException e) {
                    // Do nothing.
                }
            }

            Map commandsByName = new HashMap();

            for (Iterator iterator = commandService.getDefinedCommandIds().iterator(); iterator.hasNext();) {
                Command command = commandService.getCommand((String) iterator.next());
                if (!isActive(command)) {
                    continue;
                }

                try {
                    String name = command.getName();
                    Collection commands = (Collection) commandsByName.get(name);

                    if (commands == null) {
                        commands = new HashSet();
                        commandsByName.put(name, commands);
                    }

                    commands.add(command);
                } catch (NotDefinedException eNotDefined) {
                    // Do nothing
                }
            }

            // moved here to allow us to remove any empty categories
            commandIdsByCategoryId = new HashMap();

            for (Iterator iterator = commandService.getDefinedCommandIds().iterator(); iterator.hasNext();) {
                final Command command = commandService.getCommand((String) iterator.next());
                if (!isActive(command)) {
                    continue;
                }

                try {
                    String categoryId = command.getCategory().getId();
                    Collection commandIds = (Collection) commandIdsByCategoryId.get(categoryId);

                    if (commandIds == null) {
                        commandIds = new HashSet();
                        commandIdsByCategoryId.put(categoryId, commandIds);
                    }

                    commandIds.add(command.getId());
                } catch (NotDefinedException eNotDefined) {
                    // Do nothing
                }
            }

            Map categoriesByName = new HashMap();

            for (Iterator iterator = commandService.getDefinedCategoryIds().iterator(); iterator.hasNext();) {
                Category category = commandService.getCategory((String) iterator.next());

                try {
                    if (commandIdsByCategoryId.containsKey(category.getId())) {
                        String name = category.getName();
                        Collection categories = (Collection) categoriesByName.get(name);

                        if (categories == null) {
                            categories = new HashSet();
                            categoriesByName.put(name, categories);
                        }

                        categories.add(category);
                    }
                } catch (NotDefinedException eNotDefined) {
                    // Do nothing
                }
            }

            Map schemesByName = new HashMap();

            final Scheme[] definedSchemes = bindingService.getDefinedSchemes();
            for (int i = 0; i < definedSchemes.length; i++) {
                final Scheme scheme = definedSchemes[i];
                try {
                    String name = scheme.getName();
                    Collection schemes = (Collection) schemesByName.get(name);

                    if (schemes == null) {
                        schemes = new HashSet();
                        schemesByName.put(name, schemes);
                    }

                    schemes.add(scheme);
                } catch (final NotDefinedException e) {
                    // Do nothing.
                }
            }

            contextIdsByUniqueName = new HashMap();
            contextUniqueNamesById = new HashMap();

            for (Iterator iterator = contextsByName.entrySet().iterator(); iterator.hasNext();) {
                Map.Entry entry = (Map.Entry) iterator.next();
                String name = (String) entry.getKey();
                Set contexts = (Set) entry.getValue();
                Iterator iterator2 = contexts.iterator();

                if (contexts.size() == 1) {
                    Context context = (Context) iterator2.next();
                    contextIdsByUniqueName.put(name, context.getId());
                    contextUniqueNamesById.put(context.getId(), name);
                } else {
                    while (iterator2.hasNext()) {
                        Context context = (Context) iterator2.next();
                        String uniqueName = MessageFormat
                                .format(Util.translateString(RESOURCE_BUNDLE, "uniqueName"), new Object[] { name, //$NON-NLS-1$
                                        context.getId() });
                        contextIdsByUniqueName.put(uniqueName, context.getId());
                        contextUniqueNamesById.put(context.getId(), uniqueName);
                    }
                }
            }

            categoryIdsByUniqueName = new HashMap();
            categoryUniqueNamesById = new HashMap();

            for (Iterator iterator = categoriesByName.entrySet().iterator(); iterator.hasNext();) {
                Map.Entry entry = (Map.Entry) iterator.next();
                String name = (String) entry.getKey();
                Set categories = (Set) entry.getValue();
                Iterator iterator2 = categories.iterator();

                if (categories.size() == 1) {
                    Category category = (Category) iterator2.next();
                    categoryIdsByUniqueName.put(name, category.getId());
                    categoryUniqueNamesById.put(category.getId(), name);
                } else {
                    while (iterator2.hasNext()) {
                        Category category = (Category) iterator2.next();
                        String uniqueName = MessageFormat
                                .format(Util.translateString(RESOURCE_BUNDLE, "uniqueName"), new Object[] { name, //$NON-NLS-1$
                                        category.getId() });
                        categoryIdsByUniqueName.put(uniqueName, category.getId());
                        categoryUniqueNamesById.put(category.getId(), uniqueName);
                    }
                }
            }

            schemeIdsByUniqueName = new HashMap();
            schemeUniqueNamesById = new HashMap();

            for (Iterator iterator = schemesByName.entrySet().iterator(); iterator.hasNext();) {
                Map.Entry entry = (Map.Entry) iterator.next();
                String name = (String) entry.getKey();
                Set keyConfigurations = (Set) entry.getValue();
                Iterator iterator2 = keyConfigurations.iterator();

                if (keyConfigurations.size() == 1) {
                    Scheme scheme = (Scheme) iterator2.next();
                    schemeIdsByUniqueName.put(name, scheme.getId());
                    schemeUniqueNamesById.put(scheme.getId(), name);
                } else {
                    while (iterator2.hasNext()) {
                        Scheme scheme = (Scheme) iterator2.next();
                        String uniqueName = MessageFormat
                                .format(Util.translateString(RESOURCE_BUNDLE, "uniqueName"), new Object[] { name, //$NON-NLS-1$
                                        scheme.getId() });
                        schemeIdsByUniqueName.put(uniqueName, scheme.getId());
                        schemeUniqueNamesById.put(scheme.getId(), uniqueName);
                    }
                }
            }

            Scheme activeScheme = bindingService.getActiveScheme();

            // Make an internal copy of the binding manager, for local changes.
            try {
                for (int i = 0; i < definedSchemes.length; i++) {
                    final Scheme scheme = definedSchemes[i];
                    final Scheme copy = localChangeManager.getScheme(scheme.getId());
                    copy.define(scheme.getName(), scheme.getDescription(), scheme.getParentId());
                }
                localChangeManager.setActiveScheme(bindingService.getActiveScheme());
            } catch (final NotDefinedException e) {
                throw new Error("There is a programmer error in the keys preference page"); //$NON-NLS-1$
            }
            localChangeManager.setLocale(bindingService.getLocale());
            localChangeManager.setPlatform(bindingService.getPlatform());
            localChangeManager.setBindings(bindingService.getBindings());

            // Populate the category combo box.
            List categoryNames = new ArrayList(categoryIdsByUniqueName.keySet());
            Collections.sort(categoryNames, Collator.getInstance());
            if (commandIdsByCategoryId.containsKey(null)) {
                categoryNames.add(0, Util.translateString(RESOURCE_BUNDLE, "other")); //$NON-NLS-1$
            }
            comboCategory.setItems((String[]) categoryNames.toArray(new String[categoryNames.size()]));
            comboCategory.clearSelection();
            comboCategory.deselectAll();
            if (commandIdsByCategoryId.containsKey(null) || !categoryNames.isEmpty()) {
                comboCategory.select(0);
            }

            // Populate the scheme combo box.
            List schemeNames = new ArrayList(schemeIdsByUniqueName.keySet());
            Collections.sort(schemeNames, Collator.getInstance());
            comboScheme.setItems((String[]) schemeNames.toArray(new String[schemeNames.size()]));
            setScheme(activeScheme);

            // Update the entire page.
            update(true);
        }

        super.setVisible(visible);
    }

    /**
     * Updates the entire preference page -- except the view tab -- based on
     * current selection sate. This preference page is written so that
     * everything can be made consistent simply by inspecting the state of its
     * widgets. A change is triggered by the user, and an event is fired. The
     * event triggers an update. It is possible for extra work to be done by
     * this page before calling update.
     */
    private final void update() {
        update(false);
    }

    /**
     * Updates the entire preference page based on current changes. This
     * preference page is written so that everything can be made consistent
     * simply by inspecting the state of its widgets. A change is triggered by
     * the user, and an event is fired. The event triggers an update. It is
     * possible for extra work to be done by this page before calling update.
     * 
     * @param updateViewTab
     *            Whether the view tab should be updated as well.
     */
    private final void update(final boolean updateViewTab) {
        if (updateViewTab) {
            updateViewTab();
        }
        updateComboCommand();
        updateComboContext();
        final TriggerSequence triggerSequence = getKeySequence();
        updateTableBindingsForTriggerSequence(triggerSequence);
        final ParameterizedCommand command = getParameterizedCommand();
        updateTableBindingsForCommand(command);
        final String contextId = getContextId();
        updateSelection(tableBindingsForTriggerSequence, contextId, triggerSequence);
        updateSelection(tableBindingsForCommand, contextId, triggerSequence);
        updateLabelSchemeExtends();
        updateLabelContextExtends();
        updateEnabled(triggerSequence, command);
    }

    /**
     * Updates the contents of the commands combo box, based on the current
     * selection in the category combo box.
     */
    private final void updateComboCommand() {
        // Remember the current selection, so we can restore it later.
        final ParameterizedCommand command = getParameterizedCommand();

        // Figure out where command identifiers apply to the selected category.
        final String categoryId = getCategoryId();
        Set commandIds = (Set) commandIdsByCategoryId.get(categoryId);
        if (commandIds == null) {
            commandIds = Collections.EMPTY_SET;
        }

        /*
         * Generate an array of parameterized commands based on these
         * identifiers. The parameterized commands will be sorted based on their
         * names.
         */
        List commands = new ArrayList();
        final Iterator commandIdItr = commandIds.iterator();
        while (commandIdItr.hasNext()) {
            final String currentCommandId = (String) commandIdItr.next();
            final Command currentCommand = commandService.getCommand(currentCommandId);
            try {
                commands.addAll(ParameterizedCommand.generateCombinations(currentCommand));
            } catch (final NotDefinedException e) {
                // It is safe to just ignore undefined commands.
            }
        }

        // sort the commands with a collator, so they appear in the
        // combo correctly
        commands = sortParameterizedCommands(commands);

        final int commandCount = commands.size();
        this.commands = (ParameterizedCommand[]) commands.toArray(new ParameterizedCommand[commandCount]);

        /*
         * Generate an array of command names based on this array of
         * parameterized commands.
         */
        final String[] commandNames = new String[commandCount];
        for (int i = 0; i < commandCount; i++) {
            try {
                commandNames[i] = this.commands[i].getName();
            } catch (final NotDefinedException e) {
                throw new Error("Concurrent modification of the command's defined state"); //$NON-NLS-1$
            }
        }

        /*
         * Copy the command names into the combo box, but only if they've
         * changed. We do this to try to avoid unnecessary calls out to the
         * operating system, as well as to defend against bugs in SWT's event
         * mechanism.
         */
        final String[] currentItems = comboCommand.getItems();
        if (!Arrays.equals(currentItems, commandNames)) {
            comboCommand.setItems(commandNames);
        }

        // Try to restore the selection.
        setParameterizedCommand(command);

        /*
         * Just to be extra careful, make sure that we have a selection at this
         * point. This line could probably be removed, but it makes the code a
         * bit more robust.
         */
        if ((comboCommand.getSelectionIndex() == -1) && (commandCount > 0)) {
            comboCommand.select(0);
        }
    }

    /**
     * Sort the commands using the correct language.
     * @param commands the List of ParameterizedCommands
     * @return The sorted List
     */
    private List sortParameterizedCommands(List commands) {
        final Collator collator = Collator.getInstance();

        // this comparator is based on the ParameterizedCommands#compareTo(*)
        // method, but uses the collator.
        Comparator comparator = new Comparator() {
            public int compare(Object o1, Object o2) {
                String name1 = null;
                String name2 = null;
                try {
                    name1 = ((ParameterizedCommand) o1).getName();
                } catch (NotDefinedException e) {
                    return -1;
                }
                try {
                    name2 = ((ParameterizedCommand) o2).getName();
                } catch (NotDefinedException e) {
                    return 1;
                }
                int rc = collator.compare(name1, name2);
                if (rc != 0) {
                    return rc;
                }

                String id1 = ((ParameterizedCommand) o1).getId();
                String id2 = ((ParameterizedCommand) o2).getId();
                return collator.compare(id1, id2);
            }
        };
        Collections.sort(commands, comparator);
        return commands;
    }

    /**
     * Updates the contents of the context combo box, as well as its selection.
     */
    private final void updateComboContext() {
        final String contextId = getContextId();
        final Map contextIdsByName = new HashMap(contextIdsByUniqueName);

        final List contextNames = new ArrayList(contextIdsByName.keySet());
        Collections.sort(contextNames, Collator.getInstance());

        comboContext.setItems((String[]) contextNames.toArray(new String[contextNames.size()]));
        setContextId(contextId);

        if (comboContext.getSelectionIndex() == -1 && !contextNames.isEmpty()) {
            comboContext.select(0);
        }
    }

    /**
     * Updates the enabled state of the various widgets on this page. The
     * decision is based on the current trigger sequence and the currently
     * selected command.
     * 
     * @param triggerSequence
     *            The current trigger sequence; may be empty, but never
     *            <code>null</code>.
     * @param command
     *            The currently selected command, if any; <code>null</code>
     *            otherwise.
     */
    private final void updateEnabled(final TriggerSequence triggerSequence, final ParameterizedCommand command) {
        final boolean commandSelected = command != null;
        labelBindingsForCommand.setEnabled(commandSelected);
        tableBindingsForCommand.setEnabled(commandSelected);

        final boolean triggerSequenceSelected = !triggerSequence.isEmpty();
        labelBindingsForTriggerSequence.setEnabled(triggerSequenceSelected);
        tableBindingsForTriggerSequence.setEnabled(triggerSequenceSelected);

        /*
         * TODO Do some better button enablement.
         */
        final boolean buttonsEnabled = commandSelected && triggerSequenceSelected;
        buttonAdd.setEnabled(buttonsEnabled);
        buttonRemove.setEnabled(buttonsEnabled);
        buttonRestore.setEnabled(buttonsEnabled);
    }

    /**
     * Updates the label next to the context that says "extends" if the context
     * is a child of another context. If the context is not a child of another
     * context, then the label is simply blank.
     */
    private final void updateLabelContextExtends() {
        final String contextId = getContextId();

        if (contextId != null) {
            final Context context = contextService.getContext(getContextId());
            if (context.isDefined()) {
                try {
                    final String parentId = context.getParentId();
                    if (parentId != null) {
                        final String name = (String) contextUniqueNamesById.get(parentId);
                        if (name != null) {
                            labelContextExtends
                                    .setText(MessageFormat.format(Util.translateString(RESOURCE_BUNDLE, "extends"), //$NON-NLS-1$
                                            new Object[] { name }));
                            return;
                        }
                    }
                } catch (final NotDefinedException e) {
                    // Do nothing
                }
            }
        }

        labelContextExtends.setText(Util.ZERO_LENGTH_STRING);
    }

    /**
     * Updates the label next to the scheme that says "extends" if the scheme is
     * a child of another scheme. If the scheme is not a child of another
     * scheme, then the label is simply blank.
     */
    private final void updateLabelSchemeExtends() {
        final String schemeId = getSchemeId();

        if (schemeId != null) {
            final Scheme scheme = bindingService.getScheme(schemeId);
            try {
                final String name = (String) schemeUniqueNamesById.get(scheme.getParentId());
                if (name != null) {
                    labelSchemeExtends
                            .setText(MessageFormat.format(Util.translateString(RESOURCE_BUNDLE, "extends"), //$NON-NLS-1$
                                    new Object[] { name }));
                    return;
                }
            } catch (final NotDefinedException e) {
                // Do nothing
            }
        }

        labelSchemeExtends.setText(Util.ZERO_LENGTH_STRING);
    }

    /**
     * Tries to select the correct entry in table based on the currently
     * selected context and trigger sequence. If the table hasn't really
     * changed, then this method is essentially trying to restore the selection.
     * If it has changed, then it is trying to select the most entry based on
     * the context.
     * 
     * @param table
     *            The table to be changed; must not be <code>null</code>.
     * @param contextId
     *            The currently selected context; should not be
     *            <code>null</code>.
     * @param triggerSequence
     *            The current trigger sequence; should not be <code>null</code>.
     */
    private final void updateSelection(final Table table, final String contextId,
            final TriggerSequence triggerSequence) {
        if (table.getSelectionCount() > 1) {
            table.deselectAll();
        }

        final TableItem[] items = table.getItems();
        int selection = -1;
        for (int i = 0; i < items.length; i++) {
            final Binding binding = (Binding) items[i].getData(ITEM_DATA_KEY);
            if ((Util.equals(contextId, binding.getContextId()))
                    && (Util.equals(triggerSequence, binding.getTriggerSequence()))) {
                selection = i;
                break;
            }
        }

        if (selection != -1) {
            table.select(selection);
        }
    }

    /**
     * Updates the contents of the table showing the bindings for the currently
     * selected command. The selection is destroyed by this process.
     * 
     * @param parameterizedCommand
     *            The currently selected fully-parameterized command; may be
     *            <code>null</code>.
     */
    private final void updateTableBindingsForCommand(final ParameterizedCommand parameterizedCommand) {
        // Clear the table of existing items.
        tableBindingsForCommand.removeAll();

        // Add each of the bindings, if the command identifier matches.
        final Collection bindings = localChangeManager.getActiveBindingsDisregardingContextFlat();
        final Iterator bindingItr = bindings.iterator();
        while (bindingItr.hasNext()) {
            final Binding binding = (Binding) bindingItr.next();
            if (!Util.equals(parameterizedCommand, binding.getParameterizedCommand())) {
                continue; // binding does not match
            }

            final TableItem tableItem = new TableItem(tableBindingsForCommand, SWT.NULL);
            tableItem.setData(ITEM_DATA_KEY, binding);

            /*
             * Set the associated image based on the type of binding. Either it
             * is a user binding or a system binding.
             * 
             * TODO Identify more image types.
             */
            if (binding.getType() == Binding.SYSTEM) {
                tableItem.setImage(0, IMAGE_BLANK);
            } else {
                tableItem.setImage(0, IMAGE_CHANGE);
            }

            String contextName = (String) contextUniqueNamesById.get(binding.getContextId());
            if (contextName == null) {
                contextName = Util.ZERO_LENGTH_STRING;
            }
            tableItem.setText(1, contextName);
            tableItem.setText(2, binding.getTriggerSequence().format());
        }
    }

    /**
     * Updates the contents of the table showing the bindings for the current
     * trigger sequence. The selection is destroyed by this process.
     * 
     * @param triggerSequence
     *            The current trigger sequence; may be <code>null</code> or
     *            empty.
     */
    private final void updateTableBindingsForTriggerSequence(final TriggerSequence triggerSequence) {
        // Clear the table of its existing items.
        tableBindingsForTriggerSequence.removeAll();

        // Get the collection of bindings for the current command.
        final Map activeBindings = localChangeManager.getActiveBindingsDisregardingContext();
        final Collection bindings = (Collection) activeBindings.get(triggerSequence);
        if (bindings == null) {
            return;
        }

        // Add each of the bindings.
        final Iterator bindingItr = bindings.iterator();
        while (bindingItr.hasNext()) {
            final Binding binding = (Binding) bindingItr.next();
            final Context context = contextService.getContext(binding.getContextId());
            final ParameterizedCommand parameterizedCommand = binding.getParameterizedCommand();
            final Command command = parameterizedCommand.getCommand();
            if ((!context.isDefined()) && (!command.isDefined())) {
                continue;
            }

            final TableItem tableItem = new TableItem(tableBindingsForTriggerSequence, SWT.NULL);
            tableItem.setData(ITEM_DATA_KEY, binding);

            /*
             * Set the associated image based on the type of binding. Either it
             * is a user binding or a system binding.
             * 
             * TODO Identify more image types.
             */
            if (binding.getType() == Binding.SYSTEM) {
                tableItem.setImage(0, IMAGE_BLANK);
            } else {
                tableItem.setImage(0, IMAGE_CHANGE);
            }

            try {
                tableItem.setText(1, context.getName());
                tableItem.setText(2, parameterizedCommand.getName());
            } catch (final NotDefinedException e) {
                throw new Error(
                        "Context or command became undefined on a non-UI thread while the UI thread was processing."); //$NON-NLS-1$
            }
        }
    }

    /**
     * Updates the contents of the view tab. This queries the command manager
     * for a list of key sequence binding definitions, and these definitions are
     * then added to the table.
     * 
     * @since 3.1
     */
    private final void updateViewTab() {
        // Clear out the existing table contents.
        tableBindings.removeAll();

        // Get a sorted list of key binding contents.
        final List bindings = new ArrayList(localChangeManager.getActiveBindingsDisregardingContextFlat());
        Collections.sort(bindings, new Comparator() {
            /**
             * Compares two instances of <code>Binding</code> based on the
             * current sort order.
             * 
             * @param object1
             *            The first object to compare; must be an instance of
             *            <code>Binding</code> (i.e., not <code>null</code>).
             * @param object2
             *            The second object to compare; must be an instance of
             *            <code>Binding</code> (i.e., not <code>null</code>).
             * @return The integer value representing the comparison. The
             *         comparison is based on the current sort order.
             * @since 3.1
             */
            public final int compare(final Object object1, final Object object2) {
                final Binding binding1 = (Binding) object1;
                final Binding binding2 = (Binding) object2;

                /*
                 * Get the category name, command name, formatted key sequence
                 * and context name for the first binding.
                 */
                final Command command1 = binding1.getParameterizedCommand().getCommand();
                String categoryName1 = Util.ZERO_LENGTH_STRING;
                String commandName1 = Util.ZERO_LENGTH_STRING;
                try {
                    commandName1 = command1.getName();
                    categoryName1 = command1.getCategory().getName();
                } catch (final NotDefinedException e) {
                    // Just use the zero-length string.
                }
                final String triggerSequence1 = binding1.getTriggerSequence().format();
                final String contextId1 = binding1.getContextId();
                String contextName1 = Util.ZERO_LENGTH_STRING;
                if (contextId1 != null) {
                    final Context context = contextService.getContext(contextId1);
                    try {
                        contextName1 = context.getName();
                    } catch (final org.eclipse.core.commands.common.NotDefinedException e) {
                        // Just use the zero-length string.
                    }
                }

                /*
                 * Get the category name, command name, formatted key sequence
                 * and context name for the first binding.
                 */
                final Command command2 = binding2.getParameterizedCommand().getCommand();
                String categoryName2 = Util.ZERO_LENGTH_STRING;
                String commandName2 = Util.ZERO_LENGTH_STRING;
                try {
                    commandName2 = command2.getName();
                    categoryName2 = command2.getCategory().getName();
                } catch (final org.eclipse.core.commands.common.NotDefinedException e) {
                    // Just use the zero-length string.
                }
                final String keySequence2 = binding2.getTriggerSequence().format();
                final String contextId2 = binding2.getContextId();
                String contextName2 = Util.ZERO_LENGTH_STRING;
                if (contextId2 != null) {
                    final Context context = contextService.getContext(contextId2);
                    try {
                        contextName2 = context.getName();
                    } catch (final org.eclipse.core.commands.common.NotDefinedException e) {
                        // Just use the zero-length string.
                    }
                }

                // Compare the items in the current sort order.
                int compare = 0;
                for (int i = 0; i < sortOrder.length; i++) {
                    switch (sortOrder[i]) {
                    case VIEW_CATEGORY_COLUMN_INDEX:
                        compare = Util.compare(categoryName1, categoryName2);
                        if (compare != 0) {
                            return compare;
                        }
                        break;
                    case VIEW_COMMAND_COLUMN_INDEX:
                        compare = Util.compare(commandName1, commandName2);
                        if (compare != 0) {
                            return compare;
                        }
                        break;
                    case VIEW_KEY_SEQUENCE_COLUMN_INDEX:
                        compare = Util.compare(triggerSequence1, keySequence2);
                        if (compare != 0) {
                            return compare;
                        }
                        break;
                    case VIEW_CONTEXT_COLUMN_INDEX:
                        compare = Util.compare(contextName1, contextName2);
                        if (compare != 0) {
                            return compare;
                        }
                        break;
                    default:
                        throw new Error(
                                "Programmer error: added another sort column without modifying the comparator."); //$NON-NLS-1$
                    }
                }

                return compare;
            }

            /**
             * @see Object#equals(java.lang.Object)
             */
            public final boolean equals(final Object object) {
                return super.equals(object);
            }
        });

        // Add a table item for each item in the list.
        final Iterator keyBindingItr = bindings.iterator();
        while (keyBindingItr.hasNext()) {
            final Binding binding = (Binding) keyBindingItr.next();

            // Get the command and category name.
            final ParameterizedCommand command = binding.getParameterizedCommand();
            String commandName = Util.ZERO_LENGTH_STRING;
            String categoryName = Util.ZERO_LENGTH_STRING;
            try {
                commandName = command.getName();
                categoryName = command.getCommand().getCategory().getName();
            } catch (final org.eclipse.core.commands.common.NotDefinedException e) {
                // Just use the zero-length string.
            }

            // Ignore items with a meaningless command name.
            if ((commandName == null) || (commandName.length() == 0)) {
                continue;
            }

            // Get the context name.
            final String contextId = binding.getContextId();
            String contextName = Util.ZERO_LENGTH_STRING;
            if (contextId != null) {
                final Context context = contextService.getContext(contextId);
                try {
                    contextName = context.getName();
                } catch (final org.eclipse.core.commands.common.NotDefinedException e) {
                    // Just use the zero-length string.
                }
            }

            // Create the table item.
            final TableItem item = new TableItem(tableBindings, SWT.NONE);
            item.setText(VIEW_CATEGORY_COLUMN_INDEX, categoryName);
            item.setText(VIEW_COMMAND_COLUMN_INDEX, commandName);
            item.setText(VIEW_KEY_SEQUENCE_COLUMN_INDEX, binding.getTriggerSequence().format());
            item.setText(VIEW_CONTEXT_COLUMN_INDEX, contextName);
            item.setData(BINDING_KEY, binding);
        }

        // Pack the columns.
        for (int i = 0; i < tableBindings.getColumnCount(); i++) {
            tableBindings.getColumn(i).pack();
        }
    }

}