diff --git a/IPython/html/static/notebook/js/outputarea.js b/IPython/html/static/notebook/js/outputarea.js index 79b902e..9fc6d8e 100644 --- a/IPython/html/static/notebook/js/outputarea.js +++ b/IPython/html/static/notebook/js/outputarea.js @@ -268,39 +268,47 @@ var IPython = (function (IPython) { }); return json; }; - + OutputArea.prototype.append_output = function (json) { this.expand(); + + // validate output data types + json = this.validate_output(json); + // Clear the output if clear is queued. var needs_height_reset = false; if (this.clear_queued) { this.clear_output(false); needs_height_reset = true; } - - // validate output data types - json = this.validate_output(json); if (json.output_type === 'pyout') { this.append_pyout(json); } else if (json.output_type === 'pyerr') { this.append_pyerr(json); - } else if (json.output_type === 'display_data') { - this.append_display_data(json); } else if (json.output_type === 'stream') { this.append_stream(json); } - - this.outputs.push(json); - - // Only reset the height to automatic if the height is currently - // fixed (done by wait=True flag on clear_output). - if (needs_height_reset) { - this.element.height(''); - } + // We must release the animation fixed height in a callback since Gecko + // (FireFox) doesn't render the image immediately as the data is + // available. var that = this; - setTimeout(function(){that.element.trigger('resize');}, 100); + var handle_appended = function ($el) { + // Only reset the height to automatic if the height is currently + // fixed (done by wait=True flag on clear_output). + if (needs_height_reset) { + that.element.height(''); + } + that.element.trigger('resize'); + }; + if (json.output_type === 'display_data') { + this.append_display_data(json, handle_appended); + } else { + handle_appended(); + } + + this.outputs.push(json); }; @@ -475,9 +483,9 @@ var IPython = (function (IPython) { }; - OutputArea.prototype.append_display_data = function (json) { + OutputArea.prototype.append_display_data = function (json, handle_inserted) { var toinsert = this.create_output_area(); - if (this.append_mime_type(json, toinsert)) { + if (this.append_mime_type(json, toinsert, handle_inserted)) { this._safe_append(toinsert); // If we just output latex, typeset it. if ((json['text/latex'] !== undefined) || (json['text/html'] !== undefined)) { @@ -494,7 +502,7 @@ var IPython = (function (IPython) { 'image/jpeg' : true }; - OutputArea.prototype.append_mime_type = function (json, element) { + OutputArea.prototype.append_mime_type = function (json, element, handle_inserted) { for (var type_i in OutputArea.display_order) { var type = OutputArea.display_order[type_i]; var append = OutputArea.append_map[type]; @@ -511,7 +519,14 @@ var IPython = (function (IPython) { } } var md = json.metadata || {}; - var toinsert = append.apply(this, [value, md, element]); + var toinsert = append.apply(this, [value, md, element, handle_inserted]); + // Since only the png and jpeg mime types call the inserted + // callback, if the mime type is something other we must call the + // inserted callback only when the element is actually inserted + // into the DOM. Use a timeout of 0 to do this. + if (['image/png', 'image/jpeg'].indexOf(type) < 0 && handle_inserted !== undefined) { + setTimeout(handle_inserted, 0); + } $([IPython.events]).trigger('output_appended.OutputArea', [type, value, md, toinsert]); return toinsert; } @@ -611,10 +626,16 @@ var IPython = (function (IPython) { if (width !== undefined) img.attr('width', width); }; - var append_png = function (png, md, element) { + var append_png = function (png, md, element, handle_inserted) { var type = 'image/png'; var toinsert = this.create_output_subarea(md, "output_png", type); - var img = $("").attr('src','data:image/png;base64,'+png); + var img = $(""); + if (handle_inserted !== undefined) { + img.on('load', function(){ + handle_inserted(img); + }); + } + img[0].src = 'data:image/png;base64,'+ png; set_width_height(img, md, 'image/png'); this._dblclick_to_reset_size(img); toinsert.append(img); @@ -623,10 +644,16 @@ var IPython = (function (IPython) { }; - var append_jpeg = function (jpeg, md, element) { + var append_jpeg = function (jpeg, md, element, handle_inserted) { var type = 'image/jpeg'; var toinsert = this.create_output_subarea(md, "output_jpeg", type); - var img = $("").attr('src','data:image/jpeg;base64,'+jpeg); + var img = $(""); + if (handle_inserted !== undefined) { + img.on('load', function(){ + handle_inserted(img); + }); + } + img[0].src = 'data:image/jpeg;base64,'+ jpeg; set_width_height(img, md, 'image/jpeg'); this._dblclick_to_reset_size(img); toinsert.append(img); @@ -748,7 +775,10 @@ var IPython = (function (IPython) { this.clear_queued = false; } - // clear all, no need for logic + // Clear all + // Remove load event handlers from img tags because we don't want + // them to fire if the image is never added to the page. + this.element.find('img').off('load'); this.element.html(""); this.outputs = []; this.trusted = true; diff --git a/IPython/kernel/zmq/zmqshell.py b/IPython/kernel/zmq/zmqshell.py index c2d044b..357b6e1 100644 --- a/IPython/kernel/zmq/zmqshell.py +++ b/IPython/kernel/zmq/zmqshell.py @@ -90,11 +90,7 @@ class ZMQDisplayPublisher(DisplayPublisher): def clear_output(self, wait=False): content = dict(wait=wait) - - print('\r', file=sys.stdout, end='') - print('\r', file=sys.stderr, end='') self._flush_streams() - self.session.send( self.pub_socket, u'clear_output', content, parent=self.parent_header, ident=self.topic, diff --git a/IPython/qt/console/frontend_widget.py b/IPython/qt/console/frontend_widget.py index 222db04..f345a80 100644 --- a/IPython/qt/console/frontend_widget.py +++ b/IPython/qt/console/frontend_widget.py @@ -197,6 +197,9 @@ class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin): self._local_kernel = kw.get('local_kernel', FrontendWidget._local_kernel) + # Whether or not a clear_output call is pending new output. + self._pending_clearoutput = False + #--------------------------------------------------------------------------- # 'ConsoleWidget' public interface #--------------------------------------------------------------------------- @@ -339,6 +342,14 @@ class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin): #--------------------------------------------------------------------------- # 'BaseFrontendMixin' abstract interface #--------------------------------------------------------------------------- + def _handle_clear_output(self, msg): + """Handle clear output messages.""" + if not self._hidden and self._is_from_this_session(msg): + wait = msg['content'].get('wait', True) + if wait: + self._pending_clearoutput = True + else: + self.clear_output() def _handle_complete_reply(self, rep): """ Handle replies for tab completion. @@ -520,6 +531,7 @@ class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin): """ self.log.debug("pyout: %s", msg.get('content', '')) if not self._hidden and self._is_from_this_session(msg): + self.flush_clearoutput() text = msg['content']['data'] self._append_plain_text(text + '\n', before_prompt=True) @@ -528,13 +540,8 @@ class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin): """ self.log.debug("stream: %s", msg.get('content', '')) if not self._hidden and self._is_from_this_session(msg): - # Most consoles treat tabs as being 8 space characters. Convert tabs - # to spaces so that output looks as expected regardless of this - # widget's tab width. - text = msg['content']['data'].expandtabs(8) - - self._append_plain_text(text, before_prompt=True) - self._control.moveCursor(QtGui.QTextCursor.End) + self.flush_clearoutput() + self.append_stream(msg['content']['data']) def _handle_shutdown_reply(self, msg): """ Handle shutdown signal, only if from other console. @@ -685,6 +692,29 @@ class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin): before_prompt=True ) + def append_stream(self, text): + """Appends text to the output stream.""" + # Most consoles treat tabs as being 8 space characters. Convert tabs + # to spaces so that output looks as expected regardless of this + # widget's tab width. + text = text.expandtabs(8) + self._append_plain_text(text, before_prompt=True) + self._control.moveCursor(QtGui.QTextCursor.End) + + def flush_clearoutput(self): + """If a clearoutput is pending, execute it.""" + if self._pending_clearoutput: + self._pending_clearoutput = False + self.clear_output() + + def clear_output(self): + """Clears the current line of output.""" + cursor = self._control.textCursor() + cursor.beginEditBlock() + cursor.movePosition(cursor.StartOfLine, cursor.KeepAnchor) + cursor.insertText('') + cursor.endEditBlock() + #--------------------------------------------------------------------------- # 'FrontendWidget' protected interface #--------------------------------------------------------------------------- diff --git a/IPython/qt/console/ipython_widget.py b/IPython/qt/console/ipython_widget.py index 962fd86..423877a 100644 --- a/IPython/qt/console/ipython_widget.py +++ b/IPython/qt/console/ipython_widget.py @@ -140,7 +140,6 @@ class IPythonWidget(FrontendWidget): #--------------------------------------------------------------------------- # 'BaseFrontendMixin' abstract interface #--------------------------------------------------------------------------- - def _handle_complete_reply(self, rep): """ Reimplemented to support IPython's improved completion machinery. """ @@ -223,6 +222,7 @@ class IPythonWidget(FrontendWidget): """ self.log.debug("pyout: %s", msg.get('content', '')) if not self._hidden and self._is_from_this_session(msg): + self.flush_clearoutput() content = msg['content'] prompt_number = content.get('execution_count', 0) data = content['data'] @@ -250,6 +250,7 @@ class IPythonWidget(FrontendWidget): # eventually will as this allows all frontends to monitor the display # data. But we need to figure out how to handle this in the GUI. if not self._hidden and self._is_from_this_session(msg): + self.flush_clearoutput() source = msg['content']['source'] data = msg['content']['data'] metadata = msg['content']['metadata'] diff --git a/IPython/qt/console/rich_ipython_widget.py b/IPython/qt/console/rich_ipython_widget.py index 4da9855..a21c45d 100644 --- a/IPython/qt/console/rich_ipython_widget.py +++ b/IPython/qt/console/rich_ipython_widget.py @@ -114,6 +114,7 @@ class RichIPythonWidget(IPythonWidget): """ Overridden to handle rich data types, like SVG. """ if not self._hidden and self._is_from_this_session(msg): + self.flush_clearoutput() content = msg['content'] prompt_number = content.get('execution_count', 0) data = content['data'] @@ -140,6 +141,7 @@ class RichIPythonWidget(IPythonWidget): """ Overridden to handle rich data types, like SVG. """ if not self._hidden and self._is_from_this_session(msg): + self.flush_clearoutput() source = msg['content']['source'] data = msg['content']['data'] metadata = msg['content']['metadata'] diff --git a/IPython/terminal/console/interactiveshell.py b/IPython/terminal/console/interactiveshell.py index f127ab3..dc897bf 100644 --- a/IPython/terminal/console/interactiveshell.py +++ b/IPython/terminal/console/interactiveshell.py @@ -43,6 +43,7 @@ class ZMQTerminalInteractiveShell(TerminalInteractiveShell): """A subclass of TerminalInteractiveShell that uses the 0MQ kernel""" _executing = False _execution_state = Unicode('') + _pending_clearoutput = False kernel_timeout = Float(60, config=True, help="""Timeout for giving up on a kernel (in seconds). @@ -241,13 +242,22 @@ class ZMQTerminalInteractiveShell(TerminalInteractiveShell): self._execution_state = sub_msg["content"]["execution_state"] elif msg_type == 'stream': if sub_msg["content"]["name"] == "stdout": + if self._pending_clearoutput: + print("\r", file=io.stdout, end="") + self._pending_clearoutput = False print(sub_msg["content"]["data"], file=io.stdout, end="") io.stdout.flush() elif sub_msg["content"]["name"] == "stderr" : + if self._pending_clearoutput: + print("\r", file=io.stderr, end="") + self._pending_clearoutput = False print(sub_msg["content"]["data"], file=io.stderr, end="") io.stderr.flush() elif msg_type == 'pyout': + if self._pending_clearoutput: + print("\r", file=io.stdout, end="") + self._pending_clearoutput = False self.execution_count = int(sub_msg["content"]["execution_count"]) format_dict = sub_msg["content"]["data"] self.handle_rich_data(format_dict) @@ -267,6 +277,12 @@ class ZMQTerminalInteractiveShell(TerminalInteractiveShell): if 'text/plain' in data: print(data['text/plain']) + elif msg_type == 'clear_output': + if sub_msg["content"]["wait"]: + self._pending_clearoutput = True + else: + print("\r", file=io.stdout, end="") + _imagemime = { 'image/png': 'png', 'image/jpeg': 'jpeg',