Repository: spyder-ide/spyder

Issue:
Hovers don't hide when Spyder loses focus
<!--- **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)
* [ ] Could not reproduce inside ``jupyter qtconsole`` (if console-related)
* [ ] 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/)
    * [ ] Tried the other applicable steps from the Troubleshooting Guide
* [ ] Completed the **Problem Description**, **Steps to Reproduce** and **Version** sections below


## Problem Description
The issue started with version 4 of Spyder, where the help boxes remain persistent despite me going to another window/browser as can be seen with the picture below.


### What steps reproduce the problem?

1. Hovering over some variable produces a help box
2. Going to another window/browser/desktop the help box persists

![image](https://user-images.githubusercontent.com/62237347/80231040-19b63300-8653-11ea-904d-482f90e02637.png)

### What is the expected output? What do you see instead?
The help box should not be persistent on the screen.


### Paste Traceback/Error Below (if applicable)
<!--- Copy from error dialog or View > Panes > Internal Console --->

```python-traceback

NA
```

## 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. --->
(even going to get the Spyder version this pops up and persists)
![image](https://user-images.githubusercontent.com/62237347/80231199-53873980-8653-11ea-9642-6bb9d8316310.png)

* Spyder version: 4.1.2
* Python version: 3.7.4
* Qt version: 5.9.6
* PyQt version: 5.9.2
* Operating System name/version: Windows 10

### Dependencies
<!--- Please go to the menu entry Help > Dependencies,
press the Copy to clipboard button and paste below --->

```

# Mandatory:
atomicwrites >=1.2.0           :  1.3.0 (OK)
chardet >=2.0.0                :  3.0.4 (OK)
cloudpickle >=0.5.0            :  1.3.0 (OK)
diff_match_patch >=20181111    :  20181111 (OK)
intervaltree                   :  None (OK)
IPython >=4.0                  :  7.13.0 (OK)
jedi =0.15.2                   :  0.15.2 (OK)
nbconvert >=4.0                :  5.6.1 (OK)
numpydoc >=0.6.0               :  0.9.2 (OK)
paramiko >=2.4.0               :  2.7.1 (OK)
parso =0.5.2                   :  0.5.2 (OK)
pexpect >=4.4.0                :  4.8.0 (OK)
pickleshare >=0.4              :  0.7.5 (OK)
psutil >=5.3                   :  5.7.0 (OK)
pygments >=2.0                 :  2.6.1 (OK)
pylint >=0.25                  :  2.4.4 (OK)
pyls >=0.31.9;<0.32.0          :  0.31.9 (OK)
qdarkstyle >=2.8               :  2.8 (OK)
qtawesome >=0.5.7              :  0.7.0 (OK)
qtconsole >=4.6.0              :  4.7.2 (OK)
qtpy >=1.5.0                   :  1.9.0 (OK)
rtree >=0.8.3                  :  0.9.3 (OK)
sphinx >=0.6.6                 :  2.4.4 (OK)
spyder_kernels >=1.9.0;<1.10.0 :  1.9.0 (OK)
watchdog                       :  None (OK)
zmq >=17                       :  18.1.1 (OK)

# Optional:
cython >=0.21                  :  0.29.15 (OK)
matplotlib >=2.0.0             :  3.1.3 (OK)
numpy >=1.7                    :  1.18.1 (OK)
pandas >=0.13.1                :  1.0.3 (OK)
scipy >=0.17.0                 :  1.3.1 (OK)
sympy >=0.7.3                  :  1.5.1 (OK)
```



Discussion:

Resolving patch:
diff --git a/spyder/widgets/calltip.py b/spyder/widgets/calltip.py
--- a/spyder/widgets/calltip.py
+++ b/spyder/widgets/calltip.py
@@ -81,6 +81,8 @@ def __init__(self, parent=None, as_tooltip=False):
         # Signals
         self.linkHovered.connect(self._update_hover_html_link_style)
         self._timer_hide.timeout.connect(self.hide)
+        QApplication.instance().applicationStateChanged.connect(
+            self._should_hide)
 
     def paintEvent(self, event):
         """Reimplemented to paint the background panel."""
@@ -127,6 +129,10 @@ def show_tip(self, point, tip, cursor=None, completion_doc=None):
         """
         Attempts to show the specified tip at the current cursor location.
         """
+        # Don't show the widget if the main window is not focused
+        if QApplication.instance().applicationState() != Qt.ApplicationActive:
+            return
+
         # Set the text and resize the widget accordingly.
         self.tip = tip
         self.setText(tip)
@@ -228,6 +234,13 @@ def hide(self):
         super(ToolTipWidget, self).hide()
         self._timer_hide.stop()
 
+    def _should_hide(self, state):
+        """
+        This widget should hide itself if the application is not active.
+        """
+        if state != Qt.ApplicationActive:
+            self.hide()
+
 
 
 class CallTipWidget(QLabel):
@@ -271,6 +284,10 @@ def __init__(self, text_edit, hide_timer_on=False, as_tooltip=False):
         self.setMargin(1 + self.style().pixelMetric(
                 QStyle.PM_ToolTipLabelFrameWidth, None, self))
 
+        # Signals
+        QApplication.instance().applicationStateChanged.connect(
+            self._should_hide)
+
     def eventFilter(self, obj, event):
         """ Reimplemented to hide on certain key presses and on text edit focus
             changes.
@@ -389,6 +406,10 @@ def focusOutEvent(self, event):
     def show_tip(self, point, tip, wrapped_tiplines):
         """ Attempts to show the specified tip at the current cursor location.
         """
+        # Don't show the widget if the main window is not focused
+        if QApplication.instance().applicationState() != Qt.ApplicationActive:
+            return
+
         # Don't attempt to show it if it's already visible and the text
         # to be displayed is the same as the one displayed before.
         if self.isVisible():
@@ -547,3 +568,10 @@ def _cursor_position_changed(self):
             pos, _ = self._find_parenthesis(position - 1, forward=False)
             if pos == -1:
                 self.hide()
+
+    def _should_hide(self, state):
+        """
+        This widget should hide itself if the application is not active.
+        """
+        if state != Qt.ApplicationActive:
+            self.hide()


Test patch:
diff --git a/spyder/plugins/editor/widgets/tests/test_hints_and_calltips.py b/spyder/plugins/editor/widgets/tests/test_hints_and_calltips.py
--- a/spyder/plugins/editor/widgets/tests/test_hints_and_calltips.py
+++ b/spyder/plugins/editor/widgets/tests/test_hints_and_calltips.py
@@ -33,6 +33,8 @@ def {SIG}:
 
 @pytest.mark.slow
 @pytest.mark.second
+@pytest.mark.skipif(sys.platform == 'darwin' and PY2,
+                    reason='Fails on Mac and Python 2')
 def test_hide_calltip(lsp_codeeditor, qtbot):
     """Test that calltips are hidden when a matching ')' is found."""
     code_editor, _ = lsp_codeeditor
diff --git a/spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py b/spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py
--- a/spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py
+++ b/spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py
@@ -248,7 +248,8 @@ def test_banners(ipyconsole, qtbot):
       ["x", "/", "out"],
       ["Parameters<br>", "x : array_like ..."])]
     )
-@pytest.mark.skipif(sys.platform == 'darwin', reason="Times out on macOS")
+@pytest.mark.skipif(not os.name == 'nt',
+                    reason="Times out on macOS and fails on Linux")
 def test_get_calltips(ipyconsole, qtbot, function, signature, documentation):
     """Test that calltips show the documentation."""
     shell = ipyconsole.get_current_shellwidget()