Repository: spyder-ide/spyder

Issue:
Shortcuts table loads whatever is saved on the shortcuts section
We currently load shortcuts from the configuration. This means anything there will be loaded, even if it is a leftover from a shortcut name rename or similar.

![image](https://user-images.githubusercontent.com/3627835/78612566-4682e180-782f-11ea-8f7a-2062393c7b5d.png)



Discussion:

Resolving patch:
diff --git a/spyder/app/mainwindow.py b/spyder/app/mainwindow.py
--- a/spyder/app/mainwindow.py
+++ b/spyder/app/mainwindow.py
@@ -3212,26 +3212,40 @@ def register_shortcut(self, qaction_or_qshortcut, context, name,
                                    name, add_shortcut_to_tip, plugin_name))
 
     def apply_shortcuts(self):
-        """Apply shortcuts settings to all widgets/plugins"""
+        """Apply shortcuts settings to all widgets/plugins."""
         toberemoved = []
         for index, (qobject, context, name, add_shortcut_to_tip,
                     plugin_name) in enumerate(self.shortcut_data):
-            keyseq = QKeySequence(CONF.get_shortcut(context, name,
-                                                    plugin_name))
             try:
-                if isinstance(qobject, QAction):
-                    if sys.platform == 'darwin' and \
-                      qobject._shown_shortcut == 'missing':
-                        qobject._shown_shortcut = keyseq
-                    else:
-                        qobject.setShortcut(keyseq)
-                    if add_shortcut_to_tip:
-                        add_shortcut_to_tooltip(qobject, context, name)
-                elif isinstance(qobject, QShortcut):
-                    qobject.setKey(keyseq)
-            except RuntimeError:
-                # Object has been deleted
-                toberemoved.append(index)
+                shortcut_sequence = CONF.get_shortcut(context, name,
+                                                      plugin_name)
+            except (cp.NoSectionError, cp.NoOptionError):
+                # If shortcut does not exist, save it to CONF. This is an
+                # action for which there is no shortcut assigned (yet) in
+                # the configuration
+                CONF.set_shortcut(context, name, '', plugin_name)
+                shortcut_sequence = ''
+
+            if shortcut_sequence:
+                keyseq = QKeySequence(shortcut_sequence)
+                try:
+                    if isinstance(qobject, QAction):
+                        if (sys.platform == 'darwin'
+                                and qobject._shown_shortcut == 'missing'):
+                            qobject._shown_shortcut = keyseq
+                        else:
+                            qobject.setShortcut(keyseq)
+
+                        if add_shortcut_to_tip:
+                            add_shortcut_to_tooltip(qobject, context, name)
+
+                    elif isinstance(qobject, QShortcut):
+                        qobject.setKey(keyseq)
+
+                except RuntimeError:
+                    # Object has been deleted
+                    toberemoved.append(index)
+
         for index in sorted(toberemoved, reverse=True):
             self.shortcut_data.pop(index)
 
