Repository: spyder-ide/spyder

Issue:
Error report dialog pop ups when creating a new console
* Running master
* Possible cause: A search widget is created for each Name Space browser in the variable explorer
* A warning is triggered:

```
C:\Users\Daniel\Documents\Spyder\spyder\spyder\utils\registries.py:77: UserWarning: There already exists a reference <PyQt5.QtWidgets.QToolButton object at 0x00000193F3C2DD38> with id close_finder under the context __global of plugin variable_explorer. The new reference <PyQt5.QtWidgets.QToolButton object at 0x00000193ED14EF78> will overwrite the previous reference. Hint: <PyQt5.QtWidgets.QToolButton object at 0x00000193ED14EF78> should have a different id_. See 
  f'There already exists a reference {context_references[id_]} '
```


Discussion:

Resolving patch:
diff --git a/spyder/plugins/variableexplorer/widgets/main_widget.py b/spyder/plugins/variableexplorer/widgets/main_widget.py
--- a/spyder/plugins/variableexplorer/widgets/main_widget.py
+++ b/spyder/plugins/variableexplorer/widgets/main_widget.py
@@ -10,14 +10,15 @@
 
 # Third party imports
 from qtpy.QtCore import QTimer, Signal, Slot
-from qtpy.QtWidgets import QAction, QStackedWidget, QVBoxLayout
+from qtpy.QtWidgets import (
+    QAction, QHBoxLayout, QStackedWidget, QVBoxLayout, QWidget)
 
 # Local imports
 from spyder.api.config.decorators import on_conf_change
 from spyder.api.translations import get_translation
 from spyder.api.widgets import PluginMainWidget
 from spyder.plugins.variableexplorer.widgets.namespacebrowser import (
-    NamespaceBrowser)
+    NamespaceBrowser, NamespacesBrowserFinder, VALID_VARIABLE_CHARS)
 from spyder.utils.programs import is_module_installed
 
 # Localization
@@ -136,9 +137,14 @@ def __init__(self, name=None, plugin=None, parent=None):
         self.context_menu = None
         self.empty_context_menu = None
 
+        # --- Finder
+        self.finder = None
+
         # Layout
         layout = QVBoxLayout()
         layout.addWidget(self._stack)
+        # Note: Later with the addition of the first NamespaceBrowser the
+        # find/search widget is added. See 'set_current_widget'
         self.setLayout(layout)
 
         # Signals
@@ -460,8 +466,53 @@ def current_widget(self):
     def remove_widget(self, nsb):
         self._stack.removeWidget(nsb)
 
-    def set_current_widget(self, nsb):
+    def update_finder(self, nsb, old_nsb):
+        """Initialize or update finder widget."""
+        if self.finder is None:
+            # Initialize finder/search related widgets
+            self.finder = QWidget(self)
+            self.text_finder = NamespacesBrowserFinder(
+                nsb.editor,
+                callback=nsb.editor.set_regex,
+                main=nsb,
+                regex_base=VALID_VARIABLE_CHARS)
+            self.finder.text_finder = self.text_finder
+            self.finder_close_button = self.create_toolbutton(
+                'close_finder',
+                triggered=self.hide_finder,
+                icon=self.create_icon('DialogCloseButton'),
+            )
+            finder_layout = QHBoxLayout()
+            finder_layout.addWidget(self.finder_close_button)
+            finder_layout.addWidget(self.text_finder)
+            finder_layout.setContentsMargins(0, 0, 0, 0)
+            self.finder.setLayout(finder_layout)
+            self.layout().addWidget(self.finder)
+        else:
+            # Just update references to the same text_finder (Custom QLineEdit)
+            # widget to the new current NamespaceBrowser and save current
+            # finder state in the previous NamespaceBrowser
+            if old_nsb is not None:
+                self.save_finder_state(old_nsb)
+            self.text_finder.update_parent(
+                nsb.editor,
+                callback=nsb.editor.set_regex,
+                main=nsb,
+            )
+
+    def set_current_widget(self, nsb, old_nsb):
+        """
+        Set the current NamespaceBrowser.
+
+        This also setup the finder widget to work with the current
+        NamespaceBrowser.
+        """
+        self.update_finder(nsb, old_nsb)
+        finder_visible = nsb.set_text_finder(self.text_finder)
         self._stack.setCurrentWidget(nsb)
+        self.finder.setVisible(finder_visible)
+        search_action = self.get_action(VariableExplorerWidgetActions.Search)
+        search_action.setChecked(finder_visible)
 
     # ---- Public API
     # ------------------------------------------------------------------------
