From 34fabab41c65fd5c650fc931de9f53c3333fbbae 2010-08-26 21:42:09 From: epatters Date: 2010-08-26 21:42:09 Subject: [PATCH] Made IPythonWidget and its subclasses Configurable. --- diff --git a/IPython/frontend/qt/console/console_widget.py b/IPython/frontend/qt/console/console_widget.py index e49dda2..e8461bc 100644 --- a/IPython/frontend/qt/console/console_widget.py +++ b/IPython/frontend/qt/console/console_widget.py @@ -7,6 +7,9 @@ from textwrap import dedent from PyQt4 import QtCore, QtGui # Local imports +from IPython.config.configurable import Configurable +from IPython.frontend.qt.util import MetaQObjectHasTraits +from IPython.utils.traitlets import Bool, Enum, Int from ansi_code_processor import QtAnsiCodeProcessor from completion_widget import CompletionWidget @@ -32,7 +35,7 @@ class ConsoleTextEdit(QtGui.QTextEdit): def dropEvent(self, event): pass -class ConsoleWidget(QtGui.QWidget): +class ConsoleWidget(Configurable, QtGui.QWidget): """ An abstract base class for console-type widgets. This class has functionality for: @@ -45,20 +48,40 @@ class ConsoleWidget(QtGui.QWidget): ConsoleWidget also provides a number of utility methods that will be convenient to implementors of a console-style widget. """ + __metaclass__ = MetaQObjectHasTraits # Whether to process ANSI escape codes. - ansi_codes = True + ansi_codes = Bool(True, config=True) - # The maximum number of lines of text before truncation. - buffer_size = 500 + # The maximum number of lines of text before truncation. Specifying a + # non-positive number disables text truncation (not recommended). + buffer_size = Int(500, config=True) # Whether to use a list widget or plain text output for tab completion. - gui_completion = True + gui_completion = Bool(True, config=True) + + # The type of underlying text widget to use. Valid values are 'plain', which + # specifies a QPlainTextEdit, and 'rich', which specifies a QTextEdit. + # NOTE: this value can only be specified during initialization. + kind = Enum(['plain', 'rich'], default_value='plain', config=True) + + # The type of paging to use. Valid values are: + # 'inside' : The widget pages like a traditional terminal pager. + # 'hsplit' : When paging is requested, the widget is split + # horizontally. The top pane contains the console, and the + # bottom pane contains the paged text. + # 'vsplit' : Similar to 'hsplit', except that a vertical splitter used. + # 'custom' : No action is taken by the widget beyond emitting a + # 'custom_page_requested(str)' signal. + # 'none' : The text is written directly to the console. + # NOTE: this value can only be specified during initialization. + paging = Enum(['inside', 'hsplit', 'vsplit', 'custom', 'none'], + default_value='inside', config=True) # Whether to override ShortcutEvents for the keybindings defined by this # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take # priority (when it has focus) over, e.g., window-level menu shortcuts. - override_shortcuts = False + override_shortcuts = Bool(False) # Signals that indicate ConsoleWidget state. copy_available = QtCore.pyqtSignal(bool) @@ -84,42 +107,26 @@ class ConsoleWidget(QtGui.QWidget): # 'QObject' interface #--------------------------------------------------------------------------- - def __init__(self, kind='plain', paging='inside', parent=None): + def __init__(self, parent=None, **kw): """ Create a ConsoleWidget. - - Parameters - ---------- - kind : str, optional [default 'plain'] - The type of underlying text widget to use. Valid values are 'plain', - which specifies a QPlainTextEdit, and 'rich', which specifies a - QTextEdit. - - paging : str, optional [default 'inside'] - The type of paging to use. Valid values are: - 'inside' : The widget pages like a traditional terminal pager. - 'hsplit' : When paging is requested, the widget is split - horizontally. The top pane contains the console, - and the bottom pane contains the paged text. - 'vsplit' : Similar to 'hsplit', except that a vertical splitter - used. - 'custom' : No action is taken by the widget beyond emitting a - 'custom_page_requested(str)' signal. - 'none' : The text is written directly to the console. + Parameters: + ----------- parent : QWidget, optional [default None] The parent for this widget. """ - super(ConsoleWidget, self).__init__(parent) + QtGui.QWidget.__init__(self, parent) + Configurable.__init__(self, **kw) # Create the layout and underlying text widget. layout = QtGui.QStackedLayout(self) layout.setContentsMargins(0, 0, 0, 0) - self._control = self._create_control(kind) + self._control = self._create_control() self._page_control = None self._splitter = None - if paging in ('hsplit', 'vsplit'): + if self.paging in ('hsplit', 'vsplit'): self._splitter = QtGui.QSplitter() - if paging == 'hsplit': + if self.paging == 'hsplit': self._splitter.setOrientation(QtCore.Qt.Horizontal) else: self._splitter.setOrientation(QtCore.Qt.Vertical) @@ -129,16 +136,13 @@ class ConsoleWidget(QtGui.QWidget): layout.addWidget(self._control) # Create the paging widget, if necessary. - self._page_style = paging - if paging in ('inside', 'hsplit', 'vsplit'): + if self.paging in ('inside', 'hsplit', 'vsplit'): self._page_control = self._create_page_control() if self._splitter: self._page_control.hide() self._splitter.addWidget(self._page_control) else: layout.addWidget(self._page_control) - elif paging not in ('custom', 'none'): - raise ValueError('Paging style %s unknown.' % repr(paging)) # Initialize protected variables. Some variables contain useful state # information for subclasses; they should be considered read-only. @@ -210,11 +214,11 @@ class ConsoleWidget(QtGui.QWidget): # factor of one character here. width = font_metrics.maxWidth() * 81 + margin width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent) - if self._page_style == 'hsplit': + if self.paging == 'hsplit': width = width * 2 + splitwidth height = font_metrics.height() * 25 + margin - if self._page_style == 'vsplit': + if self.paging == 'vsplit': height = height * 2 + splitwidth return QtCore.QSize(width, height) @@ -577,16 +581,14 @@ class ConsoleWidget(QtGui.QWidget): return down - def _create_control(self, kind): + def _create_control(self): """ Creates and connects the underlying text widget. """ - if kind == 'plain': + if self.kind == 'plain': control = ConsolePlainTextEdit() - elif kind == 'rich': + elif self.kind == 'rich': control = ConsoleTextEdit() control.setAcceptRichText(False) - else: - raise ValueError("Kind %s unknown." % repr(kind)) control.installEventFilter(self) control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) control.customContextMenuRequested.connect(self._show_context_menu) @@ -1072,13 +1074,13 @@ class ConsoleWidget(QtGui.QWidget): """ Displays text using the pager if it exceeds the height of the visible area. """ - if self._page_style == 'none': + if self.paging == 'none': self._append_plain_text(text) else: line_height = QtGui.QFontMetrics(self.font).height() minlines = self._control.viewport().height() / line_height if re.match("(?:[^\n]*\n){%i}" % minlines, text): - if self._page_style == 'custom': + if self.paging == 'custom': self.custom_page_requested.emit(text) else: self._page_control.clear() diff --git a/IPython/frontend/qt/console/frontend_widget.py b/IPython/frontend/qt/console/frontend_widget.py index e2aaab0..658f861 100644 --- a/IPython/frontend/qt/console/frontend_widget.py +++ b/IPython/frontend/qt/console/frontend_widget.py @@ -9,6 +9,7 @@ from PyQt4 import QtCore, QtGui # Local imports from IPython.core.inputsplitter import InputSplitter from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin +from IPython.utils.traitlets import Bool, Type from call_tip_widget import CallTipWidget from completion_lexer import CompletionLexer from console_widget import HistoryConsoleWidget @@ -74,12 +75,12 @@ class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin): # An option and corresponding signal for overriding the default kernel # interrupt behavior. - custom_interrupt = False + custom_interrupt = Bool(False) custom_interrupt_requested = QtCore.pyqtSignal() # An option and corresponding signal for overriding the default kernel # restart behavior. - custom_restart = False + custom_restart = Bool(False) custom_restart_requested = QtCore.pyqtSignal() # Emitted when an 'execute_reply' has been received from the kernel and @@ -87,8 +88,8 @@ class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin): executed = QtCore.pyqtSignal(object) # Protected class variables. - _highlighter_class = FrontendHighlighter - _input_splitter_class = InputSplitter + _highlighter_class = Type(FrontendHighlighter) + _input_splitter_class = Type(InputSplitter) #--------------------------------------------------------------------------- # 'object' interface diff --git a/IPython/frontend/qt/console/ipython_widget.py b/IPython/frontend/qt/console/ipython_widget.py index fda71f9..0ee71c2 100644 --- a/IPython/frontend/qt/console/ipython_widget.py +++ b/IPython/frontend/qt/console/ipython_widget.py @@ -16,49 +16,76 @@ from PyQt4 import QtCore, QtGui # Local imports from IPython.core.inputsplitter import IPythonInputSplitter from IPython.core.usage import default_banner +from IPython.utils.traitlets import Bool, Str from frontend_widget import FrontendWidget +# The default style sheet: black text on a white background. +default_style_sheet = ''' + .error { color: red; } + .in-prompt { color: navy; } + .in-prompt-number { font-weight: bold; } + .out-prompt { color: darkred; } + .out-prompt-number { font-weight: bold; } +''' +default_syntax_style = 'default' + +# A dark style sheet: white text on a black background. +dark_style_sheet = ''' + QPlainTextEdit, QTextEdit { background-color: black; color: white } + QFrame { border: 1px solid grey; } + .error { color: red; } + .in-prompt { color: lime; } + .in-prompt-number { color: lime; font-weight: bold; } + .out-prompt { color: red; } + .out-prompt-number { color: red; font-weight: bold; } +''' +dark_syntax_style = 'monokai' + +# Default prompts. +default_in_prompt = 'In [%i]: ' +default_out_prompt = 'Out[%i]: ' + class IPythonWidget(FrontendWidget): """ A FrontendWidget for an IPython kernel. """ - # Signal emitted when an editor is needed for a file and the editor has been - # specified as 'custom'. See 'set_editor' for more information. + # If set, the 'custom_edit_requested(str, int)' signal will be emitted when + # an editor is needed for a file. This overrides 'editor' and 'editor_line' + # settings. + custom_edit = Bool(False) custom_edit_requested = QtCore.pyqtSignal(object, object) - # The default stylesheet: black text on a white background. - default_stylesheet = """ - .error { color: red; } - .in-prompt { color: navy; } - .in-prompt-number { font-weight: bold; } - .out-prompt { color: darkred; } - .out-prompt-number { font-weight: bold; } - """ - - # A dark stylesheet: white text on a black background. - dark_stylesheet = """ - QPlainTextEdit, QTextEdit { background-color: black; color: white } - QFrame { border: 1px solid grey; } - .error { color: red; } - .in-prompt { color: lime; } - .in-prompt-number { color: lime; font-weight: bold; } - .out-prompt { color: red; } - .out-prompt-number { color: red; font-weight: bold; } - """ - - # Default prompts. - in_prompt = 'In [%i]: ' - out_prompt = 'Out[%i]: ' + # A command for invoking a system text editor. If the string contains a + # {filename} format specifier, it will be used. Otherwise, the filename will + # be appended to the end the command. + editor = Str('default', config=True) + + # The editor command to use when a specific line number is requested. The + # string should contain two format specifiers: {line} and {filename}. If + # this parameter is not specified, the line number option to the %edit magic + # will be ignored. + editor_line = Str(config=True) + + # A CSS stylesheet. The stylesheet can contain classes for: + # 1. Qt: QPlainTextEdit, QFrame, QWidget, etc + # 2. Pygments: .c, .k, .o, etc (see PygmentsHighlighter) + # 3. IPython: .error, .in-prompt, .out-prompt, etc + style_sheet = Str(default_style_sheet, config=True) + + # If not empty, use this Pygments style for syntax highlighting. Otherwise, + # the style sheet is queried for Pygments style information. + syntax_style = Str(default_syntax_style, config=True) - # A type used internally by IPythonWidget for storing prompt information. - _PromptBlock = namedtuple('_PromptBlock', - ['block', 'length', 'number']) + # Prompts. + in_prompt = Str(default_in_prompt, config=True) + out_prompt = Str(default_out_prompt, config=True) # FrontendWidget protected class variables. _input_splitter_class = IPythonInputSplitter # IPythonWidget protected class variables. + _PromptBlock = namedtuple('_PromptBlock', ['block', 'length', 'number']) _payload_source_edit = 'IPython.zmq.zmqshell.ZMQInteractiveShell.edit_magic' _payload_source_page = 'IPython.zmq.page.page' @@ -72,9 +99,9 @@ class IPythonWidget(FrontendWidget): # IPythonWidget protected variables. self._previous_prompt_obj = None - # Set a default editor and stylesheet. - self.set_editor('default') - self.reset_styling() + # Initialize widget styling. + self._style_sheet_changed() + self._syntax_style_changed() #--------------------------------------------------------------------------- # 'BaseFrontendMixin' abstract interface @@ -248,67 +275,6 @@ class IPythonWidget(FrontendWidget): next_prompt['input_sep']) #--------------------------------------------------------------------------- - # 'IPythonWidget' interface - #--------------------------------------------------------------------------- - - def reset_styling(self): - """ Restores the default IPythonWidget styling. - """ - self.set_styling(self.default_stylesheet, syntax_style='default') - #self.set_styling(self.dark_stylesheet, syntax_style='monokai') - - def set_editor(self, editor, line_editor=None): - """ Sets the editor to use with the %edit magic. - - Parameters: - ----------- - editor : str - A command for invoking a system text editor. If the string contains - a {filename} format specifier, it will be used. Otherwise, the - filename will be appended to the end the command. - - This parameter also takes a special value: - 'custom' : Emit a 'custom_edit_requested(str, int)' signal - instead of opening an editor. - - line_editor : str, optional - The editor command to use when a specific line number is - requested. The string should contain two format specifiers: {line} - and {filename}. If this parameter is not specified, the line number - option to the %edit magic will be ignored. - """ - self._editor = editor - self._editor_line = line_editor - - def set_styling(self, stylesheet, syntax_style=None): - """ Sets the IPythonWidget styling. - - Parameters: - ----------- - stylesheet : str - A CSS stylesheet. The stylesheet can contain classes for: - 1. Qt: QPlainTextEdit, QFrame, QWidget, etc - 2. Pygments: .c, .k, .o, etc (see PygmentsHighlighter) - 3. IPython: .error, .in-prompt, .out-prompt, etc. - - syntax_style : str or None [default None] - If specified, use the Pygments style with given name. Otherwise, - the stylesheet is queried for Pygments style information. - """ - self.setStyleSheet(stylesheet) - self._control.document().setDefaultStyleSheet(stylesheet) - if self._page_control: - self._page_control.document().setDefaultStyleSheet(stylesheet) - - if syntax_style is None: - self._highlighter.set_style_sheet(stylesheet) - else: - self._highlighter.set_style(syntax_style) - - bg_color = self._control.palette().background().color() - self._ansi_processor.set_background_color(bg_color) - - #--------------------------------------------------------------------------- # 'IPythonWidget' protected interface #--------------------------------------------------------------------------- @@ -323,21 +289,21 @@ class IPythonWidget(FrontendWidget): line : int, optional A line of interest in the file. """ - if self._editor == 'custom': + if self.custom_edit: self.custom_edit_requested.emit(filename, line) - elif self._editor == 'default': + elif self.editor == 'default': self._append_plain_text('No default editor available.\n') else: try: filename = '"%s"' % filename - if line and self._editor_line: - command = self._editor_line.format(filename=filename, - line=line) + if line and self.editor_line: + command = self.editor_line.format(filename=filename, + line=line) else: try: - command = self._editor.format() + command = self.editor.format() except KeyError: - command = self._editor.format(filename=filename) + command = self.editor.format(filename=filename) else: command += ' ' + filename except KeyError: @@ -369,3 +335,25 @@ class IPythonWidget(FrontendWidget): """ body = self.out_prompt % number return '%s' % body + + #------ Trait change handlers --------------------------------------------- + + def _style_sheet_changed(self): + """ Set the style sheets of the underlying widgets. + """ + self.setStyleSheet(self.style_sheet) + self._control.document().setDefaultStyleSheet(self.style_sheet) + if self._page_control: + self._page_control.document().setDefaultStyleSheet(self.style_sheet) + + bg_color = self._control.palette().background().color() + self._ansi_processor.set_background_color(bg_color) + + def _syntax_style_changed(self): + """ Set the style for the syntax highlighter. + """ + if self.syntax_style: + self._highlighter.set_style(self.syntax_style) + else: + self._highlighter.set_style_sheet(self.style_sheet) +