/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.netbeans.modules.options.keymap;

import java.awt.CardLayout;
import java.awt.Component;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.swing.ComboBoxModel;
import javax.swing.DefaultCellEditor;
import javax.swing.DefaultComboBoxModel;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JPopupMenu;
import javax.swing.JTable;
import javax.swing.ListSelectionModel;
import javax.swing.Popup;
import javax.swing.PopupFactory;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.RowSorterEvent;
import javax.swing.event.TableModelEvent;
import javax.swing.table.TableColumn;
import javax.swing.table.TableModel;
import org.netbeans.api.options.OptionsDisplayer;
import org.netbeans.core.options.keymap.api.ShortcutAction;
import org.netbeans.spi.options.OptionsPanelController;
import org.openide.DialogDescriptor;
import org.openide.DialogDisplayer;
import org.openide.util.HelpCtx;
import org.openide.util.NbBundle;
import org.openide.util.Task;
import org.openide.util.TaskListener;
import org.openide.util.Utilities;


/**
 *
 * @author Max Sauer
 */
@OptionsPanelController.Keywords(keywords={"#KW_KeymapOptions"}, location=OptionsDisplayer.KEYMAPS)
public class KeymapPanel extends javax.swing.JPanel implements ActionListener, Popupable, ChangeListener {

    // Delay times for incremental search [ms]
    private static final int SEARCH_DELAY_TIME_LONG = 300; // < 3 chars
    private static final int SEARCH_DELAY_TIME_SHORT = 20; // >= 3 chars

    private volatile KeymapViewModel keymapModel;
    private TableSorter sorter;

    private JPopupMenu popup = new JPopupMenu();

    private boolean ignoreActionEvents;
    
    //search fields
    private Popup searchPopup;
    private SpecialkeyPanel specialkeyList;