@@ -474,13 +525,14 @@ def add_shellwidget(self, shellwidget):
         """
         shellwidget_id = id(shellwidget)
         if shellwidget_id not in self._shellwidgets:
+            old_nsb = self.current_widget()
             nsb = NamespaceBrowser(self)
             nsb.set_shellwidget(shellwidget)
             nsb.setup()
             self.add_widget(nsb)
             self._set_actions_and_menus(nsb)
             self._shellwidgets[shellwidget_id] = nsb
-            self.set_current_widget(nsb)
+            self.set_current_widget(nsb, old_nsb)
             self.update_actions()
             return nsb
 
@@ -493,9 +545,10 @@ def remove_shellwidget(self, shellwidget):
 
     def set_shellwidget(self, shellwidget):
         shellwidget_id = id(shellwidget)
+        old_nsb = self.current_widget()
         if shellwidget_id in self._shellwidgets:
             nsb = self._shellwidgets[shellwidget_id]
-            self.set_current_widget(nsb)
+            self.set_current_widget(nsb, old_nsb)
 
     def import_data(self, filenames=None):
         """
@@ -521,11 +574,34 @@ def reset_namespace(self):
     def show_finder(self, checked):
         if self.count():
             nsb = self.current_widget()
-            nsb.show_finder(checked)
+            if checked:
+                self.finder.text_finder.setText(nsb.last_find)
+            else:
+                self.save_finder_state(nsb)
+                self.finder.text_finder.setText('')
+            self.finder.setVisible(checked)
+            if self.finder.isVisible():
+                self.finder.text_finder.setFocus()
+            else:
+                nsb.editor.setFocus()
 
+    @Slot()
     def hide_finder(self):
         action = self.get_action(VariableExplorerWidgetActions.Search)
         action.setChecked(False)
+        nsb = self.current_widget()
+        self.save_finder_state(nsb)
+        self.finder.text_finder.setText('')
+
+    def save_finder_state(self, nsb):
+        """
+        Save finder state (last input text and visibility).
+
+        The values are saved in the given NamespaceBrowser.
+        """
+        last_find = self.text_finder.text()
+        finder_visibility = self.finder.isVisible()
+        nsb.save_finder_state(last_find, finder_visibility)
 
     def refresh_table(self):
         if self.count():
diff --git a/spyder/plugins/variableexplorer/widgets/namespacebrowser.py b/spyder/plugins/variableexplorer/widgets/namespacebrowser.py
--- a/spyder/plugins/variableexplorer/widgets/namespacebrowser.py
+++ b/spyder/plugins/variableexplorer/widgets/namespacebrowser.py
@@ -59,13 +59,14 @@ def __init__(self, parent):
         super().__init__(parent=parent, class_parent=parent)
 
         # Attributes
-        self.is_visible = True
         self.filename = None
+        self.text_finder = None
+        self.last_find = ''
+        self.finder_is_visible = False
 
         # Widgets
         self.editor = None
         self.shellwidget = None
