Repository: spyder-ide/spyder

Issue:
Click on outisde of tour should exit tour



Discussion:
@mariacamilaremolinagutierrez, since you're going to be working on our interactive tour, please also take a look at this one.


Resolving patch:
diff --git a/spyder/app/mainwindow.py b/spyder/app/mainwindow.py
--- a/spyder/app/mainwindow.py
+++ b/spyder/app/mainwindow.py
@@ -277,6 +277,8 @@ def __init__(self, options=None):
         self.open_project = options.open_project
 
         self.debug_print("Start of MainWindow constructor")
+        
+        self.setFocusPolicy(Qt.StrongFocus)
 
         def signal_handler(signum, frame=None):
             """Handler for signals."""
@@ -2091,6 +2093,12 @@ def moveEvent(self, event):
         # To be used by the tour to be able to move
         self.sig_moved.emit(event)
 
+    def focusInEvent(self, event):
+        """Reimplement Qt method."""
+        QMainWindow.focusInEvent(self, event)
+        if self.hasFocus():
+            self.tour.gain_focus()
+
     def hideEvent(self, event):
         """Reimplement Qt method"""
         for plugin in self.widgetlist:
diff --git a/spyder/app/tour.py b/spyder/app/tour.py
--- a/spyder/app/tour.py
+++ b/spyder/app/tour.py
@@ -393,10 +393,13 @@ def set_funcs_after_fade_out(self, funcs):
 
 class FadingCanvas(FadingDialog):
     """The black semi transparent canvas that covers the application"""
-    def __init__(self, parent, opacity, duration, easing_curve, color):
+    def __init__(self, parent, opacity, duration, easing_curve, color,
+                 tour=None):
+        """Create a black semi transparent canvas that covers the app."""
         super(FadingCanvas, self).__init__(parent, opacity, duration,
                                            easing_curve)
         self.parent = parent
+        self.tour = tour
 
         self.color = color              # Canvas color
         self.color_decoration = Qt.red  # Decoration color
@@ -524,14 +527,27 @@ def mousePressEvent(self, event):
         """Override Qt method"""
         pass
 
+    def focusInEvent(self, event):
+        """Override Qt method."""
+        # To be used so tips do not appear outside spyder
+        if self.hasFocus():
+            self.tour.gain_focus()
+
+    def focusOutEvent(self, event):
+        """Override Qt method."""
+        # To be used so tips do not appear outside spyder
+        if self.tour.step_current != 0:
+            self.tour.lost_focus()
+
 
 class FadingTipBox(FadingDialog):
     """ """
-    def __init__(self, parent, opacity, duration, easing_curve):
+    def __init__(self, parent, opacity, duration, easing_curve, tour=None):
         super(FadingTipBox, self).__init__(parent, opacity, duration,
                                            easing_curve)
         self.holder = self.anim  # needed for qt to work
         self.parent = parent
+        self.tour = tour
 
         self.frames = None
         self.color_top = QColor.fromRgb(230, 230, 230)
@@ -620,6 +636,7 @@ def __init__(self, parent, opacity, duration, easing_curve):
         # Windows fix, slashes should be always in unix-style
         self.stylesheet = self.stylesheet.replace('\\', '/')
 
+        self.setFocusPolicy(Qt.StrongFocus)
         for widget in self.widgets:
             widget.setFocusPolicy(Qt.NoFocus)
             widget.setStyleSheet(self.stylesheet)
@@ -671,7 +688,7 @@ def __init__(self, parent, opacity, duration, easing_curve):
         self.setLayout(layout)
 
         self.set_funcs_before_fade_in([self._disable_widgets])
-        self.set_funcs_after_fade_in([self._enable_widgets])
+        self.set_funcs_after_fade_in([self._enable_widgets, self.setFocus])
         self.set_funcs_before_fade_out([self._disable_widgets])
 
         self.setContextMenuPolicy(Qt.CustomContextMenu)
@@ -815,6 +832,11 @@ def mousePressEvent(self, event):
 #            if clicked_widget == self.label_current:
 #            self.context_menu_requested(event)
 
