# System library imports from IPython.external.qt import QtCore, QtGui import IPython.utils.html_utils as html_utils class CompletionHtml(QtGui.QWidget): """ A widget for tab completion, navigable by arrow keys """ #-------------------------------------------------------------------------- # 'QObject' interface #-------------------------------------------------------------------------- _items = () _index = (0, 0) _consecutive_tab = 0 _size = (1, 1) _old_cursor = None _start_position = 0 def __init__(self, console_widget): """ Create a completion widget that is attached to the specified Qt text edit widget. """ assert isinstance(console_widget._control, (QtGui.QTextEdit, QtGui.QPlainTextEdit)) super(CompletionHtml, self).__init__() self._text_edit = console_widget._control self._console_widget = console_widget self._text_edit.installEventFilter(self) # Ensure that the text edit keeps focus when widget is displayed. self.setFocusProxy(self._text_edit) def eventFilter(self, obj, event): """ Reimplemented to handle keyboard input and to auto-hide when the text edit loses focus. """ if obj == self._text_edit: etype = event.type() if etype == QtCore.QEvent.KeyPress: key = event.key() if self._consecutive_tab == 0 and key in (QtCore.Qt.Key_Tab,): return False elif self._consecutive_tab == 1 and key in (QtCore.Qt.Key_Tab,): # ok , called twice, we grab focus, and show the cursor self._consecutive_tab = self._consecutive_tab+1 self._update_list() return True elif self._consecutive_tab == 2: if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter): self._complete_current() return True if key in (QtCore.Qt.Key_Tab,): self.select_right() self._update_list() return True elif key in ( QtCore.Qt.Key_Down,): self.select_down() self._update_list() return True elif key in (QtCore.Qt.Key_Right,): self.select_right() self._update_list() return True elif key in ( QtCore.Qt.Key_Up,): self.select_up() self._update_list() return True elif key in ( QtCore.Qt.Key_Left,): self.select_left() self._update_list() return True else : self._cancel_completion() else: self._cancel_completion() elif etype == QtCore.QEvent.FocusOut: self._cancel_completion() return super(CompletionHtml, self).eventFilter(obj, event) #-------------------------------------------------------------------------- # 'CompletionHtml' interface #-------------------------------------------------------------------------- def _cancel_completion(self): """Cancel the completion, reseting internal variable, clearing buffer """ self._consecutive_tab = 0 self._console_widget._clear_temporary_buffer() self._index = (0, 0) # # ... 2 4 4 4 4 4 4 4 4 4 4 4 4 # 2 2 4 4 4 4 4 4 4 4 4 4 4 4 # #2 2 x x x x x x x x x x x 5 5 #6 6 x x x x x x x x x x x 5 5 #6 6 x x x x x x x x x x ? 5 5 #6 6 x x x x x x x x x x ? 1 1 # #3 3 3 3 3 3 3 3 3 3 3 3 1 1 1 ... #3 3 3 3 3 3 3 3 3 3 3 3 1 1 1 ... def _select_index(self, row, col): """Change the selection index, and make sure it stays in the right range A little more complicated than just dooing modulo the number of row columns to be sure to cycle through all element. horizontaly, the element are maped like this : to r <-- a b c d e f --> to g to f <-- g h i j k l --> to m to l <-- m n o p q r --> to a and vertically a d g j m p b e h k n q c f i l o r """ nr, nc = self._size nr = nr-1 nc = nc-1 # case 1 if (row > nr and col >= nc) or (row >= nr and col > nc): self._select_index(0, 0) # case 2 elif (row <= 0 and col < 0) or (row < 0 and col <= 0): self._select_index(nr, nc) # case 3 elif row > nr : self._select_index(0, col+1) # case 4 elif row < 0 : self._select_index(nr, col-1) # case 5 elif col > nc : self._select_index(row+1, 0) # case 6 elif col < 0 : self._select_index(row-1, nc) elif 0 <= row and row <= nr and 0 <= col and col <= nc : self._index = (row, col) else : raise NotImplementedError("you'r trying to go where no completion\ have gone before : %d:%d (%d:%d)"%(row, col, nr, nc) ) def select_up(self): """move cursor up""" r, c = self._index self._select_index(r-1, c) def select_down(self): """move cursor down""" r, c = self._index self._select_index(r+1, c) def select_left(self): """move cursor left""" r, c = self._index self._select_index(r, c-1) def select_right(self): """move cursor right""" r, c = self._index self._select_index(r, c+1) def show_items(self, cursor, items): """ Shows the completion widget with 'items' at the position specified by 'cursor'. """ if not items : return self._start_position = cursor.position() self._consecutive_tab = 1 ci = html_utils.columnize_info(items, empty=' ') self._items = ci['item_matrix'] self._size = (ci['rows_number'], ci['columns_number']) self._old_cursor = cursor self._index = (0, 0) self._update_list(hilight=False) def _update_list(self, hilight=True): """ update the list of completion and hilight the currently selected completion """ if len(self._items) > 100: items = self._items[:100] else : items = self._items items_m = items self._console_widget._clear_temporary_buffer() if(hilight): strng = html_utils.html_tableify(items_m, select=self._index) else: strng = html_utils.html_tableify(items_m, select=None) self._console_widget._fill_temporary_buffer(self._old_cursor, strng, html=True) #-------------------------------------------------------------------------- # Protected interface #-------------------------------------------------------------------------- def _complete_current(self): """ Perform the completion with the currently selected item. """ i = self._index item = self._items[i[0]][i[1]] item = item.strip() if item : self._current_text_cursor().insertText(item) self._cancel_completion() def _current_text_cursor(self): """ Returns a cursor with text between the start position and the current position selected. """ cursor = self._text_edit.textCursor() if cursor.position() >= self._start_position: cursor.setPosition(self._start_position, QtGui.QTextCursor.KeepAnchor) return cursor