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