From 226529a97bd6445f92927395b73a513bd9d0a454 2010-08-09 20:03:39 From: epatters Date: 2010-08-09 20:03:39 Subject: [PATCH] * ConsoleWidget now has better support for non-GUI tab completion. Multiple matches are formatted into columns. * Tab width can now be set at runtime rather than only during initialization. * Added external module columnize.py --- diff --git a/IPython/external/columnize.py b/IPython/external/columnize.py new file mode 100644 index 0000000..fc70433 --- /dev/null +++ b/IPython/external/columnize.py @@ -0,0 +1,160 @@ +"""Return compact set of columns as a string with newlines for an +array of strings. + +Adapted from the routine of the same name inside cmd.py + +Author: Rocky Bernstein. +License: MIT Open Source License. +""" + +import types + +def columnize(array, displaywidth=80, colsep = ' ', + arrange_vertical=True, ljust=True, lineprefix=''): + """Return a list of strings as a compact set of columns arranged + horizontally or vertically. + + For example, for a line width of 4 characters (arranged vertically): + ['1', '2,', '3', '4'] => '1 3\n2 4\n' + + or arranged horizontally: + ['1', '2,', '3', '4'] => '1 2\n3 4\n' + + Each column is only as wide as necessary. By default, columns are + separated by two spaces - one was not legible enough. Set "colsep" + to adjust the string separate columns. Set `displaywidth' to set + the line width. + + Normally, consecutive items go down from the top to bottom from + the left-most column to the right-most. If "arrange_vertical" is + set false, consecutive items will go across, left to right, top to + bottom.""" + if not isinstance(array, list) and not isinstance(array, tuple): + raise TypeError, ( + 'array needs to be an instance of a list or a tuple') + + array = [str(i) for i in array] + + # Some degenerate cases + size = len(array) + if 0 == size: + return "\n" + elif size == 1: + return '%s\n' % str(array[0]) + + displaywidth = max(4, displaywidth - len(lineprefix)) + if arrange_vertical: + array_index = lambda nrows, row, col: nrows*col + row + # Try every row count from 1 upwards + for nrows in range(1, size): + ncols = (size+nrows-1) // nrows + colwidths = [] + totwidth = -len(colsep) + 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 + x = array[i] + colwidth = max(colwidth, len(x)) + pass + colwidths.append(colwidth) + totwidth += colwidth + len(colsep) + if totwidth > displaywidth: + break + pass + if totwidth <= displaywidth: + break + pass + # 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. + s = '' + for row in range(nrows): + texts = [] + for col in range(ncols): + i = row + nrows*col + if i >= size: + x = "" + else: + x = array[i] + texts.append(x) + while texts and not texts[-1]: + del texts[-1] + for col in range(len(texts)): + if ljust: + texts[col] = texts[col].ljust(colwidths[col]) + else: + texts[col] = texts[col].rjust(colwidths[col]) + pass + pass + s += "%s%s\n" % (lineprefix, str(colsep.join(texts))) + pass + return s + else: + array_index = lambda nrows, row, col: ncols*(row-1) + col + # Try every column count from size downwards + prev_colwidths = [] + colwidths = [] + for ncols in range(size, 0, -1): + # Try every row count from 1 upwards + min_rows = (size+ncols-1) // ncols + for nrows in range(min_rows, size): + rounded_size = nrows * ncols + colwidths = [] + totwidth = -len(colsep) + for col in range(ncols): + # get max column width for this column + colwidth = 0 + for row in range(1, nrows+1): + i = array_index(nrows, row, col) + if i >= rounded_size: break + elif i < size: + x = array[i] + colwidth = max(colwidth, len(x)) + pass + pass + colwidths.append(colwidth) + totwidth += colwidth + len(colsep) + if totwidth >= displaywidth: + break + pass + if totwidth <= displaywidth and i >= rounded_size-1: + # Found the right nrows and ncols + nrows = row + break + elif totwidth >= displaywidth: + # Need to reduce ncols + break + pass + if totwidth <= displaywidth and i >= rounded_size-1: + break + pass + # 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. + s = '' + for row in range(1, nrows+1): + texts = [] + for col in range(ncols): + i = array_index(nrows, row, col) + if i >= size: + break + else: x = array[i] + texts.append(x) + pass + for col in range(len(texts)): + if ljust: + texts[col] = texts[col].ljust(colwidths[col]) + else: + texts[col] = texts[col].rjust(colwidths[col]) + pass + pass + s += "%s%s\n" % (lineprefix, str(colsep.join(texts))) + pass + return s + pass + diff --git a/IPython/frontend/qt/console/console_widget.py b/IPython/frontend/qt/console/console_widget.py index a5bba3b..2745443 100644 --- a/IPython/frontend/qt/console/console_widget.py +++ b/IPython/frontend/qt/console/console_widget.py @@ -5,6 +5,7 @@ import sys from PyQt4 import QtCore, QtGui # Local imports +from IPython.external.columnize import columnize from ansi_code_processor import QtAnsiCodeProcessor from completion_widget import CompletionWidget @@ -29,9 +30,6 @@ class ConsoleWidget(QtGui.QPlainTextEdit): # priority (when it has focus) over, e.g., window-level menu shortcuts. override_shortcuts = False - # The number of spaces to show for a tab character. - tab_width = 8 - # Protected class variables. _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left, QtCore.Qt.Key_F : QtCore.Qt.Key_Right, @@ -62,6 +60,7 @@ class ConsoleWidget(QtGui.QPlainTextEdit): self._prompt_pos = 0 self._reading = False self._reading_callback = None + self._tab_width = 8 # Set a monospaced font. self.reset_font() @@ -113,7 +112,7 @@ class ConsoleWidget(QtGui.QPlainTextEdit): self._context_menu.exec_(event.globalPos()) def dragMoveEvent(self, event): - """ Reimplemented to disable dropping text. + """ Reimplemented to disable moving text by drag and drop. """ event.ignore() @@ -467,6 +466,21 @@ class ConsoleWidget(QtGui.QPlainTextEdit): font = QtGui.QFont(name, QtGui.qApp.font().pointSize()) font.setStyleHint(QtGui.QFont.TypeWriter) self._set_font(font) + + def _get_tab_width(self): + """ The width (in terms of space characters) for tab characters. + """ + return self._tab_width + + def _set_tab_width(self, tab_width): + """ Sets the width (in terms of space characters) for tab characters. + """ + font_metrics = QtGui.QFontMetrics(self.font) + self.setTabStopWidth(tab_width * font_metrics.width(' ')) + + self._tab_width = tab_width + + tab_width = property(_get_tab_width, _set_tab_width) #--------------------------------------------------------------------------- # 'ConsoleWidget' abstract interface @@ -563,9 +577,34 @@ class ConsoleWidget(QtGui.QPlainTextEdit): if self.gui_completion: self._completion_widget.show_items(cursor, items) else: - text = '\n'.join(items) + '\n' + text = self.format_as_columns(items) self._append_plain_text_keeping_prompt(text) + def format_as_columns(self, items, separator=' ', vertical=True): + """ Transform a list of strings into a single string with columns. + + Parameters + ---------- + items : sequence [str] + The strings to process. + + separator : str, optional [default is two spaces] + The string that separates columns. + + vertical: bool, optional [default True] + If set, consecutive items will be arranged from top to bottom, then + from left to right. Otherwise, consecutive items will be aranged + from left to right, then from top to bottom. + + Returns + ------- + The formatted string. + """ + font_metrics = QtGui.QFontMetrics(self.font) + width = self.width() / font_metrics.width(' ') + return columnize(items, displaywidth=width, + colsep=separator, arrange_vertical=vertical) + def _get_block_plain_text(self, block): """ Given a QTextBlock, return its unformatted text. """ diff --git a/IPython/frontend/qt/console/frontend_widget.py b/IPython/frontend/qt/console/frontend_widget.py index f94721d..c3ae3ea 100644 --- a/IPython/frontend/qt/console/frontend_widget.py +++ b/IPython/frontend/qt/console/frontend_widget.py @@ -63,10 +63,7 @@ class FrontendHighlighter(PygmentsHighlighter): class FrontendWidget(HistoryConsoleWidget): """ A Qt frontend for a generic Python kernel. """ - - # ConsoleWidget interface. - tab_width = 4 - + # Emitted when an 'execute_reply' is received from the kernel. executed = QtCore.pyqtSignal(object) @@ -86,6 +83,7 @@ class FrontendWidget(HistoryConsoleWidget): self._kernel_manager = None # Configure the ConsoleWidget. + self.tab_width = 4 self._set_continuation_prompt('... ') self.document().contentsChange.connect(self._document_contents_change) @@ -121,7 +119,7 @@ class FrontendWidget(HistoryConsoleWidget): prompt created. When triggered by an Enter/Return key press, 'interactive' is True; otherwise, it is False. """ - complete = self._input_splitter.push(source.replace('\t', ' ')) + complete = self._input_splitter.push(source.expandtabs(4)) if interactive: complete = not self._input_splitter.push_accepts_more() return complete