|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2020 - 2021 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing a widget to visualize the Python Disassembly for some |
|
8 Python sources. |
|
9 """ |
|
10 |
|
11 import os |
|
12 import dis |
|
13 |
|
14 import enum |
|
15 |
|
16 |
|
17 from PyQt5.QtCore import pyqtSlot, Qt, QTimer |
|
18 from PyQt5.QtGui import QBrush |
|
19 from PyQt5.QtWidgets import ( |
|
20 QTreeWidgetItem, QAbstractItemView, QWidget, QMenu |
|
21 ) |
|
22 |
|
23 from E5Gui.E5Application import e5App |
|
24 from E5Gui.E5OverrideCursor import E5OverrideCursor |
|
25 |
|
26 import Preferences |
|
27 |
|
28 from .Ui_PythonDisViewer import Ui_PythonDisViewer |
|
29 |
|
30 |
|
31 class PythonDisViewerModes(enum.Enum): |
|
32 """ |
|
33 Class implementing the disassembly viewer operation modes. |
|
34 """ |
|
35 SOURCEDISASSEMBLY = 0 |
|
36 TRACEBACK = 1 |
|
37 |
|
38 |
|
39 class PythonDisViewer(QWidget, Ui_PythonDisViewer): |
|
40 """ |
|
41 Class implementing a widget to visualize the Python Disassembly for some |
|
42 Python sources. |
|
43 """ |
|
44 StartLineRole = Qt.ItemDataRole.UserRole |
|
45 EndLineRole = Qt.ItemDataRole.UserRole + 1 |
|
46 CodeInfoRole = Qt.ItemDataRole.UserRole + 2 |
|
47 |
|
48 def __init__(self, viewmanager, |
|
49 mode=PythonDisViewerModes.SOURCEDISASSEMBLY, |
|
50 parent=None): |
|
51 """ |
|
52 Constructor |
|
53 |
|
54 @param viewmanager reference to the viewmanager object |
|
55 @type ViewManager |
|
56 @param mode operation mode of the viewer |
|
57 @type int |
|
58 @param parent reference to the parent widget |
|
59 @type QWidget |
|
60 """ |
|
61 super().__init__(parent) |
|
62 self.setupUi(self) |
|
63 |
|
64 self.setWindowTitle(self.tr("Disassembly")) |
|
65 |
|
66 self.__vm = viewmanager |
|
67 self.__vmConnected = False |
|
68 |
|
69 self.__mode = mode |
|
70 |
|
71 self.__editor = None |
|
72 self.__source = "" |
|
73 |
|
74 self.disWidget.setHeaderLabels( |
|
75 [self.tr("Line"), self.tr("Offset"), self.tr("Operation"), |
|
76 self.tr("Parameters"), self.tr("Interpreted Parameters")]) |
|
77 self.codeInfoWidget.setHeaderLabels( |
|
78 [self.tr("Key"), self.tr("Value")]) |
|
79 |
|
80 self.__disMenu = QMenu(self.disWidget) |
|
81 if self.__mode == PythonDisViewerModes.SOURCEDISASSEMBLY: |
|
82 self.__codeInfoAct = self.__disMenu.addAction( |
|
83 self.tr("Show Code Info"), self.__showCodeInfo) |
|
84 self.__disMenu.addSeparator() |
|
85 self.__disMenu.addAction( |
|
86 self.tr('Expand All'), self.__expandAllDis) |
|
87 self.__disMenu.addAction( |
|
88 self.tr('Collapse All'), self.__collapseAllDis) |
|
89 self.__disMenu.addSeparator() |
|
90 self.__disMenu.addAction( |
|
91 self.tr('Configure...'), self.__configure) |
|
92 |
|
93 self.__codeInfoMenu = QMenu(self.codeInfoWidget) |
|
94 if self.__mode == PythonDisViewerModes.SOURCEDISASSEMBLY: |
|
95 self.__codeInfoMenu.addAction( |
|
96 self.tr("Hide"), self.codeInfoWidget.hide) |
|
97 self.__codeInfoMenu.addAction( |
|
98 self.tr('Expand All'), self.__expandAllCodeInfo) |
|
99 self.__codeInfoMenu.addAction( |
|
100 self.tr('Collapse All'), self.__collapseAllCodeInfo) |
|
101 self.__codeInfoMenu.addSeparator() |
|
102 self.__codeInfoMenu.addAction( |
|
103 self.tr('Configure...'), self.__configure) |
|
104 |
|
105 self.__errorColor = QBrush( |
|
106 Preferences.getPython("DisViewerErrorColor")) |
|
107 self.__currentInstructionColor = QBrush( |
|
108 Preferences.getPython("DisViewerCurrentColor")) |
|
109 self.__jumpTargetColor = QBrush( |
|
110 Preferences.getPython("DisViewerLabeledColor")) |
|
111 |
|
112 self.__showCodeInfoDetails = Preferences.getPython( |
|
113 "DisViewerExpandCodeInfoDetails") |
|
114 |
|
115 if self.__mode == PythonDisViewerModes.SOURCEDISASSEMBLY: |
|
116 self.disWidget.itemClicked.connect(self.__disItemClicked) |
|
117 self.disWidget.itemCollapsed.connect(self.__resizeDisColumns) |
|
118 self.disWidget.itemExpanded.connect(self.__resizeDisColumns) |
|
119 self.disWidget.customContextMenuRequested.connect( |
|
120 self.__disContextMenuRequested) |
|
121 |
|
122 self.codeInfoWidget.itemCollapsed.connect(self.__resizeCodeInfoColumns) |
|
123 self.codeInfoWidget.itemExpanded.connect(self.__resizeCodeInfoColumns) |
|
124 self.codeInfoWidget.customContextMenuRequested.connect( |
|
125 self.__codeInfoContextMenuRequested) |
|
126 |
|
127 if self.__mode == PythonDisViewerModes.SOURCEDISASSEMBLY: |
|
128 self.__vm.disViewerStateChanged.connect( |
|
129 self.__disViewerStateChanged) |
|
130 |
|
131 self.codeInfoWidget.hide() |
|
132 self.hide() |
|
133 |
|
134 elif self.__mode == PythonDisViewerModes.TRACEBACK: |
|
135 self.__styleLabels() |
|
136 |
|
137 def __disContextMenuRequested(self, coord): |
|
138 """ |
|
139 Private slot to show the context menu of the disassembly widget. |
|
140 |
|
141 @param coord position of the mouse pointer |
|
142 @type QPoint |
|
143 """ |
|
144 if self.__mode == PythonDisViewerModes.SOURCEDISASSEMBLY: |
|
145 itm = self.disWidget.itemAt(coord) |
|
146 self.__codeInfoAct.setEnabled(bool(itm.data(0, self.CodeInfoRole))) |
|
147 self.disWidget.setCurrentItem(itm) |
|
148 |
|
149 if self.disWidget.topLevelItemCount() > 0: |
|
150 # don't show context menu on empty list |
|
151 coord = self.disWidget.mapToGlobal(coord) |
|
152 self.__disMenu.popup(coord) |
|
153 |
|
154 def __editorChanged(self, editor): |
|
155 """ |
|
156 Private slot to handle a change of the current editor. |
|
157 |
|
158 @param editor reference to the current editor |
|
159 @type Editor |
|
160 """ |
|
161 if editor is not self.__editor: |
|
162 if self.__editor: |
|
163 self.__editor.clearAllHighlights() |
|
164 self.__editor = editor |
|
165 if self.__editor: |
|
166 self.__loadDIS() |
|
167 |
|
168 def __editorSaved(self, editor): |
|
169 """ |
|
170 Private slot to reload the Disassembly after the connected editor was |
|
171 saved. |
|
172 |
|
173 @param editor reference to the editor that performed a save action |
|
174 @type Editor |
|
175 """ |
|
176 if editor and editor is self.__editor: |
|
177 self.__loadDIS() |
|
178 |
|
179 def __editorLineChanged(self, editor, lineno): |
|
180 """ |
|
181 Private slot to handle a mouse button double click in the editor. |
|
182 |
|
183 @param editor reference to the editor, that emitted the signal |
|
184 @type Editor |
|
185 @param lineno line number of the editor's cursor (zero based) |
|
186 @type int |
|
187 """ |
|
188 if editor is self.__editor: |
|
189 if editor.isModified(): |
|
190 # reload the source |
|
191 QTimer.singleShot(0, self.__loadDIS) |
|
192 |
|
193 # highlight the corresponding entry |
|
194 QTimer.singleShot(0, self.__selectItemForEditorLine) |
|
195 |
|
196 def __editorLanguageChanged(self, editor): |
|
197 """ |
|
198 Private slot to handle a change of the editor language. |
|
199 |
|
200 @param editor reference to the editor which changed language |
|
201 @type Editor |
|
202 """ |
|
203 if editor is self.__editor: |
|
204 QTimer.singleShot(0, self.__loadDIS) |
|
205 |
|
206 def __lastEditorClosed(self): |
|
207 """ |
|
208 Private slot to handle the last editor closed signal of the view |
|
209 manager. |
|
210 """ |
|
211 self.hide() |
|
212 |
|
213 def show(self): |
|
214 """ |
|
215 Public slot to show the DIS viewer. |
|
216 """ |
|
217 super().show() |
|
218 |
|
219 if ( |
|
220 self.__mode == PythonDisViewerModes.SOURCEDISASSEMBLY and |
|
221 not self.__vmConnected |
|
222 ): |
|
223 self.__vm.editorChangedEd.connect(self.__editorChanged) |
|
224 self.__vm.editorSavedEd.connect(self.__editorSaved) |
|
225 self.__vm.editorLineChangedEd.connect(self.__editorLineChanged) |
|
226 self.__vm.editorLanguageChanged.connect( |
|
227 self.__editorLanguageChanged) |
|
228 self.__vmConnected = True |
|
229 |
|
230 self.__styleLabels() |
|
231 |
|
232 def hide(self): |
|
233 """ |
|
234 Public slot to hide the DIS viewer. |
|
235 """ |
|
236 super().hide() |
|
237 |
|
238 if self.__editor: |
|
239 self.__editor.clearAllHighlights() |
|
240 |
|
241 if ( |
|
242 self.__mode == PythonDisViewerModes.SOURCEDISASSEMBLY and |
|
243 self.__vmConnected |
|
244 ): |
|
245 self.__vm.editorChangedEd.disconnect(self.__editorChanged) |
|
246 self.__vm.editorSavedEd.disconnect(self.__editorSaved) |
|
247 self.__vm.editorLineChangedEd.disconnect(self.__editorLineChanged) |
|
248 self.__vm.editorLanguageChanged.disconnect( |
|
249 self.__editorLanguageChanged) |
|
250 self.__vmConnected = False |
|
251 |
|
252 def shutdown(self): |
|
253 """ |
|
254 Public method to perform shutdown actions. |
|
255 """ |
|
256 self.__editor = None |
|
257 |
|
258 def __disViewerStateChanged(self, on): |
|
259 """ |
|
260 Private slot to toggle the display of the Disassembly viewer. |
|
261 |
|
262 @param on flag indicating to show the Disassembly |
|
263 @type bool |
|
264 """ |
|
265 if self.__mode == PythonDisViewerModes.SOURCEDISASSEMBLY: |
|
266 editor = self.__vm.activeWindow() |
|
267 if on: |
|
268 if editor is not self.__editor: |
|
269 self.__editor = editor |
|
270 self.show() |
|
271 self.__loadDIS() |
|
272 else: |
|
273 self.hide() |
|
274 self.__editor = None |
|
275 |
|
276 def __expandAllDis(self): |
|
277 """ |
|
278 Private slot to expand all items of the disassembly widget. |
|
279 """ |
|
280 block = self.disWidget.blockSignals(True) |
|
281 self.disWidget.expandAll() |
|
282 self.disWidget.blockSignals(block) |
|
283 self.__resizeDisColumns() |
|
284 |
|
285 def __collapseAllDis(self): |
|
286 """ |
|
287 Private slot to collapse all items of the disassembly widget. |
|
288 """ |
|
289 block = self.disWidget.blockSignals(True) |
|
290 self.disWidget.collapseAll() |
|
291 self.disWidget.blockSignals(block) |
|
292 self.__resizeDisColumns() |
|
293 |
|
294 def __createErrorItem(self, error): |
|
295 """ |
|
296 Private method to create a top level error item. |
|
297 |
|
298 @param error error message |
|
299 @type str |
|
300 @return generated item |
|
301 @rtype QTreeWidgetItem |
|
302 """ |
|
303 itm = QTreeWidgetItem(self.disWidget, [error]) |
|
304 itm.setFirstColumnSpanned(True) |
|
305 itm.setForeground(0, self.__errorColor) |
|
306 return itm |
|
307 |
|
308 def __createTitleItem(self, title, line, parentItem): |
|
309 """ |
|
310 Private method to create a title item. |
|
311 |
|
312 @param title titel string for the item |
|
313 @type str |
|
314 @param line start line of the titled disassembly |
|
315 @type int |
|
316 @param parentItem reference to the parent item |
|
317 @type QTreeWidget or QTreeWidgetItem |
|
318 @return generated item |
|
319 @rtype QTreeWidgetItem |
|
320 """ |
|
321 itm = QTreeWidgetItem(parentItem, [title]) |
|
322 itm.setFirstColumnSpanned(True) |
|
323 itm.setExpanded(True) |
|
324 |
|
325 itm.setData(0, self.StartLineRole, line) |
|
326 itm.setData(0, self.EndLineRole, line) |
|
327 |
|
328 return itm |
|
329 |
|
330 def __createInstructionItem(self, instr, parent, lasti=-1): |
|
331 """ |
|
332 Private method to create an item for the given instruction. |
|
333 |
|
334 @param instr instruction the item should be based on |
|
335 @type dis.Instruction |
|
336 @param parent reference to the parent item |
|
337 @type QTreeWidgetItem |
|
338 @param lasti index of the instruction of a traceback |
|
339 @type int |
|
340 @return generated item |
|
341 @rtype QTreeWidgetItem |
|
342 """ |
|
343 fields = [] |
|
344 # Column: Source code line number (right aligned) |
|
345 if instr.starts_line: |
|
346 fields.append("{0:d}".format(instr.starts_line)) |
|
347 else: |
|
348 fields.append("") |
|
349 # Column: Instruction offset from start of code sequence |
|
350 # (right aligned) |
|
351 fields.append("{0:d}".format(instr.offset)) |
|
352 # Column: Opcode name |
|
353 fields.append(instr.opname) |
|
354 # Column: Opcode argument (right aligned) |
|
355 if instr.arg is not None: |
|
356 fields.append(repr(instr.arg)) |
|
357 # Column: Opcode argument details |
|
358 if instr.argrepr: |
|
359 fields.append('(' + instr.argrepr + ')') |
|
360 |
|
361 itm = QTreeWidgetItem(parent, fields) |
|
362 for col in (0, 1, 3): |
|
363 itm.setTextAlignment(col, Qt.AlignmentFlag.AlignRight) |
|
364 # set font to indicate current instruction and jump target |
|
365 font = itm.font(0) |
|
366 if instr.offset == lasti: |
|
367 font.setItalic(True) |
|
368 if instr.is_jump_target: |
|
369 font.setBold(True) |
|
370 for col in range(itm.columnCount()): |
|
371 itm.setFont(col, font) |
|
372 # set color to indicate current instruction or jump target |
|
373 if instr.offset == lasti: |
|
374 foreground = self.__currentInstructionColor |
|
375 elif instr.is_jump_target: |
|
376 foreground = self.__jumpTargetColor |
|
377 else: |
|
378 foreground = None |
|
379 if foreground: |
|
380 for col in range(itm.columnCount()): |
|
381 itm.setForeground(col, foreground) |
|
382 |
|
383 itm.setExpanded(True) |
|
384 |
|
385 if instr.starts_line: |
|
386 itm.setData(0, self.StartLineRole, instr.starts_line) |
|
387 itm.setData(0, self.EndLineRole, instr.starts_line) |
|
388 else: |
|
389 # get line from parent (= start line) |
|
390 lineno = parent.data(0, self.StartLineRole) |
|
391 itm.setData(0, self.StartLineRole, lineno) |
|
392 itm.setData(0, self.EndLineRole, lineno) |
|
393 return itm |
|
394 |
|
395 def __updateItemEndLine(self, itm): |
|
396 """ |
|
397 Private method to update an items end line based on its children. |
|
398 |
|
399 @param itm reference to the item to be updated |
|
400 @type QTreeWidgetItem |
|
401 """ |
|
402 endLine = ( |
|
403 max(itm.child(index).data(0, self.EndLineRole) |
|
404 for index in range(itm.childCount())) |
|
405 if itm.childCount() else |
|
406 itm.data(0, self.StartLineRole) |
|
407 ) |
|
408 itm.setData(0, self.EndLineRole, endLine) |
|
409 |
|
410 def __createCodeInfo(self, co): |
|
411 """ |
|
412 Private method to create a dictionary containing the code info data. |
|
413 |
|
414 @param co reference to the code object to generate the info for |
|
415 @type code |
|
416 @return dictionary containing the code info data |
|
417 @rtype dict |
|
418 """ |
|
419 codeInfoDict = { |
|
420 "name": co.co_name, |
|
421 "filename": co.co_filename, |
|
422 "firstlineno": co.co_firstlineno, |
|
423 "argcount": co.co_argcount, |
|
424 "kwonlyargcount": co.co_kwonlyargcount, |
|
425 "nlocals": co.co_nlocals, |
|
426 "stacksize": co.co_stacksize, |
|
427 "flags": dis.pretty_flags(co.co_flags), |
|
428 "consts": [str(const) for const in co.co_consts], |
|
429 "names": [str(name) for name in co.co_names], |
|
430 "varnames": [str(name) for name in co.co_varnames], |
|
431 "freevars": [str(var) for var in co.co_freevars], |
|
432 "cellvars": [str(var) for var in co.co_cellvars], |
|
433 } |
|
434 try: |
|
435 codeInfoDict["posonlyargcount"] = co.co_posonlyargcount |
|
436 except AttributeError: |
|
437 # does not exist prior to 3.8.0 |
|
438 codeInfoDict["posonlyargcount"] = 0 |
|
439 |
|
440 return codeInfoDict |
|
441 |
|
442 def __loadDIS(self): |
|
443 """ |
|
444 Private method to generate the Disassembly from the source of the |
|
445 current editor and visualize it. |
|
446 """ |
|
447 if self.__mode != PythonDisViewerModes.SOURCEDISASSEMBLY: |
|
448 # wrong mode, just return |
|
449 return |
|
450 |
|
451 if not self.__editor: |
|
452 self.__createErrorItem(self.tr( |
|
453 "No editor has been opened." |
|
454 )) |
|
455 return |
|
456 |
|
457 self.clear() |
|
458 self.__editor.clearAllHighlights() |
|
459 self.codeInfoWidget.hide() |
|
460 |
|
461 source = self.__editor.text() |
|
462 if not source.strip(): |
|
463 # empty editor or white space only |
|
464 self.__createErrorItem(self.tr( |
|
465 "The current editor does not contain any source code." |
|
466 )) |
|
467 return |
|
468 |
|
469 if not self.__editor.isPyFile(): |
|
470 self.__createErrorItem(self.tr( |
|
471 "The current editor does not contain Python source code." |
|
472 )) |
|
473 return |
|
474 |
|
475 filename = self.__editor.getFileName() |
|
476 filename = os.path.basename(filename) if filename else "<dis>" |
|
477 |
|
478 with E5OverrideCursor(): |
|
479 try: |
|
480 codeObject = self.__tryCompile(source, filename) |
|
481 except Exception as exc: |
|
482 codeObject = None |
|
483 self.__createErrorItem(str(exc)) |
|
484 |
|
485 if codeObject: |
|
486 self.setUpdatesEnabled(False) |
|
487 block = self.disWidget.blockSignals(True) |
|
488 |
|
489 self.__disassembleObject(codeObject, self.disWidget, filename) |
|
490 QTimer.singleShot(0, self.__resizeDisColumns) |
|
491 |
|
492 self.disWidget.blockSignals(block) |
|
493 self.setUpdatesEnabled(True) |
|
494 |
|
495 @pyqtSlot(dict) |
|
496 def showDisassembly(self, disassembly): |
|
497 """ |
|
498 Public slot to receive a code disassembly from the debug client. |
|
499 |
|
500 @param disassembly dictionary containing the disassembly information |
|
501 @type dict |
|
502 """ |
|
503 if ( |
|
504 self.__mode == PythonDisViewerModes.TRACEBACK and |
|
505 disassembly and |
|
506 "instructions" in disassembly and |
|
507 disassembly["instructions"] |
|
508 ): |
|
509 self.disWidget.clear() |
|
510 |
|
511 self.setUpdatesEnabled(False) |
|
512 block = self.disWidget.blockSignals(True) |
|
513 |
|
514 titleItem = self.__createTitleItem( |
|
515 self.tr("Disassembly of last traceback"), |
|
516 disassembly["firstlineno"], |
|
517 self.disWidget |
|
518 ) |
|
519 |
|
520 lasti = disassembly["lasti"] |
|
521 lastStartItem = None |
|
522 for instrDict in disassembly["instructions"]: |
|
523 instr = dis.Instruction( |
|
524 instrDict["opname"], |
|
525 0, # dummy value |
|
526 instrDict["arg"], |
|
527 "", # dummy value |
|
528 instrDict["argrepr"], |
|
529 instrDict["offset"], |
|
530 instrDict["lineno"], |
|
531 instrDict["isJumpTarget"], |
|
532 ) |
|
533 if instrDict["lineno"] > 0: |
|
534 if lastStartItem: |
|
535 self.__updateItemEndLine(lastStartItem) |
|
536 lastStartItem = self.__createInstructionItem( |
|
537 instr, titleItem, lasti=lasti) |
|
538 else: |
|
539 self.__createInstructionItem( |
|
540 instr, lastStartItem, lasti=lasti) |
|
541 if lastStartItem: |
|
542 self.__updateItemEndLine(lastStartItem) |
|
543 |
|
544 QTimer.singleShot(0, self.__resizeDisColumns) |
|
545 |
|
546 self.disWidget.blockSignals(block) |
|
547 self.setUpdatesEnabled(True) |
|
548 |
|
549 if lasti: |
|
550 lastInstructions = self.disWidget.findItems( |
|
551 "{0:d}".format(lasti), |
|
552 Qt.MatchFlag.MatchFixedString | |
|
553 Qt.MatchFlag.MatchRecursive, |
|
554 1 |
|
555 ) |
|
556 if lastInstructions: |
|
557 self.disWidget.scrollToItem( |
|
558 lastInstructions[0], |
|
559 QAbstractItemView.ScrollHint.PositionAtCenter) |
|
560 |
|
561 if "codeinfo" in disassembly: |
|
562 self.__showCodeInfoData(disassembly["codeinfo"]) |
|
563 |
|
564 def __resizeDisColumns(self): |
|
565 """ |
|
566 Private method to resize the columns of the disassembly widget to |
|
567 suitable values. |
|
568 """ |
|
569 for col in range(self.disWidget.columnCount()): |
|
570 self.disWidget.resizeColumnToContents(col) |
|
571 |
|
572 def resizeEvent(self, evt): |
|
573 """ |
|
574 Protected method to handle resize events. |
|
575 |
|
576 @param evt resize event |
|
577 @type QResizeEvent |
|
578 """ |
|
579 # just adjust the sizes of the columns |
|
580 self.__resizeDisColumns() |
|
581 self.__resizeCodeInfoColumns() |
|
582 |
|
583 def __clearSelection(self): |
|
584 """ |
|
585 Private method to clear all selected items. |
|
586 """ |
|
587 for itm in self.disWidget.selectedItems(): |
|
588 itm.setSelected(False) |
|
589 |
|
590 def __selectChildren(self, itm, lineno): |
|
591 """ |
|
592 Private method to select children of the given item covering the given |
|
593 line number. |
|
594 |
|
595 @param itm reference to the item |
|
596 @type QTreeWidgetItem |
|
597 @param lineno line number to base the selection on |
|
598 @type int |
|
599 """ |
|
600 for index in range(itm.childCount()): |
|
601 child = itm.child(index) |
|
602 if ( |
|
603 child.data(0, self.StartLineRole) <= lineno <= |
|
604 child.data(0, self.EndLineRole) |
|
605 ): |
|
606 child.setSelected(True) |
|
607 self.__selectChildren(child, lineno) |
|
608 |
|
609 if child.data(0, self.StartLineRole) == lineno: |
|
610 self.disWidget.scrollToItem( |
|
611 child, QAbstractItemView.ScrollHint.PositionAtCenter) |
|
612 |
|
613 def __selectItemForEditorLine(self): |
|
614 """ |
|
615 Private slot to select the items corresponding with the cursor line |
|
616 of the current editor. |
|
617 """ |
|
618 # step 1: clear all selected items |
|
619 self.__clearSelection() |
|
620 |
|
621 # step 2: retrieve the editor cursor line |
|
622 cline, cindex = self.__editor.getCursorPosition() |
|
623 # make the line numbers 1-based |
|
624 cline += 1 |
|
625 |
|
626 for index in range(self.disWidget.topLevelItemCount()): |
|
627 itm = self.disWidget.topLevelItem(index) |
|
628 if ( |
|
629 itm.data(0, self.StartLineRole) <= cline <= |
|
630 itm.data(0, self.EndLineRole) |
|
631 ): |
|
632 itm.setSelected(True) |
|
633 self.__selectChildren(itm, cline) |
|
634 |
|
635 @pyqtSlot(QTreeWidgetItem, int) |
|
636 def __disItemClicked(self, itm, column): |
|
637 """ |
|
638 Private slot handling a user click on a Disassembly node item. |
|
639 |
|
640 @param itm reference to the clicked item |
|
641 @type QTreeWidgetItem |
|
642 @param column column number of the click |
|
643 @type int |
|
644 """ |
|
645 self.__editor.clearAllHighlights() |
|
646 |
|
647 if itm is not None: |
|
648 startLine = itm.data(0, self.StartLineRole) |
|
649 endLine = itm.data(0, self.EndLineRole) |
|
650 |
|
651 self.__editor.gotoLine(startLine, firstVisible=True, |
|
652 expand=True) |
|
653 self.__editor.setHighlight(startLine - 1, 0, endLine, -1) |
|
654 |
|
655 def __tryCompile(self, source, name): |
|
656 """ |
|
657 Private method to attempt to compile the given source, first as an |
|
658 expression and then as a statement if the first approach fails. |
|
659 |
|
660 @param source source code string to be compiled |
|
661 @type str |
|
662 @param name name of the file containing the source |
|
663 @type str |
|
664 @return compiled code |
|
665 @rtype code object |
|
666 """ |
|
667 try: |
|
668 c = compile(source, name, 'eval') |
|
669 except SyntaxError: |
|
670 c = compile(source, name, 'exec') |
|
671 return c |
|
672 |
|
673 def __disassembleObject(self, co, parentItem, parentName="", lasti=-1): |
|
674 """ |
|
675 Private method to disassemble the given code object recursively. |
|
676 |
|
677 @param co code object to be disassembled |
|
678 @type code object |
|
679 @param parentItem reference to the parent item |
|
680 @type QTreeWidget or QTreeWidgetItem |
|
681 @param parentName name of the parent code object |
|
682 @type str |
|
683 @param lasti index of the instruction of a traceback |
|
684 @type int |
|
685 """ |
|
686 if co.co_name == "<module>": |
|
687 title = os.path.basename(co.co_filename) |
|
688 name = "" |
|
689 else: |
|
690 if parentName: |
|
691 name = "{0}.{1}".format(parentName, co.co_name) |
|
692 else: |
|
693 name = co.co_name |
|
694 title = self.tr("Code Object '{0}'").format(name) |
|
695 titleItem = self.__createTitleItem(title, co.co_firstlineno, |
|
696 parentItem) |
|
697 codeInfo = self.__createCodeInfo(co) |
|
698 if codeInfo: |
|
699 titleItem.setData(0, self.CodeInfoRole, codeInfo) |
|
700 |
|
701 lastStartItem = None |
|
702 for instr in dis.get_instructions(co): |
|
703 if instr.starts_line: |
|
704 if lastStartItem: |
|
705 self.__updateItemEndLine(lastStartItem) |
|
706 lastStartItem = self.__createInstructionItem( |
|
707 instr, titleItem, lasti=lasti) |
|
708 else: |
|
709 self.__createInstructionItem(instr, lastStartItem, lasti=lasti) |
|
710 if lastStartItem: |
|
711 self.__updateItemEndLine(lastStartItem) |
|
712 |
|
713 for x in co.co_consts: |
|
714 if hasattr(x, 'co_code'): |
|
715 self.__disassembleObject(x, titleItem, parentName=name, |
|
716 lasti=lasti) |
|
717 |
|
718 self.__updateItemEndLine(titleItem) |
|
719 |
|
720 @pyqtSlot() |
|
721 def preferencesChanged(self): |
|
722 """ |
|
723 Public slot handling changes of the Disassembly viewer settings. |
|
724 """ |
|
725 self.__errorColor = QBrush( |
|
726 Preferences.getPython("DisViewerErrorColor")) |
|
727 self.__currentInstructionColor = QBrush( |
|
728 Preferences.getPython("DisViewerCurrentColor")) |
|
729 self.__jumpTargetColor = QBrush( |
|
730 Preferences.getPython("DisViewerLabeledColor")) |
|
731 |
|
732 self.__showCodeInfoDetails = Preferences.getPython( |
|
733 "DisViewerExpandCodeInfoDetails") |
|
734 |
|
735 if self.isVisible(): |
|
736 self.__loadDIS() |
|
737 |
|
738 self.__styleLabels() |
|
739 |
|
740 def __styleLabels(self): |
|
741 """ |
|
742 Private method to style the info labels iaw. selected colors. |
|
743 """ |
|
744 # current instruction |
|
745 self.currentInfoLabel.setStyleSheet( |
|
746 "QLabel {{ color : {0}; }}".format( |
|
747 self.__currentInstructionColor.color().name() |
|
748 ) |
|
749 ) |
|
750 font = self.currentInfoLabel.font() |
|
751 font.setItalic(True) |
|
752 self.currentInfoLabel.setFont(font) |
|
753 |
|
754 # labeled instruction |
|
755 self.labeledInfoLabel.setStyleSheet( |
|
756 "QLabel {{ color : {0}; }}".format( |
|
757 self.__jumpTargetColor.color().name() |
|
758 ) |
|
759 ) |
|
760 font = self.labeledInfoLabel.font() |
|
761 font.setBold(True) |
|
762 self.labeledInfoLabel.setFont(font) |
|
763 |
|
764 @pyqtSlot() |
|
765 def clear(self): |
|
766 """ |
|
767 Public method to clear the display. |
|
768 """ |
|
769 self.disWidget.clear() |
|
770 self.codeInfoWidget.clear() |
|
771 |
|
772 def __showCodeInfo(self): |
|
773 """ |
|
774 Private slot handling the context menu action to show code info. |
|
775 """ |
|
776 itm = self.disWidget.currentItem() |
|
777 codeInfo = itm.data(0, self.CodeInfoRole) |
|
778 if codeInfo: |
|
779 self.codeInfoWidget.show() |
|
780 self.__showCodeInfoData(codeInfo) |
|
781 |
|
782 def __showCodeInfoData(self, codeInfo): |
|
783 """ |
|
784 Private method to show the passed code info data. |
|
785 |
|
786 @param codeInfo dictionary containing the code info data |
|
787 @type dict |
|
788 """ |
|
789 def createCodeInfoItems(title, infoList): |
|
790 """ |
|
791 Function to create code info items for the given list. |
|
792 |
|
793 @param title title string for the list |
|
794 @type str |
|
795 @param infoList list of info strings |
|
796 @type list of str |
|
797 """ |
|
798 parent = QTreeWidgetItem(self.codeInfoWidget, |
|
799 [title, str(len(infoList))]) |
|
800 parent.setExpanded(self.__showCodeInfoDetails) |
|
801 |
|
802 for index, value in enumerate(infoList): |
|
803 itm = QTreeWidgetItem(parent, [str(index), str(value)]) |
|
804 itm.setTextAlignment(0, Qt.AlignmentFlag.AlignRight) |
|
805 |
|
806 self.codeInfoWidget.clear() |
|
807 |
|
808 if codeInfo: |
|
809 QTreeWidgetItem(self.codeInfoWidget, [ |
|
810 self.tr("Name"), codeInfo["name"]]) |
|
811 QTreeWidgetItem(self.codeInfoWidget, [ |
|
812 self.tr("Filename"), codeInfo["filename"]]) |
|
813 QTreeWidgetItem(self.codeInfoWidget, [ |
|
814 self.tr("First Line"), str(codeInfo["firstlineno"])]) |
|
815 QTreeWidgetItem(self.codeInfoWidget, [ |
|
816 self.tr("Argument Count"), str(codeInfo["argcount"])]) |
|
817 QTreeWidgetItem(self.codeInfoWidget, [ |
|
818 self.tr("Positional-only Arguments"), |
|
819 str(codeInfo["posonlyargcount"])]) |
|
820 QTreeWidgetItem(self.codeInfoWidget, [ |
|
821 self.tr("Keyword-only Arguments"), |
|
822 str(codeInfo["kwonlyargcount"])]) |
|
823 QTreeWidgetItem(self.codeInfoWidget, [ |
|
824 self.tr("Number of Locals"), str(codeInfo["nlocals"])]) |
|
825 QTreeWidgetItem(self.codeInfoWidget, [ |
|
826 self.tr("Stack Size"), str(codeInfo["stacksize"])]) |
|
827 QTreeWidgetItem(self.codeInfoWidget, [ |
|
828 self.tr("Flags"), codeInfo["flags"]]) |
|
829 if codeInfo["consts"]: |
|
830 createCodeInfoItems(self.tr("Constants"), |
|
831 codeInfo["consts"]) |
|
832 if codeInfo["names"]: |
|
833 createCodeInfoItems(self.tr("Names"), |
|
834 codeInfo["names"]) |
|
835 if codeInfo["varnames"]: |
|
836 createCodeInfoItems(self.tr("Variable Names"), |
|
837 codeInfo["varnames"]) |
|
838 if codeInfo["freevars"]: |
|
839 createCodeInfoItems(self.tr("Free Variables"), |
|
840 codeInfo["freevars"]) |
|
841 if codeInfo["cellvars"]: |
|
842 createCodeInfoItems(self.tr("Cell Variables"), |
|
843 codeInfo["cellvars"]) |
|
844 |
|
845 QTimer.singleShot(0, self.__resizeCodeInfoColumns) |
|
846 |
|
847 def __resizeCodeInfoColumns(self): |
|
848 """ |
|
849 Private method to resize the columns of the code info widget to |
|
850 suitable values. |
|
851 """ |
|
852 for col in range(self.codeInfoWidget.columnCount()): |
|
853 self.codeInfoWidget.resizeColumnToContents(col) |
|
854 |
|
855 def __expandAllCodeInfo(self): |
|
856 """ |
|
857 Private slot to expand all items of the code info widget. |
|
858 """ |
|
859 block = self.codeInfoWidget.blockSignals(True) |
|
860 self.codeInfoWidget.expandAll() |
|
861 self.codeInfoWidget.blockSignals(block) |
|
862 self.__resizeCodeInfoColumns() |
|
863 |
|
864 def __collapseAllCodeInfo(self): |
|
865 """ |
|
866 Private slot to collapse all items of the code info widget. |
|
867 """ |
|
868 block = self.codeInfoWidget.blockSignals(True) |
|
869 self.codeInfoWidget.collapseAll() |
|
870 self.codeInfoWidget.blockSignals(block) |
|
871 self.__resizeCodeInfoColumns() |
|
872 |
|
873 def __codeInfoContextMenuRequested(self, coord): |
|
874 """ |
|
875 Private slot to show the context menu of the code info widget. |
|
876 |
|
877 @param coord position of the mouse pointer |
|
878 @type QPoint |
|
879 """ |
|
880 if self.disWidget.topLevelItemCount() > 0: |
|
881 # don't show context menu on empty list |
|
882 coord = self.codeInfoWidget.mapToGlobal(coord) |
|
883 self.__codeInfoMenu.popup(coord) |
|
884 |
|
885 def __configure(self): |
|
886 """ |
|
887 Private method to open the configuration dialog. |
|
888 """ |
|
889 e5App().getObject("UserInterface").showPreferences( |
|
890 "pythonPage") |