diff --git a/spyder/preferences/shortcuts.py b/spyder/preferences/shortcuts.py
--- a/spyder/preferences/shortcuts.py
+++ b/spyder/preferences/shortcuts.py
@@ -631,7 +631,7 @@ def __init__(self,
         QTableView.__init__(self, parent)
         self._parent = parent
         self.finder = None
-
+        self.shortcut_data = None
         self.source_model = ShortcutsModel(
                                     self,
                                     text_color=text_color,
@@ -654,7 +654,14 @@ def __init__(self,
         self.selectionModel().selectionChanged.connect(self.selection)
 
         self.verticalHeader().hide()
-        self.load_shortcuts()
+
+    def set_shortcut_data(self, shortcut_data):
+        """
+        Shortcut data comes from the registration of actions on the main
+        window. This allows to only display the right actions on the
+        shortcut table. This also allows to display the localize text.
+        """
+        self.shortcut_data = shortcut_data
 
     def focusOutEvent(self, e):
         """Qt Override."""
@@ -681,14 +688,22 @@ def adjust_cells(self):
 
     def load_shortcuts(self):
         """Load shortcuts and assign to table model."""
+        # item[1] -> context, item[2] -> name
+        shortcut_data = [(item[1], item[2]) for item in self.shortcut_data]
         shortcuts = []
         for context, name, keystr in CONF.iter_shortcuts():
-            shortcut = Shortcut(context, name, keystr)
-            shortcuts.append(shortcut)
+            if (context, name) in shortcut_data:
+                # Only add to table actions that are registered from the main
+                # window
+                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]
@@ -807,6 +822,8 @@ def setup_page(self):
         self.ICON = ima.icon('keyboard')
         # Widgets
         self.table = ShortcutsTable(self, text_color=ima.MAIN_FG_COLOR)
+        self.table.set_shortcut_data(self.main.shortcut_data)
+        self.table.load_shortcuts()
         self.finder = ShortcutFinder(self.table, self.table.set_regex)
         self.table.finder = self.finder
         self.table.finder.setPlaceholderText(
@@ -872,6 +889,7 @@ def test():
     from spyder.utils.qthelpers import qapplication
     app = qapplication()
     table = ShortcutsTable()
+    table.load_shortcuts()
     table.show()
     app.exec_()
 


Test patch:
diff --git a/spyder/preferences/tests/conftest.py b/spyder/preferences/tests/conftest.py
--- a/spyder/preferences/tests/conftest.py
+++ b/spyder/preferences/tests/conftest.py
@@ -19,6 +19,7 @@
 import pytest
 
 # Local imports
+from spyder.config.manager import CONF
 from spyder.preferences.appearance import AppearanceConfigPage
 from spyder.preferences.configdialog import ConfigDialog
 from spyder.preferences.general import MainConfigPage
@@ -31,6 +32,11 @@ def __init__(self):
         self.default_style = None
         self.widgetlist = []
         self.thirdparty_plugins = []
+        self.shortcut_data = []
+
+        # Load shortcuts for tests
+        for context, name, __ in CONF.iter_shortcuts():
+            self.shortcut_data.append((None, context, name, None, None))
 
         for attr in ['mem_status', 'cpu_status']:
             mock_attr = Mock()
diff --git a/spyder/preferences/tests/test_shorcuts.py b/spyder/preferences/tests/test_shorcuts.py
--- a/spyder/preferences/tests/test_shorcuts.py
+++ b/spyder/preferences/tests/test_shorcuts.py
@@ -22,11 +22,24 @@
     INVALID_KEY, SEQUENCE_EMPTY)
 
 
+# --- Helpers
+def load_shortcuts(shortcut_table):
+    # Load shortcuts from CONF
+    shortcut_data = []
+    for context, name, __ in CONF.iter_shortcuts():
+        shortcut_data.append((None, context, name, None, None))
+
+    shortcut_table.set_shortcut_data(shortcut_data)
+    shortcut_table.load_shortcuts()
+    return shortcut_table
+
+
 # ---- Qt Test Fixtures
 @pytest.fixture
 def shortcut_table(qtbot):
     """Set up shortcuts."""
     shortcut_table = ShortcutsTable()
+    shortcut_table = load_shortcuts(shortcut_table)
     qtbot.addWidget(shortcut_table)
     return shortcut_table
 
@@ -65,6 +78,24 @@ def test_shortcuts(shortcut_table):
     assert shortcut_table
 
 
+def test_shortcut_in_conf_is_filtered_with_shortcut_data(qtbot):
+    shortcut_table = ShortcutsTable()
+    shortcut_table = load_shortcuts(shortcut_table)
+    qtbot.addWidget(shortcut_table)
+    row_count = shortcut_table.model().rowCount()
+    assert row_count != 0
+
+    shortcut_table_empty = ShortcutsTable()
+    shortcut_table_empty.set_shortcut_data([
+        (None, '_', 'switch to plots', None, None),
+        (None, '_', 'switch to editor', None, None),
+    ])
+    shortcut_table_empty.load_shortcuts()
+    qtbot.addWidget(shortcut_table_empty)
+    row_count = shortcut_table_empty.model().rowCount()
+    assert row_count == 2
+
+
 def test_shortcuts_filtering(shortcut_table):
     """Run shortcuts table."""
     # Store original row count
@@ -179,6 +210,7 @@ def test_sequence_conflict(create_shortcut_editor, qtbot):
     assert shortcut_editor.warning == SEQUENCE_CONFLICT
     assert shortcut_editor.button_ok.isEnabled()
 
+
     # Check that the conflict is detected for a compound of key sequences.
     qtbot.keyClick(shortcut_editor, Qt.Key_X)
     assert shortcut_editor.new_sequence == 'Ctrl+X, X'