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