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