##// END OF EJS Templates
Merge pull request #4508 from takluyver/qtcompletion-map-fixes...
Min RK -
r13532:2f38062a merge
parent child Browse files
Show More
@@ -1,371 +1,371
1 1 """a navigable completer for the qtconsole"""
2 2 # coding : utf-8
3 3 #-----------------------------------------------------------------------------
4 4 # Copyright (c) 2012, IPython Development Team.$
5 5 #
6 6 # Distributed under the terms of the Modified BSD License.$
7 7 #
8 8 # The full license is in the file COPYING.txt, distributed with this software.
9 9 #-----------------------------------------------------------------------------
10 10
11 11 # System library imports
12 12 import IPython.utils.text as text
13 13
14 14 from IPython.external.qt import QtCore, QtGui
15 15
16 16 #--------------------------------------------------------------------------
17 17 # Return an HTML table with selected item in a special class
18 18 #--------------------------------------------------------------------------
19 19 def html_tableify(item_matrix, select=None, header=None , footer=None) :
20 20 """ returnr a string for an html table"""
21 21 if not item_matrix :
22 22 return ''
23 23 html_cols = []
24 24 tds = lambda text : u'<td>'+text+u' </td>'
25 25 trs = lambda text : u'<tr>'+text+u'</tr>'
26 tds_items = [map(tds, row) for row in item_matrix]
26 tds_items = [list(map(tds, row)) for row in item_matrix]
27 27 if select :
28 28 row, col = select
29 29 tds_items[row][col] = u'<td class="inverted">'\
30 30 +item_matrix[row][col]\
31 31 +u' </td>'
32 32 #select the right item
33 33 html_cols = map(trs, (u''.join(row) for row in tds_items))
34 34 head = ''
35 35 foot = ''
36 36 if header :
37 37 head = (u'<tr>'\
38 38 +''.join((u'<td>'+header+u'</td>')*len(item_matrix[0]))\
39 39 +'</tr>')
40 40
41 41 if footer :
42 42 foot = (u'<tr>'\
43 43 +''.join((u'<td>'+footer+u'</td>')*len(item_matrix[0]))\
44 44 +'</tr>')
45 45 html = (u'<table class="completion" style="white-space:pre">'+head+(u''.join(html_cols))+foot+u'</table>')
46 46 return html
47 47
48 48 class SlidingInterval(object):
49 49 """a bound interval that follows a cursor
50 50
51 51 internally used to scoll the completion view when the cursor
52 52 try to go beyond the edges, and show '...' when rows are hidden
53 53 """
54 54
55 55 _min = 0
56 56 _max = 1
57 57 _current = 0
58 58 def __init__(self, maximum=1, width=6, minimum=0, sticky_lenght=1):
59 59 """Create a new bounded interval
60 60
61 61 any value return by this will be bound between maximum and
62 62 minimum. usual width will be 'width', and sticky_length
63 63 set when the return interval should expand to max and min
64 64 """
65 65 self._min = minimum
66 66 self._max = maximum
67 67 self._start = 0
68 68 self._width = width
69 69 self._stop = self._start+self._width+1
70 70 self._sticky_lenght = sticky_lenght
71 71
72 72 @property
73 73 def current(self):
74 74 """current cursor position"""
75 75 return self._current
76 76
77 77 @current.setter
78 78 def current(self, value):
79 79 """set current cursor position"""
80 80 current = min(max(self._min, value), self._max)
81 81
82 82 self._current = current
83 83
84 84 if current > self._stop :
85 85 self._stop = current
86 86 self._start = current-self._width
87 87 elif current < self._start :
88 88 self._start = current
89 89 self._stop = current + self._width
90 90
91 91 if abs(self._start - self._min) <= self._sticky_lenght :
92 92 self._start = self._min
93 93
94 94 if abs(self._stop - self._max) <= self._sticky_lenght :
95 95 self._stop = self._max
96 96
97 97 @property
98 98 def start(self):
99 99 """begiiing of interval to show"""
100 100 return self._start
101 101
102 102 @property
103 103 def stop(self):
104 104 """end of interval to show"""
105 105 return self._stop
106 106
107 107 @property
108 108 def width(self):
109 109 return self._stop - self._start
110 110
111 111 @property
112 112 def nth(self):
113 113 return self.current - self.start
114 114
115 115 class CompletionHtml(QtGui.QWidget):
116 116 """ A widget for tab completion, navigable by arrow keys """
117 117
118 118 #--------------------------------------------------------------------------
119 119 # 'QObject' interface
120 120 #--------------------------------------------------------------------------
121 121
122 122 _items = ()
123 123 _index = (0, 0)
124 124 _consecutive_tab = 0
125 125 _size = (1, 1)
126 126 _old_cursor = None
127 127 _start_position = 0
128 128 _slice_start = 0
129 129 _slice_len = 4
130 130
131 131 def __init__(self, console_widget):
132 132 """ Create a completion widget that is attached to the specified Qt
133 133 text edit widget.
134 134 """
135 135 assert isinstance(console_widget._control, (QtGui.QTextEdit, QtGui.QPlainTextEdit))
136 136 super(CompletionHtml, self).__init__()
137 137
138 138 self._text_edit = console_widget._control
139 139 self._console_widget = console_widget
140 140 self._text_edit.installEventFilter(self)
141 141 self._sliding_interval = None
142 142 self._justified_items = None
143 143
144 144 # Ensure that the text edit keeps focus when widget is displayed.
145 145 self.setFocusProxy(self._text_edit)
146 146
147 147
148 148 def eventFilter(self, obj, event):
149 149 """ Reimplemented to handle keyboard input and to auto-hide when the
150 150 text edit loses focus.
151 151 """
152 152 if obj == self._text_edit:
153 153 etype = event.type()
154 154 if etype == QtCore.QEvent.KeyPress:
155 155 key = event.key()
156 156 if self._consecutive_tab == 0 and key in (QtCore.Qt.Key_Tab,):
157 157 return False
158 158 elif self._consecutive_tab == 1 and key in (QtCore.Qt.Key_Tab,):
159 159 # ok , called twice, we grab focus, and show the cursor
160 160 self._consecutive_tab = self._consecutive_tab+1
161 161 self._update_list()
162 162 return True
163 163 elif self._consecutive_tab == 2:
164 164 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
165 165 self._complete_current()
166 166 return True
167 167 if key in (QtCore.Qt.Key_Tab,):
168 168 self.select_right()
169 169 self._update_list()
170 170 return True
171 171 elif key in ( QtCore.Qt.Key_Down,):
172 172 self.select_down()
173 173 self._update_list()
174 174 return True
175 175 elif key in (QtCore.Qt.Key_Right,):
176 176 self.select_right()
177 177 self._update_list()
178 178 return True
179 179 elif key in ( QtCore.Qt.Key_Up,):
180 180 self.select_up()
181 181 self._update_list()
182 182 return True
183 183 elif key in ( QtCore.Qt.Key_Left,):
184 184 self.select_left()
185 185 self._update_list()
186 186 return True
187 187 elif key in ( QtCore.Qt.Key_Escape,):
188 188 self.cancel_completion()
189 189 return True
190 190 else :
191 191 self.cancel_completion()
192 192 else:
193 193 self.cancel_completion()
194 194
195 195 elif etype == QtCore.QEvent.FocusOut:
196 196 self.cancel_completion()
197 197
198 198 return super(CompletionHtml, self).eventFilter(obj, event)
199 199
200 200 #--------------------------------------------------------------------------
201 201 # 'CompletionHtml' interface
202 202 #--------------------------------------------------------------------------
203 203 def cancel_completion(self):
204 204 """Cancel the completion
205 205
206 206 should be called when the completer have to be dismissed
207 207
208 208 This reset internal variable, clearing the temporary buffer
209 209 of the console where the completion are shown.
210 210 """
211 211 self._consecutive_tab = 0
212 212 self._slice_start = 0
213 213 self._console_widget._clear_temporary_buffer()
214 214 self._index = (0, 0)
215 215 if(self._sliding_interval):
216 216 self._sliding_interval = None
217 217
218 218 #
219 219 # ... 2 4 4 4 4 4 4 4 4 4 4 4 4
220 220 # 2 2 4 4 4 4 4 4 4 4 4 4 4 4
221 221 #
222 222 #2 2 x x x x x x x x x x x 5 5
223 223 #6 6 x x x x x x x x x x x 5 5
224 224 #6 6 x x x x x x x x x x ? 5 5
225 225 #6 6 x x x x x x x x x x ? 1 1
226 226 #
227 227 #3 3 3 3 3 3 3 3 3 3 3 3 1 1 1 ...
228 228 #3 3 3 3 3 3 3 3 3 3 3 3 1 1 1 ...
229 229 def _select_index(self, row, col):
230 230 """Change the selection index, and make sure it stays in the right range
231 231
232 232 A little more complicated than just dooing modulo the number of row columns
233 233 to be sure to cycle through all element.
234 234
235 235 horizontaly, the element are maped like this :
236 236 to r <-- a b c d e f --> to g
237 237 to f <-- g h i j k l --> to m
238 238 to l <-- m n o p q r --> to a
239 239
240 240 and vertically
241 241 a d g j m p
242 242 b e h k n q
243 243 c f i l o r
244 244 """
245 245
246 246 nr, nc = self._size
247 247 nr = nr-1
248 248 nc = nc-1
249 249
250 250 # case 1
251 251 if (row > nr and col >= nc) or (row >= nr and col > nc):
252 252 self._select_index(0, 0)
253 253 # case 2
254 254 elif (row <= 0 and col < 0) or (row < 0 and col <= 0):
255 255 self._select_index(nr, nc)
256 256 # case 3
257 257 elif row > nr :
258 258 self._select_index(0, col+1)
259 259 # case 4
260 260 elif row < 0 :
261 261 self._select_index(nr, col-1)
262 262 # case 5
263 263 elif col > nc :
264 264 self._select_index(row+1, 0)
265 265 # case 6
266 266 elif col < 0 :
267 267 self._select_index(row-1, nc)
268 268 elif 0 <= row and row <= nr and 0 <= col and col <= nc :
269 269 self._index = (row, col)
270 270 else :
271 271 raise NotImplementedError("you'r trying to go where no completion\
272 272 have gone before : %d:%d (%d:%d)"%(row, col, nr, nc) )
273 273
274 274
275 275 @property
276 276 def _slice_end(self):
277 277 end = self._slice_start+self._slice_len
278 278 if end > len(self._items) :
279 279 return None
280 280 return end
281 281
282 282 def select_up(self):
283 283 """move cursor up"""
284 284 r, c = self._index
285 285 self._select_index(r-1, c)
286 286
287 287 def select_down(self):
288 288 """move cursor down"""
289 289 r, c = self._index
290 290 self._select_index(r+1, c)
291 291
292 292 def select_left(self):
293 293 """move cursor left"""
294 294 r, c = self._index
295 295 self._select_index(r, c-1)
296 296
297 297 def select_right(self):
298 298 """move cursor right"""
299 299 r, c = self._index
300 300 self._select_index(r, c+1)
301 301
302 302 def show_items(self, cursor, items):
303 303 """ Shows the completion widget with 'items' at the position specified
304 304 by 'cursor'.
305 305 """
306 306 if not items :
307 307 return
308 308 self._start_position = cursor.position()
309 309 self._consecutive_tab = 1
310 310 items_m, ci = text.compute_item_matrix(items, empty=' ')
311 311 self._sliding_interval = SlidingInterval(len(items_m)-1)
312 312
313 313 self._items = items_m
314 314 self._size = (ci['rows_numbers'], ci['columns_numbers'])
315 315 self._old_cursor = cursor
316 316 self._index = (0, 0)
317 317 sjoin = lambda x : [ y.ljust(w, ' ') for y, w in zip(x, ci['columns_width'])]
318 self._justified_items = map(sjoin, items_m)
318 self._justified_items = list(map(sjoin, items_m))
319 319 self._update_list(hilight=False)
320 320
321 321
322 322
323 323
324 324 def _update_list(self, hilight=True):
325 325 """ update the list of completion and hilight the currently selected completion """
326 326 self._sliding_interval.current = self._index[0]
327 327 head = None
328 328 foot = None
329 329 if self._sliding_interval.start > 0 :
330 330 head = '...'
331 331
332 332 if self._sliding_interval.stop < self._sliding_interval._max:
333 333 foot = '...'
334 334 items_m = self._justified_items[\
335 335 self._sliding_interval.start:\
336 336 self._sliding_interval.stop+1\
337 337 ]
338 338
339 339 self._console_widget._clear_temporary_buffer()
340 340 if(hilight):
341 341 sel = (self._sliding_interval.nth, self._index[1])
342 342 else :
343 343 sel = None
344 344
345 345 strng = html_tableify(items_m, select=sel, header=head, footer=foot)
346 346 self._console_widget._fill_temporary_buffer(self._old_cursor, strng, html=True)
347 347
348 348 #--------------------------------------------------------------------------
349 349 # Protected interface
350 350 #--------------------------------------------------------------------------
351 351
352 352 def _complete_current(self):
353 353 """ Perform the completion with the currently selected item.
354 354 """
355 355 i = self._index
356 356 item = self._items[i[0]][i[1]]
357 357 item = item.strip()
358 358 if item :
359 359 self._current_text_cursor().insertText(item)
360 360 self.cancel_completion()
361 361
362 362 def _current_text_cursor(self):
363 363 """ Returns a cursor with text between the start position and the
364 364 current position selected.
365 365 """
366 366 cursor = self._text_edit.textCursor()
367 367 if cursor.position() >= self._start_position:
368 368 cursor.setPosition(self._start_position,
369 369 QtGui.QTextCursor.KeepAnchor)
370 370 return cursor
371 371
General Comments 0
You need to be logged in to leave comments. Login now