diff --git a/plugins/extensions/pykrita/plugin/plugins/scripter/debugcontroller.py b/plugins/extensions/pykrita/plugin/plugins/scripter/debugcontroller.py index 9269d78c8d..4432e55518 100644 --- a/plugins/extensions/pykrita/plugin/plugins/scripter/debugcontroller.py +++ b/plugins/extensions/pykrita/plugin/plugins/scripter/debugcontroller.py @@ -1,63 +1,75 @@ from scripter.debugger_scripter import debugger from code import InteractiveConsole import asyncio class DebugController (object): def __init__(self, scripter): self._debugger = None self._cmd = None self.scripter = scripter def start(self, document): self.setCmd(compile(document.data, document.filePath, "exec")) self._debugger = debugger.Debugger(self.scripter, self._cmd) self._debugger.debugprocess.start() loop = asyncio.get_event_loop() loop.run_until_complete(self._debugger.start()) self.updateUIDebugger() def step(self): loop = asyncio.get_event_loop() loop.run_until_complete(self._debugger.step()) self.scripter.uicontroller.setStepped(True) self.updateUIDebugger() def stop(self): loop = asyncio.get_event_loop() loop.run_until_complete(self._debugger.stop()) self.updateUIDebugger() self._debugger = None def setCmd(self, cmd): self._cmd = cmd @property def isActive(self): try: if self._debugger: return self._debugger.debugprocess.is_alive() return False except: return False @property def currentLine(self): try: if self._debugger: - return int(self._debugger.application_data['lineNumber']) + return int(self._debugger.application_data['code']['lineNumber']) except: return 0 def updateUIDebugger(self): + widget = self.scripter.uicontroller.findStackWidget('Debugger') + if not self.isActive or self._quitDebugger(): - widget = self.scripter.uicontroller.findStackWidget('Debugger') widget.disableToolbar(True) + self.scripter.uicontroller.repaintDebugArea() + widget.updateWidget() + + @property + def debuggerData(self): + try: + if self._debugger: + return self._debugger.application_data + except: + return + def _quitDebugger(self): try: return self._debugger.application_data['quit'] except: return False diff --git a/plugins/extensions/pykrita/plugin/plugins/scripter/debugger_scripter/debugger.py b/plugins/extensions/pykrita/plugin/plugins/scripter/debugger_scripter/debugger.py index 2a361a9827..9b96c43857 100644 --- a/plugins/extensions/pykrita/plugin/plugins/scripter/debugger_scripter/debugger.py +++ b/plugins/extensions/pykrita/plugin/plugins/scripter/debugger_scripter/debugger.py @@ -1,93 +1,114 @@ import bdb import asyncio import inspect import multiprocessing +import re class Debugger(bdb.Bdb): def __init__(self, scripter, cmd): bdb.Bdb.__init__(self) self.quit = False self.debugq = multiprocessing.Queue() self.scripter = scripter self.applicationq = multiprocessing.Queue() # Create the debug process self.debugprocess = multiprocessing.Process(target=self.run, args=(cmd,)) self.application_data = {} self.currentLine = 0 # initialize parent bdb.Bdb.reset(self) def user_call(self, frame, args): name = frame.f_code.co_name or "" def user_line(self, frame): """Handler that executes with every line of code""" - self.setCurrentLine(frame.f_lineno) - self.applicationq.put({ "lineNumber": self.getCurrentLine()}) + co = frame.f_code + self.currentLine = frame.f_lineno + self.applicationq.put({ "code": { "file": co.co_filename, + "name": co.co_name, + "lineNumber": str(frame.f_lineno) + }, + "frame": { "firstLineNumber": co.co_firstlineno, + "locals": self.format_data(frame.f_locals), + "globals": self.format_data(frame.f_globals) + }, + "trace": "line" + }) if self.quit: return self.set_quit() - if self.getCurrentLine()==0: + if self.currentLine==0: return else: # Get a reference to the code object and source - co = frame.f_code source = inspect.getsourcelines(co)[0] # Wait for a debug command cmd = self.debugq.get() if cmd == "step": # If stepping through code, return this handler return if cmd == "stop": # If stopping execution, raise an exception return self.set_quit() def user_return(self, frame, value): name = frame.f_code.co_name or "" if name == '': self.applicationq.put({ "quit": True}) def user_exception(self, frame, exception): name = frame.f_code.co_name or "" - def getCurrentLine(self): - return self.currentLine + def format_data(self, data): + globals()['types'] = __import__('types') + + exclude_keys = ['copyright', 'credits', 'False', + 'True', 'None', 'Ellipsis', 'quit'] + exclude_valuetypes = [types.BuiltinFunctionType, + types.BuiltinMethodType, + types.ModuleType, + types.FunctionType] + + return [{k: v} for k, v in data.items() if not (k in exclude_keys or + type(v) in exclude_valuetypes or + re.search(r'^(__).*\1$', k))] - def setCurrentLine(self, line): - self.currentLine = line async def display(self): """Coroutine for updating the UI""" # Wait for the application queue to have an update to the GUI while True: if self.applicationq.empty(): - await asyncio.sleep(0.5) + await asyncio.sleep(0.3) else: # The application queue has at least one item, let's act on every item that's in it while not self.applicationq.empty(): # Get info to the GUI self.application_data = self.applicationq.get() self.scripter.uicontroller.repaintDebugArea() return async def start(self): await self.display() async def step(self): # Tell the debugger we want to step in self.debugq.put("step") await self.display() async def stop(self): # Tell the debugger we're stopping execution self.debugq.put("stop") + self.applicationq.put({ "quit": True}) + await self.display() diff --git a/plugins/extensions/pykrita/plugin/plugins/scripter/ui_scripter/editor/pythoneditor.py b/plugins/extensions/pykrita/plugin/plugins/scripter/ui_scripter/editor/pythoneditor.py index 545589ed04..fcfadd3cf0 100644 --- a/plugins/extensions/pykrita/plugin/plugins/scripter/ui_scripter/editor/pythoneditor.py +++ b/plugins/extensions/pykrita/plugin/plugins/scripter/ui_scripter/editor/pythoneditor.py @@ -1,147 +1,147 @@ from PyQt5.QtCore import * from PyQt5.QtWidgets import * from PyQt5.QtGui import * from scripter.ui_scripter.editor import linenumberarea, debugarea class CodeEditor(QPlainTextEdit): DEBUG_AREA_WIDTH = 20 def __init__(self, scripter, parent=None): super(CodeEditor, self).__init__(parent) self.setLineWrapMode(self.NoWrap) self.scripter = scripter self.lineNumberArea = linenumberarea.LineNumberArea(self) self.debugArea = debugarea.DebugArea(self) self.blockCountChanged.connect(self.updateMarginsWidth) self.updateRequest.connect(self.updateLineNumberArea) self.cursorPositionChanged.connect(self.highlightCurrentLine) self.updateMarginsWidth() self.highlightCurrentLine() self.font = "Monospace" self._stepped = False def debugAreaWidth(self): return self.DEBUG_AREA_WIDTH def lineNumberAreaWidth(self): """The lineNumberAreaWidth is the quatity of decimal places in blockCount""" digits = 1 max_ = max(1, self.blockCount()) while (max_ >= 10): max_ /= 10 digits += 1 space = 3 + self.fontMetrics().width('9') * digits return space def resizeEvent(self, event): super(CodeEditor, self).resizeEvent(event) qRect = self.contentsRect() self.debugArea.setGeometry(QRect(qRect.left(), qRect.top(), self.debugAreaWidth(), qRect.height())) self.lineNumberArea.setGeometry(QRect(qRect.left() + self.debugAreaWidth(), qRect.top(), self.lineNumberAreaWidth(), qRect.height())) def updateMarginsWidth(self): self.setViewportMargins(self.lineNumberAreaWidth() + self.debugAreaWidth(), 0, 0, 0) def updateLineNumberArea(self, rect, dy): """ This slot is invoked when the editors viewport has been scrolled """ if dy: self.lineNumberArea.scroll(0, dy) self.debugArea.scroll(0, dy) else: self.lineNumberArea.update(0, rect.y(), self.lineNumberArea.width(), rect.height()) if rect.contains(self.viewport().rect()): self.updateMarginsWidth() def lineNumberAreaPaintEvent(self, event): """This method draws the current lineNumberArea for while""" painter = QPainter(self.lineNumberArea) painter.fillRect(event.rect(), QColor(Qt.lightGray).darker(300)) block = self.firstVisibleBlock() blockNumber = block.blockNumber() top = int(self.blockBoundingGeometry(block).translated(self.contentOffset()).top()) bottom = top + int(self.blockBoundingRect(block).height()) while block.isValid() and top <= event.rect().bottom(): if block.isVisible() and bottom >= event.rect().top(): number = str(blockNumber + 1) painter.setPen(QColor(Qt.lightGray)) painter.drawText(0, top, self.lineNumberArea.width(), self.fontMetrics().height(), Qt.AlignRight, number) block = block.next() top = bottom bottom = top + int(self.blockBoundingRect(block).height()) blockNumber += 1 def debugAreaPaintEvent(self, event): if self.scripter.debugcontroller.isActive and self.scripter.debugcontroller.currentLine: lineNumber = self.scripter.debugcontroller.currentLine block = self.document().findBlockByLineNumber(lineNumber-1) if self._stepped: cursor = QTextCursor(block) self.setTextCursor(cursor) self._stepped = False top = int(self.blockBoundingGeometry(block).translated(self.contentOffset()).top()) bottom = top + int(self.blockBoundingRect(block).height()) painter = QPainter(self.debugArea) - painter.fillRect(0, top, self.debugAreaWidth()-3, int(self.blockBoundingRect(block).height()), QColor(Qt.yellow).darker(300)) + painter.fillRect(0, top, self.debugAreaWidth()-3, int(self.blockBoundingRect(block).height()), QColor(Qt.yellow)) def highlightCurrentLine(self): """Highlight current line under cursor""" currentSelection = QTextEdit.ExtraSelection() lineColor = QColor(Qt.gray).darker(250) currentSelection.format.setBackground(lineColor) currentSelection.format.setProperty(QTextFormat.FullWidthSelection, True) currentSelection.cursor = self.textCursor() currentSelection.cursor.clearSelection() self.setExtraSelections([currentSelection]) def wheelEvent(self, e): """When the CTRL is pressed during the wheelEvent, zoomIn and zoomOut slots are invoked""" if e.modifiers() == Qt.ControlModifier: delta = e.angleDelta().y() if delta < 0: self.zoomOut() elif delta > 0: self.zoomIn() else: super(CodeEditor, self).wheelEvent(e) @property def font(self): return self._font @font.setter def font(self, font): self._font = font self.setFont(QFont(font, 10)) def setStepped(self, status): self._stepped = status def repaintDebugArea(self): self.debugArea.repaint() diff --git a/plugins/extensions/pykrita/plugin/plugins/scripter/ui_scripter/stackwidgets/debuggerwidget/debuggertable.py b/plugins/extensions/pykrita/plugin/plugins/scripter/ui_scripter/stackwidgets/debuggerwidget/debuggertable.py new file mode 100644 index 0000000000..c07f78eae1 --- /dev/null +++ b/plugins/extensions/pykrita/plugin/plugins/scripter/ui_scripter/stackwidgets/debuggerwidget/debuggertable.py @@ -0,0 +1,38 @@ +from PyQt5.QtWidgets import QTableWidget, QTableWidgetItem + + +class DebuggerTable(QTableWidget): + + def __init__(self, parent=None): + super(DebuggerTable, self).__init__(parent) + + self.setRowCount(10) + self.setColumnCount(4) + + tableHeader = ['Scope', 'Name', 'Value', 'Type'] + self.setHorizontalHeaderLabels(tableHeader) + self.setEditTriggers(self.NoEditTriggers) + + def updateTable(self, data): + self.clearContents() + + if data and not data.get('quit'): + line = 0 + locals_list = data['frame']['locals'] + globals_list = data['frame']['globals'] + + for item in locals_list: + for key, value in item.items(): + self.setItem(line, 0, QTableWidgetItem('locals')) + self.setItem(line, 1, QTableWidgetItem(key)) + self.setItem(line, 2, QTableWidgetItem(str(value))) + self.setItem(line, 3, QTableWidgetItem(str(type(value)))) + line += 1 + + for item in globals_list: + for key, value in item.items(): + self.setItem(line, 0, QTableWidgetItem('globals')) + self.setItem(line, 1, QTableWidgetItem(key)) + self.setItem(line, 2, QTableWidgetItem(str(value))) + self.setItem(line, 3, QTableWidgetItem(str(type(value)))) + line += 1 diff --git a/plugins/extensions/pykrita/plugin/plugins/scripter/ui_scripter/stackwidgets/debuggerwidget/debuggerwidget.py b/plugins/extensions/pykrita/plugin/plugins/scripter/ui_scripter/stackwidgets/debuggerwidget/debuggerwidget.py index b5ee4e9cea..dbd8dba6de 100644 --- a/plugins/extensions/pykrita/plugin/plugins/scripter/ui_scripter/stackwidgets/debuggerwidget/debuggerwidget.py +++ b/plugins/extensions/pykrita/plugin/plugins/scripter/ui_scripter/stackwidgets/debuggerwidget/debuggerwidget.py @@ -1,32 +1,36 @@ from PyQt5.QtWidgets import QWidget, QVBoxLayout, QToolBar, QTableWidget, QAction -from . import stepaction, stopaction +from . import stepaction, stopaction, debuggertable class DebuggerWidget(QWidget): def __init__(self, scripter, parent=None): super(DebuggerWidget, self).__init__(parent) self.scripter = scripter self.setObjectName('Debugger') self.layout = QVBoxLayout() self.stopAction = stopaction.StopAction(self.scripter, self) self.toolbar = QToolBar() self.stepAction = stepaction.StepAction(self.scripter, self) self.toolbar.addAction(self.stopAction) self.toolbar.addAction(self.stepAction) self.disableToolbar(True) - self.table = QTableWidget(4, 4) + self.table = debuggertable.DebuggerTable() self.layout.addWidget(self.toolbar) self.layout.addWidget(self.table) self.setLayout(self.layout) def startDebugger(self): self.disableToolbar(False) def disableToolbar(self, status): for action in self.toolbar.actions(): action.setDisabled(status) + + def updateWidget(self): + data = self.scripter.debugcontroller.debuggerData + self.table.updateTable(data)