##// END OF EJS Templates
* Fixed context menu breakge after previous commit....
epatters -
Show More
@@ -1,1052 +1,1047 b''
1 1 # Standard library imports
2 2 import sys
3 3
4 4 # System library imports
5 5 from PyQt4 import QtCore, QtGui
6 6
7 7 # Local imports
8 8 from ansi_code_processor import QtAnsiCodeProcessor
9 9 from completion_widget import CompletionWidget
10 10
11 11
12 12 class ConsoleWidget(QtGui.QWidget):
13 13 """ Base class for console-type widgets. This class is mainly concerned with
14 14 dealing with the prompt, keeping the cursor inside the editing line, and
15 15 handling ANSI escape sequences.
16 16 """
17 17
18 18 # Whether to process ANSI escape codes.
19 19 ansi_codes = True
20 20
21 21 # The maximum number of lines of text before truncation.
22 22 buffer_size = 500
23 23
24 24 # Whether to use a CompletionWidget or plain text output for tab completion.
25 25 gui_completion = True
26 26
27 27 # Whether to override ShortcutEvents for the keybindings defined by this
28 28 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
29 29 # priority (when it has focus) over, e.g., window-level menu shortcuts.
30 30 override_shortcuts = False
31 31
32 32 # Signals that indicate ConsoleWidget state.
33 33 copy_available = QtCore.pyqtSignal(bool)
34 34 redo_available = QtCore.pyqtSignal(bool)
35 35 undo_available = QtCore.pyqtSignal(bool)
36 36
37 37 # Protected class variables.
38 38 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
39 39 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
40 40 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
41 41 QtCore.Qt.Key_E : QtCore.Qt.Key_End,
42 42 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
43 43 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
44 44 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
45 45 _shortcuts = set(_ctrl_down_remap.keys() +
46 46 [ QtCore.Qt.Key_C, QtCore.Qt.Key_V ])
47 47
48 48 #---------------------------------------------------------------------------
49 49 # 'QObject' interface
50 50 #---------------------------------------------------------------------------
51 51
52 52 def __init__(self, kind='plain', parent=None):
53 53 """ Create a ConsoleWidget.
54 54
55 55 Parameters
56 56 ----------
57 57 kind : str, optional [default 'plain']
58 58 The type of text widget to use. Valid values are 'plain', which
59 59 specifies a QPlainTextEdit, and 'rich', which specifies an
60 60 QTextEdit.
61 61
62 62 parent : QWidget, optional [default None]
63 63 The parent for this widget.
64 64 """
65 65 super(ConsoleWidget, self).__init__(parent)
66 66
67 67 # Create the underlying text widget.
68 68 self._control = self._create_control(kind)
69 69
70 70 # Initialize protected variables. Some variables contain useful state
71 71 # information for subclasses; they should be considered read-only.
72 72 self._ansi_processor = QtAnsiCodeProcessor()
73 73 self._completion_widget = CompletionWidget(self._control)
74 74 self._continuation_prompt = '> '
75 75 self._continuation_prompt_html = None
76 76 self._executing = False
77 77 self._prompt = ''
78 78 self._prompt_html = None
79 79 self._prompt_pos = 0
80 80 self._reading = False
81 81 self._reading_callback = None
82 82 self._tab_width = 8
83 83
84 # Define a custom context menu.
85 self._context_menu = self._create_context_menu()
86
87 84 # Set a monospaced font.
88 85 self.reset_font()
89 86
90 87 def eventFilter(self, obj, event):
91 88 """ Reimplemented to ensure a console-like behavior in the underlying
92 89 text widget.
93 90 """
94 91 if obj == self._control:
95 92 etype = event.type()
96 93
97 # Override the default context menu with one that does not have
98 # destructive actions.
99 if etype == QtCore.QEvent.ContextMenu:
100 self._context_menu.exec_(event.globalPos())
101 return True
102
103 94 # Disable moving text by drag and drop.
104 elif etype == QtCore.QEvent.DragMove:
95 if etype == QtCore.QEvent.DragMove:
105 96 return True
106 97
107 98 elif etype == QtCore.QEvent.KeyPress:
108 99 return self._event_filter_keypress(event)
109 100
110 101 # On Mac OS, it is always unnecessary to override shortcuts, hence
111 102 # the check below. Users should just use the Control key instead of
112 103 # the Command key.
113 104 elif etype == QtCore.QEvent.ShortcutOverride:
114 105 if sys.platform != 'darwin' and \
115 106 self._control_key_down(event.modifiers()) and \
116 107 event.key() in self._shortcuts:
117 108 event.accept()
118 109 return False
119 110
120 111 return super(ConsoleWidget, self).eventFilter(obj, event)
121 112
122 113 #---------------------------------------------------------------------------
123 114 # 'ConsoleWidget' public interface
124 115 #---------------------------------------------------------------------------
125 116
126 117 def clear(self, keep_input=False):
127 118 """ Clear the console, then write a new prompt. If 'keep_input' is set,
128 119 restores the old input buffer when the new prompt is written.
129 120 """
130 121 self._control.clear()
131 122 if keep_input:
132 123 input_buffer = self.input_buffer
133 124 self._show_prompt()
134 125 if keep_input:
135 126 self.input_buffer = input_buffer
136 127
137 128 def copy(self):
138 129 """ Copy the current selected text to the clipboard.
139 130 """
140 131 self._control.copy()
141 132
142 133 def execute(self, source=None, hidden=False, interactive=False):
143 134 """ Executes source or the input buffer, possibly prompting for more
144 135 input.
145 136
146 137 Parameters:
147 138 -----------
148 139 source : str, optional
149 140
150 141 The source to execute. If not specified, the input buffer will be
151 142 used. If specified and 'hidden' is False, the input buffer will be
152 143 replaced with the source before execution.
153 144
154 145 hidden : bool, optional (default False)
155 146
156 147 If set, no output will be shown and the prompt will not be modified.
157 148 In other words, it will be completely invisible to the user that
158 149 an execution has occurred.
159 150
160 151 interactive : bool, optional (default False)
161 152
162 153 Whether the console is to treat the source as having been manually
163 154 entered by the user. The effect of this parameter depends on the
164 155 subclass implementation.
165 156
166 157 Raises:
167 158 -------
168 159 RuntimeError
169 160 If incomplete input is given and 'hidden' is True. In this case,
170 161 it not possible to prompt for more input.
171 162
172 163 Returns:
173 164 --------
174 165 A boolean indicating whether the source was executed.
175 166 """
176 167 if not hidden:
177 168 if source is not None:
178 169 self.input_buffer = source
179 170
180 171 self._append_plain_text('\n')
181 172 self._executing_input_buffer = self.input_buffer
182 173 self._executing = True
183 174 self._prompt_finished()
184 175
185 176 real_source = self.input_buffer if source is None else source
186 177 complete = self._is_complete(real_source, interactive)
187 178 if complete:
188 179 if not hidden:
189 180 # The maximum block count is only in effect during execution.
190 181 # This ensures that _prompt_pos does not become invalid due to
191 182 # text truncation.
192 183 self._control.document().setMaximumBlockCount(self.buffer_size)
193 184 self._execute(real_source, hidden)
194 185 elif hidden:
195 186 raise RuntimeError('Incomplete noninteractive input: "%s"' % source)
196 187 else:
197 188 self._show_continuation_prompt()
198 189
199 190 return complete
200 191
201 192 def _get_input_buffer(self):
202 193 """ The text that the user has entered entered at the current prompt.
203 194 """
204 195 # If we're executing, the input buffer may not even exist anymore due to
205 196 # the limit imposed by 'buffer_size'. Therefore, we store it.
206 197 if self._executing:
207 198 return self._executing_input_buffer
208 199
209 200 cursor = self._get_end_cursor()
210 201 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
211 202 input_buffer = str(cursor.selection().toPlainText())
212 203
213 204 # Strip out continuation prompts.
214 205 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
215 206
216 207 def _set_input_buffer(self, string):
217 208 """ Replaces the text in the input buffer with 'string'.
218 209 """
219 210 # Remove old text.
220 211 cursor = self._get_end_cursor()
221 212 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
222 213 cursor.removeSelectedText()
223 214
224 215 # Insert new text with continuation prompts.
225 216 lines = string.splitlines(True)
226 217 if lines:
227 218 self._append_plain_text(lines[0])
228 219 for i in xrange(1, len(lines)):
229 220 if self._continuation_prompt_html is None:
230 221 self._append_plain_text(self._continuation_prompt)
231 222 else:
232 223 self._append_html(self._continuation_prompt_html)
233 224 self._append_plain_text(lines[i])
234 225 self._control.moveCursor(QtGui.QTextCursor.End)
235 226
236 227 input_buffer = property(_get_input_buffer, _set_input_buffer)
237 228
238 229 def _get_input_buffer_cursor_line(self):
239 230 """ The text in the line of the input buffer in which the user's cursor
240 231 rests. Returns a string if there is such a line; otherwise, None.
241 232 """
242 233 if self._executing:
243 234 return None
244 235 cursor = self._control.textCursor()
245 236 if cursor.position() >= self._prompt_pos:
246 237 text = self._get_block_plain_text(cursor.block())
247 238 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
248 239 return text[len(self._prompt):]
249 240 else:
250 241 return text[len(self._continuation_prompt):]
251 242 else:
252 243 return None
253 244
254 245 input_buffer_cursor_line = property(_get_input_buffer_cursor_line)
255 246
256 247 def _get_font(self):
257 248 """ The base font being used by the ConsoleWidget.
258 249 """
259 250 return self._control.document().defaultFont()
260 251
261 252 def _set_font(self, font):
262 253 """ Sets the base font for the ConsoleWidget to the specified QFont.
263 254 """
264 255 font_metrics = QtGui.QFontMetrics(font)
265 256 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
266 257
267 258 self._completion_widget.setFont(font)
268 259 self._control.document().setDefaultFont(font)
269 260
270 261 font = property(_get_font, _set_font)
271 262
272 263 def paste(self):
273 264 """ Paste the contents of the clipboard into the input region.
274 265 """
275 266 self._keep_cursor_in_buffer()
276 267 self._control.paste()
277 268
278 269 def print_(self, printer):
279 270 """ Print the contents of the ConsoleWidget to the specified QPrinter.
280 271 """
281 272 self._control.print_(printer)
282 273
283 274 def redo(self):
284 275 """ Redo the last operation. If there is no operation to redo, nothing
285 276 happens.
286 277 """
287 278 self._control.redo()
288 279
289 280 def reset_font(self):
290 281 """ Sets the font to the default fixed-width font for this platform.
291 282 """
292 283 if sys.platform == 'win32':
293 284 name = 'Courier'
294 285 elif sys.platform == 'darwin':
295 286 name = 'Monaco'
296 287 else:
297 288 name = 'Monospace'
298 289 font = QtGui.QFont(name, QtGui.qApp.font().pointSize())
299 290 font.setStyleHint(QtGui.QFont.TypeWriter)
300 291 self._set_font(font)
301 292
302 293 def select_all(self):
303 294 """ Selects all the text in the buffer.
304 295 """
305 296 self._control.selectAll()
306 297
307 298 def _get_tab_width(self):
308 299 """ The width (in terms of space characters) for tab characters.
309 300 """
310 301 return self._tab_width
311 302
312 303 def _set_tab_width(self, tab_width):
313 304 """ Sets the width (in terms of space characters) for tab characters.
314 305 """
315 306 font_metrics = QtGui.QFontMetrics(self.font)
316 307 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
317 308
318 309 self._tab_width = tab_width
319 310
320 311 tab_width = property(_get_tab_width, _set_tab_width)
321 312
322 313 def undo(self):
323 314 """ Undo the last operation. If there is no operation to undo, nothing
324 315 happens.
325 316 """
326 317 self._control.undo()
327 318
328 319 #---------------------------------------------------------------------------
329 320 # 'ConsoleWidget' abstract interface
330 321 #---------------------------------------------------------------------------
331 322
332 323 def _is_complete(self, source, interactive):
333 324 """ Returns whether 'source' can be executed. When triggered by an
334 325 Enter/Return key press, 'interactive' is True; otherwise, it is
335 326 False.
336 327 """
337 328 raise NotImplementedError
338 329
339 330 def _execute(self, source, hidden):
340 331 """ Execute 'source'. If 'hidden', do not show any output.
341 332 """
342 333 raise NotImplementedError
343 334
344 335 def _prompt_started_hook(self):
345 336 """ Called immediately after a new prompt is displayed.
346 337 """
347 338 pass
348 339
349 340 def _prompt_finished_hook(self):
350 341 """ Called immediately after a prompt is finished, i.e. when some input
351 342 will be processed and a new prompt displayed.
352 343 """
353 344 pass
354 345
355 346 def _up_pressed(self):
356 347 """ Called when the up key is pressed. Returns whether to continue
357 348 processing the event.
358 349 """
359 350 return True
360 351
361 352 def _down_pressed(self):
362 353 """ Called when the down key is pressed. Returns whether to continue
363 354 processing the event.
364 355 """
365 356 return True
366 357
367 358 def _tab_pressed(self):
368 359 """ Called when the tab key is pressed. Returns whether to continue
369 360 processing the event.
370 361 """
371 362 return False
372 363
373 364 #--------------------------------------------------------------------------
374 365 # 'ConsoleWidget' protected interface
375 366 #--------------------------------------------------------------------------
376 367
377 368 def _append_html(self, html):
378 369 """ Appends html at the end of the console buffer.
379 370 """
380 371 cursor = self._get_end_cursor()
381 372 self._insert_html(cursor, html)
382 373
383 374 def _append_html_fetching_plain_text(self, html):
384 375 """ Appends 'html', then returns the plain text version of it.
385 376 """
386 377 anchor = self._get_end_cursor().position()
387 378 self._append_html(html)
388 379 cursor = self._get_end_cursor()
389 380 cursor.setPosition(anchor, QtGui.QTextCursor.KeepAnchor)
390 381 return str(cursor.selection().toPlainText())
391 382
392 383 def _append_plain_text(self, text):
393 384 """ Appends plain text at the end of the console buffer, processing
394 385 ANSI codes if enabled.
395 386 """
396 387 cursor = self._get_end_cursor()
397 388 if self.ansi_codes:
398 389 for substring in self._ansi_processor.split_string(text):
399 390 format = self._ansi_processor.get_format()
400 391 cursor.insertText(substring, format)
401 392 else:
402 393 cursor.insertText(text)
403 394
404 395 def _append_plain_text_keeping_prompt(self, text):
405 396 """ Writes 'text' after the current prompt, then restores the old prompt
406 397 with its old input buffer.
407 398 """
408 399 input_buffer = self.input_buffer
409 400 self._append_plain_text('\n')
410 401 self._prompt_finished()
411 402
412 403 self._append_plain_text(text)
413 404 self._show_prompt()
414 405 self.input_buffer = input_buffer
415 406
416 407 def _complete_with_items(self, cursor, items):
417 408 """ Performs completion with 'items' at the specified cursor location.
418 409 """
419 410 if len(items) == 1:
420 411 cursor.setPosition(self._control.textCursor().position(),
421 412 QtGui.QTextCursor.KeepAnchor)
422 413 cursor.insertText(items[0])
423 414 elif len(items) > 1:
424 415 if self.gui_completion:
425 416 self._completion_widget.show_items(cursor, items)
426 417 else:
427 418 text = self._format_as_columns(items)
428 419 self._append_plain_text_keeping_prompt(text)
429 420
430 421 def _control_key_down(self, modifiers):
431 422 """ Given a KeyboardModifiers flags object, return whether the Control
432 423 key is down (on Mac OS, treat the Command key as a synonym for
433 424 Control).
434 425 """
435 426 down = bool(modifiers & QtCore.Qt.ControlModifier)
436 427
437 428 # Note: on Mac OS, ControlModifier corresponds to the Command key while
438 429 # MetaModifier corresponds to the Control key.
439 430 if sys.platform == 'darwin':
440 431 down = down ^ bool(modifiers & QtCore.Qt.MetaModifier)
441 432
442 433 return down
443 434
444 def _create_context_menu(self):
445 """ Creates a context menu for the underlying text widget.
446 """
447 menu = QtGui.QMenu(self)
448 clipboard = QtGui.QApplication.clipboard()
449
450 copy_action = QtGui.QAction('Copy', self)
451 copy_action.triggered.connect(self.copy)
452 self.copy_available.connect(copy_action.setEnabled)
453 menu.addAction(copy_action)
454
455 paste_action = QtGui.QAction('Paste', self)
456 paste_action.triggered.connect(self.paste)
457 clipboard.dataChanged.connect(
458 lambda: paste_action.setEnabled(not clipboard.text().isEmpty()))
459 menu.addAction(paste_action)
460 menu.addSeparator()
461
462 select_all_action = QtGui.QAction('Select All', self)
463 select_all_action.triggered.connect(self.select_all)
464 menu.addAction(select_all_action)
465
466 return menu
467
468 435 def _create_control(self, kind):
469 436 """ Creates and sets the underlying text widget.
470 437 """
471 438 layout = QtGui.QVBoxLayout(self)
472 439 layout.setMargin(0)
473 440 if kind == 'plain':
474 441 control = QtGui.QPlainTextEdit()
475 442 elif kind == 'rich':
476 443 control = QtGui.QTextEdit()
477 444 else:
478 445 raise ValueError("Kind %s unknown." % repr(kind))
479 446 layout.addWidget(control)
480 447
481 448 control.installEventFilter(self)
449 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
450 control.customContextMenuRequested.connect(self._show_context_menu)
482 451 control.copyAvailable.connect(self.copy_available)
483 452 control.redoAvailable.connect(self.redo_available)
484 453 control.undoAvailable.connect(self.undo_available)
485 454
486 455 return control
487 456
488 457 def _event_filter_keypress(self, event):
489 458 """ Filter key events for the underlying text widget to create a
490 459 console-like interface.
491 460 """
461 key = event.key()
462 ctrl_down = self._control_key_down(event.modifiers())
463
464 # If the key is remapped, return immediately.
465 if ctrl_down and key in self._ctrl_down_remap:
466 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
467 self._ctrl_down_remap[key],
468 QtCore.Qt.NoModifier)
469 QtGui.qApp.sendEvent(self._control, new_event)
470 return True
471
472 # If the completion widget accepts the key press, return immediately.
473 if self._completion_widget.isVisible():
474 self._completion_widget.keyPressEvent(event)
475 if event.isAccepted():
476 return True
477
478 # Otherwise, proceed normally and do not return early.
492 479 intercepted = False
493 replaced_event = None
494 480 cursor = self._control.textCursor()
495 481 position = cursor.position()
496 key = event.key()
497 ctrl_down = self._control_key_down(event.modifiers())
498 482 alt_down = event.modifiers() & QtCore.Qt.AltModifier
499 483 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
500 484
501 # Even though we have reimplemented 'paste', the C++ level slot is still
502 # called by Qt. So we intercept the key press here.
503 485 if event.matches(QtGui.QKeySequence.Paste):
486 # Call our paste instead of the underlying text widget's.
504 487 self.paste()
505 488 intercepted = True
506 489
507 490 elif ctrl_down:
508 if key in self._ctrl_down_remap:
509 ctrl_down = False
510 key = self._ctrl_down_remap[key]
511 replaced_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, key,
512 QtCore.Qt.NoModifier)
513
514 elif key == QtCore.Qt.Key_K:
491 if key == QtCore.Qt.Key_K:
515 492 if self._in_buffer(position):
516 493 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
517 494 QtGui.QTextCursor.KeepAnchor)
518 495 cursor.removeSelectedText()
519 496 intercepted = True
520 497
521 498 elif key == QtCore.Qt.Key_X:
522 499 intercepted = True
523 500
524 501 elif key == QtCore.Qt.Key_Y:
525 502 self.paste()
526 503 intercepted = True
527 504
528 505 elif alt_down:
529 506 if key == QtCore.Qt.Key_B:
530 507 self._set_cursor(self._get_word_start_cursor(position))
531 508 intercepted = True
532 509
533 510 elif key == QtCore.Qt.Key_F:
534 511 self._set_cursor(self._get_word_end_cursor(position))
535 512 intercepted = True
536 513
537 514 elif key == QtCore.Qt.Key_Backspace:
538 515 cursor = self._get_word_start_cursor(position)
539 516 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
540 517 cursor.removeSelectedText()
541 518 intercepted = True
542 519
543 520 elif key == QtCore.Qt.Key_D:
544 521 cursor = self._get_word_end_cursor(position)
545 522 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
546 523 cursor.removeSelectedText()
547 524 intercepted = True
548 525
549 if self._completion_widget.isVisible():
550 self._completion_widget.keyPressEvent(event)
551 intercepted = event.isAccepted()
552
553 526 else:
554 527 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
555 528 if self._reading:
556 529 self._append_plain_text('\n')
557 530 self._reading = False
558 531 if self._reading_callback:
559 532 self._reading_callback()
560 533 elif not self._executing:
561 534 self.execute(interactive=True)
562 535 intercepted = True
563 536
564 537 elif key == QtCore.Qt.Key_Up:
565 538 if self._reading or not self._up_pressed():
566 539 intercepted = True
567 540 else:
568 541 prompt_line = self._get_prompt_cursor().blockNumber()
569 542 intercepted = cursor.blockNumber() <= prompt_line
570 543
571 544 elif key == QtCore.Qt.Key_Down:
572 545 if self._reading or not self._down_pressed():
573 546 intercepted = True
574 547 else:
575 548 end_line = self._get_end_cursor().blockNumber()
576 549 intercepted = cursor.blockNumber() == end_line
577 550
578 551 elif key == QtCore.Qt.Key_Tab:
579 552 if self._reading:
580 553 intercepted = False
581 554 else:
582 555 intercepted = not self._tab_pressed()
583 556
584 557 elif key == QtCore.Qt.Key_Left:
585 558 intercepted = not self._in_buffer(position - 1)
586 559
587 560 elif key == QtCore.Qt.Key_Home:
588 561 cursor.movePosition(QtGui.QTextCursor.StartOfLine)
589 562 start_line = cursor.blockNumber()
590 563 if start_line == self._get_prompt_cursor().blockNumber():
591 564 start_pos = self._prompt_pos
592 565 else:
593 566 start_pos = cursor.position()
594 567 start_pos += len(self._continuation_prompt)
595 568 if shift_down and self._in_buffer(position):
596 569 self._set_selection(position, start_pos)
597 570 else:
598 571 self._set_position(start_pos)
599 572 intercepted = True
600 573
601 574 elif key == QtCore.Qt.Key_Backspace and not alt_down:
602 575
603 576 # Line deletion (remove continuation prompt)
604 577 len_prompt = len(self._continuation_prompt)
605 578 if not self._reading and \
606 579 cursor.columnNumber() == len_prompt and \
607 580 position != self._prompt_pos:
608 581 cursor.setPosition(position - len_prompt,
609 582 QtGui.QTextCursor.KeepAnchor)
610 583 cursor.removeSelectedText()
611 584
612 585 # Regular backwards deletion
613 586 else:
614 587 anchor = cursor.anchor()
615 588 if anchor == position:
616 589 intercepted = not self._in_buffer(position - 1)
617 590 else:
618 591 intercepted = not self._in_buffer(min(anchor, position))
619 592
620 593 elif key == QtCore.Qt.Key_Delete:
621 594 anchor = cursor.anchor()
622 595 intercepted = not self._in_buffer(min(anchor, position))
623 596
624 # Don't move cursor if control is down to allow copy-paste using
597 # Don't move the cursor if control is down to allow copy-paste using
625 598 # the keyboard in any part of the buffer.
626 599 if not ctrl_down:
627 600 self._keep_cursor_in_buffer()
628 601
629 if not intercepted and replaced_event:
630 QtGui.qApp.sendEvent(self._control, replaced_event)
631 602 return intercepted
632 603
633 604 def _format_as_columns(self, items, separator=' '):
634 605 """ Transform a list of strings into a single string with columns.
635 606
636 607 Parameters
637 608 ----------
638 609 items : sequence of strings
639 610 The strings to process.
640 611
641 612 separator : str, optional [default is two spaces]
642 613 The string that separates columns.
643 614
644 615 Returns
645 616 -------
646 617 The formatted string.
647 618 """
648 619 # Note: this code is adapted from columnize 0.3.2.
649 620 # See http://code.google.com/p/pycolumnize/
650 621
651 622 font_metrics = QtGui.QFontMetrics(self.font)
652 623 displaywidth = max(5, (self.width() / font_metrics.width(' ')) - 1)
653 624
654 625 # Some degenerate cases.
655 626 size = len(items)
656 627 if size == 0:
657 628 return '\n'
658 629 elif size == 1:
659 630 return '%s\n' % str(items[0])
660 631
661 632 # Try every row count from 1 upwards
662 633 array_index = lambda nrows, row, col: nrows*col + row
663 634 for nrows in range(1, size):
664 635 ncols = (size + nrows - 1) // nrows
665 636 colwidths = []
666 637 totwidth = -len(separator)
667 638 for col in range(ncols):
668 639 # Get max column width for this column
669 640 colwidth = 0
670 641 for row in range(nrows):
671 642 i = array_index(nrows, row, col)
672 643 if i >= size: break
673 644 x = items[i]
674 645 colwidth = max(colwidth, len(x))
675 646 colwidths.append(colwidth)
676 647 totwidth += colwidth + len(separator)
677 648 if totwidth > displaywidth:
678 649 break
679 650 if totwidth <= displaywidth:
680 651 break
681 652
682 653 # The smallest number of rows computed and the max widths for each
683 654 # column has been obtained. Now we just have to format each of the rows.
684 655 string = ''
685 656 for row in range(nrows):
686 657 texts = []
687 658 for col in range(ncols):
688 659 i = row + nrows*col
689 660 if i >= size:
690 661 texts.append('')
691 662 else:
692 663 texts.append(items[i])
693 664 while texts and not texts[-1]:
694 665 del texts[-1]
695 666 for col in range(len(texts)):
696 667 texts[col] = texts[col].ljust(colwidths[col])
697 668 string += '%s\n' % str(separator.join(texts))
698 669 return string
699 670
700 671 def _get_block_plain_text(self, block):
701 672 """ Given a QTextBlock, return its unformatted text.
702 673 """
703 674 cursor = QtGui.QTextCursor(block)
704 675 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
705 676 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
706 677 QtGui.QTextCursor.KeepAnchor)
707 678 return str(cursor.selection().toPlainText())
708 679
709 680 def _get_cursor(self):
710 681 """ Convenience method that returns a cursor for the current position.
711 682 """
712 683 return self._control.textCursor()
713 684
714 685 def _get_end_cursor(self):
715 686 """ Convenience method that returns a cursor for the last character.
716 687 """
717 688 cursor = self._control.textCursor()
718 689 cursor.movePosition(QtGui.QTextCursor.End)
719 690 return cursor
720 691
721 692 def _get_prompt_cursor(self):
722 693 """ Convenience method that returns a cursor for the prompt position.
723 694 """
724 695 cursor = self._control.textCursor()
725 696 cursor.setPosition(self._prompt_pos)
726 697 return cursor
727 698
728 699 def _get_selection_cursor(self, start, end):
729 700 """ Convenience method that returns a cursor with text selected between
730 701 the positions 'start' and 'end'.
731 702 """
732 703 cursor = self._control.textCursor()
733 704 cursor.setPosition(start)
734 705 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
735 706 return cursor
736 707
737 708 def _get_word_start_cursor(self, position):
738 709 """ Find the start of the word to the left the given position. If a
739 710 sequence of non-word characters precedes the first word, skip over
740 711 them. (This emulates the behavior of bash, emacs, etc.)
741 712 """
742 713 document = self._control.document()
743 714 position -= 1
744 715 while self._in_buffer(position) and \
745 716 not document.characterAt(position).isLetterOrNumber():
746 717 position -= 1
747 718 while self._in_buffer(position) and \
748 719 document.characterAt(position).isLetterOrNumber():
749 720 position -= 1
750 721 cursor = self._control.textCursor()
751 722 cursor.setPosition(position + 1)
752 723 return cursor
753 724
754 725 def _get_word_end_cursor(self, position):
755 726 """ Find the end of the word to the right the given position. If a
756 727 sequence of non-word characters precedes the first word, skip over
757 728 them. (This emulates the behavior of bash, emacs, etc.)
758 729 """
759 730 document = self._control.document()
760 731 end = self._get_end_cursor().position()
761 732 while position < end and \
762 733 not document.characterAt(position).isLetterOrNumber():
763 734 position += 1
764 735 while position < end and \
765 736 document.characterAt(position).isLetterOrNumber():
766 737 position += 1
767 738 cursor = self._control.textCursor()
768 739 cursor.setPosition(position)
769 740 return cursor
770 741
771 742 def _insert_html(self, cursor, html):
772 743 """ Insert HTML using the specified cursor in such a way that future
773 744 formatting is unaffected.
774 745 """
775 746 cursor.insertHtml(html)
776 747
777 748 # After inserting HTML, the text document "remembers" the current
778 749 # formatting, which means that subsequent calls adding plain text
779 750 # will result in similar formatting, a behavior that we do not want. To
780 751 # prevent this, we make sure that the last character has no formatting.
781 752 cursor.movePosition(QtGui.QTextCursor.Left,
782 753 QtGui.QTextCursor.KeepAnchor)
783 754 if cursor.selection().toPlainText().trimmed().isEmpty():
784 755 # If the last character is whitespace, it doesn't matter how it's
785 756 # formatted, so just clear the formatting.
786 757 cursor.setCharFormat(QtGui.QTextCharFormat())
787 758 else:
788 759 # Otherwise, add an unformatted space.
789 760 cursor.movePosition(QtGui.QTextCursor.Right)
790 761 cursor.insertText(' ', QtGui.QTextCharFormat())
791 762
792 763 def _in_buffer(self, position):
793 764 """ Returns whether the given position is inside the editing region.
794 765 """
795 766 return position >= self._prompt_pos
796 767
797 768 def _keep_cursor_in_buffer(self):
798 769 """ Ensures that the cursor is inside the editing region. Returns
799 770 whether the cursor was moved.
800 771 """
801 772 cursor = self._control.textCursor()
802 773 if cursor.position() < self._prompt_pos:
803 774 cursor.movePosition(QtGui.QTextCursor.End)
804 775 self._control.setTextCursor(cursor)
805 776 return True
806 777 else:
807 778 return False
808 779
809 780 def _prompt_started(self):
810 781 """ Called immediately after a new prompt is displayed.
811 782 """
812 783 # Temporarily disable the maximum block count to permit undo/redo and
813 784 # to ensure that the prompt position does not change due to truncation.
814 785 self._control.document().setMaximumBlockCount(0)
815 786 self._control.setUndoRedoEnabled(True)
816 787
817 788 self._control.setReadOnly(False)
818 789 self._control.moveCursor(QtGui.QTextCursor.End)
819 790
820 791 self._executing = False
821 792 self._prompt_started_hook()
822 793
823 794 def _prompt_finished(self):
824 795 """ Called immediately after a prompt is finished, i.e. when some input
825 796 will be processed and a new prompt displayed.
826 797 """
827 798 self._control.setUndoRedoEnabled(False)
828 799 self._control.setReadOnly(True)
829 800 self._prompt_finished_hook()
830 801
831 802 def _readline(self, prompt='', callback=None):
832 803 """ Reads one line of input from the user.
833 804
834 805 Parameters
835 806 ----------
836 807 prompt : str, optional
837 808 The prompt to print before reading the line.
838 809
839 810 callback : callable, optional
840 811 A callback to execute with the read line. If not specified, input is
841 812 read *synchronously* and this method does not return until it has
842 813 been read.
843 814
844 815 Returns
845 816 -------
846 817 If a callback is specified, returns nothing. Otherwise, returns the
847 818 input string with the trailing newline stripped.
848 819 """
849 820 if self._reading:
850 821 raise RuntimeError('Cannot read a line. Widget is already reading.')
851 822
852 823 if not callback and not self.isVisible():
853 824 # If the user cannot see the widget, this function cannot return.
854 825 raise RuntimeError('Cannot synchronously read a line if the widget'
855 826 'is not visible!')
856 827
857 828 self._reading = True
858 829 self._show_prompt(prompt, newline=False)
859 830
860 831 if callback is None:
861 832 self._reading_callback = None
862 833 while self._reading:
863 834 QtCore.QCoreApplication.processEvents()
864 835 return self.input_buffer.rstrip('\n')
865 836
866 837 else:
867 838 self._reading_callback = lambda: \
868 839 callback(self.input_buffer.rstrip('\n'))
869 840
870 841 def _reset(self):
871 842 """ Clears the console and resets internal state variables.
872 843 """
873 844 self._control.clear()
874 845 self._executing = self._reading = False
875 846
876 847 def _set_continuation_prompt(self, prompt, html=False):
877 848 """ Sets the continuation prompt.
878 849
879 850 Parameters
880 851 ----------
881 852 prompt : str
882 853 The prompt to show when more input is needed.
883 854
884 855 html : bool, optional (default False)
885 856 If set, the prompt will be inserted as formatted HTML. Otherwise,
886 857 the prompt will be treated as plain text, though ANSI color codes
887 858 will be handled.
888 859 """
889 860 if html:
890 861 self._continuation_prompt_html = prompt
891 862 else:
892 863 self._continuation_prompt = prompt
893 864 self._continuation_prompt_html = None
894 865
895 866 def _set_cursor(self, cursor):
896 867 """ Convenience method to set the current cursor.
897 868 """
898 869 self._control.setTextCursor(cursor)
899 870
900 871 def _set_position(self, position):
901 872 """ Convenience method to set the position of the cursor.
902 873 """
903 874 cursor = self._control.textCursor()
904 875 cursor.setPosition(position)
905 876 self._control.setTextCursor(cursor)
906 877
907 878 def _set_selection(self, start, end):
908 879 """ Convenience method to set the current selected text.
909 880 """
910 881 self._control.setTextCursor(self._get_selection_cursor(start, end))
911 882
883 def _show_context_menu(self, pos):
884 """ Shows a context menu at the given QPoint (in widget coordinates).
885 """
886 menu = QtGui.QMenu()
887
888 copy_action = QtGui.QAction('Copy', menu)
889 copy_action.triggered.connect(self.copy)
890 copy_action.setEnabled(self._get_cursor().hasSelection())
891 copy_action.setShortcut(QtGui.QKeySequence.Copy)
892 menu.addAction(copy_action)
893
894 paste_action = QtGui.QAction('Paste', menu)
895 paste_action.triggered.connect(self.paste)
896 paste_action.setEnabled(self._control.canPaste())
897 paste_action.setShortcut(QtGui.QKeySequence.Paste)
898 menu.addAction(paste_action)
899 menu.addSeparator()
900
901 select_all_action = QtGui.QAction('Select All', menu)
902 select_all_action.triggered.connect(self.select_all)
903 menu.addAction(select_all_action)
904
905 menu.exec_(self._control.mapToGlobal(pos))
906
912 907 def _show_prompt(self, prompt=None, html=False, newline=True):
913 908 """ Writes a new prompt at the end of the buffer.
914 909
915 910 Parameters
916 911 ----------
917 912 prompt : str, optional
918 913 The prompt to show. If not specified, the previous prompt is used.
919 914
920 915 html : bool, optional (default False)
921 916 Only relevant when a prompt is specified. If set, the prompt will
922 917 be inserted as formatted HTML. Otherwise, the prompt will be treated
923 918 as plain text, though ANSI color codes will be handled.
924 919
925 920 newline : bool, optional (default True)
926 921 If set, a new line will be written before showing the prompt if
927 922 there is not already a newline at the end of the buffer.
928 923 """
929 924 # Insert a preliminary newline, if necessary.
930 925 if newline:
931 926 cursor = self._get_end_cursor()
932 927 if cursor.position() > 0:
933 928 cursor.movePosition(QtGui.QTextCursor.Left,
934 929 QtGui.QTextCursor.KeepAnchor)
935 930 if str(cursor.selection().toPlainText()) != '\n':
936 931 self._append_plain_text('\n')
937 932
938 933 # Write the prompt.
939 934 if prompt is None:
940 935 if self._prompt_html is None:
941 936 self._append_plain_text(self._prompt)
942 937 else:
943 938 self._append_html(self._prompt_html)
944 939 else:
945 940 if html:
946 941 self._prompt = self._append_html_fetching_plain_text(prompt)
947 942 self._prompt_html = prompt
948 943 else:
949 944 self._append_plain_text(prompt)
950 945 self._prompt = prompt
951 946 self._prompt_html = None
952 947
953 948 self._prompt_pos = self._get_end_cursor().position()
954 949 self._prompt_started()
955 950
956 951 def _show_continuation_prompt(self):
957 952 """ Writes a new continuation prompt at the end of the buffer.
958 953 """
959 954 if self._continuation_prompt_html is None:
960 955 self._append_plain_text(self._continuation_prompt)
961 956 else:
962 957 self._continuation_prompt = self._append_html_fetching_plain_text(
963 958 self._continuation_prompt_html)
964 959
965 960 self._prompt_started()
966 961
967 962
968 963 class HistoryConsoleWidget(ConsoleWidget):
969 964 """ A ConsoleWidget that keeps a history of the commands that have been
970 965 executed.
971 966 """
972 967
973 968 #---------------------------------------------------------------------------
974 969 # 'object' interface
975 970 #---------------------------------------------------------------------------
976 971
977 972 def __init__(self, *args, **kw):
978 973 super(HistoryConsoleWidget, self).__init__(*args, **kw)
979 974 self._history = []
980 975 self._history_index = 0
981 976
982 977 #---------------------------------------------------------------------------
983 978 # 'ConsoleWidget' public interface
984 979 #---------------------------------------------------------------------------
985 980
986 981 def execute(self, source=None, hidden=False, interactive=False):
987 982 """ Reimplemented to the store history.
988 983 """
989 984 if not hidden:
990 985 history = self.input_buffer if source is None else source
991 986
992 987 executed = super(HistoryConsoleWidget, self).execute(
993 988 source, hidden, interactive)
994 989
995 990 if executed and not hidden:
996 991 self._history.append(history.rstrip())
997 992 self._history_index = len(self._history)
998 993
999 994 return executed
1000 995
1001 996 #---------------------------------------------------------------------------
1002 997 # 'ConsoleWidget' abstract interface
1003 998 #---------------------------------------------------------------------------
1004 999
1005 1000 def _up_pressed(self):
1006 1001 """ Called when the up key is pressed. Returns whether to continue
1007 1002 processing the event.
1008 1003 """
1009 1004 prompt_cursor = self._get_prompt_cursor()
1010 1005 if self._get_cursor().blockNumber() == prompt_cursor.blockNumber():
1011 1006 self.history_previous()
1012 1007
1013 1008 # Go to the first line of prompt for seemless history scrolling.
1014 1009 cursor = self._get_prompt_cursor()
1015 1010 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
1016 1011 self._set_cursor(cursor)
1017 1012
1018 1013 return False
1019 1014 return True
1020 1015
1021 1016 def _down_pressed(self):
1022 1017 """ Called when the down key is pressed. Returns whether to continue
1023 1018 processing the event.
1024 1019 """
1025 1020 end_cursor = self._get_end_cursor()
1026 1021 if self._get_cursor().blockNumber() == end_cursor.blockNumber():
1027 1022 self.history_next()
1028 1023 return False
1029 1024 return True
1030 1025
1031 1026 #---------------------------------------------------------------------------
1032 1027 # 'HistoryConsoleWidget' interface
1033 1028 #---------------------------------------------------------------------------
1034 1029
1035 1030 def history_previous(self):
1036 1031 """ If possible, set the input buffer to the previous item in the
1037 1032 history.
1038 1033 """
1039 1034 if self._history_index > 0:
1040 1035 self._history_index -= 1
1041 1036 self.input_buffer = self._history[self._history_index]
1042 1037
1043 1038 def history_next(self):
1044 1039 """ Set the input buffer to the next item in the history, or a blank
1045 1040 line if there is no subsequent item.
1046 1041 """
1047 1042 if self._history_index < len(self._history):
1048 1043 self._history_index += 1
1049 1044 if self._history_index < len(self._history):
1050 1045 self.input_buffer = self._history[self._history_index]
1051 1046 else:
1052 1047 self.input_buffer = ''
General Comments 0
You need to be logged in to leave comments. Login now