# -*- coding: utf-8 -*-
#
# Copyright © Spyder Project Contributors
# Licensed under the terms of the MIT License
# (see spyder/__init__.py for details)

"""Shortcut management"""

# Standard library imports
from __future__ import print_function
import os
import re
import sys

# Third party imports
from qtpy import PYQT5
from qtpy.compat import from_qvariant, to_qvariant
from qtpy.QtCore import (QAbstractTableModel, QModelIndex, QRegExp,
                         QSortFilterProxyModel, Qt, Slot)
from qtpy.QtGui import (QKeySequence, QRegExpValidator)
from qtpy.QtWidgets import (QAbstractItemView, QApplication, QDialog,
                            QDialogButtonBox, QGridLayout, QHBoxLayout, QLabel,
                            QLineEdit, QMessageBox, QPushButton, QSpacerItem,
                            QTableView, QVBoxLayout)

# Local imports
from spyder.config.base import _, debug_print
from spyder.config.gui import (get_shortcut, iter_shortcuts,
                               reset_shortcuts, set_shortcut)
from spyder.plugins.configdialog import GeneralConfigPage
from spyder.utils import icon_manager as ima
from spyder.utils.qthelpers import get_std_icon
from spyder.utils.stringmatching import get_search_scores, get_search_regex
from spyder.widgets.helperwidgets import HTMLDelegate
from spyder.widgets.helperwidgets import HelperToolButton


MODIFIERS = {Qt.Key_Shift: Qt.SHIFT,
             Qt.Key_Control: Qt.CTRL,
             Qt.Key_Alt: Qt.ALT,
             Qt.Key_Meta: Qt.META}

# Valid shortcut keys
SINGLE_KEYS = ["F{}".format(_i) for _i in range(1, 36)] + ["Delete", "Escape"]
KEYSTRINGS = ["Tab", "Backtab", "Backspace", "Return", "Enter",
              "Pause", "Print", "Clear", "Home", "End", "Left",
              "Up", "Right", "Down", "PageUp", "PageDown"] + \
             ["Space", "Exclam", "QuoteDbl", "NumberSign", "Dollar",
              "Percent", "Ampersand", "Apostrophe", "ParenLeft",
              "ParenRight", "Asterisk", "Plus", "Comma", "Minus",
              "Period", "Slash"] + \
             [str(_i) for _i in range(10)] + \
             ["Colon", "Semicolon", "Less", "Equal", "Greater",
              "Question", "At"] + [chr(_i) for _i in range(65, 91)] + \
             ["BracketLeft", "Backslash", "BracketRight", "Underscore",
              "Control", "Alt", "Shift", "Meta"]
VALID_SINGLE_KEYS = [getattr(Qt, 'Key_{0}'.format(k)) for k in SINGLE_KEYS]
VALID_KEYS = [getattr(Qt, 'Key_{0}'.format(k)) for k in KEYSTRINGS+SINGLE_KEYS]

# Valid finder chars. To be improved
VALID_ACCENT_CHARS = "ÁÉÍOÚáéíúóàèìòùÀÈÌÒÙâêîôûÂÊÎÔÛäëïöüÄËÏÖÜñÑ"
VALID_FINDER_CHARS = "[A-Za-z\s{0}]".format(VALID_ACCENT_CHARS)

BLACKLIST = {
    'Shift+Del': _('Currently used to delete lines on editor/Cut a word'),
    'Shift+Ins': _('Currently used to paste a word')
}

if os.name == 'nt':
    BLACKLIST['Alt+Backspace'] = _('We cannot support this '
                                   'shortcut on Windows')

BLACKLIST['Shift'] = _('Shortcuts that use Shift and another key'
                       ' are unsupported')


class CustomLineEdit(QLineEdit):
    """QLineEdit that filters its key press and release events."""
    def __init__(self, parent):
        super(CustomLineEdit, self).__init__(parent)
        self.setReadOnly(True)
        self.setFocusPolicy(Qt.NoFocus)

    def keyPressEvent(self, e):
        """Qt Override"""
        self.parent().keyPressEvent(e)

    def keyReleaseEvent(self, e):
        """Qt Override"""
        self.parent().keyReleaseEvent(e)


