# -----------------------------------------------------------------------------
# Copyright (c) 2021- Spyder Project Contributors
#
# Released under the terms of the MIT License
# (see LICENSE.txt in the project root directory for details)
# -----------------------------------------------------------------------------
"""
Main widget for plugins showing content from the IPython Console.
Used in, for example, the :guilabel:`Variable Explorer`, :guilabel:`Plots`,
:guilabel:`Debugger` and :guilabel:`Profiler` plugins.
"""
from __future__ import annotations
# Standard library imports
from typing import TYPE_CHECKING, Any
# Third party imports
from qtpy.QtWidgets import QStackedWidget, QVBoxLayout
# Local imports
from spyder.api.translations import _
from spyder.api.widgets.main_widget import PluginMainWidget
from spyder.widgets.emptymessage import EmptyMessageWidget
if TYPE_CHECKING:
from qtpy.QtWidgets import QWidget
import spyder.plugins.ipythonconsole.widgets
class _ErroredMessageWidget(EmptyMessageWidget):
"""Widget to show when the kernel's shell failed to start."""
def __init__(
self,
parent: ShellConnectMainWidget,
shellwidget: spyder.plugins.ipythonconsole.widgets.ShellWidget,
) -> None:
"""
Initialize :class:`EmptyMessageWidget` with content to show for errors.
Parameters
----------
parent : ShellConnectMainWidget
The parent widget of this one.
shellwidget : spyder.plugins.ipythonconsole.widgets.ShellWidget
The shell widget that failed to launch.
Returns
-------
None
"""
super().__init__(
parent,
icon_filename=(
"console-remote-off"
if shellwidget.is_remote()
else "console-off"
),
text=_("No connected console"),
description=_(
"The current console has no active kernel, so there is no "
"content to show here"
),
adjust_on_resize=True,
)
self.is_empty: bool = False
"""
If the current widget has no content to show.
Used to show this widget if :class:`ShellConnectMainWidget` has an
empty message.
"""
[docs]
class ShellConnectMainWidget(PluginMainWidget):
"""
Main widget to use in a plugin that shows different content per-console.
It is composed of a :class:`QStackedWidget` to stack the widget associated
to each console shell widget, and only show one of them at a time.
The current widget in the stack displays the content corresponding to
the console with focus.
Used in the :guilabel:`Variable Explorer` and :guilabel:`Plots` plugins,
for example, to show the variables and plots of the current console.
"""
[docs]
def __init__(
self, *args: Any, set_layout: bool = True, **kwargs: Any
) -> None:
"""
Create a new main widget to show console-specific content.
Parameters
----------
*args : Any
Positional arguments to pass to
:meth:`PluginMainWidget.__init__() <spyder.api.widgets.main_widget.PluginMainWidget.__init__>`.
set_layout : bool, optional
Whether to add the created widget to a layout, ``True`` by default.
**kwargs : Any
Keyword arguments to pass to
:meth:`PluginMainWidget.__init__() <spyder.api.widgets.main_widget.PluginMainWidget.__init__>`.
Returns
-------
None
"""
super().__init__(*args, **kwargs)
# Widgets
if not (
self.SHOW_MESSAGE_WHEN_EMPTY
and self.get_conf(
"show_message_when_panes_are_empty", section="main"
)
):
self._stack = QStackedWidget(self)
if set_layout:
layout = QVBoxLayout()
layout.addWidget(self._stack)
self.setLayout(layout)
self._shellwidgets = {}
# ---- PluginMainWidget API
# ------------------------------------------------------------------------
[docs]
def current_widget(self) -> QWidget:
"""
Get the currently displayed widget (either the stack or error widget).
Returns
-------
QWidget
The currently displayed widget, either the active widget in the
stack associated with the current
:class:`~spyder.plugins.ipythonconsole.widgets.ShellWidget`
(i.e. :guilabel:`IPython Console` tab),
or if that kernel failed with an error, the error message widget.
"""
return self._content_widget
[docs]
def get_focus_widget(self) -> QWidget:
"""
Get the stack widget associated to the currently active shell widget.
Used by
:meth:`~spyder.api.widgets.main_widget.PluginMainWidget.change_visibility`
when switching to the plugin.
Returns
-------
QWidget
The current widget in the stack, associated with the active
:class:`~spyder.plugins.ipythonconsole.widgets.ShellWidget`
(i.e. :guilabel:`IPython Console` tab), to give focus to.
"""
return self._stack.currentWidget()
# ---- SpyderWidgetMixin API
# ------------------------------------------------------------------------
[docs]
def update_style(self) -> None:
"""
Update the stylesheet and style of the stack widget.
.. caution::
If overriding this method, :class:`super` must be called
for this widget to display correctly in the Spyder UI.
Returns
-------
None
"""
self._stack.setStyleSheet("QStackedWidget {padding: 0px; border: 0px}")
# ---- Stack accesors
# ------------------------------------------------------------------------
[docs]
def count(self) -> int:
"""
Get the number of widgets in the stack.
Returns
-------
int
The number of widgets in the stack.
"""
return self._stack.count()
[docs]
def get_widget_for_shellwidget(
self,
shellwidget: spyder.plugins.ipythonconsole.widgets.ShellWidget,
) -> QWidget | None:
"""
Retrieve the stacked widget corresponding to the given shell widget.
Parameters
----------
shellwidget : spyder.plugins.ipythonconsole.widgets.ShellWidget
The shell widget to return the associated widget of.
Returns
-------
QWidget | None
The widget in the stack associated with ``shellwidget``,
or ``None`` if not found.
"""
shellwidget_id = id(shellwidget)
if shellwidget_id in self._shellwidgets:
return self._shellwidgets[shellwidget_id]
return None
# ---- Public API
# ------------------------------------------------------------------------
[docs]
def add_shellwidget(
self,
shellwidget: spyder.plugins.ipythonconsole.widgets.ShellWidget,
) -> None:
"""
Create a new widget in the stack associated to a given shell widget.
This method registers a new widget to display the content that
is associated with the given shell widget.
Parameters
----------
shellwidget : spyder.plugins.ipythonconsole.widgets.ShellWidget
The shell widget to associate the new widget to.
Returns
-------
None
"""
shellwidget_id = id(shellwidget)
if shellwidget_id not in self._shellwidgets:
widget = self.create_new_widget(shellwidget)
self._stack.addWidget(widget)
self._shellwidgets[shellwidget_id] = widget
# Add all actions to new widget for shortcuts to work.
for __, action in self.get_actions().items():
if action:
widget_actions = widget.actions()
if action not in widget_actions:
widget.addAction(action)
self.set_shellwidget(shellwidget)
[docs]
def remove_shellwidget(
self,
shellwidget: spyder.plugins.ipythonconsole.widgets.ShellWidget,
) -> None:
"""
Remove the stacked widget associated to a given shell widget.
Parameters
----------
shellwidget : spyder.plugins.ipythonconsole.widgets.ShellWidget
The shell widget to remove the associated widget of.
Returns
-------
None
"""
shellwidget_id = id(shellwidget)
if shellwidget_id in self._shellwidgets:
widget = self._shellwidgets.pop(shellwidget_id)
# If `widget` is an empty pane, we don't need to remove it from the
# stack (because it's the one we need to show since the console is
# showing an error) nor try to close it (because it makes no
# sense).
if not isinstance(widget, EmptyMessageWidget):
self._stack.removeWidget(widget)
self.close_widget(widget)
self.update_actions()
[docs]
def set_shellwidget(
self,
shellwidget: spyder.plugins.ipythonconsole.widgets.ShellWidget,
) -> None:
"""
Set as active the stack widget associated with the given shell widget.
Parameters
----------
shellwidget : spyder.plugins.ipythonconsole.widgets.ShellWidget
The shell widget that corresponds to the stacked widget to set
as currently active.
Returns
-------
None
"""
old_widget = self.current_widget()
widget = self.get_widget_for_shellwidget(shellwidget)
if widget is None:
return
self.set_content_widget(widget, add_to_stack=False)
if (
self.SHOW_MESSAGE_WHEN_EMPTY
and self.get_conf(
"show_message_when_panes_are_empty", section="main"
)
and widget.is_empty
):
self.show_empty_message()
else:
self.show_content_widget()
self.switch_widget(widget, old_widget)
self.update_actions()
[docs]
def add_errored_shellwidget(
self,
shellwidget: spyder.plugins.ipythonconsole.widgets.ShellWidget,
) -> None:
"""
Add an error widget for a shell widget whose kernel failed to start.
This is necessary to show a meaningful message when switching to
consoles with dead kernels.
Parameters
----------
shellwidget : spyder.plugins.ipythonconsole.widgets.ShellWidget
The shell widget to associate with a new error widget.
Returns
-------
None
"""
shellwidget_id = id(shellwidget)
# This can happen if the kernel started without issues but something is
# printed to its stderr stream, which we display as an error in the
# console. In that case, we need to remove the current widget
# associated to shellwidget and replace it by an empty one.
if shellwidget_id in self._shellwidgets:
self._shellwidgets.pop(shellwidget_id)
widget = _ErroredMessageWidget(self, shellwidget)
widget.set_visibility(self.is_visible)
if self.dockwidget is not None:
self.dockwidget.visibilityChanged.connect(widget.set_visibility)
self.set_content_widget(widget)
self._shellwidgets[shellwidget_id] = widget
self.set_shellwidget(shellwidget)
[docs]
def create_new_widget(
self,
shellwidget: spyder.plugins.ipythonconsole.widgets.ShellWidget,
) -> QWidget:
"""
Create a new widget to communicate with the given shell widget.
Parameters
----------
shellwidget : spyder.plugins.ipythonconsole.widgets.ShellWidget
The shell widget to create a new associated widget for.
Returns
-------
QWidget
The newly-created widget associated with ``shellwidget``.
"""
raise NotImplementedError
[docs]
def close_widget(self, widget: QWidget) -> None:
"""
Close the given stacked widget.
Parameters
----------
widget : QWidget
The widget to close.
Returns
-------
None
"""
raise NotImplementedError
[docs]
def switch_widget(self, widget: QWidget, old_widget: QWidget) -> None:
"""
Switch the given stacked widget.
Parameters
----------
widget : QWidget
The widget to switch to.
old_widget : QWidget
The previously-active widget.
Returns
-------
None
"""
raise NotImplementedError
[docs]
def refresh(self) -> None:
"""
Refresh the current stacked widget.
Returns
-------
None
"""
if self.count():
widget = self.current_widget()
widget.refresh()
[docs]
def is_current_widget_error_message(self) -> bool:
"""
Check if the current widget is showing an error message.
Returns
-------
bool
``True`` if the current widget is showing an error message,
``False`` if not.
"""
return isinstance(self.current_widget(), _ErroredMessageWidget)
[docs]
def switch_empty_message(self, value: bool) -> None:
"""
Switch between the empty message widget or the one with content.
Parameters
----------
value : bool
If ``True``, switch to the empty widget; if ``False``, switch
to the content widget.
Returns
-------
None
"""
if value:
self.show_empty_message()
else:
self.show_content_widget()