##// END OF EJS Templates
Merge pull request #388 from epatters/qt-kill-ring...
Evan Patterson -
r3841:a4b5f347 merge
parent child Browse files
Show More
@@ -0,0 +1,128
1 """ A generic Emacs-style kill ring, as well as a Qt-specific version.
2 """
3 #-----------------------------------------------------------------------------
4 # Imports
5 #-----------------------------------------------------------------------------
6
7 # System library imports
8 from IPython.external.qt import QtCore, QtGui
9
10 #-----------------------------------------------------------------------------
11 # Classes
12 #-----------------------------------------------------------------------------
13
14 class KillRing(object):
15 """ A generic Emacs-style kill ring.
16 """
17
18 def __init__(self):
19 self.clear()
20
21 def clear(self):
22 """ Clears the kill ring.
23 """
24 self._index = -1
25 self._ring = []
26
27 def kill(self, text):
28 """ Adds some killed text to the ring.
29 """
30 self._ring.append(text)
31
32 def yank(self):
33 """ Yank back the most recently killed text.
34
35 Returns:
36 --------
37 A text string or None.
38 """
39 self._index = len(self._ring)
40 return self.rotate()
41
42 def rotate(self):
43 """ Rotate the kill ring, then yank back the new top.
44
45 Returns:
46 --------
47 A text string or None.
48 """
49 self._index -= 1
50 if self._index >= 0:
51 return self._ring[self._index]
52 return None
53
54 class QtKillRing(QtCore.QObject):
55 """ A kill ring attached to Q[Plain]TextEdit.
56 """
57
58 #--------------------------------------------------------------------------
59 # QtKillRing interface
60 #--------------------------------------------------------------------------
61
62 def __init__(self, text_edit):
63 """ Create a kill ring attached to the specified Qt text edit.
64 """
65 assert isinstance(text_edit, (QtGui.QTextEdit, QtGui.QPlainTextEdit))
66 super(QtKillRing, self).__init__()
67
68 self._ring = KillRing()
69 self._prev_yank = None
70 self._skip_cursor = False
71 self._text_edit = text_edit
72
73 text_edit.cursorPositionChanged.connect(self._cursor_position_changed)
74
75 def clear(self):
76 """ Clears the kill ring.
77 """
78 self._ring.clear()
79 self._prev_yank = None
80
81 def kill(self, text):
82 """ Adds some killed text to the ring.
83 """
84 self._ring.kill(text)
85
86 def kill_cursor(self, cursor):
87 """ Kills the text selected by the give cursor.
88 """
89 text = cursor.selectedText()
90 if text:
91 cursor.removeSelectedText()
92 self.kill(text)
93
94 def yank(self):
95 """ Yank back the most recently killed text.
96 """
97 text = self._ring.yank()
98 if text:
99 self._skip_cursor = True
100 cursor = self._text_edit.textCursor()
101 cursor.insertText(text)
102 self._prev_yank = text
103
104 def rotate(self):
105 """ Rotate the kill ring, then yank back the new top.
106 """
107 if self._prev_yank:
108 text = self._ring.rotate()
109 if text:
110 self._skip_cursor = True
111 cursor = self._text_edit.textCursor()
112 cursor.movePosition(QtGui.QTextCursor.Left,
113 QtGui.QTextCursor.KeepAnchor,
114 n = len(self._prev_yank))
115 cursor.insertText(text)
116 self._prev_yank = text
117
118 #--------------------------------------------------------------------------
119 # Protected interface
120 #--------------------------------------------------------------------------
121
122 #------ Signal handlers ----------------------------------------------------
123
124 def _cursor_position_changed(self):
125 if self._skip_cursor:
126 self._skip_cursor = False
127 else:
128 self._prev_yank = None
@@ -0,0 +1,83
1 # Standard library imports
2 import unittest
3
4 # System library imports
5 from IPython.external.qt import QtCore, QtGui
6
7 # Local imports
8 from IPython.frontend.qt.console.kill_ring import KillRing, QtKillRing
9
10
11 class TestKillRing(unittest.TestCase):
12
13 @classmethod
14 def setUpClass(cls):
15 """ Create the application for the test case.
16 """
17 cls._app = QtGui.QApplication([])
18 cls._app.setQuitOnLastWindowClosed(False)
19
20 @classmethod
21 def tearDownClass(cls):
22 """ Exit the application.
23 """
24 QtGui.QApplication.quit()
25
26 def test_generic(self):
27 """ Does the generic kill ring work?
28 """
29 ring = KillRing()
30 self.assert_(ring.yank() is None)
31 self.assert_(ring.rotate() is None)
32
33 ring.kill('foo')
34 self.assertEqual(ring.yank(), 'foo')
35 self.assert_(ring.rotate() is None)
36 self.assertEqual(ring.yank(), 'foo')
37
38 ring.kill('bar')
39 self.assertEqual(ring.yank(), 'bar')
40 self.assertEqual(ring.rotate(), 'foo')
41
42 ring.clear()
43 self.assert_(ring.yank() is None)
44 self.assert_(ring.rotate() is None)
45
46 def test_qt_basic(self):
47 """ Does the Qt kill ring work?
48 """
49 text_edit = QtGui.QPlainTextEdit()
50 ring = QtKillRing(text_edit)
51
52 ring.kill('foo')
53 ring.kill('bar')
54 ring.yank()
55 ring.rotate()
56 ring.yank()
57 self.assertEqual(text_edit.toPlainText(), 'foobar')
58
59 text_edit.clear()
60 ring.kill('baz')
61 ring.yank()
62 ring.rotate()
63 ring.rotate()
64 ring.rotate()
65 self.assertEqual(text_edit.toPlainText(), 'foo')
66
67 def test_qt_cursor(self):
68 """ Does the Qt kill ring maintain state with cursor movement?
69 """
70 text_edit = QtGui.QPlainTextEdit()
71 ring = QtKillRing(text_edit)
72
73 ring.kill('foo')
74 ring.kill('bar')
75 ring.yank()
76 text_edit.moveCursor(QtGui.QTextCursor.Left)
77 ring.rotate()
78 self.assertEqual(text_edit.toPlainText(), 'bar')
79
80
81 if __name__ == '__main__':
82 import nose
83 nose.main()
@@ -1,1738 +1,1745
1 """ An abstract base class for console-type widgets.
1 """ An abstract base class for console-type widgets.
2 """
2 """
3 #-----------------------------------------------------------------------------
3 #-----------------------------------------------------------------------------
4 # Imports
4 # Imports
5 #-----------------------------------------------------------------------------
5 #-----------------------------------------------------------------------------
6
6
7 # Standard library imports
7 # Standard library imports
8 import os
8 import os
9 from os.path import commonprefix
9 from os.path import commonprefix
10 import re
10 import re
11 import sys
11 import sys
12 from textwrap import dedent
12 from textwrap import dedent
13 from unicodedata import category
13 from unicodedata import category
14
14
15 # System library imports
15 # System library imports
16 from IPython.external.qt import QtCore, QtGui
16 from IPython.external.qt import QtCore, QtGui
17
17
18 # Local imports
18 # Local imports
19 from IPython.config.configurable import Configurable
19 from IPython.config.configurable import Configurable
20 from IPython.frontend.qt.rich_text import HtmlExporter
20 from IPython.frontend.qt.rich_text import HtmlExporter
21 from IPython.frontend.qt.util import MetaQObjectHasTraits, get_font
21 from IPython.frontend.qt.util import MetaQObjectHasTraits, get_font
22 from IPython.utils.traitlets import Bool, Enum, Int
22 from IPython.utils.traitlets import Bool, Enum, Int
23 from ansi_code_processor import QtAnsiCodeProcessor
23 from ansi_code_processor import QtAnsiCodeProcessor
24 from completion_widget import CompletionWidget
24 from completion_widget import CompletionWidget
25 from kill_ring import QtKillRing
25
26
26 #-----------------------------------------------------------------------------
27 #-----------------------------------------------------------------------------
27 # Functions
28 # Functions
28 #-----------------------------------------------------------------------------
29 #-----------------------------------------------------------------------------
29
30
30 def is_letter_or_number(char):
31 def is_letter_or_number(char):
31 """ Returns whether the specified unicode character is a letter or a number.
32 """ Returns whether the specified unicode character is a letter or a number.
32 """
33 """
33 cat = category(char)
34 cat = category(char)
34 return cat.startswith('L') or cat.startswith('N')
35 return cat.startswith('L') or cat.startswith('N')
35
36
36 #-----------------------------------------------------------------------------
37 #-----------------------------------------------------------------------------
37 # Classes
38 # Classes
38 #-----------------------------------------------------------------------------
39 #-----------------------------------------------------------------------------
39
40
40 class ConsoleWidget(Configurable, QtGui.QWidget):
41 class ConsoleWidget(Configurable, QtGui.QWidget):
41 """ An abstract base class for console-type widgets. This class has
42 """ An abstract base class for console-type widgets. This class has
42 functionality for:
43 functionality for:
43
44
44 * Maintaining a prompt and editing region
45 * Maintaining a prompt and editing region
45 * Providing the traditional Unix-style console keyboard shortcuts
46 * Providing the traditional Unix-style console keyboard shortcuts
46 * Performing tab completion
47 * Performing tab completion
47 * Paging text
48 * Paging text
48 * Handling ANSI escape codes
49 * Handling ANSI escape codes
49
50
50 ConsoleWidget also provides a number of utility methods that will be
51 ConsoleWidget also provides a number of utility methods that will be
51 convenient to implementors of a console-style widget.
52 convenient to implementors of a console-style widget.
52 """
53 """
53 __metaclass__ = MetaQObjectHasTraits
54 __metaclass__ = MetaQObjectHasTraits
54
55
55 #------ Configuration ------------------------------------------------------
56 #------ Configuration ------------------------------------------------------
56
57
57 # Whether to process ANSI escape codes.
58 # Whether to process ANSI escape codes.
58 ansi_codes = Bool(True, config=True)
59 ansi_codes = Bool(True, config=True)
59
60
60 # The maximum number of lines of text before truncation. Specifying a
61 # The maximum number of lines of text before truncation. Specifying a
61 # non-positive number disables text truncation (not recommended).
62 # non-positive number disables text truncation (not recommended).
62 buffer_size = Int(500, config=True)
63 buffer_size = Int(500, config=True)
63
64
64 # Whether to use a list widget or plain text output for tab completion.
65 # Whether to use a list widget or plain text output for tab completion.
65 gui_completion = Bool(False, config=True)
66 gui_completion = Bool(False, config=True)
66
67
67 # The type of underlying text widget to use. Valid values are 'plain', which
68 # The type of underlying text widget to use. Valid values are 'plain', which
68 # specifies a QPlainTextEdit, and 'rich', which specifies a QTextEdit.
69 # specifies a QPlainTextEdit, and 'rich', which specifies a QTextEdit.
69 # NOTE: this value can only be specified during initialization.
70 # NOTE: this value can only be specified during initialization.
70 kind = Enum(['plain', 'rich'], default_value='plain', config=True)
71 kind = Enum(['plain', 'rich'], default_value='plain', config=True)
71
72
72 # The type of paging to use. Valid values are:
73 # The type of paging to use. Valid values are:
73 # 'inside' : The widget pages like a traditional terminal.
74 # 'inside' : The widget pages like a traditional terminal.
74 # 'hsplit' : When paging is requested, the widget is split
75 # 'hsplit' : When paging is requested, the widget is split
75 # horizontally. The top pane contains the console, and the
76 # horizontally. The top pane contains the console, and the
76 # bottom pane contains the paged text.
77 # bottom pane contains the paged text.
77 # 'vsplit' : Similar to 'hsplit', except that a vertical splitter used.
78 # 'vsplit' : Similar to 'hsplit', except that a vertical splitter used.
78 # 'custom' : No action is taken by the widget beyond emitting a
79 # 'custom' : No action is taken by the widget beyond emitting a
79 # 'custom_page_requested(str)' signal.
80 # 'custom_page_requested(str)' signal.
80 # 'none' : The text is written directly to the console.
81 # 'none' : The text is written directly to the console.
81 # NOTE: this value can only be specified during initialization.
82 # NOTE: this value can only be specified during initialization.
82 paging = Enum(['inside', 'hsplit', 'vsplit', 'custom', 'none'],
83 paging = Enum(['inside', 'hsplit', 'vsplit', 'custom', 'none'],
83 default_value='inside', config=True)
84 default_value='inside', config=True)
84
85
85 # Whether to override ShortcutEvents for the keybindings defined by this
86 # Whether to override ShortcutEvents for the keybindings defined by this
86 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
87 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
87 # priority (when it has focus) over, e.g., window-level menu shortcuts.
88 # priority (when it has focus) over, e.g., window-level menu shortcuts.
88 override_shortcuts = Bool(False)
89 override_shortcuts = Bool(False)
89
90
90 #------ Signals ------------------------------------------------------------
91 #------ Signals ------------------------------------------------------------
91
92
92 # Signals that indicate ConsoleWidget state.
93 # Signals that indicate ConsoleWidget state.
93 copy_available = QtCore.Signal(bool)
94 copy_available = QtCore.Signal(bool)
94 redo_available = QtCore.Signal(bool)
95 redo_available = QtCore.Signal(bool)
95 undo_available = QtCore.Signal(bool)
96 undo_available = QtCore.Signal(bool)
96
97
97 # Signal emitted when paging is needed and the paging style has been
98 # Signal emitted when paging is needed and the paging style has been
98 # specified as 'custom'.
99 # specified as 'custom'.
99 custom_page_requested = QtCore.Signal(object)
100 custom_page_requested = QtCore.Signal(object)
100
101
101 # Signal emitted when the font is changed.
102 # Signal emitted when the font is changed.
102 font_changed = QtCore.Signal(QtGui.QFont)
103 font_changed = QtCore.Signal(QtGui.QFont)
103
104
104 #------ Protected class variables ------------------------------------------
105 #------ Protected class variables ------------------------------------------
105
106
106 # When the control key is down, these keys are mapped.
107 # When the control key is down, these keys are mapped.
107 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
108 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
108 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
109 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
109 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
110 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
110 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
111 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
111 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
112 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
112 QtCore.Qt.Key_H : QtCore.Qt.Key_Backspace,
113 QtCore.Qt.Key_H : QtCore.Qt.Key_Backspace,
113 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
114 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
114 if not sys.platform == 'darwin':
115 if not sys.platform == 'darwin':
115 # On OS X, Ctrl-E already does the right thing, whereas End moves the
116 # On OS X, Ctrl-E already does the right thing, whereas End moves the
116 # cursor to the bottom of the buffer.
117 # cursor to the bottom of the buffer.
117 _ctrl_down_remap[QtCore.Qt.Key_E] = QtCore.Qt.Key_End
118 _ctrl_down_remap[QtCore.Qt.Key_E] = QtCore.Qt.Key_End
118
119
119 # The shortcuts defined by this widget. We need to keep track of these to
120 # The shortcuts defined by this widget. We need to keep track of these to
120 # support 'override_shortcuts' above.
121 # support 'override_shortcuts' above.
121 _shortcuts = set(_ctrl_down_remap.keys() +
122 _shortcuts = set(_ctrl_down_remap.keys() +
122 [ QtCore.Qt.Key_C, QtCore.Qt.Key_G, QtCore.Qt.Key_O,
123 [ QtCore.Qt.Key_C, QtCore.Qt.Key_G, QtCore.Qt.Key_O,
123 QtCore.Qt.Key_V ])
124 QtCore.Qt.Key_V ])
124
125
125 #---------------------------------------------------------------------------
126 #---------------------------------------------------------------------------
126 # 'QObject' interface
127 # 'QObject' interface
127 #---------------------------------------------------------------------------
128 #---------------------------------------------------------------------------
128
129
129 def __init__(self, parent=None, **kw):
130 def __init__(self, parent=None, **kw):
130 """ Create a ConsoleWidget.
131 """ Create a ConsoleWidget.
131
132
132 Parameters:
133 Parameters:
133 -----------
134 -----------
134 parent : QWidget, optional [default None]
135 parent : QWidget, optional [default None]
135 The parent for this widget.
136 The parent for this widget.
136 """
137 """
137 QtGui.QWidget.__init__(self, parent)
138 QtGui.QWidget.__init__(self, parent)
138 Configurable.__init__(self, **kw)
139 Configurable.__init__(self, **kw)
139
140
140 # Create the layout and underlying text widget.
141 # Create the layout and underlying text widget.
141 layout = QtGui.QStackedLayout(self)
142 layout = QtGui.QStackedLayout(self)
142 layout.setContentsMargins(0, 0, 0, 0)
143 layout.setContentsMargins(0, 0, 0, 0)
143 self._control = self._create_control()
144 self._control = self._create_control()
144 self._page_control = None
145 self._page_control = None
145 self._splitter = None
146 self._splitter = None
146 if self.paging in ('hsplit', 'vsplit'):
147 if self.paging in ('hsplit', 'vsplit'):
147 self._splitter = QtGui.QSplitter()
148 self._splitter = QtGui.QSplitter()
148 if self.paging == 'hsplit':
149 if self.paging == 'hsplit':
149 self._splitter.setOrientation(QtCore.Qt.Horizontal)
150 self._splitter.setOrientation(QtCore.Qt.Horizontal)
150 else:
151 else:
151 self._splitter.setOrientation(QtCore.Qt.Vertical)
152 self._splitter.setOrientation(QtCore.Qt.Vertical)
152 self._splitter.addWidget(self._control)
153 self._splitter.addWidget(self._control)
153 layout.addWidget(self._splitter)
154 layout.addWidget(self._splitter)
154 else:
155 else:
155 layout.addWidget(self._control)
156 layout.addWidget(self._control)
156
157
157 # Create the paging widget, if necessary.
158 # Create the paging widget, if necessary.
158 if self.paging in ('inside', 'hsplit', 'vsplit'):
159 if self.paging in ('inside', 'hsplit', 'vsplit'):
159 self._page_control = self._create_page_control()
160 self._page_control = self._create_page_control()
160 if self._splitter:
161 if self._splitter:
161 self._page_control.hide()
162 self._page_control.hide()
162 self._splitter.addWidget(self._page_control)
163 self._splitter.addWidget(self._page_control)
163 else:
164 else:
164 layout.addWidget(self._page_control)
165 layout.addWidget(self._page_control)
165
166
166 # Initialize protected variables. Some variables contain useful state
167 # Initialize protected variables. Some variables contain useful state
167 # information for subclasses; they should be considered read-only.
168 # information for subclasses; they should be considered read-only.
168 self._ansi_processor = QtAnsiCodeProcessor()
169 self._ansi_processor = QtAnsiCodeProcessor()
169 self._completion_widget = CompletionWidget(self._control)
170 self._completion_widget = CompletionWidget(self._control)
170 self._continuation_prompt = '> '
171 self._continuation_prompt = '> '
171 self._continuation_prompt_html = None
172 self._continuation_prompt_html = None
172 self._executing = False
173 self._executing = False
173 self._filter_drag = False
174 self._filter_drag = False
174 self._filter_resize = False
175 self._filter_resize = False
175 self._html_exporter = HtmlExporter(self._control)
176 self._html_exporter = HtmlExporter(self._control)
177 self._kill_ring = QtKillRing(self._control)
176 self._prompt = ''
178 self._prompt = ''
177 self._prompt_html = None
179 self._prompt_html = None
178 self._prompt_pos = 0
180 self._prompt_pos = 0
179 self._prompt_sep = ''
181 self._prompt_sep = ''
180 self._reading = False
182 self._reading = False
181 self._reading_callback = None
183 self._reading_callback = None
182 self._tab_width = 8
184 self._tab_width = 8
183 self._text_completing_pos = 0
185 self._text_completing_pos = 0
184
186
185 # Set a monospaced font.
187 # Set a monospaced font.
186 self.reset_font()
188 self.reset_font()
187
189
188 # Configure actions.
190 # Configure actions.
189 action = QtGui.QAction('Print', None)
191 action = QtGui.QAction('Print', None)
190 action.setEnabled(True)
192 action.setEnabled(True)
191 printkey = QtGui.QKeySequence(QtGui.QKeySequence.Print)
193 printkey = QtGui.QKeySequence(QtGui.QKeySequence.Print)
192 if printkey.matches("Ctrl+P") and sys.platform != 'darwin':
194 if printkey.matches("Ctrl+P") and sys.platform != 'darwin':
193 # Only override the default if there is a collision.
195 # Only override the default if there is a collision.
194 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
196 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
195 printkey = "Ctrl+Shift+P"
197 printkey = "Ctrl+Shift+P"
196 action.setShortcut(printkey)
198 action.setShortcut(printkey)
197 action.triggered.connect(self.print_)
199 action.triggered.connect(self.print_)
198 self.addAction(action)
200 self.addAction(action)
199 self._print_action = action
201 self._print_action = action
200
202
201 action = QtGui.QAction('Save as HTML/XML', None)
203 action = QtGui.QAction('Save as HTML/XML', None)
202 action.setShortcut(QtGui.QKeySequence.Save)
204 action.setShortcut(QtGui.QKeySequence.Save)
203 action.triggered.connect(self.export_html)
205 action.triggered.connect(self.export_html)
204 self.addAction(action)
206 self.addAction(action)
205 self._export_action = action
207 self._export_action = action
206
208
207 action = QtGui.QAction('Select All', None)
209 action = QtGui.QAction('Select All', None)
208 action.setEnabled(True)
210 action.setEnabled(True)
209 action.setShortcut(QtGui.QKeySequence.SelectAll)
211 action.setShortcut(QtGui.QKeySequence.SelectAll)
210 action.triggered.connect(self.select_all)
212 action.triggered.connect(self.select_all)
211 self.addAction(action)
213 self.addAction(action)
212 self._select_all_action = action
214 self._select_all_action = action
213
215
214 def eventFilter(self, obj, event):
216 def eventFilter(self, obj, event):
215 """ Reimplemented to ensure a console-like behavior in the underlying
217 """ Reimplemented to ensure a console-like behavior in the underlying
216 text widgets.
218 text widgets.
217 """
219 """
218 etype = event.type()
220 etype = event.type()
219 if etype == QtCore.QEvent.KeyPress:
221 if etype == QtCore.QEvent.KeyPress:
220
222
221 # Re-map keys for all filtered widgets.
223 # Re-map keys for all filtered widgets.
222 key = event.key()
224 key = event.key()
223 if self._control_key_down(event.modifiers()) and \
225 if self._control_key_down(event.modifiers()) and \
224 key in self._ctrl_down_remap:
226 key in self._ctrl_down_remap:
225 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
227 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
226 self._ctrl_down_remap[key],
228 self._ctrl_down_remap[key],
227 QtCore.Qt.NoModifier)
229 QtCore.Qt.NoModifier)
228 QtGui.qApp.sendEvent(obj, new_event)
230 QtGui.qApp.sendEvent(obj, new_event)
229 return True
231 return True
230
232
231 elif obj == self._control:
233 elif obj == self._control:
232 return self._event_filter_console_keypress(event)
234 return self._event_filter_console_keypress(event)
233
235
234 elif obj == self._page_control:
236 elif obj == self._page_control:
235 return self._event_filter_page_keypress(event)
237 return self._event_filter_page_keypress(event)
236
238
237 # Make middle-click paste safe.
239 # Make middle-click paste safe.
238 elif etype == QtCore.QEvent.MouseButtonRelease and \
240 elif etype == QtCore.QEvent.MouseButtonRelease and \
239 event.button() == QtCore.Qt.MidButton and \
241 event.button() == QtCore.Qt.MidButton and \
240 obj == self._control.viewport():
242 obj == self._control.viewport():
241 cursor = self._control.cursorForPosition(event.pos())
243 cursor = self._control.cursorForPosition(event.pos())
242 self._control.setTextCursor(cursor)
244 self._control.setTextCursor(cursor)
243 self.paste(QtGui.QClipboard.Selection)
245 self.paste(QtGui.QClipboard.Selection)
244 return True
246 return True
245
247
246 # Manually adjust the scrollbars *after* a resize event is dispatched.
248 # Manually adjust the scrollbars *after* a resize event is dispatched.
247 elif etype == QtCore.QEvent.Resize and not self._filter_resize:
249 elif etype == QtCore.QEvent.Resize and not self._filter_resize:
248 self._filter_resize = True
250 self._filter_resize = True
249 QtGui.qApp.sendEvent(obj, event)
251 QtGui.qApp.sendEvent(obj, event)
250 self._adjust_scrollbars()
252 self._adjust_scrollbars()
251 self._filter_resize = False
253 self._filter_resize = False
252 return True
254 return True
253
255
254 # Override shortcuts for all filtered widgets.
256 # Override shortcuts for all filtered widgets.
255 elif etype == QtCore.QEvent.ShortcutOverride and \
257 elif etype == QtCore.QEvent.ShortcutOverride and \
256 self.override_shortcuts and \
258 self.override_shortcuts and \
257 self._control_key_down(event.modifiers()) and \
259 self._control_key_down(event.modifiers()) and \
258 event.key() in self._shortcuts:
260 event.key() in self._shortcuts:
259 event.accept()
261 event.accept()
260
262
261 # Ensure that drags are safe. The problem is that the drag starting
263 # Ensure that drags are safe. The problem is that the drag starting
262 # logic, which determines whether the drag is a Copy or Move, is locked
264 # logic, which determines whether the drag is a Copy or Move, is locked
263 # down in QTextControl. If the widget is editable, which it must be if
265 # down in QTextControl. If the widget is editable, which it must be if
264 # we're not executing, the drag will be a Move. The following hack
266 # we're not executing, the drag will be a Move. The following hack
265 # prevents QTextControl from deleting the text by clearing the selection
267 # prevents QTextControl from deleting the text by clearing the selection
266 # when a drag leave event originating from this widget is dispatched.
268 # when a drag leave event originating from this widget is dispatched.
267 # The fact that we have to clear the user's selection is unfortunate,
269 # The fact that we have to clear the user's selection is unfortunate,
268 # but the alternative--trying to prevent Qt from using its hardwired
270 # but the alternative--trying to prevent Qt from using its hardwired
269 # drag logic and writing our own--is worse.
271 # drag logic and writing our own--is worse.
270 elif etype == QtCore.QEvent.DragEnter and \
272 elif etype == QtCore.QEvent.DragEnter and \
271 obj == self._control.viewport() and \
273 obj == self._control.viewport() and \
272 event.source() == self._control.viewport():
274 event.source() == self._control.viewport():
273 self._filter_drag = True
275 self._filter_drag = True
274 elif etype == QtCore.QEvent.DragLeave and \
276 elif etype == QtCore.QEvent.DragLeave and \
275 obj == self._control.viewport() and \
277 obj == self._control.viewport() and \
276 self._filter_drag:
278 self._filter_drag:
277 cursor = self._control.textCursor()
279 cursor = self._control.textCursor()
278 cursor.clearSelection()
280 cursor.clearSelection()
279 self._control.setTextCursor(cursor)
281 self._control.setTextCursor(cursor)
280 self._filter_drag = False
282 self._filter_drag = False
281
283
282 # Ensure that drops are safe.
284 # Ensure that drops are safe.
283 elif etype == QtCore.QEvent.Drop and obj == self._control.viewport():
285 elif etype == QtCore.QEvent.Drop and obj == self._control.viewport():
284 cursor = self._control.cursorForPosition(event.pos())
286 cursor = self._control.cursorForPosition(event.pos())
285 if self._in_buffer(cursor.position()):
287 if self._in_buffer(cursor.position()):
286 text = event.mimeData().text()
288 text = event.mimeData().text()
287 self._insert_plain_text_into_buffer(cursor, text)
289 self._insert_plain_text_into_buffer(cursor, text)
288
290
289 # Qt is expecting to get something here--drag and drop occurs in its
291 # Qt is expecting to get something here--drag and drop occurs in its
290 # own event loop. Send a DragLeave event to end it.
292 # own event loop. Send a DragLeave event to end it.
291 QtGui.qApp.sendEvent(obj, QtGui.QDragLeaveEvent())
293 QtGui.qApp.sendEvent(obj, QtGui.QDragLeaveEvent())
292 return True
294 return True
293
295
294 return super(ConsoleWidget, self).eventFilter(obj, event)
296 return super(ConsoleWidget, self).eventFilter(obj, event)
295
297
296 #---------------------------------------------------------------------------
298 #---------------------------------------------------------------------------
297 # 'QWidget' interface
299 # 'QWidget' interface
298 #---------------------------------------------------------------------------
300 #---------------------------------------------------------------------------
299
301
300 def sizeHint(self):
302 def sizeHint(self):
301 """ Reimplemented to suggest a size that is 80 characters wide and
303 """ Reimplemented to suggest a size that is 80 characters wide and
302 25 lines high.
304 25 lines high.
303 """
305 """
304 font_metrics = QtGui.QFontMetrics(self.font)
306 font_metrics = QtGui.QFontMetrics(self.font)
305 margin = (self._control.frameWidth() +
307 margin = (self._control.frameWidth() +
306 self._control.document().documentMargin()) * 2
308 self._control.document().documentMargin()) * 2
307 style = self.style()
309 style = self.style()
308 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth)
310 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth)
309
311
310 # Note 1: Despite my best efforts to take the various margins into
312 # Note 1: Despite my best efforts to take the various margins into
311 # account, the width is still coming out a bit too small, so we include
313 # account, the width is still coming out a bit too small, so we include
312 # a fudge factor of one character here.
314 # a fudge factor of one character here.
313 # Note 2: QFontMetrics.maxWidth is not used here or anywhere else due
315 # Note 2: QFontMetrics.maxWidth is not used here or anywhere else due
314 # to a Qt bug on certain Mac OS systems where it returns 0.
316 # to a Qt bug on certain Mac OS systems where it returns 0.
315 width = font_metrics.width(' ') * 81 + margin
317 width = font_metrics.width(' ') * 81 + margin
316 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent)
318 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent)
317 if self.paging == 'hsplit':
319 if self.paging == 'hsplit':
318 width = width * 2 + splitwidth
320 width = width * 2 + splitwidth
319
321
320 height = font_metrics.height() * 25 + margin
322 height = font_metrics.height() * 25 + margin
321 if self.paging == 'vsplit':
323 if self.paging == 'vsplit':
322 height = height * 2 + splitwidth
324 height = height * 2 + splitwidth
323
325
324 return QtCore.QSize(width, height)
326 return QtCore.QSize(width, height)
325
327
326 #---------------------------------------------------------------------------
328 #---------------------------------------------------------------------------
327 # 'ConsoleWidget' public interface
329 # 'ConsoleWidget' public interface
328 #---------------------------------------------------------------------------
330 #---------------------------------------------------------------------------
329
331
330 def can_copy(self):
332 def can_copy(self):
331 """ Returns whether text can be copied to the clipboard.
333 """ Returns whether text can be copied to the clipboard.
332 """
334 """
333 return self._control.textCursor().hasSelection()
335 return self._control.textCursor().hasSelection()
334
336
335 def can_cut(self):
337 def can_cut(self):
336 """ Returns whether text can be cut to the clipboard.
338 """ Returns whether text can be cut to the clipboard.
337 """
339 """
338 cursor = self._control.textCursor()
340 cursor = self._control.textCursor()
339 return (cursor.hasSelection() and
341 return (cursor.hasSelection() and
340 self._in_buffer(cursor.anchor()) and
342 self._in_buffer(cursor.anchor()) and
341 self._in_buffer(cursor.position()))
343 self._in_buffer(cursor.position()))
342
344
343 def can_paste(self):
345 def can_paste(self):
344 """ Returns whether text can be pasted from the clipboard.
346 """ Returns whether text can be pasted from the clipboard.
345 """
347 """
346 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
348 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
347 return bool(QtGui.QApplication.clipboard().text())
349 return bool(QtGui.QApplication.clipboard().text())
348 return False
350 return False
349
351
350 def clear(self, keep_input=True):
352 def clear(self, keep_input=True):
351 """ Clear the console.
353 """ Clear the console.
352
354
353 Parameters:
355 Parameters:
354 -----------
356 -----------
355 keep_input : bool, optional (default True)
357 keep_input : bool, optional (default True)
356 If set, restores the old input buffer if a new prompt is written.
358 If set, restores the old input buffer if a new prompt is written.
357 """
359 """
358 if self._executing:
360 if self._executing:
359 self._control.clear()
361 self._control.clear()
360 else:
362 else:
361 if keep_input:
363 if keep_input:
362 input_buffer = self.input_buffer
364 input_buffer = self.input_buffer
363 self._control.clear()
365 self._control.clear()
364 self._show_prompt()
366 self._show_prompt()
365 if keep_input:
367 if keep_input:
366 self.input_buffer = input_buffer
368 self.input_buffer = input_buffer
367
369
368 def copy(self):
370 def copy(self):
369 """ Copy the currently selected text to the clipboard.
371 """ Copy the currently selected text to the clipboard.
370 """
372 """
371 self._control.copy()
373 self._control.copy()
372
374
373 def cut(self):
375 def cut(self):
374 """ Copy the currently selected text to the clipboard and delete it
376 """ Copy the currently selected text to the clipboard and delete it
375 if it's inside the input buffer.
377 if it's inside the input buffer.
376 """
378 """
377 self.copy()
379 self.copy()
378 if self.can_cut():
380 if self.can_cut():
379 self._control.textCursor().removeSelectedText()
381 self._control.textCursor().removeSelectedText()
380
382
381 def execute(self, source=None, hidden=False, interactive=False):
383 def execute(self, source=None, hidden=False, interactive=False):
382 """ Executes source or the input buffer, possibly prompting for more
384 """ Executes source or the input buffer, possibly prompting for more
383 input.
385 input.
384
386
385 Parameters:
387 Parameters:
386 -----------
388 -----------
387 source : str, optional
389 source : str, optional
388
390
389 The source to execute. If not specified, the input buffer will be
391 The source to execute. If not specified, the input buffer will be
390 used. If specified and 'hidden' is False, the input buffer will be
392 used. If specified and 'hidden' is False, the input buffer will be
391 replaced with the source before execution.
393 replaced with the source before execution.
392
394
393 hidden : bool, optional (default False)
395 hidden : bool, optional (default False)
394
396
395 If set, no output will be shown and the prompt will not be modified.
397 If set, no output will be shown and the prompt will not be modified.
396 In other words, it will be completely invisible to the user that
398 In other words, it will be completely invisible to the user that
397 an execution has occurred.
399 an execution has occurred.
398
400
399 interactive : bool, optional (default False)
401 interactive : bool, optional (default False)
400
402
401 Whether the console is to treat the source as having been manually
403 Whether the console is to treat the source as having been manually
402 entered by the user. The effect of this parameter depends on the
404 entered by the user. The effect of this parameter depends on the
403 subclass implementation.
405 subclass implementation.
404
406
405 Raises:
407 Raises:
406 -------
408 -------
407 RuntimeError
409 RuntimeError
408 If incomplete input is given and 'hidden' is True. In this case,
410 If incomplete input is given and 'hidden' is True. In this case,
409 it is not possible to prompt for more input.
411 it is not possible to prompt for more input.
410
412
411 Returns:
413 Returns:
412 --------
414 --------
413 A boolean indicating whether the source was executed.
415 A boolean indicating whether the source was executed.
414 """
416 """
415 # WARNING: The order in which things happen here is very particular, in
417 # WARNING: The order in which things happen here is very particular, in
416 # large part because our syntax highlighting is fragile. If you change
418 # large part because our syntax highlighting is fragile. If you change
417 # something, test carefully!
419 # something, test carefully!
418
420
419 # Decide what to execute.
421 # Decide what to execute.
420 if source is None:
422 if source is None:
421 source = self.input_buffer
423 source = self.input_buffer
422 if not hidden:
424 if not hidden:
423 # A newline is appended later, but it should be considered part
425 # A newline is appended later, but it should be considered part
424 # of the input buffer.
426 # of the input buffer.
425 source += '\n'
427 source += '\n'
426 elif not hidden:
428 elif not hidden:
427 self.input_buffer = source
429 self.input_buffer = source
428
430
429 # Execute the source or show a continuation prompt if it is incomplete.
431 # Execute the source or show a continuation prompt if it is incomplete.
430 complete = self._is_complete(source, interactive)
432 complete = self._is_complete(source, interactive)
431 if hidden:
433 if hidden:
432 if complete:
434 if complete:
433 self._execute(source, hidden)
435 self._execute(source, hidden)
434 else:
436 else:
435 error = 'Incomplete noninteractive input: "%s"'
437 error = 'Incomplete noninteractive input: "%s"'
436 raise RuntimeError(error % source)
438 raise RuntimeError(error % source)
437 else:
439 else:
438 if complete:
440 if complete:
439 self._append_plain_text('\n')
441 self._append_plain_text('\n')
440 self._executing_input_buffer = self.input_buffer
442 self._executing_input_buffer = self.input_buffer
441 self._executing = True
443 self._executing = True
442 self._prompt_finished()
444 self._prompt_finished()
443
445
444 # The maximum block count is only in effect during execution.
446 # The maximum block count is only in effect during execution.
445 # This ensures that _prompt_pos does not become invalid due to
447 # This ensures that _prompt_pos does not become invalid due to
446 # text truncation.
448 # text truncation.
447 self._control.document().setMaximumBlockCount(self.buffer_size)
449 self._control.document().setMaximumBlockCount(self.buffer_size)
448
450
449 # Setting a positive maximum block count will automatically
451 # Setting a positive maximum block count will automatically
450 # disable the undo/redo history, but just to be safe:
452 # disable the undo/redo history, but just to be safe:
451 self._control.setUndoRedoEnabled(False)
453 self._control.setUndoRedoEnabled(False)
452
454
453 # Perform actual execution.
455 # Perform actual execution.
454 self._execute(source, hidden)
456 self._execute(source, hidden)
455
457
456 else:
458 else:
457 # Do this inside an edit block so continuation prompts are
459 # Do this inside an edit block so continuation prompts are
458 # removed seamlessly via undo/redo.
460 # removed seamlessly via undo/redo.
459 cursor = self._get_end_cursor()
461 cursor = self._get_end_cursor()
460 cursor.beginEditBlock()
462 cursor.beginEditBlock()
461 cursor.insertText('\n')
463 cursor.insertText('\n')
462 self._insert_continuation_prompt(cursor)
464 self._insert_continuation_prompt(cursor)
463 cursor.endEditBlock()
465 cursor.endEditBlock()
464
466
465 # Do not do this inside the edit block. It works as expected
467 # Do not do this inside the edit block. It works as expected
466 # when using a QPlainTextEdit control, but does not have an
468 # when using a QPlainTextEdit control, but does not have an
467 # effect when using a QTextEdit. I believe this is a Qt bug.
469 # effect when using a QTextEdit. I believe this is a Qt bug.
468 self._control.moveCursor(QtGui.QTextCursor.End)
470 self._control.moveCursor(QtGui.QTextCursor.End)
469
471
470 return complete
472 return complete
471
473
472 def export_html(self):
474 def export_html(self):
473 """ Shows a dialog to export HTML/XML in various formats.
475 """ Shows a dialog to export HTML/XML in various formats.
474 """
476 """
475 self._html_exporter.export()
477 self._html_exporter.export()
476
478
477 def _get_input_buffer(self):
479 def _get_input_buffer(self):
478 """ The text that the user has entered entered at the current prompt.
480 """ The text that the user has entered entered at the current prompt.
479 """
481 """
480 # If we're executing, the input buffer may not even exist anymore due to
482 # If we're executing, the input buffer may not even exist anymore due to
481 # the limit imposed by 'buffer_size'. Therefore, we store it.
483 # the limit imposed by 'buffer_size'. Therefore, we store it.
482 if self._executing:
484 if self._executing:
483 return self._executing_input_buffer
485 return self._executing_input_buffer
484
486
485 cursor = self._get_end_cursor()
487 cursor = self._get_end_cursor()
486 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
488 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
487 input_buffer = cursor.selection().toPlainText()
489 input_buffer = cursor.selection().toPlainText()
488
490
489 # Strip out continuation prompts.
491 # Strip out continuation prompts.
490 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
492 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
491
493
492 def _set_input_buffer(self, string):
494 def _set_input_buffer(self, string):
493 """ Replaces the text in the input buffer with 'string'.
495 """ Replaces the text in the input buffer with 'string'.
494 """
496 """
495 # For now, it is an error to modify the input buffer during execution.
497 # For now, it is an error to modify the input buffer during execution.
496 if self._executing:
498 if self._executing:
497 raise RuntimeError("Cannot change input buffer during execution.")
499 raise RuntimeError("Cannot change input buffer during execution.")
498
500
499 # Remove old text.
501 # Remove old text.
500 cursor = self._get_end_cursor()
502 cursor = self._get_end_cursor()
501 cursor.beginEditBlock()
503 cursor.beginEditBlock()
502 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
504 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
503 cursor.removeSelectedText()
505 cursor.removeSelectedText()
504
506
505 # Insert new text with continuation prompts.
507 # Insert new text with continuation prompts.
506 self._insert_plain_text_into_buffer(self._get_prompt_cursor(), string)
508 self._insert_plain_text_into_buffer(self._get_prompt_cursor(), string)
507 cursor.endEditBlock()
509 cursor.endEditBlock()
508 self._control.moveCursor(QtGui.QTextCursor.End)
510 self._control.moveCursor(QtGui.QTextCursor.End)
509
511
510 input_buffer = property(_get_input_buffer, _set_input_buffer)
512 input_buffer = property(_get_input_buffer, _set_input_buffer)
511
513
512 def _get_font(self):
514 def _get_font(self):
513 """ The base font being used by the ConsoleWidget.
515 """ The base font being used by the ConsoleWidget.
514 """
516 """
515 return self._control.document().defaultFont()
517 return self._control.document().defaultFont()
516
518
517 def _set_font(self, font):
519 def _set_font(self, font):
518 """ Sets the base font for the ConsoleWidget to the specified QFont.
520 """ Sets the base font for the ConsoleWidget to the specified QFont.
519 """
521 """
520 font_metrics = QtGui.QFontMetrics(font)
522 font_metrics = QtGui.QFontMetrics(font)
521 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
523 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
522
524
523 self._completion_widget.setFont(font)
525 self._completion_widget.setFont(font)
524 self._control.document().setDefaultFont(font)
526 self._control.document().setDefaultFont(font)
525 if self._page_control:
527 if self._page_control:
526 self._page_control.document().setDefaultFont(font)
528 self._page_control.document().setDefaultFont(font)
527
529
528 self.font_changed.emit(font)
530 self.font_changed.emit(font)
529
531
530 font = property(_get_font, _set_font)
532 font = property(_get_font, _set_font)
531
533
532 def paste(self, mode=QtGui.QClipboard.Clipboard):
534 def paste(self, mode=QtGui.QClipboard.Clipboard):
533 """ Paste the contents of the clipboard into the input region.
535 """ Paste the contents of the clipboard into the input region.
534
536
535 Parameters:
537 Parameters:
536 -----------
538 -----------
537 mode : QClipboard::Mode, optional [default QClipboard::Clipboard]
539 mode : QClipboard::Mode, optional [default QClipboard::Clipboard]
538
540
539 Controls which part of the system clipboard is used. This can be
541 Controls which part of the system clipboard is used. This can be
540 used to access the selection clipboard in X11 and the Find buffer
542 used to access the selection clipboard in X11 and the Find buffer
541 in Mac OS. By default, the regular clipboard is used.
543 in Mac OS. By default, the regular clipboard is used.
542 """
544 """
543 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
545 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
544 # Make sure the paste is safe.
546 # Make sure the paste is safe.
545 self._keep_cursor_in_buffer()
547 self._keep_cursor_in_buffer()
546 cursor = self._control.textCursor()
548 cursor = self._control.textCursor()
547
549
548 # Remove any trailing newline, which confuses the GUI and forces the
550 # Remove any trailing newline, which confuses the GUI and forces the
549 # user to backspace.
551 # user to backspace.
550 text = QtGui.QApplication.clipboard().text(mode).rstrip()
552 text = QtGui.QApplication.clipboard().text(mode).rstrip()
551 self._insert_plain_text_into_buffer(cursor, dedent(text))
553 self._insert_plain_text_into_buffer(cursor, dedent(text))
552
554
553 def print_(self, printer = None):
555 def print_(self, printer = None):
554 """ Print the contents of the ConsoleWidget to the specified QPrinter.
556 """ Print the contents of the ConsoleWidget to the specified QPrinter.
555 """
557 """
556 if (not printer):
558 if (not printer):
557 printer = QtGui.QPrinter()
559 printer = QtGui.QPrinter()
558 if(QtGui.QPrintDialog(printer).exec_() != QtGui.QDialog.Accepted):
560 if(QtGui.QPrintDialog(printer).exec_() != QtGui.QDialog.Accepted):
559 return
561 return
560 self._control.print_(printer)
562 self._control.print_(printer)
561
563
562 def prompt_to_top(self):
564 def prompt_to_top(self):
563 """ Moves the prompt to the top of the viewport.
565 """ Moves the prompt to the top of the viewport.
564 """
566 """
565 if not self._executing:
567 if not self._executing:
566 prompt_cursor = self._get_prompt_cursor()
568 prompt_cursor = self._get_prompt_cursor()
567 if self._get_cursor().blockNumber() < prompt_cursor.blockNumber():
569 if self._get_cursor().blockNumber() < prompt_cursor.blockNumber():
568 self._set_cursor(prompt_cursor)
570 self._set_cursor(prompt_cursor)
569 self._set_top_cursor(prompt_cursor)
571 self._set_top_cursor(prompt_cursor)
570
572
571 def redo(self):
573 def redo(self):
572 """ Redo the last operation. If there is no operation to redo, nothing
574 """ Redo the last operation. If there is no operation to redo, nothing
573 happens.
575 happens.
574 """
576 """
575 self._control.redo()
577 self._control.redo()
576
578
577 def reset_font(self):
579 def reset_font(self):
578 """ Sets the font to the default fixed-width font for this platform.
580 """ Sets the font to the default fixed-width font for this platform.
579 """
581 """
580 if sys.platform == 'win32':
582 if sys.platform == 'win32':
581 # Consolas ships with Vista/Win7, fallback to Courier if needed
583 # Consolas ships with Vista/Win7, fallback to Courier if needed
582 family, fallback = 'Consolas', 'Courier'
584 family, fallback = 'Consolas', 'Courier'
583 elif sys.platform == 'darwin':
585 elif sys.platform == 'darwin':
584 # OSX always has Monaco, no need for a fallback
586 # OSX always has Monaco, no need for a fallback
585 family, fallback = 'Monaco', None
587 family, fallback = 'Monaco', None
586 else:
588 else:
587 # FIXME: remove Consolas as a default on Linux once our font
589 # FIXME: remove Consolas as a default on Linux once our font
588 # selections are configurable by the user.
590 # selections are configurable by the user.
589 family, fallback = 'Consolas', 'Monospace'
591 family, fallback = 'Consolas', 'Monospace'
590 font = get_font(family, fallback)
592 font = get_font(family, fallback)
591 font.setPointSize(QtGui.qApp.font().pointSize())
593 font.setPointSize(QtGui.qApp.font().pointSize())
592 font.setStyleHint(QtGui.QFont.TypeWriter)
594 font.setStyleHint(QtGui.QFont.TypeWriter)
593 self._set_font(font)
595 self._set_font(font)
594
596
595 def change_font_size(self, delta):
597 def change_font_size(self, delta):
596 """Change the font size by the specified amount (in points).
598 """Change the font size by the specified amount (in points).
597 """
599 """
598 font = self.font
600 font = self.font
599 font.setPointSize(font.pointSize() + delta)
601 font.setPointSize(font.pointSize() + delta)
600 self._set_font(font)
602 self._set_font(font)
601
603
602 def select_all(self):
604 def select_all(self):
603 """ Selects all the text in the buffer.
605 """ Selects all the text in the buffer.
604 """
606 """
605 self._control.selectAll()
607 self._control.selectAll()
606
608
607 def _get_tab_width(self):
609 def _get_tab_width(self):
608 """ The width (in terms of space characters) for tab characters.
610 """ The width (in terms of space characters) for tab characters.
609 """
611 """
610 return self._tab_width
612 return self._tab_width
611
613
612 def _set_tab_width(self, tab_width):
614 def _set_tab_width(self, tab_width):
613 """ Sets the width (in terms of space characters) for tab characters.
615 """ Sets the width (in terms of space characters) for tab characters.
614 """
616 """
615 font_metrics = QtGui.QFontMetrics(self.font)
617 font_metrics = QtGui.QFontMetrics(self.font)
616 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
618 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
617
619
618 self._tab_width = tab_width
620 self._tab_width = tab_width
619
621
620 tab_width = property(_get_tab_width, _set_tab_width)
622 tab_width = property(_get_tab_width, _set_tab_width)
621
623
622 def undo(self):
624 def undo(self):
623 """ Undo the last operation. If there is no operation to undo, nothing
625 """ Undo the last operation. If there is no operation to undo, nothing
624 happens.
626 happens.
625 """
627 """
626 self._control.undo()
628 self._control.undo()
627
629
628 #---------------------------------------------------------------------------
630 #---------------------------------------------------------------------------
629 # 'ConsoleWidget' abstract interface
631 # 'ConsoleWidget' abstract interface
630 #---------------------------------------------------------------------------
632 #---------------------------------------------------------------------------
631
633
632 def _is_complete(self, source, interactive):
634 def _is_complete(self, source, interactive):
633 """ Returns whether 'source' can be executed. When triggered by an
635 """ Returns whether 'source' can be executed. When triggered by an
634 Enter/Return key press, 'interactive' is True; otherwise, it is
636 Enter/Return key press, 'interactive' is True; otherwise, it is
635 False.
637 False.
636 """
638 """
637 raise NotImplementedError
639 raise NotImplementedError
638
640
639 def _execute(self, source, hidden):
641 def _execute(self, source, hidden):
640 """ Execute 'source'. If 'hidden', do not show any output.
642 """ Execute 'source'. If 'hidden', do not show any output.
641 """
643 """
642 raise NotImplementedError
644 raise NotImplementedError
643
645
644 def _prompt_started_hook(self):
646 def _prompt_started_hook(self):
645 """ Called immediately after a new prompt is displayed.
647 """ Called immediately after a new prompt is displayed.
646 """
648 """
647 pass
649 pass
648
650
649 def _prompt_finished_hook(self):
651 def _prompt_finished_hook(self):
650 """ Called immediately after a prompt is finished, i.e. when some input
652 """ Called immediately after a prompt is finished, i.e. when some input
651 will be processed and a new prompt displayed.
653 will be processed and a new prompt displayed.
652 """
654 """
653 pass
655 pass
654
656
655 def _up_pressed(self, shift_modifier):
657 def _up_pressed(self, shift_modifier):
656 """ Called when the up key is pressed. Returns whether to continue
658 """ Called when the up key is pressed. Returns whether to continue
657 processing the event.
659 processing the event.
658 """
660 """
659 return True
661 return True
660
662
661 def _down_pressed(self, shift_modifier):
663 def _down_pressed(self, shift_modifier):
662 """ Called when the down key is pressed. Returns whether to continue
664 """ Called when the down key is pressed. Returns whether to continue
663 processing the event.
665 processing the event.
664 """
666 """
665 return True
667 return True
666
668
667 def _tab_pressed(self):
669 def _tab_pressed(self):
668 """ Called when the tab key is pressed. Returns whether to continue
670 """ Called when the tab key is pressed. Returns whether to continue
669 processing the event.
671 processing the event.
670 """
672 """
671 return False
673 return False
672
674
673 #--------------------------------------------------------------------------
675 #--------------------------------------------------------------------------
674 # 'ConsoleWidget' protected interface
676 # 'ConsoleWidget' protected interface
675 #--------------------------------------------------------------------------
677 #--------------------------------------------------------------------------
676
678
677 def _append_html(self, html):
679 def _append_html(self, html):
678 """ Appends html at the end of the console buffer.
680 """ Appends html at the end of the console buffer.
679 """
681 """
680 cursor = self._get_end_cursor()
682 cursor = self._get_end_cursor()
681 self._insert_html(cursor, html)
683 self._insert_html(cursor, html)
682
684
683 def _append_html_fetching_plain_text(self, html):
685 def _append_html_fetching_plain_text(self, html):
684 """ Appends 'html', then returns the plain text version of it.
686 """ Appends 'html', then returns the plain text version of it.
685 """
687 """
686 cursor = self._get_end_cursor()
688 cursor = self._get_end_cursor()
687 return self._insert_html_fetching_plain_text(cursor, html)
689 return self._insert_html_fetching_plain_text(cursor, html)
688
690
689 def _append_plain_text(self, text):
691 def _append_plain_text(self, text):
690 """ Appends plain text at the end of the console buffer, processing
692 """ Appends plain text at the end of the console buffer, processing
691 ANSI codes if enabled.
693 ANSI codes if enabled.
692 """
694 """
693 cursor = self._get_end_cursor()
695 cursor = self._get_end_cursor()
694 self._insert_plain_text(cursor, text)
696 self._insert_plain_text(cursor, text)
695
697
696 def _append_plain_text_keeping_prompt(self, text):
698 def _append_plain_text_keeping_prompt(self, text):
697 """ Writes 'text' after the current prompt, then restores the old prompt
699 """ Writes 'text' after the current prompt, then restores the old prompt
698 with its old input buffer.
700 with its old input buffer.
699 """
701 """
700 input_buffer = self.input_buffer
702 input_buffer = self.input_buffer
701 self._append_plain_text('\n')
703 self._append_plain_text('\n')
702 self._prompt_finished()
704 self._prompt_finished()
703
705
704 self._append_plain_text(text)
706 self._append_plain_text(text)
705 self._show_prompt()
707 self._show_prompt()
706 self.input_buffer = input_buffer
708 self.input_buffer = input_buffer
707
709
708 def _cancel_text_completion(self):
710 def _cancel_text_completion(self):
709 """ If text completion is progress, cancel it.
711 """ If text completion is progress, cancel it.
710 """
712 """
711 if self._text_completing_pos:
713 if self._text_completing_pos:
712 self._clear_temporary_buffer()
714 self._clear_temporary_buffer()
713 self._text_completing_pos = 0
715 self._text_completing_pos = 0
714
716
715 def _clear_temporary_buffer(self):
717 def _clear_temporary_buffer(self):
716 """ Clears the "temporary text" buffer, i.e. all the text following
718 """ Clears the "temporary text" buffer, i.e. all the text following
717 the prompt region.
719 the prompt region.
718 """
720 """
719 # Select and remove all text below the input buffer.
721 # Select and remove all text below the input buffer.
720 cursor = self._get_prompt_cursor()
722 cursor = self._get_prompt_cursor()
721 prompt = self._continuation_prompt.lstrip()
723 prompt = self._continuation_prompt.lstrip()
722 while cursor.movePosition(QtGui.QTextCursor.NextBlock):
724 while cursor.movePosition(QtGui.QTextCursor.NextBlock):
723 temp_cursor = QtGui.QTextCursor(cursor)
725 temp_cursor = QtGui.QTextCursor(cursor)
724 temp_cursor.select(QtGui.QTextCursor.BlockUnderCursor)
726 temp_cursor.select(QtGui.QTextCursor.BlockUnderCursor)
725 text = temp_cursor.selection().toPlainText().lstrip()
727 text = temp_cursor.selection().toPlainText().lstrip()
726 if not text.startswith(prompt):
728 if not text.startswith(prompt):
727 break
729 break
728 else:
730 else:
729 # We've reached the end of the input buffer and no text follows.
731 # We've reached the end of the input buffer and no text follows.
730 return
732 return
731 cursor.movePosition(QtGui.QTextCursor.Left) # Grab the newline.
733 cursor.movePosition(QtGui.QTextCursor.Left) # Grab the newline.
732 cursor.movePosition(QtGui.QTextCursor.End,
734 cursor.movePosition(QtGui.QTextCursor.End,
733 QtGui.QTextCursor.KeepAnchor)
735 QtGui.QTextCursor.KeepAnchor)
734 cursor.removeSelectedText()
736 cursor.removeSelectedText()
735
737
736 # After doing this, we have no choice but to clear the undo/redo
738 # After doing this, we have no choice but to clear the undo/redo
737 # history. Otherwise, the text is not "temporary" at all, because it
739 # history. Otherwise, the text is not "temporary" at all, because it
738 # can be recalled with undo/redo. Unfortunately, Qt does not expose
740 # can be recalled with undo/redo. Unfortunately, Qt does not expose
739 # fine-grained control to the undo/redo system.
741 # fine-grained control to the undo/redo system.
740 if self._control.isUndoRedoEnabled():
742 if self._control.isUndoRedoEnabled():
741 self._control.setUndoRedoEnabled(False)
743 self._control.setUndoRedoEnabled(False)
742 self._control.setUndoRedoEnabled(True)
744 self._control.setUndoRedoEnabled(True)
743
745
744 def _complete_with_items(self, cursor, items):
746 def _complete_with_items(self, cursor, items):
745 """ Performs completion with 'items' at the specified cursor location.
747 """ Performs completion with 'items' at the specified cursor location.
746 """
748 """
747 self._cancel_text_completion()
749 self._cancel_text_completion()
748
750
749 if len(items) == 1:
751 if len(items) == 1:
750 cursor.setPosition(self._control.textCursor().position(),
752 cursor.setPosition(self._control.textCursor().position(),
751 QtGui.QTextCursor.KeepAnchor)
753 QtGui.QTextCursor.KeepAnchor)
752 cursor.insertText(items[0])
754 cursor.insertText(items[0])
753
755
754 elif len(items) > 1:
756 elif len(items) > 1:
755 current_pos = self._control.textCursor().position()
757 current_pos = self._control.textCursor().position()
756 prefix = commonprefix(items)
758 prefix = commonprefix(items)
757 if prefix:
759 if prefix:
758 cursor.setPosition(current_pos, QtGui.QTextCursor.KeepAnchor)
760 cursor.setPosition(current_pos, QtGui.QTextCursor.KeepAnchor)
759 cursor.insertText(prefix)
761 cursor.insertText(prefix)
760 current_pos = cursor.position()
762 current_pos = cursor.position()
761
763
762 if self.gui_completion:
764 if self.gui_completion:
763 cursor.movePosition(QtGui.QTextCursor.Left, n=len(prefix))
765 cursor.movePosition(QtGui.QTextCursor.Left, n=len(prefix))
764 self._completion_widget.show_items(cursor, items)
766 self._completion_widget.show_items(cursor, items)
765 else:
767 else:
766 cursor.beginEditBlock()
768 cursor.beginEditBlock()
767 self._append_plain_text('\n')
769 self._append_plain_text('\n')
768 self._page(self._format_as_columns(items))
770 self._page(self._format_as_columns(items))
769 cursor.endEditBlock()
771 cursor.endEditBlock()
770
772
771 cursor.setPosition(current_pos)
773 cursor.setPosition(current_pos)
772 self._control.moveCursor(QtGui.QTextCursor.End)
774 self._control.moveCursor(QtGui.QTextCursor.End)
773 self._control.setTextCursor(cursor)
775 self._control.setTextCursor(cursor)
774 self._text_completing_pos = current_pos
776 self._text_completing_pos = current_pos
775
777
776 def _context_menu_make(self, pos):
778 def _context_menu_make(self, pos):
777 """ Creates a context menu for the given QPoint (in widget coordinates).
779 """ Creates a context menu for the given QPoint (in widget coordinates).
778 """
780 """
779 menu = QtGui.QMenu(self)
781 menu = QtGui.QMenu(self)
780
782
781 cut_action = menu.addAction('Cut', self.cut)
783 cut_action = menu.addAction('Cut', self.cut)
782 cut_action.setEnabled(self.can_cut())
784 cut_action.setEnabled(self.can_cut())
783 cut_action.setShortcut(QtGui.QKeySequence.Cut)
785 cut_action.setShortcut(QtGui.QKeySequence.Cut)
784
786
785 copy_action = menu.addAction('Copy', self.copy)
787 copy_action = menu.addAction('Copy', self.copy)
786 copy_action.setEnabled(self.can_copy())
788 copy_action.setEnabled(self.can_copy())
787 copy_action.setShortcut(QtGui.QKeySequence.Copy)
789 copy_action.setShortcut(QtGui.QKeySequence.Copy)
788
790
789 paste_action = menu.addAction('Paste', self.paste)
791 paste_action = menu.addAction('Paste', self.paste)
790 paste_action.setEnabled(self.can_paste())
792 paste_action.setEnabled(self.can_paste())
791 paste_action.setShortcut(QtGui.QKeySequence.Paste)
793 paste_action.setShortcut(QtGui.QKeySequence.Paste)
792
794
793 menu.addSeparator()
795 menu.addSeparator()
794 menu.addAction(self._select_all_action)
796 menu.addAction(self._select_all_action)
795
797
796 menu.addSeparator()
798 menu.addSeparator()
797 menu.addAction(self._export_action)
799 menu.addAction(self._export_action)
798 menu.addAction(self._print_action)
800 menu.addAction(self._print_action)
799
801
800 return menu
802 return menu
801
803
802 def _control_key_down(self, modifiers, include_command=False):
804 def _control_key_down(self, modifiers, include_command=False):
803 """ Given a KeyboardModifiers flags object, return whether the Control
805 """ Given a KeyboardModifiers flags object, return whether the Control
804 key is down.
806 key is down.
805
807
806 Parameters:
808 Parameters:
807 -----------
809 -----------
808 include_command : bool, optional (default True)
810 include_command : bool, optional (default True)
809 Whether to treat the Command key as a (mutually exclusive) synonym
811 Whether to treat the Command key as a (mutually exclusive) synonym
810 for Control when in Mac OS.
812 for Control when in Mac OS.
811 """
813 """
812 # Note that on Mac OS, ControlModifier corresponds to the Command key
814 # Note that on Mac OS, ControlModifier corresponds to the Command key
813 # while MetaModifier corresponds to the Control key.
815 # while MetaModifier corresponds to the Control key.
814 if sys.platform == 'darwin':
816 if sys.platform == 'darwin':
815 down = include_command and (modifiers & QtCore.Qt.ControlModifier)
817 down = include_command and (modifiers & QtCore.Qt.ControlModifier)
816 return bool(down) ^ bool(modifiers & QtCore.Qt.MetaModifier)
818 return bool(down) ^ bool(modifiers & QtCore.Qt.MetaModifier)
817 else:
819 else:
818 return bool(modifiers & QtCore.Qt.ControlModifier)
820 return bool(modifiers & QtCore.Qt.ControlModifier)
819
821
820 def _create_control(self):
822 def _create_control(self):
821 """ Creates and connects the underlying text widget.
823 """ Creates and connects the underlying text widget.
822 """
824 """
823 # Create the underlying control.
825 # Create the underlying control.
824 if self.kind == 'plain':
826 if self.kind == 'plain':
825 control = QtGui.QPlainTextEdit()
827 control = QtGui.QPlainTextEdit()
826 elif self.kind == 'rich':
828 elif self.kind == 'rich':
827 control = QtGui.QTextEdit()
829 control = QtGui.QTextEdit()
828 control.setAcceptRichText(False)
830 control.setAcceptRichText(False)
829
831
830 # Install event filters. The filter on the viewport is needed for
832 # Install event filters. The filter on the viewport is needed for
831 # mouse events and drag events.
833 # mouse events and drag events.
832 control.installEventFilter(self)
834 control.installEventFilter(self)
833 control.viewport().installEventFilter(self)
835 control.viewport().installEventFilter(self)
834
836
835 # Connect signals.
837 # Connect signals.
836 control.cursorPositionChanged.connect(self._cursor_position_changed)
838 control.cursorPositionChanged.connect(self._cursor_position_changed)
837 control.customContextMenuRequested.connect(
839 control.customContextMenuRequested.connect(
838 self._custom_context_menu_requested)
840 self._custom_context_menu_requested)
839 control.copyAvailable.connect(self.copy_available)
841 control.copyAvailable.connect(self.copy_available)
840 control.redoAvailable.connect(self.redo_available)
842 control.redoAvailable.connect(self.redo_available)
841 control.undoAvailable.connect(self.undo_available)
843 control.undoAvailable.connect(self.undo_available)
842
844
843 # Hijack the document size change signal to prevent Qt from adjusting
845 # Hijack the document size change signal to prevent Qt from adjusting
844 # the viewport's scrollbar. We are relying on an implementation detail
846 # the viewport's scrollbar. We are relying on an implementation detail
845 # of Q(Plain)TextEdit here, which is potentially dangerous, but without
847 # of Q(Plain)TextEdit here, which is potentially dangerous, but without
846 # this functionality we cannot create a nice terminal interface.
848 # this functionality we cannot create a nice terminal interface.
847 layout = control.document().documentLayout()
849 layout = control.document().documentLayout()
848 layout.documentSizeChanged.disconnect()
850 layout.documentSizeChanged.disconnect()
849 layout.documentSizeChanged.connect(self._adjust_scrollbars)
851 layout.documentSizeChanged.connect(self._adjust_scrollbars)
850
852
851 # Configure the control.
853 # Configure the control.
852 control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
854 control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
853 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
855 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
854 control.setReadOnly(True)
856 control.setReadOnly(True)
855 control.setUndoRedoEnabled(False)
857 control.setUndoRedoEnabled(False)
856 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
858 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
857 return control
859 return control
858
860
859 def _create_page_control(self):
861 def _create_page_control(self):
860 """ Creates and connects the underlying paging widget.
862 """ Creates and connects the underlying paging widget.
861 """
863 """
862 if self.kind == 'plain':
864 if self.kind == 'plain':
863 control = QtGui.QPlainTextEdit()
865 control = QtGui.QPlainTextEdit()
864 elif self.kind == 'rich':
866 elif self.kind == 'rich':
865 control = QtGui.QTextEdit()
867 control = QtGui.QTextEdit()
866 control.installEventFilter(self)
868 control.installEventFilter(self)
867 control.setReadOnly(True)
869 control.setReadOnly(True)
868 control.setUndoRedoEnabled(False)
870 control.setUndoRedoEnabled(False)
869 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
871 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
870 return control
872 return control
871
873
872 def _event_filter_console_keypress(self, event):
874 def _event_filter_console_keypress(self, event):
873 """ Filter key events for the underlying text widget to create a
875 """ Filter key events for the underlying text widget to create a
874 console-like interface.
876 console-like interface.
875 """
877 """
876 intercepted = False
878 intercepted = False
877 cursor = self._control.textCursor()
879 cursor = self._control.textCursor()
878 position = cursor.position()
880 position = cursor.position()
879 key = event.key()
881 key = event.key()
880 ctrl_down = self._control_key_down(event.modifiers())
882 ctrl_down = self._control_key_down(event.modifiers())
881 alt_down = event.modifiers() & QtCore.Qt.AltModifier
883 alt_down = event.modifiers() & QtCore.Qt.AltModifier
882 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
884 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
883
885
884 #------ Special sequences ----------------------------------------------
886 #------ Special sequences ----------------------------------------------
885
887
886 if event.matches(QtGui.QKeySequence.Copy):
888 if event.matches(QtGui.QKeySequence.Copy):
887 self.copy()
889 self.copy()
888 intercepted = True
890 intercepted = True
889
891
890 elif event.matches(QtGui.QKeySequence.Cut):
892 elif event.matches(QtGui.QKeySequence.Cut):
891 self.cut()
893 self.cut()
892 intercepted = True
894 intercepted = True
893
895
894 elif event.matches(QtGui.QKeySequence.Paste):
896 elif event.matches(QtGui.QKeySequence.Paste):
895 self.paste()
897 self.paste()
896 intercepted = True
898 intercepted = True
897
899
898 #------ Special modifier logic -----------------------------------------
900 #------ Special modifier logic -----------------------------------------
899
901
900 elif key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
902 elif key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
901 intercepted = True
903 intercepted = True
902
904
903 # Special handling when tab completing in text mode.
905 # Special handling when tab completing in text mode.
904 self._cancel_text_completion()
906 self._cancel_text_completion()
905
907
906 if self._in_buffer(position):
908 if self._in_buffer(position):
907 if self._reading:
909 if self._reading:
908 self._append_plain_text('\n')
910 self._append_plain_text('\n')
909 self._reading = False
911 self._reading = False
910 if self._reading_callback:
912 if self._reading_callback:
911 self._reading_callback()
913 self._reading_callback()
912
914
913 # If the input buffer is a single line or there is only
915 # If the input buffer is a single line or there is only
914 # whitespace after the cursor, execute. Otherwise, split the
916 # whitespace after the cursor, execute. Otherwise, split the
915 # line with a continuation prompt.
917 # line with a continuation prompt.
916 elif not self._executing:
918 elif not self._executing:
917 cursor.movePosition(QtGui.QTextCursor.End,
919 cursor.movePosition(QtGui.QTextCursor.End,
918 QtGui.QTextCursor.KeepAnchor)
920 QtGui.QTextCursor.KeepAnchor)
919 at_end = len(cursor.selectedText().strip()) == 0
921 at_end = len(cursor.selectedText().strip()) == 0
920 single_line = (self._get_end_cursor().blockNumber() ==
922 single_line = (self._get_end_cursor().blockNumber() ==
921 self._get_prompt_cursor().blockNumber())
923 self._get_prompt_cursor().blockNumber())
922 if (at_end or shift_down or single_line) and not ctrl_down:
924 if (at_end or shift_down or single_line) and not ctrl_down:
923 self.execute(interactive = not shift_down)
925 self.execute(interactive = not shift_down)
924 else:
926 else:
925 # Do this inside an edit block for clean undo/redo.
927 # Do this inside an edit block for clean undo/redo.
926 cursor.beginEditBlock()
928 cursor.beginEditBlock()
927 cursor.setPosition(position)
929 cursor.setPosition(position)
928 cursor.insertText('\n')
930 cursor.insertText('\n')
929 self._insert_continuation_prompt(cursor)
931 self._insert_continuation_prompt(cursor)
930 cursor.endEditBlock()
932 cursor.endEditBlock()
931
933
932 # Ensure that the whole input buffer is visible.
934 # Ensure that the whole input buffer is visible.
933 # FIXME: This will not be usable if the input buffer is
935 # FIXME: This will not be usable if the input buffer is
934 # taller than the console widget.
936 # taller than the console widget.
935 self._control.moveCursor(QtGui.QTextCursor.End)
937 self._control.moveCursor(QtGui.QTextCursor.End)
936 self._control.setTextCursor(cursor)
938 self._control.setTextCursor(cursor)
937
939
938 #------ Control/Cmd modifier -------------------------------------------
940 #------ Control/Cmd modifier -------------------------------------------
939
941
940 elif ctrl_down:
942 elif ctrl_down:
941 if key == QtCore.Qt.Key_G:
943 if key == QtCore.Qt.Key_G:
942 self._keyboard_quit()
944 self._keyboard_quit()
943 intercepted = True
945 intercepted = True
944
946
945 elif key == QtCore.Qt.Key_K:
947 elif key == QtCore.Qt.Key_K:
946 if self._in_buffer(position):
948 if self._in_buffer(position):
947 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
949 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
948 QtGui.QTextCursor.KeepAnchor)
950 QtGui.QTextCursor.KeepAnchor)
949 if not cursor.hasSelection():
951 if not cursor.hasSelection():
950 # Line deletion (remove continuation prompt)
952 # Line deletion (remove continuation prompt)
951 cursor.movePosition(QtGui.QTextCursor.NextBlock,
953 cursor.movePosition(QtGui.QTextCursor.NextBlock,
952 QtGui.QTextCursor.KeepAnchor)
954 QtGui.QTextCursor.KeepAnchor)
953 cursor.movePosition(QtGui.QTextCursor.Right,
955 cursor.movePosition(QtGui.QTextCursor.Right,
954 QtGui.QTextCursor.KeepAnchor,
956 QtGui.QTextCursor.KeepAnchor,
955 len(self._continuation_prompt))
957 len(self._continuation_prompt))
956 cursor.removeSelectedText()
958 self._kill_ring.kill_cursor(cursor)
957 intercepted = True
959 intercepted = True
958
960
959 elif key == QtCore.Qt.Key_L:
961 elif key == QtCore.Qt.Key_L:
960 self.prompt_to_top()
962 self.prompt_to_top()
961 intercepted = True
963 intercepted = True
962
964
963 elif key == QtCore.Qt.Key_O:
965 elif key == QtCore.Qt.Key_O:
964 if self._page_control and self._page_control.isVisible():
966 if self._page_control and self._page_control.isVisible():
965 self._page_control.setFocus()
967 self._page_control.setFocus()
966 intercepted = True
968 intercepted = True
967
969
968 elif key == QtCore.Qt.Key_U:
970 elif key == QtCore.Qt.Key_U:
969 if self._in_buffer(position):
971 if self._in_buffer(position):
970 start_line = cursor.blockNumber()
972 start_line = cursor.blockNumber()
971 if start_line == self._get_prompt_cursor().blockNumber():
973 if start_line == self._get_prompt_cursor().blockNumber():
972 offset = len(self._prompt)
974 offset = len(self._prompt)
973 else:
975 else:
974 offset = len(self._continuation_prompt)
976 offset = len(self._continuation_prompt)
975 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
977 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
976 QtGui.QTextCursor.KeepAnchor)
978 QtGui.QTextCursor.KeepAnchor)
977 cursor.movePosition(QtGui.QTextCursor.Right,
979 cursor.movePosition(QtGui.QTextCursor.Right,
978 QtGui.QTextCursor.KeepAnchor, offset)
980 QtGui.QTextCursor.KeepAnchor, offset)
979 cursor.removeSelectedText()
981 self._kill_ring.kill_cursor(cursor)
980 intercepted = True
982 intercepted = True
981
983
982 elif key == QtCore.Qt.Key_Y:
984 elif key == QtCore.Qt.Key_Y:
983 self.paste()
985 self._keep_cursor_in_buffer()
986 self._kill_ring.yank()
984 intercepted = True
987 intercepted = True
985
988
986 elif key in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete):
989 elif key in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete):
987 intercepted = True
990 intercepted = True
988
991
989 elif key == QtCore.Qt.Key_Plus:
992 elif key == QtCore.Qt.Key_Plus:
990 self.change_font_size(1)
993 self.change_font_size(1)
991 intercepted = True
994 intercepted = True
992
995
993 elif key == QtCore.Qt.Key_Minus:
996 elif key == QtCore.Qt.Key_Minus:
994 self.change_font_size(-1)
997 self.change_font_size(-1)
995 intercepted = True
998 intercepted = True
996
999
997 #------ Alt modifier ---------------------------------------------------
1000 #------ Alt modifier ---------------------------------------------------
998
1001
999 elif alt_down:
1002 elif alt_down:
1000 if key == QtCore.Qt.Key_B:
1003 if key == QtCore.Qt.Key_B:
1001 self._set_cursor(self._get_word_start_cursor(position))
1004 self._set_cursor(self._get_word_start_cursor(position))
1002 intercepted = True
1005 intercepted = True
1003
1006
1004 elif key == QtCore.Qt.Key_F:
1007 elif key == QtCore.Qt.Key_F:
1005 self._set_cursor(self._get_word_end_cursor(position))
1008 self._set_cursor(self._get_word_end_cursor(position))
1006 intercepted = True
1009 intercepted = True
1007
1010
1011 elif key == QtCore.Qt.Key_Y:
1012 self._kill_ring.rotate()
1013 intercepted = True
1014
1008 elif key == QtCore.Qt.Key_Backspace:
1015 elif key == QtCore.Qt.Key_Backspace:
1009 cursor = self._get_word_start_cursor(position)
1016 cursor = self._get_word_start_cursor(position)
1010 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1017 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1011 cursor.removeSelectedText()
1018 self._kill_ring.kill_cursor(cursor)
1012 intercepted = True
1019 intercepted = True
1013
1020
1014 elif key == QtCore.Qt.Key_D:
1021 elif key == QtCore.Qt.Key_D:
1015 cursor = self._get_word_end_cursor(position)
1022 cursor = self._get_word_end_cursor(position)
1016 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1023 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1017 cursor.removeSelectedText()
1024 self._kill_ring.kill_cursor(cursor)
1018 intercepted = True
1025 intercepted = True
1019
1026
1020 elif key == QtCore.Qt.Key_Delete:
1027 elif key == QtCore.Qt.Key_Delete:
1021 intercepted = True
1028 intercepted = True
1022
1029
1023 elif key == QtCore.Qt.Key_Greater:
1030 elif key == QtCore.Qt.Key_Greater:
1024 self._control.moveCursor(QtGui.QTextCursor.End)
1031 self._control.moveCursor(QtGui.QTextCursor.End)
1025 intercepted = True
1032 intercepted = True
1026
1033
1027 elif key == QtCore.Qt.Key_Less:
1034 elif key == QtCore.Qt.Key_Less:
1028 self._control.setTextCursor(self._get_prompt_cursor())
1035 self._control.setTextCursor(self._get_prompt_cursor())
1029 intercepted = True
1036 intercepted = True
1030
1037
1031 #------ No modifiers ---------------------------------------------------
1038 #------ No modifiers ---------------------------------------------------
1032
1039
1033 else:
1040 else:
1034 if shift_down:
1041 if shift_down:
1035 anchormode = QtGui.QTextCursor.KeepAnchor
1042 anchormode = QtGui.QTextCursor.KeepAnchor
1036 else:
1043 else:
1037 anchormode = QtGui.QTextCursor.MoveAnchor
1044 anchormode = QtGui.QTextCursor.MoveAnchor
1038
1045
1039 if key == QtCore.Qt.Key_Escape:
1046 if key == QtCore.Qt.Key_Escape:
1040 self._keyboard_quit()
1047 self._keyboard_quit()
1041 intercepted = True
1048 intercepted = True
1042
1049
1043 elif key == QtCore.Qt.Key_Up:
1050 elif key == QtCore.Qt.Key_Up:
1044 if self._reading or not self._up_pressed(shift_down):
1051 if self._reading or not self._up_pressed(shift_down):
1045 intercepted = True
1052 intercepted = True
1046 else:
1053 else:
1047 prompt_line = self._get_prompt_cursor().blockNumber()
1054 prompt_line = self._get_prompt_cursor().blockNumber()
1048 intercepted = cursor.blockNumber() <= prompt_line
1055 intercepted = cursor.blockNumber() <= prompt_line
1049
1056
1050 elif key == QtCore.Qt.Key_Down:
1057 elif key == QtCore.Qt.Key_Down:
1051 if self._reading or not self._down_pressed(shift_down):
1058 if self._reading or not self._down_pressed(shift_down):
1052 intercepted = True
1059 intercepted = True
1053 else:
1060 else:
1054 end_line = self._get_end_cursor().blockNumber()
1061 end_line = self._get_end_cursor().blockNumber()
1055 intercepted = cursor.blockNumber() == end_line
1062 intercepted = cursor.blockNumber() == end_line
1056
1063
1057 elif key == QtCore.Qt.Key_Tab:
1064 elif key == QtCore.Qt.Key_Tab:
1058 if not self._reading:
1065 if not self._reading:
1059 intercepted = not self._tab_pressed()
1066 intercepted = not self._tab_pressed()
1060
1067
1061 elif key == QtCore.Qt.Key_Left:
1068 elif key == QtCore.Qt.Key_Left:
1062
1069
1063 # Move to the previous line
1070 # Move to the previous line
1064 line, col = cursor.blockNumber(), cursor.columnNumber()
1071 line, col = cursor.blockNumber(), cursor.columnNumber()
1065 if line > self._get_prompt_cursor().blockNumber() and \
1072 if line > self._get_prompt_cursor().blockNumber() and \
1066 col == len(self._continuation_prompt):
1073 col == len(self._continuation_prompt):
1067 self._control.moveCursor(QtGui.QTextCursor.PreviousBlock,
1074 self._control.moveCursor(QtGui.QTextCursor.PreviousBlock,
1068 mode=anchormode)
1075 mode=anchormode)
1069 self._control.moveCursor(QtGui.QTextCursor.EndOfBlock,
1076 self._control.moveCursor(QtGui.QTextCursor.EndOfBlock,
1070 mode=anchormode)
1077 mode=anchormode)
1071 intercepted = True
1078 intercepted = True
1072
1079
1073 # Regular left movement
1080 # Regular left movement
1074 else:
1081 else:
1075 intercepted = not self._in_buffer(position - 1)
1082 intercepted = not self._in_buffer(position - 1)
1076
1083
1077 elif key == QtCore.Qt.Key_Right:
1084 elif key == QtCore.Qt.Key_Right:
1078 original_block_number = cursor.blockNumber()
1085 original_block_number = cursor.blockNumber()
1079 cursor.movePosition(QtGui.QTextCursor.Right,
1086 cursor.movePosition(QtGui.QTextCursor.Right,
1080 mode=anchormode)
1087 mode=anchormode)
1081 if cursor.blockNumber() != original_block_number:
1088 if cursor.blockNumber() != original_block_number:
1082 cursor.movePosition(QtGui.QTextCursor.Right,
1089 cursor.movePosition(QtGui.QTextCursor.Right,
1083 n=len(self._continuation_prompt),
1090 n=len(self._continuation_prompt),
1084 mode=anchormode)
1091 mode=anchormode)
1085 self._set_cursor(cursor)
1092 self._set_cursor(cursor)
1086 intercepted = True
1093 intercepted = True
1087
1094
1088 elif key == QtCore.Qt.Key_Home:
1095 elif key == QtCore.Qt.Key_Home:
1089 start_line = cursor.blockNumber()
1096 start_line = cursor.blockNumber()
1090 if start_line == self._get_prompt_cursor().blockNumber():
1097 if start_line == self._get_prompt_cursor().blockNumber():
1091 start_pos = self._prompt_pos
1098 start_pos = self._prompt_pos
1092 else:
1099 else:
1093 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1100 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1094 QtGui.QTextCursor.KeepAnchor)
1101 QtGui.QTextCursor.KeepAnchor)
1095 start_pos = cursor.position()
1102 start_pos = cursor.position()
1096 start_pos += len(self._continuation_prompt)
1103 start_pos += len(self._continuation_prompt)
1097 cursor.setPosition(position)
1104 cursor.setPosition(position)
1098 if shift_down and self._in_buffer(position):
1105 if shift_down and self._in_buffer(position):
1099 cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor)
1106 cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor)
1100 else:
1107 else:
1101 cursor.setPosition(start_pos)
1108 cursor.setPosition(start_pos)
1102 self._set_cursor(cursor)
1109 self._set_cursor(cursor)
1103 intercepted = True
1110 intercepted = True
1104
1111
1105 elif key == QtCore.Qt.Key_Backspace:
1112 elif key == QtCore.Qt.Key_Backspace:
1106
1113
1107 # Line deletion (remove continuation prompt)
1114 # Line deletion (remove continuation prompt)
1108 line, col = cursor.blockNumber(), cursor.columnNumber()
1115 line, col = cursor.blockNumber(), cursor.columnNumber()
1109 if not self._reading and \
1116 if not self._reading and \
1110 col == len(self._continuation_prompt) and \
1117 col == len(self._continuation_prompt) and \
1111 line > self._get_prompt_cursor().blockNumber():
1118 line > self._get_prompt_cursor().blockNumber():
1112 cursor.beginEditBlock()
1119 cursor.beginEditBlock()
1113 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1120 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1114 QtGui.QTextCursor.KeepAnchor)
1121 QtGui.QTextCursor.KeepAnchor)
1115 cursor.removeSelectedText()
1122 cursor.removeSelectedText()
1116 cursor.deletePreviousChar()
1123 cursor.deletePreviousChar()
1117 cursor.endEditBlock()
1124 cursor.endEditBlock()
1118 intercepted = True
1125 intercepted = True
1119
1126
1120 # Regular backwards deletion
1127 # Regular backwards deletion
1121 else:
1128 else:
1122 anchor = cursor.anchor()
1129 anchor = cursor.anchor()
1123 if anchor == position:
1130 if anchor == position:
1124 intercepted = not self._in_buffer(position - 1)
1131 intercepted = not self._in_buffer(position - 1)
1125 else:
1132 else:
1126 intercepted = not self._in_buffer(min(anchor, position))
1133 intercepted = not self._in_buffer(min(anchor, position))
1127
1134
1128 elif key == QtCore.Qt.Key_Delete:
1135 elif key == QtCore.Qt.Key_Delete:
1129
1136
1130 # Line deletion (remove continuation prompt)
1137 # Line deletion (remove continuation prompt)
1131 if not self._reading and self._in_buffer(position) and \
1138 if not self._reading and self._in_buffer(position) and \
1132 cursor.atBlockEnd() and not cursor.hasSelection():
1139 cursor.atBlockEnd() and not cursor.hasSelection():
1133 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1140 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1134 QtGui.QTextCursor.KeepAnchor)
1141 QtGui.QTextCursor.KeepAnchor)
1135 cursor.movePosition(QtGui.QTextCursor.Right,
1142 cursor.movePosition(QtGui.QTextCursor.Right,
1136 QtGui.QTextCursor.KeepAnchor,
1143 QtGui.QTextCursor.KeepAnchor,
1137 len(self._continuation_prompt))
1144 len(self._continuation_prompt))
1138 cursor.removeSelectedText()
1145 cursor.removeSelectedText()
1139 intercepted = True
1146 intercepted = True
1140
1147
1141 # Regular forwards deletion:
1148 # Regular forwards deletion:
1142 else:
1149 else:
1143 anchor = cursor.anchor()
1150 anchor = cursor.anchor()
1144 intercepted = (not self._in_buffer(anchor) or
1151 intercepted = (not self._in_buffer(anchor) or
1145 not self._in_buffer(position))
1152 not self._in_buffer(position))
1146
1153
1147 # Don't move the cursor if Control/Cmd is pressed to allow copy-paste
1154 # Don't move the cursor if Control/Cmd is pressed to allow copy-paste
1148 # using the keyboard in any part of the buffer.
1155 # using the keyboard in any part of the buffer.
1149 if not self._control_key_down(event.modifiers(), include_command=True):
1156 if not self._control_key_down(event.modifiers(), include_command=True):
1150 self._keep_cursor_in_buffer()
1157 self._keep_cursor_in_buffer()
1151
1158
1152 return intercepted
1159 return intercepted
1153
1160
1154 def _event_filter_page_keypress(self, event):
1161 def _event_filter_page_keypress(self, event):
1155 """ Filter key events for the paging widget to create console-like
1162 """ Filter key events for the paging widget to create console-like
1156 interface.
1163 interface.
1157 """
1164 """
1158 key = event.key()
1165 key = event.key()
1159 ctrl_down = self._control_key_down(event.modifiers())
1166 ctrl_down = self._control_key_down(event.modifiers())
1160 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1167 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1161
1168
1162 if ctrl_down:
1169 if ctrl_down:
1163 if key == QtCore.Qt.Key_O:
1170 if key == QtCore.Qt.Key_O:
1164 self._control.setFocus()
1171 self._control.setFocus()
1165 intercept = True
1172 intercept = True
1166
1173
1167 elif alt_down:
1174 elif alt_down:
1168 if key == QtCore.Qt.Key_Greater:
1175 if key == QtCore.Qt.Key_Greater:
1169 self._page_control.moveCursor(QtGui.QTextCursor.End)
1176 self._page_control.moveCursor(QtGui.QTextCursor.End)
1170 intercepted = True
1177 intercepted = True
1171
1178
1172 elif key == QtCore.Qt.Key_Less:
1179 elif key == QtCore.Qt.Key_Less:
1173 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1180 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1174 intercepted = True
1181 intercepted = True
1175
1182
1176 elif key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
1183 elif key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
1177 if self._splitter:
1184 if self._splitter:
1178 self._page_control.hide()
1185 self._page_control.hide()
1179 else:
1186 else:
1180 self.layout().setCurrentWidget(self._control)
1187 self.layout().setCurrentWidget(self._control)
1181 return True
1188 return True
1182
1189
1183 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
1190 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
1184 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1191 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1185 QtCore.Qt.Key_PageDown,
1192 QtCore.Qt.Key_PageDown,
1186 QtCore.Qt.NoModifier)
1193 QtCore.Qt.NoModifier)
1187 QtGui.qApp.sendEvent(self._page_control, new_event)
1194 QtGui.qApp.sendEvent(self._page_control, new_event)
1188 return True
1195 return True
1189
1196
1190 elif key == QtCore.Qt.Key_Backspace:
1197 elif key == QtCore.Qt.Key_Backspace:
1191 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1198 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1192 QtCore.Qt.Key_PageUp,
1199 QtCore.Qt.Key_PageUp,
1193 QtCore.Qt.NoModifier)
1200 QtCore.Qt.NoModifier)
1194 QtGui.qApp.sendEvent(self._page_control, new_event)
1201 QtGui.qApp.sendEvent(self._page_control, new_event)
1195 return True
1202 return True
1196
1203
1197 return False
1204 return False
1198
1205
1199 def _format_as_columns(self, items, separator=' '):
1206 def _format_as_columns(self, items, separator=' '):
1200 """ Transform a list of strings into a single string with columns.
1207 """ Transform a list of strings into a single string with columns.
1201
1208
1202 Parameters
1209 Parameters
1203 ----------
1210 ----------
1204 items : sequence of strings
1211 items : sequence of strings
1205 The strings to process.
1212 The strings to process.
1206
1213
1207 separator : str, optional [default is two spaces]
1214 separator : str, optional [default is two spaces]
1208 The string that separates columns.
1215 The string that separates columns.
1209
1216
1210 Returns
1217 Returns
1211 -------
1218 -------
1212 The formatted string.
1219 The formatted string.
1213 """
1220 """
1214 # Note: this code is adapted from columnize 0.3.2.
1221 # Note: this code is adapted from columnize 0.3.2.
1215 # See http://code.google.com/p/pycolumnize/
1222 # See http://code.google.com/p/pycolumnize/
1216
1223
1217 # Calculate the number of characters available.
1224 # Calculate the number of characters available.
1218 width = self._control.viewport().width()
1225 width = self._control.viewport().width()
1219 char_width = QtGui.QFontMetrics(self.font).width(' ')
1226 char_width = QtGui.QFontMetrics(self.font).width(' ')
1220 displaywidth = max(10, (width / char_width) - 1)
1227 displaywidth = max(10, (width / char_width) - 1)
1221
1228
1222 # Some degenerate cases.
1229 # Some degenerate cases.
1223 size = len(items)
1230 size = len(items)
1224 if size == 0:
1231 if size == 0:
1225 return '\n'
1232 return '\n'
1226 elif size == 1:
1233 elif size == 1:
1227 return '%s\n' % items[0]
1234 return '%s\n' % items[0]
1228
1235
1229 # Try every row count from 1 upwards
1236 # Try every row count from 1 upwards
1230 array_index = lambda nrows, row, col: nrows*col + row
1237 array_index = lambda nrows, row, col: nrows*col + row
1231 for nrows in range(1, size):
1238 for nrows in range(1, size):
1232 ncols = (size + nrows - 1) // nrows
1239 ncols = (size + nrows - 1) // nrows
1233 colwidths = []
1240 colwidths = []
1234 totwidth = -len(separator)
1241 totwidth = -len(separator)
1235 for col in range(ncols):
1242 for col in range(ncols):
1236 # Get max column width for this column
1243 # Get max column width for this column
1237 colwidth = 0
1244 colwidth = 0
1238 for row in range(nrows):
1245 for row in range(nrows):
1239 i = array_index(nrows, row, col)
1246 i = array_index(nrows, row, col)
1240 if i >= size: break
1247 if i >= size: break
1241 x = items[i]
1248 x = items[i]
1242 colwidth = max(colwidth, len(x))
1249 colwidth = max(colwidth, len(x))
1243 colwidths.append(colwidth)
1250 colwidths.append(colwidth)
1244 totwidth += colwidth + len(separator)
1251 totwidth += colwidth + len(separator)
1245 if totwidth > displaywidth:
1252 if totwidth > displaywidth:
1246 break
1253 break
1247 if totwidth <= displaywidth:
1254 if totwidth <= displaywidth:
1248 break
1255 break
1249
1256
1250 # The smallest number of rows computed and the max widths for each
1257 # The smallest number of rows computed and the max widths for each
1251 # column has been obtained. Now we just have to format each of the rows.
1258 # column has been obtained. Now we just have to format each of the rows.
1252 string = ''
1259 string = ''
1253 for row in range(nrows):
1260 for row in range(nrows):
1254 texts = []
1261 texts = []
1255 for col in range(ncols):
1262 for col in range(ncols):
1256 i = row + nrows*col
1263 i = row + nrows*col
1257 if i >= size:
1264 if i >= size:
1258 texts.append('')
1265 texts.append('')
1259 else:
1266 else:
1260 texts.append(items[i])
1267 texts.append(items[i])
1261 while texts and not texts[-1]:
1268 while texts and not texts[-1]:
1262 del texts[-1]
1269 del texts[-1]
1263 for col in range(len(texts)):
1270 for col in range(len(texts)):
1264 texts[col] = texts[col].ljust(colwidths[col])
1271 texts[col] = texts[col].ljust(colwidths[col])
1265 string += '%s\n' % separator.join(texts)
1272 string += '%s\n' % separator.join(texts)
1266 return string
1273 return string
1267
1274
1268 def _get_block_plain_text(self, block):
1275 def _get_block_plain_text(self, block):
1269 """ Given a QTextBlock, return its unformatted text.
1276 """ Given a QTextBlock, return its unformatted text.
1270 """
1277 """
1271 cursor = QtGui.QTextCursor(block)
1278 cursor = QtGui.QTextCursor(block)
1272 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1279 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1273 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
1280 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
1274 QtGui.QTextCursor.KeepAnchor)
1281 QtGui.QTextCursor.KeepAnchor)
1275 return cursor.selection().toPlainText()
1282 return cursor.selection().toPlainText()
1276
1283
1277 def _get_cursor(self):
1284 def _get_cursor(self):
1278 """ Convenience method that returns a cursor for the current position.
1285 """ Convenience method that returns a cursor for the current position.
1279 """
1286 """
1280 return self._control.textCursor()
1287 return self._control.textCursor()
1281
1288
1282 def _get_end_cursor(self):
1289 def _get_end_cursor(self):
1283 """ Convenience method that returns a cursor for the last character.
1290 """ Convenience method that returns a cursor for the last character.
1284 """
1291 """
1285 cursor = self._control.textCursor()
1292 cursor = self._control.textCursor()
1286 cursor.movePosition(QtGui.QTextCursor.End)
1293 cursor.movePosition(QtGui.QTextCursor.End)
1287 return cursor
1294 return cursor
1288
1295
1289 def _get_input_buffer_cursor_column(self):
1296 def _get_input_buffer_cursor_column(self):
1290 """ Returns the column of the cursor in the input buffer, excluding the
1297 """ Returns the column of the cursor in the input buffer, excluding the
1291 contribution by the prompt, or -1 if there is no such column.
1298 contribution by the prompt, or -1 if there is no such column.
1292 """
1299 """
1293 prompt = self._get_input_buffer_cursor_prompt()
1300 prompt = self._get_input_buffer_cursor_prompt()
1294 if prompt is None:
1301 if prompt is None:
1295 return -1
1302 return -1
1296 else:
1303 else:
1297 cursor = self._control.textCursor()
1304 cursor = self._control.textCursor()
1298 return cursor.columnNumber() - len(prompt)
1305 return cursor.columnNumber() - len(prompt)
1299
1306
1300 def _get_input_buffer_cursor_line(self):
1307 def _get_input_buffer_cursor_line(self):
1301 """ Returns the text of the line of the input buffer that contains the
1308 """ Returns the text of the line of the input buffer that contains the
1302 cursor, or None if there is no such line.
1309 cursor, or None if there is no such line.
1303 """
1310 """
1304 prompt = self._get_input_buffer_cursor_prompt()
1311 prompt = self._get_input_buffer_cursor_prompt()
1305 if prompt is None:
1312 if prompt is None:
1306 return None
1313 return None
1307 else:
1314 else:
1308 cursor = self._control.textCursor()
1315 cursor = self._control.textCursor()
1309 text = self._get_block_plain_text(cursor.block())
1316 text = self._get_block_plain_text(cursor.block())
1310 return text[len(prompt):]
1317 return text[len(prompt):]
1311
1318
1312 def _get_input_buffer_cursor_prompt(self):
1319 def _get_input_buffer_cursor_prompt(self):
1313 """ Returns the (plain text) prompt for line of the input buffer that
1320 """ Returns the (plain text) prompt for line of the input buffer that
1314 contains the cursor, or None if there is no such line.
1321 contains the cursor, or None if there is no such line.
1315 """
1322 """
1316 if self._executing:
1323 if self._executing:
1317 return None
1324 return None
1318 cursor = self._control.textCursor()
1325 cursor = self._control.textCursor()
1319 if cursor.position() >= self._prompt_pos:
1326 if cursor.position() >= self._prompt_pos:
1320 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
1327 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
1321 return self._prompt
1328 return self._prompt
1322 else:
1329 else:
1323 return self._continuation_prompt
1330 return self._continuation_prompt
1324 else:
1331 else:
1325 return None
1332 return None
1326
1333
1327 def _get_prompt_cursor(self):
1334 def _get_prompt_cursor(self):
1328 """ Convenience method that returns a cursor for the prompt position.
1335 """ Convenience method that returns a cursor for the prompt position.
1329 """
1336 """
1330 cursor = self._control.textCursor()
1337 cursor = self._control.textCursor()
1331 cursor.setPosition(self._prompt_pos)
1338 cursor.setPosition(self._prompt_pos)
1332 return cursor
1339 return cursor
1333
1340
1334 def _get_selection_cursor(self, start, end):
1341 def _get_selection_cursor(self, start, end):
1335 """ Convenience method that returns a cursor with text selected between
1342 """ Convenience method that returns a cursor with text selected between
1336 the positions 'start' and 'end'.
1343 the positions 'start' and 'end'.
1337 """
1344 """
1338 cursor = self._control.textCursor()
1345 cursor = self._control.textCursor()
1339 cursor.setPosition(start)
1346 cursor.setPosition(start)
1340 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
1347 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
1341 return cursor
1348 return cursor
1342
1349
1343 def _get_word_start_cursor(self, position):
1350 def _get_word_start_cursor(self, position):
1344 """ Find the start of the word to the left the given position. If a
1351 """ Find the start of the word to the left the given position. If a
1345 sequence of non-word characters precedes the first word, skip over
1352 sequence of non-word characters precedes the first word, skip over
1346 them. (This emulates the behavior of bash, emacs, etc.)
1353 them. (This emulates the behavior of bash, emacs, etc.)
1347 """
1354 """
1348 document = self._control.document()
1355 document = self._control.document()
1349 position -= 1
1356 position -= 1
1350 while position >= self._prompt_pos and \
1357 while position >= self._prompt_pos and \
1351 not is_letter_or_number(document.characterAt(position)):
1358 not is_letter_or_number(document.characterAt(position)):
1352 position -= 1
1359 position -= 1
1353 while position >= self._prompt_pos and \
1360 while position >= self._prompt_pos and \
1354 is_letter_or_number(document.characterAt(position)):
1361 is_letter_or_number(document.characterAt(position)):
1355 position -= 1
1362 position -= 1
1356 cursor = self._control.textCursor()
1363 cursor = self._control.textCursor()
1357 cursor.setPosition(position + 1)
1364 cursor.setPosition(position + 1)
1358 return cursor
1365 return cursor
1359
1366
1360 def _get_word_end_cursor(self, position):
1367 def _get_word_end_cursor(self, position):
1361 """ Find the end of the word to the right the given position. If a
1368 """ Find the end of the word to the right the given position. If a
1362 sequence of non-word characters precedes the first word, skip over
1369 sequence of non-word characters precedes the first word, skip over
1363 them. (This emulates the behavior of bash, emacs, etc.)
1370 them. (This emulates the behavior of bash, emacs, etc.)
1364 """
1371 """
1365 document = self._control.document()
1372 document = self._control.document()
1366 end = self._get_end_cursor().position()
1373 end = self._get_end_cursor().position()
1367 while position < end and \
1374 while position < end and \
1368 not is_letter_or_number(document.characterAt(position)):
1375 not is_letter_or_number(document.characterAt(position)):
1369 position += 1
1376 position += 1
1370 while position < end and \
1377 while position < end and \
1371 is_letter_or_number(document.characterAt(position)):
1378 is_letter_or_number(document.characterAt(position)):
1372 position += 1
1379 position += 1
1373 cursor = self._control.textCursor()
1380 cursor = self._control.textCursor()
1374 cursor.setPosition(position)
1381 cursor.setPosition(position)
1375 return cursor
1382 return cursor
1376
1383
1377 def _insert_continuation_prompt(self, cursor):
1384 def _insert_continuation_prompt(self, cursor):
1378 """ Inserts new continuation prompt using the specified cursor.
1385 """ Inserts new continuation prompt using the specified cursor.
1379 """
1386 """
1380 if self._continuation_prompt_html is None:
1387 if self._continuation_prompt_html is None:
1381 self._insert_plain_text(cursor, self._continuation_prompt)
1388 self._insert_plain_text(cursor, self._continuation_prompt)
1382 else:
1389 else:
1383 self._continuation_prompt = self._insert_html_fetching_plain_text(
1390 self._continuation_prompt = self._insert_html_fetching_plain_text(
1384 cursor, self._continuation_prompt_html)
1391 cursor, self._continuation_prompt_html)
1385
1392
1386 def _insert_html(self, cursor, html):
1393 def _insert_html(self, cursor, html):
1387 """ Inserts HTML using the specified cursor in such a way that future
1394 """ Inserts HTML using the specified cursor in such a way that future
1388 formatting is unaffected.
1395 formatting is unaffected.
1389 """
1396 """
1390 cursor.beginEditBlock()
1397 cursor.beginEditBlock()
1391 cursor.insertHtml(html)
1398 cursor.insertHtml(html)
1392
1399
1393 # After inserting HTML, the text document "remembers" it's in "html
1400 # After inserting HTML, the text document "remembers" it's in "html
1394 # mode", which means that subsequent calls adding plain text will result
1401 # mode", which means that subsequent calls adding plain text will result
1395 # in unwanted formatting, lost tab characters, etc. The following code
1402 # in unwanted formatting, lost tab characters, etc. The following code
1396 # hacks around this behavior, which I consider to be a bug in Qt, by
1403 # hacks around this behavior, which I consider to be a bug in Qt, by
1397 # (crudely) resetting the document's style state.
1404 # (crudely) resetting the document's style state.
1398 cursor.movePosition(QtGui.QTextCursor.Left,
1405 cursor.movePosition(QtGui.QTextCursor.Left,
1399 QtGui.QTextCursor.KeepAnchor)
1406 QtGui.QTextCursor.KeepAnchor)
1400 if cursor.selection().toPlainText() == ' ':
1407 if cursor.selection().toPlainText() == ' ':
1401 cursor.removeSelectedText()
1408 cursor.removeSelectedText()
1402 else:
1409 else:
1403 cursor.movePosition(QtGui.QTextCursor.Right)
1410 cursor.movePosition(QtGui.QTextCursor.Right)
1404 cursor.insertText(' ', QtGui.QTextCharFormat())
1411 cursor.insertText(' ', QtGui.QTextCharFormat())
1405 cursor.endEditBlock()
1412 cursor.endEditBlock()
1406
1413
1407 def _insert_html_fetching_plain_text(self, cursor, html):
1414 def _insert_html_fetching_plain_text(self, cursor, html):
1408 """ Inserts HTML using the specified cursor, then returns its plain text
1415 """ Inserts HTML using the specified cursor, then returns its plain text
1409 version.
1416 version.
1410 """
1417 """
1411 cursor.beginEditBlock()
1418 cursor.beginEditBlock()
1412 cursor.removeSelectedText()
1419 cursor.removeSelectedText()
1413
1420
1414 start = cursor.position()
1421 start = cursor.position()
1415 self._insert_html(cursor, html)
1422 self._insert_html(cursor, html)
1416 end = cursor.position()
1423 end = cursor.position()
1417 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
1424 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
1418 text = cursor.selection().toPlainText()
1425 text = cursor.selection().toPlainText()
1419
1426
1420 cursor.setPosition(end)
1427 cursor.setPosition(end)
1421 cursor.endEditBlock()
1428 cursor.endEditBlock()
1422 return text
1429 return text
1423
1430
1424 def _insert_plain_text(self, cursor, text):
1431 def _insert_plain_text(self, cursor, text):
1425 """ Inserts plain text using the specified cursor, processing ANSI codes
1432 """ Inserts plain text using the specified cursor, processing ANSI codes
1426 if enabled.
1433 if enabled.
1427 """
1434 """
1428 cursor.beginEditBlock()
1435 cursor.beginEditBlock()
1429 if self.ansi_codes:
1436 if self.ansi_codes:
1430 for substring in self._ansi_processor.split_string(text):
1437 for substring in self._ansi_processor.split_string(text):
1431 for act in self._ansi_processor.actions:
1438 for act in self._ansi_processor.actions:
1432
1439
1433 # Unlike real terminal emulators, we don't distinguish
1440 # Unlike real terminal emulators, we don't distinguish
1434 # between the screen and the scrollback buffer. A screen
1441 # between the screen and the scrollback buffer. A screen
1435 # erase request clears everything.
1442 # erase request clears everything.
1436 if act.action == 'erase' and act.area == 'screen':
1443 if act.action == 'erase' and act.area == 'screen':
1437 cursor.select(QtGui.QTextCursor.Document)
1444 cursor.select(QtGui.QTextCursor.Document)
1438 cursor.removeSelectedText()
1445 cursor.removeSelectedText()
1439
1446
1440 # Simulate a form feed by scrolling just past the last line.
1447 # Simulate a form feed by scrolling just past the last line.
1441 elif act.action == 'scroll' and act.unit == 'page':
1448 elif act.action == 'scroll' and act.unit == 'page':
1442 cursor.insertText('\n')
1449 cursor.insertText('\n')
1443 cursor.endEditBlock()
1450 cursor.endEditBlock()
1444 self._set_top_cursor(cursor)
1451 self._set_top_cursor(cursor)
1445 cursor.joinPreviousEditBlock()
1452 cursor.joinPreviousEditBlock()
1446 cursor.deletePreviousChar()
1453 cursor.deletePreviousChar()
1447
1454
1448 format = self._ansi_processor.get_format()
1455 format = self._ansi_processor.get_format()
1449 cursor.insertText(substring, format)
1456 cursor.insertText(substring, format)
1450 else:
1457 else:
1451 cursor.insertText(text)
1458 cursor.insertText(text)
1452 cursor.endEditBlock()
1459 cursor.endEditBlock()
1453
1460
1454 def _insert_plain_text_into_buffer(self, cursor, text):
1461 def _insert_plain_text_into_buffer(self, cursor, text):
1455 """ Inserts text into the input buffer using the specified cursor (which
1462 """ Inserts text into the input buffer using the specified cursor (which
1456 must be in the input buffer), ensuring that continuation prompts are
1463 must be in the input buffer), ensuring that continuation prompts are
1457 inserted as necessary.
1464 inserted as necessary.
1458 """
1465 """
1459 lines = text.splitlines(True)
1466 lines = text.splitlines(True)
1460 if lines:
1467 if lines:
1461 cursor.beginEditBlock()
1468 cursor.beginEditBlock()
1462 cursor.insertText(lines[0])
1469 cursor.insertText(lines[0])
1463 for line in lines[1:]:
1470 for line in lines[1:]:
1464 if self._continuation_prompt_html is None:
1471 if self._continuation_prompt_html is None:
1465 cursor.insertText(self._continuation_prompt)
1472 cursor.insertText(self._continuation_prompt)
1466 else:
1473 else:
1467 self._continuation_prompt = \
1474 self._continuation_prompt = \
1468 self._insert_html_fetching_plain_text(
1475 self._insert_html_fetching_plain_text(
1469 cursor, self._continuation_prompt_html)
1476 cursor, self._continuation_prompt_html)
1470 cursor.insertText(line)
1477 cursor.insertText(line)
1471 cursor.endEditBlock()
1478 cursor.endEditBlock()
1472
1479
1473 def _in_buffer(self, position=None):
1480 def _in_buffer(self, position=None):
1474 """ Returns whether the current cursor (or, if specified, a position) is
1481 """ Returns whether the current cursor (or, if specified, a position) is
1475 inside the editing region.
1482 inside the editing region.
1476 """
1483 """
1477 cursor = self._control.textCursor()
1484 cursor = self._control.textCursor()
1478 if position is None:
1485 if position is None:
1479 position = cursor.position()
1486 position = cursor.position()
1480 else:
1487 else:
1481 cursor.setPosition(position)
1488 cursor.setPosition(position)
1482 line = cursor.blockNumber()
1489 line = cursor.blockNumber()
1483 prompt_line = self._get_prompt_cursor().blockNumber()
1490 prompt_line = self._get_prompt_cursor().blockNumber()
1484 if line == prompt_line:
1491 if line == prompt_line:
1485 return position >= self._prompt_pos
1492 return position >= self._prompt_pos
1486 elif line > prompt_line:
1493 elif line > prompt_line:
1487 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1494 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1488 prompt_pos = cursor.position() + len(self._continuation_prompt)
1495 prompt_pos = cursor.position() + len(self._continuation_prompt)
1489 return position >= prompt_pos
1496 return position >= prompt_pos
1490 return False
1497 return False
1491
1498
1492 def _keep_cursor_in_buffer(self):
1499 def _keep_cursor_in_buffer(self):
1493 """ Ensures that the cursor is inside the editing region. Returns
1500 """ Ensures that the cursor is inside the editing region. Returns
1494 whether the cursor was moved.
1501 whether the cursor was moved.
1495 """
1502 """
1496 moved = not self._in_buffer()
1503 moved = not self._in_buffer()
1497 if moved:
1504 if moved:
1498 cursor = self._control.textCursor()
1505 cursor = self._control.textCursor()
1499 cursor.movePosition(QtGui.QTextCursor.End)
1506 cursor.movePosition(QtGui.QTextCursor.End)
1500 self._control.setTextCursor(cursor)
1507 self._control.setTextCursor(cursor)
1501 return moved
1508 return moved
1502
1509
1503 def _keyboard_quit(self):
1510 def _keyboard_quit(self):
1504 """ Cancels the current editing task ala Ctrl-G in Emacs.
1511 """ Cancels the current editing task ala Ctrl-G in Emacs.
1505 """
1512 """
1506 if self._text_completing_pos:
1513 if self._text_completing_pos:
1507 self._cancel_text_completion()
1514 self._cancel_text_completion()
1508 else:
1515 else:
1509 self.input_buffer = ''
1516 self.input_buffer = ''
1510
1517
1511 def _page(self, text, html=False):
1518 def _page(self, text, html=False):
1512 """ Displays text using the pager if it exceeds the height of the
1519 """ Displays text using the pager if it exceeds the height of the
1513 viewport.
1520 viewport.
1514
1521
1515 Parameters:
1522 Parameters:
1516 -----------
1523 -----------
1517 html : bool, optional (default False)
1524 html : bool, optional (default False)
1518 If set, the text will be interpreted as HTML instead of plain text.
1525 If set, the text will be interpreted as HTML instead of plain text.
1519 """
1526 """
1520 line_height = QtGui.QFontMetrics(self.font).height()
1527 line_height = QtGui.QFontMetrics(self.font).height()
1521 minlines = self._control.viewport().height() / line_height
1528 minlines = self._control.viewport().height() / line_height
1522 if self.paging != 'none' and \
1529 if self.paging != 'none' and \
1523 re.match("(?:[^\n]*\n){%i}" % minlines, text):
1530 re.match("(?:[^\n]*\n){%i}" % minlines, text):
1524 if self.paging == 'custom':
1531 if self.paging == 'custom':
1525 self.custom_page_requested.emit(text)
1532 self.custom_page_requested.emit(text)
1526 else:
1533 else:
1527 self._page_control.clear()
1534 self._page_control.clear()
1528 cursor = self._page_control.textCursor()
1535 cursor = self._page_control.textCursor()
1529 if html:
1536 if html:
1530 self._insert_html(cursor, text)
1537 self._insert_html(cursor, text)
1531 else:
1538 else:
1532 self._insert_plain_text(cursor, text)
1539 self._insert_plain_text(cursor, text)
1533 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1540 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1534
1541
1535 self._page_control.viewport().resize(self._control.size())
1542 self._page_control.viewport().resize(self._control.size())
1536 if self._splitter:
1543 if self._splitter:
1537 self._page_control.show()
1544 self._page_control.show()
1538 self._page_control.setFocus()
1545 self._page_control.setFocus()
1539 else:
1546 else:
1540 self.layout().setCurrentWidget(self._page_control)
1547 self.layout().setCurrentWidget(self._page_control)
1541 elif html:
1548 elif html:
1542 self._append_plain_html(text)
1549 self._append_plain_html(text)
1543 else:
1550 else:
1544 self._append_plain_text(text)
1551 self._append_plain_text(text)
1545
1552
1546 def _prompt_finished(self):
1553 def _prompt_finished(self):
1547 """ Called immediately after a prompt is finished, i.e. when some input
1554 """ Called immediately after a prompt is finished, i.e. when some input
1548 will be processed and a new prompt displayed.
1555 will be processed and a new prompt displayed.
1549 """
1556 """
1550 self._control.setReadOnly(True)
1557 self._control.setReadOnly(True)
1551 self._prompt_finished_hook()
1558 self._prompt_finished_hook()
1552
1559
1553 def _prompt_started(self):
1560 def _prompt_started(self):
1554 """ Called immediately after a new prompt is displayed.
1561 """ Called immediately after a new prompt is displayed.
1555 """
1562 """
1556 # Temporarily disable the maximum block count to permit undo/redo and
1563 # Temporarily disable the maximum block count to permit undo/redo and
1557 # to ensure that the prompt position does not change due to truncation.
1564 # to ensure that the prompt position does not change due to truncation.
1558 self._control.document().setMaximumBlockCount(0)
1565 self._control.document().setMaximumBlockCount(0)
1559 self._control.setUndoRedoEnabled(True)
1566 self._control.setUndoRedoEnabled(True)
1560
1567
1561 # Work around bug in QPlainTextEdit: input method is not re-enabled
1568 # Work around bug in QPlainTextEdit: input method is not re-enabled
1562 # when read-only is disabled.
1569 # when read-only is disabled.
1563 self._control.setReadOnly(False)
1570 self._control.setReadOnly(False)
1564 self._control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
1571 self._control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
1565
1572
1566 self._control.moveCursor(QtGui.QTextCursor.End)
1573 self._control.moveCursor(QtGui.QTextCursor.End)
1567 self._executing = False
1574 self._executing = False
1568 self._prompt_started_hook()
1575 self._prompt_started_hook()
1569
1576
1570 def _readline(self, prompt='', callback=None):
1577 def _readline(self, prompt='', callback=None):
1571 """ Reads one line of input from the user.
1578 """ Reads one line of input from the user.
1572
1579
1573 Parameters
1580 Parameters
1574 ----------
1581 ----------
1575 prompt : str, optional
1582 prompt : str, optional
1576 The prompt to print before reading the line.
1583 The prompt to print before reading the line.
1577
1584
1578 callback : callable, optional
1585 callback : callable, optional
1579 A callback to execute with the read line. If not specified, input is
1586 A callback to execute with the read line. If not specified, input is
1580 read *synchronously* and this method does not return until it has
1587 read *synchronously* and this method does not return until it has
1581 been read.
1588 been read.
1582
1589
1583 Returns
1590 Returns
1584 -------
1591 -------
1585 If a callback is specified, returns nothing. Otherwise, returns the
1592 If a callback is specified, returns nothing. Otherwise, returns the
1586 input string with the trailing newline stripped.
1593 input string with the trailing newline stripped.
1587 """
1594 """
1588 if self._reading:
1595 if self._reading:
1589 raise RuntimeError('Cannot read a line. Widget is already reading.')
1596 raise RuntimeError('Cannot read a line. Widget is already reading.')
1590
1597
1591 if not callback and not self.isVisible():
1598 if not callback and not self.isVisible():
1592 # If the user cannot see the widget, this function cannot return.
1599 # If the user cannot see the widget, this function cannot return.
1593 raise RuntimeError('Cannot synchronously read a line if the widget '
1600 raise RuntimeError('Cannot synchronously read a line if the widget '
1594 'is not visible!')
1601 'is not visible!')
1595
1602
1596 self._reading = True
1603 self._reading = True
1597 self._show_prompt(prompt, newline=False)
1604 self._show_prompt(prompt, newline=False)
1598
1605
1599 if callback is None:
1606 if callback is None:
1600 self._reading_callback = None
1607 self._reading_callback = None
1601 while self._reading:
1608 while self._reading:
1602 QtCore.QCoreApplication.processEvents()
1609 QtCore.QCoreApplication.processEvents()
1603 return self.input_buffer.rstrip('\n')
1610 return self.input_buffer.rstrip('\n')
1604
1611
1605 else:
1612 else:
1606 self._reading_callback = lambda: \
1613 self._reading_callback = lambda: \
1607 callback(self.input_buffer.rstrip('\n'))
1614 callback(self.input_buffer.rstrip('\n'))
1608
1615
1609 def _set_continuation_prompt(self, prompt, html=False):
1616 def _set_continuation_prompt(self, prompt, html=False):
1610 """ Sets the continuation prompt.
1617 """ Sets the continuation prompt.
1611
1618
1612 Parameters
1619 Parameters
1613 ----------
1620 ----------
1614 prompt : str
1621 prompt : str
1615 The prompt to show when more input is needed.
1622 The prompt to show when more input is needed.
1616
1623
1617 html : bool, optional (default False)
1624 html : bool, optional (default False)
1618 If set, the prompt will be inserted as formatted HTML. Otherwise,
1625 If set, the prompt will be inserted as formatted HTML. Otherwise,
1619 the prompt will be treated as plain text, though ANSI color codes
1626 the prompt will be treated as plain text, though ANSI color codes
1620 will be handled.
1627 will be handled.
1621 """
1628 """
1622 if html:
1629 if html:
1623 self._continuation_prompt_html = prompt
1630 self._continuation_prompt_html = prompt
1624 else:
1631 else:
1625 self._continuation_prompt = prompt
1632 self._continuation_prompt = prompt
1626 self._continuation_prompt_html = None
1633 self._continuation_prompt_html = None
1627
1634
1628 def _set_cursor(self, cursor):
1635 def _set_cursor(self, cursor):
1629 """ Convenience method to set the current cursor.
1636 """ Convenience method to set the current cursor.
1630 """
1637 """
1631 self._control.setTextCursor(cursor)
1638 self._control.setTextCursor(cursor)
1632
1639
1633 def _set_top_cursor(self, cursor):
1640 def _set_top_cursor(self, cursor):
1634 """ Scrolls the viewport so that the specified cursor is at the top.
1641 """ Scrolls the viewport so that the specified cursor is at the top.
1635 """
1642 """
1636 scrollbar = self._control.verticalScrollBar()
1643 scrollbar = self._control.verticalScrollBar()
1637 scrollbar.setValue(scrollbar.maximum())
1644 scrollbar.setValue(scrollbar.maximum())
1638 original_cursor = self._control.textCursor()
1645 original_cursor = self._control.textCursor()
1639 self._control.setTextCursor(cursor)
1646 self._control.setTextCursor(cursor)
1640 self._control.ensureCursorVisible()
1647 self._control.ensureCursorVisible()
1641 self._control.setTextCursor(original_cursor)
1648 self._control.setTextCursor(original_cursor)
1642
1649
1643 def _show_prompt(self, prompt=None, html=False, newline=True):
1650 def _show_prompt(self, prompt=None, html=False, newline=True):
1644 """ Writes a new prompt at the end of the buffer.
1651 """ Writes a new prompt at the end of the buffer.
1645
1652
1646 Parameters
1653 Parameters
1647 ----------
1654 ----------
1648 prompt : str, optional
1655 prompt : str, optional
1649 The prompt to show. If not specified, the previous prompt is used.
1656 The prompt to show. If not specified, the previous prompt is used.
1650
1657
1651 html : bool, optional (default False)
1658 html : bool, optional (default False)
1652 Only relevant when a prompt is specified. If set, the prompt will
1659 Only relevant when a prompt is specified. If set, the prompt will
1653 be inserted as formatted HTML. Otherwise, the prompt will be treated
1660 be inserted as formatted HTML. Otherwise, the prompt will be treated
1654 as plain text, though ANSI color codes will be handled.
1661 as plain text, though ANSI color codes will be handled.
1655
1662
1656 newline : bool, optional (default True)
1663 newline : bool, optional (default True)
1657 If set, a new line will be written before showing the prompt if
1664 If set, a new line will be written before showing the prompt if
1658 there is not already a newline at the end of the buffer.
1665 there is not already a newline at the end of the buffer.
1659 """
1666 """
1660 # Insert a preliminary newline, if necessary.
1667 # Insert a preliminary newline, if necessary.
1661 if newline:
1668 if newline:
1662 cursor = self._get_end_cursor()
1669 cursor = self._get_end_cursor()
1663 if cursor.position() > 0:
1670 if cursor.position() > 0:
1664 cursor.movePosition(QtGui.QTextCursor.Left,
1671 cursor.movePosition(QtGui.QTextCursor.Left,
1665 QtGui.QTextCursor.KeepAnchor)
1672 QtGui.QTextCursor.KeepAnchor)
1666 if cursor.selection().toPlainText() != '\n':
1673 if cursor.selection().toPlainText() != '\n':
1667 self._append_plain_text('\n')
1674 self._append_plain_text('\n')
1668
1675
1669 # Write the prompt.
1676 # Write the prompt.
1670 self._append_plain_text(self._prompt_sep)
1677 self._append_plain_text(self._prompt_sep)
1671 if prompt is None:
1678 if prompt is None:
1672 if self._prompt_html is None:
1679 if self._prompt_html is None:
1673 self._append_plain_text(self._prompt)
1680 self._append_plain_text(self._prompt)
1674 else:
1681 else:
1675 self._append_html(self._prompt_html)
1682 self._append_html(self._prompt_html)
1676 else:
1683 else:
1677 if html:
1684 if html:
1678 self._prompt = self._append_html_fetching_plain_text(prompt)
1685 self._prompt = self._append_html_fetching_plain_text(prompt)
1679 self._prompt_html = prompt
1686 self._prompt_html = prompt
1680 else:
1687 else:
1681 self._append_plain_text(prompt)
1688 self._append_plain_text(prompt)
1682 self._prompt = prompt
1689 self._prompt = prompt
1683 self._prompt_html = None
1690 self._prompt_html = None
1684
1691
1685 self._prompt_pos = self._get_end_cursor().position()
1692 self._prompt_pos = self._get_end_cursor().position()
1686 self._prompt_started()
1693 self._prompt_started()
1687
1694
1688 #------ Signal handlers ----------------------------------------------------
1695 #------ Signal handlers ----------------------------------------------------
1689
1696
1690 def _adjust_scrollbars(self):
1697 def _adjust_scrollbars(self):
1691 """ Expands the vertical scrollbar beyond the range set by Qt.
1698 """ Expands the vertical scrollbar beyond the range set by Qt.
1692 """
1699 """
1693 # This code is adapted from _q_adjustScrollbars in qplaintextedit.cpp
1700 # This code is adapted from _q_adjustScrollbars in qplaintextedit.cpp
1694 # and qtextedit.cpp.
1701 # and qtextedit.cpp.
1695 document = self._control.document()
1702 document = self._control.document()
1696 scrollbar = self._control.verticalScrollBar()
1703 scrollbar = self._control.verticalScrollBar()
1697 viewport_height = self._control.viewport().height()
1704 viewport_height = self._control.viewport().height()
1698 if isinstance(self._control, QtGui.QPlainTextEdit):
1705 if isinstance(self._control, QtGui.QPlainTextEdit):
1699 maximum = max(0, document.lineCount() - 1)
1706 maximum = max(0, document.lineCount() - 1)
1700 step = viewport_height / self._control.fontMetrics().lineSpacing()
1707 step = viewport_height / self._control.fontMetrics().lineSpacing()
1701 else:
1708 else:
1702 # QTextEdit does not do line-based layout and blocks will not in
1709 # QTextEdit does not do line-based layout and blocks will not in
1703 # general have the same height. Therefore it does not make sense to
1710 # general have the same height. Therefore it does not make sense to
1704 # attempt to scroll in line height increments.
1711 # attempt to scroll in line height increments.
1705 maximum = document.size().height()
1712 maximum = document.size().height()
1706 step = viewport_height
1713 step = viewport_height
1707 diff = maximum - scrollbar.maximum()
1714 diff = maximum - scrollbar.maximum()
1708 scrollbar.setRange(0, maximum)
1715 scrollbar.setRange(0, maximum)
1709 scrollbar.setPageStep(step)
1716 scrollbar.setPageStep(step)
1710
1717
1711 # Compensate for undesirable scrolling that occurs automatically due to
1718 # Compensate for undesirable scrolling that occurs automatically due to
1712 # maximumBlockCount() text truncation.
1719 # maximumBlockCount() text truncation.
1713 if diff < 0 and document.blockCount() == document.maximumBlockCount():
1720 if diff < 0 and document.blockCount() == document.maximumBlockCount():
1714 scrollbar.setValue(scrollbar.value() + diff)
1721 scrollbar.setValue(scrollbar.value() + diff)
1715
1722
1716 def _cursor_position_changed(self):
1723 def _cursor_position_changed(self):
1717 """ Clears the temporary buffer based on the cursor position.
1724 """ Clears the temporary buffer based on the cursor position.
1718 """
1725 """
1719 if self._text_completing_pos:
1726 if self._text_completing_pos:
1720 document = self._control.document()
1727 document = self._control.document()
1721 if self._text_completing_pos < document.characterCount():
1728 if self._text_completing_pos < document.characterCount():
1722 cursor = self._control.textCursor()
1729 cursor = self._control.textCursor()
1723 pos = cursor.position()
1730 pos = cursor.position()
1724 text_cursor = self._control.textCursor()
1731 text_cursor = self._control.textCursor()
1725 text_cursor.setPosition(self._text_completing_pos)
1732 text_cursor.setPosition(self._text_completing_pos)
1726 if pos < self._text_completing_pos or \
1733 if pos < self._text_completing_pos or \
1727 cursor.blockNumber() > text_cursor.blockNumber():
1734 cursor.blockNumber() > text_cursor.blockNumber():
1728 self._clear_temporary_buffer()
1735 self._clear_temporary_buffer()
1729 self._text_completing_pos = 0
1736 self._text_completing_pos = 0
1730 else:
1737 else:
1731 self._clear_temporary_buffer()
1738 self._clear_temporary_buffer()
1732 self._text_completing_pos = 0
1739 self._text_completing_pos = 0
1733
1740
1734 def _custom_context_menu_requested(self, pos):
1741 def _custom_context_menu_requested(self, pos):
1735 """ Shows a context menu at the given QPoint (in widget coordinates).
1742 """ Shows a context menu at the given QPoint (in widget coordinates).
1736 """
1743 """
1737 menu = self._context_menu_make(pos)
1744 menu = self._context_menu_make(pos)
1738 menu.exec_(self._control.mapToGlobal(pos))
1745 menu.exec_(self._control.mapToGlobal(pos))
General Comments 0
You need to be logged in to leave comments. Login now