13 |
13 |
14 from PyQt5.QtCore import pyqtSlot, Qt, QTimer |
14 from PyQt5.QtCore import pyqtSlot, Qt, QTimer |
15 from PyQt5.QtGui import QCursor, QBrush |
15 from PyQt5.QtGui import QCursor, QBrush |
16 from PyQt5.QtWidgets import ( |
16 from PyQt5.QtWidgets import ( |
17 QTreeWidget, QApplication, QTreeWidgetItem, QAbstractItemView, QWidget, |
17 QTreeWidget, QApplication, QTreeWidgetItem, QAbstractItemView, QWidget, |
18 QVBoxLayout, QLabel |
18 QVBoxLayout, QLabel, QMenu |
19 ) |
19 ) |
|
20 |
|
21 import Preferences |
20 |
22 |
21 |
23 |
22 class PythonDisViewer(QWidget): |
24 class PythonDisViewer(QWidget): |
23 """ |
25 """ |
24 Class implementing a widget to visualize the Python Disassembly for some |
26 Class implementing a widget to visualize the Python Disassembly for some |
42 self.setLayout(self.__layout) |
44 self.setLayout(self.__layout) |
43 self.__disWidget = QTreeWidget(self) |
45 self.__disWidget = QTreeWidget(self) |
44 self.__layout.addWidget(self.__disWidget) |
46 self.__layout.addWidget(self.__disWidget) |
45 self.__layout.setContentsMargins(0, 0, 0, 0) |
47 self.__layout.setContentsMargins(0, 0, 0, 0) |
46 |
48 |
47 self.__infoLabel = QLabel(self.tr( |
49 self.__currentInfoLabel = QLabel(self.tr( |
48 "italic: current instruction\n" |
50 "italic: current instruction")) |
49 "bold: labelled instruction" |
51 self.__labeledInfoLabel = QLabel(self.tr( |
50 )) |
52 "bold: labeled instruction")) |
51 self.__layout.addWidget(self.__infoLabel) |
53 self.__layout.addWidget(self.__currentInfoLabel) |
|
54 self.__layout.addWidget(self.__labeledInfoLabel) |
52 |
55 |
53 self.__vm = viewmanager |
56 self.__vm = viewmanager |
54 self.__vmConnected = False |
57 self.__vmConnected = False |
55 |
58 |
56 self.__editor = None |
59 self.__editor = None |
62 self.__disWidget.setSortingEnabled(False) |
65 self.__disWidget.setSortingEnabled(False) |
63 self.__disWidget.setSelectionBehavior(QAbstractItemView.SelectRows) |
66 self.__disWidget.setSelectionBehavior(QAbstractItemView.SelectRows) |
64 self.__disWidget.setSelectionMode(QAbstractItemView.SingleSelection) |
67 self.__disWidget.setSelectionMode(QAbstractItemView.SingleSelection) |
65 self.__disWidget.setAlternatingRowColors(True) |
68 self.__disWidget.setAlternatingRowColors(True) |
66 |
69 |
|
70 self.__menu = QMenu(self.__disWidget) |
|
71 self.__menu.addAction(self.tr('Expand All'), self.__expandAll) |
|
72 self.__menu.addAction(self.tr('Collapse All'), self.__collapseAll) |
|
73 |
|
74 self.__disWidget.setContextMenuPolicy(Qt.CustomContextMenu) |
|
75 self.__disWidget.customContextMenuRequested.connect( |
|
76 self.__contextMenuRequested) |
|
77 |
|
78 self.__errorColor = QBrush( |
|
79 Preferences.getPython("DisViewerErrorColor")) |
|
80 self.__currentInstructionColor = QBrush( |
|
81 Preferences.getPython("DisViewerCurrentColor")) |
|
82 self.__jumpTargetColor = QBrush( |
|
83 Preferences.getPython("DisViewerLabeledColor")) |
|
84 |
67 self.__disWidget.itemClicked.connect(self.__disItemClicked) |
85 self.__disWidget.itemClicked.connect(self.__disItemClicked) |
|
86 self.__disWidget.itemCollapsed.connect(self.__resizeColumns) |
|
87 self.__disWidget.itemExpanded.connect(self.__resizeColumns) |
68 |
88 |
69 self.__vm.disViewerStateChanged.connect(self.__disViewerStateChanged) |
89 self.__vm.disViewerStateChanged.connect(self.__disViewerStateChanged) |
70 |
90 |
71 self.hide() |
91 self.hide() |
|
92 |
|
93 def __contextMenuRequested(self, coord): |
|
94 """ |
|
95 Private slot to show the context menu. |
|
96 |
|
97 @param coord position of the mouse pointer |
|
98 @type QPoint |
|
99 """ |
|
100 coord = self.__disWidget.mapToGlobal(coord) |
|
101 self.__menu.popup(coord) |
72 |
102 |
73 def __editorChanged(self, editor): |
103 def __editorChanged(self, editor): |
74 """ |
104 """ |
75 Private slot to handle a change of the current editor. |
105 Private slot to handle a change of the current editor. |
76 |
106 |
93 @type Editor |
123 @type Editor |
94 """ |
124 """ |
95 if editor and editor is self.__editor: |
125 if editor and editor is self.__editor: |
96 self.__loadDIS() |
126 self.__loadDIS() |
97 |
127 |
98 def __editorDoubleClicked(self, editor, pos, buttons): |
128 def __editorLineChanged(self, editor, lineno): |
99 """ |
129 """ |
100 Private slot to handle a mouse button double click in the editor. |
130 Private slot to handle a mouse button double click in the editor. |
101 |
131 |
102 @param editor reference to the editor, that emitted the signal |
132 @param editor reference to the editor, that emitted the signal |
103 @type Editor |
133 @type Editor |
104 @param pos position of the double click |
134 @param lineno line number of the editor's cursor (zero based) |
105 @type QPoint |
135 @type int |
106 @param buttons mouse buttons that were double clicked |
136 """ |
107 @type Qt.MouseButtons |
137 if editor is self.__editor: |
108 """ |
|
109 if editor is self.__editor and buttons == Qt.LeftButton: |
|
110 if editor.isModified(): |
138 if editor.isModified(): |
111 # reload the source |
139 # reload the source |
112 QTimer.singleShot(0, self.__loadDIS) |
140 QTimer.singleShot(0, self.__loadDIS) |
113 |
141 |
114 # highlight the corresponding entry |
142 # highlight the corresponding entry |
115 ## QTimer.singleShot(0, self.__selectItemForEditorSelection) |
143 QTimer.singleShot(0, self.__selectItemForEditorLine) |
116 QTimer.singleShot(0, self.__grabFocus) |
|
117 |
144 |
118 def __lastEditorClosed(self): |
145 def __lastEditorClosed(self): |
119 """ |
146 """ |
120 Private slot to handle the last editor closed signal of the view |
147 Private slot to handle the last editor closed signal of the view |
121 manager. |
148 manager. |
129 super(PythonDisViewer, self).show() |
156 super(PythonDisViewer, self).show() |
130 |
157 |
131 if not self.__vmConnected: |
158 if not self.__vmConnected: |
132 self.__vm.editorChangedEd.connect(self.__editorChanged) |
159 self.__vm.editorChangedEd.connect(self.__editorChanged) |
133 self.__vm.editorSavedEd.connect(self.__editorSaved) |
160 self.__vm.editorSavedEd.connect(self.__editorSaved) |
134 self.__vm.editorDoubleClickedEd.connect(self.__editorDoubleClicked) |
161 self.__vm.editorLineChangedEd.connect(self.__editorLineChanged) |
135 self.__vmConnected = True |
162 self.__vmConnected = True |
136 |
163 |
137 def hide(self): |
164 def hide(self): |
138 """ |
165 """ |
139 Public slot to hide the DIS viewer. |
166 Public slot to hide the DIS viewer. |
144 self.__editor.clearAllHighlights() |
171 self.__editor.clearAllHighlights() |
145 |
172 |
146 if self.__vmConnected: |
173 if self.__vmConnected: |
147 self.__vm.editorChangedEd.disconnect(self.__editorChanged) |
174 self.__vm.editorChangedEd.disconnect(self.__editorChanged) |
148 self.__vm.editorSavedEd.disconnect(self.__editorSaved) |
175 self.__vm.editorSavedEd.disconnect(self.__editorSaved) |
149 self.__vm.editorDoubleClickedEd.disconnect( |
176 self.__vm.editorLineChangedEd.disconnect(self.__editorLineChanged) |
150 self.__editorDoubleClicked) |
|
151 self.__vmConnected = False |
177 self.__vmConnected = False |
152 |
178 |
153 def shutdown(self): |
179 def shutdown(self): |
154 """ |
180 """ |
155 Public method to perform shutdown actions. |
181 Public method to perform shutdown actions. |
171 self.__loadDIS() |
197 self.__loadDIS() |
172 else: |
198 else: |
173 self.hide() |
199 self.hide() |
174 self.__editor = None |
200 self.__editor = None |
175 |
201 |
|
202 def __expandAll(self): |
|
203 """ |
|
204 Private slot to expand all items. |
|
205 """ |
|
206 block = self.__disWidget.blockSignals(True) |
|
207 self.__disWidget.expandAll() |
|
208 self.__disWidget.blockSignals(block) |
|
209 self.__resizeColumns() |
|
210 |
|
211 def __collapseAll(self): |
|
212 """ |
|
213 Private slot to collapse all items. |
|
214 """ |
|
215 block = self.__disWidget.blockSignals(True) |
|
216 self.__disWidget.collapseAll() |
|
217 self.__disWidget.blockSignals(block) |
|
218 self.__resizeColumns() |
|
219 |
176 def __createErrorItem(self, error): |
220 def __createErrorItem(self, error): |
177 """ |
221 """ |
178 Private method to create a top level error item. |
222 Private method to create a top level error item. |
179 |
223 |
180 @param error error message |
224 @param error error message |
182 @return generated item |
226 @return generated item |
183 @rtype QTreeWidgetItem |
227 @rtype QTreeWidgetItem |
184 """ |
228 """ |
185 itm = QTreeWidgetItem(self.__disWidget, [error]) |
229 itm = QTreeWidgetItem(self.__disWidget, [error]) |
186 itm.setFirstColumnSpanned(True) |
230 itm.setFirstColumnSpanned(True) |
187 itm.setForeground(0, QBrush(Qt.red)) |
231 itm.setForeground(0, self.__errorColor) |
188 return itm |
232 return itm |
189 |
233 |
190 def __createTitleItem(self, title, line): |
234 def __createTitleItem(self, title, line, parentItem): |
191 """ |
235 """ |
192 Private method to create a title item. |
236 Private method to create a title item. |
193 |
237 |
194 @param title titel string for the item |
238 @param title titel string for the item |
195 @type str |
239 @type str |
196 @param line start line of the titled disassembly |
240 @param line start line of the titled disassembly |
197 @type int |
241 @type int |
|
242 @param parentItem reference to the parent item |
|
243 @type QTreeWidget or QTreeWidgetItem |
198 @return generated item |
244 @return generated item |
199 @rtype QTreeWidgetItem |
245 @rtype QTreeWidgetItem |
200 """ |
246 """ |
201 itm = QTreeWidgetItem(self.__disWidget, [title]) |
247 itm = QTreeWidgetItem(parentItem, [title]) |
202 itm.setFirstColumnSpanned(True) |
248 itm.setFirstColumnSpanned(True) |
203 itm.setExpanded(True) |
249 itm.setExpanded(True) |
204 |
250 |
205 itm.setData(0, self.StartLineRole, line) |
251 itm.setData(0, self.StartLineRole, line) |
206 itm.setData(0, self.EndLineRole, line) |
252 itm.setData(0, self.EndLineRole, line) |
239 fields.append('(' + instr.argrepr + ')') |
285 fields.append('(' + instr.argrepr + ')') |
240 |
286 |
241 itm = QTreeWidgetItem(parent, fields) |
287 itm = QTreeWidgetItem(parent, fields) |
242 for col in (0, 1, 3): |
288 for col in (0, 1, 3): |
243 itm.setTextAlignment(col, Qt.AlignRight) |
289 itm.setTextAlignment(col, Qt.AlignRight) |
|
290 # set font to indicate current instruction and jump target |
244 font = itm.font(0) |
291 font = itm.font(0) |
245 if instr.offset == lasti: |
292 if instr.offset == lasti: |
246 font.setItalic(True) |
293 font.setItalic(True) |
247 if instr.is_jump_target: |
294 if instr.is_jump_target: |
248 font.setBold(True) |
295 font.setBold(True) |
249 for col in range(itm.columnCount()): |
296 for col in range(itm.columnCount()): |
250 itm.setFont(col, font) |
297 itm.setFont(col, font) |
|
298 # set color to indicate current instruction or jump target |
|
299 if instr.offset == lasti: |
|
300 foreground = self.__currentInstructionColor |
|
301 elif instr.is_jump_target: |
|
302 foreground = self.__jumpTargetColor |
|
303 else: |
|
304 foreground = None |
|
305 if foreground: |
|
306 for col in range(itm.columnCount()): |
|
307 itm.setForeground(col, foreground) |
251 |
308 |
252 itm.setExpanded(True) |
309 itm.setExpanded(True) |
253 |
310 |
254 if instr.starts_line: |
311 if instr.starts_line: |
255 itm.setData(0, self.StartLineRole, instr.starts_line) |
312 itm.setData(0, self.StartLineRole, instr.starts_line) |
298 if not source.strip(): |
355 if not source.strip(): |
299 # empty editor or white space only |
356 # empty editor or white space only |
300 return |
357 return |
301 |
358 |
302 filename = self.__editor.getFileName() |
359 filename = self.__editor.getFileName() |
303 if not filename: |
360 if filename: |
|
361 filename = os.path.basename(filename) |
|
362 else: |
304 filename = "<dis>" |
363 filename = "<dis>" |
305 |
364 |
306 QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) |
365 QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) |
307 try: |
366 try: |
308 codeObject = self.__tryCompile(source, filename) |
367 codeObject = self.__tryCompile(source, filename) |
310 codeObject = None |
369 codeObject = None |
311 self.__createErrorItem(str(exc)) |
370 self.__createErrorItem(str(exc)) |
312 |
371 |
313 if codeObject: |
372 if codeObject: |
314 self.setUpdatesEnabled(False) |
373 self.setUpdatesEnabled(False) |
|
374 block = self.__disWidget.blockSignals(True) |
315 |
375 |
316 self.__disassembleObject(codeObject, self.__disWidget, filename) |
376 self.__disassembleObject(codeObject, self.__disWidget, filename) |
317 QTimer.singleShot(0, self.__resizeColumns) |
377 QTimer.singleShot(0, self.__resizeColumns) |
318 |
378 |
|
379 self.__disWidget.blockSignals(block) |
319 self.setUpdatesEnabled(True) |
380 self.setUpdatesEnabled(True) |
320 |
381 |
321 QApplication.restoreOverrideCursor() |
382 QApplication.restoreOverrideCursor() |
322 |
383 |
323 self.__grabFocus() |
384 self.__grabFocus() |
336 @param evt resize event |
397 @param evt resize event |
337 @type QResizeEvent |
398 @type QResizeEvent |
338 """ |
399 """ |
339 # just adjust the sizes of the columns |
400 # just adjust the sizes of the columns |
340 self.__resizeColumns() |
401 self.__resizeColumns() |
|
402 |
|
403 def __clearSelection(self): |
|
404 """ |
|
405 Private method to clear all selected items. |
|
406 """ |
|
407 for itm in self.__disWidget.selectedItems(): |
|
408 itm.setSelected(False) |
|
409 |
|
410 def __selectChildren(self, itm, lineno): |
|
411 """ |
|
412 Private method to select children of the given item covering the given |
|
413 line number. |
|
414 |
|
415 @param itm reference to the item |
|
416 @type QTreeWidgetItem |
|
417 @param lineno line number to base the selection on |
|
418 @type int |
|
419 """ |
|
420 for index in range(itm.childCount()): |
|
421 child = itm.child(index) |
|
422 if ( |
|
423 child.data(0, self.StartLineRole) <= lineno <= |
|
424 child.data(0, self.EndLineRole) |
|
425 ): |
|
426 child.setSelected(True) |
|
427 self.__selectChildren(child, lineno) |
|
428 |
|
429 if child.data(0, self.StartLineRole) == lineno: |
|
430 self.__disWidget.scrollToItem( |
|
431 child, QAbstractItemView.PositionAtCenter) |
|
432 |
|
433 def __selectItemForEditorLine(self): |
|
434 """ |
|
435 Private slot to select the items corresponding with the cursor line |
|
436 of the current editor. |
|
437 """ |
|
438 # step 1: clear all selected items |
|
439 self.__clearSelection() |
|
440 |
|
441 # step 2: retrieve the editor cursor line |
|
442 cline, cindex = self.__editor.getCursorPosition() |
|
443 # make the line numbers 1-based |
|
444 cline += 1 |
|
445 |
|
446 for index in range(self.__disWidget.topLevelItemCount()): |
|
447 itm = self.__disWidget.topLevelItem(index) |
|
448 if ( |
|
449 itm.data(0, self.StartLineRole) <= cline <= |
|
450 itm.data(0, self.EndLineRole) |
|
451 ): |
|
452 itm.setSelected(True) |
|
453 self.__selectChildren(itm, cline) |
341 |
454 |
342 def __grabFocus(self): |
455 def __grabFocus(self): |
343 """ |
456 """ |
344 Private method to grab the input focus. |
457 Private method to grab the input focus. |
345 """ |
458 """ |
381 c = compile(source, name, 'eval') |
494 c = compile(source, name, 'eval') |
382 except SyntaxError: |
495 except SyntaxError: |
383 c = compile(source, name, 'exec') |
496 c = compile(source, name, 'exec') |
384 return c |
497 return c |
385 |
498 |
386 def __disassembleObject(self, co, parentItem, name="", lasti=-1): |
499 def __disassembleObject(self, co, parentItem, parentName="", lasti=-1): |
387 """ |
500 """ |
388 Private method to disassemble the given code object recursively. |
501 Private method to disassemble the given code object recursively. |
389 |
502 |
390 @param co code object to be disassembled |
503 @param co code object to be disassembled |
391 @type code object |
504 @type code object |
392 @param parentItem reference to the parent item |
505 @param parentItem reference to the parent item |
393 @type QTreeWidget or QTreeWidgetItem |
506 @type QTreeWidget or QTreeWidgetItem |
394 @param name name of the code object |
507 @param parentName name of the parent code object |
395 @type str |
508 @type str |
396 @param lasti index of the instruction of a traceback |
509 @param lasti index of the instruction of a traceback |
397 @type int |
510 @type int |
398 """ |
511 """ |
399 if co.co_name == "<module>": |
512 if co.co_name == "<module>": |
400 title = ( |
513 title = os.path.basename(co.co_filename) |
401 self.tr("Disassembly of module '{0}'") |
514 name = "" |
402 .format(os.path.basename(co.co_filename)) |
515 else: |
403 ) |
516 if parentName: |
404 else: |
517 name = "{0}.{1}".format(parentName, co.co_name) |
405 title = ( |
518 else: |
406 self.tr("Disassembly of code object '{0}'") |
519 name = co.co_name |
407 .format(co.co_name) |
520 title = self.tr("Code Object '{0}'").format(name) |
408 ) |
521 titleItem = self.__createTitleItem(title, co.co_firstlineno, |
409 titleItem = self.__createTitleItem(title, co.co_firstlineno) |
522 parentItem) |
410 lastStartItem = None |
523 lastStartItem = None |
411 for instr in dis.get_instructions(co): |
524 for instr in dis.get_instructions(co): |
412 if instr.starts_line: |
525 if instr.starts_line: |
413 if lastStartItem: |
526 if lastStartItem: |
414 self.__updateItemEndLine(lastStartItem) |
527 self.__updateItemEndLine(lastStartItem) |
417 else: |
530 else: |
418 self.__createInstructionItem(instr, lastStartItem, lasti=lasti) |
531 self.__createInstructionItem(instr, lastStartItem, lasti=lasti) |
419 if lastStartItem: |
532 if lastStartItem: |
420 self.__updateItemEndLine(lastStartItem) |
533 self.__updateItemEndLine(lastStartItem) |
421 |
534 |
422 self.__updateItemEndLine(titleItem) |
|
423 |
|
424 for x in co.co_consts: |
535 for x in co.co_consts: |
425 if hasattr(x, 'co_code'): |
536 if hasattr(x, 'co_code'): |
426 self.__disassembleObject(x, self.__disWidget, lasti=lasti) |
537 self.__disassembleObject(x, titleItem, parentName=name, |
|
538 lasti=lasti) |
|
539 |
|
540 self.__updateItemEndLine(titleItem) |
|
541 |
|
542 @pyqtSlot() |
|
543 def preferencesChanged(self): |
|
544 """ |
|
545 Public slot handling changes of the Disassembly viewer settings. |
|
546 """ |
|
547 self.__errorColor = QBrush( |
|
548 Preferences.getPython("DisViewerErrorColor")) |
|
549 self.__currentInstructionColor = QBrush( |
|
550 Preferences.getPython("DisViewerCurrentColor")) |
|
551 self.__jumpTargetColor = QBrush( |
|
552 Preferences.getPython("DisViewerLabeledColor")) |
|
553 |
|
554 if self.isVisible(): |
|
555 self.__loadDIS() |