├── LICENSE.txt ├── QCodeEditor.py ├── README.rst └── demo.gif /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2017, Ivan Luchko and Project Contributors 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /QCodeEditor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | # -*- coding: utf-8 -*- 3 | ''' 4 | Licensed under the terms of the MIT License 5 | https://github.com/luchko/QCodeEditor 6 | @author: Ivan Luchko (luchko.ivan@gmail.com) 7 | 8 | This module contains the light QPlainTextEdit based QCodeEditor widget which 9 | provides the line numbers bar and the syntax and the current line highlighting. 10 | 11 | class XMLHighlighter(QSyntaxHighlighter): 12 | class QCodeEditor(QPlainTextEdit): 13 | 14 | testing and examples: 15 | 16 | def run_test(): 17 | 18 | Module is compatible with both pyQt4 and pyQt5 19 | 20 | ''' 21 | try: 22 | import PyQt4 as PyQt 23 | pyQtVersion = "PyQt4" 24 | 25 | except ImportError: 26 | try: 27 | import PyQt5 as PyQt 28 | pyQtVersion = "PyQt5" 29 | except ImportError: 30 | raise ImportError("neither PyQt4 or PyQt5 is found") 31 | 32 | # imports requied PyQt modules 33 | if pyQtVersion == "PyQt4": 34 | from PyQt4.QtCore import Qt, QRect, QRegExp 35 | from PyQt4.QtGui import (QWidget, QTextEdit, QPlainTextEdit, QColor, 36 | QPainter, QFont, QSyntaxHighlighter, 37 | QTextFormat, QTextCharFormat) 38 | else: 39 | from PyQt5.QtCore import Qt, QRect, QRegExp 40 | from PyQt5.QtWidgets import QWidget, QTextEdit, QPlainTextEdit 41 | from PyQt5.QtGui import (QColor, QPainter, QFont, QSyntaxHighlighter, 42 | QTextFormat, QTextCharFormat) 43 | # classes definition 44 | 45 | class XMLHighlighter(QSyntaxHighlighter): 46 | ''' 47 | Class for highlighting xml text inherited from QSyntaxHighlighter 48 | 49 | reference: 50 | http://www.yasinuludag.com/blog/?p=49 51 | 52 | ''' 53 | def __init__(self, parent=None): 54 | 55 | super(XMLHighlighter, self).__init__(parent) 56 | 57 | self.highlightingRules = [] 58 | 59 | xmlElementFormat = QTextCharFormat() 60 | xmlElementFormat.setForeground(QColor("#000070")) #blue 61 | self.highlightingRules.append((QRegExp("\\b[A-Za-z0-9_]+(?=[\s/>])"), xmlElementFormat)) 62 | 63 | xmlAttributeFormat = QTextCharFormat() 64 | xmlAttributeFormat.setFontItalic(True) 65 | xmlAttributeFormat.setForeground(QColor("#177317")) #green 66 | self.highlightingRules.append((QRegExp("\\b[A-Za-z0-9_]+(?=\\=)"), xmlAttributeFormat)) 67 | self.highlightingRules.append((QRegExp("="), xmlAttributeFormat)) 68 | 69 | self.valueFormat = QTextCharFormat() 70 | self.valueFormat.setForeground(QColor("#e35e00")) #orange 71 | self.valueStartExpression = QRegExp("\"") 72 | self.valueEndExpression = QRegExp("\"(?=[\s>"), singleLineCommentFormat)) 77 | 78 | textFormat = QTextCharFormat() 79 | textFormat.setForeground(QColor("#000000")) #black 80 | # (?<=...) - lookbehind is not supported 81 | self.highlightingRules.append((QRegExp(">(.+)(?=", ">", "<", "= 0: 100 | #Get the length of how long the expression is true, set the format from the start to the length with the text format 101 | length = expression.matchedLength() 102 | self.setFormat(index, length, format) 103 | #Set index to where the expression ends in the text 104 | index = expression.indexIn(text, index + length) 105 | 106 | #HANDLE QUOTATION MARKS NOW.. WE WANT TO START WITH " AND END WITH ".. A THIRD " SHOULD NOT CAUSE THE WORDS INBETWEEN SECOND AND THIRD TO BE COLORED 107 | self.setCurrentBlockState(0) 108 | startIndex = 0 109 | if self.previousBlockState() != 1: 110 | startIndex = self.valueStartExpression.indexIn(text) 111 | while startIndex >= 0: 112 | endIndex = self.valueEndExpression.indexIn(text, startIndex) 113 | if endIndex == -1: 114 | self.setCurrentBlockState(1) 115 | commentLength = len(text) - startIndex 116 | else: 117 | commentLength = endIndex - startIndex + self.valueEndExpression.matchedLength() 118 | self.setFormat(startIndex, commentLength, self.valueFormat) 119 | startIndex = self.valueStartExpression.indexIn(text, startIndex + commentLength); 120 | 121 | 122 | class QCodeEditor(QPlainTextEdit): 123 | ''' 124 | QCodeEditor inherited from QPlainTextEdit providing: 125 | 126 | numberBar - set by DISPLAY_LINE_NUMBERS flag equals True 127 | curent line highligthing - set by HIGHLIGHT_CURRENT_LINE flag equals True 128 | setting up QSyntaxHighlighter 129 | 130 | references: 131 | https://john.nachtimwald.com/2009/08/19/better-qplaintextedit-with-line-numbers/ 132 | http://doc.qt.io/qt-5/qtwidgets-widgets-codeeditor-example.html 133 | 134 | ''' 135 | class NumberBar(QWidget): 136 | '''class that deifnes textEditor numberBar''' 137 | 138 | def __init__(self, editor): 139 | QWidget.__init__(self, editor) 140 | 141 | self.editor = editor 142 | self.editor.blockCountChanged.connect(self.updateWidth) 143 | self.editor.updateRequest.connect(self.updateContents) 144 | self.font = QFont() 145 | self.numberBarColor = QColor("#e8e8e8") 146 | 147 | def paintEvent(self, event): 148 | 149 | painter = QPainter(self) 150 | painter.fillRect(event.rect(), self.numberBarColor) 151 | 152 | block = self.editor.firstVisibleBlock() 153 | 154 | # Iterate over all visible text blocks in the document. 155 | while block.isValid(): 156 | blockNumber = block.blockNumber() 157 | block_top = self.editor.blockBoundingGeometry(block).translated(self.editor.contentOffset()).top() 158 | 159 | # Check if the position of the block is out side of the visible area. 160 | if not block.isVisible() or block_top >= event.rect().bottom(): 161 | break 162 | 163 | # We want the line number for the selected line to be bold. 164 | if blockNumber == self.editor.textCursor().blockNumber(): 165 | self.font.setBold(True) 166 | painter.setPen(QColor("#000000")) 167 | else: 168 | self.font.setBold(False) 169 | painter.setPen(QColor("#717171")) 170 | painter.setFont(self.font) 171 | 172 | # Draw the line number right justified at the position of the line. 173 | paint_rect = QRect(0, block_top, self.width(), self.editor.fontMetrics().height()) 174 | painter.drawText(paint_rect, Qt.AlignRight, str(blockNumber+1)) 175 | 176 | block = block.next() 177 | 178 | painter.end() 179 | 180 | QWidget.paintEvent(self, event) 181 | 182 | def getWidth(self): 183 | count = self.editor.blockCount() 184 | width = self.fontMetrics().width(str(count)) + 10 185 | return width 186 | 187 | def updateWidth(self): 188 | width = self.getWidth() 189 | if self.width() != width: 190 | self.setFixedWidth(width) 191 | self.editor.setViewportMargins(width, 0, 0, 0); 192 | 193 | def updateContents(self, rect, scroll): 194 | if scroll: 195 | self.scroll(0, scroll) 196 | else: 197 | self.update(0, rect.y(), self.width(), rect.height()) 198 | 199 | if rect.contains(self.editor.viewport().rect()): 200 | fontSize = self.editor.currentCharFormat().font().pointSize() 201 | self.font.setPointSize(fontSize) 202 | self.font.setStyle(QFont.StyleNormal) 203 | self.updateWidth() 204 | 205 | 206 | def __init__(self, DISPLAY_LINE_NUMBERS=True, HIGHLIGHT_CURRENT_LINE=True, 207 | SyntaxHighlighter=None, *args): 208 | ''' 209 | Parameters 210 | ---------- 211 | DISPLAY_LINE_NUMBERS : bool 212 | switch on/off the presence of the lines number bar 213 | HIGHLIGHT_CURRENT_LINE : bool 214 | switch on/off the current line highliting 215 | SyntaxHighlighter : QSyntaxHighlighter 216 | should be inherited from QSyntaxHighlighter 217 | 218 | ''' 219 | super(QCodeEditor, self).__init__() 220 | 221 | self.setFont(QFont("Ubuntu Mono", 11)) 222 | self.setLineWrapMode(QPlainTextEdit.NoWrap) 223 | 224 | self.DISPLAY_LINE_NUMBERS = DISPLAY_LINE_NUMBERS 225 | 226 | if DISPLAY_LINE_NUMBERS: 227 | self.number_bar = self.NumberBar(self) 228 | 229 | if HIGHLIGHT_CURRENT_LINE: 230 | self.currentLineNumber = None 231 | self.currentLineColor = self.palette().alternateBase() 232 | # self.currentLineColor = QColor("#e8e8e8") 233 | self.cursorPositionChanged.connect(self.highligtCurrentLine) 234 | 235 | if SyntaxHighlighter is not None: # add highlighter to textdocument 236 | self.highlighter = SyntaxHighlighter(self.document()) 237 | 238 | def resizeEvent(self, *e): 239 | '''overload resizeEvent handler''' 240 | 241 | if self.DISPLAY_LINE_NUMBERS: # resize number_bar widget 242 | cr = self.contentsRect() 243 | rec = QRect(cr.left(), cr.top(), self.number_bar.getWidth(), cr.height()) 244 | self.number_bar.setGeometry(rec) 245 | 246 | QPlainTextEdit.resizeEvent(self, *e) 247 | 248 | def highligtCurrentLine(self): 249 | newCurrentLineNumber = self.textCursor().blockNumber() 250 | if newCurrentLineNumber != self.currentLineNumber: 251 | self.currentLineNumber = newCurrentLineNumber 252 | hi_selection = QTextEdit.ExtraSelection() 253 | hi_selection.format.setBackground(self.currentLineColor) 254 | hi_selection.format.setProperty(QTextFormat.FullWidthSelection, True) 255 | hi_selection.cursor = self.textCursor() 256 | hi_selection.cursor.clearSelection() 257 | self.setExtraSelections([hi_selection]) 258 | 259 | ############################################################################## 260 | 261 | if __name__ == '__main__': 262 | 263 | # TESTING 264 | 265 | def run_test(): 266 | 267 | print("\n {} is imported".format(pyQtVersion)) 268 | # imports requied PyQt modules 269 | if pyQtVersion == "PyQt4": 270 | from PyQt4.QtGui import QApplication 271 | else: 272 | from PyQt5.QtWidgets import QApplication 273 | 274 | import sys 275 | 276 | app = QApplication([]) 277 | 278 | editor = QCodeEditor(DISPLAY_LINE_NUMBERS=True, 279 | HIGHLIGHT_CURRENT_LINE=True, 280 | SyntaxHighlighter=XMLHighlighter) 281 | 282 | text = ''' 283 | 284 | 285 | 1.0 0.0 0.0 286 | 0.0 1.0 0.0 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | ''' 296 | editor.setPlainText(text) 297 | editor.resize(400,250) 298 | editor.show() 299 | 300 | sys.exit(app.exec_()) 301 | 302 | 303 | run_test() 304 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | QCodeEditor widget (PyQt) 2 | ************************* 3 | 4 | - Git-hub repo: https://github.com/luchko/QCodeEditor 5 | - Free software: MIT license 6 | 7 | Overview 8 | ======== 9 | 10 | ``QCodeEditor`` is a light code editor widget based on ``QPlainTextEdit`` and provides the following features: 11 | 12 | - **line numbers bar** - set by ``DISPLAY_LINE_NUMBERS`` flag equals ``True`` 13 | 14 | - **curent line highligthing** - set by ``HIGHLIGHT_CURRENT_LINE`` flag equals ``True`` 15 | 16 | - **syntax highlighting** - setting up a ``QSyntaxHighlighter``. As an example XML syntax highlighter was used. 17 | 18 | Widget is compatible with Python 2.7 or Python 3.3+ and PyQt4 4.6+ or PyQt5 5.2+. 19 | 20 | ------------------------- 21 | 22 | .. figure:: ./demo.gif 23 | :align: center 24 | :figwidth: 100 % 25 | 26 | ------------------------- 27 | 28 | API 29 | === 30 | 31 | .. code-block:: python 32 | 33 | class QCodeEditor(QPlainTextEdit): 34 | 35 | def __init__(self, DISPLAY_LINE_NUMBERS=True, HIGHLIGHT_CURRENT_LINE=True, 36 | SyntaxHighlighter=None, *args): 37 | ''' 38 | Parameters 39 | ---------- 40 | DISPLAY_LINE_NUMBERS : bool 41 | defines the presence of the lines number bar 42 | HIGHLIGHT_CURRENT_LINE : bool 43 | switch on/off the current line highliting 44 | SyntaxHighlighter : QSyntaxHighlighter 45 | should be inherited from QSyntaxHighlighter 46 | ''' 47 | 48 | References: 49 | =========== 50 | 51 | - XMLHighlighter: http://www.yasinuludag.com/blog/?p=49 52 | - https://john.nachtimwald.com/2009/08/19/better-qplaintextedit-with-line-numbers/ 53 | - http://doc.qt.io/qt-4.8/qt-widgets-codeeditor-example.html 54 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luchko/QCodeEditor/0505f69535b61bf01d6fb67fda5a8004a474d475/demo.gif --------------------------------------------------------------------------------