-        self.finder = None
 
     def setup(self):
         """
@@ -94,35 +95,10 @@ def setup(self):
             self.editor.sig_editor_shown.connect(
                 self.sig_stop_spinner_requested)
 
-            # Finder
-            self.finder = QWidget(self)
-            text_finder = NamespacesBrowserFinder(
-                self.editor,
-                callback=self.editor.set_regex,
-                main=self,
-                regex_base=VALID_VARIABLE_CHARS,
-            )
-            close_button = self.create_toolbutton(
-                'close_finder',
-                triggered=self.sig_hide_finder_requested,
-                icon=self.create_icon('DialogCloseButton'),
-            )
-
-            self.editor.finder = text_finder
-            self.finder.text_finder = text_finder
-            self.finder.setVisible(False)
-
-            finder_layout = QHBoxLayout()
-            finder_layout.addWidget(close_button)
-            finder_layout.addWidget(text_finder)
-            finder_layout.setContentsMargins(0, 0, 0, 0)
-            self.finder.setLayout(finder_layout)
-
             # Layout
             layout = QVBoxLayout()
             layout.setContentsMargins(0, 0, 0, 0)
             layout.addWidget(self.editor)
-            layout.addWidget(self.finder)
             self.setLayout(layout)
 
     def get_view_settings(self):
@@ -138,30 +114,34 @@ def set_shellwidget(self, shellwidget):
         self.shellwidget = shellwidget
         shellwidget.set_namespacebrowser(self)
 
-    def show_finder(self, set_visible=False):
-        """Handle showing/hiding the finder widget."""
-        self.finder.text_finder.setText('')
-        self.finder.setVisible(set_visible)
+    def set_text_finder(self, text_finder):
+        """Bind NamespaceBrowsersFinder to namespace browser."""
+        self.text_finder = text_finder
+        if self.finder_is_visible:
+            self.text_finder.setText(self.last_find)
+        self.editor.finder = text_finder
 
-        if self.finder.isVisible():
-            self.finder.text_finder.setFocus()
-        else:
-            self.editor.setFocus()
+        return self.finder_is_visible
+
+    def save_finder_state(self, last_find, finder_visibility):
+        """Save last finder/search text input and finder visibility."""
+        if last_find and finder_visibility:
+            self.last_find = last_find
+        self.finder_is_visible = finder_visibility
 
     def refresh_table(self):
-        """Refresh variable table"""
-        if self.is_visible and self.isVisible():
-            self.shellwidget.refresh_namespacebrowser()
-            try:
-                self.editor.resizeRowToContents()
-            except TypeError:
-                pass
+        """Refresh variable table."""
+        self.shellwidget.refresh_namespacebrowser()
+        try:
+            self.editor.resizeRowToContents()
+        except TypeError:
+            pass
 
     def process_remote_view(self, remote_view):
         """Process remote view"""
         # To load all variables when a new filtering search is
         # started.
-        self.finder.text_finder.load_all = False
+        self.text_finder.load_all = False
 
         if remote_view is not None:
             self.set_data(remote_view)
@@ -297,6 +277,16 @@ class NamespacesBrowserFinder(FinderLineEdit):
     # To load all variables when filtering.
     load_all = False
 
+    def update_parent(self, parent, callback=None, main=None):
+        self._parent = parent
+        self.main = main
+        try:
+            self.textChanged.disconnect()
+        except TypeError:
+            pass
+        if callback:
+            self.textChanged.connect(callback)
+
     def load_all_variables(self):
         """Load all variables to correctly filter them."""
         if not self.load_all:
diff --git a/spyder/widgets/collectionseditor.py b/spyder/widgets/collectionseditor.py
--- a/spyder/widgets/collectionseditor.py
+++ b/spyder/widgets/collectionseditor.py
@@ -1408,6 +1408,7 @@ def __init__(self, parent, data, shellwidget=None, remote_editing=False,
         self.dictfilter = None
         self.delegate = None
         self.readonly = False
+        self.finder = None
 
         self.source_model = CollectionsModel(
             self, data, names=True,
@@ -1528,7 +1529,7 @@ def setup_menu(self):
 
     def set_regex(self, regex=None, reset=False):
         """Update the regex text for the variable finder."""
-        if reset or not self.finder.text():
+        if reset or self.finder is None or not self.finder.text():
             text = ''
         else:
             text = self.finder.text().replace(' ', '').lower()


Test patch:
diff --git a/spyder/plugins/variableexplorer/widgets/tests/test_namespacebrowser.py b/spyder/plugins/variableexplorer/widgets/tests/test_namespacebrowser.py
--- a/spyder/plugins/variableexplorer/widgets/tests/test_namespacebrowser.py
+++ b/spyder/plugins/variableexplorer/widgets/tests/test_namespacebrowser.py
@@ -24,7 +24,7 @@
 
 # Local imports
 from spyder.plugins.variableexplorer.widgets.namespacebrowser import (
-    NamespaceBrowser)
+    NamespaceBrowser, NamespacesBrowserFinder, VALID_VARIABLE_CHARS)
 from spyder.py3compat import PY2
 from spyder.widgets.collectionseditor import ROWS_TO_LOAD
 from spyder.widgets.tests.test_collectioneditor import data, data_table
@@ -153,6 +153,12 @@ def test_filtering_with_large_rows(qtbot):
     qtbot.addWidget(browser)
     browser.set_shellwidget(Mock())
     browser.setup()
+    text_finder = NamespacesBrowserFinder(
+        browser.editor,
+        callback=browser .editor.set_regex,
+        main=browser,
+        regex_base=VALID_VARIABLE_CHARS)
+    browser.set_text_finder(text_finder)
 
     # Create data
     variables = {}
@@ -174,15 +180,15 @@ def test_filtering_with_large_rows(qtbot):
     assert data(model, 49, 0) == 'e49'
 
     # Assert we can filter variables not loaded yet.
-    qtbot.keyClicks(browser.finder.text_finder, "t19")
+    qtbot.keyClicks(text_finder, "t19")
     assert model.rowCount() == 10
 
     # Assert all variables effectively start with 't19'.
     for i in range(10):
         assert data(model, i, 0) == 't19{}'.format(i)
 
-    # Hide finder widget in order to reset it.
-    browser.show_finder(set_visible=False)
+    # Reset text_finder widget.
+    text_finder.setText('')
 
     # Create a new variable that starts with a different letter than
     # the rest.
@@ -196,7 +202,7 @@ def test_filtering_with_large_rows(qtbot):
     browser.process_remote_view(new_variables)
 
     # Assert that can find 'z' among the declared variables.
-    qtbot.keyClicks(browser.finder.text_finder, "z")
+    qtbot.keyClicks(text_finder, "z")
     assert model.rowCount() == 1