##// END OF EJS Templates
* Added undo/redo support to ConsoleWidget...
epatters -
Show More
@@ -1,650 +1,668 b''
1 # Standard library imports
1 # Standard library imports
2 import re
2 import re
3
3
4 # System library imports
4 # System library imports
5 from PyQt4 import QtCore, QtGui
5 from PyQt4 import QtCore, QtGui
6
6
7 # Local imports
7 # Local imports
8 from completion_widget import CompletionWidget
8 from completion_widget import CompletionWidget
9
9
10
10
11 class AnsiCodeProcessor(object):
11 class AnsiCodeProcessor(object):
12 """ Translates ANSI escape codes into readable attributes.
12 """ Translates ANSI escape codes into readable attributes.
13 """
13 """
14
14
15 def __init__(self):
15 def __init__(self):
16 self.ansi_colors = ( # Normal, Bright/Light
16 self.ansi_colors = ( # Normal, Bright/Light
17 ('#000000', '#7f7f7f'), # 0: black
17 ('#000000', '#7f7f7f'), # 0: black
18 ('#cd0000', '#ff0000'), # 1: red
18 ('#cd0000', '#ff0000'), # 1: red
19 ('#00cd00', '#00ff00'), # 2: green
19 ('#00cd00', '#00ff00'), # 2: green
20 ('#cdcd00', '#ffff00'), # 3: yellow
20 ('#cdcd00', '#ffff00'), # 3: yellow
21 ('#0000ee', '#0000ff'), # 4: blue
21 ('#0000ee', '#0000ff'), # 4: blue
22 ('#cd00cd', '#ff00ff'), # 5: magenta
22 ('#cd00cd', '#ff00ff'), # 5: magenta
23 ('#00cdcd', '#00ffff'), # 6: cyan
23 ('#00cdcd', '#00ffff'), # 6: cyan
24 ('#e5e5e5', '#ffffff')) # 7: white
24 ('#e5e5e5', '#ffffff')) # 7: white
25 self.reset()
25 self.reset()
26
26
27 def set_code(self, code):
27 def set_code(self, code):
28 """ Set attributes based on code.
28 """ Set attributes based on code.
29 """
29 """
30 if code == 0:
30 if code == 0:
31 self.reset()
31 self.reset()
32 elif code == 1:
32 elif code == 1:
33 self.intensity = 1
33 self.intensity = 1
34 self.bold = True
34 self.bold = True
35 elif code == 3:
35 elif code == 3:
36 self.italic = True
36 self.italic = True
37 elif code == 4:
37 elif code == 4:
38 self.underline = True
38 self.underline = True
39 elif code == 22:
39 elif code == 22:
40 self.intensity = 0
40 self.intensity = 0
41 self.bold = False
41 self.bold = False
42 elif code == 23:
42 elif code == 23:
43 self.italic = False
43 self.italic = False
44 elif code == 24:
44 elif code == 24:
45 self.underline = False
45 self.underline = False
46 elif code >= 30 and code <= 37:
46 elif code >= 30 and code <= 37:
47 self.foreground_color = code - 30
47 self.foreground_color = code - 30
48 elif code == 39:
48 elif code == 39:
49 self.foreground_color = None
49 self.foreground_color = None
50 elif code >= 40 and code <= 47:
50 elif code >= 40 and code <= 47:
51 self.background_color = code - 40
51 self.background_color = code - 40
52 elif code == 49:
52 elif code == 49:
53 self.background_color = None
53 self.background_color = None
54
54
55 def reset(self):
55 def reset(self):
56 """ Reset attributs to their default values.
56 """ Reset attributs to their default values.
57 """
57 """
58 self.intensity = 0
58 self.intensity = 0
59 self.italic = False
59 self.italic = False
60 self.bold = False
60 self.bold = False
61 self.underline = False
61 self.underline = False
62 self.foreground_color = None
62 self.foreground_color = None
63 self.background_color = None
63 self.background_color = None
64
64
65
65
66 class QtAnsiCodeProcessor(AnsiCodeProcessor):
66 class QtAnsiCodeProcessor(AnsiCodeProcessor):
67 """ Translates ANSI escape codes into QTextCharFormats.
67 """ Translates ANSI escape codes into QTextCharFormats.
68 """
68 """
69
69
70 def get_format(self):
70 def get_format(self):
71 """ Returns a QTextCharFormat that encodes the current style attributes.
71 """ Returns a QTextCharFormat that encodes the current style attributes.
72 """
72 """
73 format = QtGui.QTextCharFormat()
73 format = QtGui.QTextCharFormat()
74
74
75 # Set foreground color
75 # Set foreground color
76 if self.foreground_color is not None:
76 if self.foreground_color is not None:
77 color = self.ansi_colors[self.foreground_color][self.intensity]
77 color = self.ansi_colors[self.foreground_color][self.intensity]
78 format.setForeground(QtGui.QColor(color))
78 format.setForeground(QtGui.QColor(color))
79
79
80 # Set background color
80 # Set background color
81 if self.background_color is not None:
81 if self.background_color is not None:
82 color = self.ansi_colors[self.background_color][self.intensity]
82 color = self.ansi_colors[self.background_color][self.intensity]
83 format.setBackground(QtGui.QColor(color))
83 format.setBackground(QtGui.QColor(color))
84
84
85 # Set font weight/style options
85 # Set font weight/style options
86 if self.bold:
86 if self.bold:
87 format.setFontWeight(QtGui.QFont.Bold)
87 format.setFontWeight(QtGui.QFont.Bold)
88 else:
88 else:
89 format.setFontWeight(QtGui.QFont.Normal)
89 format.setFontWeight(QtGui.QFont.Normal)
90 format.setFontItalic(self.italic)
90 format.setFontItalic(self.italic)
91 format.setFontUnderline(self.underline)
91 format.setFontUnderline(self.underline)
92
92
93 return format
93 return format
94
94
95
95
96 class ConsoleWidget(QtGui.QPlainTextEdit):
96 class ConsoleWidget(QtGui.QPlainTextEdit):
97 """ Base class for console-type widgets. This class is mainly concerned with
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
98 dealing with the prompt, keeping the cursor inside the editing line, and
99 handling ANSI escape sequences.
99 handling ANSI escape sequences.
100 """
100 """
101
101
102 # Regex to match ANSI escape sequences
102 # Regex to match ANSI escape sequences
103 _ansi_pattern = re.compile('\x01?\x1b\[(.*?)m\x02?')
103 _ansi_pattern = re.compile('\x01?\x1b\[(.*?)m\x02?')
104
104
105 # When ctrl is pressed, map certain keys to other keys (without the ctrl):
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,
106 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
107 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
107 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
108 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
108 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
109 QtCore.Qt.Key_E : QtCore.Qt.Key_End,
109 QtCore.Qt.Key_E : QtCore.Qt.Key_End,
110 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
110 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
111 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
111 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
112 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
112 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
113
113
114 #---------------------------------------------------------------------------
114 #---------------------------------------------------------------------------
115 # 'QWidget' interface
115 # 'QWidget' interface
116 #---------------------------------------------------------------------------
116 #---------------------------------------------------------------------------
117
117
118 def __init__(self, parent=None):
118 def __init__(self, parent=None):
119 QtGui.QPlainTextEdit.__init__(self, parent)
119 QtGui.QPlainTextEdit.__init__(self, parent)
120
120
121 # Initialize public and protected variables
121 # Initialize public and protected variables
122 self.ansi_codes = True
122 self.ansi_codes = True
123 self.buffer_size = 500
123 self.continuation_prompt = '> '
124 self.continuation_prompt = '> '
124 self.gui_completion = True
125 self.gui_completion = True
125 self._ansi_processor = QtAnsiCodeProcessor()
126 self._ansi_processor = QtAnsiCodeProcessor()
126 self._completion_widget = CompletionWidget(self)
127 self._completion_widget = CompletionWidget(self)
127 self._executing = False
128 self._executing = False
128 self._prompt = ''
129 self._prompt = ''
129 self._prompt_pos = 0
130 self._prompt_pos = 0
130 self._reading = False
131 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
132
137 # Set a monospaced font
133 # Set a monospaced font
138 point_size = QtGui.QApplication.font().pointSize()
134 point_size = QtGui.QApplication.font().pointSize()
139 font = QtGui.QFont('Monospace', point_size)
135 font = QtGui.QFont('Monospace', point_size)
140 font.setStyleHint(QtGui.QFont.TypeWriter)
136 font.setStyleHint(QtGui.QFont.TypeWriter)
141 self._completion_widget.setFont(font)
137 self._completion_widget.setFont(font)
142 self.document().setDefaultFont(font)
138 self.document().setDefaultFont(font)
143
139
144 # Define a custom context menu
140 # Define a custom context menu
145 self._context_menu = QtGui.QMenu(self)
141 self._context_menu = QtGui.QMenu(self)
146
142
147 copy_action = QtGui.QAction('Copy', self)
143 copy_action = QtGui.QAction('Copy', self)
148 copy_action.triggered.connect(self.copy)
144 copy_action.triggered.connect(self.copy)
149 self.copyAvailable.connect(copy_action.setEnabled)
145 self.copyAvailable.connect(copy_action.setEnabled)
150 self._context_menu.addAction(copy_action)
146 self._context_menu.addAction(copy_action)
151
147
152 self._paste_action = QtGui.QAction('Paste', self)
148 self._paste_action = QtGui.QAction('Paste', self)
153 self._paste_action.triggered.connect(self.paste)
149 self._paste_action.triggered.connect(self.paste)
154 self._context_menu.addAction(self._paste_action)
150 self._context_menu.addAction(self._paste_action)
155 self._context_menu.addSeparator()
151 self._context_menu.addSeparator()
156
152
157 select_all_action = QtGui.QAction('Select All', self)
153 select_all_action = QtGui.QAction('Select All', self)
158 select_all_action.triggered.connect(self.selectAll)
154 select_all_action.triggered.connect(self.selectAll)
159 self._context_menu.addAction(select_all_action)
155 self._context_menu.addAction(select_all_action)
160
156
161 def contextMenuEvent(self, event):
157 def contextMenuEvent(self, event):
162 """ Reimplemented to create a menu without destructive actions like
158 """ Reimplemented to create a menu without destructive actions like
163 'Cut' and 'Delete'.
159 'Cut' and 'Delete'.
164 """
160 """
165 clipboard_empty = QtGui.QApplication.clipboard().text().isEmpty()
161 clipboard_empty = QtGui.QApplication.clipboard().text().isEmpty()
166 self._paste_action.setEnabled(not clipboard_empty)
162 self._paste_action.setEnabled(not clipboard_empty)
167
163
168 self._context_menu.exec_(event.globalPos())
164 self._context_menu.exec_(event.globalPos())
169
165
170 def keyPressEvent(self, event):
166 def keyPressEvent(self, event):
171 """ Reimplemented to create a console-like interface.
167 """ Reimplemented to create a console-like interface.
172 """
168 """
173 intercepted = False
169 intercepted = False
174 cursor = self.textCursor()
170 cursor = self.textCursor()
175 position = cursor.position()
171 position = cursor.position()
176 key = event.key()
172 key = event.key()
177 ctrl_down = event.modifiers() & QtCore.Qt.ControlModifier
173 ctrl_down = event.modifiers() & QtCore.Qt.ControlModifier
178 alt_down = event.modifiers() & QtCore.Qt.AltModifier
174 alt_down = event.modifiers() & QtCore.Qt.AltModifier
179 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
175 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
180
176
181 if ctrl_down:
177 # Even though we have reimplemented 'paste', the C++ level slot is still
178 # called by Qt. So we intercept the key press here.
179 if event.matches(QtGui.QKeySequence.Paste):
180 self.paste()
181 intercepted = True
182
183 elif ctrl_down:
182 if key in self._ctrl_down_remap:
184 if key in self._ctrl_down_remap:
183 ctrl_down = False
185 ctrl_down = False
184 key = self._ctrl_down_remap[key]
186 key = self._ctrl_down_remap[key]
185 event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, key,
187 event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, key,
186 QtCore.Qt.NoModifier)
188 QtCore.Qt.NoModifier)
187
189
188 elif key == QtCore.Qt.Key_K:
190 elif key == QtCore.Qt.Key_K:
189 if self._in_buffer(position):
191 if self._in_buffer(position):
190 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
192 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
191 QtGui.QTextCursor.KeepAnchor)
193 QtGui.QTextCursor.KeepAnchor)
192 cursor.removeSelectedText()
194 cursor.removeSelectedText()
193 intercepted = True
195 intercepted = True
194
196
195 elif key == QtCore.Qt.Key_Y:
197 elif key == QtCore.Qt.Key_Y:
196 self.paste()
198 self.paste()
197 intercepted = True
199 intercepted = True
198
200
199 elif alt_down:
201 elif alt_down:
200 if key == QtCore.Qt.Key_B:
202 if key == QtCore.Qt.Key_B:
201 self.setTextCursor(self._get_word_start_cursor(position))
203 self.setTextCursor(self._get_word_start_cursor(position))
202 intercepted = True
204 intercepted = True
203
205
204 elif key == QtCore.Qt.Key_F:
206 elif key == QtCore.Qt.Key_F:
205 self.setTextCursor(self._get_word_end_cursor(position))
207 self.setTextCursor(self._get_word_end_cursor(position))
206 intercepted = True
208 intercepted = True
207
209
208 elif key == QtCore.Qt.Key_Backspace:
210 elif key == QtCore.Qt.Key_Backspace:
209 cursor = self._get_word_start_cursor(position)
211 cursor = self._get_word_start_cursor(position)
210 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
212 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
211 cursor.removeSelectedText()
213 cursor.removeSelectedText()
212 intercepted = True
214 intercepted = True
213
215
214 elif key == QtCore.Qt.Key_D:
216 elif key == QtCore.Qt.Key_D:
215 cursor = self._get_word_end_cursor(position)
217 cursor = self._get_word_end_cursor(position)
216 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
218 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
217 cursor.removeSelectedText()
219 cursor.removeSelectedText()
218 intercepted = True
220 intercepted = True
219
221
220 if self._completion_widget.isVisible():
222 if self._completion_widget.isVisible():
221 self._completion_widget.keyPressEvent(event)
223 self._completion_widget.keyPressEvent(event)
222 intercepted = event.isAccepted()
224 intercepted = event.isAccepted()
223
225
224 else:
226 else:
225 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
227 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
226 if self._reading:
228 if self._reading:
227 self._reading = False
229 self._reading = False
228 elif not self._executing:
230 elif not self._executing:
229 self._executing = True
230 self.execute(interactive=True)
231 self.execute(interactive=True)
231 intercepted = True
232 intercepted = True
232
233
233 elif key == QtCore.Qt.Key_Up:
234 elif key == QtCore.Qt.Key_Up:
234 if self._reading or not self._up_pressed():
235 if self._reading or not self._up_pressed():
235 intercepted = True
236 intercepted = True
236 else:
237 else:
237 prompt_line = self._get_prompt_cursor().blockNumber()
238 prompt_line = self._get_prompt_cursor().blockNumber()
238 intercepted = cursor.blockNumber() <= prompt_line
239 intercepted = cursor.blockNumber() <= prompt_line
239
240
240 elif key == QtCore.Qt.Key_Down:
241 elif key == QtCore.Qt.Key_Down:
241 if self._reading or not self._down_pressed():
242 if self._reading or not self._down_pressed():
242 intercepted = True
243 intercepted = True
243 else:
244 else:
244 end_line = self._get_end_cursor().blockNumber()
245 end_line = self._get_end_cursor().blockNumber()
245 intercepted = cursor.blockNumber() == end_line
246 intercepted = cursor.blockNumber() == end_line
246
247
247 elif key == QtCore.Qt.Key_Tab:
248 elif key == QtCore.Qt.Key_Tab:
248 if self._reading:
249 if self._reading:
249 intercepted = False
250 intercepted = False
250 else:
251 else:
251 intercepted = not self._tab_pressed()
252 intercepted = not self._tab_pressed()
252
253
253 elif key == QtCore.Qt.Key_Left:
254 elif key == QtCore.Qt.Key_Left:
254 intercepted = not self._in_buffer(position - 1)
255 intercepted = not self._in_buffer(position - 1)
255
256
256 elif key == QtCore.Qt.Key_Home:
257 elif key == QtCore.Qt.Key_Home:
257 cursor.movePosition(QtGui.QTextCursor.StartOfLine)
258 cursor.movePosition(QtGui.QTextCursor.StartOfLine)
258 start_pos = cursor.position()
259 start_pos = cursor.position()
259 start_line = cursor.blockNumber()
260 start_line = cursor.blockNumber()
260 if start_line == self._get_prompt_cursor().blockNumber():
261 if start_line == self._get_prompt_cursor().blockNumber():
261 start_pos += len(self._prompt)
262 start_pos += len(self._prompt)
262 else:
263 else:
263 start_pos += len(self.continuation_prompt)
264 start_pos += len(self.continuation_prompt)
264 if shift_down and self._in_buffer(position):
265 if shift_down and self._in_buffer(position):
265 self._set_selection(position, start_pos)
266 self._set_selection(position, start_pos)
266 else:
267 else:
267 self._set_position(start_pos)
268 self._set_position(start_pos)
268 intercepted = True
269 intercepted = True
269
270
270 elif key == QtCore.Qt.Key_Backspace and not alt_down:
271 elif key == QtCore.Qt.Key_Backspace and not alt_down:
271
272
272 # Line deletion (remove continuation prompt)
273 # Line deletion (remove continuation prompt)
273 len_prompt = len(self.continuation_prompt)
274 len_prompt = len(self.continuation_prompt)
274 if cursor.columnNumber() == len_prompt and \
275 if cursor.columnNumber() == len_prompt and \
275 position != self._prompt_pos:
276 position != self._prompt_pos:
276 cursor.setPosition(position - len_prompt,
277 cursor.setPosition(position - len_prompt,
277 QtGui.QTextCursor.KeepAnchor)
278 QtGui.QTextCursor.KeepAnchor)
278 cursor.removeSelectedText()
279 cursor.removeSelectedText()
279
280
280 # Regular backwards deletion
281 # Regular backwards deletion
281 else:
282 else:
282 anchor = cursor.anchor()
283 anchor = cursor.anchor()
283 if anchor == position:
284 if anchor == position:
284 intercepted = not self._in_buffer(position - 1)
285 intercepted = not self._in_buffer(position - 1)
285 else:
286 else:
286 intercepted = not self._in_buffer(min(anchor, position))
287 intercepted = not self._in_buffer(min(anchor, position))
287
288
288 elif key == QtCore.Qt.Key_Delete:
289 elif key == QtCore.Qt.Key_Delete:
289 anchor = cursor.anchor()
290 anchor = cursor.anchor()
290 intercepted = not self._in_buffer(min(anchor, position))
291 intercepted = not self._in_buffer(min(anchor, position))
291
292
292 # Don't move cursor if control is down to allow copy-paste using
293 # Don't move cursor if control is down to allow copy-paste using
293 # the keyboard in any part of the buffer
294 # the keyboard in any part of the buffer.
294 if not ctrl_down:
295 if not ctrl_down:
295 self._keep_cursor_in_buffer()
296 self._keep_cursor_in_buffer()
296
297
297 if not intercepted:
298 if not intercepted:
298 QtGui.QPlainTextEdit.keyPressEvent(self, event)
299 QtGui.QPlainTextEdit.keyPressEvent(self, event)
299
300
300 #--------------------------------------------------------------------------
301 #--------------------------------------------------------------------------
301 # 'QPlainTextEdit' interface
302 # 'QPlainTextEdit' interface
302 #--------------------------------------------------------------------------
303 #--------------------------------------------------------------------------
303
304
304 def appendPlainText(self, text):
305 def appendPlainText(self, text):
305 """ Reimplemented to not append text as a new paragraph, which doesn't
306 """ 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 make sense for a console widget. Also, if enabled, handle ANSI
307 codes.
308 codes.
308 """
309 """
309 cursor = self.textCursor()
310 cursor = self.textCursor()
310 cursor.movePosition(QtGui.QTextCursor.End)
311 cursor.movePosition(QtGui.QTextCursor.End)
311
312
312 if self.ansi_codes:
313 if self.ansi_codes:
313 format = QtGui.QTextCharFormat()
314 format = QtGui.QTextCharFormat()
314 previous_end = 0
315 previous_end = 0
315 for match in self._ansi_pattern.finditer(text):
316 for match in self._ansi_pattern.finditer(text):
316 cursor.insertText(text[previous_end:match.start()], format)
317 cursor.insertText(text[previous_end:match.start()], format)
317 previous_end = match.end()
318 previous_end = match.end()
318 for code in match.group(1).split(';'):
319 for code in match.group(1).split(';'):
319 self._ansi_processor.set_code(int(code))
320 self._ansi_processor.set_code(int(code))
320 format = self._ansi_processor.get_format()
321 format = self._ansi_processor.get_format()
321 cursor.insertText(text[previous_end:], format)
322 cursor.insertText(text[previous_end:], format)
322 else:
323 else:
323 cursor.insertText(text)
324 cursor.insertText(text)
324
325
325 def paste(self):
326 def paste(self):
326 """ Reimplemented to ensure that text is pasted in the editing region.
327 """ Reimplemented to ensure that text is pasted in the editing region.
327 """
328 """
328 self._keep_cursor_in_buffer()
329 self._keep_cursor_in_buffer()
329 QtGui.QPlainTextEdit.paste(self)
330 QtGui.QPlainTextEdit.paste(self)
330
331
331 #---------------------------------------------------------------------------
332 #---------------------------------------------------------------------------
332 # 'ConsoleWidget' public interface
333 # 'ConsoleWidget' public interface
333 #---------------------------------------------------------------------------
334 #---------------------------------------------------------------------------
334
335
335 def execute(self, interactive=False):
336 def execute(self, interactive=False):
336 """ Execute the text in the input buffer. Returns whether the input
337 """ Execute the text in the input buffer. Returns whether the input
337 buffer was completely processed and a new prompt created.
338 buffer was completely processed and a new prompt created.
338 """
339 """
339 self.appendPlainText('\n')
340 self.appendPlainText('\n')
341 self._executing_input_buffer = self.input_buffer
342 self._executing = True
340 self._prompt_finished()
343 self._prompt_finished()
341 return self._execute(interactive=interactive)
344 return self._execute(interactive=interactive)
342
345
343 def _get_input_buffer(self):
346 def _get_input_buffer(self):
347 # If we're executing, the input buffer may not even exist anymore due
348 # the limit imposed by 'buffer_size'. Therefore, we store it.
349 if self._executing:
350 return self._executing_input_buffer
351
344 cursor = self._get_end_cursor()
352 cursor = self._get_end_cursor()
345 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
353 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
346
354
347 # Use QTextDocumentFragment intermediate object because it strips
355 # Use QTextDocumentFragment intermediate object because it strips
348 # out the Unicode line break characters that Qt insists on inserting.
356 # out the Unicode line break characters that Qt insists on inserting.
349 input_buffer = str(cursor.selection().toPlainText())
357 input_buffer = str(cursor.selection().toPlainText())
350
358
351 # Strip out continuation prompts
359 # Strip out continuation prompts
352 return input_buffer.replace('\n' + self.continuation_prompt, '\n')
360 return input_buffer.replace('\n' + self.continuation_prompt, '\n')
353
361
354 def _set_input_buffer(self, string):
362 def _set_input_buffer(self, string):
355 # Add continuation prompts where necessary
363 # Add continuation prompts where necessary
356 lines = string.splitlines()
364 lines = string.splitlines()
357 for i in xrange(1, len(lines)):
365 for i in xrange(1, len(lines)):
358 lines[i] = self.continuation_prompt + lines[i]
366 lines[i] = self.continuation_prompt + lines[i]
359 string = '\n'.join(lines)
367 string = '\n'.join(lines)
360
368
361 # Replace buffer with new text
369 # Replace buffer with new text
362 cursor = self._get_end_cursor()
370 cursor = self._get_end_cursor()
363 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
371 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
364 cursor.insertText(string)
372 cursor.insertText(string)
365 self.moveCursor(QtGui.QTextCursor.End)
373 self.moveCursor(QtGui.QTextCursor.End)
366
374
367 input_buffer = property(_get_input_buffer, _set_input_buffer)
375 input_buffer = property(_get_input_buffer, _set_input_buffer)
368
376
369 def _get_input_buffer_cursor_line(self):
377 def _get_input_buffer_cursor_line(self):
378 if self._executing:
379 return None
370 cursor = self.textCursor()
380 cursor = self.textCursor()
371 if cursor.position() >= self._prompt_pos:
381 if cursor.position() >= self._prompt_pos:
372 text = str(cursor.block().text())
382 text = str(cursor.block().text())
373 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
383 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
374 return text[len(self._prompt):]
384 return text[len(self._prompt):]
375 else:
385 else:
376 return text[len(self.continuation_prompt):]
386 return text[len(self.continuation_prompt):]
377 else:
387 else:
378 return None
388 return None
379
389
380 input_buffer_cursor_line = property(_get_input_buffer_cursor_line)
390 input_buffer_cursor_line = property(_get_input_buffer_cursor_line)
381
391
382 #---------------------------------------------------------------------------
392 #---------------------------------------------------------------------------
383 # 'ConsoleWidget' abstract interface
393 # 'ConsoleWidget' abstract interface
384 #---------------------------------------------------------------------------
394 #---------------------------------------------------------------------------
385
395
386 def _execute(self, interactive):
396 def _execute(self, interactive):
387 """ Called to execute the input buffer. When triggered by an the enter
397 """ Called to execute the input buffer. When triggered by an the enter
388 key press, 'interactive' is True; otherwise, it is False. Returns
398 key press, 'interactive' is True; otherwise, it is False. Returns
389 whether the input buffer was completely processed and a new prompt
399 whether the input buffer was completely processed and a new prompt
390 created.
400 created.
391 """
401 """
392 raise NotImplementedError
402 raise NotImplementedError
393
403
394 def _prompt_started_hook(self):
404 def _prompt_started_hook(self):
395 """ Called immediately after a new prompt is displayed.
405 """ Called immediately after a new prompt is displayed.
396 """
406 """
397 pass
407 pass
398
408
399 def _prompt_finished_hook(self):
409 def _prompt_finished_hook(self):
400 """ Called immediately after a prompt is finished, i.e. when some input
410 """ Called immediately after a prompt is finished, i.e. when some input
401 will be processed and a new prompt displayed.
411 will be processed and a new prompt displayed.
402 """
412 """
403 pass
413 pass
404
414
405 def _up_pressed(self):
415 def _up_pressed(self):
406 """ Called when the up key is pressed. Returns whether to continue
416 """ Called when the up key is pressed. Returns whether to continue
407 processing the event.
417 processing the event.
408 """
418 """
409 return True
419 return True
410
420
411 def _down_pressed(self):
421 def _down_pressed(self):
412 """ Called when the down key is pressed. Returns whether to continue
422 """ Called when the down key is pressed. Returns whether to continue
413 processing the event.
423 processing the event.
414 """
424 """
415 return True
425 return True
416
426
417 def _tab_pressed(self):
427 def _tab_pressed(self):
418 """ Called when the tab key is pressed. Returns whether to continue
428 """ Called when the tab key is pressed. Returns whether to continue
419 processing the event.
429 processing the event.
420 """
430 """
421 return False
431 return False
422
432
423 #--------------------------------------------------------------------------
433 #--------------------------------------------------------------------------
424 # 'ConsoleWidget' protected interface
434 # 'ConsoleWidget' protected interface
425 #--------------------------------------------------------------------------
435 #--------------------------------------------------------------------------
426
436
427 def _complete_with_items(self, cursor, items):
437 def _complete_with_items(self, cursor, items):
428 """ Performs completion with 'items' at the specified cursor location.
438 """ Performs completion with 'items' at the specified cursor location.
429 """
439 """
430 if len(items) == 1:
440 if len(items) == 1:
431 cursor.setPosition(self.textCursor().position(),
441 cursor.setPosition(self.textCursor().position(),
432 QtGui.QTextCursor.KeepAnchor)
442 QtGui.QTextCursor.KeepAnchor)
433 cursor.insertText(items[0])
443 cursor.insertText(items[0])
434 elif len(items) > 1:
444 elif len(items) > 1:
435 if self.gui_completion:
445 if self.gui_completion:
436 self._completion_widget.show_items(cursor, items)
446 self._completion_widget.show_items(cursor, items)
437 else:
447 else:
438 text = '\n'.join(items) + '\n'
448 text = '\n'.join(items) + '\n'
439 self._write_text_keeping_prompt(text)
449 self._write_text_keeping_prompt(text)
440
450
441 def _get_end_cursor(self):
451 def _get_end_cursor(self):
442 """ Convenience method that returns a cursor for the last character.
452 """ Convenience method that returns a cursor for the last character.
443 """
453 """
444 cursor = self.textCursor()
454 cursor = self.textCursor()
445 cursor.movePosition(QtGui.QTextCursor.End)
455 cursor.movePosition(QtGui.QTextCursor.End)
446 return cursor
456 return cursor
447
457
448 def _get_prompt_cursor(self):
458 def _get_prompt_cursor(self):
449 """ Convenience method that returns a cursor for the prompt position.
459 """ Convenience method that returns a cursor for the prompt position.
450 """
460 """
451 cursor = self.textCursor()
461 cursor = self.textCursor()
452 cursor.setPosition(self._prompt_pos)
462 cursor.setPosition(self._prompt_pos)
453 return cursor
463 return cursor
454
464
455 def _get_selection_cursor(self, start, end):
465 def _get_selection_cursor(self, start, end):
456 """ Convenience method that returns a cursor with text selected between
466 """ Convenience method that returns a cursor with text selected between
457 the positions 'start' and 'end'.
467 the positions 'start' and 'end'.
458 """
468 """
459 cursor = self.textCursor()
469 cursor = self.textCursor()
460 cursor.setPosition(start)
470 cursor.setPosition(start)
461 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
471 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
462 return cursor
472 return cursor
463
473
464 def _get_word_start_cursor(self, position):
474 def _get_word_start_cursor(self, position):
465 """ Find the start of the word to the left the given position. If a
475 """ 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
476 sequence of non-word characters precedes the first word, skip over
467 them. (This emulates the behavior of bash, emacs, etc.)
477 them. (This emulates the behavior of bash, emacs, etc.)
468 """
478 """
469 document = self.document()
479 document = self.document()
470 position -= 1
480 position -= 1
471 while self._in_buffer(position) and \
481 while self._in_buffer(position) and \
472 not document.characterAt(position).isLetterOrNumber():
482 not document.characterAt(position).isLetterOrNumber():
473 position -= 1
483 position -= 1
474 while self._in_buffer(position) and \
484 while self._in_buffer(position) and \
475 document.characterAt(position).isLetterOrNumber():
485 document.characterAt(position).isLetterOrNumber():
476 position -= 1
486 position -= 1
477 cursor = self.textCursor()
487 cursor = self.textCursor()
478 cursor.setPosition(position + 1)
488 cursor.setPosition(position + 1)
479 return cursor
489 return cursor
480
490
481 def _get_word_end_cursor(self, position):
491 def _get_word_end_cursor(self, position):
482 """ Find the end of the word to the right the given position. If a
492 """ 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
493 sequence of non-word characters precedes the first word, skip over
484 them. (This emulates the behavior of bash, emacs, etc.)
494 them. (This emulates the behavior of bash, emacs, etc.)
485 """
495 """
486 document = self.document()
496 document = self.document()
487 end = self._get_end_cursor().position()
497 end = self._get_end_cursor().position()
488 while position < end and \
498 while position < end and \
489 not document.characterAt(position).isLetterOrNumber():
499 not document.characterAt(position).isLetterOrNumber():
490 position += 1
500 position += 1
491 while position < end and \
501 while position < end and \
492 document.characterAt(position).isLetterOrNumber():
502 document.characterAt(position).isLetterOrNumber():
493 position += 1
503 position += 1
494 cursor = self.textCursor()
504 cursor = self.textCursor()
495 cursor.setPosition(position)
505 cursor.setPosition(position)
496 return cursor
506 return cursor
497
507
498 def _prompt_started(self):
508 def _prompt_started(self):
499 """ Called immediately after a new prompt is displayed.
509 """ Called immediately after a new prompt is displayed.
500 """
510 """
511 # Temporarily disable the maximum block count to permit undo/redo.
512 self.setMaximumBlockCount(0)
513 self.setUndoRedoEnabled(True)
514
515 self.setReadOnly(False)
501 self.moveCursor(QtGui.QTextCursor.End)
516 self.moveCursor(QtGui.QTextCursor.End)
502 self.centerCursor()
517 self.centerCursor()
503 self.setReadOnly(False)
518
504 self._executing = False
519 self._executing = False
505 self._prompt_started_hook()
520 self._prompt_started_hook()
506
521
507 def _prompt_finished(self):
522 def _prompt_finished(self):
508 """ Called immediately after a prompt is finished, i.e. when some input
523 """ Called immediately after a prompt is finished, i.e. when some input
509 will be processed and a new prompt displayed.
524 will be processed and a new prompt displayed.
510 """
525 """
526 # This has the (desired) side effect of disabling the undo/redo history.
527 self.setMaximumBlockCount(self.buffer_size)
528
511 self.setReadOnly(True)
529 self.setReadOnly(True)
512 self._prompt_finished_hook()
530 self._prompt_finished_hook()
513
531
514 def _set_position(self, position):
532 def _set_position(self, position):
515 """ Convenience method to set the position of the cursor.
533 """ Convenience method to set the position of the cursor.
516 """
534 """
517 cursor = self.textCursor()
535 cursor = self.textCursor()
518 cursor.setPosition(position)
536 cursor.setPosition(position)
519 self.setTextCursor(cursor)
537 self.setTextCursor(cursor)
520
538
521 def _set_selection(self, start, end):
539 def _set_selection(self, start, end):
522 """ Convenience method to set the current selected text.
540 """ Convenience method to set the current selected text.
523 """
541 """
524 self.setTextCursor(self._get_selection_cursor(start, end))
542 self.setTextCursor(self._get_selection_cursor(start, end))
525
543
526 def _show_prompt(self, prompt):
544 def _show_prompt(self, prompt):
527 """ Writes a new prompt at the end of the buffer.
545 """ Writes a new prompt at the end of the buffer.
528 """
546 """
529 self.appendPlainText('\n' + prompt)
547 self.appendPlainText('\n' + prompt)
530 self._prompt = prompt
548 self._prompt = prompt
531 self._prompt_pos = self._get_end_cursor().position()
549 self._prompt_pos = self._get_end_cursor().position()
532 self._prompt_started()
550 self._prompt_started()
533
551
534 def _show_continuation_prompt(self):
552 def _show_continuation_prompt(self):
535 """ Writes a new continuation prompt at the end of the buffer.
553 """ Writes a new continuation prompt at the end of the buffer.
536 """
554 """
537 self.appendPlainText(self.continuation_prompt)
555 self.appendPlainText(self.continuation_prompt)
538 self._prompt_started()
556 self._prompt_started()
539
557
540 def _write_text_keeping_prompt(self, text):
558 def _write_text_keeping_prompt(self, text):
541 """ Writes 'text' after the current prompt, then restores the old prompt
559 """ Writes 'text' after the current prompt, then restores the old prompt
542 with its old input buffer.
560 with its old input buffer.
543 """
561 """
544 input_buffer = self.input_buffer
562 input_buffer = self.input_buffer
545 self.appendPlainText('\n')
563 self.appendPlainText('\n')
546 self._prompt_finished()
564 self._prompt_finished()
547
565
548 self.appendPlainText(text)
566 self.appendPlainText(text)
549 self._show_prompt(self._prompt)
567 self._show_prompt(self._prompt)
550 self.input_buffer = input_buffer
568 self.input_buffer = input_buffer
551
569
552 def _in_buffer(self, position):
570 def _in_buffer(self, position):
553 """ Returns whether the given position is inside the editing region.
571 """ Returns whether the given position is inside the editing region.
554 """
572 """
555 return position >= self._prompt_pos
573 return position >= self._prompt_pos
556
574
557 def _keep_cursor_in_buffer(self):
575 def _keep_cursor_in_buffer(self):
558 """ Ensures that the cursor is inside the editing region. Returns
576 """ Ensures that the cursor is inside the editing region. Returns
559 whether the cursor was moved.
577 whether the cursor was moved.
560 """
578 """
561 cursor = self.textCursor()
579 cursor = self.textCursor()
562 if cursor.position() < self._prompt_pos:
580 if cursor.position() < self._prompt_pos:
563 cursor.movePosition(QtGui.QTextCursor.End)
581 cursor.movePosition(QtGui.QTextCursor.End)
564 self.setTextCursor(cursor)
582 self.setTextCursor(cursor)
565 return True
583 return True
566 else:
584 else:
567 return False
585 return False
568
586
569
587
570 class HistoryConsoleWidget(ConsoleWidget):
588 class HistoryConsoleWidget(ConsoleWidget):
571 """ A ConsoleWidget that keeps a history of the commands that have been
589 """ A ConsoleWidget that keeps a history of the commands that have been
572 executed.
590 executed.
573 """
591 """
574
592
575 #---------------------------------------------------------------------------
593 #---------------------------------------------------------------------------
576 # 'QWidget' interface
594 # 'QWidget' interface
577 #---------------------------------------------------------------------------
595 #---------------------------------------------------------------------------
578
596
579 def __init__(self, parent=None):
597 def __init__(self, parent=None):
580 super(HistoryConsoleWidget, self).__init__(parent)
598 super(HistoryConsoleWidget, self).__init__(parent)
581
599
582 self._history = []
600 self._history = []
583 self._history_index = 0
601 self._history_index = 0
584
602
585 #---------------------------------------------------------------------------
603 #---------------------------------------------------------------------------
586 # 'ConsoleWidget' public interface
604 # 'ConsoleWidget' public interface
587 #---------------------------------------------------------------------------
605 #---------------------------------------------------------------------------
588
606
589 def execute(self, interactive=False):
607 def execute(self, interactive=False):
590 """ Reimplemented to the store history.
608 """ Reimplemented to the store history.
591 """
609 """
592 stripped = self.input_buffer.rstrip()
610 stripped = self.input_buffer.rstrip()
593 executed = super(HistoryConsoleWidget, self).execute(interactive)
611 executed = super(HistoryConsoleWidget, self).execute(interactive)
594 if executed:
612 if executed:
595 self._history.append(stripped)
613 self._history.append(stripped)
596 self._history_index = len(self._history)
614 self._history_index = len(self._history)
597 return executed
615 return executed
598
616
599 #---------------------------------------------------------------------------
617 #---------------------------------------------------------------------------
600 # 'ConsoleWidget' abstract interface
618 # 'ConsoleWidget' abstract interface
601 #---------------------------------------------------------------------------
619 #---------------------------------------------------------------------------
602
620
603 def _up_pressed(self):
621 def _up_pressed(self):
604 """ Called when the up key is pressed. Returns whether to continue
622 """ Called when the up key is pressed. Returns whether to continue
605 processing the event.
623 processing the event.
606 """
624 """
607 prompt_cursor = self._get_prompt_cursor()
625 prompt_cursor = self._get_prompt_cursor()
608 if self.textCursor().blockNumber() == prompt_cursor.blockNumber():
626 if self.textCursor().blockNumber() == prompt_cursor.blockNumber():
609 self.history_previous()
627 self.history_previous()
610
628
611 # Go to the first line of prompt for seemless history scrolling.
629 # Go to the first line of prompt for seemless history scrolling.
612 cursor = self._get_prompt_cursor()
630 cursor = self._get_prompt_cursor()
613 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
631 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
614 self.setTextCursor(cursor)
632 self.setTextCursor(cursor)
615
633
616 return False
634 return False
617 return True
635 return True
618
636
619 def _down_pressed(self):
637 def _down_pressed(self):
620 """ Called when the down key is pressed. Returns whether to continue
638 """ Called when the down key is pressed. Returns whether to continue
621 processing the event.
639 processing the event.
622 """
640 """
623 end_cursor = self._get_end_cursor()
641 end_cursor = self._get_end_cursor()
624 if self.textCursor().blockNumber() == end_cursor.blockNumber():
642 if self.textCursor().blockNumber() == end_cursor.blockNumber():
625 self.history_next()
643 self.history_next()
626 return False
644 return False
627 return True
645 return True
628
646
629 #---------------------------------------------------------------------------
647 #---------------------------------------------------------------------------
630 # 'HistoryConsoleWidget' interface
648 # 'HistoryConsoleWidget' interface
631 #---------------------------------------------------------------------------
649 #---------------------------------------------------------------------------
632
650
633 def history_previous(self):
651 def history_previous(self):
634 """ If possible, set the input buffer to the previous item in the
652 """ If possible, set the input buffer to the previous item in the
635 history.
653 history.
636 """
654 """
637 if self._history_index > 0:
655 if self._history_index > 0:
638 self._history_index -= 1
656 self._history_index -= 1
639 self.input_buffer = self._history[self._history_index]
657 self.input_buffer = self._history[self._history_index]
640
658
641 def history_next(self):
659 def history_next(self):
642 """ Set the input buffer to the next item in the history, or a blank
660 """ Set the input buffer to the next item in the history, or a blank
643 line if there is no subsequent item.
661 line if there is no subsequent item.
644 """
662 """
645 if self._history_index < len(self._history):
663 if self._history_index < len(self._history):
646 self._history_index += 1
664 self._history_index += 1
647 if self._history_index < len(self._history):
665 if self._history_index < len(self._history):
648 self.input_buffer = self._history[self._history_index]
666 self.input_buffer = self._history[self._history_index]
649 else:
667 else:
650 self.input_buffer = ''
668 self.input_buffer = ''
General Comments 0
You need to be logged in to leave comments. Login now