diff --git a/changelogs/Spyder-6.md b/changelogs/Spyder-6.md index c7604919e3b..21b7b50c017 100644 --- a/changelogs/Spyder-6.md +++ b/changelogs/Spyder-6.md @@ -29,6 +29,8 @@ `Preferences > Appearance` * Files can be opened in the editor by pasting their path in the Working Directory toolbar. +* Add a new button to the Variable Explorer to indicate when variables are being + filtered. ### New API features @@ -39,8 +41,9 @@ * Generalize the Run plugin to support generic inputs and executors. This allows plugins to declare what kind of inputs (i.e. file, cell or selection) they can execute and how they will display the result. -* Add a new plugin called `Switcher` for the files and symbols switcher. +* Add a new plugin called Switcher for the files and symbols switcher. * Declare a proper API for the Projects plugin. +* Remove the Breakpoints plugin and add its functionality to the Debugger one. ---- diff --git a/setup.py b/setup.py index 881dad503a6..aa471dda520 100644 --- a/setup.py +++ b/setup.py @@ -281,7 +281,6 @@ def run(self): spyder_plugins_entry_points = [ 'appearance = spyder.plugins.appearance.plugin:Appearance', 'application = spyder.plugins.application.plugin:Application', - 'breakpoints = spyder.plugins.breakpoints.plugin:Breakpoints', 'completions = spyder.plugins.completion.plugin:CompletionPlugin', 'debugger = spyder.plugins.debugger.plugin:Debugger', 'editor = spyder.plugins.editor.plugin:Editor', diff --git a/spyder/api/plugins/enum.py b/spyder/api/plugins/enum.py index 54d7f53118a..d6090e69f9a 100644 --- a/spyder/api/plugins/enum.py +++ b/spyder/api/plugins/enum.py @@ -14,7 +14,6 @@ class Plugins: All = "all" # Wildcard to populate REQUIRES with all available plugins Appearance = 'appearance' Application = 'application' - Breakpoints = 'breakpoints' Completions = 'completions' Console = 'internal_console' Debugger = 'debugger' @@ -47,7 +46,6 @@ class Plugins: class DockablePlugins: - Breakpoints = 'breakpoints' Console = 'internal_console' Debugger = 'debugger' Editor = 'editor' diff --git a/spyder/api/shellconnect/main_widget.py b/spyder/api/shellconnect/main_widget.py index dffa2727fe3..924df955d7a 100644 --- a/spyder/api/shellconnect/main_widget.py +++ b/spyder/api/shellconnect/main_widget.py @@ -29,17 +29,18 @@ class ShellConnectMainWidget(PluginMainWidget): * The current widget in the stack will display the content associated to the console with focus. """ - def __init__(self, *args, **kwargs): + def __init__(self, *args, set_layout=True, **kwargs): super().__init__(*args, **kwargs) # Widgets self._stack = QStackedWidget(self) self._shellwidgets = {} - # Layout - layout = QVBoxLayout() - layout.addWidget(self._stack) - self.setLayout(layout) + if set_layout: + # Layout + layout = QVBoxLayout() + layout.addWidget(self._stack) + self.setLayout(layout) # ---- PluginMainWidget API # ------------------------------------------------------------------------ diff --git a/spyder/config/main.py b/spyder/config/main.py index 73ebfb53aa1..7f345c13ba3 100644 --- a/spyder/config/main.py +++ b/spyder/config/main.py @@ -190,7 +190,8 @@ 'pdb_execute_events': True, 'pdb_use_exclamation_mark': True, 'pdb_stop_first_line': True, - 'breakpoints_panel': True, + 'editor_debugger_panel': True, + 'breakpoints_table_visible': False, }), ('run', { @@ -329,10 +330,6 @@ 'exclude_case_sensitive': False, 'max_results': 1000, }), - ('breakpoints', - { - 'enable': True, - }), ('completions', { 'enable': True, @@ -526,6 +523,7 @@ 'debugger/stop': "Ctrl+Shift+F12", 'debugger/toggle breakpoint': 'F12', 'debugger/toggle conditional breakpoint': 'Shift+F12', + 'debugger/show breakpoint table': "", # -- Plots -- 'plots/copy': 'Ctrl+C', 'plots/previous figure': 'Ctrl+PgUp', diff --git a/spyder/plugins/breakpoints/__init__.py b/spyder/plugins/breakpoints/__init__.py deleted file mode 100644 index 707b9c8b569..00000000000 --- a/spyder/plugins/breakpoints/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -# -*- coding: utf-8 -*- -# ----------------------------------------------------------------------------- -# Copyright (c) 2009- Spyder Project Contributors -# -# Distributed under the terms of the MIT License -# (see spyder/__init__.py for details) -# ----------------------------------------------------------------------------- - -""" -spyder.plugins.breakpoints -========================== - -Breakpoints Plugin. -""" diff --git a/spyder/plugins/breakpoints/api.py b/spyder/plugins/breakpoints/api.py deleted file mode 100644 index c00f01b80e0..00000000000 --- a/spyder/plugins/breakpoints/api.py +++ /dev/null @@ -1,14 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2009- Spyder Project Contributors -# -# Distributed under the terms of the MIT License -# (see spyder/__init__.py for details) - -""" -Breakpoints Plugin API. -""" - -# Local imports -from spyder.plugins.breakpoints.plugin import BreakpointsActions -from spyder.plugins.breakpoints.widgets.main_widget import ( - BreakpointTableViewActions) diff --git a/spyder/plugins/breakpoints/plugin.py b/spyder/plugins/breakpoints/plugin.py deleted file mode 100644 index a1d0c428f91..00000000000 --- a/spyder/plugins/breakpoints/plugin.py +++ /dev/null @@ -1,222 +0,0 @@ -# -*- coding: utf-8 -*- -# ----------------------------------------------------------------------------- -# Copyright (c) 2009- Spyder Project Contributors -# -# Distributed under the terms of the MIT License -# (see spyder/__init__.py for details) -# ----------------------------------------------------------------------------- - -""" -Breakpoint Plugin. -""" - -# Standard library imports -import os.path as osp - -# Third party imports -from qtpy.QtCore import Signal - -# Local imports -from spyder.api.plugins import Plugins, SpyderDockablePlugin -from spyder.api.plugin_registration.decorators import ( - on_plugin_available, on_plugin_teardown) -from spyder.api.translations import _ -from spyder.plugins.breakpoints.widgets.main_widget import BreakpointWidget -from spyder.plugins.mainmenu.api import ApplicationMenus, DebugMenuSections - - -# --- Constants -# ---------------------------------------------------------------------------- -class BreakpointsActions: - ListBreakpoints = 'list_breakpoints_action' - - -# --- Plugin -# ---------------------------------------------------------------------------- -class Breakpoints(SpyderDockablePlugin): - """ - Breakpoint list Plugin. - """ - NAME = 'breakpoints' - REQUIRES = [Plugins.Editor, Plugins.Debugger] - OPTIONAL = [Plugins.MainMenu] - TABIFY = [Plugins.Help] - WIDGET_CLASS = BreakpointWidget - CONF_SECTION = NAME - CONF_FILE = False - - # --- Signals - # ------------------------------------------------------------------------ - sig_clear_all_breakpoints_requested = Signal() - """ - This signal is emitted to send a request to clear all assigned - breakpoints. - """ - - sig_clear_breakpoint_requested = Signal(str, int) - """ - This signal is emitted to send a request to clear a single breakpoint. - - Parameters - ---------- - filename: str - The path to filename containing the breakpoint. - line_number: int - The line number of the breakpoint. - """ - - sig_edit_goto_requested = Signal(str, int, str) - """ - Send a request to open a file in the editor at a given row and word. - - Parameters - ---------- - filename: str - The path to the filename containing the breakpoint. - line_number: int - The line number of the breakpoint. - word: str - Text `word` to select on given `line_number`. - """ - - sig_conditional_breakpoint_requested = Signal() - """ - Send a request to set/edit a condition on a single selected breakpoint. - """ - - # --- SpyderDockablePlugin API - # ------------------------------------------------------------------------ - @staticmethod - def get_name(): - return _("Breakpoints") - - @staticmethod - def get_description(): - return _("Manage debugger breakpoints in a unified pane.") - - @classmethod - def get_icon(cls): - return cls.create_icon('breakpoints') - - def on_initialize(self): - widget = self.get_widget() - - widget.sig_clear_all_breakpoints_requested.connect( - self.sig_clear_all_breakpoints_requested) - widget.sig_clear_breakpoint_requested.connect( - self.sig_clear_breakpoint_requested) - widget.sig_edit_goto_requested.connect(self.sig_edit_goto_requested) - widget.sig_conditional_breakpoint_requested.connect( - self.sig_conditional_breakpoint_requested) - - self.create_action( - BreakpointsActions.ListBreakpoints, - _("List breakpoints"), - triggered=self.switch_to_plugin, - icon=self.get_icon(), - ) - - @on_plugin_available(plugin=Plugins.Editor) - def on_editor_available(self): - widget = self.get_widget() - editor = self.get_plugin(Plugins.Editor) - list_action = self.get_action(BreakpointsActions.ListBreakpoints) - - widget.sig_edit_goto_requested.connect(editor.load) - - # TODO: Fix location once the sections are defined - editor.pythonfile_dependent_actions += [list_action] - - @on_plugin_teardown(plugin=Plugins.Editor) - def on_editor_teardown(self): - widget = self.get_widget() - editor = self.get_plugin(Plugins.Editor) - list_action = self.get_action(BreakpointsActions.ListBreakpoints) - - widget.sig_edit_goto_requested.disconnect(editor.load) - editor.pythonfile_dependent_actions.remove(list_action) - - @on_plugin_available(plugin=Plugins.Debugger) - def on_debugger_available(self): - debugger = self.get_plugin(Plugins.Debugger) - widget = self.get_widget() - debugger.get_widget().sig_breakpoints_saved.connect(self.set_data) - - widget.sig_clear_all_breakpoints_requested.connect( - debugger.clear_all_breakpoints) - widget.sig_clear_breakpoint_requested.connect( - debugger.clear_breakpoint) - widget.sig_conditional_breakpoint_requested.connect( - debugger._set_or_edit_conditional_breakpoint) - - @on_plugin_teardown(plugin=Plugins.Debugger) - def on_debugger_teardown(self): - debugger = self.get_plugin(Plugins.Debugger) - widget = self.get_widget() - debugger.get_widget().sig_breakpoints_saved.disconnect(self.set_data) - - widget.sig_clear_all_breakpoints_requested.disconnect( - debugger.clear_all_breakpoints) - widget.sig_clear_breakpoint_requested.disconnect( - debugger.clear_breakpoint) - widget.sig_conditional_breakpoint_requested.disconnect( - debugger._set_or_edit_conditional_breakpoint) - - @on_plugin_available(plugin=Plugins.MainMenu) - def on_main_menu_available(self): - mainmenu = self.get_plugin(Plugins.MainMenu) - list_action = self.get_action(BreakpointsActions.ListBreakpoints) - mainmenu.add_item_to_application_menu( - list_action, - menu_id=ApplicationMenus.Debug, - section=DebugMenuSections.ListBreakpoints) - - @on_plugin_teardown(plugin=Plugins.MainMenu) - def on_main_menu_teardown(self): - mainmenu = self.get_plugin(Plugins.MainMenu) - mainmenu.remove_item_from_application_menu( - BreakpointsActions.ListBreakpoints, - menu_id=ApplicationMenus.Debug) - - # --- Private API - # ------------------------------------------------------------------------ - def _load_data(self): - """ - Load breakpoint data from configuration file. - """ - breakpoints_dict = self.get_conf( - "breakpoints", - default={}, - section='debugger', - ) - for filename in list(breakpoints_dict.keys()): - if not osp.isfile(filename): - breakpoints_dict.pop(filename) - continue - # Make sure we don't have the same file under different names - new_filename = osp.normcase(filename) - if new_filename != filename: - bp = breakpoints_dict.pop(filename) - if new_filename in breakpoints_dict: - breakpoints_dict[new_filename].extend(bp) - else: - breakpoints_dict[new_filename] = bp - - return breakpoints_dict - - # --- Public API - # ------------------------------------------------------------------------ - def set_data(self, data=None): - """ - Set breakpoint data on widget. - - Parameters - ---------- - data: dict, optional - Breakpoint data to use. If None, data from the configuration - will be loaded. Default is None. - """ - if data is None: - data = self._load_data() - - self.get_widget().set_data(data) diff --git a/spyder/plugins/breakpoints/widgets/__init__.py b/spyder/plugins/breakpoints/widgets/__init__.py deleted file mode 100644 index 839eae7ce43..00000000000 --- a/spyder/plugins/breakpoints/widgets/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# -*- coding: utf-8 -*- -# ----------------------------------------------------------------------------- -# Copyright (c) 2009- Spyder Project Contributors -# -# Distributed under the terms of the MIT License -# (see spyder/__init__.py for details) -# ----------------------------------------------------------------------------- diff --git a/spyder/plugins/debugger/plugin.py b/spyder/plugins/debugger/plugin.py index ce4cf4aacd1..6af8eab283b 100644 --- a/spyder/plugins/debugger/plugin.py +++ b/spyder/plugins/debugger/plugin.py @@ -72,9 +72,9 @@ def on_initialize(self): widget.sig_toggle_conditional_breakpoints.connect( self._set_or_edit_conditional_breakpoint) widget.sig_clear_all_breakpoints.connect(self.clear_all_breakpoints) - - widget.sig_load_pdb_file.connect( - self._load_pdb_file_in_editor) + widget.sig_load_pdb_file.connect(self._load_pdb_file_in_editor) + widget.sig_clear_breakpoint.connect(self.clear_breakpoint) + widget.sig_switch_to_plugin_requested.connect(self.switch_to_plugin) self.python_editor_run_configuration = { 'origin': self.NAME, @@ -212,6 +212,7 @@ def on_editor_available(self): editor_shortcuts = [ DebuggerBreakpointActions.ToggleBreakpoint, DebuggerBreakpointActions.ToggleConditionalBreakpoint, + DebuggerBreakpointActions.ShowBreakpointsTable, ] for name in editor_shortcuts: action = self.get_action(name) @@ -241,6 +242,7 @@ def on_editor_teardown(self): editor_shortcuts = [ DebuggerBreakpointActions.ToggleBreakpoint, DebuggerBreakpointActions.ToggleConditionalBreakpoint, + DebuggerBreakpointActions.ShowBreakpointsTable, ] for name in editor_shortcuts: action = self.get_action(name) @@ -275,12 +277,12 @@ def on_main_menu_available(self): # Breakpoints section for action in [DebuggerBreakpointActions.ToggleBreakpoint, DebuggerBreakpointActions.ToggleConditionalBreakpoint, - DebuggerBreakpointActions.ClearAllBreakpoints]: + DebuggerBreakpointActions.ClearAllBreakpoints, + DebuggerBreakpointActions.ShowBreakpointsTable]: mainmenu.add_item_to_application_menu( self.get_action(action), menu_id=ApplicationMenus.Debug, - section=DebugMenuSections.EditBreakpoints, - before_section=DebugMenuSections.ListBreakpoints) + section=DebugMenuSections.EditBreakpoints) @on_plugin_teardown(plugin=Plugins.MainMenu) def on_main_menu_teardown(self): @@ -294,7 +296,8 @@ def on_main_menu_teardown(self): DebuggerWidgetActions.Stop, DebuggerBreakpointActions.ToggleBreakpoint, DebuggerBreakpointActions.ToggleConditionalBreakpoint, - DebuggerBreakpointActions.ClearAllBreakpoints + DebuggerBreakpointActions.ClearAllBreakpoints, + DebuggerBreakpointActions.ShowBreakpointsTable, ] for name in names: mainmenu.remove_item_from_application_menu( diff --git a/spyder/plugins/debugger/utils/breakpointsmanager.py b/spyder/plugins/debugger/utils/breakpointsmanager.py index d422f482575..47a1d977891 100644 --- a/spyder/plugins/debugger/utils/breakpointsmanager.py +++ b/spyder/plugins/debugger/utils/breakpointsmanager.py @@ -122,10 +122,10 @@ def set_filename(self, filename): def update_panel_visibility(self): """Update the panel visibility.""" self.debugger_panel.setVisible( - self.get_conf('breakpoints_panel', default=True)) + self.get_conf('editor_debugger_panel', default=True)) - @on_conf_change(option='breakpoints_panel') - def on_breakpoints_panel_update(self, value): + @on_conf_change(option='editor_debugger_panel') + def on_editor_debugger_panel_update(self, value): self.update_panel_visibility() def toogle_breakpoint( diff --git a/spyder/plugins/breakpoints/widgets/main_widget.py b/spyder/plugins/debugger/widgets/breakpoint_table_view.py similarity index 66% rename from spyder/plugins/breakpoints/widgets/main_widget.py rename to spyder/plugins/debugger/widgets/breakpoint_table_view.py index d884bd44ef6..b0167968de4 100644 --- a/spyder/plugins/breakpoints/widgets/main_widget.py +++ b/spyder/plugins/debugger/widgets/breakpoint_table_view.py @@ -14,30 +14,29 @@ # pylint: disable=R0911 # pylint: disable=R0201 -# Standard library imports -import sys # Third party imports +import qstylizer.style from qtpy import PYQT5 from qtpy.compat import to_qvariant from qtpy.QtCore import QAbstractTableModel, QModelIndex, Qt, Signal -from qtpy.QtWidgets import QItemDelegate, QTableView, QVBoxLayout +from qtpy.QtWidgets import QTableView # Local imports from spyder.api.translations import _ -from spyder.api.widgets.main_widget import (PluginMainWidgetMenus, - PluginMainWidget) +from spyder.api.widgets.main_widget import PluginMainWidgetMenus from spyder.api.widgets.mixins import SpyderWidgetMixin from spyder.utils.sourcecode import disambiguate_fname +from spyder.utils.palette import QStylePalette # --- Constants # ---------------------------------------------------------------------------- -COLUMN_COUNT = 4 +COLUMN_COUNT = 3 EXTRA_COLUMNS = 1 -COL_FILE, COL_LINE, COL_CONDITION, COL_BLANK, COL_FULL = list( +COL_FILE, COL_LINE, COL_CONDITION, COL_FULL = list( range(COLUMN_COUNT + EXTRA_COLUMNS)) -COLUMN_HEADERS = (_("File"), _("Line"), _("Condition"), ("")) +COLUMN_HEADERS = (_("File"), _("Line"), _("Condition")) class BreakpointTableViewActions: @@ -47,7 +46,7 @@ class BreakpointTableViewActions: EditBreakpoint = 'edit_breakpoint_action' -# --- Widgets +# --- Model # ---------------------------------------------------------------------------- class BreakpointTableModel(QAbstractTableModel): """ @@ -84,7 +83,7 @@ def set_data(self, data): for item in data[key]: # Store full file name in last position, which is not shown self.breakpoints.append((disambiguate_fname(files, key), - item[0], item[1], "", key)) + item[0], item[1], key)) self.reset() def rowCount(self, qindex=QModelIndex()): @@ -110,8 +109,6 @@ def sort(self, column, order=Qt.DescendingOrder): pass elif column == COL_CONDITION: pass - elif column == COL_BLANK: - pass self.reset() @@ -163,17 +160,14 @@ def reset(self): self.endResetModel() -class BreakpointDelegate(QItemDelegate): - - def __init__(self, parent=None): - super().__init__(parent) - - class BreakpointTableView(QTableView, SpyderWidgetMixin): """ Table to display code breakpoints. """ + # Constants + MIN_WIDTH = 300 + # Signals sig_clear_all_breakpoints_requested = Signal() sig_clear_breakpoint_requested = Signal(str, int) @@ -189,19 +183,32 @@ def __init__(self, parent, data): # Widgets self.model = BreakpointTableModel(self, data) - self.delegate = BreakpointDelegate(self) # Setup self.setSortingEnabled(False) self.setSelectionBehavior(self.SelectRows) self.setSelectionMode(self.SingleSelection) self.setModel(self.model) - self.setItemDelegate(self.delegate) - self.adjust_columns() - self.columnAt(0) + self._adjust_columns() self.horizontalHeader().setStretchLastSection(True) + self.verticalHeader().hide() + self.setMinimumWidth(self.MIN_WIDTH) + + # Attributes + self._update_when_shown = True + + # Style + # Remove border radius to the left and add it to the right. + css = qstylizer.style.StyleSheet() + css.setValues( + borderTopLeftRadius='0px', + borderBottomLeftRadius='0px', + borderTopRightRadius=f'{QStylePalette.SIZE_BORDER_RADIUS}', + borderBottomRightRadius=f'{QStylePalette.SIZE_BORDER_RADIUS}', + ) + self.setStyleSheet(css.toString()) - # --- SpyderWidgetMixin API + # ---- SpyderWidgetMixin API # ------------------------------------------------------------------------ def setup(self): clear_all_action = self.create_action( @@ -224,7 +231,7 @@ def setup(self): for item in [clear_all_action, clear_action, edit_action]: self.add_item_to_menu(item, menu=self.popup_menu) - # --- Qt overrides + # ---- Qt overrides # ------------------------------------------------------------------------ def contextMenuEvent(self, event): """ @@ -258,7 +265,19 @@ def mouseDoubleClickEvent(self, event): if index_clicked.column() == COL_CONDITION: self.sig_conditional_breakpoint_requested.emit() - # --- API + def showEvent(self, event): + """Adjustments when the widget is shown.""" + if self._update_when_shown: + self._adjust_file_column() + self._update_when_shown = False + super().showEvent(event) + + def resizeEvent(self, event): + """Adjustments when the widget is resized.""" + self._adjust_file_column() + super().resizeEvent(event) + + # ---- Public API # ------------------------------------------------------------------------ def set_data(self, data): """ @@ -270,16 +289,10 @@ def set_data(self, data): Breakpoint data to use. """ self.model.set_data(data) - self.adjust_columns() + if self.model.rowCount() > 0: + self._adjust_columns() self.sortByColumn(COL_FILE, Qt.DescendingOrder) - def adjust_columns(self): - """ - Resize three first columns to contents. - """ - for col in range(COLUMN_COUNT - 1): - self.resizeColumnToContents(col) - def clear_breakpoints(self): """ Clear selected row breakpoint. @@ -305,122 +318,14 @@ def edit_breakpoints(self): self.sig_edit_goto_requested.emit(filename, lineno, '') self.sig_conditional_breakpoint_requested.emit() - -class BreakpointWidget(PluginMainWidget): - """ - Breakpoints widget. - """ - - # --- Signals - # ------------------------------------------------------------------------ - sig_clear_all_breakpoints_requested = Signal() - """ - This signal is emitted to send a request to clear all assigned - breakpoints. - """ - - sig_clear_breakpoint_requested = Signal(str, int) - """ - This signal is emitted to send a request to clear a single breakpoint. - - Parameters - ---------- - filename: str - The path to filename cotaining the breakpoint. - line_number: int - The line number of the breakpoint. - """ - - sig_edit_goto_requested = Signal(str, int, str) - """ - Send a request to open a file in the editor at a given row and word. - - Parameters - ---------- - filename: str - The path to the filename containing the breakpoint. - line_number: int - The line number of the breakpoint. - word: str - Text `word` to select on given `line_number`. - """ - - sig_conditional_breakpoint_requested = Signal() - """ - Send a request to set/edit a condition on a single selected breakpoint. - """ - - def __init__(self, name=None, plugin=None, parent=None): - super().__init__(name, plugin, parent=parent) - - # Widgets - self.breakpoints_table = BreakpointTableView(self, {}) - - # Layout - layout = QVBoxLayout() - layout.addWidget(self.breakpoints_table) - self.setLayout(layout) - - # Signals - bpt = self.breakpoints_table - bpt.sig_clear_all_breakpoints_requested.connect( - self.sig_clear_all_breakpoints_requested) - bpt.sig_clear_breakpoint_requested.connect( - self.sig_clear_breakpoint_requested) - bpt.sig_edit_goto_requested.connect(self.sig_edit_goto_requested) - bpt.sig_conditional_breakpoint_requested.connect( - self.sig_conditional_breakpoint_requested) - - # --- PluginMainWidget API - # ------------------------------------------------------------------------ - def get_title(self): - return _('Breakpoints') - - def get_focus_widget(self): - return self.breakpoints_table - - def setup(self): - self.breakpoints_table.setup() - - def update_actions(self): - rows = self.breakpoints_table.selectionModel().selectedRows() - c_row = rows[0] if rows else None - - enabled = (bool(self.breakpoints_table.model.breakpoints) - and c_row is not None) - clear_action = self.get_action( - BreakpointTableViewActions.ClearBreakpoint) - edit_action = self.get_action( - BreakpointTableViewActions.EditBreakpoint) - clear_action.setEnabled(enabled) - edit_action.setEnabled(enabled) - - # --- Public API + # ---- Private API # ------------------------------------------------------------------------ - def set_data(self, data): + def _adjust_columns(self): """ - Set breakpoint data on widget. - - Parameters - ---------- - data: dict - Breakpoint data to use. + Resize three first columns to contents. """ - self.breakpoints_table.set_data(data) - - -# ============================================================================= -# Tests -# ============================================================================= -def test(): - """Run breakpoint widget test.""" - from spyder.utils.qthelpers import qapplication - - app = qapplication() - widget = BreakpointWidget() - widget.show() - sys.exit(app.exec_()) - + for col in range(COLUMN_COUNT - 1): + self.resizeColumnToContents(col) -if __name__ == '__main__': - test() + def _adjust_file_column(self): + self.horizontalHeader().resizeSection(COL_FILE, self.width() / 2) diff --git a/spyder/plugins/debugger/widgets/main_widget.py b/spyder/plugins/debugger/widgets/main_widget.py index 46ac1d191bc..a3d8521873d 100644 --- a/spyder/plugins/debugger/widgets/main_widget.py +++ b/spyder/plugins/debugger/widgets/main_widget.py @@ -8,16 +8,25 @@ Debugger Main Plugin Widget. """ +# Standard library imports +import os.path as osp + # Third party imports from qtpy.QtCore import Signal, Slot +from qtpy.QtWidgets import QHBoxLayout, QSplitter # Local imports +import qstylizer.style +from spyder.api.config.decorators import on_conf_change from spyder.api.shellconnect.main_widget import ShellConnectMainWidget from spyder.api.translations import _ from spyder.config.manager import CONF from spyder.config.gui import get_color_scheme from spyder.plugins.debugger.widgets.framesbrowser import ( FramesBrowser, FramesBrowserState) +from spyder.plugins.debugger.widgets.breakpoint_table_view import ( + BreakpointTableView, BreakpointTableViewActions) +from spyder.utils.palette import QStylePalette # ============================================================================= @@ -43,6 +52,8 @@ class DebuggerBreakpointActions: ToggleBreakpoint = 'toggle breakpoint' ToggleConditionalBreakpoint = 'toggle conditional breakpoint' ClearAllBreakpoints = 'clear all breakpoints' + ShowBreakpointsTable = 'show breakpoint table' + ToggleBreakpointsTable = 'toggle breakpoint table' class DebuggerWidgetOptionsMenuSections: @@ -115,6 +126,18 @@ class DebuggerWidget(ShellConnectMainWidget): sig_clear_all_breakpoints = Signal() """Clear all breakpoints in all files.""" + sig_clear_breakpoint = Signal(str, int) + """ + Clear single breakpoint. + + Parameters + ---------- + filename: str + The filename + line_number: int + The line number + """ + sig_pdb_state_changed = Signal(bool) """ This signal is emitted every time a Pdb interaction happens. @@ -137,12 +160,53 @@ class DebuggerWidget(ShellConnectMainWidget): The line number the debugger stepped in """ + sig_switch_to_plugin_requested = Signal() + """ + This signal will request to change the focus to the plugin. + """ + def __init__(self, name=None, plugin=None, parent=None): - super().__init__(name, plugin, parent) + super().__init__(name, plugin, parent, set_layout=False) # Widgets self.context_menu = None self.empty_context_menu = None + self.breakpoints_table = BreakpointTableView(self, {}) + self.breakpoints_table.hide() + + # Attributes + self._update_when_shown = True + + # Splitter + self.splitter = QSplitter(self) + self.splitter.addWidget(self._stack) + self.splitter.addWidget(self.breakpoints_table) + self.splitter.setContentsMargins(0, 0, 0, 0) + self.splitter.setChildrenCollapsible(False) + + # This is necessary so that the border radius is maintained when + # showing/hiding the breakpoints table + self.splitter.setStyleSheet( + f"border-radius: {QStylePalette.SIZE_BORDER_RADIUS}") + + # Layout + # Create the layout. + layout = QHBoxLayout() + layout.setSpacing(0) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(self.splitter) + self.setLayout(layout) + + # Signals + bpt = self.breakpoints_table + bpt.sig_clear_all_breakpoints_requested.connect( + self.sig_clear_all_breakpoints) + bpt.sig_clear_breakpoint_requested.connect( + self.sig_clear_breakpoint) + bpt.sig_edit_goto_requested.connect(self.sig_edit_goto) + bpt.sig_conditional_breakpoint_requested.connect( + self.sig_toggle_conditional_breakpoints) + self.sig_breakpoints_saved.connect(self.set_data) # ---- PluginMainWidget API # ------------------------------------------------------------------------ @@ -154,6 +218,10 @@ def get_focus_widget(self): def setup(self): """Setup the widget.""" + + self.breakpoints_table.setup() + self.set_data() + # ---- Options menu actions exclude_internal_action = self.create_action( DebuggerWidgetActions.ToggleExcludeInternal, @@ -262,6 +330,21 @@ def setup(self): triggered=self.sig_clear_all_breakpoints ) + self.create_action( + DebuggerBreakpointActions.ShowBreakpointsTable, + _("List breakpoints"), + triggered=self.list_breakpoints, + ) + + toggle_breakpoints_action = self.create_action( + DebuggerBreakpointActions.ToggleBreakpointsTable, + _("Show breakpoints"), + icon=self.create_icon('breakpoint_big'), + toggled=True, + initial=self.get_conf('breakpoints_table_visible'), + option='breakpoints_table_visible' + ) + # ---- Context menu actions self.view_locals_action = self.create_action( DebuggerContextMenuActions.ViewLocalsAction, @@ -289,7 +372,8 @@ def setup(self): goto_cursor_action, enter_debug_action, inspect_action, - search_action]: + search_action, + toggle_breakpoints_action]: self.add_item_to_toolbar( item, toolbar=main_toolbar, @@ -355,6 +439,36 @@ def update_actions(self): action = self.get_action(action_name) action.setEnabled(pdb_prompt) + rows = self.breakpoints_table.selectionModel().selectedRows() + initial_row = rows[0] if rows else None + + enabled = ( + bool(self.breakpoints_table.model.breakpoints) + and initial_row is not None + ) + clear_action = self.get_action( + BreakpointTableViewActions.ClearBreakpoint) + edit_action = self.get_action( + BreakpointTableViewActions.EditBreakpoint) + clear_action.setEnabled(enabled) + edit_action.setEnabled(enabled) + + @on_conf_change(option='breakpoints_table_visible') + def on_breakpoints_table_option_update(self, value): + action = self.get_action( + DebuggerBreakpointActions.ToggleBreakpointsTable) + + if value: + self.breakpoints_table.show() + action.setToolTip(_("Hide breakpoints")) + self._update_stylesheet(is_table_shown=True) + self._stack.setMinimumWidth(450) + else: + self.breakpoints_table.hide() + action.setToolTip(_("Show breakpoints")) + self._update_stylesheet(is_table_shown=False) + self._stack.setMinimumWidth(100) + # ---- ShellConnectMainWidget API # ------------------------------------------------------------------------ def create_new_widget(self, shellwidget): @@ -568,3 +682,93 @@ def debug_command(self, command): if widget is None: return widget.shellwidget.pdb_execute_command(command) + + def load_data(self): + """ + Load breakpoint data from configuration file. + """ + breakpoints_dict = self.get_conf( + "breakpoints", + default={}, + ) + for filename in list(breakpoints_dict.keys()): + if not osp.isfile(filename): + breakpoints_dict.pop(filename) + continue + # Make sure we don't have the same file under different names + new_filename = osp.normcase(filename) + if new_filename != filename: + bp = breakpoints_dict.pop(filename) + if new_filename in breakpoints_dict: + breakpoints_dict[new_filename].extend(bp) + else: + breakpoints_dict[new_filename] = bp + + return breakpoints_dict + + def set_data(self, data=None): + """ + Set breakpoint data on widget. + + Parameters + ---------- + data: dict, optional + Breakpoint data to use. If None, data from the configuration + will be loaded. Default is None. + """ + if data is None: + data = self.load_data() + self.breakpoints_table.set_data(data) + + def list_breakpoints(self): + """Show breakpoints state and switch to plugin.""" + self.set_conf('breakpoints_table_visible', True) + self.sig_switch_to_plugin_requested.emit() + + # ---- Qt methods + # ------------------------------------------------------------------------ + def showEvent(self, event): + """Adjustments when the widget is shown.""" + if self._update_when_shown: + # We only do this the first time the widget is shown to not change + # the splitter widths that users can set themselves. + self._update_splitter_widths(self.width()) + self._update_when_shown = False + + super().showEvent(event) + + # ---- Private API + # ------------------------------------------------------------------------ + def _update_splitter_widths(self, base_width): + """ + Update the splitter widths to provide the breakpoints table with a + fixed minimum width. + + Parameters + ---------- + base_width: int + The available splitter width. + """ + if (base_width / 3) > self.breakpoints_table.MIN_WIDTH: + table_width = base_width / 3 + else: + table_width = self.breakpoints_table.MIN_WIDTH + + if base_width - table_width > 0: + self.splitter.setSizes([base_width - table_width, table_width]) + + def _update_stylesheet(self, is_table_shown=False): + """Update stylesheet when the breakpoints table is shown/hidden.""" + # Remove right border radius for stack when table is shown and restore + # it when hidden. + if is_table_shown: + border_radius = '0px' + else: + border_radius = QStylePalette.SIZE_BORDER_RADIUS + + css = qstylizer.style.StyleSheet() + css.setValues( + borderTopRightRadius=f'{border_radius}', + borderBottomRightRadius=f'{border_radius}', + ) + self._stack.setStyleSheet(css.toString()) diff --git a/spyder/plugins/editor/confpage.py b/spyder/plugins/editor/confpage.py index 5f3ac1d0feb..e25162d507d 100644 --- a/spyder/plugins/editor/confpage.py +++ b/spyder/plugins/editor/confpage.py @@ -48,7 +48,7 @@ def setup_page(self): 'indent_guides') showcodefolding_box = newcb(_("Show code folding"), 'code_folding') linenumbers_box = newcb(_("Show line numbers"), 'line_numbers') - breakpoints_box = newcb(_("Show breakpoints"), 'breakpoints_panel', + breakpoints_box = newcb(_("Show breakpoints"), 'editor_debugger_panel', section='debugger') blanks_box = newcb(_("Show blank spaces"), 'blank_spaces') currentline_box = newcb(_("Highlight current line"), diff --git a/spyder/plugins/layout/plugin.py b/spyder/plugins/layout/plugin.py index cf106349663..ca0423563be 100644 --- a/spyder/plugins/layout/plugin.py +++ b/spyder/plugins/layout/plugin.py @@ -895,7 +895,7 @@ def create_plugins_menu(self): order = ['editor', 'ipython_console', 'variable_explorer', 'debugger', 'help', 'plots', None, 'explorer', 'outline_explorer', 'project_explorer', 'find_in_files', None, - 'historylog', 'profiler', 'breakpoints', 'pylint', None, + 'historylog', 'profiler', 'pylint', None, 'onlinehelp', 'internal_console', None] for plugin in self.get_dockable_plugins(): diff --git a/spyder/plugins/mainmenu/api.py b/spyder/plugins/mainmenu/api.py index a28733da19e..d7499dc5d25 100644 --- a/spyder/plugins/mainmenu/api.py +++ b/spyder/plugins/mainmenu/api.py @@ -69,7 +69,6 @@ class DebugMenuSections: StartDebug = 'start_debug_section' ControlDebug = 'control_debug_section' EditBreakpoints = 'edit_breakpoints_section' - ListBreakpoints = 'list_breakpoints_section' class ConsolesMenuSections: diff --git a/spyder/plugins/shortcuts/tests/test_shortcuts.py b/spyder/plugins/shortcuts/tests/test_shortcuts.py index b2ca4b665f8..1ab779b1137 100644 --- a/spyder/plugins/shortcuts/tests/test_shortcuts.py +++ b/spyder/plugins/shortcuts/tests/test_shortcuts.py @@ -97,7 +97,7 @@ def test_shortcuts_filtering(shortcut_table): assert not shortcut_table.isSortingEnabled() # Six hits (causes a bit of an issue to hardcode it like this if new # shortcuts are added...) - assert shortcut_table.model().rowCount() == 12 + assert shortcut_table.model().rowCount() == 13 # Remove filter text shortcut_table.finder = FilterTextMock('') shortcut_table.set_regex()