class ShortcutFinder(QLineEdit):
    """Textbox for filtering listed shortcuts in the table."""
    def __init__(self, parent, callback=None):
        super(ShortcutFinder, self).__init__(parent)
        self._parent = parent

        # Widget setup
        regex = QRegExp(VALID_FINDER_CHARS + "{100}")
        self.setValidator(QRegExpValidator(regex))

        # Signals
        if callback:
            self.textChanged.connect(callback)

    def set_text(self, text):
        """Set the filter text."""
        text = text.strip()
        new_text = self.text() + text
        self.setText(new_text)

    def keyPressEvent(self, event):
        """Qt Override."""
        key = event.key()
        if key in [Qt.Key_Up]:
            self._parent.previous_row()
        elif key in [Qt.Key_Down]:
            self._parent.next_row()
        elif key in [Qt.Key_Enter, Qt.Key_Return]:
            self._parent.show_editor()
        else:
            super(ShortcutFinder, self).keyPressEvent(event)


# Error codes for the shortcut editor dialog
(NO_WARNING, SEQUENCE_LENGTH, SEQUENCE_CONFLICT,
 INVALID_KEY, IN_BLACKLIST, SHIFT_BLACKLIST) = [0, 1, 2, 3, 4, 5]


class ShortcutEditor(QDialog):
    """A dialog for entering key sequences."""
    def __init__(self, parent, context, name, sequence, shortcuts):
        super(ShortcutEditor, self).__init__(parent)
        self._parent = parent

        self.context = context
        self.npressed = 0
        self.keys = set()
        self.key_modifiers = set()
        self.key_non_modifiers = list()
        self.key_text = list()
        self.sequence = sequence
        self.new_sequence = None
        self.edit_state = True
        self.shortcuts = shortcuts

        # Widgets
        self.label_info = QLabel()
        self.label_info.setText(_("Press the new shortcut and select 'Ok': \n"
             "(Press 'Tab' once to switch focus between the shortcut entry \n"
             "and the buttons below it)"))
        self.label_current_sequence = QLabel(_("Current shortcut:"))
        self.text_current_sequence = QLabel(sequence)
        self.label_new_sequence = QLabel(_("New shortcut:"))
        self.text_new_sequence = CustomLineEdit(self)
        self.text_new_sequence.setPlaceholderText(sequence)
        self.helper_button = HelperToolButton()
        self.helper_button.hide()
        self.label_warning = QLabel()
        self.label_warning.hide()

        bbox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
        self.button_ok = bbox.button(QDialogButtonBox.Ok)
        self.button_cancel = bbox.button(QDialogButtonBox.Cancel)

        # Setup widgets
        self.setWindowTitle(_('Shortcut: {0}').format(name))
        self.button_ok.setFocusPolicy(Qt.NoFocus)
        self.button_ok.setEnabled(False)
        self.button_cancel.setFocusPolicy(Qt.NoFocus)
        self.helper_button.setToolTip('')
        self.helper_button.setFocusPolicy(Qt.NoFocus)
        style = """
            QToolButton {
              margin:1px;
              border: 0px solid grey;
              padding:0px;
              border-radius: 0px;
            }"""
        self.helper_button.setStyleSheet(style)
        self.text_new_sequence.setFocusPolicy(Qt.NoFocus)
        self.label_warning.setFocusPolicy(Qt.NoFocus)

        # Layout
        spacing = 5
        layout_sequence = QGridLayout()
        layout_sequence.addWidget(self.label_info, 0, 0, 1, 3)
        layout_sequence.addItem(QSpacerItem(spacing, spacing), 1, 0, 1, 2)
        layout_sequence.addWidget(self.label_current_sequence, 2, 0)
        layout_sequence.addWidget(self.text_current_sequence, 2, 2)
        layout_sequence.addWidget(self.label_new_sequence, 3, 0)
        layout_sequence.addWidget(self.helper_button, 3, 1)
        layout_sequence.addWidget(self.text_new_sequence, 3, 2)
        layout_sequence.addWidget(self.label_warning, 4, 2, 1, 2)

        layout = QVBoxLayout()
        layout.addLayout(layout_sequence)
        layout.addSpacing(spacing)
        layout.addWidget(bbox)
        self.setLayout(layout)

        # Signals
        bbox.accepted.connect(self.accept)
        bbox.rejected.connect(self.reject)

    @Slot()
    def reject(self):
        """Slot for rejected signal."""
        # Added for issue #5426.  Due to the focusPolicy of Qt.NoFocus for the
        # buttons, if the cancel button was clicked without first setting focus
        # to the button, it would cause a seg fault crash.
        self.button_cancel.setFocus()
        super().reject()

    @Slot()
    def accept(self):
        """Slot for accepted signal."""
        # Added for issue #5426.  Due to the focusPolicy of Qt.NoFocus for the
        # buttons, if the ok button was clicked without first setting focus to
        # the button, it would cause a seg fault crash.
        self.button_ok.setFocus()
        super().accept()

    def keyPressEvent(self, e):
        """Qt override."""
        key = e.key()
        # Check if valid keys
        if key not in VALID_KEYS:
            self.invalid_key_flag = True
            return

        self.npressed += 1
        self.key_non_modifiers.append(key)
        self.key_modifiers.add(key)
        self.key_text.append(e.text())
        self.invalid_key_flag = False

        debug_print('key {0}, npressed: {1}'.format(key, self.npressed))

        if key == Qt.Key_unknown:
            return

        # The user clicked just and only the special keys
        # Ctrl, Shift, Alt, Meta.
        if (key == Qt.Key_Control or
                key == Qt.Key_Shift or
                key == Qt.Key_Alt or
                key == Qt.Key_Meta):
            return

        modifiers = e.modifiers()
        if modifiers & Qt.ShiftModifier:
            key += Qt.SHIFT
        if modifiers & Qt.ControlModifier:
            key += Qt.CTRL
            if sys.platform == 'darwin':
                self.npressed -= 1
            debug_print('decrementing')
        if modifiers & Qt.AltModifier:
            key += Qt.ALT
        if modifiers & Qt.MetaModifier:
            key += Qt.META

        self.keys.add(key)

    def toggle_state(self):
        """Switch between shortcut entry and Accept/Cancel shortcut mode."""
        self.edit_state = not self.edit_state

        if not self.edit_state:
            self.text_new_sequence.setEnabled(False)
            if self.button_ok.isEnabled():
                self.button_ok.setFocus()
            else:
                self.button_cancel.setFocus()
        else:
            self.text_new_sequence.setEnabled(True)
            self.text_new_sequence.setFocus()

    def nonedit_keyrelease(self, e):
        """Key release event for non-edit state."""
        key = e.key()
        if key in [Qt.Key_Escape]:
            self.close()
            return

        if key in [Qt.Key_Left, Qt.Key_Right, Qt.Key_Up,
                   Qt.Key_Down]:
            if self.button_ok.hasFocus():
                self.button_cancel.setFocus()
            else:
                self.button_ok.setFocus()

    def keyReleaseEvent(self, e):
        """Qt override."""
        self.npressed -= 1
        if self.npressed <= 0:
            key = e.key()

            if len(self.keys) == 1 and key == Qt.Key_Tab:
                self.toggle_state()
                return

            if len(self.keys) == 1 and key == Qt.Key_Escape:
                self.set_sequence('')
                self.label_warning.setText(_("Please introduce a different "
                                             "shortcut"))

            if len(self.keys) == 1 and key in [Qt.Key_Return, Qt.Key_Enter]:
                self.toggle_state()
                return

            if not self.edit_state:
                self.nonedit_keyrelease(e)
            else:
                debug_print('keys: {}'.format(self.keys))
                if self.keys and key != Qt.Key_Escape:
                    self.validate_sequence()
                self.keys = set()
                self.key_modifiers = set()
                self.key_non_modifiers = list()
                self.key_text = list()
                self.npressed = 0

    def check_conflicts(self):
        """Check shortcuts for conflicts."""
        conflicts = []
        for index, shortcut in enumerate(self.shortcuts):
            sequence = str(shortcut.key)
            if sequence == self.new_sequence and \
                (shortcut.context == self.context or shortcut.context == '_' or
                 self.context == '_'):
                conflicts.append(shortcut)
        return conflicts

    def update_warning(self, warning_type=NO_WARNING, conflicts=[]):
        """Update warning label to reflect conflict status of new shortcut"""
        if warning_type == NO_WARNING:
            warn = False
            tip = 'This shortcut is correct!'
        elif warning_type == SEQUENCE_CONFLICT:
            template = '<i>{0}<b>{1}</b></i>'
            tip_title = _('The new shortcut conflicts with:') + '<br>'
            tip_body = ''
            for s in conflicts:
                tip_body += ' - {0}: {1}<br>'.format(s.context, s.name)
            tip_body = tip_body[:-4]  # Removing last <br>
            tip = template.format(tip_title, tip_body)
            warn = True
        elif warning_type == IN_BLACKLIST:
            template = '<i>{0}<b>{1}</b></i>'
            tip_title = _('Forbidden key sequence!') + '<br>'
            tip_body = ''
            use = BLACKLIST[self.new_sequence]
            if use is not None:
                tip_body = use
            tip = template.format(tip_title, tip_body)
            warn = True
        elif warning_type == SHIFT_BLACKLIST:
            template = '<i>{0}<b>{1}</b></i>'
            tip_title = _('Forbidden key sequence!') + '<br>'
            tip_body = ''
            use = BLACKLIST['Shift']
            if use is not None:
                tip_body = use
            tip = template.format(tip_title, tip_body)
            warn = True
        elif warning_type == SEQUENCE_LENGTH:
            # Sequences with 5 keysequences (i.e. Ctrl+1, Ctrl+2, Ctrl+3,
            # Ctrl+4, Ctrl+5) are invalid
            template = '<i>{0}</i>'
            tip = _('A compound sequence can have {break} a maximum of '
                    '4 subsequences.{break}').format(**{'break': '<br>'})
            warn = True
        elif warning_type == INVALID_KEY:
            template = '<i>{0}</i>'
            tip = _('Invalid key entered') + '<br>'
            warn = True

        self.helper_button.show()
        if warn:
            self.label_warning.show()
            self.helper_button.setIcon(get_std_icon('MessageBoxWarning'))
            self.button_ok.setEnabled(False)
        else:
            self.helper_button.setIcon(get_std_icon('DialogApplyButton'))

        self.label_warning.setText(tip)

    def set_sequence(self, sequence):
        """Set the new shortcut and update buttons."""
        if not sequence or self.sequence == sequence:
            self.button_ok.setEnabled(False)
            different_sequence = False
        else:
            self.button_ok.setEnabled(True)
            different_sequence = True

        if sys.platform == 'darwin':
            if 'Meta+Ctrl' in sequence:
                shown_sequence = sequence.replace('Meta+Ctrl', 'Ctrl+Cmd')
            elif 'Ctrl+Meta' in sequence:
                shown_sequence = sequence.replace('Ctrl+Meta', 'Cmd+Ctrl')
            elif 'Ctrl' in sequence:
                shown_sequence = sequence.replace('Ctrl', 'Cmd')
            elif 'Meta' in sequence:
                shown_sequence = sequence.replace('Meta', 'Ctrl')
            else:
                shown_sequence = sequence
        else:
            shown_sequence = sequence
        self.text_new_sequence.setText(shown_sequence)
        self.new_sequence = sequence

        conflicts = self.check_conflicts()
        blacklist = self.new_sequence in BLACKLIST
        individual_keys = self.new_sequence.split('+')
        if conflicts and different_sequence:
            warning_type = SEQUENCE_CONFLICT
        elif blacklist:
            warning_type = IN_BLACKLIST
        elif len(individual_keys) == 2 and individual_keys[0] == 'Shift':
            warning_type = SHIFT_BLACKLIST
        else:
            warning_type = NO_WARNING

        self.update_warning(warning_type=warning_type, conflicts=conflicts)

    def validate_sequence(self):
        """Provide additional checks for accepting or rejecting shortcuts."""
        if self.invalid_key_flag:
            self.update_warning(warning_type=INVALID_KEY)
            return

        for mod in MODIFIERS:
            non_mod = set(self.key_non_modifiers)
            non_mod.discard(mod)
            if mod in self.key_non_modifiers:
                self.key_non_modifiers.remove(mod)

        self.key_modifiers = self.key_modifiers - non_mod

        while u'' in self.key_text:
            self.key_text.remove(u'')

        self.key_text = [k.upper() for k in self.key_text]

        # Fix Backtab, Tab issue
        if Qt.Key_Backtab in self.key_non_modifiers:
            idx = self.key_non_modifiers.index(Qt.Key_Backtab)
            self.key_non_modifiers[idx] = Qt.Key_Tab

        if len(self.key_modifiers) == 0:
            # Filter single key allowed
            if self.key_non_modifiers[0] not in VALID_SINGLE_KEYS:
                return
            # Filter
            elif len(self.key_non_modifiers) > 1:
                return

        # QKeySequence accepts a maximum of 4 different sequences
        if len(self.keys) > 4:
            # Update warning
            self.update_warning(warning_type=SEQUENCE_LENGTH)
            return

        keys = []
        for i in range(len(self.keys)):
            key_seq = 0
            for m in self.key_modifiers:
                key_seq += MODIFIERS[m]
            key_seq += self.key_non_modifiers[i]
            keys.append(key_seq)

        sequence = QKeySequence(*keys)

        self.set_sequence(sequence.toString())


