##// END OF EJS Templates
Initial checkin of Qt frontend code.
epatters -
Show More
@@ -0,0 +1,156 b''
1 # System library imports
2 from PyQt4 import QtCore, QtGui
3
4
5 class CallTipWidget(QtGui.QLabel):
6 """ Shows call tips by parsing the current text of Q[Plain]TextEdit.
7 """
8
9 #--------------------------------------------------------------------------
10 # 'QWidget' interface
11 #--------------------------------------------------------------------------
12
13 def __init__(self, parent):
14 """ Create a call tip manager that is attached to the specified Qt
15 text edit widget.
16 """
17 assert isinstance(parent, (QtGui.QTextEdit, QtGui.QPlainTextEdit))
18 QtGui.QLabel.__init__(self, parent, QtCore.Qt.ToolTip)
19
20 self.setFont(parent.document().defaultFont())
21 self.setForegroundRole(QtGui.QPalette.ToolTipText)
22 self.setBackgroundRole(QtGui.QPalette.ToolTipBase)
23 self.setPalette(QtGui.QToolTip.palette())
24
25 self.setAlignment(QtCore.Qt.AlignLeft)
26 self.setIndent(1)
27 self.setFrameStyle(QtGui.QFrame.NoFrame)
28 self.setMargin(1 + self.style().pixelMetric(
29 QtGui.QStyle.PM_ToolTipLabelFrameWidth, None, self))
30 self.setWindowOpacity(self.style().styleHint(
31 QtGui.QStyle.SH_ToolTipLabel_Opacity, None, self) / 255.0)
32
33 def hideEvent(self, event):
34 """ Reimplemented to disconnect the cursor movement handler.
35 """
36 QtGui.QListWidget.hideEvent(self, event)
37 self.parent().cursorPositionChanged.disconnect(self._update_tip)
38
39 def paintEvent(self, event):
40 """ Reimplemented to paint the background panel.
41 """
42 painter = QtGui.QStylePainter(self)
43 option = QtGui.QStyleOptionFrame()
44 option.init(self)
45 painter.drawPrimitive(QtGui.QStyle.PE_PanelTipLabel, option)
46 painter.end()
47
48 QtGui.QLabel.paintEvent(self, event)
49
50 def showEvent(self, event):
51 """ Reimplemented to connect the cursor movement handler.
52 """
53 QtGui.QListWidget.showEvent(self, event)
54 self.parent().cursorPositionChanged.connect(self._update_tip)
55
56 #--------------------------------------------------------------------------
57 # 'CallTipWidget' interface
58 #--------------------------------------------------------------------------
59
60 def show_tip(self, tip):
61 """ Attempts to show the specified tip at the current cursor location.
62 """
63 text_edit = self.parent()
64 document = text_edit.document()
65 cursor = text_edit.textCursor()
66 search_pos = cursor.position() - 1
67 self._start_position, _ = self._find_parenthesis(search_pos,
68 forward=False)
69 if self._start_position == -1:
70 return False
71
72 point = text_edit.cursorRect(cursor).bottomRight()
73 point = text_edit.mapToGlobal(point)
74 self.move(point)
75 self.setText(tip)
76 if self.isVisible():
77 self.resize(self.sizeHint())
78 else:
79 self.show()
80 return True
81
82 #--------------------------------------------------------------------------
83 # Protected interface
84 #--------------------------------------------------------------------------
85
86 def _find_parenthesis(self, position, forward=True):
87 """ If 'forward' is True (resp. False), proceed forwards
88 (resp. backwards) through the line that contains 'position' until an
89 unmatched closing (resp. opening) parenthesis is found. Returns a
90 tuple containing the position of this parenthesis (or -1 if it is
91 not found) and the number commas (at depth 0) found along the way.
92 """
93 commas = depth = 0
94 document = self.parent().document()
95 qchar = document.characterAt(position)
96 while (position > 0 and qchar.isPrint() and
97 # Need to check explicitly for line/paragraph separators:
98 qchar.unicode() not in (0x2028, 0x2029)):
99 char = qchar.toAscii()
100 if char == ',' and depth == 0:
101 commas += 1
102 elif char == ')':
103 if forward and depth == 0:
104 break
105 depth += 1
106 elif char == '(':
107 if not forward and depth == 0:
108 break
109 depth -= 1
110 position += 1 if forward else -1
111 qchar = document.characterAt(position)
112 else:
113 position = -1
114 return position, commas
115
116 def _highlight_tip(self, tip, current_argument):
117 """ Highlight the current argument (arguments start at 0), ending at the
118 next comma or unmatched closing parenthesis.
119
120 FIXME: This is an unreliable way to do things and it isn't being
121 used right now. Instead, we should use inspect.getargspec
122 metadata for this purpose.
123 """
124 start = tip.find('(')
125 if start != -1:
126 for i in xrange(current_argument):
127 start = tip.find(',', start)
128 if start != -1:
129 end = start + 1
130 while end < len(tip):
131 char = tip[end]
132 depth = 0
133 if (char == ',' and depth == 0):
134 break
135 elif char == '(':
136 depth += 1
137 elif char == ')':
138 if depth == 0:
139 break
140 depth -= 1
141 end += 1
142 tip = tip[:start+1] + '<font color="blue">' + \
143 tip[start+1:end] + '</font>' + tip[end:]
144 tip = tip.replace('\n', '<br/>')
145 return tip
146
147 def _update_tip(self):
148 """ Updates the tip based on user cursor movement.
149 """
150 cursor = self.parent().textCursor()
151 if cursor.position() <= self._start_position:
152 self.hide()
153 else:
154 position, commas = self._find_parenthesis(self._start_position + 1)
155 if position != -1:
156 self.hide()
@@ -0,0 +1,57 b''
1 # System library imports
2 from pygments.token import Token, is_token_subtype
3
4
5 class CompletionLexer(object):
6 """ Uses Pygments and some auxillary information to lex code snippets for
7 symbol contexts.
8 """
9
10 # Maps Lexer names to a list of possible name separators
11 separator_map = { 'C' : [ '.', '->' ],
12 'C++' : [ '.', '->', '::' ],
13 'Python' : [ '.' ] }
14
15 def __init__(self, lexer):
16 self.lexer = lexer
17
18 def get_context(self, string):
19 """ Assuming the cursor is at the end of the specified string, get the
20 context (a list of names) for the symbol at cursor position.
21 """
22 context = []
23 reversed_tokens = list(self._lexer.get_tokens(string))
24 reversed_tokens.reverse()
25
26 # Pygments often tacks on a newline when none is specified in the input
27 if reversed_tokens and reversed_tokens[0][1].endswith('\n') and \
28 not string.endswith('\n'):
29 reversed_tokens.pop(0)
30
31 current_op = unicode()
32 for token, text in reversed_tokens:
33 if is_token_subtype(token, Token.Name) and \
34 (not context or current_op in self._name_separators):
35 if not context and current_op in self._name_separators:
36 context.insert(0, unicode())
37 context.insert(0, text)
38 current_op = unicode()
39 elif token is Token.Operator or token is Token.Punctuation:
40 current_op = text + current_op
41 else:
42 break
43
44 return context
45
46 def get_lexer(self, lexer):
47 return self._lexer
48
49 def set_lexer(self, lexer, name_separators=None):
50 self._lexer = lexer
51 if name_separators is None:
52 self._name_separators = self.separator_map.get(lexer.name, ['.'])
53 else:
54 self._name_separators = list(name_separators)
55
56 lexer = property(get_lexer, set_lexer)
57
@@ -0,0 +1,121 b''
1 # System library imports
2 from PyQt4 import QtCore, QtGui
3
4
5 class CompletionWidget(QtGui.QListWidget):
6 """ A widget for GUI tab completion.
7 """
8
9 #--------------------------------------------------------------------------
10 # 'QWidget' interface
11 #--------------------------------------------------------------------------
12
13 def __init__(self, parent):
14 """ Create a completion widget that is attached to the specified Qt
15 text edit widget.
16 """
17 assert isinstance(parent, (QtGui.QTextEdit, QtGui.QPlainTextEdit))
18 QtGui.QListWidget.__init__(self, parent)
19
20 self.setWindowFlags(QtCore.Qt.ToolTip | QtCore.Qt.WindowStaysOnTopHint)
21 self.setAttribute(QtCore.Qt.WA_StaticContents)
22
23 # Ensure that parent keeps focus when widget is displayed.
24 self.setFocusProxy(parent)
25
26 self.setFrameShadow(QtGui.QFrame.Plain)
27 self.setFrameShape(QtGui.QFrame.StyledPanel)
28
29 self.itemActivated.connect(self._complete_current)
30
31 def hideEvent(self, event):
32 """ Reimplemented to disconnect the cursor movement handler.
33 """
34 QtGui.QListWidget.hideEvent(self, event)
35 self.parent().cursorPositionChanged.disconnect(self._update_current)
36
37 def keyPressEvent(self, event):
38 """ Reimplemented to update the list.
39 """
40 key, text = event.key(), event.text()
41
42 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter,
43 QtCore.Qt.Key_Tab):
44 self._complete_current()
45 event.accept()
46
47 elif key == QtCore.Qt.Key_Escape:
48 self.hide()
49 event.accept()
50
51 elif key in (QtCore.Qt.Key_Up, QtCore.Qt.Key_Down,
52 QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown,
53 QtCore.Qt.Key_Home, QtCore.Qt.Key_End):
54 QtGui.QListWidget.keyPressEvent(self, event)
55 event.accept()
56
57 else:
58 event.ignore()
59
60 def showEvent(self, event):
61 """ Reimplemented to connect the cursor movement handler.
62 """
63 QtGui.QListWidget.showEvent(self, event)
64 self.parent().cursorPositionChanged.connect(self._update_current)
65
66 #--------------------------------------------------------------------------
67 # 'CompletionWidget' interface
68 #--------------------------------------------------------------------------
69
70 def show_items(self, cursor, items):
71 """ Shows the completion widget with 'items' at the position specified
72 by 'cursor'.
73 """
74 text_edit = self.parent()
75 point = text_edit.cursorRect(cursor).bottomRight()
76 point = text_edit.mapToGlobal(point)
77 screen_rect = QtGui.QApplication.desktop().availableGeometry(self)
78 if screen_rect.size().height() - point.y() - self.height() < 0:
79 point = text_edit.mapToGlobal(text_edit.cursorRect().topRight())
80 point.setY(point.y() - self.height())
81 self.move(point)
82
83 self._start_position = cursor.position()
84 self.clear()
85 self.addItems(items)
86 self.setCurrentRow(0)
87 self.show()
88
89 #--------------------------------------------------------------------------
90 # Protected interface
91 #--------------------------------------------------------------------------
92
93 def _complete_current(self):
94 """ Perform the completion with the currently selected item.
95 """
96 self._current_text_cursor().insertText(self.currentItem().text())
97 self.hide()
98
99 def _current_text_cursor(self):
100 """ Returns a cursor with text between the start position and the
101 current position selected.
102 """
103 cursor = self.parent().textCursor()
104 if cursor.position() >= self._start_position:
105 cursor.setPosition(self._start_position,
106 QtGui.QTextCursor.KeepAnchor)
107 return cursor
108
109 def _update_current(self):
110 """ Updates the current item based on the current text.
111 """
112 prefix = self._current_text_cursor().selectedText()
113 if prefix:
114 items = self.findItems(prefix, (QtCore.Qt.MatchStartsWith |
115 QtCore.Qt.MatchCaseSensitive))
116 if items:
117 self.setCurrentItem(items[0])
118 else:
119 self.hide()
120 else:
121 self.hide()
This diff has been collapsed as it changes many lines, (650 lines changed) Show them Hide them
@@ -0,0 +1,650 b''
1 # Standard library imports
2 import re
3
4 # System library imports
5 from PyQt4 import QtCore, QtGui
6
7 # Local imports
8 from completion_widget import CompletionWidget
9
10
11 class AnsiCodeProcessor(object):
12 """ Translates ANSI escape codes into readable attributes.
13 """
14
15 def __init__(self):
16 self.ansi_colors = ( # Normal, Bright/Light
17 ('#000000', '#7f7f7f'), # 0: black
18 ('#cd0000', '#ff0000'), # 1: red
19 ('#00cd00', '#00ff00'), # 2: green
20 ('#cdcd00', '#ffff00'), # 3: yellow
21 ('#0000ee', '#0000ff'), # 4: blue
22 ('#cd00cd', '#ff00ff'), # 5: magenta
23 ('#00cdcd', '#00ffff'), # 6: cyan
24 ('#e5e5e5', '#ffffff')) # 7: white
25 self.reset()
26
27 def set_code(self, code):
28 """ Set attributes based on code.
29 """
30 if code == 0:
31 self.reset()
32 elif code == 1:
33 self.intensity = 1
34 self.bold = True
35 elif code == 3:
36 self.italic = True
37 elif code == 4:
38 self.underline = True
39 elif code == 22:
40 self.intensity = 0
41 self.bold = False
42 elif code == 23:
43 self.italic = False
44 elif code == 24:
45 self.underline = False
46 elif code >= 30 and code <= 37:
47 self.foreground_color = code - 30
48 elif code == 39:
49 self.foreground_color = None
50 elif code >= 40 and code <= 47:
51 self.background_color = code - 40
52 elif code == 49:
53 self.background_color = None
54
55 def reset(self):
56 """ Reset attributs to their default values.
57 """
58 self.intensity = 0
59 self.italic = False
60 self.bold = False
61 self.underline = False
62 self.foreground_color = None
63 self.background_color = None
64
65
66 class QtAnsiCodeProcessor(AnsiCodeProcessor):
67 """ Translates ANSI escape codes into QTextCharFormats.
68 """
69
70 def get_format(self):
71 """ Returns a QTextCharFormat that encodes the current style attributes.
72 """
73 format = QtGui.QTextCharFormat()
74
75 # Set foreground color
76 if self.foreground_color is not None:
77 color = self.ansi_colors[self.foreground_color][self.intensity]
78 format.setForeground(QtGui.QColor(color))
79
80 # Set background color
81 if self.background_color is not None:
82 color = self.ansi_colors[self.background_color][self.intensity]
83 format.setBackground(QtGui.QColor(color))
84
85 # Set font weight/style options
86 if self.bold:
87 format.setFontWeight(QtGui.QFont.Bold)
88 else:
89 format.setFontWeight(QtGui.QFont.Normal)
90 format.setFontItalic(self.italic)
91 format.setFontUnderline(self.underline)
92
93 return format
94
95
96 class ConsoleWidget(QtGui.QPlainTextEdit):
97 """ Base class for console-type widgets. This class is mainly concerned with
98 dealing with the prompt, keeping the cursor inside the editing line, and
99 handling ANSI escape sequences.
100 """
101
102 # Regex to match ANSI escape sequences
103 _ansi_pattern = re.compile('\x01?\x1b\[(.*?)m\x02?')
104
105 # When ctrl is pressed, map certain keys to other keys (without the ctrl):
106 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
107 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
108 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
109 QtCore.Qt.Key_E : QtCore.Qt.Key_End,
110 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
111 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
112 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
113
114 #---------------------------------------------------------------------------
115 # 'QWidget' interface
116 #---------------------------------------------------------------------------
117
118 def __init__(self, parent=None):
119 QtGui.QPlainTextEdit.__init__(self, parent)
120
121 # Initialize public and protected variables
122 self.ansi_codes = True
123 self.continuation_prompt = '> '
124 self.gui_completion = True
125 self._ansi_processor = QtAnsiCodeProcessor()
126 self._completion_widget = CompletionWidget(self)
127 self._executing = False
128 self._prompt = ''
129 self._prompt_pos = 0
130 self._reading = False
131
132 # Configure some basic QPlainTextEdit settings
133 self.setLineWrapMode(QtGui.QPlainTextEdit.WidgetWidth)
134 self.setMaximumBlockCount(500) # Limit text buffer size
135 self.setUndoRedoEnabled(False)
136
137 # Set a monospaced font
138 point_size = QtGui.QApplication.font().pointSize()
139 font = QtGui.QFont('Monospace', point_size)
140 font.setStyleHint(QtGui.QFont.TypeWriter)
141 self._completion_widget.setFont(font)
142 self.document().setDefaultFont(font)
143
144 # Define a custom context menu
145 self._context_menu = QtGui.QMenu(self)
146
147 copy_action = QtGui.QAction('Copy', self)
148 copy_action.triggered.connect(self.copy)
149 self.copyAvailable.connect(copy_action.setEnabled)
150 self._context_menu.addAction(copy_action)
151
152 self._paste_action = QtGui.QAction('Paste', self)
153 self._paste_action.triggered.connect(self.paste)
154 self._context_menu.addAction(self._paste_action)
155 self._context_menu.addSeparator()
156
157 select_all_action = QtGui.QAction('Select All', self)
158 select_all_action.triggered.connect(self.selectAll)
159 self._context_menu.addAction(select_all_action)
160
161 def contextMenuEvent(self, event):
162 """ Reimplemented to create a menu without destructive actions like
163 'Cut' and 'Delete'.
164 """
165 clipboard_empty = QtGui.QApplication.clipboard().text().isEmpty()
166 self._paste_action.setEnabled(not clipboard_empty)
167
168 self._context_menu.exec_(event.globalPos())
169
170 def keyPressEvent(self, event):
171 """ Reimplemented to create a console-like interface.
172 """
173 intercepted = False
174 cursor = self.textCursor()
175 position = cursor.position()
176 key = event.key()
177 ctrl_down = event.modifiers() & QtCore.Qt.ControlModifier
178 alt_down = event.modifiers() & QtCore.Qt.AltModifier
179 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
180
181 if ctrl_down:
182 if key in self._ctrl_down_remap:
183 ctrl_down = False
184 key = self._ctrl_down_remap[key]
185 event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, key,
186 QtCore.Qt.NoModifier)
187
188 elif key == QtCore.Qt.Key_K:
189 if self._in_buffer(position):
190 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
191 QtGui.QTextCursor.KeepAnchor)
192 cursor.removeSelectedText()
193 intercepted = True
194
195 elif key == QtCore.Qt.Key_Y:
196 self.paste()
197 intercepted = True
198
199 elif alt_down:
200 if key == QtCore.Qt.Key_B:
201 self.setTextCursor(self._get_word_start_cursor(position))
202 intercepted = True
203
204 elif key == QtCore.Qt.Key_F:
205 self.setTextCursor(self._get_word_end_cursor(position))
206 intercepted = True
207
208 elif key == QtCore.Qt.Key_Backspace:
209 cursor = self._get_word_start_cursor(position)
210 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
211 cursor.removeSelectedText()
212 intercepted = True
213
214 elif key == QtCore.Qt.Key_D:
215 cursor = self._get_word_end_cursor(position)
216 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
217 cursor.removeSelectedText()
218 intercepted = True
219
220 if self._completion_widget.isVisible():
221 self._completion_widget.keyPressEvent(event)
222 intercepted = event.isAccepted()
223
224 else:
225 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
226 if self._reading:
227 self._reading = False
228 elif not self._executing:
229 self._executing = True
230 self.execute(interactive=True)
231 intercepted = True
232
233 elif key == QtCore.Qt.Key_Up:
234 if self._reading or not self._up_pressed():
235 intercepted = True
236 else:
237 prompt_line = self._get_prompt_cursor().blockNumber()
238 intercepted = cursor.blockNumber() <= prompt_line
239
240 elif key == QtCore.Qt.Key_Down:
241 if self._reading or not self._down_pressed():
242 intercepted = True
243 else:
244 end_line = self._get_end_cursor().blockNumber()
245 intercepted = cursor.blockNumber() == end_line
246
247 elif key == QtCore.Qt.Key_Tab:
248 if self._reading:
249 intercepted = False
250 else:
251 intercepted = not self._tab_pressed()
252
253 elif key == QtCore.Qt.Key_Left:
254 intercepted = not self._in_buffer(position - 1)
255
256 elif key == QtCore.Qt.Key_Home:
257 cursor.movePosition(QtGui.QTextCursor.StartOfLine)
258 start_pos = cursor.position()
259 start_line = cursor.blockNumber()
260 if start_line == self._get_prompt_cursor().blockNumber():
261 start_pos += len(self._prompt)
262 else:
263 start_pos += len(self.continuation_prompt)
264 if shift_down and self._in_buffer(position):
265 self._set_selection(position, start_pos)
266 else:
267 self._set_position(start_pos)
268 intercepted = True
269
270 elif key == QtCore.Qt.Key_Backspace and not alt_down:
271
272 # Line deletion (remove continuation prompt)
273 len_prompt = len(self.continuation_prompt)
274 if cursor.columnNumber() == len_prompt and \
275 position != self._prompt_pos:
276 cursor.setPosition(position - len_prompt,
277 QtGui.QTextCursor.KeepAnchor)
278 cursor.removeSelectedText()
279
280 # Regular backwards deletion
281 else:
282 anchor = cursor.anchor()
283 if anchor == position:
284 intercepted = not self._in_buffer(position - 1)
285 else:
286 intercepted = not self._in_buffer(min(anchor, position))
287
288 elif key == QtCore.Qt.Key_Delete:
289 anchor = cursor.anchor()
290 intercepted = not self._in_buffer(min(anchor, position))
291
292 # Don't move cursor if control is down to allow copy-paste using
293 # the keyboard in any part of the buffer
294 if not ctrl_down:
295 self._keep_cursor_in_buffer()
296
297 if not intercepted:
298 QtGui.QPlainTextEdit.keyPressEvent(self, event)
299
300 #--------------------------------------------------------------------------
301 # 'QPlainTextEdit' interface
302 #--------------------------------------------------------------------------
303
304 def appendPlainText(self, text):
305 """ Reimplemented to not append text as a new paragraph, which doesn't
306 make sense for a console widget. Also, if enabled, handle ANSI
307 codes.
308 """
309 cursor = self.textCursor()
310 cursor.movePosition(QtGui.QTextCursor.End)
311
312 if self.ansi_codes:
313 format = QtGui.QTextCharFormat()
314 previous_end = 0
315 for match in self._ansi_pattern.finditer(text):
316 cursor.insertText(text[previous_end:match.start()], format)
317 previous_end = match.end()
318 for code in match.group(1).split(';'):
319 self._ansi_processor.set_code(int(code))
320 format = self._ansi_processor.get_format()
321 cursor.insertText(text[previous_end:], format)
322 else:
323 cursor.insertText(text)
324
325 def paste(self):
326 """ Reimplemented to ensure that text is pasted in the editing region.
327 """
328 self._keep_cursor_in_buffer()
329 QtGui.QPlainTextEdit.paste(self)
330
331 #---------------------------------------------------------------------------
332 # 'ConsoleWidget' public interface
333 #---------------------------------------------------------------------------
334
335 def execute(self, interactive=False):
336 """ Execute the text in the input buffer. Returns whether the input
337 buffer was completely processed and a new prompt created.
338 """
339 self.appendPlainText('\n')
340 self._prompt_finished()
341 return self._execute(interactive=interactive)
342
343 def _get_input_buffer(self):
344 cursor = self._get_end_cursor()
345 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
346
347 # Use QTextDocumentFragment intermediate object because it strips
348 # out the Unicode line break characters that Qt insists on inserting.
349 input_buffer = str(cursor.selection().toPlainText())
350
351 # Strip out continuation prompts
352 return input_buffer.replace('\n' + self.continuation_prompt, '\n')
353
354 def _set_input_buffer(self, string):
355 # Add continuation prompts where necessary
356 lines = string.splitlines()
357 for i in xrange(1, len(lines)):
358 lines[i] = self.continuation_prompt + lines[i]
359 string = '\n'.join(lines)
360
361 # Replace buffer with new text
362 cursor = self._get_end_cursor()
363 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
364 cursor.insertText(string)
365 self.moveCursor(QtGui.QTextCursor.End)
366
367 input_buffer = property(_get_input_buffer, _set_input_buffer)
368
369 def _get_input_buffer_cursor_line(self):
370 cursor = self.textCursor()
371 if cursor.position() >= self._prompt_pos:
372 text = str(cursor.block().text())
373 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
374 return text[len(self._prompt):]
375 else:
376 return text[len(self.continuation_prompt):]
377 else:
378 return None
379
380 input_buffer_cursor_line = property(_get_input_buffer_cursor_line)
381
382 #---------------------------------------------------------------------------
383 # 'ConsoleWidget' abstract interface
384 #---------------------------------------------------------------------------
385
386 def _execute(self, interactive):
387 """ Called to execute the input buffer. When triggered by an the enter
388 key press, 'interactive' is True; otherwise, it is False. Returns
389 whether the input buffer was completely processed and a new prompt
390 created.
391 """
392 raise NotImplementedError
393
394 def _prompt_started_hook(self):
395 """ Called immediately after a new prompt is displayed.
396 """
397 pass
398
399 def _prompt_finished_hook(self):
400 """ Called immediately after a prompt is finished, i.e. when some input
401 will be processed and a new prompt displayed.
402 """
403 pass
404
405 def _up_pressed(self):
406 """ Called when the up key is pressed. Returns whether to continue
407 processing the event.
408 """
409 return True
410
411 def _down_pressed(self):
412 """ Called when the down key is pressed. Returns whether to continue
413 processing the event.
414 """
415 return True
416
417 def _tab_pressed(self):
418 """ Called when the tab key is pressed. Returns whether to continue
419 processing the event.
420 """
421 return False
422
423 #--------------------------------------------------------------------------
424 # 'ConsoleWidget' protected interface
425 #--------------------------------------------------------------------------
426
427 def _complete_with_items(self, cursor, items):
428 """ Performs completion with 'items' at the specified cursor location.
429 """
430 if len(items) == 1:
431 cursor.setPosition(self.textCursor().position(),
432 QtGui.QTextCursor.KeepAnchor)
433 cursor.insertText(items[0])
434 elif len(items) > 1:
435 if self.gui_completion:
436 self._completion_widget.show_items(cursor, items)
437 else:
438 text = '\n'.join(items) + '\n'
439 self._write_text_keeping_prompt(text)
440
441 def _get_end_cursor(self):
442 """ Convenience method that returns a cursor for the last character.
443 """
444 cursor = self.textCursor()
445 cursor.movePosition(QtGui.QTextCursor.End)
446 return cursor
447
448 def _get_prompt_cursor(self):
449 """ Convenience method that returns a cursor for the prompt position.
450 """
451 cursor = self.textCursor()
452 cursor.setPosition(self._prompt_pos)
453 return cursor
454
455 def _get_selection_cursor(self, start, end):
456 """ Convenience method that returns a cursor with text selected between
457 the positions 'start' and 'end'.
458 """
459 cursor = self.textCursor()
460 cursor.setPosition(start)
461 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
462 return cursor
463
464 def _get_word_start_cursor(self, position):
465 """ Find the start of the word to the left the given position. If a
466 sequence of non-word characters precedes the first word, skip over
467 them. (This emulates the behavior of bash, emacs, etc.)
468 """
469 document = self.document()
470 position -= 1
471 while self._in_buffer(position) and \
472 not document.characterAt(position).isLetterOrNumber():
473 position -= 1
474 while self._in_buffer(position) and \
475 document.characterAt(position).isLetterOrNumber():
476 position -= 1
477 cursor = self.textCursor()
478 cursor.setPosition(position + 1)
479 return cursor
480
481 def _get_word_end_cursor(self, position):
482 """ Find the end of the word to the right the given position. If a
483 sequence of non-word characters precedes the first word, skip over
484 them. (This emulates the behavior of bash, emacs, etc.)
485 """
486 document = self.document()
487 end = self._get_end_cursor().position()
488 while position < end and \
489 not document.characterAt(position).isLetterOrNumber():
490 position += 1
491 while position < end and \
492 document.characterAt(position).isLetterOrNumber():
493 position += 1
494 cursor = self.textCursor()
495 cursor.setPosition(position)
496 return cursor
497
498 def _prompt_started(self):
499 """ Called immediately after a new prompt is displayed.
500 """
501 self.moveCursor(QtGui.QTextCursor.End)
502 self.centerCursor()
503 self.setReadOnly(False)
504 self._executing = False
505 self._prompt_started_hook()
506
507 def _prompt_finished(self):
508 """ Called immediately after a prompt is finished, i.e. when some input
509 will be processed and a new prompt displayed.
510 """
511 self.setReadOnly(True)
512 self._prompt_finished_hook()
513
514 def _set_position(self, position):
515 """ Convenience method to set the position of the cursor.
516 """
517 cursor = self.textCursor()
518 cursor.setPosition(position)
519 self.setTextCursor(cursor)
520
521 def _set_selection(self, start, end):
522 """ Convenience method to set the current selected text.
523 """
524 self.setTextCursor(self._get_selection_cursor(start, end))
525
526 def _show_prompt(self, prompt):
527 """ Writes a new prompt at the end of the buffer.
528 """
529 self.appendPlainText('\n' + prompt)
530 self._prompt = prompt
531 self._prompt_pos = self._get_end_cursor().position()
532 self._prompt_started()
533
534 def _show_continuation_prompt(self):
535 """ Writes a new continuation prompt at the end of the buffer.
536 """
537 self.appendPlainText(self.continuation_prompt)
538 self._prompt_started()
539
540 def _write_text_keeping_prompt(self, text):
541 """ Writes 'text' after the current prompt, then restores the old prompt
542 with its old input buffer.
543 """
544 input_buffer = self.input_buffer
545 self.appendPlainText('\n')
546 self._prompt_finished()
547
548 self.appendPlainText(text)
549 self._show_prompt(self._prompt)
550 self.input_buffer = input_buffer
551
552 def _in_buffer(self, position):
553 """ Returns whether the given position is inside the editing region.
554 """
555 return position >= self._prompt_pos
556
557 def _keep_cursor_in_buffer(self):
558 """ Ensures that the cursor is inside the editing region. Returns
559 whether the cursor was moved.
560 """
561 cursor = self.textCursor()
562 if cursor.position() < self._prompt_pos:
563 cursor.movePosition(QtGui.QTextCursor.End)
564 self.setTextCursor(cursor)
565 return True
566 else:
567 return False
568
569
570 class HistoryConsoleWidget(ConsoleWidget):
571 """ A ConsoleWidget that keeps a history of the commands that have been
572 executed.
573 """
574
575 #---------------------------------------------------------------------------
576 # 'QWidget' interface
577 #---------------------------------------------------------------------------
578
579 def __init__(self, parent=None):
580 super(HistoryConsoleWidget, self).__init__(parent)
581
582 self._history = []
583 self._history_index = 0
584
585 #---------------------------------------------------------------------------
586 # 'ConsoleWidget' public interface
587 #---------------------------------------------------------------------------
588
589 def execute(self, interactive=False):
590 """ Reimplemented to the store history.
591 """
592 stripped = self.input_buffer.rstrip()
593 executed = super(HistoryConsoleWidget, self).execute(interactive)
594 if executed:
595 self._history.append(stripped)
596 self._history_index = len(self._history)
597 return executed
598
599 #---------------------------------------------------------------------------
600 # 'ConsoleWidget' abstract interface
601 #---------------------------------------------------------------------------
602
603 def _up_pressed(self):
604 """ Called when the up key is pressed. Returns whether to continue
605 processing the event.
606 """
607 prompt_cursor = self._get_prompt_cursor()
608 if self.textCursor().blockNumber() == prompt_cursor.blockNumber():
609 self.history_previous()
610
611 # Go to the first line of prompt for seemless history scrolling.
612 cursor = self._get_prompt_cursor()
613 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
614 self.setTextCursor(cursor)
615
616 return False
617 return True
618
619 def _down_pressed(self):
620 """ Called when the down key is pressed. Returns whether to continue
621 processing the event.
622 """
623 end_cursor = self._get_end_cursor()
624 if self.textCursor().blockNumber() == end_cursor.blockNumber():
625 self.history_next()
626 return False
627 return True
628
629 #---------------------------------------------------------------------------
630 # 'HistoryConsoleWidget' interface
631 #---------------------------------------------------------------------------
632
633 def history_previous(self):
634 """ If possible, set the input buffer to the previous item in the
635 history.
636 """
637 if self._history_index > 0:
638 self._history_index -= 1
639 self.input_buffer = self._history[self._history_index]
640
641 def history_next(self):
642 """ Set the input buffer to the next item in the history, or a blank
643 line if there is no subsequent item.
644 """
645 if self._history_index < len(self._history):
646 self._history_index += 1
647 if self._history_index < len(self._history):
648 self.input_buffer = self._history[self._history_index]
649 else:
650 self.input_buffer = ''
@@ -0,0 +1,383 b''
1 # Standard library imports
2 from codeop import CommandCompiler
3 from threading import Thread
4 import time
5 import types
6
7 # System library imports
8 from IPython.zmq.session import Message, Session
9 from pygments.lexers import PythonLexer
10 from PyQt4 import QtCore, QtGui
11 import zmq
12
13 # ETS imports
14 from enthought.pyface.ui.qt4.code_editor.pygments_highlighter import \
15 PygmentsHighlighter
16
17 # Local imports
18 from call_tip_widget import CallTipWidget
19 from completion_lexer import CompletionLexer
20 from console_widget import HistoryConsoleWidget
21
22
23 class FrontendReplyThread(Thread, QtCore.QObject):
24 """ A Thread that receives a reply from the kernel for the frontend.
25 """
26
27 finished = QtCore.pyqtSignal()
28 output_received = QtCore.pyqtSignal(Message)
29 reply_received = QtCore.pyqtSignal(Message)
30
31 def __init__(self, parent):
32 """ Create a FrontendReplyThread for the specified frontend.
33 """
34 assert isinstance(parent, FrontendWidget)
35 QtCore.QObject.__init__(self, parent)
36 Thread.__init__(self)
37
38 self.sleep_time = 0.05
39
40 def run(self):
41 """ The starting point for the thread.
42 """
43 frontend = self.parent()
44 while True:
45 rep = frontend._recv_reply()
46 if rep is not None:
47 self._recv_output()
48 self.reply_received.emit(rep)
49 break
50
51 self._recv_output()
52 time.sleep(self.sleep_time)
53
54 self.finished.emit()
55
56 def _recv_output(self):
57 """ Send any output to the frontend.
58 """
59 frontend = self.parent()
60 omsgs = frontend._recv_output()
61 for omsg in omsgs:
62 self.output_received.emit(omsg)
63
64
65 class FrontendHighlighter(PygmentsHighlighter):
66 """ A Python PygmentsHighlighter that can be turned on and off and which
67 knows about continuation prompts.
68 """
69
70 def __init__(self, frontend):
71 PygmentsHighlighter.__init__(self, frontend.document(), PythonLexer())
72 self._current_offset = 0
73 self._frontend = frontend
74 self.highlighting_on = False
75
76 def highlightBlock(self, qstring):
77 """ Highlight a block of text. Reimplemented to highlight selectively.
78 """
79 if self.highlighting_on:
80 for prompt in (self._frontend._prompt,
81 self._frontend.continuation_prompt):
82 if qstring.startsWith(prompt):
83 qstring.remove(0, len(prompt))
84 self._current_offset = len(prompt)
85 break
86 PygmentsHighlighter.highlightBlock(self, qstring)
87
88 def setFormat(self, start, count, format):
89 """ Reimplemented to avoid highlighting continuation prompts.
90 """
91 start += self._current_offset
92 PygmentsHighlighter.setFormat(self, start, count, format)
93
94
95 class FrontendWidget(HistoryConsoleWidget):
96 """ A Qt frontend for an IPython kernel.
97 """
98
99 # Emitted when an 'execute_reply' is received from the kernel.
100 executed = QtCore.pyqtSignal(Message)
101
102 #---------------------------------------------------------------------------
103 # 'QWidget' interface
104 #---------------------------------------------------------------------------
105
106 def __init__(self, parent=None, session=None, request_socket=None,
107 sub_socket=None):
108 super(FrontendWidget, self).__init__(parent)
109 self.continuation_prompt = '... '
110
111 self._call_tip_widget = CallTipWidget(self)
112 self._compile = CommandCompiler()
113 self._completion_lexer = CompletionLexer(PythonLexer())
114 self._highlighter = FrontendHighlighter(self)
115
116 self.session = Session() if session is None else session
117 self.request_socket = request_socket
118 self.sub_socket = sub_socket
119
120 self.document().contentsChange.connect(self._document_contents_change)
121
122 self._kernel_connected() # XXX
123
124 def focusOutEvent(self, event):
125 """ Reimplemented to hide calltips.
126 """
127 self._call_tip_widget.hide()
128 return super(FrontendWidget, self).focusOutEvent(event)
129
130 def keyPressEvent(self, event):
131 """ Reimplemented to hide calltips.
132 """
133 if event.key() == QtCore.Qt.Key_Escape:
134 self._call_tip_widget.hide()
135 return super(FrontendWidget, self).keyPressEvent(event)
136
137 #---------------------------------------------------------------------------
138 # 'ConsoleWidget' abstract interface
139 #---------------------------------------------------------------------------
140
141 def _execute(self, interactive):
142 """ Called to execute the input buffer. When triggered by an the enter
143 key press, 'interactive' is True; otherwise, it is False. Returns
144 whether the input buffer was completely processed and a new prompt
145 created.
146 """
147 return self.execute_source(self.input_buffer, interactive=interactive)
148
149 def _prompt_started_hook(self):
150 """ Called immediately after a new prompt is displayed.
151 """
152 self._highlighter.highlighting_on = True
153
154 def _prompt_finished_hook(self):
155 """ Called immediately after a prompt is finished, i.e. when some input
156 will be processed and a new prompt displayed.
157 """
158 self._highlighter.highlighting_on = False
159
160 def _tab_pressed(self):
161 """ Called when the tab key is pressed. Returns whether to continue
162 processing the event.
163 """
164 self._keep_cursor_in_buffer()
165 cursor = self.textCursor()
166 if not self._complete():
167 cursor.insertText(' ')
168 return False
169
170 #---------------------------------------------------------------------------
171 # 'FrontendWidget' interface
172 #---------------------------------------------------------------------------
173
174 def execute_source(self, source, hidden=False, interactive=False):
175 """ Execute a string containing Python code. If 'hidden', no output is
176 shown. Returns whether the source executed (i.e., returns True only
177 if no more input is necessary).
178 """
179 try:
180 code = self._compile(source, symbol='single')
181 except (OverflowError, SyntaxError, ValueError):
182 # Just let IPython deal with the syntax error.
183 code = Exception
184
185 # Only execute interactive multiline input if it ends with a blank line
186 lines = source.splitlines()
187 if interactive and len(lines) > 1 and lines[-1].strip() != '':
188 code = None
189
190 executed = code is not None
191 if executed:
192 msg = self.session.send(self.request_socket, 'execute_request',
193 dict(code=source))
194 thread = FrontendReplyThread(self)
195 if not hidden:
196 thread.output_received.connect(self._handle_output)
197 thread.reply_received.connect(self._handle_reply)
198 thread.finished.connect(thread.deleteLater)
199 thread.start()
200 else:
201 space = 0
202 for char in lines[-1]:
203 if char == '\t':
204 space += 4
205 elif char == ' ':
206 space += 1
207 else:
208 break
209 if source.endswith(':') or source.endswith(':\n'):
210 space += 4
211 self._show_continuation_prompt()
212 self.appendPlainText(' ' * space)
213
214 return executed
215
216 def execute_file(self, path, hidden=False):
217 """ Attempts to execute file with 'path'. If 'hidden', no output is
218 shown.
219 """
220 self.execute_source('run %s' % path, hidden=hidden)
221
222 #---------------------------------------------------------------------------
223 # 'FrontendWidget' protected interface
224 #---------------------------------------------------------------------------
225
226 def _call_tip(self):
227 """ Shows a call tip, if appropriate, at the current cursor location.
228 """
229 # Decide if it makes sense to show a call tip
230 cursor = self.textCursor()
231 cursor.movePosition(QtGui.QTextCursor.Left)
232 document = self.document()
233 if document.characterAt(cursor.position()).toAscii() != '(':
234 return False
235 context = self._get_context(cursor)
236 if not context:
237 return False
238
239 # Send the metadata request to the kernel
240 text = '.'.join(context)
241 msg = self.session.send(self.request_socket, 'metadata_request',
242 dict(context=text))
243
244 # Give the kernel some time to respond
245 rep = self._recv_reply_now('metadata_reply')
246 doc = rep.content.docstring if rep else ''
247
248 # Show the call tip
249 if doc:
250 self._call_tip_widget.show_tip(doc)
251 return True
252
253 def _complete(self):
254 """ Performs completion at the current cursor location.
255 """
256 # Decide if it makes sense to do completion
257 context = self._get_context()
258 if not context:
259 return False
260
261 # Send the completion request to the kernel
262 text = '.'.join(context)
263 line = self.input_buffer_cursor_line
264 msg = self.session.send(self.request_socket, 'complete_request',
265 dict(text=text, line=line))
266
267 # Give the kernel some time to respond
268 rep = self._recv_reply_now('complete_reply')
269 matches = rep.content.matches if rep else []
270
271 # Show the completion at the correct location
272 cursor = self.textCursor()
273 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
274 self._complete_with_items(cursor, matches)
275 return True
276
277 def _kernel_connected(self):
278 """ Called when the frontend is connected to a kernel.
279 """
280 self._show_prompt('>>> ')
281
282 def _get_context(self, cursor=None):
283 """ Gets the context at the current cursor location.
284 """
285 if cursor is None:
286 cursor = self.textCursor()
287 cursor.movePosition(QtGui.QTextCursor.StartOfLine,
288 QtGui.QTextCursor.KeepAnchor)
289 text = unicode(cursor.selectedText())
290 return self._completion_lexer.get_context(text)
291
292 #------ Signal handlers ----------------------------------------------------
293
294 def _document_contents_change(self, position, removed, added):
295 """ Called whenever the document's content changes. Display a calltip
296 if appropriate.
297 """
298 # Calculate where the cursor should be *after* the change:
299 position += added
300
301 document = self.document()
302 if position == self.textCursor().position():
303 self._call_tip()
304
305 def _handle_output(self, omsg):
306 handler = getattr(self, '_handle_%s' % omsg.msg_type, None)
307 if handler is not None:
308 handler(omsg)
309
310 def _handle_pyout(self, omsg):
311 if omsg.parent_header.session == self.session.session:
312 self.appendPlainText(omsg.content.data + '\n')
313
314 def _handle_stream(self, omsg):
315 self.appendPlainText(omsg.content.data)
316
317 def _handle_reply(self, rep):
318 if rep is not None:
319 if rep.msg_type == 'execute_reply':
320 if rep.content.status == 'error':
321 self.appendPlainText(rep.content.traceback[-1])
322 elif rep.content.status == 'aborted':
323 text = "ERROR: ABORTED\n"
324 ab = self.messages[rep.parent_header.msg_id].content
325 if 'code' in ab:
326 text += ab.code
327 else:
328 text += ab
329 self.appendPlainText(text)
330 self._show_prompt('>>> ')
331 self.executed.emit(rep)
332
333 #------ Communication methods ----------------------------------------------
334
335 def _recv_output(self):
336 omsgs = []
337 while True:
338 omsg = self.session.recv(self.sub_socket)
339 if omsg is None:
340 break
341 else:
342 omsgs.append(omsg)
343 return omsgs
344
345 def _recv_reply(self):
346 return self.session.recv(self.request_socket)
347
348 def _recv_reply_now(self, msg_type):
349 for i in xrange(5):
350 rep = self._recv_reply()
351 if rep is not None and rep.msg_type == msg_type:
352 return rep
353 time.sleep(0.1)
354 return None
355
356
357 if __name__ == '__main__':
358 import sys
359
360 # Defaults
361 ip = '127.0.0.1'
362 port_base = 5555
363 connection = ('tcp://%s' % ip) + ':%i'
364 req_conn = connection % port_base
365 sub_conn = connection % (port_base+1)
366
367 # Create initial sockets
368 c = zmq.Context()
369 request_socket = c.socket(zmq.XREQ)
370 request_socket.connect(req_conn)
371 sub_socket = c.socket(zmq.SUB)
372 sub_socket.connect(sub_conn)
373 sub_socket.setsockopt(zmq.SUBSCRIBE, '')
374
375 # Launch application
376 app = QtGui.QApplication(sys.argv)
377 widget = FrontendWidget(request_socket=request_socket,
378 sub_socket=sub_socket)
379 widget.setWindowTitle('Python')
380 widget.resize(640, 480)
381 widget.show()
382 sys.exit(app.exec_())
383
@@ -0,0 +1,221 b''
1 #------------------------------------------------------------------------------
2 # Copyright (c) 2010, Enthought Inc
3 # All rights reserved.
4 #
5 # This software is provided without warranty under the terms of the BSD license.
6
7 #
8 # Author: Enthought Inc
9 # Description: <Enthought pyface code editor>
10 #------------------------------------------------------------------------------
11
12 from PyQt4 import QtGui
13
14 from pygments.lexer import RegexLexer, _TokenType, Text, Error
15 from pygments.lexers import CLexer, CppLexer, PythonLexer
16 from pygments.styles.default import DefaultStyle
17 from pygments.token import Comment
18
19
20 def get_tokens_unprocessed(self, text, stack=('root',)):
21 """ Split ``text`` into (tokentype, text) pairs.
22
23 Monkeypatched to store the final stack on the object itself.
24 """
25 pos = 0
26 tokendefs = self._tokens
27 if hasattr(self, '_epd_state_stack'):
28 statestack = list(self._epd_state_stack)
29 else:
30 statestack = list(stack)
31 statetokens = tokendefs[statestack[-1]]
32 while 1:
33 for rexmatch, action, new_state in statetokens:
34 m = rexmatch(text, pos)
35 if m:
36 if type(action) is _TokenType:
37 yield pos, action, m.group()
38 else:
39 for item in action(self, m):
40 yield item
41 pos = m.end()
42 if new_state is not None:
43 # state transition
44 if isinstance(new_state, tuple):
45 for state in new_state:
46 if state == '#pop':
47 statestack.pop()
48 elif state == '#push':
49 statestack.append(statestack[-1])
50 else:
51 statestack.append(state)
52 elif isinstance(new_state, int):
53 # pop
54 del statestack[new_state:]
55 elif new_state == '#push':
56 statestack.append(statestack[-1])
57 else:
58 assert False, "wrong state def: %r" % new_state
59 statetokens = tokendefs[statestack[-1]]
60 break
61 else:
62 try:
63 if text[pos] == '\n':
64 # at EOL, reset state to "root"
65 pos += 1
66 statestack = ['root']
67 statetokens = tokendefs['root']
68 yield pos, Text, u'\n'
69 continue
70 yield pos, Error, text[pos]
71 pos += 1
72 except IndexError:
73 break
74 self._epd_state_stack = list(statestack)
75
76 # Monkeypatch!
77 RegexLexer.get_tokens_unprocessed = get_tokens_unprocessed
78
79
80 # Even with the above monkey patch to store state, multiline comments do not
81 # work since they are stateless (Pygments uses a single multiline regex for
82 # these comments, but Qt lexes by line). So we need to add a state for comments
83 # to the C and C++ lexers. This means that nested multiline comments will appear
84 # to be valid C/C++, but this is better than the alternative for now.
85
86 def replace_pattern(tokens, new_pattern):
87 """ Given a RegexLexer token dictionary 'tokens', replace all patterns that
88 match the token specified in 'new_pattern' with 'new_pattern'.
89 """
90 for state in tokens.values():
91 for index, pattern in enumerate(state):
92 if isinstance(pattern, tuple) and pattern[1] == new_pattern[1]:
93 state[index] = new_pattern
94
95 # More monkeypatching!
96 comment_start = (r'/\*', Comment.Multiline, 'comment')
97 comment_state = [ (r'[^*/]', Comment.Multiline),
98 (r'/\*', Comment.Multiline, '#push'),
99 (r'\*/', Comment.Multiline, '#pop'),
100 (r'[*/]', Comment.Multiline) ]
101 replace_pattern(CLexer.tokens, comment_start)
102 replace_pattern(CppLexer.tokens, comment_start)
103 CLexer.tokens['comment'] = comment_state
104 CppLexer.tokens['comment'] = comment_state
105
106
107 class BlockUserData(QtGui.QTextBlockUserData):
108 """ Storage for the user data associated with each line.
109 """
110
111 syntax_stack = ('root',)
112
113 def __init__(self, **kwds):
114 for key, value in kwds.iteritems():
115 setattr(self, key, value)
116 QtGui.QTextBlockUserData.__init__(self)
117
118 def __repr__(self):
119 attrs = ['syntax_stack']
120 kwds = ', '.join([ '%s=%r' % (attr, getattr(self, attr))
121 for attr in attrs ])
122 return 'BlockUserData(%s)' % kwds
123
124
125 class PygmentsHighlighter(QtGui.QSyntaxHighlighter):
126 """ Syntax highlighter that uses Pygments for parsing. """
127
128 def __init__(self, parent, lexer=None):
129 super(PygmentsHighlighter, self).__init__(parent)
130
131 self._lexer = lexer if lexer else PythonLexer()
132 self._style = DefaultStyle
133 # Caches for formats and brushes.
134 self._brushes = {}
135 self._formats = {}
136
137 def highlightBlock(self, qstring):
138 """ Highlight a block of text.
139 """
140 qstring = unicode(qstring)
141 prev_data = self.previous_block_data()
142
143 if prev_data is not None:
144 self._lexer._epd_state_stack = prev_data.syntax_stack
145 elif hasattr(self._lexer, '_epd_state_stack'):
146 del self._lexer._epd_state_stack
147
148 index = 0
149 # Lex the text using Pygments
150 for token, text in self._lexer.get_tokens(qstring):
151 l = len(text)
152 format = self._get_format(token)
153 if format is not None:
154 self.setFormat(index, l, format)
155 index += l
156
157 if hasattr(self._lexer, '_epd_state_stack'):
158 data = BlockUserData(syntax_stack=self._lexer._epd_state_stack)
159 self.currentBlock().setUserData(data)
160 # Clean up for the next go-round.
161 del self._lexer._epd_state_stack
162
163 def previous_block_data(self):
164 """ Convenience method for returning the previous block's user data.
165 """
166 return self.currentBlock().previous().userData()
167
168 def _get_format(self, token):
169 """ Returns a QTextCharFormat for token or None.
170 """
171 if token in self._formats:
172 return self._formats[token]
173 result = None
174 for key, value in self._style.style_for_token(token) .items():
175 if value:
176 if result is None:
177 result = QtGui.QTextCharFormat()
178 if key == 'color':
179 result.setForeground(self._get_brush(value))
180 elif key == 'bgcolor':
181 result.setBackground(self._get_brush(value))
182 elif key == 'bold':
183 result.setFontWeight(QtGui.QFont.Bold)
184 elif key == 'italic':
185 result.setFontItalic(True)
186 elif key == 'underline':
187 result.setUnderlineStyle(
188 QtGui.QTextCharFormat.SingleUnderline)
189 elif key == 'sans':
190 result.setFontStyleHint(QtGui.QFont.SansSerif)
191 elif key == 'roman':
192 result.setFontStyleHint(QtGui.QFont.Times)
193 elif key == 'mono':
194 result.setFontStyleHint(QtGui.QFont.TypeWriter)
195 elif key == 'border':
196 # Borders are normally used for errors. We can't do a border
197 # so instead we do a wavy underline
198 result.setUnderlineStyle(
199 QtGui.QTextCharFormat.WaveUnderline)
200 result.setUnderlineColor(self._get_color(value))
201 self._formats[token] = result
202 return result
203
204 def _get_brush(self, color):
205 """ Returns a brush for the color.
206 """
207 result = self._brushes.get(color)
208 if result is None:
209 qcolor = self._get_color(color)
210 result = QtGui.QBrush(qcolor)
211 self._brushes[color] = result
212
213 return result
214
215 def _get_color(self, color):
216 qcolor = QtGui.QColor()
217 qcolor.setRgb(int(color[:2],base=16),
218 int(color[2:4], base=16),
219 int(color[4:6], base=16))
220 return qcolor
221
General Comments 0
You need to be logged in to leave comments. Login now