completion_html.py 13 KB

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