    /** Creates new form KeymapPanel */
    public KeymapPanel() {
        sorter = new TableSorter(getModel());
        initComponents();
        specialkeyList = new SpecialkeyPanel(this, searchSCField);
        
        // close the popup when user clicks elsewhere
        moreButton.addFocusListener(new FocusAdapter() {
            @Override
            public void focusLost(FocusEvent e) {
                hidePopup();
            }
        });

        sorter.setTableHeader(actionsTable.getTableHeader());
        sorter.getTableHeader().setReorderingAllowed(false);
        actionsTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
        actionsTable.setAutoscrolls(true);
        
        ActionListener al = new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                getModel().setSearchText(searchField.getText());
                getModel().update();
            }
        };

        final Timer searchDelayTimer = new Timer(SEARCH_DELAY_TIME_LONG, al);
        searchDelayTimer.setRepeats(false);

        searchField.getDocument().addDocumentListener(new DocumentListener() {

            @Override
            public void insertUpdate(DocumentEvent e) {
                searchSCField.setText("");
                ((ShortcutListener)searchSCField.getKeyListeners()[0]).clear();
                
                if (searchField.getText().length() > 3)
                    searchDelayTimer.setInitialDelay(SEARCH_DELAY_TIME_SHORT);
                searchDelayTimer.restart();
            }

            @Override
            public void removeUpdate(DocumentEvent e) {
                if (searchField.getText().length() > 3)
                    searchDelayTimer.setInitialDelay(SEARCH_DELAY_TIME_LONG);
                searchDelayTimer.restart();
            }

            @Override
            public void changedUpdate(DocumentEvent e) {
                searchSCField.setText("");
                getModel().setSearchText(searchField.getText());
                getModel().update();
            }
        });

        searchSCField.addKeyListener(new ShortcutListener(false));

        ActionListener al2 = new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                narrowByShortcut();
            }
        };

        final Timer searchDelayTimer2 = new Timer(SEARCH_DELAY_TIME_SHORT, al2);
        searchDelayTimer2.setRepeats(false);
        searchSCField.getDocument().addDocumentListener(new DocumentListener() {

            @Override
            public void insertUpdate(DocumentEvent e) {
                searchField.setText("");
                searchDelayTimer2.restart();
            }

            @Override
            public void removeUpdate(DocumentEvent e) {
                searchDelayTimer2.restart();
            }

            @Override
            public void changedUpdate(DocumentEvent e) {
                searchDelayTimer2.restart();
            }
        });

        actionsTable.addMouseListener(new ButtonCellMouseListener(actionsTable));
        actionsTable.addKeyListener(new KeyAdapter() {

            @Override
            public void keyPressed(KeyEvent e) {
                if (e.getKeyCode() != KeyEvent.VK_CONTEXT_MENU &&
                    e.getKeyCode() != KeyEvent.VK_F2) {
                    return;
                }
		int leadRow = actionsTable.getSelectionModel().getLeadSelectionIndex();
		int leadColumn = actionsTable.getColumnModel().getSelectionModel().
		                   getLeadSelectionIndex();
		if (leadRow != -1 && leadColumn != -1 && !actionsTable.isEditing()) {
                    showPopupMenu(leadRow, leadColumn, -1, -1);
                    e.consume();
		}
            }
            
        });
        TableColumn column = actionsTable.getColumnModel().getColumn(1);
        column.setCellEditor(new ButtonCellEditor(getModel()));
        column.setCellRenderer(new ButtonCellRenderer(actionsTable.getDefaultRenderer(ButtonCellRenderer.class)));
        setColumnWidths();
        popupPanel = new ShortcutPopupPanel(actionsTable, popup);
        popup.add(popupPanel);
        cbProfile.addActionListener(this);
        manageButton.addActionListener(this);
        btnPrintAsHTML.setVisible(ExportShortcutsAction.getExportShortcutsToHTMLAction().isEnabled());
    }
    
    private ShortcutPopupPanel popupPanel;

    private class KeymapTable extends JTable {
        int lastRow;
        int lastColumn;

        @Override
        public boolean editCellAt(int row, int column) {
            lastRow = row;
            lastColumn = column;

            boolean editCellAt = super.editCellAt(row, column);
            ((DefaultCellEditor) getCellEditor(lastRow, lastColumn)).getComponent().requestFocus();
           return editCellAt;
        }

        @Override
        protected void processKeyEvent(KeyEvent e) {

            if (!isEditing())
                super.processKeyEvent(e);
            else {
                Component component = ((DefaultCellEditor) getCellEditor(lastRow, lastColumn)).getComponent();
                component.requestFocus();
                component.dispatchEvent(new KeyEvent(component, e.getID(), e.getWhen(), e.getModifiers(), e.getKeyCode(), e.getKeyChar()));
            }
        }
        
        private String selectedActionId;

        @Override
        public void valueChanged(ListSelectionEvent e) {
            super.valueChanged(e);
            if (!e.getValueIsAdjusting()) {
                int index = getSelectedRow();
                selectedActionId = getActionId(index);
            }
        }
        
        

        @Override
        public void sorterChanged(RowSorterEvent e) {
            String aid = selectedActionId;
            int colIndex = getSelectedColumn();
            super.sorterChanged(e);
            restoreSelection(aid, colIndex);
        }
        
        private void restoreSelection(String id, int colIndex) {
            if (id == null) {
                clearSelection();
                return;
            }
            TableModel tm = getModel();
            for (int i = 0; i < tm.getRowCount(); i++) {
                ActionHolder ah = (ActionHolder)tm.getValueAt(i, 0);
                if (ah != null && id.equals(ah.getAction().getId())) {
                    changeSelection(i, colIndex, false, false);
                    break;
                }
            }
        }
        
        private String getActionId(int modelIndex) {
            if (modelIndex >= 0 && modelIndex < getModel().getRowCount()) {
                ActionHolder h = (ActionHolder)getModel().getValueAt(modelIndex, 0);
                if (h != null) {
                    ShortcutAction sa = h.getAction();
                    return sa.getId();
                }
            }
            return null;
        }
        
        @Override
        public void tableChanged(TableModelEvent e) {
            String aid = selectedActionId;
            // preserve also table column selection:
            int colIndex = getSelectedColumn();
            super.tableChanged(e);
            restoreSelection(aid, colIndex);
        }

    }

    //todo: merge with update
    private void narrowByShortcut() {
        if (searchSCField.getText().length() != 0) {
            final String searchText = searchSCField.getText();
            getModel().runWithoutEvents(new Runnable() {
                public void run() {
                    getModel().getDataVector().removeAllElements();
                    for (String categorySet : getModel().getCategories().keySet()) {
                        for (String category : getModel().getCategories().get(categorySet)) {
                            for (Object o : getMutableModel().getItems(category, false)) {
                                ShortcutAction sca = (ShortcutAction) o;
                                String[] shortcuts = getMutableModel().getShortcuts(sca);
                                for (int i = 0; i < shortcuts.length; i++) {
                                    String shortcut = shortcuts[i];
                                    if (searched(shortcut, searchText))
                                        getModel().addRow(new Object[]{new ActionHolder(sca, false), shortcut, category, ""});
                                }
                            }
                        }
                    }
                }
            });
            getModel().fireTableDataChanged();
        } else
            getModel().update();
    }

    KeymapViewModel getModel() {
        if (keymapModel == null) {
            KeymapViewModel tmpModel = new KeymapViewModel();
            synchronized (this) {
                if (keymapModel == null) {
                    keymapModel = tmpModel;
                    tmpModel.getMutableModel().addChangeListener(this);
                }
            }
        }
        return keymapModel;
    }
    
    MutableShortcutsModel getMutableModel() {
        return getModel().getMutableModel();
    }

    //controller methods
    void applyChanges() {
        stopCurrentCellEditing();
        getMutableModel().apply();
    }

    void cancel() {
        stopCurrentCellEditing();
        if (keymapModel == null)
            return;
        getMutableModel().cancel();
    }

    boolean dataValid() {
        return true;
    }

    boolean isChanged() {
        return getMutableModel().isChanged();
    }

    void update() {
        //do not remember search state
        getModel().setSearchText(""); //NOI18N
        searchSCField.setText("");
        ((ShortcutListener)searchSCField.getKeyListeners()[0]).clear();
        searchField.setText(""); //NOI18N

        //setup profiles
        refreshProfileCombo ();
        
        class I implements Runnable, TaskListener {
            int stage;
            
            public void run() {
                if (stage > 0) {
                    ((CardLayout)actionsView.getLayout()).show(actionsView, "actions"); // NOI18N
                } else {
                    getMutableModel().refreshActions();
                    Task t = getModel().postUpdate();
                    t.addTaskListener(this);
                }
            }
            
            @Override
            public void taskFinished(Task t) {
                stage++;
                SwingUtilities.invokeLater(this);
            } 
        }

        //update model
        KeymapModel.RP.post(new I());
    }

    //controller method end


    private void refreshProfileCombo() {
        ignoreActionEvents = true;
        String currentProfile = getMutableModel().getCurrentProfile();
        List keymaps = getMutableModel().getProfiles();
        ComboBoxModel model = new DefaultComboBoxModel(keymaps.toArray());
        currentProfile = getMutableModel().getProfileDisplayName(currentProfile);
        cbProfile.setModel(model);
        cbProfile.setSelectedItem(currentProfile);
        ignoreActionEvents = false;
    }

    private void stopCurrentCellEditing() {
        int row = actionsTable.getEditingRow();
        int col = actionsTable.getEditingColumn();
        if (row != -1)
            actionsTable.getCellEditor(row,col).stopCellEditing();
    }

    /**
     * @param shortcut shortcut compared with searched text
     * @return true if search text is empty || shortcut starts with or contains
     * searchtext
     */
    private boolean searched(String shortcut, String searchText) {
        //shortcut.equals(searchSCField.getText())
        if (searchText.length() == 0 || shortcut.startsWith(searchText) ||
                shortcut.contains(searchText))
            return true;
        else
            return false;
    }


    /**
     * Adjust column widths
     */
    private void setColumnWidths() {
        TableColumn column = null;
        for (int i = 0; i < actionsTable.getColumnCount(); i++) {
            column = actionsTable.getColumnModel().getColumn(i);
            switch (i) {
                case 0:
                    column.setPreferredWidth(250);
                    break;
                case 1:
                    column.setPreferredWidth(175);
                    break;
                case 2:
                    column.setPreferredWidth(60);
                    break;
                case 3:
                    column.setPreferredWidth(60);
                    break;
            }
        }
    }

    /** This method is called from within the constructor to
     * initialize the form.
     * WARNING: Do NOT modify this code. The content of this method is
     * always regenerated by the Form Editor.
     */
    @SuppressWarnings("unchecked")
    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
    private void initComponents() {

        lProfile = new javax.swing.JLabel();
        cbProfile = new javax.swing.JComboBox();
        manageButton = new javax.swing.JButton();
        spShortcuts = new javax.swing.JScrollPane();
        liShortcuts = new javax.swing.JList();
        searchField = new javax.swing.JTextField();
        searchLabel = new javax.swing.JLabel();
        searchSCLabel = new javax.swing.JLabel();
        searchSCField = new javax.swing.JTextField();
        moreButton = new javax.swing.JButton();
        actionsView = new javax.swing.JPanel();
        jPanel1 = new javax.swing.JPanel();
        actionProgress = new javax.swing.JProgressBar();
        waitLabel = new javax.swing.JLabel();
        jScrollPane1 = new javax.swing.JScrollPane();
        actionsTable = new KeymapTable();
        filler1 = new javax.swing.Box.Filler(new java.awt.Dimension(0, 0), new java.awt.Dimension(0, 0), new java.awt.Dimension(32767, 0));
        filler2 = new javax.swing.Box.Filler(new java.awt.Dimension(0, 0), new java.awt.Dimension(0, 0), new java.awt.Dimension(0, 32767));
        btnPrintAsHTML = new javax.swing.JButton();
        jSeparator1 = new javax.swing.JSeparator();

        lProfile.setLabelFor(cbProfile);
        org.openide.awt.Mnemonics.setLocalizedText(lProfile, org.openide.util.NbBundle.getMessage(KeymapPanel.class, "CTL_Keymap_Name")); // NOI18N

        org.openide.awt.Mnemonics.setLocalizedText(manageButton, org.openide.util.NbBundle.getMessage(KeymapPanel.class, "CTL_Duplicate")); // NOI18N

        spShortcuts.setViewportView(liShortcuts);

        searchField.setText(org.openide.util.NbBundle.getMessage(KeymapPanel.class, "KeymapPanel.searchField.text")); // NOI18N

        searchLabel.setLabelFor(searchField);
        org.openide.awt.Mnemonics.setLocalizedText(searchLabel, org.openide.util.NbBundle.getMessage(KeymapPanel.class, "KeymapPanel.searchLabel.text")); // NOI18N

        searchSCLabel.setLabelFor(searchSCField);
        org.openide.awt.Mnemonics.setLocalizedText(searchSCLabel, org.openide.util.NbBundle.getMessage(KeymapPanel.class, "KeymapPanel.searchSCLabel.text")); // NOI18N

        searchSCField.setText(org.openide.util.NbBundle.getMessage(KeymapPanel.class, "KeymapPanel.searchSCField.text")); // NOI18N

        org.openide.awt.Mnemonics.setLocalizedText(moreButton, org.openide.util.NbBundle.getMessage(KeymapPanel.class, "KeymapPanel.moreButton.text")); // NOI18N
        moreButton.setBorder(javax.swing.BorderFactory.createEtchedBorder());
        moreButton.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                moreButtonActionPerformed(evt);
            }
        });

        actionsView.setLayout(new java.awt.CardLayout());

        actionProgress.setIndeterminate(true);
        actionProgress.setString(org.openide.util.NbBundle.getMessage(KeymapPanel.class, "KeymapPanel.actionProgress.string")); // NOI18N

        waitLabel.setFont(new java.awt.Font("Dialog", 2, 12)); // NOI18N
        org.openide.awt.Mnemonics.setLocalizedText(waitLabel, org.openide.util.NbBundle.getMessage(KeymapPanel.class, "KeymapPanel.waitLabel.text")); // NOI18N

        javax.swing.GroupLayout jPanel1Layout = new javax.swing.GroupLayout(jPanel1);
        jPanel1.setLayout(jPanel1Layout);
        jPanel1Layout.setHorizontalGroup(
            jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(jPanel1Layout.createSequentialGroup()
                .addGap(0, 0, Short.MAX_VALUE)
                .addComponent(waitLabel)
                .addGap(0, 0, Short.MAX_VALUE))
            .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, jPanel1Layout.createSequentialGroup()
                .addContainerGap(225, Short.MAX_VALUE)
                .addComponent(actionProgress, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                .addContainerGap(224, Short.MAX_VALUE))
        );
        jPanel1Layout.setVerticalGroup(
            jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(jPanel1Layout.createSequentialGroup()
                .addGap(0, 57, Short.MAX_VALUE)
                .addComponent(waitLabel)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addComponent(actionProgress, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                .addGap(0, 90, Short.MAX_VALUE))
        );

        actionsView.add(jPanel1, "wait");

        jScrollPane1.setPreferredSize(new java.awt.Dimension(453, 100));

        actionsTable.setModel(sorter);
        jScrollPane1.setViewportView(actionsTable);

        actionsView.add(jScrollPane1, "actions");

        org.openide.awt.Mnemonics.setLocalizedText(btnPrintAsHTML, org.openide.util.NbBundle.getMessage(KeymapPanel.class, "KeymapPanel.btnPrintAsHTML.text")); // NOI18N
        btnPrintAsHTML.setToolTipText(org.openide.util.NbBundle.getMessage(KeymapPanel.class, "KeymapPanel.btnPrintAsHTML.toolTipText")); // NOI18N
        btnPrintAsHTML.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                btnPrintAsHTMLActionPerformed(evt);
            }
        });

        jSeparator1.setOrientation(javax.swing.SwingConstants.VERTICAL);

        javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this);
        this.setLayout(layout);
        layout.setHorizontalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup()
                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING)
                    .addGroup(layout.createSequentialGroup()
                        .addContainerGap()
                        .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                            .addGroup(layout.createSequentialGroup()
                                .addComponent(spShortcuts, javax.swing.GroupLayout.PREFERRED_SIZE, 175, javax.swing.GroupLayout.PREFERRED_SIZE)
                                .addGap(83, 83, 83)
                                .addComponent(filler1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
                            .addGroup(layout.createSequentialGroup()
                                .addComponent(lProfile)
                                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                                .addComponent(cbProfile, 0, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                                .addComponent(btnPrintAsHTML)
                                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                                .addComponent(jSeparator1, javax.swing.GroupLayout.PREFERRED_SIZE, 2, javax.swing.GroupLayout.PREFERRED_SIZE)
                                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                                .addComponent(manageButton))))
                    .addGroup(layout.createSequentialGroup()
                        .addGap(0, 0, Short.MAX_VALUE)
                        .addComponent(searchLabel)
                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                        .addComponent(searchField, javax.swing.GroupLayout.PREFERRED_SIZE, 120, javax.swing.GroupLayout.PREFERRED_SIZE)
                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                        .addComponent(searchSCLabel)
                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                        .addComponent(searchSCField, javax.swing.GroupLayout.PREFERRED_SIZE, 125, javax.swing.GroupLayout.PREFERRED_SIZE)
                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                        .addComponent(moreButton))
                    .addComponent(actionsView, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addComponent(filler2, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
        );

        layout.linkSize(javax.swing.SwingConstants.HORIZONTAL, new java.awt.Component[] {searchField, searchSCField});

        layout.setVerticalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(layout.createSequentialGroup()
                .addContainerGap()
                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addComponent(manageButton, javax.swing.GroupLayout.Alignment.TRAILING)
                    .addComponent(jSeparator1, javax.swing.GroupLayout.PREFERRED_SIZE, 23, javax.swing.GroupLayout.PREFERRED_SIZE)
                    .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                        .addComponent(lProfile)
                        .addComponent(cbProfile, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                        .addComponent(btnPrintAsHTML)))
                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addGroup(layout.createSequentialGroup()
                        .addGap(18, 18, 18)
                        .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                            .addComponent(moreButton)
                            .addComponent(searchSCField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                            .addComponent(searchSCLabel)
                            .addComponent(searchField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                            .addComponent(searchLabel))
                        .addGap(10, 10, 10)
                        .addComponent(actionsView, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED))
                    .addGroup(layout.createSequentialGroup()
                        .addGap(87, 87, 87)
                        .addComponent(filler2, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)))
                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addComponent(spShortcuts, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.PREFERRED_SIZE, 0, javax.swing.GroupLayout.PREFERRED_SIZE)
                    .addComponent(filler1, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
                .addContainerGap())
        );

        searchField.getAccessibleContext().setAccessibleDescription(org.openide.util.NbBundle.getMessage(KeymapPanel.class, "KeymapPanel.searchField.AccessibleContext.accessibleDescription")); // NOI18N
    }// </editor-fold>//GEN-END:initComponents

    public void hidePopup() {
        if (searchPopup != null) {
            searchPopup.hide();
            searchPopup = null;
        }
    }
    /**
     * Shows popup with ESC and TAB keys
     */
    private void moreButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_moreButtonActionPerformed
        if (searchPopup != null) {
            return;
        }
        JComponent tf = (JComponent) evt.getSource();
        Point p = new Point(tf.getX(), tf.getY());
        SwingUtilities.convertPointToScreen(p, this);
        Rectangle usableScreenBounds = Utilities.getUsableScreenBounds();
        if (p.x + specialkeyList.getWidth() > usableScreenBounds.width) {
            p.x = usableScreenBounds.width - specialkeyList.getWidth();
        }
        if (p.y + specialkeyList.getHeight() > usableScreenBounds.height) {
            p.y = usableScreenBounds.height - specialkeyList.getHeight();
        }
        //show special key popup
        searchPopup = PopupFactory.getSharedInstance().getPopup(this, specialkeyList, p.x, p.y);
        searchPopup.show();
}//GEN-LAST:event_moreButtonActionPerformed

    private void btnPrintAsHTMLActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_btnPrintAsHTMLActionPerformed
	if (getMutableModel().getCurrentProfile()!=null){
	    ExportShortcutsAction.exportShortcutsOfProfileToHTML(getMutableModel().getCurrentProfile());
	}        
    }//GEN-LAST:event_btnPrintAsHTMLActionPerformed

    // Variables declaration - do not modify//GEN-BEGIN:variables
    private javax.swing.JProgressBar actionProgress;
    private javax.swing.JTable actionsTable;
    private javax.swing.JPanel actionsView;
    private javax.swing.JButton btnPrintAsHTML;
    private javax.swing.JComboBox cbProfile;
    private javax.swing.Box.Filler filler1;
    private javax.swing.Box.Filler filler2;
    private javax.swing.JPanel jPanel1;
    private javax.swing.JScrollPane jScrollPane1;
    private javax.swing.JSeparator jSeparator1;
    private javax.swing.JLabel lProfile;
    private javax.swing.JList liShortcuts;
    private javax.swing.JButton manageButton;
    private javax.swing.JButton moreButton;
    private javax.swing.JTextField searchField;
    private javax.swing.JLabel searchLabel;
    private javax.swing.JTextField searchSCField;
    private javax.swing.JLabel searchSCLabel;
    private javax.swing.JScrollPane spShortcuts;
    private javax.swing.JLabel waitLabel;
    // End of variables declaration//GEN-END:variables


    @Override
    public Popup getPopup() {
        return searchPopup;
    }

    class ButtonCellMouseListener implements MouseListener {

        private JTable table;

        public ButtonCellMouseListener(JTable table) {
            this.table = table;
        }

        @Override
        public void mouseClicked(MouseEvent e) {
            forwardEvent(e);
        }

        @Override
        public void mousePressed(MouseEvent e) {
        }

        @Override
        public void mouseReleased(MouseEvent e) {
        }

        @Override
        public void mouseEntered(MouseEvent e) {
        }

        @Override
        public void mouseExited(MouseEvent e) {
        }

        private void forwardEvent(MouseEvent e) {
            Point p = new Point(e.getX(), e.getY());
            int row = table.rowAtPoint(p);
            int col = table.columnAtPoint(p);
            
            if (showPopupMenu(row, col, e.getX(), e.getY())) {
                e.consume();
            }
        }
    }
    
    private boolean showPopupMenu(int row, int col, int x, int y) {
        JTable table = actionsTable;
        
        if (col != 1) {
            return false;
        }
        
        Object valueAt = table.getValueAt(row, col);
        ShortcutCellPanel scCell = (ShortcutCellPanel) table.getCellRenderer(row, col).getTableCellRendererComponent(table, valueAt, true, true, row, col);
        Rectangle cellRect = table.getCellRect(row, col, false);
        JButton button = scCell.getButton();
        if (x < 0  || x > (cellRect.x + cellRect.width - button.getWidth())) { //inside changeButton
            boolean isShortcutSet = scCell.getTextField().getText().length() != 0;
            final ShortcutPopupPanel panel = (ShortcutPopupPanel) popup.getComponents()[0];
            panel.setDisplayAddAlternative(isShortcutSet);
            panel.setRow(row);

            if (x == -1 || y == -1) {
                x = button.getX() + 1;
                y = button.getY() + 1;
            }
            panel.setCustomProfile(keymapModel.getMutableModel().isCustomProfile(keymapModel.getMutableModel().getCurrentProfile()));
            popup.show(table, x, y);
            SwingUtilities.invokeLater(new Runnable() {
                public void run() {
                    panel.requestFocus();
                }
            });
            popup.requestFocus();
            return true;
        }
        return false;
    }

    static String loc (String key) {
        return NbBundle.getMessage (KeymapPanel.class, key);
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        if (ignoreActionEvents) {
            return;
        }
        Object source = e.getSource();

        if (source == cbProfile) {
            String profile = (String) cbProfile.getSelectedItem();
            if (profile != null)
                getMutableModel().setCurrentProfile(profile);
            getModel().update();
        } else if (source == manageButton) {
            //remember previous profile state, in case user will cancel dialog
            Map<String, Map<ShortcutAction, Set<String>>> modifiedProfiles = getMutableModel().getModifiedProfiles();
            Set<String> deletedProfiles = getMutableModel().getDeletedProfiles();

            //show manage profiles dialog
            final ProfilesPanel profilesPanel = new ProfilesPanel(this);
            DialogDescriptor dd = new DialogDescriptor(
                    profilesPanel, 
                    NbBundle.getMessage(KeymapPanel.class, "CTL_Manage_Keymap_Profiles"),
                    true, new Object[] { DialogDescriptor.CLOSED_OPTION }, DialogDescriptor.CLOSED_OPTION, 
                    DialogDescriptor.BOTTOM_ALIGN,
                    new HelpCtx("org.netbeans.modules.options.keymap.ProfilesPanel"), 
                    null
            );
            DialogDisplayer.getDefault().notify(dd);

            final String selectedProfile = profilesPanel.getSelectedProfile();
            getMutableModel().setCurrentProfile(selectedProfile);
            refreshProfileCombo();
            keymapModel.update();
        }
        return;
    }
    
    public void stateChanged(ChangeEvent e) {
        if (e.getSource() == getMutableModel()) {
            firePropertyChange(OptionsPanelController.PROP_CHANGED, Boolean.FALSE, Boolean.TRUE);
        }
    }
}
