Krita Source Code Documentation
Loading...
Searching...
No Matches
pythoneditor.py
Go to the documentation of this file.
1"""
2SPDX-FileCopyrightText: 2017 Eliakin Costa <eliakim170@gmail.com>
3
4SPDX-License-Identifier: GPL-2.0-or-later
5"""
6try:
7 from PyQt6.QtCore import Qt, QRect, QSize, QPoint, pyqtSlot
8 from PyQt6.QtWidgets import QPlainTextEdit, QTextEdit, QLabel
9 from PyQt6.QtGui import QIcon, QColor, QPainter, QTextFormat, QFont, QTextCursor
10except:
11 from PyQt5.QtCore import Qt, QRect, QSize, QPoint, pyqtSlot
12 from PyQt5.QtWidgets import QPlainTextEdit, QTextEdit, QLabel
13 from PyQt5.QtGui import QIcon, QColor, QPainter, QTextFormat, QFont, QTextCursor
14from scripter.ui_scripter.editor import linenumberarea, debugarea
15
16
19
20INDENT_WIDTH = 4 # size in spaces of indent in editor window.
21# ideally make this a setting sometime?
22
23MODIFIER_COMMENT = Qt.KeyboardModifier.ControlModifier
24KEY_COMMENT = Qt.Key.Key_M
25CHAR_COMMENT = "#"
26
27CHAR_SPACE = " "
28CHAR_COLON = ":"
29CHAR_COMMA = ","
30CHAR_CONTINUATION = "\\"
31CHAR_OPEN_BRACKET = "("
32CHAR_OPEN_SQUARE_BRACKET = "["
33CHAR_OPEN_BRACE = "{"
34CHAR_EQUALS = "="
35
36
37class CodeEditor(QPlainTextEdit):
38
39 DEBUG_AREA_WIDTH = 20
40
41 def __init__(self, scripter, parent=None):
42 super(CodeEditor, self).__init__(parent)
43
44 self.setLineWrapMode(QPlainTextEdit.LineWrapMode.NoWrap)
45
46 self.scripter = scripter
49
50 self.blockCountChanged.connect(self.updateMarginsWidthupdateMarginsWidth)
52 self.cursorPositionChanged.connect(self.highlightCurrentLinehighlightCurrentLine)
53
56 self.fontfontfont = "Monospace"
57 self._stepped = False
58 self.debugArrow = QIcon(':/icons/debug_arrow.svg')
59 self.setCornerWidget(QLabel(str()))
60 self._documentChanged = False
61 self.indent_width = INDENT_WIDTH # maybe one day connect this to a setting
62
64
65 def debugAreaWidth(self):
66 return self.DEBUG_AREA_WIDTH
67
69 """The lineNumberAreaWidth is the quatity of decimal places in blockCount"""
70 digits = 1
71 max_ = max(1, self.blockCount())
72 while (max_ >= 10):
73 max_ /= 10
74 digits += 1
75
76 space = 3 + self.fontMetrics().horizontalAdvance('9') * digits + 3
77
78 return space
79
80 def resizeEvent(self, event):
81 super(CodeEditor, self).resizeEvent(event)
82
83 qRect = self.contentsRect()
84 self.debugArea.setGeometry(QRect(qRect.left(),
85 qRect.top(),
86 self.debugAreaWidth(),
87 qRect.height()))
88 scrollBarHeight = 0
89 if (self.horizontalScrollBar().isVisible()):
90 scrollBarHeight = self.horizontalScrollBar().height()
91
92 self.lineNumberArea.setGeometry(QRect(qRect.left() + self.debugAreaWidth(),
93 qRect.top(),
95 qRect.height() - scrollBarHeight))
96
98 self.setViewportMargins(self.lineNumberAreaWidth() + self.debugAreaWidth(), 0, 0, 0)
99
100 def updateLineNumberArea(self, rect, dy):
101 """ This slot is invoked when the editors viewport has been scrolled """
102
103 if dy:
104 self.lineNumberArea.scroll(0, dy)
105 self.debugArea.scroll(0, dy)
106 else:
107 self.lineNumberArea.update(0, rect.y(), self.lineNumberArea.width(), rect.height())
108
109 if rect.contains(self.viewport().rect()):
111
112 def lineNumberAreaPaintEvent(self, event):
113 """This method draws the current lineNumberArea for while"""
114 blockColor = QColor(self.palette().base().color()).darker(120)
115 if (self.palette().base().color().lightness() < 128):
116 blockColor = QColor(self.palette().base().color()).lighter(120)
117 if (self.palette().base().color().lightness() < 1):
118 blockColor = QColor(43, 43, 43)
119 painter = QPainter(self.lineNumberArea)
120 painter.fillRect(event.rect(), blockColor)
121
122 block = self.firstVisibleBlock()
123 blockNumber = block.blockNumber()
124 top = int(self.blockBoundingGeometry(block).translated(self.contentOffset()).top())
125 bottom = top + int(self.blockBoundingRect(block).height())
126 while block.isValid() and top <= event.rect().bottom():
127 if block.isVisible() and bottom >= event.rect().top():
128 number = str(blockNumber + 1)
129 painter.setPen(self.palette().text().color())
130 painter.drawText(0, top, self.lineNumberArea.width() - 3, self.fontMetrics().height(),
131 Qt.AlignmentFlag.AlignRight, number)
132
133 block = block.next()
134 top = bottom
135 bottom = top + int(self.blockBoundingRect(block).height())
136 blockNumber += 1
137
138 def debugAreaPaintEvent(self, event):
139 if self.scripter.debugcontroller.isActive and self.scripter.debugcontroller.currentLine:
140 lineNumber = self.scripter.debugcontroller.currentLine
141 block = self.document().findBlockByLineNumber(lineNumber - 1)
142
143 if self._stepped:
144 cursor = QTextCursor(block)
145 self.setTextCursor(cursor)
146 self._stepped = False
147
148 top = int(self.blockBoundingGeometry(block).translated(self.contentOffset()).top())
149
150 painter = QPainter(self.debugArea)
151 pixmap = self.debugArrow.pixmap(QSize(self.debugAreaWidth() - 3, int(self.blockBoundingRect(block).height())))
152 painter.drawPixmap(QPoint(0, top), pixmap)
153
155 """Highlight current line under cursor"""
156 currentSelection = QTextEdit.ExtraSelection()
157
158 lineColor = QColor(self.palette().base().color()).darker(120)
159 if (self.palette().base().color().lightness() < 128):
160 lineColor = QColor(self.palette().base().color()).lighter(120)
161 if (self.palette().base().color().lightness() < 1):
162 lineColor = QColor(43, 43, 43)
163
164 currentSelection.format.setBackground(lineColor)
165 currentSelection.format.setProperty(QTextFormat.Property.FullWidthSelection, True)
166 currentSelection.cursor = self.textCursor()
167 currentSelection.cursor.clearSelection()
168
169 self.setExtraSelections([currentSelection])
170
171 def wheelEvent(self, e):
172 """When the CTRL is pressed during the wheelEvent, zoomIn and zoomOut
173 slots are invoked"""
174 if e.modifiers() == Qt.KeyboardModifier.ControlModifier:
175 delta = e.angleDelta().y()
176 if delta < 0:
177 self.zoomOut()
178 elif delta > 0:
179 self.zoomIn()
180 else:
181 super(CodeEditor, self).wheelEvent(e)
182
183 def keyPressEvent(self, e):
184 modifiers = e.modifiers()
185 if (e.key() == Qt.Key.Key_Tab):
186 self.indent()
187 elif e.key() == Qt.Key.Key_Backtab:
188 self.dedent()
189 elif modifiers == MODIFIER_COMMENT and e.key() == KEY_COMMENT:
190 self.toggleComment()
191 elif e.key() == Qt.Key.Key_Return:
192 super(CodeEditor, self).keyPressEvent(e)
193 self.autoindent()
194 else:
195 super(CodeEditor, self).keyPressEvent(e)
196
197 def isEmptyBlock(self, blockNumber):
198 """ test whether block with number blockNumber contains any non-whitespace
199 If only whitespace: return true, else return false"""
200
201 # get block text
202 cursor = self.textCursor()
203 cursor.movePosition(QTextCursor.MoveOperation.Start)
204 cursor.movePosition(QTextCursor.MoveOperation.NextBlock, n=blockNumber)
205 cursor.movePosition(QTextCursor.MoveOperation.StartOfLine)
206 cursor.movePosition(QTextCursor.MoveOperation.EndOfLine, QTextCursor.MoveMode.KeepAnchor)
207 text = cursor.selectedText()
208 if text.strip() == "":
209 return True
210 else:
211 return False
212
213 def indent(self):
214 # tab key has been pressed. Indent current line or selected block by self.indent_width
215
216 cursor = self.textCursor()
217 # is there a selection?
218
219 selectionStart = cursor.selectionStart()
220 selectionEnd = cursor.selectionEnd()
221
222 if selectionStart == selectionEnd and cursor.atBlockEnd():
223 # ie no selection and don't insert in the middle of text
224 # something smarter might skip whitespace and add a tab in front of
225 # the next non whitespace character
226 cursor.insertText(" " * self.indent_width)
227 return
228
229 cursor.setPosition(selectionStart)
230 startBlock = cursor.blockNumber()
231 cursor.setPosition(selectionEnd)
232 endBlock = cursor.blockNumber()
233
234 cursor.movePosition(QTextCursor.MoveOperation.Start)
235 cursor.movePosition(QTextCursor.MoveOperation.NextBlock, n=startBlock)
236
237 for i in range(0, endBlock - startBlock + 1):
238 if not self.isEmptyBlock(startBlock + i): # Don't insert whitespace on empty lines
239 cursor.movePosition(QTextCursor.MoveOperation.StartOfLine)
240 cursor.insertText(" " * self.indent_width)
241
242 cursor.movePosition(QTextCursor.MoveOperation.NextBlock)
243
244 # QT maintains separate cursors, so don't need to track or reset user's cursor
245
246 def dedentBlock(self, blockNumber):
247 # dedent the line at blockNumber
248 cursor = self.textCursor()
249 cursor.movePosition(QTextCursor.MoveOperation.Start)
250 cursor.movePosition(QTextCursor.MoveOperation.NextBlock, n=blockNumber)
251
252 for _ in range(self.indent_width):
253 cursor.movePosition(QTextCursor.MoveOperation.StartOfLine)
254 cursor.movePosition(QTextCursor.MoveOperation.Right, QTextCursor.ModeMode.KeepAnchor)
255 if cursor.selectedText() == " ": # need to test each char
256 cursor.removeSelectedText()
257 else:
258 break # stop deleting!
259
260 return
261
262 def dedent(self):
263 cursor = self.textCursor()
264 selectionStart = cursor.selectionStart()
265 selectionEnd = cursor.selectionEnd()
266
267 cursor.setPosition(selectionStart)
268 startBlock = cursor.blockNumber()
269 cursor.setPosition(selectionEnd)
270 endBlock = cursor.blockNumber()
271
272 if endBlock < startBlock:
273 startBlock, endBlock = endBlock, startBlock
274
275 for blockNumber in range(startBlock, endBlock + 1):
276 self.dedentBlock(blockNumber)
277
278 def autoindent(self):
279 r"""The return key has just been pressed (and processed by the editor)
280 now insert leading spaces to reflect an appropriate indent level
281 against the previous line.
282 This will depend on the end of the previous line. If it ends:
283 * with a colon (:) then indent to a new indent level
284 * with a comma (,) then this is an implied continuation line, probably
285 in the middle of a function's parameter list
286 - look for last open bracket on previous line (, [ or {
287 - if found indent to that level + one character,
288 - otherwise use previous line whitespace, this is probably a list or
289 parameter list so line up with other elements
290 * with a backslash (\‍) then this is a continuation line, probably
291 on the RHS of an assignment
292 - similar rules as for comma, but if there is an = character
293 use that plus one indent level if that is greater
294 * if it is an open bracket of some sort treat similarly to comma
295
296
297 * anything else - a new line at the same indent level. This will preserve
298 the indent level of whitespace lines. User can shift-tab to dedent
299 as necessary
300 """
301
302 cursor = self.textCursor()
303 block = cursor.block()
304 block = block.previous()
305 text = block.text()
306 indentLevel = len(text) - len(text.lstrip()) # base indent level
307
308 # get last char
309 try:
310 lastChar = text.rstrip()[-1]
311 except IndexError:
312 lastChar = None
313
314 # work out indent level
315 if lastChar == CHAR_COLON:
316 indentLevel = indentLevel + self.indent_width
317 elif lastChar == CHAR_COMMA: # technically these are mutually exclusive so if would work
318 braceLevels = []
319 for c in [CHAR_OPEN_BRACE, CHAR_OPEN_BRACKET, CHAR_OPEN_SQUARE_BRACKET]:
320 braceLevels.append(text.rfind(c))
321 bracePosition = max(braceLevels)
322 if bracePosition > 0:
323 indentLevel = bracePosition + 1
324 elif lastChar == CHAR_CONTINUATION:
325 braceLevels = []
326 for c in [CHAR_OPEN_BRACE, CHAR_OPEN_BRACKET, CHAR_OPEN_SQUARE_BRACKET]:
327 braceLevels.append(text.rfind(c))
328 bracePosition = max(braceLevels)
329 equalPosition = text.rfind(CHAR_EQUALS)
330 if bracePosition > equalPosition:
331 indentLevel = bracePosition + 1
332 if equalPosition > bracePosition:
333 indentLevel = equalPosition + self.indent_width
334 # otherwise they're the same - ie both -1 so use base indent level
335 elif lastChar in [CHAR_OPEN_BRACE, CHAR_OPEN_BRACKET, CHAR_OPEN_SQUARE_BRACKET]:
336 indentLevel = len(text.rstrip())
337
338 # indent
339 cursor.insertText(CHAR_SPACE * indentLevel)
340
341 def toggleComment(self):
342 """Toggle lines of selected text to/from either comment or uncomment
343 selected text is obtained from text cursor
344 If selected text contains both commented and uncommented text this will
345 flip the state of each line - which may not be desirable.
346 """
347
348 cursor = self.textCursor()
349 selectionStart = cursor.selectionStart()
350 selectionEnd = cursor.selectionEnd()
351
352 cursor.setPosition(selectionStart)
353 startBlock = cursor.blockNumber()
354 cursor.setPosition(selectionEnd)
355 endBlock = cursor.blockNumber()
356
357 cursor.movePosition(QTextCursor.MoveOperation.Start)
358 cursor.movePosition(QTextCursor.MoveOperation.NextBlock, n=startBlock)
359
360 for _ in range(0, endBlock - startBlock + 1):
361 # Test for empty line (if the line is empty moving the cursor right will overflow
362 # to next line, throwing the line tracking off)
363 cursor.movePosition(QTextCursor.MoveOperation.StartOfLine)
364 p1 = cursor.position()
365 cursor.movePosition(QTextCursor.MoveOperation.EndOfLine)
366 p2 = cursor.position()
367 if p1 == p2: # empty line - comment it
368 cursor.movePosition(QTextCursor.MoveOperation.StartOfLine)
369 cursor.insertText(CHAR_COMMENT)
370 cursor.movePosition(QTextCursor.MoveOperation.NextBlock)
371 continue
372
373 cursor.movePosition(QTextCursor.MoveOperation.StartOfLine)
374 cursor.movePosition(QTextCursor.MoveOperation.Right, QTextCursor.MoveMode.KeepAnchor)
375 text = cursor.selectedText()
376
377 if text == CHAR_COMMENT:
378 cursor.removeSelectedText()
379 else:
380 cursor.movePosition(QTextCursor.MoveOperation.StartOfLine)
381 cursor.insertText(CHAR_COMMENT)
382
383 cursor.movePosition(QTextCursor.MoveOperation.NextBlock)
384
385 @property
386 def font(self):
387 return self._font
388
389 @font.setter
390 def font(self, font="Monospace"):
391 self._font = font
392 self.setFont(QFont(font, self.fontInfo().pointSize()))
393
394 def setFontSize(self, size=10):
395 self.setFont(QFont(self._font, size))
396
397 def setStepped(self, status):
398 self._stepped = status
399
400 def repaintDebugArea(self):
401 self.debugArea.repaint()
402
403 @pyqtSlot(bool)
404 def setDocumentModified(self, changed=False):
405 self._documentModified = changed
connect(this, SIGNAL(optionsChanged()), this, SLOT(saveOptions()))