class Shortcut(object):
    """Shortcut convenience class for holding shortcut context, name,
    original ordering index, key sequence for the shortcut and localized text.
    """
    def __init__(self, context, name, key=None):
        self.index = 0  # Sorted index. Populated when loading shortcuts
        self.context = context
        self.name = name
        self.key = key

    def __str__(self):
        return "{0}/{1}: {2}".format(self.context, self.name, self.key)

    def load(self):
        self.key = get_shortcut(self.context, self.name)

    def save(self):
        set_shortcut(self.context, self.name, self.key)


CONTEXT, NAME, SEQUENCE, SEARCH_SCORE = [0, 1, 2, 3]


class ShortcutsModel(QAbstractTableModel):
    def __init__(self, parent):
        QAbstractTableModel.__init__(self)
        self._parent = parent

        self.shortcuts = []
        self.scores = []
        self.rich_text = []
        self.normal_text = []
        self.letters = ''
        self.label = QLabel()
        self.widths = []

        # Needed to compensate for the HTMLDelegate color selection unawarness
        palette = parent.palette()
        self.text_color = palette.text().color().name()
        self.text_color_highlight = palette.highlightedText().color().name()

    def current_index(self):
        """Get the currently selected index in the parent table view."""
        i = self._parent.proxy_model.mapToSource(self._parent.currentIndex())
        return i

    def sortByName(self):
        """Qt Override."""
        self.shortcuts = sorted(self.shortcuts,
                                key=lambda x: x.context+'/'+x.name)
        self.reset()

    def flags(self, index):
        """Qt Override."""
        if not index.isValid():
            return Qt.ItemIsEnabled
        return Qt.ItemFlags(QAbstractTableModel.flags(self, index))

    def data(self, index, role=Qt.DisplayRole):
        """Qt Override."""
        row = index.row()
        if not index.isValid() or not (0 <= row < len(self.shortcuts)):
            return to_qvariant()

        shortcut = self.shortcuts[row]
        key = shortcut.key
        column = index.column()

        if role == Qt.DisplayRole:
            if column == CONTEXT:
                return to_qvariant(shortcut.context)
            elif column == NAME:
                color = self.text_color
                if self._parent == QApplication.focusWidget():
                    if self.current_index().row() == row:
                        color = self.text_color_highlight
                    else:
                        color = self.text_color
                text = self.rich_text[row]
                text = '<p style="color:{0}">{1}</p>'.format(color, text)
                return to_qvariant(text)
            elif column == SEQUENCE:
                text = QKeySequence(key).toString(QKeySequence.NativeText)
                return to_qvariant(text)
            elif column == SEARCH_SCORE:
                # Treating search scores as a table column simplifies the
                # sorting once a score for a specific string in the finder
                # has been defined. This column however should always remain
                # hidden.
                return to_qvariant(self.scores[row])
        elif role == Qt.TextAlignmentRole:
            return to_qvariant(int(Qt.AlignHCenter | Qt.AlignVCenter))
        return to_qvariant()

    def headerData(self, section, orientation, role=Qt.DisplayRole):
        """Qt Override."""
        if role == Qt.TextAlignmentRole:
            if orientation == Qt.Horizontal:
                return to_qvariant(int(Qt.AlignHCenter | Qt.AlignVCenter))
            return to_qvariant(int(Qt.AlignRight | Qt.AlignVCenter))
        if role != Qt.DisplayRole:
            return to_qvariant()
        if orientation == Qt.Horizontal:
            if section == CONTEXT:
                return to_qvariant(_("Context"))
            elif section == NAME:
                return to_qvariant(_("Name"))
            elif section == SEQUENCE:
                return to_qvariant(_("Shortcut"))
            elif section == SEARCH_SCORE:
                return to_qvariant(_("Score"))
        return to_qvariant()

    def rowCount(self, index=QModelIndex()):
        """Qt Override."""
        return len(self.shortcuts)

    def columnCount(self, index=QModelIndex()):
        """Qt Override."""
        return 4

    def setData(self, index, value, role=Qt.EditRole):
        """Qt Override."""
        if index.isValid() and 0 <= index.row() < len(self.shortcuts):
            shortcut = self.shortcuts[index.row()]
            column = index.column()
            text = from_qvariant(value, str)
            if column == SEQUENCE:
                shortcut.key = text
            self.dataChanged.emit(index, index)
            return True
        return False

    def update_search_letters(self, text):
        """Update search letters with text input in search box."""
        self.letters = text
        names = [shortcut.name for shortcut in self.shortcuts]
        results = get_search_scores(text, names, template='<b>{0}</b>')
        self.normal_text, self.rich_text, self.scores = zip(*results)
        self.reset()

    def update_active_row(self):
        """Update active row to update color in selected text."""
        self.data(self.current_index())

    def row(self, row_num):
        """Get row based on model index. Needed for the custom proxy model."""
        return self.shortcuts[row_num]

    def reset(self):
        """"Reset model to take into account new search letters."""
        self.beginResetModel()
        self.endResetModel()


