diff -r 9251c4dc4f7a -r 90a9aefd4253 eric6/UI/PythonDisViewer.py --- a/eric6/UI/PythonDisViewer.py Sat Sep 19 19:04:21 2020 +0200 +++ b/eric6/UI/PythonDisViewer.py Sun Sep 20 18:32:28 2020 +0200 @@ -15,9 +15,11 @@ from PyQt5.QtGui import QCursor, QBrush from PyQt5.QtWidgets import ( QTreeWidget, QApplication, QTreeWidgetItem, QAbstractItemView, QWidget, - QVBoxLayout, QLabel + QVBoxLayout, QLabel, QMenu ) +import Preferences + class PythonDisViewer(QWidget): """ @@ -44,11 +46,12 @@ self.__layout.addWidget(self.__disWidget) self.__layout.setContentsMargins(0, 0, 0, 0) - self.__infoLabel = QLabel(self.tr( - "italic: current instruction\n" - "bold: labelled instruction" - )) - self.__layout.addWidget(self.__infoLabel) + self.__currentInfoLabel = QLabel(self.tr( + "italic: current instruction")) + self.__labeledInfoLabel = QLabel(self.tr( + "bold: labeled instruction")) + self.__layout.addWidget(self.__currentInfoLabel) + self.__layout.addWidget(self.__labeledInfoLabel) self.__vm = viewmanager self.__vmConnected = False @@ -64,12 +67,39 @@ self.__disWidget.setSelectionMode(QAbstractItemView.SingleSelection) self.__disWidget.setAlternatingRowColors(True) + self.__menu = QMenu(self.__disWidget) + self.__menu.addAction(self.tr('Expand All'), self.__expandAll) + self.__menu.addAction(self.tr('Collapse All'), self.__collapseAll) + + self.__disWidget.setContextMenuPolicy(Qt.CustomContextMenu) + self.__disWidget.customContextMenuRequested.connect( + self.__contextMenuRequested) + + self.__errorColor = QBrush( + Preferences.getPython("DisViewerErrorColor")) + self.__currentInstructionColor = QBrush( + Preferences.getPython("DisViewerCurrentColor")) + self.__jumpTargetColor = QBrush( + Preferences.getPython("DisViewerLabeledColor")) + self.__disWidget.itemClicked.connect(self.__disItemClicked) + self.__disWidget.itemCollapsed.connect(self.__resizeColumns) + self.__disWidget.itemExpanded.connect(self.__resizeColumns) self.__vm.disViewerStateChanged.connect(self.__disViewerStateChanged) self.hide() + def __contextMenuRequested(self, coord): + """ + Private slot to show the context menu. + + @param coord position of the mouse pointer + @type QPoint + """ + coord = self.__disWidget.mapToGlobal(coord) + self.__menu.popup(coord) + def __editorChanged(self, editor): """ Private slot to handle a change of the current editor. @@ -95,25 +125,22 @@ if editor and editor is self.__editor: self.__loadDIS() - def __editorDoubleClicked(self, editor, pos, buttons): + def __editorLineChanged(self, editor, lineno): """ Private slot to handle a mouse button double click in the editor. @param editor reference to the editor, that emitted the signal @type Editor - @param pos position of the double click - @type QPoint - @param buttons mouse buttons that were double clicked - @type Qt.MouseButtons + @param lineno line number of the editor's cursor (zero based) + @type int """ - if editor is self.__editor and buttons == Qt.LeftButton: + if editor is self.__editor: if editor.isModified(): # reload the source QTimer.singleShot(0, self.__loadDIS) # highlight the corresponding entry -## QTimer.singleShot(0, self.__selectItemForEditorSelection) - QTimer.singleShot(0, self.__grabFocus) + QTimer.singleShot(0, self.__selectItemForEditorLine) def __lastEditorClosed(self): """ @@ -131,7 +158,7 @@ if not self.__vmConnected: self.__vm.editorChangedEd.connect(self.__editorChanged) self.__vm.editorSavedEd.connect(self.__editorSaved) - self.__vm.editorDoubleClickedEd.connect(self.__editorDoubleClicked) + self.__vm.editorLineChangedEd.connect(self.__editorLineChanged) self.__vmConnected = True def hide(self): @@ -146,8 +173,7 @@ if self.__vmConnected: self.__vm.editorChangedEd.disconnect(self.__editorChanged) self.__vm.editorSavedEd.disconnect(self.__editorSaved) - self.__vm.editorDoubleClickedEd.disconnect( - self.__editorDoubleClicked) + self.__vm.editorLineChangedEd.disconnect(self.__editorLineChanged) self.__vmConnected = False def shutdown(self): @@ -173,6 +199,24 @@ self.hide() self.__editor = None + def __expandAll(self): + """ + Private slot to expand all items. + """ + block = self.__disWidget.blockSignals(True) + self.__disWidget.expandAll() + self.__disWidget.blockSignals(block) + self.__resizeColumns() + + def __collapseAll(self): + """ + Private slot to collapse all items. + """ + block = self.__disWidget.blockSignals(True) + self.__disWidget.collapseAll() + self.__disWidget.blockSignals(block) + self.__resizeColumns() + def __createErrorItem(self, error): """ Private method to create a top level error item. @@ -184,10 +228,10 @@ """ itm = QTreeWidgetItem(self.__disWidget, [error]) itm.setFirstColumnSpanned(True) - itm.setForeground(0, QBrush(Qt.red)) + itm.setForeground(0, self.__errorColor) return itm - def __createTitleItem(self, title, line): + def __createTitleItem(self, title, line, parentItem): """ Private method to create a title item. @@ -195,10 +239,12 @@ @type str @param line start line of the titled disassembly @type int + @param parentItem reference to the parent item + @type QTreeWidget or QTreeWidgetItem @return generated item @rtype QTreeWidgetItem """ - itm = QTreeWidgetItem(self.__disWidget, [title]) + itm = QTreeWidgetItem(parentItem, [title]) itm.setFirstColumnSpanned(True) itm.setExpanded(True) @@ -241,6 +287,7 @@ itm = QTreeWidgetItem(parent, fields) for col in (0, 1, 3): itm.setTextAlignment(col, Qt.AlignRight) + # set font to indicate current instruction and jump target font = itm.font(0) if instr.offset == lasti: font.setItalic(True) @@ -248,6 +295,16 @@ font.setBold(True) for col in range(itm.columnCount()): itm.setFont(col, font) + # set color to indicate current instruction or jump target + if instr.offset == lasti: + foreground = self.__currentInstructionColor + elif instr.is_jump_target: + foreground = self.__jumpTargetColor + else: + foreground = None + if foreground: + for col in range(itm.columnCount()): + itm.setForeground(col, foreground) itm.setExpanded(True) @@ -300,7 +357,9 @@ return filename = self.__editor.getFileName() - if not filename: + if filename: + filename = os.path.basename(filename) + else: filename = "<dis>" QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) @@ -312,10 +371,12 @@ if codeObject: self.setUpdatesEnabled(False) + block = self.__disWidget.blockSignals(True) self.__disassembleObject(codeObject, self.__disWidget, filename) QTimer.singleShot(0, self.__resizeColumns) + self.__disWidget.blockSignals(block) self.setUpdatesEnabled(True) QApplication.restoreOverrideCursor() @@ -339,6 +400,58 @@ # just adjust the sizes of the columns self.__resizeColumns() + def __clearSelection(self): + """ + Private method to clear all selected items. + """ + for itm in self.__disWidget.selectedItems(): + itm.setSelected(False) + + def __selectChildren(self, itm, lineno): + """ + Private method to select children of the given item covering the given + line number. + + @param itm reference to the item + @type QTreeWidgetItem + @param lineno line number to base the selection on + @type int + """ + for index in range(itm.childCount()): + child = itm.child(index) + if ( + child.data(0, self.StartLineRole) <= lineno <= + child.data(0, self.EndLineRole) + ): + child.setSelected(True) + self.__selectChildren(child, lineno) + + if child.data(0, self.StartLineRole) == lineno: + self.__disWidget.scrollToItem( + child, QAbstractItemView.PositionAtCenter) + + def __selectItemForEditorLine(self): + """ + Private slot to select the items corresponding with the cursor line + of the current editor. + """ + # step 1: clear all selected items + self.__clearSelection() + + # step 2: retrieve the editor cursor line + cline, cindex = self.__editor.getCursorPosition() + # make the line numbers 1-based + cline += 1 + + for index in range(self.__disWidget.topLevelItemCount()): + itm = self.__disWidget.topLevelItem(index) + if ( + itm.data(0, self.StartLineRole) <= cline <= + itm.data(0, self.EndLineRole) + ): + itm.setSelected(True) + self.__selectChildren(itm, cline) + def __grabFocus(self): """ Private method to grab the input focus. @@ -383,7 +496,7 @@ c = compile(source, name, 'exec') return c - def __disassembleObject(self, co, parentItem, name="", lasti=-1): + def __disassembleObject(self, co, parentItem, parentName="", lasti=-1): """ Private method to disassemble the given code object recursively. @@ -391,22 +504,22 @@ @type code object @param parentItem reference to the parent item @type QTreeWidget or QTreeWidgetItem - @param name name of the code object + @param parentName name of the parent code object @type str @param lasti index of the instruction of a traceback @type int """ if co.co_name == "<module>": - title = ( - self.tr("Disassembly of module '{0}'") - .format(os.path.basename(co.co_filename)) - ) + title = os.path.basename(co.co_filename) + name = "" else: - title = ( - self.tr("Disassembly of code object '{0}'") - .format(co.co_name) - ) - titleItem = self.__createTitleItem(title, co.co_firstlineno) + if parentName: + name = "{0}.{1}".format(parentName, co.co_name) + else: + name = co.co_name + title = self.tr("Code Object '{0}'").format(name) + titleItem = self.__createTitleItem(title, co.co_firstlineno, + parentItem) lastStartItem = None for instr in dis.get_instructions(co): if instr.starts_line: @@ -419,8 +532,24 @@ if lastStartItem: self.__updateItemEndLine(lastStartItem) - self.__updateItemEndLine(titleItem) - for x in co.co_consts: if hasattr(x, 'co_code'): - self.__disassembleObject(x, self.__disWidget, lasti=lasti) + self.__disassembleObject(x, titleItem, parentName=name, + lasti=lasti) + + self.__updateItemEndLine(titleItem) + + @pyqtSlot() + def preferencesChanged(self): + """ + Public slot handling changes of the Disassembly viewer settings. + """ + self.__errorColor = QBrush( + Preferences.getPython("DisViewerErrorColor")) + self.__currentInstructionColor = QBrush( + Preferences.getPython("DisViewerCurrentColor")) + self.__jumpTargetColor = QBrush( + Preferences.getPython("DisViewerLabeledColor")) + + if self.isVisible(): + self.__loadDIS()