Repository: spyder-ide/spyder
Pull request: PR: Add a way to create plugins like Plots and the Variable explorer to the API
<!--- Make sure to read the Contributing Guidelines:                   --->
<!--- https://github.com/spyder-ide/spyder/blob/master/CONTRIBUTING.md --->
<!--- and follow PEP 8, PEP 257 and Spyder's code style:               --->
<!--- https://github.com/spyder-ide/spyder/wiki/Dev:-Coding-Style      --->

## Description of Changes


* [ ] Wrote at least one-line docstrings (for any new functions)
* [ ] Added unit test(s) covering the changes (if testable)
<!--- Remember that an image/animation is worth a thousand words! --->
* [ ] Included a screenshot or animation (if affecting the UI, see [Licecap](https://www.cockos.com/licecap/))


<!--- Explain what you've done and why --->




### Issue(s) Resolved

<!--- List the issue(s) below, in the form "Fixes #1234"; one per line --->

Fixes #15921


### Affirmation

By submitting this Pull Request or typing my (user)name below,
I affirm the [Developer Certificate of Origin](https://developercertificate.org)
with respect to all commits and content included in this PR,
and understand I am releasing the same under Spyder's MIT (Expat) license.

<!--- TYPE YOUR USER/NAME AFTER THE FOLLOWING: --->
I certify the above statement is true and correct:

<!--- Thanks for your help making Spyder better for everyone! --->


Problem statement:
Consoles connected to external ipykernels break Plots and the Variable Explorer
## Problem Description
When switching to an external ipykernel, the plugins do not update correctly.

For example, the variable explorer would show the namespace of the previous console instead of the current one

I know that the variable explorer  is not expected to work for ipykernels, but it should not show the content of another console.


### What steps reproduce the problem?

1. In a terminal: `python -m ipykernel`
2. Open spyder with one console named "Console 1"
3. Console > Connect to existing console > paste the kernel number (`kernel-XXXX.json`)
4. A new console (Console 2) opens
5. Go to console one, type `a = 1`
6. Open the variable explorer and see that a is 1
7. Go to Console 2 (The external one)
8. The variable explorer hasn't changed, even though `a` is not defined

The same problem happens for the plot

### What is the expected output? What do you see instead?
I would expect to see an empty variable explorer (Or plot plugin). Instead I see the data from the previous console I visited.
(Of course the best case would be if the system works with ipkernels) 



## 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: dev
* Python version: 3.8.5
* Qt version: 
* PyQt version: 
* Operating System name/version: 

### Dependencies
<!--- Please go to the menu entry Help > Dependencies,
press the Copy to clipboard button and paste below --->

```
PASTE DEPENDENCIES HERE
```

Hints from issue discussion:

Commit message: Revert changed names

Patch to review:
diff --git a/spyder/plugins/help/plugin.py b/spyder/plugins/help/plugin.py
index 612a3f66280..b3d734c3574 100644
--- a/spyder/plugins/help/plugin.py
+++ b/spyder/plugins/help/plugin.py
@@ -184,7 +184,7 @@ def _setup_menus(self):
 
     # --- Public API
     # ------------------------------------------------------------------------
-    def set_shellwidget(self, shellwidget):
+    def set_shellwidget(self, shellwidget, external=None):
         """
         Set IPython Console `shelwidget` as the current shellwidget.
 
diff --git a/spyder/plugins/ipythonconsole/plugin.py b/spyder/plugins/ipythonconsole/plugin.py
index a870b8fbac5..ebdfbcaa818 100644
--- a/spyder/plugins/ipythonconsole/plugin.py
+++ b/spyder/plugins/ipythonconsole/plugin.py
@@ -85,24 +85,30 @@ class IPythonConsole(SpyderPluginWidget):
     sig_edit_goto_requested = Signal((str, int, str), (str, int, str, bool))
     sig_pdb_state_changed = Signal(bool, dict)
 
-    sig_shellwidget_created = Signal(object)
+    sig_shellwidget_created = Signal(object, bool)
     """
-    This signal is emitted when a shellwidget is created.
+    This signal is emitted when a shellwidget is connected to
+    a kernel.
 
     Parameters
     ----------
     shellwidget: spyder.plugins.ipyconsole.widgets.shell.ShellWidget
         The shellwigdet.
+    external: bool
+        True if the kernel is external
     """
 
-    sig_shellwidget_deleted = Signal(object)
+    sig_shellwidget_deleted = Signal(object, bool)
     """
-    This signal is emitted when a shellwidget is deleted/removed.
+    This signal is emitted when a shellwidget is disconnected from
+    a kernel.
 
     Parameters
     ----------
     shellwidget: spyder.plugins.ipyconsole.widgets.shell.ShellWidget
         The shellwigdet.
+    external: bool
+        True if the kernel is external
     """
 
     sig_shellwidget_changed = Signal(object)
@@ -583,7 +589,6 @@ def refresh_plugin(self):
 
         if client:
             sw = client.shellwidget
-            self.main.variableexplorer.set_shellwidget(sw)
             self.sig_pdb_state_changed.emit(
                 sw.is_waiting_pdb_input(), sw.get_pdb_last_step())
             self.sig_shellwidget_changed.emit(sw)
@@ -1611,18 +1616,6 @@ def connect_external_kernel(self, shellwidget):
         kc = shellwidget.kernel_client
         self.sig_shellwidget_changed.emit(sw)
 
-        if self.main.variableexplorer is not None:
-            self.main.variableexplorer.add_shellwidget(sw)
-            sw.set_namespace_view_settings()
-            sw.refresh_namespacebrowser()
-            kc.stopped_channels.connect(lambda :
-                self.main.variableexplorer.remove_shellwidget(id(sw)))
-
-        if self.main.plots is not None:
-            self.main.plots.add_shellwidget(sw)
-            kc.stopped_channels.connect(lambda :
-                self.main.plots.remove_shellwidget(id(sw)))
-
     #------ Public API (for tabs) ---------------------------------------------
     def add_tab(self, widget, name, filename=''):
         """Add tab"""
@@ -1797,16 +1790,10 @@ def _new_connection_file(self):
         return cf
 
     def shellwidget_started(self, client):
-        if self.main.variableexplorer is not None:
-            self.main.variableexplorer.add_shellwidget(client.shellwidget)
-
-        self.sig_shellwidget_created.emit(client.shellwidget)
+        self.sig_shellwidget_created.emit(client.shellwidget, False)
 
     def shellwidget_deleted(self, client):
-        if self.main.variableexplorer is not None:
-            self.main.variableexplorer.remove_shellwidget(client.shellwidget)
-
-        self.sig_shellwidget_deleted.emit(client.shellwidget)
+        self.sig_shellwidget_deleted.emit(client.shellwidget, False)
 
     def _create_client_for_kernel(self, connection_file, hostname, sshkey,
                                   password):
@@ -1828,7 +1815,7 @@ def _create_client_for_kernel(self, connection_file, hostname, sshkey,
         # (i.e. the i in i/A)
         master_id = None
         given_name = None
-        external_kernel = False
+        is_master = False
         slave_ord = ord('A') - 1
         kernel_manager = None
 
@@ -1849,7 +1836,7 @@ def _create_client_for_kernel(self, connection_file, hostname, sshkey,
         if master_id is None:
             self.master_clients += 1
             master_id = to_text_string(self.master_clients)
-            external_kernel = True
+            is_master = True
 
         # Set full client name
         client_id = dict(int_id=master_id,
@@ -1869,7 +1856,7 @@ def _create_client_for_kernel(self, connection_file, hostname, sshkey,
                               connection_file=connection_file,
                               menu_actions=self.menu_actions,
                               hostname=hostname,
-                              external_kernel=external_kernel,
+                              external_kernel=is_master,
                               slave=True,
                               show_elapsed_time=show_elapsed_time,
                               reset_warning=reset_warning,
@@ -1922,13 +1909,18 @@ def _create_client_for_kernel(self, connection_file, hostname, sshkey,
         shellwidget.sig_exception_occurred.connect(
             self.sig_exception_occurred)
 
-        if external_kernel:
+        if is_master:
             shellwidget.sig_is_spykernel.connect(
                 self.connect_external_kernel)
-            shellwidget.check_spyder_kernel()
+
+        # Here we notify about external shellwidgets
+        shellwidget.check_spyder_kernel()
+        self.sig_shellwidget_created.emit(shellwidget, True)
+        kernel_client.stopped_channels.connect(
+            lambda: self.sig_shellwidget_deleted.emit(shellwidget, True))
 
         # Set elapsed time, if possible
-        if not external_kernel:
+        if not is_master:
             self.set_elapsed_time(client)
 
         # Adding a new tab for the client
diff --git a/spyder/plugins/ipythonconsole/utils/shellconnect.py b/spyder/plugins/ipythonconsole/utils/shellconnect.py
new file mode 100644
index 00000000000..28d5ce161bb
--- /dev/null
+++ b/spyder/plugins/ipythonconsole/utils/shellconnect.py
@@ -0,0 +1,195 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © Spyder Project Contributors
+# Licensed under the terms of the MIT License
+# (see spyder/__init__.py for details)
+
+"""
+classes to connect to a shellwidget.
+"""
+
+# Third party imports
+from qtpy.QtWidgets import (
+    QStackedWidget, QVBoxLayout)
+
+# Local imports
+from spyder.api.config.decorators import on_conf_change
+from spyder.api.translations import get_translation
+from spyder.api.widgets.main_widget import PluginMainWidget
+
+# Localization
+_ = get_translation('spyder')
+
+
+class ShellConnectManager():
+    """
+    Manager to connect a stacked widget to a shell widget.
+
+    It is assumed that self.get_widget() returns a child of
+    StackedShellConnectWidget
+    """
+
+    def register_ipyconsole(self, ipyconsole):
+        """Connect to ipyconsole."""
+        # Signals
+        ipyconsole.sig_shellwidget_changed.connect(self.set_shellwidget)
+        ipyconsole.sig_shellwidget_created.connect(self.add_shellwidget)
+        ipyconsole.sig_shellwidget_deleted.connect(
+            self.remove_shellwidget)
+
+    def unregister_ipyconsole(self, ipyconsole):
+        """Disconnect from ipyconsole."""
+        # Signals
+        ipyconsole.sig_shellwidget_changed.disconnect(self.set_shellwidget)
+        ipyconsole.sig_shellwidget_created.disconnect(self.add_shellwidget)
+        ipyconsole.sig_shellwidget_deleted.disconnect(
+            self.remove_shellwidget)
+
+    # ---- Public API
+    # ------------------------------------------------------------------------
+    def set_shellwidget(self, shellwidget):
+        """
+        Update the current shellwidget.
+
+        Parameters
+        ----------
+        shellwidget: spyder.plugins.ipyconsole.widgets.shell.ShellWidget
+            The shell widget.
+        """
+        self.get_widget().set_shellwidget(shellwidget)
+
+    def add_shellwidget(self, shellwidget, external):
+        """
+        Add a new shellwidget to be registered.
+
+        This function registers a new NamespaceBrowser for browsing variables
+        in the shellwidget.
+
+        Parameters
+        ----------
+        shellwidget: spyder.plugins.ipyconsole.widgets.shell.ShellWidget
+            The shell widget.
+        external: bool
+            True if the kernel is external
+        """
+        self.get_widget().add_shellwidget(shellwidget)
+
+    def remove_shellwidget(self, shellwidget, external):
+        """
+        Remove the shellwidget registered.
+
+        Parameters
+        ----------
+        shellwidget: spyder.plugins.ipyconsole.widgets.shell.ShellWidget
+            The shell widget.
+        external: bool
+            True if the kernel is external
+        """
+        self.get_widget().remove_shellwidget(shellwidget)
+
+
+class StackedShellConnectWidget(PluginMainWidget):
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+
+        # Widgets
+        self._stack = QStackedWidget(self)
+        self._shellwidgets = {}
+
+        # Layout
+        layout = QVBoxLayout()
+        layout.setSpacing(0)
+        layout.setContentsMargins(0, 0, 0, 0)
+        layout.addWidget(self._stack)
+        self.setLayout(layout)
+
+    def update_style(self):
+        self._stack.setStyleSheet(
+            "QStackedWidget {padding: 0px; border: 0px}")
+
+    # ---- Stack accesors
+    # ------------------------------------------------------------------------
+
+    def count(self):
+        """
+        Return the number of widgets in the stack.
+
+        Returns
+        -------
+        int
+            The number of widgets in the stack.
+        """
+        return self._stack.count()
+
+    def current_widget(self):
+        """
+        Return the current figure browser widget in the stack.
+
+        Returns
+        -------
+        spyder.plugins.plots.widgets.figurebrowser.FigureBrowser
+            The current widget.
+        """
+        return self._stack.currentWidget()
+
+    def get_focus_widget(self):
+        return self.current_widget()
+
+    # ---- Public API
+    # ------------------------------------------------------------------------
+    def add_shellwidget(self, shellwidget):
+        """
+        Register shell.
+
+        This function creates a new Widget.
+        """
+        shellwidget_id = id(shellwidget)
+        if shellwidget_id not in self._shellwidgets:
+            nsb = self.new_widget(shellwidget)
+            self._stack.addWidget(nsb)
+            self._shellwidgets[shellwidget_id] = nsb
+            self.set_shellwidget(shellwidget)
+            self.update_actions()
+
+    def remove_shellwidget(self, shellwidget):
+        shellwidget_id = id(shellwidget)
+        if shellwidget_id in self._shellwidgets:
+            nsb = self._shellwidgets.pop(shellwidget_id)
+            self._stack.removeWidget(nsb)
+            self.close_widget(nsb)
+
+    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.switch_widget(nsb, old_nsb)
+            self._stack.setCurrentWidget(nsb)
+
+    def new_widget(self, shellwidget):
+        raise NotImplementedError
+
+    def close_widget(self, nsb):
+        raise NotImplementedError
+
+    def switch_widget(self, nsb, old_nsb):
+        raise NotImplementedError
+
+    def refresh(self):
+        if self.count():
+            nsb = self.current_widget()
+            nsb.refresh()
+
+    def update_actions(self):
+        nsb = self.current_widget()
+
+        for __, action in self.get_actions().items():
+            if action:
+                # IMPORTANT: Since we are defining the main actions in here
+                # and the context is WidgetWithChildrenShortcut we need to
+                # assign the same actions to the children widgets in order
+                # for shortcuts to work
+                if nsb:
+                    nsb_actions = nsb.actions()
+                    if action not in nsb_actions:
+                        nsb.addAction(action)
diff --git a/spyder/plugins/plots/plugin.py b/spyder/plugins/plots/plugin.py
index c8e1562425a..00e81e88730 100644
--- a/spyder/plugins/plots/plugin.py
+++ b/spyder/plugins/plots/plugin.py
@@ -15,13 +15,14 @@
 from spyder.api.plugins import Plugins, SpyderDockablePlugin
 from spyder.api.translations import get_translation
 from spyder.plugins.plots.widgets.main_widget import PlotsWidget
-
+from spyder.plugins.ipythonconsole.utils.shellconnect import (
+    ShellConnectManager)
 
 # Localization
 _ = get_translation('spyder')
 
 
-class Plots(SpyderDockablePlugin):
+class Plots(SpyderDockablePlugin, ShellConnectManager):
     """
     Plots plugin.
     """
@@ -49,11 +50,7 @@ def register(self):
         ipyconsole = self.get_plugin(Plugins.IPythonConsole)
 
         # Signals
-        ipyconsole.sig_shellwidget_changed.connect(self.set_shellwidget)
-        ipyconsole.sig_shellwidget_created.connect(
-            self.add_shellwidget)
-        ipyconsole.sig_shellwidget_deleted.connect(
-            self.remove_shellwidget)
+        self.register_ipyconsole(ipyconsole)
 
         # If a figure is loaded raise the dockwidget but do not give focus
         self.get_widget().sig_figure_loaded.connect(
@@ -64,10 +61,7 @@ def unregister(self):
         ipyconsole = self.get_plugin(Plugins.IPythonConsole)
 
         # Signals
-        ipyconsole.sig_shellwidget_created.disconnect(
-            self.add_shellwidget)
-        ipyconsole.sig_shellwidget_deleted.connect(
-            self.remove_shellwidget)
+        self.unregister_ipyconsole(ipyconsole)
 
     def switch_to_plugin(self, force_focus=False):
         # Only switch when inline plotting is muted. This avoids
@@ -89,39 +83,3 @@ def current_widget(self):
             The current figure browser widget.
         """
         return self.get_widget().current_widget()
-
-    def add_shellwidget(self, shellwidget):
-        """
-        Add a new shellwidget to be registered with the Plots plugin.
-
-        This function registers a new FigureBrowser for browsing the figures
-        in the shellwidget.
-
-        Parameters
-        ----------
-        shellwidget: spyder.plugins.ipyconsole.widgets.shell.ShellWidget
-            The shell widget.
-        """
-        self.get_widget().add_shellwidget(shellwidget)
-
-    def remove_shellwidget(self, shellwidget):
-        """
-        Remove the shellwidget registered with the plots plugin.
-
-        Parameters
-        ----------
-        shellwidget: spyder.plugins.ipyconsole.widgets.shell.ShellWidget
-            The shell widget.
-        """
-        self.get_widget().remove_shellwidget(shellwidget)
-
-    def set_shellwidget(self, shellwidget):
-        """
-        Update the current shellwidget displayed with the plots plugin.
-
-        Parameters
-        ----------
-        shellwidget: spyder.plugins.ipyconsole.widgets.shell.ShellWidget
-            The shell widget.
-        """
-        self.get_widget().set_shellwidget(shellwidget)
diff --git a/spyder/plugins/plots/widgets/main_widget.py b/spyder/plugins/plots/widgets/main_widget.py
index 954497a2076..3fb6f4bf45b 100644
--- a/spyder/plugins/plots/widgets/main_widget.py
+++ b/spyder/plugins/plots/widgets/main_widget.py
@@ -21,7 +21,8 @@
 from spyder.plugins.plots.widgets.figurebrowser import FigureBrowser
 from spyder.utils.misc import getcwd_or_home
 from spyder.utils.palette import QStylePalette
-
+from spyder.plugins.ipythonconsole.utils.shellconnect import (
+    StackedShellConnectWidget)
 
 # Localization
 _ = get_translation('spyder')
@@ -58,73 +59,7 @@ class PlotsWidgetMainToolbarSections:
 
 # --- Widgets
 # ----------------------------------------------------------------------------
-class PlotsStackedWidget(QStackedWidget, SpyderWidgetMixin):
-    # Signals
-    sig_thumbnail_menu_requested = Signal(QPoint, object)
-    """
-    This signal is emitted to request a context menu on the figure thumbnails.
-
-    Parameters
-    ----------
-    point: QPoint
-        The QPoint in global coordinates where the menu was requested.
-    figure_thumbnail: spyder.plugins.plots.widget.figurebrowser.FigureThumbnail
-        The clicked figure thumbnail.
-    """
-
-    sig_figure_menu_requested = Signal(QPoint)
-    """
-    This signal is emitted to request a context menu on the main figure
-    canvas.
-
-    Parameters
-    ----------
-    point: QPoint
-        The QPoint in global coordinates where the menu was requested.
-    """
-
-    sig_figure_loaded = Signal()
-    """This signal is emitted when a new figure is loaded."""
-
-    sig_save_dir_changed = Signal(str)
-    """
-    This signal is emitted to inform that the current folder where images are
-    saved has changed.
-
-    Parameters
-    ----------
-    save_dir: str
-        The new path where images are saved.
-    """
-
-    sig_zoom_changed = Signal(int)
-    """
-    This signal is emitted when zoom has changed.
-
-    Parameters
-    ----------
-    zoom_value: int
-        The new value for the zoom property.
-    """
-
-    def __init__(self, parent):
-        super().__init__(parent=parent, class_parent=parent)
-
-    def addWidget(self, widget):
-        """Override Qt method."""
-        if isinstance(widget, FigureBrowser):
-            widget.sig_figure_menu_requested.connect(
-                self.sig_figure_menu_requested)
-            widget.sig_thumbnail_menu_requested.connect(
-                self.sig_thumbnail_menu_requested)
-            widget.sig_figure_loaded.connect(self.sig_figure_loaded)
-            widget.sig_save_dir_changed.connect(self.sig_save_dir_changed)
-            widget.sig_zoom_changed.connect(self.sig_zoom_changed)
-
-        super().addWidget(widget)
-
-
-class PlotsWidget(PluginMainWidget):
+class PlotsWidget(StackedShellConnectWidget):
     sig_figure_loaded = Signal()
     """This signal is emitted when a figure is loaded succesfully"""
 
@@ -143,8 +78,6 @@ def __init__(self, name=None, plugin=None, parent=None):
         super().__init__(name, plugin, parent)
 
         # Widgets
-        self._stack = PlotsStackedWidget(parent=self)
-        self._shellwidgets = {}
         self.zoom_disp = QSpinBox(self)
         self._right_clicked_thumbnail = None
 
@@ -156,23 +89,6 @@ def __init__(self, name=None, plugin=None, parent=None):
         self.zoom_disp.setRange(0, 9999)
         self.zoom_disp.setValue(100)
 
-        # Layout
-        layout = QHBoxLayout()
-        layout.setSpacing(0)
-        layout.setContentsMargins(0, 0, 0, 0)
-        layout.addWidget(self._stack)
-        self.setLayout(layout)
-
-        # Signals
-        self._stack.sig_figure_loaded.connect(self.sig_figure_loaded)
-        self._stack.sig_figure_menu_requested.connect(self.show_figure_menu)
-        self._stack.sig_thumbnail_menu_requested.connect(
-            self.show_thumbnail_menu)
-        self._stack.sig_zoom_changed.connect(self.zoom_disp.setValue)
-        self._stack.sig_figure_loaded.connect(self.update_actions)
-        self._stack.sig_save_dir_changed.connect(
-            lambda val: self.set_conf('save_dir', val))
-
     # --- PluginMainWidget API
     # ------------------------------------------------------------------------
     def get_title(self):
@@ -361,121 +277,36 @@ def on_section_conf_change(self, option, value):
 
     # --- Public API:
     # ------------------------------------------------------------------------
-    def set_current_widget(self, fig_browser):
-        """
-        Set the current figure browser widget in the stack.
-
-        Parameters
-        ----------
-        fig_browser: spyder.plugins.plots.widgets.figurebrowser.FigureBrowser
-            The widget to set.
-        """
-        self._stack.setCurrentWidget(fig_browser)
-
-    def current_widget(self):
-        """
-        Return the current figure browser widget in the stack.
-
-        Returns
-        -------
-        spyder.plugins.plots.widgets.figurebrowser.FigureBrowser
-            The current widget.
-        """
-        return self._stack.currentWidget()
-
-    def count(self):
-        """
-        Return the number of widgets in the stack.
-
-        Returns
-        -------
-        int
-            The number of widgets in the stack.
-        """
-        return self._stack.count()
-
-    def remove_widget(self, fig_browser):
-        """
-        Remove widget from stack.
-
-        Parameters
-        ----------
-        fig_browser: spyder.plugins.plots.widgets.figurebrowser.FigureBrowser
-            The figure browser widget to remove.
-        """
-        self._stack.removeWidget(fig_browser)
-
-    def add_widget(self, fig_browser):
-        """
-        Add widget to stack.
-
-        Parameters
-        ----------
-        fig_browser: spyder.plugins.plots.widgets.figurebrowser.FigureBrowser
-            The figure browser widget to add.
-        """
-        self._stack.addWidget(fig_browser)
-
-    def add_shellwidget(self, shellwidget):
-        """
-        Add a new shellwidget registered with the plots plugin.
-
-        This function registers a new FigureBrowser for browsing the figures
-        in the shell.
-
-        Parameters
-        ----------
-        shelwidget: spyder.plugins.ipyconsole.widgets.shell.ShellWidget
-            The shell widget.
-        """
-        shellwidget_id = id(shellwidget)
-        if shellwidget_id not in self._shellwidgets:
-            fig_browser = FigureBrowser(parent=self._stack,
-                                        background_color=MAIN_BG_COLOR)
-            fig_browser.update_splitter_widths(self.width())
-            fig_browser.set_shellwidget(shellwidget)
-            fig_browser.sig_redirect_stdio_requested.connect(
-                self.sig_redirect_stdio_requested)
-            self.add_widget(fig_browser)
-            self._shellwidgets[shellwidget_id] = fig_browser
-            self.set_shellwidget(shellwidget)
-            return fig_browser
-
-    def remove_shellwidget(self, shellwidget):
-        """
-        Remove the shellwidget registered with the plots plugin.
 
-        Parameters
-        ----------
-        shelwidget: spyder.plugins.ipyconsole.widgets.shell.ShellWidget
-            The shell widget.
-        """
-        shellwidget_id = id(shellwidget)
-        if shellwidget_id in self._shellwidgets:
-            fig_browser = self._shellwidgets.pop(shellwidget_id)
-            self.remove_widget(fig_browser)
-            fig_browser.close()
+    def new_widget(self, shellwidget):
+        fig_browser = FigureBrowser(parent=self,
+                                    background_color=MAIN_BG_COLOR)
+        fig_browser.update_splitter_widths(self.width())
+        fig_browser.set_shellwidget(shellwidget)
+        fig_browser.sig_redirect_stdio_requested.connect(
+            self.sig_redirect_stdio_requested)
+
+        fig_browser.sig_figure_menu_requested.connect(
+            self.show_figure_menu)
+        fig_browser.sig_thumbnail_menu_requested.connect(
+            self.show_thumbnail_menu)
+        fig_browser.sig_figure_loaded.connect(self.update_actions)
+        fig_browser.sig_save_dir_changed.connect(
+            lambda val: self.set_conf('save_dir', val))
+        fig_browser.sig_zoom_changed.connect(self.zoom_disp.setValue)
+        return fig_browser
 
-    def set_shellwidget(self, shellwidget):
-        """
-        Update the current shellwidget displayed with the plots plugin.
+    def close_widget(self, fig_browser):
+        fig_browser.close()
 
-        Parameters
-        ----------
-        shelwidget: spyder.plugins.ipyconsole.widgets.shell.ShellWidget
-            The shell widget.
-        """
+    def switch_widget(self, fig_browser, old_fig_browser):
         option_keys = [('auto_fit_plotting', True),
                        ('mute_inline_plotting', True),
                        ('show_plot_outline', True),
                        ('save_dir', getcwd_or_home())]
 
         conf_values = {k: self.get_conf(k, d) for k, d in option_keys}
-        shellwidget_id = id(shellwidget)
-        if shellwidget_id in self._shellwidgets:
-            fig_browser = self._shellwidgets[shellwidget_id]
-            fig_browser.setup(conf_values)
-            self.set_current_widget(fig_browser)
+        fig_browser.setup(conf_values)
 
     def show_figure_menu(self, qpoint):
         """
diff --git a/spyder/plugins/variableexplorer/plugin.py b/spyder/plugins/variableexplorer/plugin.py
index eba4e72a1d3..b171afdf2fd 100644
--- a/spyder/plugins/variableexplorer/plugin.py
+++ b/spyder/plugins/variableexplorer/plugin.py
@@ -15,13 +15,14 @@
     VariableExplorerConfigPage)
 from spyder.plugins.variableexplorer.widgets.main_widget import (
     VariableExplorerWidget)
-
+from spyder.plugins.ipythonconsole.utils.shellconnect import (
+    ShellConnectManager)
 
 # Localization
 _ = get_translation('spyder')
 
 
-class VariableExplorer(SpyderDockablePlugin):
+class VariableExplorer(SpyderDockablePlugin, ShellConnectManager):
     """
     Variable explorer plugin.
     """
@@ -55,11 +56,7 @@ def register(self):
         preferences.register_plugin_preferences(self)
 
         # Signals
-        ipyconsole.sig_shellwidget_changed.connect(self.set_shellwidget)
-        ipyconsole.sig_shellwidget_created.connect(
-            self.add_shellwidget)
-        ipyconsole.sig_shellwidget_deleted.connect(
-            self.remove_shellwidget)
+        self.register_ipyconsole(ipyconsole)
 
         self.get_widget().sig_free_memory_requested.connect(
             self.sig_free_memory_requested)
@@ -69,11 +66,7 @@ def unregister(self):
         ipyconsole = self.get_plugin(Plugins.IPythonConsole)
 
         # Signals
-        ipyconsole.sig_shellwidget_changed.disconnect(self.set_shellwidget)
-        ipyconsole.sig_shellwidget_created.disconnect(
-            self.add_shellwidget)
-        ipyconsole.sig_shellwidget_deleted.disconnect(
-            self.remove_shellwidget)
+        self.unregister_ipyconsole(ipyconsole)
 
     # ---- Public API
     # ------------------------------------------------------------------------
@@ -87,20 +80,9 @@ def current_widget(self):
         """
         return self.get_widget().current_widget()
 
-    def set_shellwidget(self, shelwidget):
-        """
-        Update the current shellwidget associated to the Variable Explorer.
-
-        Parameters
-        ----------
-        shellwidget: spyder.plugins.ipyconsole.widgets.shell.ShellWidget
-            The shell widget.
-        """
-        self.get_widget().set_shellwidget(shelwidget)
-
-    def add_shellwidget(self, shelwidget):
+    def add_shellwidget(self, shellwidget, external):
         """
-        Add a new shellwidget to be registered with the Variable Explorer.
+        Add a new shellwidget to be registered.
 
         This function registers a new NamespaceBrowser for browsing variables
         in the shellwidget.
@@ -109,16 +91,11 @@ def add_shellwidget(self, shelwidget):
         ----------
         shellwidget: spyder.plugins.ipyconsole.widgets.shell.ShellWidget
             The shell widget.
+        external: bool
+            True if the kernel is external
         """
-        self.get_widget().add_shellwidget(shelwidget)
-
-    def remove_shellwidget(self, shelwidget):
-        """
-        Remove the shellwidget registered with the Variable Explorer.
+        if external:
+            shellwidget.set_namespace_view_settings()
+            shellwidget.refresh_namespacebrowser()
 
-        Parameters
-        ----------
-        shellwidget: spyder.plugins.ipyconsole.widgets.shell.ShellWidget
-            The shell widget.
-        """
-        self.get_widget().remove_shellwidget(shelwidget)
+        self.get_widget().add_shellwidget(shellwidget)
diff --git a/spyder/plugins/variableexplorer/widgets/main_widget.py b/spyder/plugins/variableexplorer/widgets/main_widget.py
index ee83cc3aade..f287f3000d3 100644
--- a/spyder/plugins/variableexplorer/widgets/main_widget.py
+++ b/spyder/plugins/variableexplorer/widgets/main_widget.py
@@ -20,6 +20,8 @@
 from spyder.plugins.variableexplorer.widgets.namespacebrowser import (
     NamespaceBrowser, NamespacesBrowserFinder, VALID_VARIABLE_CHARS)
 from spyder.utils.programs import is_module_installed
+from spyder.plugins.ipythonconsole.utils.shellconnect import (
+    StackedShellConnectWidget)
 
 # Localization
 _ = get_translation('spyder')
@@ -89,34 +91,8 @@ class VariableExplorerContextMenuSections:
 # =============================================================================
 # ---- Widgets
 # =============================================================================
-class NamespaceStackedWidget(QStackedWidget):
-    # Signals
-    sig_free_memory_requested = Signal()
-    sig_start_spinner_requested = Signal()
-    sig_stop_spinner_requested = Signal()
-    sig_hide_finder_requested = Signal()
-
-    def __init__(self, parent):
-        super().__init__(parent=parent)
-
-    def addWidget(self, widget):
-        """
-        Override Qt method.
-        """
-        if isinstance(widget, NamespaceBrowser):
-            widget.sig_free_memory_requested.connect(
-                self.sig_free_memory_requested)
-            widget.sig_start_spinner_requested.connect(
-                self.sig_start_spinner_requested)
-            widget.sig_stop_spinner_requested.connect(
-                self.sig_stop_spinner_requested)
-            widget.sig_hide_finder_requested.connect(
-                self.sig_hide_finder_requested)
-
-        super().addWidget(widget)
-
 
-class VariableExplorerWidget(PluginMainWidget):
+class VariableExplorerWidget(StackedShellConnectWidget):
 
     # PluginMainWidget class constants
     ENABLE_SPINNER = True
@@ -132,35 +108,17 @@ def __init__(self, name=None, plugin=None, parent=None):
         super().__init__(name, plugin, parent)
 
         # Widgets
-        self._stack = NamespaceStackedWidget(self)
-        self._shellwidgets = {}
         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
-        self._stack.sig_free_memory_requested.connect(self.free_memory)
-        self._stack.sig_start_spinner_requested.connect(self.start_spinner)
-        self._stack.sig_stop_spinner_requested.connect(self.stop_spinner)
-        self._stack.sig_hide_finder_requested.connect(self.hide_finder)
-
     # ---- PluginMainWidget API
     # ------------------------------------------------------------------------
     def get_title(self):
         return _('Variable Explorer')
 
-    def get_focus_widget(self):
-        return self.current_widget()
-
     def setup(self):
         # ---- Options menu actions
         exclude_private_action = self.create_action(
@@ -421,10 +379,6 @@ def setup(self):
                 section=VariableExplorerContextMenuSections.Edit,
             )
 
-    def update_style(self):
-        self._stack.setStyleSheet(
-            "NamespaceStackedWidget {padding: 0px; border: 0px}")
-
     def update_actions(self):
         action = self.get_action(VariableExplorerWidgetActions.ToggleMinMax)
         action.setEnabled(is_module_installed('numpy'))
@@ -454,18 +408,6 @@ def on_section_conf_change(self, section):
 
     # ---- Stack accesors
     # ------------------------------------------------------------------------
-    def add_widget(self, nsb):
-        self._stack.addWidget(nsb)
-
-    def count(self):
-        return self._stack.count()
-
-    def current_widget(self):
-        return self._stack.currentWidget()
-
-    def remove_widget(self, nsb):
-        self._stack.removeWidget(nsb)
-
     def update_finder(self, nsb, old_nsb):
         """Initialize or update finder widget."""
         if self.finder is None:
@@ -504,7 +446,7 @@ def update_finder(self, nsb, old_nsb):
                 main=nsb,
             )
 
-    def set_current_widget(self, nsb, old_nsb):
+    def switch_widget(self, nsb, old_nsb):
         """
         Set the current NamespaceBrowser.
 
@@ -513,46 +455,30 @@ def set_current_widget(self, nsb, old_nsb):
         """
         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
     # ------------------------------------------------------------------------
-    def add_shellwidget(self, shellwidget):
-        """
-        Register shell with variable explorer.
 
-        This function creates a new NamespaceBrowser for browsing
-        variables in the shell.
-        """
-        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, old_nsb)
-            self.update_actions()
-            return nsb
-
-    def remove_shellwidget(self, shellwidget):
-        shellwidget_id = id(shellwidget)
-        if shellwidget_id in self._shellwidgets:
-            nsb = self._shellwidgets.pop(shellwidget_id)
-            self.remove_widget(nsb)
-            nsb.close()
-
-    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, old_nsb)
+    def new_widget(self, shellwidget):
+        nsb = NamespaceBrowser(self)
+        nsb.set_shellwidget(shellwidget)
+        nsb.setup()
+        nsb.sig_free_memory_requested.connect(
+            self.free_memory)
+        nsb.sig_start_spinner_requested.connect(
+            self.start_spinner)
+        nsb.sig_stop_spinner_requested.connect(
+            self.stop_spinner)
+        nsb.sig_hide_finder_requested.connect(
+            self.hide_finder)
+        self._set_actions_and_menus(nsb)
+        return nsb
+
+    def close_widget(self, nsb):
+        nsb.close()
 
     def import_data(self, filenames=None):
         """


Code review comments:
diff_hunk: @@ -0,0 +1,195 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © Spyder Project Contributors
+# Licensed under the terms of the MIT License
+# (see spyder/__init__.py for details)
+
+"""
+classes to connect to a shellwidget.
+"""
+
+# Third party imports
+from qtpy.QtWidgets import (
+    QStackedWidget, QVBoxLayout)
+
+# Local imports
+from spyder.api.config.decorators import on_conf_change
+from spyder.api.translations import get_translation
+from spyder.api.widgets.main_widget import PluginMainWidget
+
+# Localization
+_ = get_translation('spyder')
+
+
+class ShellConnectManager():
path: spyder/plugins/ipythonconsole/utils/shellconnect.py
text: @user1:
* Let's rename this to `ShellConnectMixin`
* Also, no need to have parenthesis here.

diff_hunk: @@ -0,0 +1,195 @@
+# -*- coding: utf-8 -*-
path: spyder/plugins/ipythonconsole/utils/shellconnect.py
text: @user1:
I think this file should be moved to `spyder/api/shellconnect` and split it in two:

* `ShellConnectMixin` (see below) should be in `spyder/api/shellconnect/mixins.py`
* `ShellConnectMainWidget` (see below) should be in `spyder/api/shellconnect/main_widget.py`

That's because this code can be used by any plugin that works like the Variable Explorer or Plots, which I think it's a great idea!

diff_hunk: @@ -0,0 +1,195 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © Spyder Project Contributors
+# Licensed under the terms of the MIT License
+# (see spyder/__init__.py for details)
+
+"""
+classes to connect to a shellwidget.
+"""
+
+# Third party imports
+from qtpy.QtWidgets import (
+    QStackedWidget, QVBoxLayout)
+
+# Local imports
+from spyder.api.config.decorators import on_conf_change
+from spyder.api.translations import get_translation
+from spyder.api.widgets.main_widget import PluginMainWidget
+
+# Localization
+_ = get_translation('spyder')
+
+
+class ShellConnectManager():
+    """
+    Manager to connect a stacked widget to a shell widget.
+
+    It is assumed that self.get_widget() returns a child of
+    StackedShellConnectWidget
+    """
+
+    def register_ipyconsole(self, ipyconsole):
+        """Connect to ipyconsole."""
+        # Signals
+        ipyconsole.sig_shellwidget_changed.connect(self.set_shellwidget)
+        ipyconsole.sig_shellwidget_created.connect(self.add_shellwidget)
+        ipyconsole.sig_shellwidget_deleted.connect(
+            self.remove_shellwidget)
+
+    def unregister_ipyconsole(self, ipyconsole):
+        """Disconnect from ipyconsole."""
+        # Signals
+        ipyconsole.sig_shellwidget_changed.disconnect(self.set_shellwidget)
+        ipyconsole.sig_shellwidget_created.disconnect(self.add_shellwidget)
+        ipyconsole.sig_shellwidget_deleted.disconnect(
+            self.remove_shellwidget)
+
+    # ---- Public API
+    # ------------------------------------------------------------------------
+    def set_shellwidget(self, shellwidget):
+        """
+        Update the current shellwidget.
+
+        Parameters
+        ----------
+        shellwidget: spyder.plugins.ipyconsole.widgets.shell.ShellWidget
+            The shell widget.
+        """
+        self.get_widget().set_shellwidget(shellwidget)
+
+    def add_shellwidget(self, shellwidget, external):
+        """
+        Add a new shellwidget to be registered.
+
+        This function registers a new NamespaceBrowser for browsing variables
+        in the shellwidget.
+
+        Parameters
+        ----------
+        shellwidget: spyder.plugins.ipyconsole.widgets.shell.ShellWidget
+            The shell widget.
+        external: bool
+            True if the kernel is external
+        """
+        self.get_widget().add_shellwidget(shellwidget)
+
+    def remove_shellwidget(self, shellwidget, external):
+        """
+        Remove the shellwidget registered.
+
+        Parameters
+        ----------
+        shellwidget: spyder.plugins.ipyconsole.widgets.shell.ShellWidget
+            The shell widget.
+        external: bool
+            True if the kernel is external
+        """
+        self.get_widget().remove_shellwidget(shellwidget)
+
+
+class StackedShellConnectWidget(PluginMainWidget):
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+
+        # Widgets
+        self._stack = QStackedWidget(self)
+        self._shellwidgets = {}
+
+        # Layout
+        layout = QVBoxLayout()
+        layout.setSpacing(0)
+        layout.setContentsMargins(0, 0, 0, 0)
+        layout.addWidget(self._stack)
+        self.setLayout(layout)
+
+    def update_style(self):
+        self._stack.setStyleSheet(
+            "QStackedWidget {padding: 0px; border: 0px}")
+
+    # ---- Stack accesors
+    # ------------------------------------------------------------------------
+
path: spyder/plugins/ipythonconsole/utils/shellconnect.py
text: @user1:
Remove this line.

diff_hunk: @@ -0,0 +1,195 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © Spyder Project Contributors
+# Licensed under the terms of the MIT License
+# (see spyder/__init__.py for details)
+
+"""
+classes to connect to a shellwidget.
+"""
+
+# Third party imports
+from qtpy.QtWidgets import (
+    QStackedWidget, QVBoxLayout)
+
+# Local imports
+from spyder.api.config.decorators import on_conf_change
+from spyder.api.translations import get_translation
+from spyder.api.widgets.main_widget import PluginMainWidget
+
+# Localization
+_ = get_translation('spyder')
+
+
+class ShellConnectManager():
+    """
+    Manager to connect a stacked widget to a shell widget.
+
+    It is assumed that self.get_widget() returns a child of
+    StackedShellConnectWidget
+    """
+
+    def register_ipyconsole(self, ipyconsole):
+        """Connect to ipyconsole."""
+        # Signals
+        ipyconsole.sig_shellwidget_changed.connect(self.set_shellwidget)
+        ipyconsole.sig_shellwidget_created.connect(self.add_shellwidget)
+        ipyconsole.sig_shellwidget_deleted.connect(
+            self.remove_shellwidget)
+
+    def unregister_ipyconsole(self, ipyconsole):
+        """Disconnect from ipyconsole."""
+        # Signals
+        ipyconsole.sig_shellwidget_changed.disconnect(self.set_shellwidget)
+        ipyconsole.sig_shellwidget_created.disconnect(self.add_shellwidget)
+        ipyconsole.sig_shellwidget_deleted.disconnect(
+            self.remove_shellwidget)
+
+    # ---- Public API
+    # ------------------------------------------------------------------------
+    def set_shellwidget(self, shellwidget):
+        """
+        Update the current shellwidget.
+
+        Parameters
+        ----------
+        shellwidget: spyder.plugins.ipyconsole.widgets.shell.ShellWidget
+            The shell widget.
+        """
+        self.get_widget().set_shellwidget(shellwidget)
+
+    def add_shellwidget(self, shellwidget, external):
+        """
+        Add a new shellwidget to be registered.
+
+        This function registers a new NamespaceBrowser for browsing variables
+        in the shellwidget.
+
+        Parameters
+        ----------
+        shellwidget: spyder.plugins.ipyconsole.widgets.shell.ShellWidget
+            The shell widget.
+        external: bool
+            True if the kernel is external
+        """
+        self.get_widget().add_shellwidget(shellwidget)
+
+    def remove_shellwidget(self, shellwidget, external):
+        """
+        Remove the shellwidget registered.
+
+        Parameters
+        ----------
+        shellwidget: spyder.plugins.ipyconsole.widgets.shell.ShellWidget
+            The shell widget.
+        external: bool
+            True if the kernel is external
+        """
+        self.get_widget().remove_shellwidget(shellwidget)
+
+
+class StackedShellConnectWidget(PluginMainWidget):
path: spyder/plugins/ipythonconsole/utils/shellconnect.py
text: @user1:
* Let's rename this to ShellConnectMainWidget.
* Please add a docstring to it.

diff_hunk: @@ -0,0 +1,195 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © Spyder Project Contributors
+# Licensed under the terms of the MIT License
+# (see spyder/__init__.py for details)
+
+"""
+classes to connect to a shellwidget.
+"""
+
+# Third party imports
+from qtpy.QtWidgets import (
+    QStackedWidget, QVBoxLayout)
+
+# Local imports
+from spyder.api.config.decorators import on_conf_change
+from spyder.api.translations import get_translation
+from spyder.api.widgets.main_widget import PluginMainWidget
+
+# Localization
+_ = get_translation('spyder')
+
+
+class ShellConnectManager():
+    """
+    Manager to connect a stacked widget to a shell widget.
+
+    It is assumed that self.get_widget() returns a child of
+    StackedShellConnectWidget
+    """
+
+    def register_ipyconsole(self, ipyconsole):
+        """Connect to ipyconsole."""
+        # Signals
+        ipyconsole.sig_shellwidget_changed.connect(self.set_shellwidget)
+        ipyconsole.sig_shellwidget_created.connect(self.add_shellwidget)
+        ipyconsole.sig_shellwidget_deleted.connect(
+            self.remove_shellwidget)
+
+    def unregister_ipyconsole(self, ipyconsole):
+        """Disconnect from ipyconsole."""
+        # Signals
+        ipyconsole.sig_shellwidget_changed.disconnect(self.set_shellwidget)
+        ipyconsole.sig_shellwidget_created.disconnect(self.add_shellwidget)
+        ipyconsole.sig_shellwidget_deleted.disconnect(
+            self.remove_shellwidget)
+
+    # ---- Public API
+    # ------------------------------------------------------------------------
+    def set_shellwidget(self, shellwidget):
+        """
+        Update the current shellwidget.
+
+        Parameters
+        ----------
+        shellwidget: spyder.plugins.ipyconsole.widgets.shell.ShellWidget
+            The shell widget.
+        """
+        self.get_widget().set_shellwidget(shellwidget)
+
+    def add_shellwidget(self, shellwidget, external):
+        """
+        Add a new shellwidget to be registered.
+
+        This function registers a new NamespaceBrowser for browsing variables
+        in the shellwidget.
+
+        Parameters
+        ----------
+        shellwidget: spyder.plugins.ipyconsole.widgets.shell.ShellWidget
+            The shell widget.
+        external: bool
+            True if the kernel is external
+        """
+        self.get_widget().add_shellwidget(shellwidget)
+
+    def remove_shellwidget(self, shellwidget, external):
+        """
+        Remove the shellwidget registered.
+
+        Parameters
+        ----------
+        shellwidget: spyder.plugins.ipyconsole.widgets.shell.ShellWidget
+            The shell widget.
+        external: bool
+            True if the kernel is external
+        """
+        self.get_widget().remove_shellwidget(shellwidget)
+
+
+class StackedShellConnectWidget(PluginMainWidget):
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+
+        # Widgets
+        self._stack = QStackedWidget(self)
+        self._shellwidgets = {}
+
+        # Layout
+        layout = QVBoxLayout()
+        layout.setSpacing(0)
+        layout.setContentsMargins(0, 0, 0, 0)
+        layout.addWidget(self._stack)
+        self.setLayout(layout)
+
+    def update_style(self):
+        self._stack.setStyleSheet(
+            "QStackedWidget {padding: 0px; border: 0px}")
+
+    # ---- Stack accesors
+    # ------------------------------------------------------------------------
+
+    def count(self):
+        """
+        Return the number of widgets in the stack.
+
+        Returns
+        -------
+        int
+            The number of widgets in the stack.
+        """
+        return self._stack.count()
+
+    def current_widget(self):
+        """
+        Return the current figure browser widget in the stack.
+
+        Returns
+        -------
+        spyder.plugins.plots.widgets.figurebrowser.FigureBrowser
+            The current widget.
+        """
+        return self._stack.currentWidget()
+
+    def get_focus_widget(self):
+        return self.current_widget()
+
+    # ---- Public API
+    # ------------------------------------------------------------------------
+    def add_shellwidget(self, shellwidget):
+        """
+        Register shell.
+
+        This function creates a new Widget.
+        """
+        shellwidget_id = id(shellwidget)
+        if shellwidget_id not in self._shellwidgets:
+            nsb = self.new_widget(shellwidget)
+            self._stack.addWidget(nsb)
+            self._shellwidgets[shellwidget_id] = nsb
path: spyder/plugins/ipythonconsole/utils/shellconnect.py
text: @user1:
```suggestion
            widget = self.create_new_widget(shellwidget)
            self._stack.addWidget(widget)
            self._shellwidgets[shellwidget_id] = widget
```

diff_hunk: @@ -0,0 +1,195 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © Spyder Project Contributors
+# Licensed under the terms of the MIT License
+# (see spyder/__init__.py for details)
+
+"""
+classes to connect to a shellwidget.
+"""
+
+# Third party imports
+from qtpy.QtWidgets import (
+    QStackedWidget, QVBoxLayout)
+
+# Local imports
+from spyder.api.config.decorators import on_conf_change
+from spyder.api.translations import get_translation
+from spyder.api.widgets.main_widget import PluginMainWidget
+
+# Localization
+_ = get_translation('spyder')
+
+
+class ShellConnectManager():
+    """
+    Manager to connect a stacked widget to a shell widget.
+
+    It is assumed that self.get_widget() returns a child of
+    StackedShellConnectWidget
+    """
+
+    def register_ipyconsole(self, ipyconsole):
+        """Connect to ipyconsole."""
+        # Signals
+        ipyconsole.sig_shellwidget_changed.connect(self.set_shellwidget)
+        ipyconsole.sig_shellwidget_created.connect(self.add_shellwidget)
+        ipyconsole.sig_shellwidget_deleted.connect(
+            self.remove_shellwidget)
+
+    def unregister_ipyconsole(self, ipyconsole):
+        """Disconnect from ipyconsole."""
+        # Signals
+        ipyconsole.sig_shellwidget_changed.disconnect(self.set_shellwidget)
+        ipyconsole.sig_shellwidget_created.disconnect(self.add_shellwidget)
+        ipyconsole.sig_shellwidget_deleted.disconnect(
+            self.remove_shellwidget)
+
+    # ---- Public API
+    # ------------------------------------------------------------------------
+    def set_shellwidget(self, shellwidget):
+        """
+        Update the current shellwidget.
+
+        Parameters
+        ----------
+        shellwidget: spyder.plugins.ipyconsole.widgets.shell.ShellWidget
+            The shell widget.
+        """
+        self.get_widget().set_shellwidget(shellwidget)
+
+    def add_shellwidget(self, shellwidget, external):
+        """
+        Add a new shellwidget to be registered.
+
+        This function registers a new NamespaceBrowser for browsing variables
+        in the shellwidget.
+
+        Parameters
+        ----------
+        shellwidget: spyder.plugins.ipyconsole.widgets.shell.ShellWidget
+            The shell widget.
+        external: bool
+            True if the kernel is external
+        """
+        self.get_widget().add_shellwidget(shellwidget)
+
+    def remove_shellwidget(self, shellwidget, external):
+        """
+        Remove the shellwidget registered.
+
+        Parameters
+        ----------
+        shellwidget: spyder.plugins.ipyconsole.widgets.shell.ShellWidget
+            The shell widget.
+        external: bool
+            True if the kernel is external
+        """
+        self.get_widget().remove_shellwidget(shellwidget)
+
+
+class StackedShellConnectWidget(PluginMainWidget):
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+
+        # Widgets
+        self._stack = QStackedWidget(self)
+        self._shellwidgets = {}
+
+        # Layout
+        layout = QVBoxLayout()
+        layout.setSpacing(0)
+        layout.setContentsMargins(0, 0, 0, 0)
+        layout.addWidget(self._stack)
+        self.setLayout(layout)
+
+    def update_style(self):
+        self._stack.setStyleSheet(
+            "QStackedWidget {padding: 0px; border: 0px}")
+
+    # ---- Stack accesors
+    # ------------------------------------------------------------------------
+
+    def count(self):
+        """
+        Return the number of widgets in the stack.
+
+        Returns
+        -------
+        int
+            The number of widgets in the stack.
+        """
+        return self._stack.count()
+
+    def current_widget(self):
+        """
+        Return the current figure browser widget in the stack.
+
+        Returns
+        -------
+        spyder.plugins.plots.widgets.figurebrowser.FigureBrowser
+            The current widget.
+        """
+        return self._stack.currentWidget()
+
+    def get_focus_widget(self):
+        return self.current_widget()
+
+    # ---- Public API
+    # ------------------------------------------------------------------------
+    def add_shellwidget(self, shellwidget):
+        """
+        Register shell.
+
+        This function creates a new Widget.
+        """
+        shellwidget_id = id(shellwidget)
+        if shellwidget_id not in self._shellwidgets:
+            nsb = self.new_widget(shellwidget)
+            self._stack.addWidget(nsb)
+            self._shellwidgets[shellwidget_id] = nsb
+            self.set_shellwidget(shellwidget)
+            self.update_actions()
+
+    def remove_shellwidget(self, shellwidget):
+        shellwidget_id = id(shellwidget)
+        if shellwidget_id in self._shellwidgets:
+            nsb = self._shellwidgets.pop(shellwidget_id)
+            self._stack.removeWidget(nsb)
path: spyder/plugins/ipythonconsole/utils/shellconnect.py
text: @user1:
```suggestion
            widget = self._shellwidgets.pop(shellwidget_id)
            self._stack.removeWidget(widget)
```

diff_hunk: @@ -0,0 +1,195 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © Spyder Project Contributors
+# Licensed under the terms of the MIT License
+# (see spyder/__init__.py for details)
+
+"""
+classes to connect to a shellwidget.
+"""
+
+# Third party imports
+from qtpy.QtWidgets import (
+    QStackedWidget, QVBoxLayout)
+
+# Local imports
+from spyder.api.config.decorators import on_conf_change
+from spyder.api.translations import get_translation
+from spyder.api.widgets.main_widget import PluginMainWidget
+
+# Localization
+_ = get_translation('spyder')
+
+
+class ShellConnectManager():
+    """
+    Manager to connect a stacked widget to a shell widget.
+
+    It is assumed that self.get_widget() returns a child of
+    StackedShellConnectWidget
+    """
+
+    def register_ipyconsole(self, ipyconsole):
+        """Connect to ipyconsole."""
+        # Signals
+        ipyconsole.sig_shellwidget_changed.connect(self.set_shellwidget)
+        ipyconsole.sig_shellwidget_created.connect(self.add_shellwidget)
+        ipyconsole.sig_shellwidget_deleted.connect(
+            self.remove_shellwidget)
+
+    def unregister_ipyconsole(self, ipyconsole):
+        """Disconnect from ipyconsole."""
+        # Signals
+        ipyconsole.sig_shellwidget_changed.disconnect(self.set_shellwidget)
+        ipyconsole.sig_shellwidget_created.disconnect(self.add_shellwidget)
+        ipyconsole.sig_shellwidget_deleted.disconnect(
+            self.remove_shellwidget)
+
+    # ---- Public API
+    # ------------------------------------------------------------------------
+    def set_shellwidget(self, shellwidget):
+        """
+        Update the current shellwidget.
+
+        Parameters
+        ----------
+        shellwidget: spyder.plugins.ipyconsole.widgets.shell.ShellWidget
+            The shell widget.
+        """
+        self.get_widget().set_shellwidget(shellwidget)
+
+    def add_shellwidget(self, shellwidget, external):
+        """
+        Add a new shellwidget to be registered.
+
+        This function registers a new NamespaceBrowser for browsing variables
+        in the shellwidget.
+
+        Parameters
+        ----------
+        shellwidget: spyder.plugins.ipyconsole.widgets.shell.ShellWidget
+            The shell widget.
+        external: bool
+            True if the kernel is external
+        """
+        self.get_widget().add_shellwidget(shellwidget)
+
+    def remove_shellwidget(self, shellwidget, external):
+        """
+        Remove the shellwidget registered.
+
+        Parameters
+        ----------
+        shellwidget: spyder.plugins.ipyconsole.widgets.shell.ShellWidget
+            The shell widget.
+        external: bool
+            True if the kernel is external
+        """
+        self.get_widget().remove_shellwidget(shellwidget)
+
+
+class StackedShellConnectWidget(PluginMainWidget):
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+
+        # Widgets
+        self._stack = QStackedWidget(self)
+        self._shellwidgets = {}
+
+        # Layout
+        layout = QVBoxLayout()
+        layout.setSpacing(0)
+        layout.setContentsMargins(0, 0, 0, 0)
+        layout.addWidget(self._stack)
+        self.setLayout(layout)
+
+    def update_style(self):
+        self._stack.setStyleSheet(
+            "QStackedWidget {padding: 0px; border: 0px}")
+
+    # ---- Stack accesors
+    # ------------------------------------------------------------------------
+
+    def count(self):
+        """
+        Return the number of widgets in the stack.
+
+        Returns
+        -------
+        int
+            The number of widgets in the stack.
+        """
+        return self._stack.count()
+
+    def current_widget(self):
+        """
+        Return the current figure browser widget in the stack.
+
+        Returns
+        -------
+        spyder.plugins.plots.widgets.figurebrowser.FigureBrowser
+            The current widget.
+        """
+        return self._stack.currentWidget()
+
+    def get_focus_widget(self):
+        return self.current_widget()
+
+    # ---- Public API
+    # ------------------------------------------------------------------------
+    def add_shellwidget(self, shellwidget):
+        """
+        Register shell.
+
+        This function creates a new Widget.
+        """
+        shellwidget_id = id(shellwidget)
+        if shellwidget_id not in self._shellwidgets:
+            nsb = self.new_widget(shellwidget)
+            self._stack.addWidget(nsb)
+            self._shellwidgets[shellwidget_id] = nsb
+            self.set_shellwidget(shellwidget)
+            self.update_actions()
+
+    def remove_shellwidget(self, shellwidget):
+        shellwidget_id = id(shellwidget)
+        if shellwidget_id in self._shellwidgets:
+            nsb = self._shellwidgets.pop(shellwidget_id)
+            self._stack.removeWidget(nsb)
+            self.close_widget(nsb)
+
+    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.switch_widget(nsb, old_nsb)
+            self._stack.setCurrentWidget(nsb)
path: spyder/plugins/ipythonconsole/utils/shellconnect.py
text: @user1:
```suggestion
        old_widget = self.current_widget()
        if shellwidget_id in self._shellwidgets:
            widget = self._shellwidgets[shellwidget_id]
            self.switch_widget(widget, old_widget)
            self._stack.setCurrentWidget(widget)
```

diff_hunk: @@ -0,0 +1,195 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © Spyder Project Contributors
+# Licensed under the terms of the MIT License
+# (see spyder/__init__.py for details)
+
+"""
+classes to connect to a shellwidget.
+"""
+
+# Third party imports
+from qtpy.QtWidgets import (
+    QStackedWidget, QVBoxLayout)
+
+# Local imports
+from spyder.api.config.decorators import on_conf_change
+from spyder.api.translations import get_translation
+from spyder.api.widgets.main_widget import PluginMainWidget
+
+# Localization
+_ = get_translation('spyder')
+
+
+class ShellConnectManager():
+    """
+    Manager to connect a stacked widget to a shell widget.
+
+    It is assumed that self.get_widget() returns a child of
+    StackedShellConnectWidget
+    """
+
+    def register_ipyconsole(self, ipyconsole):
+        """Connect to ipyconsole."""
+        # Signals
+        ipyconsole.sig_shellwidget_changed.connect(self.set_shellwidget)
+        ipyconsole.sig_shellwidget_created.connect(self.add_shellwidget)
+        ipyconsole.sig_shellwidget_deleted.connect(
+            self.remove_shellwidget)
+
+    def unregister_ipyconsole(self, ipyconsole):
+        """Disconnect from ipyconsole."""
+        # Signals
+        ipyconsole.sig_shellwidget_changed.disconnect(self.set_shellwidget)
+        ipyconsole.sig_shellwidget_created.disconnect(self.add_shellwidget)
+        ipyconsole.sig_shellwidget_deleted.disconnect(
+            self.remove_shellwidget)
+
+    # ---- Public API
+    # ------------------------------------------------------------------------
+    def set_shellwidget(self, shellwidget):
+        """
+        Update the current shellwidget.
+
+        Parameters
+        ----------
+        shellwidget: spyder.plugins.ipyconsole.widgets.shell.ShellWidget
+            The shell widget.
+        """
+        self.get_widget().set_shellwidget(shellwidget)
+
+    def add_shellwidget(self, shellwidget, external):
+        """
+        Add a new shellwidget to be registered.
+
+        This function registers a new NamespaceBrowser for browsing variables
+        in the shellwidget.
+
+        Parameters
+        ----------
+        shellwidget: spyder.plugins.ipyconsole.widgets.shell.ShellWidget
+            The shell widget.
+        external: bool
+            True if the kernel is external
+        """
+        self.get_widget().add_shellwidget(shellwidget)
+
+    def remove_shellwidget(self, shellwidget, external):
+        """
+        Remove the shellwidget registered.
+
+        Parameters
+        ----------
+        shellwidget: spyder.plugins.ipyconsole.widgets.shell.ShellWidget
+            The shell widget.
+        external: bool
+            True if the kernel is external
+        """
+        self.get_widget().remove_shellwidget(shellwidget)
+
+
+class StackedShellConnectWidget(PluginMainWidget):
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+
+        # Widgets
+        self._stack = QStackedWidget(self)
+        self._shellwidgets = {}
+
+        # Layout
+        layout = QVBoxLayout()
+        layout.setSpacing(0)
+        layout.setContentsMargins(0, 0, 0, 0)
+        layout.addWidget(self._stack)
+        self.setLayout(layout)
+
+    def update_style(self):
+        self._stack.setStyleSheet(
+            "QStackedWidget {padding: 0px; border: 0px}")
+
+    # ---- Stack accesors
+    # ------------------------------------------------------------------------
+
+    def count(self):
+        """
+        Return the number of widgets in the stack.
+
+        Returns
+        -------
+        int
+            The number of widgets in the stack.
+        """
+        return self._stack.count()
+
+    def current_widget(self):
+        """
+        Return the current figure browser widget in the stack.
+
+        Returns
+        -------
+        spyder.plugins.plots.widgets.figurebrowser.FigureBrowser
+            The current widget.
+        """
+        return self._stack.currentWidget()
+
+    def get_focus_widget(self):
+        return self.current_widget()
+
+    # ---- Public API
+    # ------------------------------------------------------------------------
+    def add_shellwidget(self, shellwidget):
+        """
+        Register shell.
+
+        This function creates a new Widget.
+        """
+        shellwidget_id = id(shellwidget)
+        if shellwidget_id not in self._shellwidgets:
+            nsb = self.new_widget(shellwidget)
+            self._stack.addWidget(nsb)
+            self._shellwidgets[shellwidget_id] = nsb
+            self.set_shellwidget(shellwidget)
+            self.update_actions()
+
+    def remove_shellwidget(self, shellwidget):
+        shellwidget_id = id(shellwidget)
+        if shellwidget_id in self._shellwidgets:
+            nsb = self._shellwidgets.pop(shellwidget_id)
+            self._stack.removeWidget(nsb)
+            self.close_widget(nsb)
+
+    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.switch_widget(nsb, old_nsb)
+            self._stack.setCurrentWidget(nsb)
+
+    def new_widget(self, shellwidget):
path: spyder/plugins/ipythonconsole/utils/shellconnect.py
text: @user1:
Let's rename this to `create_new_widget`.

diff_hunk: @@ -0,0 +1,195 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © Spyder Project Contributors
+# Licensed under the terms of the MIT License
+# (see spyder/__init__.py for details)
+
+"""
+classes to connect to a shellwidget.
+"""
+
+# Third party imports
+from qtpy.QtWidgets import (
+    QStackedWidget, QVBoxLayout)
+
+# Local imports
+from spyder.api.config.decorators import on_conf_change
+from spyder.api.translations import get_translation
+from spyder.api.widgets.main_widget import PluginMainWidget
+
+# Localization
+_ = get_translation('spyder')
+
+
+class ShellConnectManager():
+    """
+    Manager to connect a stacked widget to a shell widget.
+
+    It is assumed that self.get_widget() returns a child of
+    StackedShellConnectWidget
+    """
+
+    def register_ipyconsole(self, ipyconsole):
+        """Connect to ipyconsole."""
+        # Signals
+        ipyconsole.sig_shellwidget_changed.connect(self.set_shellwidget)
+        ipyconsole.sig_shellwidget_created.connect(self.add_shellwidget)
+        ipyconsole.sig_shellwidget_deleted.connect(
+            self.remove_shellwidget)
+
+    def unregister_ipyconsole(self, ipyconsole):
+        """Disconnect from ipyconsole."""
+        # Signals
+        ipyconsole.sig_shellwidget_changed.disconnect(self.set_shellwidget)
+        ipyconsole.sig_shellwidget_created.disconnect(self.add_shellwidget)
+        ipyconsole.sig_shellwidget_deleted.disconnect(
+            self.remove_shellwidget)
+
+    # ---- Public API
+    # ------------------------------------------------------------------------
+    def set_shellwidget(self, shellwidget):
+        """
+        Update the current shellwidget.
+
+        Parameters
+        ----------
+        shellwidget: spyder.plugins.ipyconsole.widgets.shell.ShellWidget
+            The shell widget.
+        """
+        self.get_widget().set_shellwidget(shellwidget)
+
+    def add_shellwidget(self, shellwidget, external):
+        """
+        Add a new shellwidget to be registered.
+
+        This function registers a new NamespaceBrowser for browsing variables
+        in the shellwidget.
+
+        Parameters
+        ----------
+        shellwidget: spyder.plugins.ipyconsole.widgets.shell.ShellWidget
+            The shell widget.
+        external: bool
+            True if the kernel is external
+        """
+        self.get_widget().add_shellwidget(shellwidget)
+
+    def remove_shellwidget(self, shellwidget, external):
+        """
+        Remove the shellwidget registered.
+
+        Parameters
+        ----------
+        shellwidget: spyder.plugins.ipyconsole.widgets.shell.ShellWidget
+            The shell widget.
+        external: bool
+            True if the kernel is external
+        """
+        self.get_widget().remove_shellwidget(shellwidget)
+
+
+class StackedShellConnectWidget(PluginMainWidget):
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+
+        # Widgets
+        self._stack = QStackedWidget(self)
+        self._shellwidgets = {}
+
+        # Layout
+        layout = QVBoxLayout()
+        layout.setSpacing(0)
+        layout.setContentsMargins(0, 0, 0, 0)
+        layout.addWidget(self._stack)
+        self.setLayout(layout)
+
+    def update_style(self):
+        self._stack.setStyleSheet(
+            "QStackedWidget {padding: 0px; border: 0px}")
+
+    # ---- Stack accesors
+    # ------------------------------------------------------------------------
+
+    def count(self):
+        """
+        Return the number of widgets in the stack.
+
+        Returns
+        -------
+        int
+            The number of widgets in the stack.
+        """
+        return self._stack.count()
+
+    def current_widget(self):
+        """
+        Return the current figure browser widget in the stack.
+
+        Returns
+        -------
+        spyder.plugins.plots.widgets.figurebrowser.FigureBrowser
+            The current widget.
+        """
+        return self._stack.currentWidget()
+
+    def get_focus_widget(self):
+        return self.current_widget()
+
+    # ---- Public API
+    # ------------------------------------------------------------------------
+    def add_shellwidget(self, shellwidget):
+        """
+        Register shell.
+
+        This function creates a new Widget.
+        """
+        shellwidget_id = id(shellwidget)
+        if shellwidget_id not in self._shellwidgets:
+            nsb = self.new_widget(shellwidget)
+            self._stack.addWidget(nsb)
+            self._shellwidgets[shellwidget_id] = nsb
+            self.set_shellwidget(shellwidget)
+            self.update_actions()
+
+    def remove_shellwidget(self, shellwidget):
+        shellwidget_id = id(shellwidget)
+        if shellwidget_id in self._shellwidgets:
+            nsb = self._shellwidgets.pop(shellwidget_id)
+            self._stack.removeWidget(nsb)
+            self.close_widget(nsb)
+
+    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.switch_widget(nsb, old_nsb)
+            self._stack.setCurrentWidget(nsb)
+
+    def new_widget(self, shellwidget):
+        raise NotImplementedError
+
+    def close_widget(self, nsb):
path: spyder/plugins/ipythonconsole/utils/shellconnect.py
text: @user1:
```suggestion
    def close_widget(self, widget):
```

diff_hunk: @@ -0,0 +1,195 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © Spyder Project Contributors
+# Licensed under the terms of the MIT License
+# (see spyder/__init__.py for details)
+
+"""
+classes to connect to a shellwidget.
+"""
+
+# Third party imports
+from qtpy.QtWidgets import (
+    QStackedWidget, QVBoxLayout)
+
+# Local imports
+from spyder.api.config.decorators import on_conf_change
+from spyder.api.translations import get_translation
+from spyder.api.widgets.main_widget import PluginMainWidget
+
+# Localization
+_ = get_translation('spyder')
+
+
+class ShellConnectManager():
+    """
+    Manager to connect a stacked widget to a shell widget.
+
+    It is assumed that self.get_widget() returns a child of
+    StackedShellConnectWidget
+    """
+
+    def register_ipyconsole(self, ipyconsole):
+        """Connect to ipyconsole."""
+        # Signals
+        ipyconsole.sig_shellwidget_changed.connect(self.set_shellwidget)
+        ipyconsole.sig_shellwidget_created.connect(self.add_shellwidget)
+        ipyconsole.sig_shellwidget_deleted.connect(
+            self.remove_shellwidget)
+
+    def unregister_ipyconsole(self, ipyconsole):
+        """Disconnect from ipyconsole."""
+        # Signals
+        ipyconsole.sig_shellwidget_changed.disconnect(self.set_shellwidget)
+        ipyconsole.sig_shellwidget_created.disconnect(self.add_shellwidget)
+        ipyconsole.sig_shellwidget_deleted.disconnect(
+            self.remove_shellwidget)
+
+    # ---- Public API
+    # ------------------------------------------------------------------------
+    def set_shellwidget(self, shellwidget):
+        """
+        Update the current shellwidget.
+
+        Parameters
+        ----------
+        shellwidget: spyder.plugins.ipyconsole.widgets.shell.ShellWidget
+            The shell widget.
+        """
+        self.get_widget().set_shellwidget(shellwidget)
+
+    def add_shellwidget(self, shellwidget, external):
+        """
+        Add a new shellwidget to be registered.
+
+        This function registers a new NamespaceBrowser for browsing variables
+        in the shellwidget.
+
+        Parameters
+        ----------
+        shellwidget: spyder.plugins.ipyconsole.widgets.shell.ShellWidget
+            The shell widget.
+        external: bool
+            True if the kernel is external
+        """
+        self.get_widget().add_shellwidget(shellwidget)
+
+    def remove_shellwidget(self, shellwidget, external):
+        """
+        Remove the shellwidget registered.
+
+        Parameters
+        ----------
+        shellwidget: spyder.plugins.ipyconsole.widgets.shell.ShellWidget
+            The shell widget.
+        external: bool
+            True if the kernel is external
+        """
+        self.get_widget().remove_shellwidget(shellwidget)
+
+
+class StackedShellConnectWidget(PluginMainWidget):
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+
+        # Widgets
+        self._stack = QStackedWidget(self)
+        self._shellwidgets = {}
+
+        # Layout
+        layout = QVBoxLayout()
+        layout.setSpacing(0)
+        layout.setContentsMargins(0, 0, 0, 0)
+        layout.addWidget(self._stack)
+        self.setLayout(layout)
+
+    def update_style(self):
+        self._stack.setStyleSheet(
+            "QStackedWidget {padding: 0px; border: 0px}")
+
+    # ---- Stack accesors
+    # ------------------------------------------------------------------------
+
+    def count(self):
+        """
+        Return the number of widgets in the stack.
+
+        Returns
+        -------
+        int
+            The number of widgets in the stack.
+        """
+        return self._stack.count()
+
+    def current_widget(self):
+        """
+        Return the current figure browser widget in the stack.
+
+        Returns
+        -------
+        spyder.plugins.plots.widgets.figurebrowser.FigureBrowser
+            The current widget.
+        """
+        return self._stack.currentWidget()
+
+    def get_focus_widget(self):
+        return self.current_widget()
+
+    # ---- Public API
+    # ------------------------------------------------------------------------
+    def add_shellwidget(self, shellwidget):
+        """
+        Register shell.
+
+        This function creates a new Widget.
+        """
+        shellwidget_id = id(shellwidget)
+        if shellwidget_id not in self._shellwidgets:
+            nsb = self.new_widget(shellwidget)
+            self._stack.addWidget(nsb)
+            self._shellwidgets[shellwidget_id] = nsb
+            self.set_shellwidget(shellwidget)
+            self.update_actions()
+
+    def remove_shellwidget(self, shellwidget):
+        shellwidget_id = id(shellwidget)
+        if shellwidget_id in self._shellwidgets:
+            nsb = self._shellwidgets.pop(shellwidget_id)
+            self._stack.removeWidget(nsb)
+            self.close_widget(nsb)
+
+    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.switch_widget(nsb, old_nsb)
+            self._stack.setCurrentWidget(nsb)
+
+    def new_widget(self, shellwidget):
+        raise NotImplementedError
+
+    def close_widget(self, nsb):
+        raise NotImplementedError
+
+    def switch_widget(self, nsb, old_nsb):
path: spyder/plugins/ipythonconsole/utils/shellconnect.py
text: @user1:
```suggestion
    def switch_widget(self, widget, old_widget):
```

diff_hunk: @@ -0,0 +1,195 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © Spyder Project Contributors
+# Licensed under the terms of the MIT License
+# (see spyder/__init__.py for details)
+
+"""
+classes to connect to a shellwidget.
+"""
+
+# Third party imports
+from qtpy.QtWidgets import (
+    QStackedWidget, QVBoxLayout)
+
+# Local imports
+from spyder.api.config.decorators import on_conf_change
+from spyder.api.translations import get_translation
+from spyder.api.widgets.main_widget import PluginMainWidget
+
+# Localization
+_ = get_translation('spyder')
+
+
+class ShellConnectManager():
+    """
+    Manager to connect a stacked widget to a shell widget.
+
+    It is assumed that self.get_widget() returns a child of
+    StackedShellConnectWidget
+    """
+
+    def register_ipyconsole(self, ipyconsole):
+        """Connect to ipyconsole."""
+        # Signals
+        ipyconsole.sig_shellwidget_changed.connect(self.set_shellwidget)
+        ipyconsole.sig_shellwidget_created.connect(self.add_shellwidget)
+        ipyconsole.sig_shellwidget_deleted.connect(
+            self.remove_shellwidget)
+
+    def unregister_ipyconsole(self, ipyconsole):
+        """Disconnect from ipyconsole."""
+        # Signals
+        ipyconsole.sig_shellwidget_changed.disconnect(self.set_shellwidget)
+        ipyconsole.sig_shellwidget_created.disconnect(self.add_shellwidget)
+        ipyconsole.sig_shellwidget_deleted.disconnect(
+            self.remove_shellwidget)
+
+    # ---- Public API
+    # ------------------------------------------------------------------------
+    def set_shellwidget(self, shellwidget):
+        """
+        Update the current shellwidget.
+
+        Parameters
+        ----------
+        shellwidget: spyder.plugins.ipyconsole.widgets.shell.ShellWidget
+            The shell widget.
+        """
+        self.get_widget().set_shellwidget(shellwidget)
+
+    def add_shellwidget(self, shellwidget, external):
+        """
+        Add a new shellwidget to be registered.
+
+        This function registers a new NamespaceBrowser for browsing variables
+        in the shellwidget.
+
+        Parameters
+        ----------
+        shellwidget: spyder.plugins.ipyconsole.widgets.shell.ShellWidget
+            The shell widget.
+        external: bool
+            True if the kernel is external
+        """
+        self.get_widget().add_shellwidget(shellwidget)
+
+    def remove_shellwidget(self, shellwidget, external):
+        """
+        Remove the shellwidget registered.
+
+        Parameters
+        ----------
+        shellwidget: spyder.plugins.ipyconsole.widgets.shell.ShellWidget
+            The shell widget.
+        external: bool
+            True if the kernel is external
+        """
+        self.get_widget().remove_shellwidget(shellwidget)
+
+
+class StackedShellConnectWidget(PluginMainWidget):
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+
+        # Widgets
+        self._stack = QStackedWidget(self)
+        self._shellwidgets = {}
+
+        # Layout
+        layout = QVBoxLayout()
+        layout.setSpacing(0)
+        layout.setContentsMargins(0, 0, 0, 0)
+        layout.addWidget(self._stack)
+        self.setLayout(layout)
+
+    def update_style(self):
+        self._stack.setStyleSheet(
+            "QStackedWidget {padding: 0px; border: 0px}")
+
+    # ---- Stack accesors
+    # ------------------------------------------------------------------------
+
+    def count(self):
+        """
+        Return the number of widgets in the stack.
+
+        Returns
+        -------
+        int
+            The number of widgets in the stack.
+        """
+        return self._stack.count()
+
+    def current_widget(self):
+        """
+        Return the current figure browser widget in the stack.
+
+        Returns
+        -------
+        spyder.plugins.plots.widgets.figurebrowser.FigureBrowser
+            The current widget.
+        """
+        return self._stack.currentWidget()
+
+    def get_focus_widget(self):
+        return self.current_widget()
+
+    # ---- Public API
+    # ------------------------------------------------------------------------
+    def add_shellwidget(self, shellwidget):
+        """
+        Register shell.
+
+        This function creates a new Widget.
+        """
+        shellwidget_id = id(shellwidget)
+        if shellwidget_id not in self._shellwidgets:
+            nsb = self.new_widget(shellwidget)
+            self._stack.addWidget(nsb)
+            self._shellwidgets[shellwidget_id] = nsb
+            self.set_shellwidget(shellwidget)
+            self.update_actions()
+
+    def remove_shellwidget(self, shellwidget):
+        shellwidget_id = id(shellwidget)
+        if shellwidget_id in self._shellwidgets:
+            nsb = self._shellwidgets.pop(shellwidget_id)
+            self._stack.removeWidget(nsb)
+            self.close_widget(nsb)
+
+    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.switch_widget(nsb, old_nsb)
+            self._stack.setCurrentWidget(nsb)
+
+    def new_widget(self, shellwidget):
+        raise NotImplementedError
+
+    def close_widget(self, nsb):
+        raise NotImplementedError
+
+    def switch_widget(self, nsb, old_nsb):
+        raise NotImplementedError
+
+    def refresh(self):
+        if self.count():
+            nsb = self.current_widget()
+            nsb.refresh()
path: spyder/plugins/ipythonconsole/utils/shellconnect.py
text: @user1:
```suggestion
            widget = self.current_widget()
            widget.refresh()
```

diff_hunk: @@ -0,0 +1,195 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © Spyder Project Contributors
+# Licensed under the terms of the MIT License
+# (see spyder/__init__.py for details)
+
+"""
+classes to connect to a shellwidget.
+"""
+
+# Third party imports
+from qtpy.QtWidgets import (
+    QStackedWidget, QVBoxLayout)
+
+# Local imports
+from spyder.api.config.decorators import on_conf_change
+from spyder.api.translations import get_translation
+from spyder.api.widgets.main_widget import PluginMainWidget
+
+# Localization
+_ = get_translation('spyder')
+
+
+class ShellConnectManager():
+    """
+    Manager to connect a stacked widget to a shell widget.
+
+    It is assumed that self.get_widget() returns a child of
+    StackedShellConnectWidget
+    """
+
+    def register_ipyconsole(self, ipyconsole):
+        """Connect to ipyconsole."""
+        # Signals
+        ipyconsole.sig_shellwidget_changed.connect(self.set_shellwidget)
+        ipyconsole.sig_shellwidget_created.connect(self.add_shellwidget)
+        ipyconsole.sig_shellwidget_deleted.connect(
+            self.remove_shellwidget)
+
+    def unregister_ipyconsole(self, ipyconsole):
+        """Disconnect from ipyconsole."""
+        # Signals
+        ipyconsole.sig_shellwidget_changed.disconnect(self.set_shellwidget)
+        ipyconsole.sig_shellwidget_created.disconnect(self.add_shellwidget)
+        ipyconsole.sig_shellwidget_deleted.disconnect(
+            self.remove_shellwidget)
+
+    # ---- Public API
+    # ------------------------------------------------------------------------
+    def set_shellwidget(self, shellwidget):
+        """
+        Update the current shellwidget.
+
+        Parameters
+        ----------
+        shellwidget: spyder.plugins.ipyconsole.widgets.shell.ShellWidget
+            The shell widget.
+        """
+        self.get_widget().set_shellwidget(shellwidget)
+
+    def add_shellwidget(self, shellwidget, external):
+        """
+        Add a new shellwidget to be registered.
+
+        This function registers a new NamespaceBrowser for browsing variables
+        in the shellwidget.
+
+        Parameters
+        ----------
+        shellwidget: spyder.plugins.ipyconsole.widgets.shell.ShellWidget
+            The shell widget.
+        external: bool
+            True if the kernel is external
+        """
+        self.get_widget().add_shellwidget(shellwidget)
+
+    def remove_shellwidget(self, shellwidget, external):
+        """
+        Remove the shellwidget registered.
+
+        Parameters
+        ----------
+        shellwidget: spyder.plugins.ipyconsole.widgets.shell.ShellWidget
+            The shell widget.
+        external: bool
+            True if the kernel is external
+        """
+        self.get_widget().remove_shellwidget(shellwidget)
+
+
+class StackedShellConnectWidget(PluginMainWidget):
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+
+        # Widgets
+        self._stack = QStackedWidget(self)
+        self._shellwidgets = {}
+
+        # Layout
+        layout = QVBoxLayout()
+        layout.setSpacing(0)
+        layout.setContentsMargins(0, 0, 0, 0)
+        layout.addWidget(self._stack)
+        self.setLayout(layout)
+
+    def update_style(self):
+        self._stack.setStyleSheet(
+            "QStackedWidget {padding: 0px; border: 0px}")
+
+    # ---- Stack accesors
+    # ------------------------------------------------------------------------
+
+    def count(self):
+        """
+        Return the number of widgets in the stack.
+
+        Returns
+        -------
+        int
+            The number of widgets in the stack.
+        """
+        return self._stack.count()
+
+    def current_widget(self):
+        """
+        Return the current figure browser widget in the stack.
+
+        Returns
+        -------
+        spyder.plugins.plots.widgets.figurebrowser.FigureBrowser
+            The current widget.
+        """
+        return self._stack.currentWidget()
+
+    def get_focus_widget(self):
+        return self.current_widget()
+
+    # ---- Public API
+    # ------------------------------------------------------------------------
+    def add_shellwidget(self, shellwidget):
+        """
+        Register shell.
+
+        This function creates a new Widget.
+        """
+        shellwidget_id = id(shellwidget)
+        if shellwidget_id not in self._shellwidgets:
+            nsb = self.new_widget(shellwidget)
+            self._stack.addWidget(nsb)
+            self._shellwidgets[shellwidget_id] = nsb
+            self.set_shellwidget(shellwidget)
+            self.update_actions()
+
+    def remove_shellwidget(self, shellwidget):
+        shellwidget_id = id(shellwidget)
+        if shellwidget_id in self._shellwidgets:
+            nsb = self._shellwidgets.pop(shellwidget_id)
+            self._stack.removeWidget(nsb)
+            self.close_widget(nsb)
+
+    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.switch_widget(nsb, old_nsb)
+            self._stack.setCurrentWidget(nsb)
+
+    def new_widget(self, shellwidget):
+        raise NotImplementedError
+
+    def close_widget(self, nsb):
+        raise NotImplementedError
+
+    def switch_widget(self, nsb, old_nsb):
+        raise NotImplementedError
+
+    def refresh(self):
+        if self.count():
+            nsb = self.current_widget()
+            nsb.refresh()
+
+    def update_actions(self):
+        nsb = self.current_widget()
path: spyder/plugins/ipythonconsole/utils/shellconnect.py
text: @user1:
```suggestion
        widget = self.current_widget()
```

diff_hunk: @@ -0,0 +1,195 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © Spyder Project Contributors
+# Licensed under the terms of the MIT License
+# (see spyder/__init__.py for details)
+
+"""
+classes to connect to a shellwidget.
+"""
+
+# Third party imports
+from qtpy.QtWidgets import (
+    QStackedWidget, QVBoxLayout)
+
+# Local imports
+from spyder.api.config.decorators import on_conf_change
+from spyder.api.translations import get_translation
+from spyder.api.widgets.main_widget import PluginMainWidget
+
+# Localization
+_ = get_translation('spyder')
+
+
+class ShellConnectManager():
+    """
+    Manager to connect a stacked widget to a shell widget.
+
+    It is assumed that self.get_widget() returns a child of
+    StackedShellConnectWidget
+    """
+
+    def register_ipyconsole(self, ipyconsole):
+        """Connect to ipyconsole."""
+        # Signals
+        ipyconsole.sig_shellwidget_changed.connect(self.set_shellwidget)
+        ipyconsole.sig_shellwidget_created.connect(self.add_shellwidget)
+        ipyconsole.sig_shellwidget_deleted.connect(
+            self.remove_shellwidget)
+
+    def unregister_ipyconsole(self, ipyconsole):
+        """Disconnect from ipyconsole."""
+        # Signals
+        ipyconsole.sig_shellwidget_changed.disconnect(self.set_shellwidget)
+        ipyconsole.sig_shellwidget_created.disconnect(self.add_shellwidget)
+        ipyconsole.sig_shellwidget_deleted.disconnect(
+            self.remove_shellwidget)
+
+    # ---- Public API
+    # ------------------------------------------------------------------------
+    def set_shellwidget(self, shellwidget):
+        """
+        Update the current shellwidget.
+
+        Parameters
+        ----------
+        shellwidget: spyder.plugins.ipyconsole.widgets.shell.ShellWidget
+            The shell widget.
+        """
+        self.get_widget().set_shellwidget(shellwidget)
+
+    def add_shellwidget(self, shellwidget, external):
+        """
+        Add a new shellwidget to be registered.
+
+        This function registers a new NamespaceBrowser for browsing variables
+        in the shellwidget.
+
+        Parameters
+        ----------
+        shellwidget: spyder.plugins.ipyconsole.widgets.shell.ShellWidget
+            The shell widget.
+        external: bool
+            True if the kernel is external
+        """
+        self.get_widget().add_shellwidget(shellwidget)
+
+    def remove_shellwidget(self, shellwidget, external):
+        """
+        Remove the shellwidget registered.
+
+        Parameters
+        ----------
+        shellwidget: spyder.plugins.ipyconsole.widgets.shell.ShellWidget
+            The shell widget.
+        external: bool
+            True if the kernel is external
+        """
+        self.get_widget().remove_shellwidget(shellwidget)
+
+
+class StackedShellConnectWidget(PluginMainWidget):
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+
+        # Widgets
+        self._stack = QStackedWidget(self)
+        self._shellwidgets = {}
+
+        # Layout
+        layout = QVBoxLayout()
+        layout.setSpacing(0)
+        layout.setContentsMargins(0, 0, 0, 0)
+        layout.addWidget(self._stack)
+        self.setLayout(layout)
+
+    def update_style(self):
+        self._stack.setStyleSheet(
+            "QStackedWidget {padding: 0px; border: 0px}")
+
+    # ---- Stack accesors
+    # ------------------------------------------------------------------------
+
+    def count(self):
+        """
+        Return the number of widgets in the stack.
+
+        Returns
+        -------
+        int
+            The number of widgets in the stack.
+        """
+        return self._stack.count()
+
+    def current_widget(self):
+        """
+        Return the current figure browser widget in the stack.
+
+        Returns
+        -------
+        spyder.plugins.plots.widgets.figurebrowser.FigureBrowser
+            The current widget.
+        """
+        return self._stack.currentWidget()
+
+    def get_focus_widget(self):
+        return self.current_widget()
+
+    # ---- Public API
+    # ------------------------------------------------------------------------
+    def add_shellwidget(self, shellwidget):
+        """
+        Register shell.
+
+        This function creates a new Widget.
+        """
+        shellwidget_id = id(shellwidget)
+        if shellwidget_id not in self._shellwidgets:
+            nsb = self.new_widget(shellwidget)
+            self._stack.addWidget(nsb)
+            self._shellwidgets[shellwidget_id] = nsb
+            self.set_shellwidget(shellwidget)
+            self.update_actions()
+
+    def remove_shellwidget(self, shellwidget):
+        shellwidget_id = id(shellwidget)
+        if shellwidget_id in self._shellwidgets:
+            nsb = self._shellwidgets.pop(shellwidget_id)
+            self._stack.removeWidget(nsb)
+            self.close_widget(nsb)
+
+    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.switch_widget(nsb, old_nsb)
+            self._stack.setCurrentWidget(nsb)
+
+    def new_widget(self, shellwidget):
+        raise NotImplementedError
+
+    def close_widget(self, nsb):
+        raise NotImplementedError
+
+    def switch_widget(self, nsb, old_nsb):
+        raise NotImplementedError
+
+    def refresh(self):
+        if self.count():
+            nsb = self.current_widget()
+            nsb.refresh()
+
+    def update_actions(self):
+        nsb = self.current_widget()
+
+        for __, action in self.get_actions().items():
+            if action:
+                # IMPORTANT: Since we are defining the main actions in here
+                # and the context is WidgetWithChildrenShortcut we need to
+                # assign the same actions to the children widgets in order
+                # for shortcuts to work
+                if nsb:
+                    nsb_actions = nsb.actions()
+                    if action not in nsb_actions:
+                        nsb.addAction(action)
path: spyder/plugins/ipythonconsole/utils/shellconnect.py
text: @user1:
```suggestion
                if widget:
                    widget_actions = widget.actions()
                    if action not in widget_actions:
                        widget.addAction(action)
```

diff_hunk: @@ -64,10 +61,7 @@ def unregister(self):
         ipyconsole = self.get_plugin(Plugins.IPythonConsole)
 
         # Signals
path: spyder/plugins/plots/plugin.py
text: @user1:
```suggestion
        # Unregister IPython console.
```

diff_hunk: @@ -69,11 +66,7 @@ def unregister(self):
         ipyconsole = self.get_plugin(Plugins.IPythonConsole)
 
         # Signals
path: spyder/plugins/variableexplorer/plugin.py
text: @user1:
```suggestion
        # Unregister IPython console.
```

diff_hunk: @@ -1828,7 +1815,7 @@ def _create_client_for_kernel(self, connection_file, hostname, sshkey,
         # (i.e. the i in i/A)
         master_id = None
         given_name = None
-        external_kernel = False
+        is_master = False
path: spyder/plugins/ipythonconsole/plugin.py
text: @user1:
I don't understand the rename of this variable. I think it's clearer as `external_kernel`, so I'd prefer to revert this change.

@author:
I reverted it but `external_kernel ` means the kernel is already open in spyder. It does not mean that the kernel is external.
For example, if you open an external kernel twice, the first time, `external_kernel=True` and the second time, `external_kernel=False`

@author:
I fixed this

diff_hunk: @@ -0,0 +1,195 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © Spyder Project Contributors
+# Licensed under the terms of the MIT License
+# (see spyder/__init__.py for details)
+
+"""
+classes to connect to a shellwidget.
+"""
+
+# Third party imports
+from qtpy.QtWidgets import (
+    QStackedWidget, QVBoxLayout)
+
+# Local imports
+from spyder.api.config.decorators import on_conf_change
+from spyder.api.translations import get_translation
+from spyder.api.widgets.main_widget import PluginMainWidget
+
+# Localization
+_ = get_translation('spyder')
+
+
+class ShellConnectManager():
+    """
+    Manager to connect a stacked widget to a shell widget.
+
+    It is assumed that self.get_widget() returns a child of
+    StackedShellConnectWidget
+    """
+
+    def register_ipyconsole(self, ipyconsole):
+        """Connect to ipyconsole."""
+        # Signals
+        ipyconsole.sig_shellwidget_changed.connect(self.set_shellwidget)
+        ipyconsole.sig_shellwidget_created.connect(self.add_shellwidget)
+        ipyconsole.sig_shellwidget_deleted.connect(
+            self.remove_shellwidget)
+
+    def unregister_ipyconsole(self, ipyconsole):
path: spyder/plugins/ipythonconsole/utils/shellconnect.py
text: @user1:
Let's rename this to unregister_ipythonconsole

diff_hunk: @@ -0,0 +1,195 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © Spyder Project Contributors
+# Licensed under the terms of the MIT License
+# (see spyder/__init__.py for details)
+
+"""
+classes to connect to a shellwidget.
+"""
+
+# Third party imports
+from qtpy.QtWidgets import (
+    QStackedWidget, QVBoxLayout)
+
+# Local imports
+from spyder.api.config.decorators import on_conf_change
+from spyder.api.translations import get_translation
+from spyder.api.widgets.main_widget import PluginMainWidget
+
+# Localization
+_ = get_translation('spyder')
+
+
+class ShellConnectManager():
+    """
+    Manager to connect a stacked widget to a shell widget.
+
+    It is assumed that self.get_widget() returns a child of
+    StackedShellConnectWidget
+    """
+
+    def register_ipyconsole(self, ipyconsole):
path: spyder/plugins/ipythonconsole/utils/shellconnect.py
text: @user1:
Let's rename this to register_ipythonconsole