class CustomSortFilterProxy(QSortFilterProxyModel):
    """Custom column filter based on regex."""
    def __init__(self, parent=None):
        super(CustomSortFilterProxy, self).__init__(parent)
        self._parent = parent
        self.pattern = re.compile(u'')

    def set_filter(self, text):
        """Set regular expression for filter."""
        self.pattern = get_search_regex(text)
        if self.pattern:
            self._parent.setSortingEnabled(False)
        else:
            self._parent.setSortingEnabled(True)
        self.invalidateFilter()

    def filterAcceptsRow(self, row_num, parent):
        """Qt override.

        Reimplemented from base class to allow the use of custom filtering.
        """
        model = self.sourceModel()
        name = model.row(row_num).name
        r = re.search(self.pattern, name)

        if r is None:
            return False
        else:
            return True


class ShortcutsTable(QTableView):
    def __init__(self, parent=None):
        QTableView.__init__(self, parent)
        self._parent = parent
        self.finder = None

        self.source_model = ShortcutsModel(self)
        self.proxy_model = CustomSortFilterProxy(self)
        self.last_regex = ''

        self.proxy_model.setSourceModel(self.source_model)
        self.proxy_model.setDynamicSortFilter(True)
        self.proxy_model.setFilterKeyColumn(NAME)
        self.proxy_model.setFilterCaseSensitivity(Qt.CaseInsensitive)
        self.setModel(self.proxy_model)

        self.hideColumn(SEARCH_SCORE)
        self.setItemDelegateForColumn(NAME, HTMLDelegate(self, margin=9))
        self.setSelectionBehavior(QAbstractItemView.SelectRows)
        self.setSelectionMode(QAbstractItemView.SingleSelection)
        self.setSortingEnabled(True)
        self.setEditTriggers(QAbstractItemView.AllEditTriggers)
        self.selectionModel().selectionChanged.connect(self.selection)

        self.verticalHeader().hide()
        self.load_shortcuts()

    def focusOutEvent(self, e):
        """Qt Override."""
        self.source_model.update_active_row()
        super(ShortcutsTable, self).focusOutEvent(e)

    def focusInEvent(self, e):
        """Qt Override."""
        super(ShortcutsTable, self).focusInEvent(e)
        self.selectRow(self.currentIndex().row())

    def selection(self, index):
        """Update selected row."""
        self.update()
        self.isActiveWindow()

    def adjust_cells(self):
        """Adjust column size based on contents."""
        self.resizeRowsToContents()
        self.resizeColumnsToContents()
        fm = self.horizontalHeader().fontMetrics()
        names = [fm.width(s.name + ' '*9) for s in self.source_model.shortcuts]
        self.setColumnWidth(NAME, max(names))
        self.horizontalHeader().setStretchLastSection(True)

    def load_shortcuts(self):
        """Load shortcuts and assign to table model."""
        shortcuts = []
        for context, name, keystr in iter_shortcuts():
            shortcut = Shortcut(context, name, keystr)
            shortcuts.append(shortcut)
        shortcuts = sorted(shortcuts, key=lambda x: x.context+x.name)
        # Store the original order of shortcuts
        for i, shortcut in enumerate(shortcuts):
            shortcut.index = i
        self.source_model.shortcuts = shortcuts
        self.source_model.scores = [0]*len(shortcuts)
        self.source_model.rich_text = [s.name for s in shortcuts]
        self.source_model.reset()
        self.adjust_cells()
        self.sortByColumn(CONTEXT, Qt.AscendingOrder)

    def check_shortcuts(self):
        """Check shortcuts for conflicts."""
        conflicts = []
        for index, sh1 in enumerate(self.source_model.shortcuts):
            if index == len(self.source_model.shortcuts)-1:
                break
            for sh2 in self.source_model.shortcuts[index+1:]:
                if sh2 is sh1:
                    continue
                if str(sh2.key) == str(sh1.key) \
                   and (sh1.context == sh2.context or sh1.context == '_' or
                        sh2.context == '_'):
                    conflicts.append((sh1, sh2))
        if conflicts:
            self.parent().show_this_page.emit()
            cstr = "\n".join(['%s <---> %s' % (sh1, sh2)
                              for sh1, sh2 in conflicts])
            QMessageBox.warning(self, _("Conflicts"),
                                _("The following conflicts have been "
                                  "detected:")+"\n"+cstr, QMessageBox.Ok)

    def save_shortcuts(self):
        """Save shortcuts from table model."""
        self.check_shortcuts()
        for shortcut in self.source_model.shortcuts:
            shortcut.save()

    def show_editor(self):
        """Create, setup and display the shortcut editor dialog."""
        index = self.proxy_model.mapToSource(self.currentIndex())
        row, column = index.row(), index.column()
        shortcuts = self.source_model.shortcuts
        context = shortcuts[row].context
        name = shortcuts[row].name

        sequence_index = self.source_model.index(row, SEQUENCE)
        sequence = sequence_index.data()

        dialog = ShortcutEditor(self, context, name, sequence, shortcuts)

        if dialog.exec_():
            new_sequence = dialog.new_sequence
            self.source_model.setData(sequence_index, new_sequence)

    def set_regex(self, regex=None, reset=False):
        """Update the regex text for the shortcut finder."""
        if reset:
            text = ''
        else:
            text = self.finder.text().replace(' ', '').lower()

        self.proxy_model.set_filter(text)
        self.source_model.update_search_letters(text)
        self.sortByColumn(SEARCH_SCORE, Qt.AscendingOrder)

        if self.last_regex != regex:
            self.selectRow(0)
        self.last_regex = regex

    def next_row(self):
        """Move to next row from currently selected row."""
        row = self.currentIndex().row()
        rows = self.proxy_model.rowCount()
        if row + 1 == rows:
            row = -1
        self.selectRow(row + 1)

    def previous_row(self):
        """Move to previous row from currently selected row."""
        row = self.currentIndex().row()
        rows = self.proxy_model.rowCount()
        if row == 0:
            row = rows
        self.selectRow(row - 1)

    def keyPressEvent(self, event):
        """Qt Override."""
        key = event.key()
        if key in [Qt.Key_Enter, Qt.Key_Return]:
            self.show_editor()
        elif key in [Qt.Key_Tab]:
            self.finder.setFocus()
        elif key in [Qt.Key_Backtab]:
            self.parent().reset_btn.setFocus()
        elif key in [Qt.Key_Up, Qt.Key_Down, Qt.Key_Left, Qt.Key_Right]:
            super(ShortcutsTable, self).keyPressEvent(event)
        elif key not in [Qt.Key_Escape, Qt.Key_Space]:
            text = event.text()
            if text:
                if re.search(VALID_FINDER_CHARS, text) is not None:
                    self.finder.setFocus()
                    self.finder.set_text(text)
        elif key in [Qt.Key_Escape]:
            self.finder.keyPressEvent(event)

    def mouseDoubleClickEvent(self, event):
        """Qt Override."""
        self.show_editor()