+    def focusOutEvent(self, event):
+        """Override Qt method."""
+        # To be used so tips do not appear outside spyder
+        self.tour.lost_focus()
+
     def context_menu_requested(self, event):
         """ """
         pos = QPoint(event.x(), event.y())
@@ -868,13 +890,15 @@ def __init__(self, parent):
         self.run = None
 
         self.is_tour_set = False
+        self.is_running = False
 
         # Widgets
         self.canvas = FadingCanvas(self.parent, self.opacity_canvas,
                                    self.duration_canvas, self.easing_curve,
-                                   self.color)
+                                   self.color, tour=self)
         self.tips = FadingTipBox(self.parent, self.opacity_tips,
-                                 self.duration_tips, self.easing_curve)
+                                 self.duration_tips, self.easing_curve,
+                                 tour=self)
 
         # Widgets setup
         # Needed to fix issue #2204
@@ -898,6 +922,10 @@ def __init__(self, parent):
         # To capture the arrow keys that allow moving the tour
         self.tips.sig_key_pressed.connect(self._key_pressed)
 
+        # To control the focus of tour
+        self.setting_data = False
+        self.hidden = False
+
     def _resized(self, event):
         """ """
         size = event.size()
@@ -986,7 +1014,8 @@ def _process_widgets(self, names, spy_window):
         return widgets, dockwidgets
 
     def _set_data(self):
-        """ """
+        """Set data that is displayed in each step of the tour."""
+        self.setting_data = True
         step, steps, frames = self.step_current, self.steps, self.frames
         current = '{0}/{1}'.format(step + 1, steps)
         frame = frames[step]
@@ -1044,6 +1073,7 @@ def _set_data(self):
         # Make canvas black when starting a new place of decoration
         self.canvas.update_widgets(dockwidgets)
         self.canvas.update_decoration(decoration)
+        self.setting_data = False
 
     def _locate_tip_box(self):
         """ """
@@ -1124,6 +1154,10 @@ def _key_pressed(self):
             pos = self.tips.label_current.pos()
             self.tips.context_menu_requested(pos)
 
+    def _hiding(self):
+        self.hidden = True
+        self.tips.hide()
+
     # --- public api
     def run_code(self):
         """ """
@@ -1161,6 +1195,8 @@ def start_tour(self):
         self.canvas.fade_in(self._move_step)
         self._clear_canvas()
 
+        self.is_running = True
+
     def close_tour(self):
         """ """
         self.tips.fade_out(self._close_canvas)
@@ -1174,6 +1210,19 @@ def close_tour(self):
         except:
             pass
 
+        self.is_running = False
+
+    def hide_tips(self):
+        """Hide tips dialog when the main window loses focus."""
+        self._clear_canvas()
+        self.tips.fade_out(self._hiding)
+
+    def unhide_tips(self):
+        """Unhide tips dialog when the main window loses focus."""
+        self._clear_canvas()
+        self._move_step()
+        self.hidden = False
+
     def next_step(self):
         """ """
         self._clear_canvas()
@@ -1200,6 +1249,24 @@ def first_step(self):
         """ """
         self.go_to_step(0)
 
+    def lost_focus(self):
+        """Confirm if the tour loses focus and hides the tips."""
+        if (self.is_running and not self.any_has_focus() and
+            not self.setting_data and not self.hidden):
+            self.hide_tips()
+
+    def gain_focus(self):
+        """Confirm if the tour regains focus and unhides the tips."""
+        if (self.is_running and self.any_has_focus() and 
+            not self.setting_data and self.hidden):
+            self.unhide_tips()
+
+    def any_has_focus(self):
+        """Returns if tour or any of its components has focus."""
+        f = (self.hasFocus() or self.parent.hasFocus() or 
+             self.tips.hasFocus() or self.canvas.hasFocus())
+        return f
+
 # ----------------------------------------------------------------------------
 # Used for testing the functionality
 


Test patch: