Skip to content

Commit

Permalink
Overhaul code widgets
Browse files Browse the repository at this point in the history
A custom widget has been created to tailor the behavior of
QPlainTextEdit to our needs. The new widget, which inherits from it,
adds on top:

- Current line and error highlighting
- Line numbers with customizable offset

The previous notification widget has been further integrated onto the
program, and now the snippet honors the selected font size and source
line numbering.
  • Loading branch information
jmi2k committed Jul 26, 2020
1 parent 7fec008 commit 0492971
Show file tree
Hide file tree
Showing 6 changed files with 284 additions and 52 deletions.
9 changes: 7 additions & 2 deletions learnbot_dsl/guis/Learnblock.py
Expand Up @@ -15,6 +15,8 @@
QPixmap, QRadialGradient)
from PySide2.QtWidgets import *

from learnbot_dsl.learnbotCode.CodeEdit import CodeEdit


class Ui_MainWindow(object):
def setupUi(self, MainWindow):
Expand Down Expand Up @@ -213,7 +215,7 @@ def setupUi(self, MainWindow):

self.verticalLayout_18.addWidget(self.label_4)

self.pythonCode = QTextEdit(self.tab)
self.pythonCode = CodeEdit(self.tab)
self.pythonCode.setObjectName(u"pythonCode")

self.verticalLayout_18.addWidget(self.pythonCode)
Expand Down Expand Up @@ -284,11 +286,13 @@ def setupUi(self, MainWindow):
self.textCodeSplitter = QSplitter(self.program_text)
self.textCodeSplitter.setObjectName(u"textCodeSplitter")
self.textCodeSplitter.setOrientation(Qt.Vertical)
self.textCode = QTextEdit(self.textCodeSplitter)
self.textCode = CodeEdit(self.textCodeSplitter)
self.textCode.setObjectName(u"textCode")
self.textCodeSplitter.addWidget(self.textCode)
self.notificationList = QListWidget(self.textCodeSplitter)
self.notificationList.setObjectName(u"notificationList")
self.notificationList.setAutoScroll(False)
self.notificationList.setAlternatingRowColors(True)
self.notificationList.setVerticalScrollMode(QAbstractItemView.ScrollPerPixel)
self.textCodeSplitter.addWidget(self.notificationList)

Expand Down Expand Up @@ -588,6 +592,7 @@ def setupUi(self, MainWindow):
self.statusbar.setObjectName(u"statusbar")
self.position = QLabel(self.statusbar)
self.position.setObjectName(u"position")
self.position.setGeometry(QRect(0, 0, 100, 30))
MainWindow.setStatusBar(self.statusbar)

self.menubar.addAction(self.menuFile.menuAction())
Expand Down
19 changes: 17 additions & 2 deletions learnbot_dsl/guis/Learnblock.ui
Expand Up @@ -209,7 +209,7 @@
</widget>
</item>
<item>
<widget class="QTextEdit" name="pythonCode"/>
<widget class="CodeEdit" name="pythonCode"/>
</item>
</layout>
</item>
Expand Down Expand Up @@ -306,7 +306,7 @@
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<widget class="QTextEdit" name="textCode"/>
<widget class="CodeEdit" name="textCode"/>
<widget class="QListWidget" name="notificationList">
<property name="autoScroll">
<bool>false</bool>
Expand Down Expand Up @@ -834,6 +834,14 @@
</widget>
<widget class="QStatusBar" name="statusbar">
<widget class="QLabel" name="position">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>100</width>
<height>30</height>
</rect>
</property>
</widget>
</widget>
<action name="actionExit">
Expand Down Expand Up @@ -1127,6 +1135,13 @@
</property>
</action>
</widget>
<customwidgets>
<customwidget>
<class>CodeEdit</class>
<extends>QPlainTextEdit</extends>
<header>learnbot_dsl.learnbotCode.CodeEdit</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>
158 changes: 158 additions & 0 deletions learnbot_dsl/learnbotCode/CodeEdit.py
@@ -0,0 +1,158 @@
from PySide2 import QtWidgets, QtGui, QtCore
from math import log10, ceil

class CodeEdit(QtWidgets.QPlainTextEdit):
class LineNumberArea(QtWidgets.QWidget):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._offset = 1
self._count = 0
self._fontScale = 0.8

def sizeHint(self):
lastLine = self._offset + self._count
digits = ceil(log10(lastLine))
metrics = QtGui.QFontMetrics(self.font())
margins = self.contentsMargins()
width = margins.left() + margins.right() + metrics.horizontalAdvance('9' * digits)

return QtCore.QSize(width, -1)

def paintEvent(self, event):
parent = self.parentWidget()
painter = QtGui.QPainter(self)
background = self.palette().color(QtGui.QPalette.Background)
foreground = self.palette().color(QtGui.QPalette.Foreground)
painter.fillRect(event.rect(), background)

block = parent.firstVisibleBlock()
number = block.blockNumber() + self.offset()
top = parent.blockBoundingGeometry(block).translated(parent.contentOffset()).top()
bottom = top + parent.blockBoundingRect(block).height()

font = parent.font()
font.setPointSizeF(font.pointSizeF() * self._fontScale)
painter.setFont(font)

while block.isValid() and top <= event.rect().bottom():
if block.isVisible() and bottom >= event.rect().top():
width = self.sizeHint().width()
height = parent.fontMetrics().height()

painter.setPen(foreground)
painter.drawText(0, top + height * ((1 - self._fontScale) / 2),
width - self.contentsMargins().right(), height,
QtGui.Qt.AlignRight, str(number))

block = block.next()
top = bottom
bottom = top + parent.blockBoundingRect(block).height()
number += 1

painter.end()

def count(self):
return self._count

def setCount(self, count):
self._count = count

def offset(self):
return self._offset

def setOffset(self, offset):
self._offset = offset

def fontScale(self):
return self._fontScale

def setFontScale(self, fontScale):
self._fontScale = fontScale

def __init__(self, *args, **kwargs):
super().__init__(*args, *kwargs)

self.lineNumbers = CodeEdit.LineNumberArea(self)
self.lineNumbers.setContentsMargins(QtCore.QMargins(4, 0, 4, 0))
p = self.palette()
self.lineNumbers.setPalette(p)
self.textChanged.connect(self.updateLineCount)

self.blockCountChanged.connect(self.updateLineCount)
self.updateRequest.connect(self.updateLineNumbers)
self.cursorPositionChanged.connect(self.highlightCurrentLine)

self.errorSels = []

self.lineSel = QtWidgets.QTextEdit.ExtraSelection()
self.lineSel.format.setBackground(QtGui.QColor(255, 255, 0, 16))
self.lineSel.format.setProperty(QtGui.QTextFormat.FullWidthSelection, True)
self.lineSel.cursor = self.textCursor()
self.lineSel.cursor.clearSelection()
self.updateSelections()

def offset(self):
return self.lineNumbers.offset()

def setOffset(self, offset):
self.lineNumbers.setOffset(offset)

def updateLineCount(self):
self.lineNumbers.setCount(self.document().blockCount())
self.setViewportMargins(self.lineNumbers.sizeHint().width(), 0, 0, 0)

def updateLineNumbers(self, rect, dy):
if dy:
self.lineNumbers.scroll(0, dy)
else:
size = self.lineNumbers.sizeHint()
self.lineNumbers.update(0, rect.y(), size.width(), rect.height())

if rect.contains(self.viewport().rect()):
self.updateLineCount()

def highlightCurrentLine(self):
if not self.isReadOnly():
self.lineSel.cursor = self.textCursor()
self.lineSel.cursor.clearSelection()

self.updateSelections()

def clearErrorHighlight(self):
self.errorSels = []
self.updateSelections()

def addErrorHighlight(self, start, end):
errorSel = QtWidgets.QTextEdit.ExtraSelection()
errorSel.format.setBackground(QtGui.QColor(255, 0, 0, 16))
errorSel.format.setProperty(QtGui.QTextFormat.TextUnderlineColor, QtGui.QColor(255, 0, 0))
errorSel.format.setProperty(QtGui.QTextFormat.TextUnderlineStyle, QtGui.QTextCharFormat.UnderlineStyle.SpellCheckUnderline)
errorSel.cursor = QtGui.QTextCursor(self.document())
errorSel.cursor.setPosition(start)
errorSel.cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
self.errorSels.append(errorSel)

self.updateSelections()

def updateSelections(self):
selections = self.errorSels.copy()

if not self.isReadOnly():
selections.append(self.lineSel)

self.setExtraSelections(selections)

def setFont(self, font):
super().setFont(font)
self.lineNumbers.setFont(font)

def setReadOnly(self, ro):
super().setReadOnly(ro)
self.updateSelections()

def resizeEvent(self, event):
super().resizeEvent(event)

cr = self.contentsRect()
width = self.lineNumbers.sizeHint().width()
self.lineNumbers.setGeometry(QtCore.QRect(cr.left(), cr.top(), width, cr.height()))
38 changes: 28 additions & 10 deletions learnbot_dsl/learnbotCode/LearnBlock.py
Expand Up @@ -584,9 +584,9 @@ def updatePythonCodeStyle(self):
font.setFixedPitch(True)
font.setPointSize(self.ui.spinBoxPythonSize.value())
self.ui.pythonCode.setFont(font)
self.ui.pythonCode.setTextColor(QtCore.Qt.white)
self.ui.pythonCode.setCursorWidth(2)
p = self.ui.pythonCode.palette()
p.setColor(self.ui.pythonCode.viewport().foregroundRole(), QtCore.Qt.white)
p.setColor(self.ui.pythonCode.viewport().backgroundRole(), QtGui.QColor(51, 51, 51, 255))
self.ui.pythonCode.setPalette(p)

Expand All @@ -598,9 +598,19 @@ def updateTextCodeStyle(self):
self.ui.textCode.setFont(font)
self.ui.textCode.setCursorWidth(2)
p = self.ui.textCode.palette()
p.setColor(self.ui.textCode.viewport().foregroundRole(), QtCore.Qt.white)
p.setColor(self.ui.textCode.viewport().backgroundRole(), QtGui.QColor(51, 51, 51, 255))
self.ui.textCode.setPalette(p)

for notification in self.notifications:
notification.snippet.setFont(font)
notification.snippet.setCursorWidth(2)
p = notification.snippet.palette()
p.setColor(notification.snippet.viewport().foregroundRole(), QtCore.Qt.white)
p.setColor(notification.snippet.viewport().backgroundRole(), QtGui.QColor(51, 51, 51, 255))
notification.snippet.setPalette(p)
notification.fitSnippetToContent()

def loadConfigFile(self):
self.confFile = os.path.join(tempfile.gettempdir(), ".learnblock.conf")
if not os.path.exists(self.confFile):
Expand Down Expand Up @@ -870,7 +880,7 @@ def blocksToTextCode(self):
blocks = self.scene.getListInstructions()
code = self.parserBlocks(blocks, self.toLBotPy)
self.ui.textCode.clear()
self.ui.textCode.setText(text + code)
self.ui.textCode.setPlainText(text + code)

def checkConnectionToBot(self, showWarning=False):
r = os.system("ping -c 1 -W 1 " + configSSH["ip"])
Expand Down Expand Up @@ -972,7 +982,7 @@ def textCodeToPython(self, name_Client):

self.updateNotifications()
else:
self.ui.pythonCode.setText(code)
self.ui.pythonCode.setPlainText(code)
return code
except ParseException as e:
traceback.print_exc()
Expand All @@ -994,18 +1004,26 @@ def textCodeToPython(self, name_Client):
return False

def updateNotifications(self):
nErr = 0
nInfo = 0

self.ui.textCode.clearErrorHighlight()
self.ui.notificationList.clear()

for notification in self.notifications:
item = QtWidgets.QListWidgetItem(self.ui.notificationList)
item.setSizeHint(notification.sizeHint())
notification.resized.connect(lambda: item.setSizeHint(notification.sizeHint()))

start = notification.start[2]

if notification.end:
end = notification.end[2]
else:
end = len(notification.src)

self.ui.textCode.addErrorHighlight(start, end)
self.ui.notificationList.addItem(item)
self.ui.notificationList.setItemWidget(item, notification)

self.updateTextCodeStyle()

def startSimulatorRobot(self):
self.scene.stopAllblocks()
path = os.environ.get('HOME')
Expand Down Expand Up @@ -1406,7 +1424,7 @@ def loadBlockTextCode(self):
f = open(fileName, "r")
code = f.read()
self.ui.textCode.clear()
self.ui.textCode.setText(code)
self.ui.textCode.setPlainText(code)
f.close()

def saveBlockTextCode(self):
Expand All @@ -1416,7 +1434,7 @@ def saveBlockTextCode(self):
if extension == "":
fileName += ".bt"
f = open(fileName, "w")
code = self.ui.textCode.toPlainText()
code = self.ui.textCode.document().toPlainText()
f.write(code)
f.close()

Expand All @@ -1426,7 +1444,7 @@ def loadPythonCode(self):
f = open(fileName, "r")
code = f.read()
self.ui.pythonCode.clear()
self.ui.pythonCode.setText(code)
self.ui.pythonCode.setPlainText(code)
f.close()


Expand Down

0 comments on commit 0492971

Please sign in to comment.