{"action":"created","comment":{"url":"https://api.github.com/repos/spyder-ide/spyder/pulls/comments/34163037","id":34163037,"diff_hunk":"@@ -7,179 +7,505 @@\n \"\"\"Shortcut management\"\"\"\r\n \r\n from __future__ import print_function\r\n+import os\r\n+import re\r\n+import sys\r\n \r\n-from spyderlib.qt.QtGui import (QVBoxLayout, QComboBox, QItemDelegate,\r\n-                                QTableView, QMessageBox, QPushButton)\r\n-from spyderlib.qt.QtCore import Qt, QSize, QAbstractTableModel, QModelIndex\r\n+from spyderlib.qt.QtGui import (QVBoxLayout, QTableView, QMessageBox,\r\n+                                QPushButton, QKeySequence, QDialog,\r\n+                                QDialogButtonBox, QLabel, QGridLayout,\r\n+                                QLineEdit, QAbstractItemView,\r\n+                                QSortFilterProxyModel, QStyledItemDelegate,\r\n+                                QStyleOptionViewItemV4, QApplication,\r\n+                                QTextDocument, QStyle, QSpacerItem,\r\n+                                QAbstractTextDocumentLayout,\r\n+                                QRegExpValidator, QToolTip,\r\n+                                QHBoxLayout)\r\n+from spyderlib.qt.QtCore import (Qt, QAbstractTableModel, QModelIndex, QSize,\r\n+                                 QPoint, QRegExp)\r\n from spyderlib.qt.compat import to_qvariant, from_qvariant\r\n import spyderlib.utils.icon_manager as ima\r\n \r\n-import sys\r\n-\r\n # Local imports\r\n-from spyderlib.baseconfig import _\r\n+from spyderlib.baseconfig import _, debug_print\r\n from spyderlib.guiconfig import (get_shortcut, set_shortcut,\r\n                                  iter_shortcuts, reset_shortcuts)\r\n from spyderlib.plugins.configdialog import GeneralConfigPage\r\n-from spyderlib.py3compat import to_text_string, is_text_string\r\n+from spyderlib.utils.qthelpers import get_std_icon\r\n+from spyderlib.widgets.arraybuilder import HelperToolButton\r\n \r\n+MODIFIERS = {Qt.Key_Shift: Qt.SHIFT,\r\n+             Qt.Key_Control: Qt.CTRL,\r\n+             Qt.Key_Alt: Qt.ALT,\r\n+             Qt.Key_Meta: Qt.META}\r\n \r\n-KEYSTRINGS = [\"Escape\", \"Tab\", \"Backtab\", \"Backspace\", \"Return\", \"Enter\",\r\n-              \"Delete\", \"Pause\", \"Print\", \"Clear\", \"Home\", \"End\", \"Left\",\r\n+SINGLE_KEYS = [\"F{}\".format(_i) for _i in range(1, 36)] + [\"Delete\", \"Escape\"]\r\n+KEYSTRINGS = [\"Tab\", \"Backtab\", \"Backspace\", \"Return\", \"Enter\",\r\n+              \"Pause\", \"Print\", \"Clear\", \"Home\", \"End\", \"Left\",\r\n               \"Up\", \"Right\", \"Down\", \"PageUp\", \"PageDown\"] + \\\r\n-             [\"F%d\" % _i for _i in range(1, 36)] + \\\r\n-             [\"Space\", \"Exclam\", \"QuoteDbl\", \"NumberSign\", \"Dollar\", \"Percent\",\r\n-              \"Ampersand\", \"Apostrophe\", \"ParenLeft\", \"ParenRight\", \"Asterisk\",\r\n-              \"Plus\", \"Comma\", \"Minus\", \"Period\", \"Slash\"] + \\\r\n+             [\"Space\", \"Exclam\", \"QuoteDbl\", \"NumberSign\", \"Dollar\",\r\n+              \"Percent\", \"Ampersand\", \"Apostrophe\", \"ParenLeft\",\r\n+              \"ParenRight\", \"Asterisk\", \"Plus\", \"Comma\", \"Minus\",\r\n+              \"Period\", \"Slash\"] + \\\r\n              [str(_i) for _i in range(10)] + \\\r\n-             [\"Colon\", \"Semicolon\", \"Less\", \"Equal\", \"Greater\", \"Question\",\r\n-              \"At\"] + [chr(_i) for _i in range(65, 91)] + \\\r\n-             [\"BracketLeft\", \"Backslash\", \"BracketRight\", \"Underscore\"]\r\n-\r\n-\r\n-class Key(object):\r\n-    MODIFIERS = {Qt.NoModifier: \"\", Qt.ShiftModifier: \"Shift\",\r\n-                 Qt.ControlModifier: \"Ctrl\", Qt.AltModifier: \"Alt\",\r\n-                 Qt.MetaModifier: \"Meta\"}\r\n-    if sys.platform == 'darwin':\r\n-        MODIFIERNAMES = {Qt.NoModifier: \"\", Qt.ShiftModifier: \"Shift\",\r\n-                         Qt.ControlModifier: \"Cmd\", Qt.AltModifier: \"Alt\",\r\n-                         Qt.MetaModifier: \"Ctrl\"}\r\n-    elif sys.platform == 'win32':\r\n-        MODIFIERNAMES = {Qt.NoModifier: \"\", Qt.ShiftModifier: \"Shift\",\r\n-                         Qt.ControlModifier: \"Ctrl\", Qt.AltModifier: \"Alt\",\r\n-                         Qt.MetaModifier: \"Win\"}\r\n-    else:\r\n-        MODIFIERNAMES = {Qt.NoModifier: \"\", Qt.ShiftModifier: \"Shift\",\r\n-                         Qt.ControlModifier: \"Ctrl\", Qt.AltModifier: \"Alt\",\r\n-                         Qt.MetaModifier: \"Meta\"}\r\n-    KEYS = {}\r\n-    for attr in KEYSTRINGS:\r\n-        # ISSUE\r\n-        # This is needed to cope with a strange behaviour of Qt.\r\n-        # Qt.Key_PageUp/Qt.Key_PageDown when string parsed use PgDown/PgUp\r\n-        attr2 = attr\r\n-        if attr == 'PageDown':\r\n-            attr2 = 'PgDown'\r\n-        elif attr == 'PageUp':\r\n-            attr2 = 'PgUp'\r\n-\r\n-        KEYS[getattr(Qt, \"Key_\"+attr)] = attr2\r\n-\r\n-    def __init__(self, key, mod1=Qt.NoModifier, mod2=Qt.NoModifier,\r\n-                 mod3=Qt.NoModifier):\r\n-        modifiers = [mod1, mod2, mod3]\r\n-        assert all([mod in self.MODIFIERS for mod in modifiers])\r\n-        self.modifiers = sorted(modifiers)\r\n-        assert key in self.KEYS\r\n-        self.key = key\r\n+             [\"Colon\", \"Semicolon\", \"Less\", \"Equal\", \"Greater\",\r\n+              \"Question\", \"At\"] + [chr(_i) for _i in range(65, 91)] + \\\r\n+             [\"BracketLeft\", \"Backslash\", \"BracketRight\", \"Underscore\",\r\n+              \"Control\", \"Alt\", \"Shift\", \"Meta\"]\r\n+VALID_SINGLE_KEYS = [getattr(Qt, 'Key_{0}'.format(k)) for k in SINGLE_KEYS]\r\n+VALID_KEYS = [getattr(Qt, 'Key_{0}'.format(k)) for k in KEYSTRINGS+SINGLE_KEYS]\r\n+VALID_ACCENT_CHARS = \"ÁÉÍOÚáéíúóàèìòùÀÈÌÒÙâêîôûÂÊÎÔÛäëïöüÄËÏÖÜñÑ\"\r\n+VALID_FINDER_CHARS = \"[A-Za-z\\s{0}]\".format(VALID_ACCENT_CHARS)\r\n+\r\n+\r\n+class CustomLineEdit(QLineEdit):\r\n+    \"\"\"QLineEdit that filters its key press and release events.\"\"\"\r\n+    def __init__(self, parent):\r\n+        super(CustomLineEdit, self).__init__(parent)\r\n+        self.setReadOnly(True)\r\n+        self.setFocusPolicy(Qt.NoFocus)\r\n+\r\n+    def keyPressEvent(self, e):\r\n+        \"\"\"Qt Override\"\"\"\r\n+        self.parent().keyPressEvent(e)\r\n+\r\n+    def keyReleaseEvent(self, e):\r\n+        \"\"\"Qt Override\"\"\"\r\n+        self.parent().keyReleaseEvent(e)\r\n+\r\n+\r\n+# TODO: Move to another place for common use widgets afterwards.\r\n+class HTMLDelegate(QStyledItemDelegate):\r\n+    \"\"\"With this delegate, a QListWidgetItem can render HTML.\r\n+\r\n+    Taken from http://stackoverflow.com/a/5443112/2399799\r\n+    \"\"\"\r\n+    def __init__(self, parent, margin=0):\r\n+        super(HTMLDelegate, self).__init__(parent)\r\n+        self._margin = margin\r\n+\r\n+    def paint(self, painter, option, index):\r\n+        options = QStyleOptionViewItemV4(option)\r\n+        self.initStyleOption(options, index)\r\n+\r\n+        style = (QApplication.style() if options.widget is None\r\n+                 else options.widget.style())\r\n+\r\n+        doc = QTextDocument()\r\n+        doc.setDocumentMargin(self._margin)\r\n+        doc.setHtml(options.text)\r\n+\r\n+        options.text = \"\"\r\n+        style.drawControl(QStyle.CE_ItemViewItem, options, painter)\r\n+\r\n+        ctx = QAbstractTextDocumentLayout.PaintContext()\r\n+\r\n+        textRect = style.subElementRect(QStyle.SE_ItemViewItemText, options)\r\n+        painter.save()\r\n+        painter.translate(textRect.topLeft())\r\n+        painter.setClipRect(textRect.translated(-textRect.topLeft()))\r\n+        doc.documentLayout().draw(painter, ctx)\r\n+\r\n+        painter.restore()\r\n+\r\n+    def sizeHint(self, option, index):\r\n+        options = QStyleOptionViewItemV4(option)\r\n+        self.initStyleOption(options, index)\r\n+\r\n+        doc = QTextDocument()\r\n+        doc.setHtml(options.text)\r\n+        doc.setTextWidth(options.rect.width())\r\n+\r\n+        return QSize(doc.idealWidth(), doc.size().height())\r\n+\r\n+\r\n+class ShortcutFinder(QLineEdit):\r\n+    \"\"\"Textbox for filtering listed shortcuts in the table.\"\"\"\r\n+    def __init__(self, parent, callback=None):\r\n+        super(ShortcutFinder, self).__init__(parent)\r\n+\r\n+        self._parent = parent\r\n+        regex = QRegExp(VALID_FINDER_CHARS + \"{100}\")\r\n+        self.setValidator(QRegExpValidator(regex))\r\n+\r\n+        # Signals\r\n+        if callback:\r\n+            self.textChanged.connect(callback)\r\n+\r\n+    def set_text(self, text):\r\n+        \"\"\"Set the filter text\"\"\"\r\n+        text = text.strip()\r\n+        new_text = self.text() + text\r\n+        self.setText(new_text)\r\n+\r\n+    def keyPressEvent(self, event):\r\n+        \"\"\"Qt Override\"\"\"\r\n+        key = event.key()\r\n+        if key in [Qt.Key_Up]:\r\n+            self._parent.previous_row()\r\n+        elif key in [Qt.Key_Down]:\r\n+            self._parent.next_row()\r\n+        elif key in [Qt.Key_Enter, Qt.Key_Return]:\r\n+            self._parent.show_editor()\r\n+        else:\r\n+            super(ShortcutFinder, self).keyPressEvent(event)\r\n+\r\n+\r\n+class ShortcutEditor(QDialog):\r\n+    \"\"\"A dialog for entering key sequences.\"\"\"\r\n+    def __init__(self, parent, context, name, sequence, shortcuts):\r\n+        super(ShortcutEditor, self).__init__(parent)\r\n+        self.context = context\r\n+        self.npressed = 0\r\n+        self.keys = set()\r\n+        self.key_modifiers = set()\r\n+        self.key_non_modifiers = list()\r\n+        self.key_text = list()\r\n+        self.sequence = sequence\r\n+        self.new_sequence = None\r\n+        self.edit_state = True\r\n+        self.shortcuts = shortcuts\r\n+\r\n+        # Widgets\r\n+        self.label_info = QLabel(_(\"Press the new shortcut and select 'Ok': \\n\"\r\n+                                   \"(Press 'Tab' once to switch focus between \"\r\n+                                   \"shortcut entry and buttons)\"))\r\n+        self.label_current_sequence = QLabel(_(\"Current shortcut:\"))\r\n+        self.text_current_sequence = QLabel(sequence)\r\n+        self.label_new_sequence = QLabel(_(\"New shortcut:\"))\r\n+        self.text_new_sequence = CustomLineEdit(self)\r\n+        self.text_new_sequence.setPlaceholderText(sequence)\r\n+        self.helper_button = HelperToolButton()\r\n+\r\n+        bbox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)\r\n+        self.button_ok = bbox.button(QDialogButtonBox.Ok)\r\n+        self.button_cancel = bbox.button(QDialogButtonBox.Cancel)\r\n+\r\n+        # Setup widgets\r\n+        self.setWindowTitle(_('Shortcut: {0}').format(name))\r\n+        self.button_ok.setFocusPolicy(Qt.NoFocus)\r\n+        self.button_ok.setEnabled(False)\r\n+        self.button_cancel.setFocusPolicy(Qt.NoFocus)\r\n+        self.helper_button.setToolTip('')\r\n+        self.helper_button.setFocusPolicy(Qt.NoFocus)\r\n+        style = \"\"\"\r\n+            QToolButton {\r\n+              margin:1px;\r\n+              border: 0px solid grey;\r\n+              padding:0px;\r\n+              border-radius: 0px;\r\n+            }\"\"\"\r\n+        self.helper_button.setStyleSheet(style)\r\n+\r\n+        # Layout\r\n+        spacing = 24\r\n+        layout_sequence = QGridLayout()\r\n+        layout_sequence.addWidget(self.label_info, 0, 0, 1, 3)\r\n+        layout_sequence.addItem(QSpacerItem(spacing, spacing), 1, 0, 1, 2)\r\n+        layout_sequence.addWidget(self.label_current_sequence, 2, 0)\r\n+        layout_sequence.addWidget(self.text_current_sequence, 2, 2)\r\n+        layout_sequence.addWidget(self.label_new_sequence, 3, 0)\r\n+        layout_sequence.addWidget(self.helper_button, 3, 1)\r\n+        layout_sequence.addWidget(self.text_new_sequence, 3, 2)\r\n+\r\n+        layout = QVBoxLayout()\r\n+        layout.addLayout(layout_sequence)\r\n+        layout.addSpacing(spacing)\r\n+        layout.addWidget(bbox)\r\n+        self.setLayout(layout)\r\n+\r\n+        # Signals\r\n+        bbox.accepted.connect(self.accept)\r\n+        bbox.rejected.connect(self.reject)\r\n+\r\n+    def keyPressEvent(self, e):\r\n+        \"\"\"Qt override\"\"\"\r\n+        key = e.key()\r\n+\r\n+        self.npressed += 1\r\n+        self.key_non_modifiers.append(key)\r\n+        self.key_modifiers.add(key)\r\n+        self.key_text.append(e.text())\r\n+\r\n+        debug_print('key %s, npressed: %s' % (key, self.npressed))\r\n+\r\n+        if key == Qt.Key_unknown:\r\n+            return\r\n+\r\n+        # The user clicked just and only the special keys\r\n+        # Ctrl, Shift, Alt, Meta.\r\n+        if (key == Qt.Key_Control or\r\n+                key == Qt.Key_Shift or\r\n+                key == Qt.Key_Alt or\r\n+                key == Qt.Key_Meta):\r\n+            return\r\n+\r\n+        # Check if valid keys\r\n+        if key not in VALID_KEYS:\r\n+            return\r\n+\r\n+        modifiers = e.modifiers()\r\n+        if modifiers & Qt.ShiftModifier:\r\n+            key += Qt.SHIFT\r\n+        if modifiers & Qt.ControlModifier:\r\n+            key += Qt.CTRL\r\n+            if sys.platform == 'darwin':\r\n+                self.npressed -= 1\r\n+            debug_print('decrementing')\r\n+        if modifiers & Qt.AltModifier:\r\n+            key += Qt.ALT\r\n+        if modifiers & Qt.MetaModifier:\r\n+            key += Qt.META\r\n+\r\n+        self.keys.add(key)\r\n+\r\n+    def toggle_state(self):\r\n+        \"\"\"Switch between shortcut entry and Accept/Cancel shortcut mode.\"\"\"\r\n+        self.edit_state = not self.edit_state\r\n+\r\n+        if not self.edit_state:\r\n+            self.text_new_sequence.setEnabled(False)\r\n+            if self.button_ok.isEnabled():\r\n+                self.button_ok.setFocus()\r\n+            else:\r\n+                self.button_cancel.setFocus()\r\n+        else:\r\n+            self.text_new_sequence.setEnabled(True)\r\n+            self.text_new_sequence.setFocus()\r\n+\r\n+    def nonedit_keyrelease(self, e):\r\n+        \"\"\"Key release event for non-edit state.\"\"\"\r\n+        key = e.key()\r\n+\r\n+        if key == Qt.Key_Escape:\r\n+#            self.close()\r\n+            pass\r\n+\r\n+        if key in [Qt.Key_Left, Qt.Key_Right, Qt.Key_Up,\r\n+                   Qt.Key_Down]:\r\n+\r\n+            if self.button_ok.hasFocus():\r\n+                self.button_cancel.setFocus()\r\n+            else:\r\n+                self.button_ok.setFocus()\r\n+\r\n+    def keyReleaseEvent(self, e):\r\n+        \"\"\"Qt override\"\"\"\r\n+        self.npressed -= 1\r\n+        if self.npressed <= 0:\r\n+            if len(self.keys) == 1 and list(self.keys)[0] == Qt.Key_Tab:\r\n+                self.toggle_state()\r\n+\r\n+            if not self.edit_state:\r\n+                self.nonedit_keyrelease(e)\r\n+            else:\r\n+                debug_print('keys: {}'.format(self.keys))\r\n+                if self.keys:\r\n+                    self.validate_sequence()\r\n+                self.keys = set()\r\n+                self.key_modifiers = set()\r\n+                self.key_non_modifiers = list()\r\n+                self.key_text = list()\r\n+                self.npressed = 0\r\n+\r\n+    def check_conflicts(self):\r\n+        \"\"\"Check shortcuts for conflicts\"\"\"\r\n+        conflicts = []\r\n+        for index, shortcut in enumerate(self.shortcuts):\r\n+            sequence = str(shortcut.key)\r\n+            if sequence == self.new_sequence and \\\r\n+                (shortcut.context == self.context or shortcut.context == '_' or\r\n+                 self.context == '_'):\r\n+                conflicts.append(shortcut)\r\n+        return conflicts\r\n+\r\n+    def update_warning(self, reset=False):\r\n+        \"\"\" \"\"\"\r\n+        conflicts = self.check_conflicts()\r\n+        if reset:\r\n+            conflicts = []\r\n+\r\n+        widget = self.helper_button\r\n+\r\n+        if conflicts:\r\n+            tip_title = _('The new entered shorcut conflicts with:') + '\\n'\r\n+            tip_body = ''\r\n+            for s in conflicts:\r\n+                tip_body += ' - {0}: {1}\\n'.format(s.context, s.name)\r\n+            tip = '{0}{1}'.format(tip_title, tip_body)\r\n+            widget.setIcon(get_std_icon('MessageBoxWarning'))\r\n+            widget.setToolTip(tip)\r\n+            QToolTip.showText(widget.mapToGlobal(QPoint(0, 5)), tip)\r\n+        else:\r\n+            widget.setToolTip('')\r\n+            QToolTip.hideText()\r\n+            widget.setIcon(get_std_icon('DialogApplyButton'))\r\n+\r\n+    def set_sequence(self, sequence):\r\n+        \"\"\"Set the new shortcut and update buttons.\"\"\"\r\n+        if self.sequence != sequence:\r\n+            self.button_ok.setEnabled(True)\r\n+            reset = False\r\n+        else:\r\n+            self.button_ok.setEnabled(False)\r\n+            reset = True\r\n+\r\n+        self.text_new_sequence.setText(sequence)\r\n+        self.new_sequence = sequence\r\n+        self.update_warning(reset=reset)\r\n+\r\n+    def validate_sequence(self):\r\n+        \"\"\"Provide additional checks for accepting or rejecting shortcuts.\"\"\"\r\n+        for mod in MODIFIERS:\r\n+            non_mod = set(self.key_non_modifiers)\r\n+            non_mod.discard(mod)\r\n+            if mod in self.key_non_modifiers:\r\n+                self.key_non_modifiers.remove(mod)\r\n+\r\n+        self.key_modifiers = self.key_modifiers - non_mod\r\n+\r\n+        while u'' in self.key_text:\r\n+            self.key_text.remove(u'')\r\n+\r\n+        self.key_text = [k.upper() for k in self.key_text]\r\n+\r\n+        # Fix Backtab, Tab issue\r\n+        if os.name == 'nt':\r\n+            if Qt.Key_Backtab in self.key_non_modifiers:\r\n+                idx = self.key_non_modifiers.index(Qt.Key_Backtab)\r\n+                self.key_non_modifiers[idx] = Qt.Key_Tab\r\n+\r\n+        if len(self.key_modifiers) == 0:\r\n+            # Filter single key allowed\r\n+            if self.key_non_modifiers[0] not in VALID_SINGLE_KEYS:\r\n+                return\r\n+            # Filter\r\n+            elif len(self.key_non_modifiers) > 1:\r\n+                return\r\n+\r\n+        # QKeySequence accepts a maximum of 4 different sequences\r\n+        if len(self.keys) > 4:\r\n+            # If the user enters more than 4, take the first 4 only\r\n+            self.keys = set([0, 1, 2, 3])\r","path":"spyderlib/plugins/shortcuts.py","position":437,"original_position":437,"commit_id":"982277b0c7067078a7826a4a0cca57ec077f5c61","original_commit_id":"982277b0c7067078a7826a4a0cca57ec077f5c61","user":{"login":"goanpeca","id":3627835,"avatar_url":"https://avatars.githubusercontent.com/u/3627835?v=3","gravatar_id":"","url":"https://api.github.com/users/goanpeca","html_url":"https://github.com/goanpeca","followers_url":"https://api.github.com/users/goanpeca/followers","following_url":"https://api.github.com/users/goanpeca/following{/other_user}","gists_url":"https://api.github.com/users/goanpeca/gists{/gist_id}","starred_url":"https://api.github.com/users/goanpeca/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/goanpeca/subscriptions","organizations_url":"https://api.github.com/users/goanpeca/orgs","repos_url":"https://api.github.com/users/goanpeca/repos","events_url":"https://api.github.com/users/goanpeca/events{/privacy}","received_events_url":"https://api.github.com/users/goanpeca/received_events","type":"User","site_admin":false},"body":"So if an user enter a sequence of 4 and bigger nothing should happen ??? ","created_at":"2015-07-08T15:43:52Z","updated_at":"2015-07-08T15:43:52Z","html_url":"https://github.com/spyder-ide/spyder/pull/2367#discussion_r34163037","pull_request_url":"https://api.github.com/repos/spyder-ide/spyder/pulls/2367","_links":{"self":{"href":"https://api.github.com/repos/spyder-ide/spyder/pulls/comments/34163037"},"html":{"href":"https://github.com/spyder-ide/spyder/pull/2367#discussion_r34163037"},"pull_request":{"href":"https://api.github.com/repos/spyder-ide/spyder/pulls/2367"}}},"pull_request":{"url":"https://api.github.com/repos/spyder-ide/spyder/pulls/2367","id":34124225,"html_url":"https://github.com/spyder-ide/spyder/pull/2367","diff_url":"https://github.com/spyder-ide/spyder/pull/2367.diff","patch_url":"https://github.com/spyder-ide/spyder/pull/2367.patch","issue_url":"https://api.github.com/repos/spyder-ide/spyder/issues/2367","number":2367,"state":"open","locked":false,"title":"KeySequence Editor for Keyboard Shortcut Preferences","user":{"login":"blink1073","id":2096628,"avatar_url":"https://avatars.githubusercontent.com/u/2096628?v=3","gravatar_id":"","url":"https://api.github.com/users/blink1073","html_url":"https://github.com/blink1073","followers_url":"https://api.github.com/users/blink1073/followers","following_url":"https://api.github.com/users/blink1073/following{/other_user}","gists_url":"https://api.github.com/users/blink1073/gists{/gist_id}","starred_url":"https://api.github.com/users/blink1073/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/blink1073/subscriptions","organizations_url":"https://api.github.com/users/blink1073/orgs","repos_url":"https://api.github.com/users/blink1073/repos","events_url":"https://api.github.com/users/blink1073/events{/privacy}","received_events_url":"https://api.github.com/users/blink1073/received_events","type":"User","site_admin":false},"body":"Fixes #478\r\nFixes #494\r\nFixes #1940\r\nFixes #2125\r\n\r\n----\r\n\r\nAllows the user to enter keyboard shortcut preferences by typing them in.","created_at":"2015-04-26T14:52:44Z","updated_at":"2015-07-08T15:43:52Z","closed_at":null,"merged_at":null,"merge_commit_sha":"0780d1feb2920fddb608a2de06ca87cd09f755fd","assignee":{"login":"blink1073","id":2096628,"avatar_url":"https://avatars.githubusercontent.com/u/2096628?v=3","gravatar_id":"","url":"https://api.github.com/users/blink1073","html_url":"https://github.com/blink1073","followers_url":"https://api.github.com/users/blink1073/followers","following_url":"https://api.github.com/users/blink1073/following{/other_user}","gists_url":"https://api.github.com/users/blink1073/gists{/gist_id}","starred_url":"https://api.github.com/users/blink1073/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/blink1073/subscriptions","organizations_url":"https://api.github.com/users/blink1073/orgs","repos_url":"https://api.github.com/users/blink1073/repos","events_url":"https://api.github.com/users/blink1073/events{/privacy}","received_events_url":"https://api.github.com/users/blink1073/received_events","type":"User","site_admin":false},"milestone":{"url":"https://api.github.com/repos/spyder-ide/spyder/milestones/5","html_url":"https://github.com/spyder-ide/spyder/milestones/v3.0","labels_url":"https://api.github.com/repos/spyder-ide/spyder/milestones/5/labels","id":982077,"number":5,"title":"v3.0","description":"New major release","creator":{"login":"spyder-issues-migrator","id":11021581,"avatar_url":"https://avatars.githubusercontent.com/u/11021581?v=3","gravatar_id":"","url":"https://api.github.com/users/spyder-issues-migrator","html_url":"https://github.com/spyder-issues-migrator","followers_url":"https://api.github.com/users/spyder-issues-migrator/followers","following_url":"https://api.github.com/users/spyder-issues-migrator/following{/other_user}","gists_url":"https://api.github.com/users/spyder-issues-migrator/gists{/gist_id}","starred_url":"https://api.github.com/users/spyder-issues-migrator/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/spyder-issues-migrator/subscriptions","organizations_url":"https://api.github.com/users/spyder-issues-migrator/orgs","repos_url":"https://api.github.com/users/spyder-issues-migrator/repos","events_url":"https://api.github.com/users/spyder-issues-migrator/events{/privacy}","received_events_url":"https://api.github.com/users/spyder-issues-migrator/received_events","type":"User","site_admin":false},"open_issues":268,"closed_issues":102,"state":"open","created_at":"2015-02-16T23:17:23Z","updated_at":"2015-07-06T16:53:02Z","due_on":null,"closed_at":null},"commits_url":"https://api.github.com/repos/spyder-ide/spyder/pulls/2367/commits","review_comments_url":"https://api.github.com/repos/spyder-ide/spyder/pulls/2367/comments","review_comment_url":"https://api.github.com/repos/spyder-ide/spyder/pulls/comments{/number}","comments_url":"https://api.github.com/repos/spyder-ide/spyder/issues/2367/comments","statuses_url":"https://api.github.com/repos/spyder-ide/spyder/statuses/982277b0c7067078a7826a4a0cca57ec077f5c61","head":{"label":"blink1073:shortcut-editor","ref":"shortcut-editor","sha":"982277b0c7067078a7826a4a0cca57ec077f5c61","user":{"login":"blink1073","id":2096628,"avatar_url":"https://avatars.githubusercontent.com/u/2096628?v=3","gravatar_id":"","url":"https://api.github.com/users/blink1073","html_url":"https://github.com/blink1073","followers_url":"https://api.github.com/users/blink1073/followers","following_url":"https://api.github.com/users/blink1073/following{/other_user}","gists_url":"https://api.github.com/users/blink1073/gists{/gist_id}","starred_url":"https://api.github.com/users/blink1073/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/blink1073/subscriptions","organizations_url":"https://api.github.com/users/blink1073/orgs","repos_url":"https://api.github.com/users/blink1073/repos","events_url":"https://api.github.com/users/blink1073/events{/privacy}","received_events_url":"https://api.github.com/users/blink1073/received_events","type":"User","site_admin":false},"repo":{"id":31164085,"name":"spyder","full_name":"blink1073/spyder","owner":{"login":"blink1073","id":2096628,"avatar_url":"https://avatars.githubusercontent.com/u/2096628?v=3","gravatar_id":"","url":"https://api.github.com/users/blink1073","html_url":"https://github.com/blink1073","followers_url":"https://api.github.com/users/blink1073/followers","following_url":"https://api.github.com/users/blink1073/following{/other_user}","gists_url":"https://api.github.com/users/blink1073/gists{/gist_id}","starred_url":"https://api.github.com/users/blink1073/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/blink1073/subscriptions","organizations_url":"https://api.github.com/users/blink1073/orgs","repos_url":"https://api.github.com/users/blink1073/repos","events_url":"https://api.github.com/users/blink1073/events{/privacy}","received_events_url":"https://api.github.com/users/blink1073/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/blink1073/spyder","description":"Official repository for Spyder - The Scientific PYthon Development EnviRonment","fork":true,"url":"https://api.github.com/repos/blink1073/spyder","forks_url":"https://api.github.com/repos/blink1073/spyder/forks","keys_url":"https://api.github.com/repos/blink1073/spyder/keys{/key_id}","collaborators_url":"https://api.github.com/repos/blink1073/spyder/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/blink1073/spyder/teams","hooks_url":"https://api.github.com/repos/blink1073/spyder/hooks","issue_events_url":"https://api.github.com/repos/blink1073/spyder/issues/events{/number}","events_url":"https://api.github.com/repos/blink1073/spyder/events","assignees_url":"https://api.github.com/repos/blink1073/spyder/assignees{/user}","branches_url":"https://api.github.com/repos/blink1073/spyder/branches{/branch}","tags_url":"https://api.github.com/repos/blink1073/spyder/tags","blobs_url":"https://api.github.com/repos/blink1073/spyder/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/blink1073/spyder/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/blink1073/spyder/git/refs{/sha}","trees_url":"https://api.github.com/repos/blink1073/spyder/git/trees{/sha}","statuses_url":"https://api.github.com/repos/blink1073/spyder/statuses/{sha}","languages_url":"https://api.github.com/repos/blink1073/spyder/languages","stargazers_url":"https://api.github.com/repos/blink1073/spyder/stargazers","contributors_url":"https://api.github.com/repos/blink1073/spyder/contributors","subscribers_url":"https://api.github.com/repos/blink1073/spyder/subscribers","subscription_url":"https://api.github.com/repos/blink1073/spyder/subscription","commits_url":"https://api.github.com/repos/blink1073/spyder/commits{/sha}","git_commits_url":"https://api.github.com/repos/blink1073/spyder/git/commits{/sha}","comments_url":"https://api.github.com/repos/blink1073/spyder/comments{/number}","issue_comment_url":"https://api.github.com/repos/blink1073/spyder/issues/comments{/number}","contents_url":"https://api.github.com/repos/blink1073/spyder/contents/{+path}","compare_url":"https://api.github.com/repos/blink1073/spyder/compare/{base}...{head}","merges_url":"https://api.github.com/repos/blink1073/spyder/merges","archive_url":"https://api.github.com/repos/blink1073/spyder/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/blink1073/spyder/downloads","issues_url":"https://api.github.com/repos/blink1073/spyder/issues{/number}","pulls_url":"https://api.github.com/repos/blink1073/spyder/pulls{/number}","milestones_url":"https://api.github.com/repos/blink1073/spyder/milestones{/number}","notifications_url":"https://api.github.com/repos/blink1073/spyder/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/blink1073/spyder/labels{/name}","releases_url":"https://api.github.com/repos/blink1073/spyder/releases{/id}","created_at":"2015-02-22T13:39:06Z","updated_at":"2015-04-24T03:05:42Z","pushed_at":"2015-07-08T13:31:43Z","git_url":"git://github.com/blink1073/spyder.git","ssh_url":"git@github.com:blink1073/spyder.git","clone_url":"https://github.com/blink1073/spyder.git","svn_url":"https://github.com/blink1073/spyder","homepage":"","size":22393,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":0,"forks":0,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"spyder-ide:master","ref":"master","sha":"30e1b5bdc0adae233fa1aa3b543f6f66783a9d87","user":{"login":"spyder-ide","id":1284937,"avatar_url":"https://avatars.githubusercontent.com/u/1284937?v=3","gravatar_id":"","url":"https://api.github.com/users/spyder-ide","html_url":"https://github.com/spyder-ide","followers_url":"https://api.github.com/users/spyder-ide/followers","following_url":"https://api.github.com/users/spyder-ide/following{/other_user}","gists_url":"https://api.github.com/users/spyder-ide/gists{/gist_id}","starred_url":"https://api.github.com/users/spyder-ide/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/spyder-ide/subscriptions","organizations_url":"https://api.github.com/users/spyder-ide/orgs","repos_url":"https://api.github.com/users/spyder-ide/repos","events_url":"https://api.github.com/users/spyder-ide/events{/privacy}","received_events_url":"https://api.github.com/users/spyder-ide/received_events","type":"Organization","site_admin":false},"repo":{"id":30892477,"name":"spyder","full_name":"spyder-ide/spyder","owner":{"login":"spyder-ide","id":1284937,"avatar_url":"https://avatars.githubusercontent.com/u/1284937?v=3","gravatar_id":"","url":"https://api.github.com/users/spyder-ide","html_url":"https://github.com/spyder-ide","followers_url":"https://api.github.com/users/spyder-ide/followers","following_url":"https://api.github.com/users/spyder-ide/following{/other_user}","gists_url":"https://api.github.com/users/spyder-ide/gists{/gist_id}","starred_url":"https://api.github.com/users/spyder-ide/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/spyder-ide/subscriptions","organizations_url":"https://api.github.com/users/spyder-ide/orgs","repos_url":"https://api.github.com/users/spyder-ide/repos","events_url":"https://api.github.com/users/spyder-ide/events{/privacy}","received_events_url":"https://api.github.com/users/spyder-ide/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/spyder-ide/spyder","description":"Official repository for Spyder - The Scientific PYthon Development EnviRonment","fork":false,"url":"https://api.github.com/repos/spyder-ide/spyder","forks_url":"https://api.github.com/repos/spyder-ide/spyder/forks","keys_url":"https://api.github.com/repos/spyder-ide/spyder/keys{/key_id}","collaborators_url":"https://api.github.com/repos/spyder-ide/spyder/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/spyder-ide/spyder/teams","hooks_url":"https://api.github.com/repos/spyder-ide/spyder/hooks","issue_events_url":"https://api.github.com/repos/spyder-ide/spyder/issues/events{/number}","events_url":"https://api.github.com/repos/spyder-ide/spyder/events","assignees_url":"https://api.github.com/repos/spyder-ide/spyder/assignees{/user}","branches_url":"https://api.github.com/repos/spyder-ide/spyder/branches{/branch}","tags_url":"https://api.github.com/repos/spyder-ide/spyder/tags","blobs_url":"https://api.github.com/repos/spyder-ide/spyder/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/spyder-ide/spyder/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/spyder-ide/spyder/git/refs{/sha}","trees_url":"https://api.github.com/repos/spyder-ide/spyder/git/trees{/sha}","statuses_url":"https://api.github.com/repos/spyder-ide/spyder/statuses/{sha}","languages_url":"https://api.github.com/repos/spyder-ide/spyder/languages","stargazers_url":"https://api.github.com/repos/spyder-ide/spyder/stargazers","contributors_url":"https://api.github.com/repos/spyder-ide/spyder/contributors","subscribers_url":"https://api.github.com/repos/spyder-ide/spyder/subscribers","subscription_url":"https://api.github.com/repos/spyder-ide/spyder/subscription","commits_url":"https://api.github.com/repos/spyder-ide/spyder/commits{/sha}","git_commits_url":"https://api.github.com/repos/spyder-ide/spyder/git/commits{/sha}","comments_url":"https://api.github.com/repos/spyder-ide/spyder/comments{/number}","issue_comment_url":"https://api.github.com/repos/spyder-ide/spyder/issues/comments{/number}","contents_url":"https://api.github.com/repos/spyder-ide/spyder/contents/{+path}","compare_url":"https://api.github.com/repos/spyder-ide/spyder/compare/{base}...{head}","merges_url":"https://api.github.com/repos/spyder-ide/spyder/merges","archive_url":"https://api.github.com/repos/spyder-ide/spyder/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/spyder-ide/spyder/downloads","issues_url":"https://api.github.com/repos/spyder-ide/spyder/issues{/number}","pulls_url":"https://api.github.com/repos/spyder-ide/spyder/pulls{/number}","milestones_url":"https://api.github.com/repos/spyder-ide/spyder/milestones{/number}","notifications_url":"https://api.github.com/repos/spyder-ide/spyder/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/spyder-ide/spyder/labels{/name}","releases_url":"https://api.github.com/repos/spyder-ide/spyder/releases{/id}","created_at":"2015-02-16T22:49:48Z","updated_at":"2015-07-08T07:16:08Z","pushed_at":"2015-07-08T15:43:18Z","git_url":"git://github.com/spyder-ide/spyder.git","ssh_url":"git@github.com:spyder-ide/spyder.git","clone_url":"https://github.com/spyder-ide/spyder.git","svn_url":"https://github.com/spyder-ide/spyder","homepage":"","size":39996,"stargazers_count":380,"watchers_count":380,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":107,"mirror_url":null,"open_issues_count":526,"forks":107,"open_issues":526,"watchers":380,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/spyder-ide/spyder/pulls/2367"},"html":{"href":"https://github.com/spyder-ide/spyder/pull/2367"},"issue":{"href":"https://api.github.com/repos/spyder-ide/spyder/issues/2367"},"comments":{"href":"https://api.github.com/repos/spyder-ide/spyder/issues/2367/comments"},"review_comments":{"href":"https://api.github.com/repos/spyder-ide/spyder/pulls/2367/comments"},"review_comment":{"href":"https://api.github.com/repos/spyder-ide/spyder/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/spyder-ide/spyder/pulls/2367/commits"},"statuses":{"href":"https://api.github.com/repos/spyder-ide/spyder/statuses/982277b0c7067078a7826a4a0cca57ec077f5c61"}}}}