Mercurial > lcfOS
annotate python/codeedit.py @ 280:02385f62f250
Rework from str interface to Instruction interface
author | Windel Bouwman |
---|---|
date | Sat, 02 Nov 2013 10:03:26 +0100 |
parents | 2ccd57b1d78c |
children |
rev | line source |
---|---|
160 | 1 #!/usr/bin/python |
2 | |
3 import sys | |
279 | 4 import os |
160 | 5 from PyQt4.QtCore import * |
6 from PyQt4.QtGui import * | |
7 import inspect | |
8 | |
9 GAP = 5 | |
10 | |
11 def clipVal(v, mn, mx): | |
12 if v < mn: return mn | |
13 if v > mx: return mx | |
14 return v | |
15 | |
16 class InnerCode(QWidget): | |
249 | 17 textChanged = pyqtSignal() |
18 def __init__(self, scrollArea): | |
160 | 19 super().__init__(scrollArea) |
20 self.scrollArea = scrollArea | |
169
ee0d30533dae
Added more tests and improved the diagnostic update
Windel Bouwman
parents:
167
diff
changeset
|
21 self.setFont(QFont('Courier', 12)) |
160 | 22 self.setFocusPolicy(Qt.StrongFocus) |
163 | 23 # TODO: only beam cursor in text area.. |
24 self.setCursor(Qt.IBeamCursor) | |
162 | 25 h = QFontMetrics(self.font()).height() |
247 | 26 self.errorPixmap = QPixmap('icons/error.png').scaled(h, h) |
249 | 27 self.arrowPixmap = QPixmap('icons/arrow.png').scaled(h, h) |
160 | 28 self.blinkcursor = False |
162 | 29 self.errorlist = [] |
249 | 30 self.arrow = None |
160 | 31 # Initial values: |
32 self.setSource('') | |
33 self.CursorPosition = 0 | |
250 | 34 self.t = QTimer(self) |
35 self.t.timeout.connect(self.updateCursor) | |
36 self.t.setInterval(500) | |
37 self.t.start() | |
38 | |
249 | 39 def updateCursor(self): |
256 | 40 self.blinkcursor = not self.blinkcursor |
41 self.update() | |
42 #self.update(self.cursorX, self.cursorY, self.charWidth, self.charHeight) | |
249 | 43 |
44 def setSource(self, src): | |
256 | 45 self.src = src |
46 self.adjust() | |
249 | 47 |
48 def getSource(self): | |
256 | 49 return self.src |
249 | 50 |
51 def setErrors(self, el): | |
256 | 52 self.errorlist = el |
53 self.update() | |
249 | 54 |
55 def setCursorPosition(self, c): | |
256 | 56 self.cursorPosition = clipVal(c, 0, len(self.src)) |
57 self.update() | |
249 | 58 |
59 CursorPosition = property(lambda self: self.cursorPosition, setCursorPosition) | |
60 | |
61 @property | |
62 def Rows(self): | |
63 # Make this nicer: | |
64 return self.src.split('\n') | |
65 | |
66 @property | |
67 def CursorRow(self): | |
68 # TODO: make this nice. | |
69 txt = self.src[0:self.cursorPosition] | |
70 return len(txt.split('\n')) | |
71 | |
72 @property | |
73 def CursorCol(self): | |
74 txt = self.src[0:self.cursorPosition] | |
75 curLine = txt.split('\n')[-1] | |
76 return len(curLine) + 1 | |
77 | |
78 @property | |
79 def CurrentLine(self): | |
80 return self.getRow(self.CursorRow) | |
81 | |
82 def setRowCol(self, r, c): | |
163 | 83 prevRows = self.Rows[:r-1] |
161 | 84 txt = '\n'.join(prevRows) |
163 | 85 c = clipVal(c, 1, len(self.getRow(r))) |
162 | 86 self.CursorPosition = len(txt) + c + 1 |
167 | 87 self.showRow(self.CursorRow) |
249 | 88 |
89 def getRow(self, r): | |
161 | 90 rows = self.Rows |
160 | 91 r = r - 1 |
92 if r < 0 or r > len(rows) - 1: | |
93 return '' | |
94 else: | |
95 return rows[r] | |
249 | 96 |
97 def showRow(self, r): | |
163 | 98 self.scrollArea.ensureVisible(self.xposTXT, r * self.charHeight, 4, self.charHeight) |
256 | 99 |
249 | 100 # Annotations: |
101 def addAnnotation(self, row, col, ln, msg): | |
162 | 102 pass |
256 | 103 |
249 | 104 # Text modification: |
105 def getChar(self, pos): | |
160 | 106 pass |
256 | 107 |
249 | 108 def insertText(self, txt): |
256 | 109 self.setSource(self.src[0:self.CursorPosition] + txt + self.src[self.CursorPosition:]) |
110 self.CursorPosition += len(txt) | |
111 self.textChanged.emit() | |
112 | |
249 | 113 def deleteChar(self): |
256 | 114 self.setSource(self.src[0:self.CursorPosition] + self.src[self.CursorPosition+1:]) |
115 self.textChanged.emit() | |
116 | |
249 | 117 def GotoNextChar(self): |
256 | 118 if self.src[self.CursorPosition] != '\n': |
119 self.CursorPosition += 1 | |
120 | |
249 | 121 def GotoPrevChar(self): |
256 | 122 if self.src[self.CursorPosition - 1] != '\n': |
123 self.CursorPosition -= 1 | |
124 | |
249 | 125 def GotoNextLine(self): |
256 | 126 curLine = self.CurrentLine |
127 c = self.CursorCol - 1 # go to zero based | |
128 self.CursorPosition += len(curLine) - c + 1 # line break char! | |
129 curLine = self.CurrentLine | |
130 if len(curLine) < c: | |
131 self.CursorPosition += len(curLine) | |
132 else: | |
133 self.CursorPosition += c | |
134 self.showRow(self.CursorRow) | |
135 | |
249 | 136 def GotoPrevLine(self): |
256 | 137 c = self.CursorCol - 1 # go to zero based |
138 self.CursorPosition -= c + 1 # line break char! | |
139 curLine = self.CurrentLine | |
140 if len(curLine) > c: | |
141 self.CursorPosition -= len(curLine) - c | |
142 self.showRow(self.CursorRow) | |
143 | |
249 | 144 def paintEvent(self, event): |
160 | 145 # Helper variables: |
146 er = event.rect() | |
147 chw, chh = self.charWidth, self.charHeight | |
148 painter = QPainter(self) | |
149 # Background: | |
150 painter.fillRect(er, self.palette().color(QPalette.Base)) | |
162 | 151 painter.fillRect(QRect(self.xposLNA, er.top(), 4 * chw, er.bottom() + 1), Qt.gray) |
152 errorPen = QPen(Qt.red, 3) | |
160 | 153 # first and last row: |
161 | 154 row1 = max(int(er.top() / chh) - 1, 1) |
155 row2 = max(int(er.bottom() / chh) + 1, 1) | |
160 | 156 # Draw contents: |
163 | 157 ypos = row1 * chh - self.charDescent |
158 curRow = self.CursorRow | |
159 ydt = -chh + self.charDescent | |
160 | 160 for row in range(row1, row2 + 1): |
250 | 161 if curRow == row and self.hasFocus(): |
163 | 162 painter.fillRect(self.xposTXT, ypos + ydt, er.width(), chh, Qt.yellow) |
163 # cursor | |
164 if self.blinkcursor: | |
165 cursorX = self.CursorCol * self.charWidth + self.xposTXT - self.charWidth | |
166 cursorY = ypos + ydt | |
167 painter.fillRect(cursorX, cursorY, 2, chh, Qt.black) | |
160 | 168 painter.setPen(Qt.black) |
162 | 169 painter.drawText(self.xposLNA, ypos, '{0}'.format(row)) |
160 | 170 xpos = self.xposTXT |
161 | 171 painter.drawText(xpos, ypos, self.getRow(row)) |
249 | 172 if self.arrow and self.arrow.row == row: |
173 painter.drawPixmap(self.xposERR, ypos + ydt, self.arrowPixmap) | |
277 | 174 curErrors = [e for e in self.errorlist if e.loc and e.loc.row == row] |
163 | 175 for e in curErrors: |
176 painter.drawPixmap(self.xposERR, ypos + ydt, self.errorPixmap) | |
162 | 177 painter.setPen(errorPen) |
163 | 178 x = self.xposTXT + (e.loc.col - 1) * chw - 2 |
179 wt = e.loc.length * chw + 4 | |
167 | 180 dy = self.charDescent |
181 painter.drawLine(x, ypos + dy, x + wt, ypos + dy) | |
182 #painter.drawRoundedRect(x, ypos + ydt, wt, chh, 7, 7) | |
163 | 183 # print error balloon |
167 | 184 #painter.drawText(x, ypos + chh, e.msg) |
185 #if len(curErrors) > 0: | |
186 # ypos += chh | |
249 | 187 ypos += chh |
163 | 188 |
249 | 189 def keyPressEvent(self, event): |
160 | 190 if event.matches(QKeySequence.MoveToNextChar): |
191 self.GotoNextChar() | |
161 | 192 elif event.matches(QKeySequence.MoveToPreviousChar): |
160 | 193 self.GotoPrevChar() |
161 | 194 elif event.matches(QKeySequence.MoveToNextLine): |
160 | 195 self.GotoNextLine() |
161 | 196 elif event.matches(QKeySequence.MoveToPreviousLine): |
160 | 197 self.GotoPrevLine() |
161 | 198 elif event.matches(QKeySequence.MoveToNextPage): |
160 | 199 for i in range(5): |
200 self.GotoNextLine() | |
161 | 201 elif event.matches(QKeySequence.MoveToPreviousPage): |
160 | 202 for i in range(5): |
203 self.GotoPrevLine() | |
161 | 204 elif event.matches(QKeySequence.MoveToEndOfLine): |
160 | 205 self.CursorPosition += len(self.CurrentLine) - self.CursorCol + 1 |
161 | 206 elif event.matches(QKeySequence.MoveToStartOfLine): |
160 | 207 self.CursorPosition -= self.CursorCol - 1 |
161 | 208 elif event.matches(QKeySequence.Delete): |
209 self.deleteChar() | |
210 elif event.matches(QKeySequence.InsertParagraphSeparator): | |
211 self.insertText('\n') | |
212 elif event.key() == Qt.Key_Backspace: | |
213 self.CursorPosition -= 1 | |
214 self.deleteChar() | |
215 else: | |
216 char = event.text() | |
217 if char: | |
218 self.insertText(char) | |
160 | 219 self.update() |
249 | 220 |
221 def mousePressEvent(self, event): | |
256 | 222 pos = event.pos() |
223 if pos.x() > self.xposTXT and pos.x(): | |
224 c = round((pos.x() - self.xposTXT) / self.charWidth) | |
225 r = int(pos.y() / self.charHeight) + 1 | |
226 self.setRowCol(r, c) | |
227 super().mousePressEvent(event) | |
249 | 228 |
229 def adjust(self): | |
256 | 230 metrics = self.fontMetrics() |
231 self.charHeight = metrics.height() | |
232 self.charWidth = metrics.width('x') | |
233 self.charDescent = metrics.descent() | |
234 self.xposERR = GAP | |
235 self.xposLNA = self.xposERR + GAP + self.errorPixmap.width() | |
236 self.xposTXT = self.xposLNA + 4 * self.charWidth + GAP | |
237 self.xposEnd = self.xposTXT + self.charWidth * 80 | |
238 self.setMinimumWidth(self.xposEnd) | |
239 txt = self.src.split('\n') | |
240 self.setMinimumHeight(self.charHeight * len(txt)) | |
241 self.update() | |
160 | 242 |
243 class CodeEdit(QScrollArea): | |
248 | 244 def __init__(self): |
245 super().__init__() | |
246 self.ic = InnerCode(self) | |
247 self.textChanged = self.ic.textChanged | |
248 self.setWidget(self.ic) | |
249 self.setWidgetResizable(True) | |
250 self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) | |
251 self.setFocusPolicy(Qt.NoFocus) | |
252 self.showRow = self.ic.showRow | |
253 self.setRowCol = self.ic.setRowCol | |
254 self.FileName = None | |
255 Source = property(lambda s: s.ic.getSource(), lambda s, v: s.ic.setSource(v)) | |
249 | 256 |
248 | 257 def setErrors(self, el): |
258 self.ic.setErrors(el) | |
259 | |
260 def setFocus(self): | |
261 self.ic.setFocus() | |
256 | 262 super().setFocus() |
248 | 263 |
264 def setFileName(self, fn): | |
265 self.filename = fn | |
279 | 266 if fn: |
267 fn = os.path.basename(fn) | |
268 else: | |
248 | 269 fn = 'Untitled' |
270 self.setWindowTitle(fn) | |
249 | 271 |
248 | 272 def getFileName(self): |
273 return self.filename | |
274 FileName = property(getFileName, setFileName) | |
275 | |
280
02385f62f250
Rework from str interface to Instruction interface
Windel Bouwman
parents:
279
diff
changeset
|
276 def save(self): |
02385f62f250
Rework from str interface to Instruction interface
Windel Bouwman
parents:
279
diff
changeset
|
277 if self.FileName: |
02385f62f250
Rework from str interface to Instruction interface
Windel Bouwman
parents:
279
diff
changeset
|
278 s = self.Source |
02385f62f250
Rework from str interface to Instruction interface
Windel Bouwman
parents:
279
diff
changeset
|
279 with open(self.FileName, 'w') as f: |
02385f62f250
Rework from str interface to Instruction interface
Windel Bouwman
parents:
279
diff
changeset
|
280 f.write(s) |
02385f62f250
Rework from str interface to Instruction interface
Windel Bouwman
parents:
279
diff
changeset
|
281 |
160 | 282 |
283 if __name__ == '__main__': | |
249 | 284 app = QApplication(sys.argv) |
285 ce = CodeEdit() | |
286 ce.show() | |
287 src = ''.join(inspect.getsourcelines(InnerCode)[0]) | |
288 ce.Source = src | |
289 ce.resize(600, 800) | |
290 app.exec() | |
160 | 291 |