Repository: spyder-ide/spyder

Issue:
Alt key pressed moves focus to the scrollbar (the left one) when focused on auto complete
<!--- **PLEASE READ:** When submitting here, please ensure you've completed the following checklist and checked the boxes to confirm. Issue reports without it may be closed. Thanks! --->

### Issue Report Checklist

* [ x] Searched the [issues page](https://github.com/spyder-ide/spyder/issues?q=is%3Aissue) for similar reports
* [ x] Read the relevant sections of the [Spyder Troubleshooting Guide](https://github.com/spyder-ide/spyder/wiki/Troubleshooting-Guide-and-FAQ) and followed its advice
* [x ] Reproduced the issue after updating with ``conda update spyder`` (or ``pip``, if not using Anaconda)
* [ x] Could not reproduce inside ``jupyter qtconsole`` (if console-related)
* [ x] Tried basic troubleshooting (if a bug/error)
    * [x ] Restarted Spyder
    * [ x] Reset preferences with ``spyder --reset``
    * [ x] Reinstalled the latest version of [Anaconda](https://www.anaconda.com/download/)
    * [ x] Tried the other applicable steps from the Troubleshooting Guide
* [ x] Completed the **Problem Description**, **Steps to Reproduce** and **Version** sections below


## Problem Description
My end goal is to be able to use my KDE shortcut alt+m to trigger a down event while the auto-complete box is open
so that I can choose/browse the auto-complete box.  I use the alt+m shortcut in all applications for down, and even use it successfully in spyder when just typing in the editor.

However when I trigger the auto-complete box then try to go down in the list with alt+m, as soon as I hit the "alt" key the box disappears and I see the focus go to the first scrollbar to the right (seems to be the "scroll-to" scrollbar).  I then have to remind myself of that, then backspace and trigger the auto-complete again, and then use the actual "down arrow" key.  Yes, yes, first world problems.

So the problem is that the "alt" key loses the focus of the auto-complete box.


### What steps reproduce the problem?

1. in the editor, trigger the auto-complete box
2. hit the "alt" key


### What is the expected output? What do you see instead?

I expect the auto-complete box to stay visible and have focus when I hit the alt key
so that when I go to hit alt+m I can browse the auto-complete list

What I see instead is the auto-complete box disappears and I see the scrollbar highlighted.



## Versions
<!--- You can get this information from Help > About Spyder...
or (if Spyder won't launch) the "conda list" command
from the Anaconda Prompt/Terminal/command line. --->

* Spyder version: 5.3.2
* Python version: 3.9.12 64-bit
* Qt version: 5.15.2
* PyQt version: 5.15.7
* Operating System name/version: Opensuse Leap 15.3   kernel: linux 5.16.11-lp153.11-default 

### Dependencies
<!--- Please go to the menu entry Help > Dependencies,
press the Copy to clipboard button and paste below --->

```
# Mandatory:
atomicwrites >=1.2.0          :  1.4.1 (OK)
chardet >=2.0.0               :  5.0.0 (OK)
cloudpickle >=0.5.0           :  2.1.0 (OK)
cookiecutter >=1.6.0          :  2.1.1 (OK)
diff_match_patch >=20181111   :  20200713 (OK)
intervaltree >=3.0.2          :  3.1.0 (OK)
IPython >=7.31.1;<8.0.0       :  7.34.0 (OK)
jedi >=0.17.2;<0.19.0         :  0.18.1 (OK)
jellyfish >=0.7               :  0.9.0 (OK)
jsonschema >=3.2.0            :  4.9.1 (OK)
keyring >=17.0.0              :  23.8.2 (OK)
nbconvert >=4.0               :  6.5.3 (OK)
numpydoc >=0.6.0              :  1.4.0 (OK)
parso >=0.7.0;<0.9.0          :  0.8.3 (OK)
pexpect >=4.4.0               :  4.8.0 (OK)
pickleshare >=0.4             :  0.7.5 (OK)
psutil >=5.3                  :  5.9.1 (OK)
pygments >=2.0                :  2.13.0 (OK)
pylint >=2.5.0;<3.0           :  2.14.5 (OK)
pyls_spyder >=0.4.0           :  0.4.0 (OK)
pylsp >=1.5.0;<1.6.0          :  1.5.0 (OK)
pylsp_black >=1.2.0           :  1.2.1 (OK)
qdarkstyle >=3.0.2;<3.1.0     :  3.0.3 (OK)
qstylizer >=0.1.10            :  0.2.1 (OK)
qtawesome >=1.0.2             :  1.1.1 (OK)
qtconsole >=5.3.0;<5.4.0      :  5.3.1 (OK)
qtpy >=2.1.0                  :  2.2.0 (OK)
rtree >=0.9.7                 :  1.0.0 (OK)
setuptools >=49.6.0           :  61.2.0 (OK)
sphinx >=0.6.6                :  5.1.1 (OK)
spyder_kernels >=2.3.2;<2.4.0 :  2.3.2 (OK)
textdistance >=4.2.0          :  4.3.0 (OK)
three_merge >=0.1.1           :  0.1.1 (OK)
watchdog >=0.10.3             :  2.1.9 (OK)
xdg >=0.26                    :  0.28 (OK)
zmq >=22.1.0                  :  23.2.1 (OK)

# Optional:
cython >=0.21                 :  0.29.32 (OK)
matplotlib >=3.0.0            :  3.5.3 (OK)
numpy >=1.7                   :  1.22.4 (OK)
pandas >=1.1.1                :  1.4.3 (OK)
scipy >=0.17.0                :  1.9.0 (OK)
sympy >=0.7.3                 :  None (NOK)
```



Discussion:
Hey @cw9000, thanks for reporting. I also noticed this small regression and I agree with you that we can handle the case of pressing modifier keys such as `Alt`, `Shift` and `Ctrl` more gracefully when the completion widget is visible.

We'll take a look at it for our next version (5.4.0), to be released next month.

Resolving patch:
diff --git a/spyder/plugins/editor/widgets/codeeditor.py b/spyder/plugins/editor/widgets/codeeditor.py
--- a/spyder/plugins/editor/widgets/codeeditor.py
+++ b/spyder/plugins/editor/widgets/codeeditor.py
@@ -284,6 +284,9 @@ class CodeEditor(TextEditBaseWidget):
     # Used to signal font change
     sig_font_changed = Signal()
 
+    # Used to request saving a file
+    sig_save_requested = Signal()
+
     def __init__(self, parent=None):
         TextEditBaseWidget.__init__(self, parent)
 
@@ -4628,12 +4631,6 @@ def keyPressEvent(self, event):
             # Fixes spyder-ide/spyder#11021
             self._start_completion_timer()
 
-        if event.modifiers() and self.is_completion_widget_visible():
-            # Hide completion widget before passing event modifiers
-            # since the keypress could be then a shortcut
-            # See spyder-ide/spyder#14806
-            self.completion_widget.hide()
-
         if key in {Qt.Key_Up, Qt.Key_Left, Qt.Key_Right, Qt.Key_Down}:
             self.hide_tooltip()
 
diff --git a/spyder/plugins/editor/widgets/completion.py b/spyder/plugins/editor/widgets/completion.py
--- a/spyder/plugins/editor/widgets/completion.py
+++ b/spyder/plugins/editor/widgets/completion.py
@@ -12,10 +12,11 @@
 
 # Third psrty imports
 from qtpy.QtCore import QPoint, Qt, Signal, Slot
-from qtpy.QtGui import QFontMetrics, QFocusEvent
+from qtpy.QtGui import QFontMetrics, QFocusEvent, QKeySequence
 from qtpy.QtWidgets import QListWidget, QListWidgetItem, QToolTip
 
 # Local imports
+from spyder.api.config.mixins import SpyderConfigurationAccessor
 from spyder.utils.icon_manager import ima
 from spyder.plugins.completion.api import CompletionItemKind
 from spyder.py3compat import to_text_string
@@ -26,7 +27,7 @@
 DEFAULT_COMPLETION_ITEM_WIDTH = 250
 
 
-class CompletionWidget(QListWidget):
+class CompletionWidget(QListWidget, SpyderConfigurationAccessor):
     """Completion list widget."""
     ITEM_TYPE_MAP = {
         CompletionItemKind.TEXT: 'text',
@@ -238,6 +239,7 @@ def set_item_display(self, item_widget, item_info, height, width):
 
         item_widget.setText(item_text)
         item_widget.setIcon(self._get_cached_icon(item_type))
+
         # Set data for accessible readers using item label and type
         # See spyder-ide/spyder#17047 and
         # https://doc.qt.io/qt-5/qt.html#ItemDataRole-enum
@@ -314,10 +316,12 @@ def keyPressEvent(self, event):
         shift = event.modifiers() & Qt.ShiftModifier
         ctrl = event.modifiers() & Qt.ControlModifier
         altgr = event.modifiers() and (key == Qt.Key_AltGr)
+
         # Needed to properly handle Neo2 and other keyboard layouts
         # See spyder-ide/spyder#11293
         neo2_level4 = (key == 0)  # AltGr (ISO_Level5_Shift) in Neo2 on Linux
         modifier = shift or ctrl or alt or altgr or neo2_level4
+
         if key in (Qt.Key_Return, Qt.Key_Enter, Qt.Key_Tab):
             # Check that what was selected can be selected,
             # otherwise timing issues
@@ -344,7 +348,35 @@ def keyPressEvent(self, event):
                 self.setCurrentRow(0)
             else:
                 QListWidget.keyPressEvent(self, event)
-        elif len(text) or key == Qt.Key_Backspace:
+        elif key == Qt.Key_Backspace:
+            self.textedit.keyPressEvent(event)
+            self.update_current(new=False)
+        elif len(text):
+            # Determine the key sequence introduced by the user to check if
+            # we need to call the action associated to it in textedit. This
+            # way is not necessary to hide this widget for the sequence to
+            # take effect in textedit.
+            # Fixes spyder-ide/spyder#19372
+            if modifier:
+                # Build the sequence as an int.
+                # See https://stackoverflow.com/a/23919177/438386 for context.
+                # Note: We only accept Ctrl, Shift and Alt as modifiers for
+                # keyboard shortcuts in Preferences.
+                key_sequence = key
+                if ctrl:
+                    key_sequence += Qt.CTRL
+                if shift:
+                    key_sequence += Qt.SHIFT
+                if alt:
+                    key_sequence += Qt.ALT
+
+                # Ask to save file if the user pressed the sequence for that.
+                # Fixes spyder-ide/spyder#14806
+                save_shortcut = self.get_conf(
+                    'editor/save file', section='shortcuts')
+                if QKeySequence(key_sequence) == QKeySequence(save_shortcut):
+                    self.textedit.sig_save_requested.emit()
+
             self.textedit.keyPressEvent(event)
             self.update_current(new=False)
         elif modifier:
diff --git a/spyder/plugins/editor/widgets/editor.py b/spyder/plugins/editor/widgets/editor.py
--- a/spyder/plugins/editor/widgets/editor.py
+++ b/spyder/plugins/editor/widgets/editor.py
@@ -2531,6 +2531,7 @@ def create_new_editor(self, fname, enc, txt, set_current, new=False,
         editor.sig_process_code_analysis.connect(
             self.sig_update_code_analysis_actions)
         editor.sig_refresh_formatting.connect(self.sig_refresh_formatting)
+        editor.sig_save_requested.connect(self.save)
         language = get_file_language(fname, txt)
         editor.setup_editor(
             linenumbers=self.linenumbers_enabled,


Test patch:
diff --git a/spyder/plugins/editor/widgets/tests/test_completions_hide.py b/spyder/plugins/editor/widgets/tests/test_completions_hide.py
--- a/spyder/plugins/editor/widgets/tests/test_completions_hide.py
+++ b/spyder/plugins/editor/widgets/tests/test_completions_hide.py
@@ -6,18 +6,11 @@
 
 """Tests some cases were completions need to be hidden."""
 
-# Standard lirary imports
-import sys
-
 # Third party imports
 from flaky import flaky
 import pytest
-
 from qtpy.QtCore import Qt
 
-# Local imports
-from spyder.config.base import running_in_ci
-
 
 @pytest.mark.slow
 @pytest.mark.order(1)
@@ -127,38 +120,5 @@ def test_automatic_completions_widget_visible(completions_codeeditor, qtbot):
     code_editor.toggle_code_snippets(True)
 
 
-@pytest.mark.slow
-@pytest.mark.order(1)
-@flaky(max_runs=5)
-@pytest.mark.skipif(running_in_ci() and sys.platform.startswith('linux'),
-                    reason="Stalls test suite with Linux on CI")
-def test_automatic_completions_hide_on_save(completions_codeeditor, qtbot):
-    """Test on-the-fly completion closing when using save shortcut (Ctrl + S).
-
-    Regression test for issue #14806.
-    """
-    code_editor, _ = completions_codeeditor
-    completion = code_editor.completion_widget
-    code_editor.toggle_code_snippets(False)
-
-    code_editor.set_text('some = 0\nsomething = 1\n')
-    cursor = code_editor.textCursor()
-    code_editor.moveCursor(cursor.End)
-
-    # Complete some -> [some, something]
-    with qtbot.waitSignal(completion.sig_show_completions,
-                          timeout=10000) as sig:
-        qtbot.keyClicks(code_editor, 'some')
-    assert "some" in [x['label'] for x in sig.args[0]]
-    assert "something" in [x['label'] for x in sig.args[0]]
-
-    # Hide completion widget when saving
-    qtbot.keyPress(
-        code_editor, Qt.Key_S, modifier=Qt.ControlModifier, delay=300)
-    qtbot.waitUntil(lambda: completion.isHidden())
-
-    code_editor.toggle_code_snippets(True)
-
-
 if __name__ == '__main__':
     pytest.main(['test_completion.py', '--run-slow'])
diff --git a/spyder/plugins/editor/widgets/tests/test_save.py b/spyder/plugins/editor/widgets/tests/test_save.py
--- a/spyder/plugins/editor/widgets/tests/test_save.py
+++ b/spyder/plugins/editor/widgets/tests/test_save.py
@@ -10,13 +10,17 @@
 
 # Standard library imports
 import os.path as osp
+import sys
 from textwrap import dedent
 from unittest.mock import Mock
 
 # Third party imports
+from flaky import flaky
 import pytest
+from qtpy.QtCore import Qt
 
 # Local imports
+from spyder.config.base import running_in_ci
 from spyder.plugins.editor.panels import DebuggerPanel
 from spyder.plugins.editor.widgets import editor
 from spyder.plugins.outlineexplorer.main_widget import OutlineExplorerWidget
@@ -66,7 +70,7 @@ def editor_bot(base_editor_bot, request):
 @pytest.fixture
 def editor_splitter_bot(qtbot):
     """Create editor splitter."""
-    es = editor_splitter = editor.EditorSplitter(None, Mock(), [], first=True)
+    es = editor.EditorSplitter(None, Mock(), [], first=True)
     qtbot.addWidget(es)
     es.show()
     yield es
@@ -503,5 +507,45 @@ def test_save_as_change_file_type(editor_bot, mocker, tmpdir):
     assert not debugger_panel.isVisible()
 
 
+@pytest.mark.slow
+@pytest.mark.order(1)
+@flaky(max_runs=5)
+@pytest.mark.skipif(running_in_ci() and sys.platform.startswith('linux'),
+                    reason="Stalls test suite with Linux on CI")
+def test_save_when_completions_are_visible(completions_editor, qtbot):
+    """
+    Test that save works when the completion widget is visible and the user
+    press the save shortcut (Ctrl+S).
+
+    Regression test for issue spyder-ide/spyder#14806.
+    """
+    file_path, editorstack, code_editor, __ = completions_editor
+    completion = code_editor.completion_widget
+    code_editor.toggle_code_snippets(False)
+
+    code_editor.set_text('some = 0\nsomething = 1\n')
+    editorstack.save(force=True)
+    cursor = code_editor.textCursor()
+    code_editor.moveCursor(cursor.End)
+
+    # Complete some -> [some, something]
+    with qtbot.waitSignal(completion.sig_show_completions,
+                          timeout=10000) as sig:
+        qtbot.keyClicks(code_editor, 'some')
+    assert "some" in [x['label'] for x in sig.args[0]]
+    assert "something" in [x['label'] for x in sig.args[0]]
+
+    # Press keyboard shortcut corresponding to save
+    qtbot.keyPress(
+        completion, Qt.Key_S, modifier=Qt.ControlModifier, delay=300)
+
+    # Assert file was saved
+    with open(file_path, 'r') as f:
+        saved_text = f.read()
+    assert saved_text == 'some = 0\nsomething = 1\nsome'
+
+    code_editor.toggle_code_snippets(True)
+
+
 if __name__ == "__main__":
     pytest.main()