diff --git a/IPython/frontend/qt/console/completion_html.py b/IPython/frontend/qt/console/completion_html.py new file mode 100644 index 0000000..0875627 --- /dev/null +++ b/IPython/frontend/qt/console/completion_html.py @@ -0,0 +1,226 @@ +# 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 + + diff --git a/IPython/frontend/qt/console/completion_plain.py b/IPython/frontend/qt/console/completion_plain.py new file mode 100644 index 0000000..8db0069 --- /dev/null +++ b/IPython/frontend/qt/console/completion_plain.py @@ -0,0 +1,73 @@ +# System library imports +from IPython.external.qt import QtCore, QtGui +import IPython.utils.html_utils as html_utils + + +class CompletionPlain(QtGui.QWidget): + """ A widget for tab completion, navigable by arrow keys """ + + #-------------------------------------------------------------------------- + # 'QObject' interface + #-------------------------------------------------------------------------- + + _items = () + _index = (0, 0) + _old_cursor = None + + 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(CompletionPlain, self).__init__() + + self._text_edit = console_widget._control + self._console_widget = console_widget + + self._text_edit.installEventFilter(self) + + 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: + self._cancel_completion() + + return super(CompletionPlain, self).eventFilter(obj, event) + + #-------------------------------------------------------------------------- + # 'CompletionPlain' interface + #-------------------------------------------------------------------------- + def _cancel_completion(self): + """Cancel the completion, reseting internal variable, clearing buffer """ + self._console_widget._clear_temporary_buffer() + self._index = (0, 0) + + + def show_items(self, cursor, items): + """ Shows the completion widget with 'items' at the position specified + by 'cursor'. + """ + if not items : + return + + ci = html_utils.columnize_info(items, empty=' ') + self._items = ci['item_matrix'] + self._old_cursor = cursor + self._update_list() + + + def _update_list(self): + """ 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() + strng = html_utils.html_tableify(items_m, select=None) + self._console_widget._fill_temporary_buffer(self._old_cursor, strng, html=True) diff --git a/IPython/frontend/qt/console/completion_widget.py b/IPython/frontend/qt/console/completion_widget.py index 194b843..a3846d7 100644 --- a/IPython/frontend/qt/console/completion_widget.py +++ b/IPython/frontend/qt/console/completion_widget.py @@ -10,10 +10,11 @@ class CompletionWidget(QtGui.QListWidget): # 'QObject' interface #-------------------------------------------------------------------------- - def __init__(self, text_edit): + def __init__(self, console_widget): """ Create a completion widget that is attached to the specified Qt text edit widget. """ + text_edit = console_widget._text_edit assert isinstance(text_edit, (QtGui.QTextEdit, QtGui.QPlainTextEdit)) super(CompletionWidget, self).__init__() diff --git a/IPython/frontend/qt/console/console_widget.py b/IPython/frontend/qt/console/console_widget.py index e4b3d6c..d01181a 100644 --- a/IPython/frontend/qt/console/console_widget.py +++ b/IPython/frontend/qt/console/console_widget.py @@ -5,7 +5,6 @@ #----------------------------------------------------------------------------- # Standard library imports -import os from os.path import commonprefix import re import sys @@ -23,6 +22,8 @@ from IPython.utils.text import columnize from IPython.utils.traitlets import Bool, Enum, Integer, Unicode from ansi_code_processor import QtAnsiCodeProcessor from completion_widget import CompletionWidget +from completion_html import CompletionHtml +from completion_plain import CompletionPlain from kill_ring import QtKillRing #----------------------------------------------------------------------------- @@ -65,10 +66,19 @@ class ConsoleWidget(LoggingConfigurable, QtGui.QWidget): non-positive number disables text truncation (not recommended). """ ) - gui_completion = Bool(False, config=True, - help=""" - Use a list widget instead of plain text output for tab completion. - """ + gui_completion = Enum(['plain', 'droplist', 'ncurses'], config=True, + default_value = 'ncurses', + help=""" + The type of completer to use. Valid values are: + + 'plain' : Show the availlable completion as a text list + Below the editting area. + 'droplist': Show the completion in a drop down list navigable + by the arrow keys, and from which you can select + completion by pressing Return. + 'ncurses' : Show the completion as a text list which is navigable by + `tab` and arrow keys. + """ ) # NOTE: this value can only be specified during initialization. kind = Enum(['plain', 'rich'], default_value='plain', config=True, @@ -137,12 +147,12 @@ class ConsoleWidget(LoggingConfigurable, QtGui.QWidget): font_changed = QtCore.Signal(QtGui.QFont) #------ Protected class variables ------------------------------------------ - + # control handles _control = None _page_control = None _splitter = None - + # When the control key is down, these keys are mapped. _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left, QtCore.Qt.Key_F : QtCore.Qt.Key_Right, @@ -161,6 +171,8 @@ class ConsoleWidget(LoggingConfigurable, QtGui.QWidget): [ QtCore.Qt.Key_C, QtCore.Qt.Key_G, QtCore.Qt.Key_O, QtCore.Qt.Key_V ]) + _is_completing = False + #--------------------------------------------------------------------------- # 'QObject' interface #--------------------------------------------------------------------------- @@ -211,7 +223,13 @@ class ConsoleWidget(LoggingConfigurable, QtGui.QWidget): # information for subclasses; they should be considered read-only. self._append_before_prompt_pos = 0 self._ansi_processor = QtAnsiCodeProcessor() - self._completion_widget = CompletionWidget(self._control) + if self.gui_completion == 'ncurses': + self._completion_widget = CompletionHtml(self) + elif self.gui_completion == 'droplist': + self._completion_widget = CompletionWidget(self) + elif self.gui_completion == 'plain': + self._completion_widget = CompletionPlain(self) + self._continuation_prompt = '> ' self._continuation_prompt_html = None self._executing = False @@ -228,7 +246,6 @@ class ConsoleWidget(LoggingConfigurable, QtGui.QWidget): self._reading = False self._reading_callback = None self._tab_width = 8 - self._text_completing_pos = 0 # Set a monospaced font. self.reset_font() @@ -823,12 +840,10 @@ class ConsoleWidget(LoggingConfigurable, QtGui.QWidget): """ self._append_custom(self._insert_plain_text, text, before_prompt) - def _cancel_text_completion(self): + def _cancel_completion(self): """ If text completion is progress, cancel it. """ - if self._text_completing_pos: - self._clear_temporary_buffer() - self._text_completing_pos = 0 + self._completion_widget._cancel_completion() def _clear_temporary_buffer(self): """ Clears the "temporary text" buffer, i.e. all the text following @@ -862,7 +877,7 @@ class ConsoleWidget(LoggingConfigurable, QtGui.QWidget): def _complete_with_items(self, cursor, items): """ Performs completion with 'items' at the specified cursor location. """ - self._cancel_text_completion() + self._cancel_completion() if len(items) == 1: cursor.setPosition(self._control.textCursor().position(), @@ -877,19 +892,25 @@ class ConsoleWidget(LoggingConfigurable, QtGui.QWidget): cursor.insertText(prefix) current_pos = cursor.position() - if self.gui_completion: - cursor.movePosition(QtGui.QTextCursor.Left, n=len(prefix)) - self._completion_widget.show_items(cursor, items) - else: - cursor.beginEditBlock() - self._append_plain_text('\n') - self._page(self._format_as_columns(items)) - cursor.endEditBlock() + cursor.movePosition(QtGui.QTextCursor.Left, n=len(prefix)) + self._completion_widget.show_items(cursor, items) + self._is_completing = True + + + def _fill_temporary_buffer(self, cursor, text, html=False): + """fill the area below the active editting zone with text""" + + current_pos = self._control.textCursor().position() + + cursor.beginEditBlock() + self._append_plain_text('\n') + self._page(text, html=html) + cursor.endEditBlock() + + cursor.setPosition(current_pos) + self._control.moveCursor(QtGui.QTextCursor.End) + self._control.setTextCursor(cursor) - cursor.setPosition(current_pos) - self._control.moveCursor(QtGui.QTextCursor.End) - self._control.setTextCursor(cursor) - self._text_completing_pos = current_pos def _context_menu_make(self, pos): """ Creates a context menu for the given QPoint (in widget coordinates). @@ -951,7 +972,6 @@ class ConsoleWidget(LoggingConfigurable, QtGui.QWidget): control.viewport().installEventFilter(self) # Connect signals. - control.cursorPositionChanged.connect(self._cursor_position_changed) control.customContextMenuRequested.connect( self._custom_context_menu_requested) control.copyAvailable.connect(self.copy_available) @@ -1021,7 +1041,7 @@ class ConsoleWidget(LoggingConfigurable, QtGui.QWidget): intercepted = True # Special handling when tab completing in text mode. - self._cancel_text_completion() + self._cancel_completion() if self._in_buffer(position): # Special handling when a reading a line of raw input. @@ -1634,8 +1654,8 @@ class ConsoleWidget(LoggingConfigurable, QtGui.QWidget): def _keyboard_quit(self): """ Cancels the current editing task ala Ctrl-G in Emacs. """ - if self._text_completing_pos: - self._cancel_text_completion() + if self._is_completing: + self._cancel_completion() else: self.input_buffer = '' @@ -1853,24 +1873,6 @@ class ConsoleWidget(LoggingConfigurable, QtGui.QWidget): if diff < 0 and document.blockCount() == document.maximumBlockCount(): scrollbar.setValue(scrollbar.value() + diff) - def _cursor_position_changed(self): - """ Clears the temporary buffer based on the cursor position. - """ - if self._text_completing_pos: - document = self._control.document() - if self._text_completing_pos < document.characterCount(): - cursor = self._control.textCursor() - pos = cursor.position() - text_cursor = self._control.textCursor() - text_cursor.setPosition(self._text_completing_pos) - if pos < self._text_completing_pos or \ - cursor.blockNumber() > text_cursor.blockNumber(): - self._clear_temporary_buffer() - self._text_completing_pos = 0 - else: - self._clear_temporary_buffer() - self._text_completing_pos = 0 - def _custom_context_menu_requested(self, pos): """ Shows a context menu at the given QPoint (in widget coordinates). """ diff --git a/IPython/frontend/qt/console/qtconsoleapp.py b/IPython/frontend/qt/console/qtconsoleapp.py index 5824607..e1eac4c 100644 --- a/IPython/frontend/qt/console/qtconsoleapp.py +++ b/IPython/frontend/qt/console/qtconsoleapp.py @@ -104,11 +104,14 @@ qt_flags = { 'plain' : ({'IPythonQtConsoleApp' : {'plain' : True}}, "Disable rich text support."), } -qt_flags.update(boolean_flag( - 'gui-completion', 'ConsoleWidget.gui_completion', - "use a GUI widget for tab completion", - "use plaintext output for completion" -)) + +# not quite sure on how this works +#qt_flags.update(boolean_flag( +# 'gui-completion', 'ConsoleWidget.gui_completion', +# "use a GUI widget for tab completion", +# "use plaintext output for completion" +#)) + # and app_flags from the Console Mixin qt_flags.update(app_flags) # add frontend flags to the full set @@ -117,7 +120,6 @@ flags.update(qt_flags) # start with copy of front&backend aliases list aliases = dict(aliases) qt_aliases = dict( - style = 'IPythonWidget.syntax_style', stylesheet = 'IPythonQtConsoleApp.stylesheet', colors = 'ZMQInteractiveShell.colors', @@ -127,6 +129,7 @@ qt_aliases = dict( ) # and app_aliases from the Console Mixin qt_aliases.update(app_aliases) +qt_aliases.update({'gui-completion':'ConsoleWidget.gui_completion'}) # add frontend aliases to the full set aliases.update(qt_aliases) diff --git a/IPython/frontend/qt/console/styles.py b/IPython/frontend/qt/console/styles.py index c8e2ba7..c72808f 100644 --- a/IPython/frontend/qt/console/styles.py +++ b/IPython/frontend/qt/console/styles.py @@ -22,6 +22,7 @@ default_light_style_template = ''' .in-prompt-number { font-weight: bold; } .out-prompt { color: darkred; } .out-prompt-number { font-weight: bold; } + .inverted { background-color: %(fgcolor)s ; color:%(bgcolor)s;} ''' default_light_style_sheet = default_light_style_template%dict( bgcolor='white', fgcolor='black', select="#ccc") @@ -38,6 +39,7 @@ default_dark_style_template = ''' .in-prompt-number { color: lime; font-weight: bold; } .out-prompt { color: red; } .out-prompt-number { color: red; font-weight: bold; } + .inverted { background-color: %(fgcolor)s ; color:%(bgcolor)s;} ''' default_dark_style_sheet = default_dark_style_template%dict( bgcolor='black', fgcolor='white', select="#555") @@ -50,6 +52,7 @@ default_bw_style_sheet = ''' selection-background-color: #cccccc} .in-prompt-number { font-weight: bold; } .out-prompt-number { font-weight: bold; } + .inverted { background-color: black ; color: white;} ''' default_bw_syntax_style = 'bw' diff --git a/IPython/utils/html_utils.py b/IPython/utils/html_utils.py new file mode 100644 index 0000000..aaf2f2b --- /dev/null +++ b/IPython/utils/html_utils.py @@ -0,0 +1,124 @@ +"""some html utilis""" +from IPython.core.display import HTML + + +def columnize_info(items, separator_width=1, displaywidth=80, empty=None): + """ Get info on a list of string to display it as a multicolumns list + + returns : + --------- + + a dict containing several parameters: + + 'item_matrix' : list of list with the innermost list representing a row + 'columns_number': number of columns + 'rows_number' : number of rown + 'columns_width' : a list indicating the maximum length of the element in each columns + + Parameters : + ------------ + separator_width : when trying to ajust the number of column, consider a separator size of this much caracters + displaywidth : try to fit the columns in this width + empty : if the number of items is different from nrows * ncols, fill with empty + + """ + # Note: this code is adapted from columnize 0.3.2. + # See http://code.google.com/p/pycolumnize/ + + # Some degenerate cases. + size = len(items) + if size == 0: + return {'item_matrix' :[[empty]], + 'columns_number':1, + 'rows_number':1, + 'columns_width':[0]} + elif size == 1: + return {'item_matrix' :[[items[0]]], + 'columns_number':1, + 'rows_number':1, + 'columns_width':[len(items[0])]} + + # Special case: if any item is longer than the maximum width, there's no + # point in triggering the logic below... + item_len = map(len, items) # save these, we can reuse them below + #longest = max(item_len) + #if longest >= displaywidth: + # return (items, [longest]) + + # Try every row count from 1 upwards + array_index = lambda nrows, row, col: nrows*col + row + nrows = 1 + for nrows in range(1, size): + ncols = (size + nrows - 1) // nrows + colwidths = [] + totwidth = -separator_width + for col in range(ncols): + # Get max column width for this column + colwidth = 0 + for row in range(nrows): + i = array_index(nrows, row, col) + if i >= size: + break + len_x = item_len[i] + colwidth = max(colwidth, len_x) + colwidths.append(colwidth) + totwidth += colwidth + separator_width + if totwidth > displaywidth: + break + if totwidth <= displaywidth: + break + + # The smallest number of rows computed and the max widths for each + # column has been obtained. Now we just have to format each of the rows. + reorderd_items = [] + for row in range(nrows): + texts = [] + for col in range(ncols): + i = row + nrows*col + if i >= size: + texts.append(empty) + else: + texts.append(items[i]) + #while texts and not texts[-1]: + # del texts[-1] + #for col in range(len(texts)): + # texts[col] = texts[col].ljust(colwidths[col]) + reorderd_items.append(texts) + + return {'item_matrix' :reorderd_items, + 'columns_number':ncols, + 'rows_number':nrows, + 'columns_width':colwidths} + + +def column_table(items, select=None) : + """ return a html table of the item with a select class on one""" + items_m = columnize_info(items)['item_matrix'] + return HTML(html_tableify(items_m, select=select)) + +def html_tableify(item_matrix, select=None) : + """ returnr a string for an html table""" + if not item_matrix : + return '' + html_cols = [] + tds = lambda text : u''+text+u'' + trs = lambda text : u''+text+u'' + tds_items = [map(tds, row) for row in item_matrix ] + if select : + row, col = select + try : + tds_items[row][col] = u''\ + +item_matrix[row][col]\ + +u'' + except IndexError : + pass + #select the right item + html_cols = map(trs, (u''.join(row) for row in tds_items)) + html = (u''+(u''.join(html_cols))+u'
') + css = u""" + + """ + return css+html