From 422a62e7e14ed2619abba748992f341774397f9a 2010-07-23 17:54:10 From: epatters Date: 2010-07-23 17:54:10 Subject: [PATCH] * Added 'started_listening' and 'stopped_listening' signals to QtKernelManager. The FrontendWidget listens for these signals. * Created a metaclass to permit inheriting from both HasTraits and QObject * Made 'continuation_prompt' a protected variable of ConsoleWidget for API consistency * Made FrontendWidget's constructor consistent with QWidget conventions. --- diff --git a/IPython/frontend/qt/console/console_widget.py b/IPython/frontend/qt/console/console_widget.py index b91abbd..4a4b14f 100644 --- a/IPython/frontend/qt/console/console_widget.py +++ b/IPython/frontend/qt/console/console_widget.py @@ -121,10 +121,10 @@ class ConsoleWidget(QtGui.QPlainTextEdit): # Initialize public and protected variables self.ansi_codes = True self.buffer_size = 500 - self.continuation_prompt = '> ' self.gui_completion = True self._ansi_processor = QtAnsiCodeProcessor() self._completion_widget = CompletionWidget(self) + self._continuation_prompt = '> ' self._executing = False self._prompt = '' self._prompt_pos = 0 @@ -261,7 +261,7 @@ class ConsoleWidget(QtGui.QPlainTextEdit): if start_line == self._get_prompt_cursor().blockNumber(): start_pos += len(self._prompt) else: - start_pos += len(self.continuation_prompt) + start_pos += len(self._continuation_prompt) if shift_down and self._in_buffer(position): self._set_selection(position, start_pos) else: @@ -271,7 +271,7 @@ class ConsoleWidget(QtGui.QPlainTextEdit): elif key == QtCore.Qt.Key_Backspace and not alt_down: # Line deletion (remove continuation prompt) - len_prompt = len(self.continuation_prompt) + len_prompt = len(self._continuation_prompt) if cursor.columnNumber() == len_prompt and \ position != self._prompt_pos: cursor.setPosition(position - len_prompt, @@ -323,6 +323,18 @@ class ConsoleWidget(QtGui.QPlainTextEdit): else: cursor.insertText(text) + def clear(self, keep_input=False): + """ Reimplemented to write a new prompt. If 'keep_input' is set, + restores the old input buffer when the new prompt is written. + """ + super(ConsoleWidget, self).clear() + + if keep_input: + input_buffer = self.input_buffer + self._show_prompt() + if keep_input: + self.input_buffer = input_buffer + def paste(self): """ Reimplemented to ensure that text is pasted in the editing region. """ @@ -330,8 +342,8 @@ class ConsoleWidget(QtGui.QPlainTextEdit): QtGui.QPlainTextEdit.paste(self) def print_(self, printer): - """ Reimplemented to work around bug in PyQt where the C++ level - 'print_' slot has the wrong signature. + """ Reimplemented to work around a bug in PyQt: the C++ level 'print_' + slot has the wrong signature. """ QtGui.QPlainTextEdit.print_(self, printer) @@ -363,13 +375,13 @@ class ConsoleWidget(QtGui.QPlainTextEdit): input_buffer = str(cursor.selection().toPlainText()) # Strip out continuation prompts - return input_buffer.replace('\n' + self.continuation_prompt, '\n') + return input_buffer.replace('\n' + self._continuation_prompt, '\n') def _set_input_buffer(self, string): # Add continuation prompts where necessary lines = string.splitlines() for i in xrange(1, len(lines)): - lines[i] = self.continuation_prompt + lines[i] + lines[i] = self._continuation_prompt + lines[i] string = '\n'.join(lines) # Replace buffer with new text @@ -389,7 +401,7 @@ class ConsoleWidget(QtGui.QPlainTextEdit): if cursor.blockNumber() == self._get_prompt_cursor().blockNumber(): return text[len(self._prompt):] else: - return text[len(self.continuation_prompt):] + return text[len(self._continuation_prompt):] else: return None @@ -547,18 +559,20 @@ class ConsoleWidget(QtGui.QPlainTextEdit): """ self.setTextCursor(self._get_selection_cursor(start, end)) - def _show_prompt(self, prompt): - """ Writes a new prompt at the end of the buffer. + def _show_prompt(self, prompt=None): + """ Writes a new prompt at the end of the buffer. If 'prompt' is not + specified, uses the previous prompt. """ - self.appendPlainText('\n' + prompt) - self._prompt = prompt + if prompt is not None: + self._prompt = prompt + self.appendPlainText('\n' + self._prompt) self._prompt_pos = self._get_end_cursor().position() self._prompt_started() def _show_continuation_prompt(self): """ Writes a new continuation prompt at the end of the buffer. """ - self.appendPlainText(self.continuation_prompt) + self.appendPlainText(self._continuation_prompt) self._prompt_started() def _write_text_keeping_prompt(self, text): @@ -570,7 +584,7 @@ class ConsoleWidget(QtGui.QPlainTextEdit): self._prompt_finished() self.appendPlainText(text) - self._show_prompt(self._prompt) + self._show_prompt() self.input_buffer = input_buffer def _in_buffer(self, position): diff --git a/IPython/frontend/qt/console/frontend_widget.py b/IPython/frontend/qt/console/frontend_widget.py index 2f7d67c..4792397 100644 --- a/IPython/frontend/qt/console/frontend_widget.py +++ b/IPython/frontend/qt/console/frontend_widget.py @@ -26,8 +26,8 @@ class FrontendHighlighter(PygmentsHighlighter): """ Highlight a block of text. Reimplemented to highlight selectively. """ if self.highlighting_on: - for prompt in (self._frontend._prompt, - self._frontend.continuation_prompt): + for prompt in (self._frontend._continuation_prompt, + self._frontend._prompt): if qstring.startsWith(prompt): qstring.remove(0, len(prompt)) self._current_offset = len(prompt) @@ -52,9 +52,14 @@ class FrontendWidget(HistoryConsoleWidget): # 'QWidget' interface #--------------------------------------------------------------------------- - def __init__(self, kernel_manager, parent=None): + def __init__(self, parent=None): super(FrontendWidget, self).__init__(parent) + # ConsoleWidget protected variables. + self._continuation_prompt = '... ' + self._prompt = '>>> ' + + # FrontendWidget protected variables. self._blockbreaker = BlockBreaker(input_mode='replace') self._call_tip_widget = CallTipWidget(self) self._completion_lexer = CompletionLexer(PythonLexer()) @@ -62,9 +67,6 @@ class FrontendWidget(HistoryConsoleWidget): self._highlighter = FrontendHighlighter(self) self._kernel_manager = None - self.continuation_prompt = '... ' - self.kernel_manager = kernel_manager - self.document().contentsChange.connect(self._document_contents_change) def focusOutEvent(self, event): @@ -144,10 +146,17 @@ class FrontendWidget(HistoryConsoleWidget): return self._kernel_manager def _set_kernel_manager(self, kernel_manager): - """ Sets a new kernel manager, configuring its channels as necessary. + """ Disconnect from the current kernel manager (if any) and set a new + kernel manager. """ - # Disconnect the old kernel manager. + # Disconnect the old kernel manager, if necessary. if self._kernel_manager is not None: + self._kernel_manager.started_listening.disconnect( + self._started_listening) + self._kernel_manager.stopped_listening.disconnect( + self._stopped_listening) + + # Disconnect the old kernel manager's channels. sub = self._kernel_manager.sub_channel xreq = self._kernel_manager.xreq_channel sub.message_received.disconnect(self._handle_sub) @@ -155,8 +164,16 @@ class FrontendWidget(HistoryConsoleWidget): xreq.complete_reply.disconnect(self._handle_complete_reply) xreq.object_info_reply.disconnect(self._handle_object_info_reply) - # Connect the new kernel manager. + # Set the new kernel manager. self._kernel_manager = kernel_manager + if kernel_manager is None: + return + + # Connect the new kernel manager. + kernel_manager.started_listening.connect(self._started_listening) + kernel_manager.stopped_listening.connect(self._stopped_listening) + + # Connect the new kernel manager's channels. sub = kernel_manager.sub_channel xreq = kernel_manager.xreq_channel sub.message_received.connect(self._handle_sub) @@ -164,7 +181,10 @@ class FrontendWidget(HistoryConsoleWidget): xreq.complete_reply.connect(self._handle_complete_reply) xreq.object_info_reply.connect(self._handle_object_info_reply) - self._show_prompt('>>> ') + # Handle the case where the kernel manager started listening before + # we connected. + if kernel_manager.is_listening: + self._started_listening() kernel_manager = property(_get_kernel_manager, _set_kernel_manager) @@ -256,7 +276,7 @@ class FrontendWidget(HistoryConsoleWidget): text = "ERROR: ABORTED\n" self.appendPlainText(text) self._hidden = True - self._show_prompt('>>> ') + self._show_prompt() self.executed.emit(rep) def _handle_complete_reply(self, rep): @@ -274,3 +294,9 @@ class FrontendWidget(HistoryConsoleWidget): doc = rep['content']['docstring'] if doc: self._call_tip_widget.show_tip(doc) + + def _started_listening(self): + self.clear() + + def _stopped_listening(self): + pass diff --git a/IPython/frontend/qt/console/ipython_widget.py b/IPython/frontend/qt/console/ipython_widget.py index 1472d3f..1df14a1 100644 --- a/IPython/frontend/qt/console/ipython_widget.py +++ b/IPython/frontend/qt/console/ipython_widget.py @@ -13,8 +13,8 @@ class IPythonWidget(FrontendWidget): # 'FrontendWidget' interface #--------------------------------------------------------------------------- - def __init__(self, kernel_manager, parent=None): - super(IPythonWidget, self).__init__(kernel_manager, parent) + def __init__(self, parent=None): + super(IPythonWidget, self).__init__(parent) self._magic_overrides = {} @@ -87,7 +87,8 @@ if __name__ == '__main__': # Launch application app = QtGui.QApplication([]) - widget = IPythonWidget(kernel_manager) + widget = IPythonWidget() + widget.kernel_manager = kernel_manager widget.setWindowTitle('Python') widget.resize(640, 480) widget.show() diff --git a/IPython/frontend/qt/kernelmanager.py b/IPython/frontend/qt/kernelmanager.py index c88872f..f06d84b 100644 --- a/IPython/frontend/qt/kernelmanager.py +++ b/IPython/frontend/qt/kernelmanager.py @@ -1,4 +1,4 @@ -""" A KernelManager that provides channels that use signals and slots. +""" Defines a KernelManager that provides signals and slots. """ # System library imports. @@ -7,6 +7,7 @@ from PyQt4 import QtCore # IPython imports. from IPython.zmq.kernelmanager import KernelManager, SubSocketChannel, \ XReqSocketChannel, RepSocketChannel +from util import MetaQObjectHasTraits class QtSubSocketChannel(SubSocketChannel, QtCore.QObject): @@ -109,10 +110,35 @@ class QtRepSocketChannel(RepSocketChannel, QtCore.QObject): RepSocketChannel.__init__(self, *args, **kw) -class QtKernelManager(KernelManager): - """ A KernelManager that provides channels that use signals and slots. +class QtKernelManager(KernelManager, QtCore.QObject): + """ A KernelManager that provides signals and slots. """ + __metaclass__ = MetaQObjectHasTraits + + # Emitted when the kernel manager has started listening. + started_listening = QtCore.pyqtSignal() + + # Emitted when the kernel manager has stopped listening. + stopped_listening = QtCore.pyqtSignal() + + # Use Qt-specific channel classes that emit signals. sub_channel_class = QtSubSocketChannel xreq_channel_class = QtXReqSocketChannel rep_channel_class = QtRepSocketChannel + + #--------------------------------------------------------------------------- + # 'KernelManager' interface + #--------------------------------------------------------------------------- + + def start_listening(self): + """ Reimplemented to emit signal. + """ + super(QtKernelManager, self).start_listening() + self.started_listening.emit() + + def stop_listening(self): + """ Reimplemented to emit signal. + """ + super(QtKernelManager, self).stop_listening() + self.stopped_listening.emit() diff --git a/IPython/frontend/qt/util.py b/IPython/frontend/qt/util.py new file mode 100644 index 0000000..a1dab85 --- /dev/null +++ b/IPython/frontend/qt/util.py @@ -0,0 +1,25 @@ +""" Defines miscellaneous Qt-related helper classes and functions. +""" + +# System library imports. +from PyQt4 import QtCore + +# IPython imports. +from IPython.utils.traitlets import HasTraits + + +MetaHasTraits = type(HasTraits) +MetaQObject = type(QtCore.QObject) + +class MetaQObjectHasTraits(MetaQObject, MetaHasTraits): + """ A metaclass that inherits from the metaclasses of both HasTraits and + QObject. + + Using this metaclass allows a class to inherit from both HasTraits and + QObject. See QtKernelManager for an example. + """ + + def __init__(cls, name, bases, dct): + MetaQObject.__init__(cls, name, bases, dct) + MetaHasTraits.__init__(cls, name, bases, dct) +