From 47e848adb92e76a4d82b9590bd5fd3a27e840022 Mon Sep 17 00:00:00 2001 From: goanpeca Date: Sat, 21 Mar 2020 09:34:48 -0500 Subject: [PATCH] Migrate breakpoint plugin to new API --- spyder/api/plugins.py | 1 - spyder/app/mainwindow.py | 10 +- spyder/plugins/breakpoints/api.py | 14 + spyder/plugins/breakpoints/plugin.py | 165 +++++--- .../breakpoints/widgets/breakpointsgui.py | 282 ------------- .../breakpoints/widgets/main_widget.py | 395 ++++++++++++++++++ 6 files changed, 515 insertions(+), 352 deletions(-) create mode 100644 spyder/plugins/breakpoints/api.py delete mode 100644 spyder/plugins/breakpoints/widgets/breakpointsgui.py create mode 100644 spyder/plugins/breakpoints/widgets/main_widget.py diff --git a/spyder/api/plugins.py b/spyder/api/plugins.py index 57d85664c47..d5acec2e7ec 100644 --- a/spyder/api/plugins.py +++ b/spyder/api/plugins.py @@ -49,7 +49,6 @@ from spyder.utils import icon_manager as ima from spyder.utils.qthelpers import create_action - # Localization _ = get_translation('spyder') diff --git a/spyder/app/mainwindow.py b/spyder/app/mainwindow.py index 33272f7dd4e..0b2eec37ec6 100644 --- a/spyder/app/mainwindow.py +++ b/spyder/app/mainwindow.py @@ -1268,7 +1268,15 @@ def create_edit_action(text, tr_text, icon): # Load other plugins (former external plugins) # TODO: Use this bucle to load all internall plugins and remove # duplicated code - other_plugins = ['breakpoints', 'profiler', 'pylint'] + + # Breakpoints + if CONF.get('breakpoints', 'enable'): + from spyder.plugins.breakpoints.plugin import Breakpoints + self.breakpoints = Breakpoints(self, configuration=CONF) + self.register_plugin(self.breakpoints) + self.thirdparty_plugins.append(self.breakpoints) + + other_plugins = ['profiler', 'pylint'] for plugin_name in other_plugins: if CONF.get(plugin_name, 'enable'): module = importlib.import_module( diff --git a/spyder/plugins/breakpoints/api.py b/spyder/plugins/breakpoints/api.py new file mode 100644 index 00000000000..fe356dbbe79 --- /dev/null +++ b/spyder/plugins/breakpoints/api.py @@ -0,0 +1,14 @@ +# -*- 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 index 47a97b77440..b9eef88bce4 100644 --- a/spyder/plugins/breakpoints/plugin.py +++ b/spyder/plugins/breakpoints/plugin.py @@ -6,91 +6,120 @@ # (see spyder/__init__.py for details) # ----------------------------------------------------------------------------- - -"""Breakpoint Plugin.""" - - -# pylint: disable=C0103 -# pylint: disable=R0903 -# pylint: disable=R0911 -# pylint: disable=R0201 +""" +Breakpoint Plugin. +""" # Standard library imports -import os.path as osp +import os # Third party imports -from qtpy.QtWidgets import QVBoxLayout +from qtpy.QtCore import Signal # Local imports -from spyder.config.base import _ -from spyder.utils import icon_manager as ima -from spyder.utils.qthelpers import create_action -from spyder.api.plugins import SpyderPluginWidget +from spyder.api.plugins import ApplicationMenus, Plugins, SpyderDockablePlugin +from spyder.api.translations import get_translation +from spyder.plugins.breakpoints.widgets.main_widget import BreakpointWidget -from .widgets.breakpointsgui import BreakpointWidget +# Localization +_ = get_translation('spyder') -class Breakpoints(SpyderPluginWidget): - """Breakpoint list""" - CONF_SECTION = 'breakpoints' - CONF_FILE = False - def __init__(self, parent=None): - """Initialization.""" - super().__init__(parent) +# --- Constants +# ---------------------------------------------------------------------------- +class BreakpointsActions: + ListBreakpoints = 'list_breakpoints_action' + - self.breakpoints = BreakpointWidget(self, - options_button=self.options_button) +# --- Plugin +# ---------------------------------------------------------------------------- +class Breakpoints(SpyderDockablePlugin): + """ + Breakpoint list Plugin. + """ + NAME = 'breakpoints' + REQUIRES = [Plugins.Editor] + TABIFY = [Plugins.Help] + WIDGET_CLASS = BreakpointWidget + CONF_SECTION = NAME + CONF_FILE = False + + # --- Signals + # ------------------------------------------------------------------------ + # Send a request to clear all assigned breakpoints + sig_clear_all_breakpoints_requested = Signal() - layout = QVBoxLayout() - layout.addWidget(self.breakpoints) - self.setLayout(layout) + # Send a request to clear a single selected breakpoint + sig_clear_breakpoint_requested = Signal(str, int) - self.breakpoints.set_data() + # Send a request to open file in the editor at a given row and column + sig_edit_goto_requested = Signal(str, int, str) - path = osp.join(self.PLUGIN_PATH, self.IMG_PATH) - self.icon = ima.icon('breakpoints', icon_path=path) + # Send a request to set/edit a condition on a single selected breakpoint + sig_set_or_edit_conditional_breakpoint_requested = Signal() - # ----- SpyderPluginWidget API -------------------------------------------- - def get_plugin_title(self): - """Return widget title""" + # --- SpyderDockablePlugin API + # ------------------------------------------------------------------------ + def get_name(self): return _("Breakpoints") - def get_plugin_icon(self): - """Return widget icon""" - return self.icon + def get_description(self): + return _("Manage code breakpoints in a unified pane.") + + def get_icon(self): + path = os.path.join(self.get_path(), self.IMG_PATH) + return self.create_icon('breakpoints', path=path) + + def register(self): + widget = self.get_widget() + editor = self.get_plugin(Plugins.Editor) + + # TODO: change name of this signal on editor + editor.breakpoints_saved.connect(self.set_data) + widget.sig_clear_all_breakpoints_requested.connect( + editor.clear_all_breakpoints) + widget.sig_clear_breakpoint_requested.connect(editor.clear_breakpoint) + widget.sig_edit_goto_requested.connect(editor.load) + widget.sig_set_or_edit_conditional_breakpoint_requested.connect( + editor.set_or_edit_conditional_breakpoint) + + 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_set_or_edit_conditional_breakpoint_requested.connect( + self.sig_set_or_edit_conditional_breakpoint_requested) + + list_action = self.create_action( + BreakpointsActions.ListBreakpoints, + _("List breakpoints"), + triggered=lambda: self.switch_to_plugin(), + icon=self.get_icon(), + ) + + # TODO: Fix location once the sections are defined + debug_menu = self.get_application_menu(ApplicationMenus.Debug) + self.add_item_to_application_menu(list_action, debug_menu) + editor.pythonfile_dependent_actions += [list_action] + + # --- API + # ------------------------------------------------------------------------ + def set_data(self): + """ + Set breakpoint data on widget. + """ + self.get_widget().set_data(self.load_data()) - def get_focus_widget(self): + def load_data(self): """ - Return the widget to give focus to when - this plugin's dockwidget is raised on top-level + Load breakpoint data from configuration file. """ - return self.breakpoints.dictwidget - - def on_first_registration(self): - """Action to be performed on first plugin registration""" - self.tabify(self.main.help) - - def register_plugin(self): - """Register plugin in Spyder's main window""" - self.breakpoints.edit_goto.connect(self.main.editor.load) - self.breakpoints.clear_all_breakpoints.connect( - self.main.editor.clear_all_breakpoints) - self.breakpoints.clear_breakpoint.connect( - self.main.editor.clear_breakpoint) - self.main.editor.breakpoints_saved.connect(self.breakpoints.set_data) - self.breakpoints.set_or_edit_conditional_breakpoint.connect( - self.main.editor.set_or_edit_conditional_breakpoint) - - self.add_dockwidget() - - list_action = create_action(self, _("List breakpoints"), - triggered=self.show, icon=self.icon) - list_action.setEnabled(True) - pos = self.main.debug_menu_actions.index('list_breakpoints') - self.main.debug_menu_actions.insert(pos, list_action) - self.main.editor.pythonfile_dependent_actions += [list_action] - - def show(self): - """Show the breakpoints dockwidget""" - self.switch_to_plugin() + breakpoints_dict = self.get_conf_option('breakpoints', default={}, + section='run') + for filename in list(breakpoints_dict.keys()): + if not os.path.isfile(filename): + breakpoints_dict.pop(filename) + + return breakpoints_dict diff --git a/spyder/plugins/breakpoints/widgets/breakpointsgui.py b/spyder/plugins/breakpoints/widgets/breakpointsgui.py deleted file mode 100644 index 8e0b227f669..00000000000 --- a/spyder/plugins/breakpoints/widgets/breakpointsgui.py +++ /dev/null @@ -1,282 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright © Spyder Project Contributors -# based loosley on pylintgui.py by Pierre Raybaut -# Licensed under the terms of the MIT License -# (see spyder/__init__.py for details) - -"""Breakpoint widget""" - -# pylint: disable=C0103 -# pylint: disable=R0903 -# pylint: disable=R0911 -# pylint: disable=R0201 - -# Standard library imports -import os.path as osp -import sys - -# Third party imports -from qtpy import API -from qtpy.compat import to_qvariant -from qtpy.QtCore import QAbstractTableModel, QModelIndex, Qt, Signal -from qtpy.QtWidgets import (QItemDelegate, QMenu, QTableView, QHBoxLayout, - QVBoxLayout, QWidget) - -# Local imports -from spyder.config.base import get_translation -from spyder.config.manager import CONF -from spyder.utils.qthelpers import (add_actions, create_action, - create_plugin_layout) -from spyder.utils.sourcecode import disambiguate_fname - -# This is needed for testing this module as a stand alone script -try: - _ = get_translation("breakpoints", "spyder_breakpoints") -except KeyError: - import gettext - _ = gettext.gettext - -COLUMN_COUNT = 4 -EXTRA_COLUMNS = 1 -COL_FILE, COL_LINE, COL_CONDITION, COL_BLANK, COL_FULL = list( - range(COLUMN_COUNT + EXTRA_COLUMNS)) -COLUMN_HEADERS = (_("File"), _("Line"), _("Condition"), ("")) - - -class BreakpointTableModel(QAbstractTableModel): - """ - Table model for breakpoints dictionary - """ - - def __init__(self, parent, data): - super().__init__(parent) - if data is None: - data = {} - self._data = None - self.breakpoints = None - self.set_data(data) - - def set_data(self, data): - """Set model data""" - self._data = data - self.breakpoints = [] - files = [] - # Generate list of filenames with active breakpoints - for key in data: - if data[key] and key not in files: - files.append(key) - # Insert items - for key in files: - 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)) - self.reset() - - def rowCount(self, qindex=QModelIndex()): - """Array row number""" - return len(self.breakpoints) - - def columnCount(self, qindex=QModelIndex()): - """Array column count""" - return COLUMN_COUNT - - def sort(self, column, order=Qt.DescendingOrder): - """Overriding sort method""" - if column == COL_FILE: - self.breakpoints.sort(key=lambda breakp: int(breakp[COL_LINE])) - self.breakpoints.sort(key=lambda breakp: breakp[COL_FILE]) - elif column == COL_LINE: - pass - elif column == COL_CONDITION: - pass - elif column == COL_BLANK: - pass - self.reset() - - def headerData(self, section, orientation, role=Qt.DisplayRole): - """Overriding method headerData""" - if role != Qt.DisplayRole: - return to_qvariant() - i_column = int(section) - if orientation == Qt.Horizontal: - return to_qvariant(COLUMN_HEADERS[i_column]) - else: - return to_qvariant() - - def get_value(self, index): - """Return current value""" - return self.breakpoints[index.row()][index.column()] - - def data(self, index, role=Qt.DisplayRole): - """Return data at table index""" - if not index.isValid(): - return to_qvariant() - if role == Qt.DisplayRole: - value = self.get_value(index) - return to_qvariant(value) - elif role == Qt.TextAlignmentRole: - if index.column() == COL_LINE: - # Align line number right - return to_qvariant(int(Qt.AlignRight | Qt.AlignVCenter)) - else: - return to_qvariant(int(Qt.AlignLeft | Qt.AlignVCenter)) - elif role == Qt.ToolTipRole: - if index.column() == COL_FILE: - # Return full file name (in last position) - value = self.breakpoints[index.row()][COL_FULL] - return to_qvariant(value) - else: - return to_qvariant() - - def reset(self): - self.beginResetModel() - self.endResetModel() - - -class BreakpointDelegate(QItemDelegate): - def __init__(self, parent=None): - super().__init__(parent) - - -class BreakpointTableView(QTableView): - edit_goto = Signal(str, int, str) - clear_breakpoint = Signal(str, int) - clear_all_breakpoints = Signal() - set_or_edit_conditional_breakpoint = Signal() - - def __init__(self, parent, data): - super().__init__(parent) - self.model = BreakpointTableModel(self, data) - self.setModel(self.model) - self.delegate = BreakpointDelegate(self) - self.setItemDelegate(self.delegate) - - self.setup_table() - - def setup_table(self): - """Setup table""" - self.horizontalHeader().setStretchLastSection(True) - self.adjust_columns() - self.columnAt(0) - # Sorting columns - self.setSortingEnabled(False) - 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 mouseDoubleClickEvent(self, event): - """Reimplement Qt method""" - index_clicked = self.indexAt(event.pos()) - if self.model.breakpoints: - c_row = index_clicked.row() - filename = self.model.breakpoints[c_row][COL_FULL] - line_number_str = self.model.breakpoints[c_row][COL_LINE] - self.edit_goto.emit(filename, int(line_number_str), '') - if index_clicked.column() == COL_CONDITION: - self.set_or_edit_conditional_breakpoint.emit() - - def contextMenuEvent(self, event): - index_clicked = self.indexAt(event.pos()) - actions = [] - self.popup_menu = QMenu(self) - clear_all_breakpoints_action = create_action( - self, _("Clear breakpoints in all files"), - triggered=lambda: self.clear_all_breakpoints.emit()) - actions.append(clear_all_breakpoints_action) - if self.model.breakpoints: - c_row = index_clicked.row() - filename = self.model.breakpoints[c_row][COL_FULL] - lineno = int(self.model.breakpoints[c_row][COL_LINE]) - - def clear_slot(filename=filename, lineno=lineno): - return self.clear_breakpoint.emit(filename, lineno) - - def edit_slot(filename=filename, lineno=lineno): - self.edit_goto.emit(filename, lineno, '') - self.set_or_edit_conditional_breakpoint.emit() - - clear_breakpoint_action = create_action( - self, _("Clear this breakpoint"), - triggered=clear_slot) - actions.insert(0, clear_breakpoint_action) - - edit_breakpoint_action = create_action( - self, _("Edit this breakpoint"), - triggered=edit_slot) - actions.append(edit_breakpoint_action) - add_actions(self.popup_menu, actions) - self.popup_menu.popup(event.globalPos()) - event.accept() - - -class BreakpointWidget(QWidget): - """ - Breakpoint widget - """ - VERSION = '1.0.0' - clear_all_breakpoints = Signal() - set_or_edit_conditional_breakpoint = Signal() - clear_breakpoint = Signal(str, int) - edit_goto = Signal(str, int, str) - - def __init__(self, parent, options_button=None): - super().__init__(parent) - - self.setWindowTitle("Breakpoints") - self.dictwidget = BreakpointTableView(self, - self._load_all_breakpoints()) - if options_button: - btn_layout = QHBoxLayout() - btn_layout.setAlignment(Qt.AlignLeft) - btn_layout.addStretch() - btn_layout.addWidget(options_button, Qt.AlignRight) - layout = create_plugin_layout(btn_layout, self.dictwidget) - else: - layout = QVBoxLayout() - layout.addWidget(self.dictwidget) - self.setLayout(layout) - self.dictwidget.clear_all_breakpoints.connect( - lambda: self.clear_all_breakpoints.emit()) - self.dictwidget.clear_breakpoint.connect( - lambda s1, lino: self.clear_breakpoint.emit(s1, lino)) - self.dictwidget.edit_goto.connect( - lambda s1, lino, s2: self.edit_goto.emit(s1, lino, s2)) - self.dictwidget.set_or_edit_conditional_breakpoint.connect( - lambda: self.set_or_edit_conditional_breakpoint.emit()) - - def _load_all_breakpoints(self): - bp_dict = CONF.get('run', 'breakpoints', {}) - for filename in list(bp_dict.keys()): - if not osp.isfile(filename): - bp_dict.pop(filename) - return bp_dict - - def get_data(self): - pass - - def set_data(self): - bp_dict = self._load_all_breakpoints() - self.dictwidget.model.set_data(bp_dict) - self.dictwidget.adjust_columns() - self.dictwidget.sortByColumn(COL_FILE, Qt.DescendingOrder) - - -# ============================================================================= -# Tests -# ============================================================================= -def test(): - """Run breakpoint widget test""" - from spyder.utils.qthelpers import qapplication - app = qapplication() - widget = BreakpointWidget(None) - widget.show() - sys.exit(app.exec_()) - - -if __name__ == '__main__': - test() diff --git a/spyder/plugins/breakpoints/widgets/main_widget.py b/spyder/plugins/breakpoints/widgets/main_widget.py new file mode 100644 index 00000000000..100a72dff92 --- /dev/null +++ b/spyder/plugins/breakpoints/widgets/main_widget.py @@ -0,0 +1,395 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# based loosley on pylintgui.py by Pierre Raybaut +# Licensed under the terms of the MIT License +# (see spyder/__init__.py for details) + +""" +Breakpoint widget. +""" + +# pylint: disable=C0103 +# pylint: disable=R0903 +# pylint: disable=R0911 +# pylint: disable=R0201 + +# Standard library imports +import sys + +# Third party imports +from qtpy import API +from qtpy.compat import to_qvariant +from qtpy.QtCore import QAbstractTableModel, QModelIndex, Qt, Signal +from qtpy.QtWidgets import QItemDelegate, QTableView, QVBoxLayout + +# Local imports +from spyder.api.translations import get_translation +from spyder.api.widgets import (PluginMainWidgetMenus, PluginMainWidget, + SpyderWidgetMixin) +from spyder.utils.sourcecode import disambiguate_fname + + +# Localization +_ = get_translation('spyder') + + +# --- Constants +# ---------------------------------------------------------------------------- +COLUMN_COUNT = 4 +EXTRA_COLUMNS = 1 +COL_FILE, COL_LINE, COL_CONDITION, COL_BLANK, COL_FULL = list( + range(COLUMN_COUNT + EXTRA_COLUMNS)) +COLUMN_HEADERS = (_("File"), _("Line"), _("Condition"), ("")) + + +class BreakpointTableViewActions: + # Triggers + ClearAllBreakpoints = 'clear_all_breakpoints_action' + ClearBreakpoint = 'clear_breakpoint_action' + EditBreakpoint = 'edit_breakpoint_action' + + +# --- Widgets +# ---------------------------------------------------------------------------- +class BreakpointTableModel(QAbstractTableModel): + """ + Table model for breakpoints dictionary. + """ + + def __init__(self, parent, data): + super().__init__(parent) + + self._data = {} if data is None else data + self.breakpoints = None + + self.set_data(self._data) + + def set_data(self, data): + """ + Set model data. + """ + self._data = data + self.breakpoints = [] + files = [] + # Generate list of filenames with active breakpoints + for key in data: + if data[key] and key not in files: + files.append(key) + + # Insert items + for key in files: + 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)) + self.reset() + + def rowCount(self, qindex=QModelIndex()): + """ + Array row number. + """ + return len(self.breakpoints) + + def columnCount(self, qindex=QModelIndex()): + """ + Array column count. + """ + return COLUMN_COUNT + + def sort(self, column, order=Qt.DescendingOrder): + """ + Overriding sort method. + """ + if column == COL_FILE: + self.breakpoints.sort(key=lambda breakp: int(breakp[COL_LINE])) + self.breakpoints.sort(key=lambda breakp: breakp[COL_FILE]) + elif column == COL_LINE: + pass + elif column == COL_CONDITION: + pass + elif column == COL_BLANK: + pass + + self.reset() + + def headerData(self, section, orientation, role=Qt.DisplayRole): + """ + Overriding method headerData. + """ + if role != Qt.DisplayRole: + return to_qvariant() + + i_column = int(section) + if orientation == Qt.Horizontal: + return to_qvariant(COLUMN_HEADERS[i_column]) + else: + return to_qvariant() + + def get_value(self, index): + """ + Return current value. + """ + return self.breakpoints[index.row()][index.column()] + + def data(self, index, role=Qt.DisplayRole): + """ + Return data at table index. + """ + if not index.isValid(): + return to_qvariant() + + if role == Qt.DisplayRole: + value = self.get_value(index) + return to_qvariant(value) + elif role == Qt.TextAlignmentRole: + if index.column() == COL_LINE: + # Align line number right + return to_qvariant(int(Qt.AlignRight | Qt.AlignVCenter)) + else: + return to_qvariant(int(Qt.AlignLeft | Qt.AlignVCenter)) + elif role == Qt.ToolTipRole: + if index.column() == COL_FILE: + # Return full file name (in last position) + value = self.breakpoints[index.row()][COL_FULL] + return to_qvariant(value) + else: + return to_qvariant() + + def reset(self): + self.beginResetModel() + self.endResetModel() + + +class BreakpointDelegate(QItemDelegate): + + def __init__(self, parent=None): + super().__init__(parent) + + +class BreakpointTableView(QTableView, SpyderWidgetMixin): + """ + Table to display editor code breakpoints. + """ + + # Signals + sig_clear_all_breakpoints_requested = Signal() + sig_clear_breakpoint_requested = Signal(str, int) + sig_edit_goto_requested = Signal(str, int, str) + sig_set_or_edit_conditional_breakpoint_requested = Signal() + + def __init__(self, parent, data): + super().__init__(parent) + + # 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.horizontalHeader().setStretchLastSection(True) + + # --- SpyderWidgetMixin API + # ------------------------------------------------------------------------ + def setup(self, options={}): + clear_all_action = self.create_action( + BreakpointTableViewActions.ClearAllBreakpoints, + _("Clear breakpoints in all files"), + triggered=self.sig_clear_all_breakpoints_requested, + ) + clear_action = self.create_action( + BreakpointTableViewActions.ClearBreakpoint, + _("Clear selected breakpoint"), + triggered=self.clear_breakpoints, + ) + edit_action = self.create_action( + BreakpointTableViewActions.EditBreakpoint, + _("Edit selected breakpoint"), + triggered=self.edit_breakpoints, + ) + + self.popup_menu = self.create_menu(PluginMainWidgetMenus.Context) + self.add_item_to_menu(clear_all_action, menu=self.popup_menu) + self.add_item_to_menu(clear_action, menu=self.popup_menu) + self.add_item_to_menu(edit_action, menu=self.popup_menu) + + # --- Qt overrides + # ------------------------------------------------------------------------ + def contextMenuEvent(self, event): + """ + Override Qt method. + """ + c_row = self.indexAt(event.pos()).row() + enabled = bool(self.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) + + self.popup_menu.popup(event.globalPos()) + event.accept() + + def mouseDoubleClickEvent(self, event): + """ + Override Qt method. + """ + index_clicked = self.indexAt(event.pos()) + if self.model.breakpoints: + c_row = index_clicked.row() + filename = self.model.breakpoints[c_row][COL_FULL] + line_number_str = self.model.breakpoints[c_row][COL_LINE] + + self.sig_edit_goto_requested.emit( + filename, int(line_number_str), '') + + if index_clicked.column() == COL_CONDITION: + self.sig_set_or_edit_conditional_breakpoint_requested.emit() + + # --- API + # ------------------------------------------------------------------------ + def set_data(self, data): + """ + Set the model breakpoint data dictionary. + """ + self.model.set_data(data) + 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. + """ + rows = self.selectionModel().selectedRows() + if rows and self.model.breakpoints: + c_row = rows[0].row() + filename = self.model.breakpoints[c_row][COL_FULL] + lineno = int(self.model.breakpoints[c_row][COL_LINE]) + + self.sig_clear_breakpoint_requested.emit(filename, lineno) + + def edit_breakpoints(self): + """ + Edit selected row breakpoint condition. + """ + rows = self.selectionModel().selectedRows() + if rows and self.model.breakpoints: + c_row = rows[0].row() + filename = self.model.breakpoints[c_row][COL_FULL] + lineno = int(self.model.breakpoints[c_row][COL_LINE]) + + self.sig_edit_goto_requested.emit(filename, lineno, '') + self.sig_set_or_edit_conditional_breakpoint_requested.emit() + + +class BreakpointWidget(PluginMainWidget): + """ + Breakpoints widget. + """ + + DEFAULT_OPTIONS = {} + + # Signals + sig_clear_all_breakpoints_requested = Signal() + sig_clear_breakpoint_requested = Signal(str, int) + sig_edit_goto_requested = Signal(str, int, str) + sig_set_or_edit_conditional_breakpoint_requested = Signal() + + def __init__(self, name=None, plugin=None, parent=None, + options=DEFAULT_OPTIONS): + super().__init__(name, plugin, parent, options) + + # 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_set_or_edit_conditional_breakpoint_requested.connect( + self.sig_set_or_edit_conditional_breakpoint_requested) + + # --- PluginMainWidget API + # ------------------------------------------------------------------------ + def get_title(self): + return _('Breakpoints') + + def setup(self, options): + self.breakpoints_table.setup() + + clear_all_action = self.get_action( + BreakpointTableViewActions.ClearAllBreakpoints) + clear_action = self.get_action( + BreakpointTableViewActions.ClearBreakpoint) + edit_action = self.get_action( + BreakpointTableViewActions.EditBreakpoint) + + options_menu = self.get_options_menu() + for item in [clear_all_action, clear_action, edit_action]: + self.add_item_to_menu(item, menu=options_menu) + + def on_option_update(self, option, value): + pass + + 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) + + def get_focus_widget(self): + return self.breakpoints_table + + # --- API + # ------------------------------------------------------------------------ + def set_data(self, data): + """ + Set the breakpoint table data dictionary. + """ + 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_()) + + +if __name__ == '__main__': + test()