1# -*- coding: utf-8 -*-
2#
3# Copyright © Spyder Project Contributors
4# based loosley on pylintgui.py by Pierre Raybaut
5# Licensed under the terms of the MIT License
6# (see spyder/__init__.py for details)
7
8"""Breakpoint widget"""
9
10# pylint: disable=C0103
11# pylint: disable=R0903
12# pylint: disable=R0911
13# pylint: disable=R0201
14
15# Standard library imports
16import os.path as osp
17import sys
18
19# Third party imports
20from qtpy import API
21from qtpy.compat import to_qvariant
22from qtpy.QtCore import (QAbstractTableModel, QModelIndex, QTextCodec, Qt,
23                         Signal)
24from qtpy.QtWidgets import (QItemDelegate, QMenu, QTableView, QVBoxLayout,
25                            QWidget)
26
27# Local imports
28from spyder.config.base import get_translation
29from spyder.config.main import CONF
30from spyder.utils.qthelpers import add_actions, create_action
31
32# This is needed for testing this module as a stand alone script
33try:
34    _ = get_translation("breakpoints", "spyder_breakpoints")
35except KeyError as error:
36    import gettext
37    _ = gettext.gettext
38
39
40locale_codec = QTextCodec.codecForLocale()
41
42
43class BreakpointTableModel(QAbstractTableModel):
44    """
45    Table model for breakpoints dictionary
46
47    """
48    def __init__(self, parent, data):
49        QAbstractTableModel.__init__(self, parent)
50        if data is None:
51            data = {}
52        self._data = None
53        self.breakpoints = None
54        self.set_data(data)
55
56    def set_data(self, data):
57        """Set model data"""
58        self._data = data
59        keys = list(data.keys())
60        self.breakpoints = []
61        for key in keys:
62            bp_list = data[key]
63            if bp_list:
64                for item in data[key]:
65                    self.breakpoints.append((key, item[0], item[1], ""))
66        self.reset()
67
68    def rowCount(self, qindex=QModelIndex()):
69        """Array row number"""
70        return len(self.breakpoints)
71
72    def columnCount(self, qindex=QModelIndex()):
73        """Array column count"""
74        return 4
75
76    def sort(self, column, order=Qt.DescendingOrder):
77        """Overriding sort method"""
78        if column == 0:
79            self.breakpoints.sort(
80                key=lambda breakpoint: breakpoint[1])
81            self.breakpoints.sort(
82                key=lambda breakpoint: osp.basename(breakpoint[0]))
83        elif column == 1:
84            pass
85        elif column == 2:
86            pass
87        elif column == 3:
88            pass
89        self.reset()
90
91    def headerData(self, section, orientation, role=Qt.DisplayRole):
92        """Overriding method headerData"""
93        if role != Qt.DisplayRole:
94            return to_qvariant()
95        i_column = int(section)
96        if orientation == Qt.Horizontal:
97            headers = (_("File"), _("Line"), _("Condition"), "")
98            return to_qvariant( headers[i_column] )
99        else:
100            return to_qvariant()
101
102    def get_value(self, index):
103        """Return current value"""
104        return self.breakpoints[index.row()][index.column()]
105
106    def data(self, index, role=Qt.DisplayRole):
107        """Return data at table index"""
108        if not index.isValid():
109            return to_qvariant()
110        if role == Qt.DisplayRole:
111            if index.column() == 0:
112                value = osp.basename(self.get_value(index))
113                return to_qvariant(value)
114            else:
115                value = self.get_value(index)
116                return to_qvariant(value)
117        elif role == Qt.TextAlignmentRole:
118            return to_qvariant(int(Qt.AlignLeft|Qt.AlignVCenter))
119        elif role == Qt.ToolTipRole:
120            if index.column() == 0:
121                value = self.get_value(index)
122                return to_qvariant(value)
123            else:
124                return to_qvariant()
125
126    def reset(self):
127        self.beginResetModel()
128        self.endResetModel()
129
130
131class BreakpointDelegate(QItemDelegate):
132    def __init__(self, parent=None):
133        QItemDelegate.__init__(self, parent)
134
135
136class BreakpointTableView(QTableView):
137    edit_goto = Signal(str, int, str)
138    clear_breakpoint = Signal(str, int)
139    clear_all_breakpoints = Signal()
140    set_or_edit_conditional_breakpoint = Signal()
141
142    def __init__(self, parent, data):
143        QTableView.__init__(self, parent)
144        self.model = BreakpointTableModel(self, data)
145        self.setModel(self.model)
146        self.delegate = BreakpointDelegate(self)
147        self.setItemDelegate(self.delegate)
148
149        self.setup_table()
150
151    def setup_table(self):
152        """Setup table"""
153        self.horizontalHeader().setStretchLastSection(True)
154        self.adjust_columns()
155        self.columnAt(0)
156        # Sorting columns
157        self.setSortingEnabled(False)
158        self.sortByColumn(0, Qt.DescendingOrder)
159
160    def adjust_columns(self):
161        """Resize three first columns to contents"""
162        for col in range(3):
163            self.resizeColumnToContents(col)
164
165    def mouseDoubleClickEvent(self, event):
166        """Reimplement Qt method"""
167        index_clicked = self.indexAt(event.pos())
168        if self.model.breakpoints:
169            filename = self.model.breakpoints[index_clicked.row()][0]
170            line_number_str = self.model.breakpoints[index_clicked.row()][1]
171            self.edit_goto.emit(filename, int(line_number_str), '')
172        if index_clicked.column()==2:
173            self.set_or_edit_conditional_breakpoint.emit()
174
175    def contextMenuEvent(self, event):
176        index_clicked = self.indexAt(event.pos())
177        actions = []
178        self.popup_menu = QMenu(self)
179        clear_all_breakpoints_action = create_action(self,
180            _("Clear breakpoints in all files"),
181            triggered=lambda: self.clear_all_breakpoints.emit())
182        actions.append(clear_all_breakpoints_action)
183        if self.model.breakpoints:
184            filename = self.model.breakpoints[index_clicked.row()][0]
185            lineno = int(self.model.breakpoints[index_clicked.row()][1])
186            # QAction.triggered works differently for PySide and PyQt
187            if not API == 'pyside':
188                clear_slot = lambda _checked, filename=filename, lineno=lineno: \
189                    self.clear_breakpoint.emit(filename, lineno)
190                edit_slot = lambda _checked, filename=filename, lineno=lineno: \
191                    (self.edit_goto.emit(filename, lineno, ''),
192                     self.set_or_edit_conditional_breakpoint.emit())
193            else:
194                clear_slot = lambda filename=filename, lineno=lineno: \
195                    self.clear_breakpoint.emit(filename, lineno)
196                edit_slot = lambda filename=filename, lineno=lineno: \
197                    (self.edit_goto.emit(filename, lineno, ''),
198                     self.set_or_edit_conditional_breakpoint.emit())
199
200            clear_breakpoint_action = create_action(self,
201                    _("Clear this breakpoint"),
202                    triggered=clear_slot)
203            actions.insert(0,clear_breakpoint_action)
204
205            edit_breakpoint_action = create_action(self,
206                    _("Edit this breakpoint"),
207                    triggered=edit_slot)
208            actions.append(edit_breakpoint_action)
209        add_actions(self.popup_menu, actions)
210        self.popup_menu.popup(event.globalPos())
211        event.accept()
212
213
214class BreakpointWidget(QWidget):
215    """
216    Breakpoint widget
217    """
218    VERSION = '1.0.0'
219    clear_all_breakpoints = Signal()
220    set_or_edit_conditional_breakpoint = Signal()
221    clear_breakpoint = Signal(str, int)
222    edit_goto = Signal(str, int, str)
223
224    def __init__(self, parent):
225        QWidget.__init__(self, parent)
226
227        self.setWindowTitle("Breakpoints")
228        self.dictwidget = BreakpointTableView(self,
229                               self._load_all_breakpoints())
230        layout = QVBoxLayout()
231        layout.addWidget(self.dictwidget)
232        self.setLayout(layout)
233        self.dictwidget.clear_all_breakpoints.connect(
234                                     lambda: self.clear_all_breakpoints.emit())
235        self.dictwidget.clear_breakpoint.connect(
236                         lambda s1, lino: self.clear_breakpoint.emit(s1, lino))
237        self.dictwidget.edit_goto.connect(
238                        lambda s1, lino, s2: self.edit_goto.emit(s1, lino, s2))
239        self.dictwidget.set_or_edit_conditional_breakpoint.connect(
240                        lambda: self.set_or_edit_conditional_breakpoint.emit())
241
242    def _load_all_breakpoints(self):
243        bp_dict = CONF.get('run', 'breakpoints', {})
244        for filename in list(bp_dict.keys()):
245            if not osp.isfile(filename):
246                bp_dict.pop(filename)
247        return bp_dict
248
249    def get_data(self):
250        pass
251
252    def set_data(self):
253        bp_dict = self._load_all_breakpoints()
254        self.dictwidget.model.set_data(bp_dict)
255        self.dictwidget.adjust_columns()
256        self.dictwidget.sortByColumn(0, Qt.DescendingOrder)
257
258
259#==============================================================================
260# Tests
261#==============================================================================
262def test():
263    """Run breakpoint widget test"""
264    from spyder.utils.qthelpers import qapplication
265    app = qapplication()
266    widget = BreakpointWidget(None)
267    widget.show()
268    sys.exit(app.exec_())
269
270
271if __name__ == '__main__':
272    test()
273