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