class ShortcutsConfigPage(GeneralConfigPage):
    CONF_SECTION = "shortcuts"

    NAME = _("Keyboard shortcuts")
    ICON = ima.icon('keyboard')

    def setup_page(self):
        # Widgets
        self.table = ShortcutsTable(self)
        self.finder = ShortcutFinder(self.table, self.table.set_regex)
        self.table.finder = self.finder
        self.label_finder = QLabel(_('Search: '))
        self.reset_btn = QPushButton(_("Reset to default values"))

        # Layout
        hlayout = QHBoxLayout()
        vlayout = QVBoxLayout()
        hlayout.addWidget(self.label_finder)
        hlayout.addWidget(self.finder)
        vlayout.addWidget(self.table)
        vlayout.addLayout(hlayout)
        vlayout.addWidget(self.reset_btn)
        self.setLayout(vlayout)

        self.setTabOrder(self.table, self.finder)
        self.setTabOrder(self.finder, self.reset_btn)

        # Signals and slots
        if PYQT5:
            # Qt5 'dataChanged' has 3 parameters
            self.table.proxy_model.dataChanged.connect(
                lambda i1, i2, roles, opt='': self.has_been_modified(opt))
        else:
            self.table.proxy_model.dataChanged.connect(
                lambda i1, i2, opt='': self.has_been_modified(opt))
        self.reset_btn.clicked.connect(self.reset_to_default)

    def check_settings(self):
        self.table.check_shortcuts()

    def reset_to_default(self):
        """Reset to default values of the shortcuts making a confirmation."""
        reset = QMessageBox.warning(self, _("Shortcuts reset"),
                                    _("Do you want to reset "
                                      "to default values?"),
                                    QMessageBox.Yes | QMessageBox.No)
        if reset == QMessageBox.No:
            return
        reset_shortcuts()
        self.main.apply_shortcuts()
        self.table.load_shortcuts()
        self.load_from_conf()
        self.set_modified(False)

    def apply_settings(self, options):
        self.table.save_shortcuts()
        self.main.apply_shortcuts()


def test():
    from spyder.utils.qthelpers import qapplication
    app = qapplication()
    table = ShortcutsTable()
    table.show()
    app.exec_()
    print([str(s) for s in table.source_model.shortcuts])  # spyder: test-skip
    table.check_shortcuts()

if __name__ == '__main__':
    test()
