From a5779bcd0e52a8e25439e405e59cf9d60eab25da Mon Sep 17 00:00:00 2001 From: Jonathan Frederic Date: Thu, 9 Oct 2014 15:58:27 -0700 Subject: [PATCH 1/8] Output Widget --- IPython/html/static/notebook/js/codecell.js | 46 ++++++++++++++++--- IPython/html/static/widgets/js/init.js | 1 + .../html/static/widgets/js/widget_output.js | 41 +++++++++++++++++ IPython/html/widgets/__init__.py | 1 + IPython/html/widgets/widget_output.py | 32 +++++++++++++ 5 files changed, 115 insertions(+), 6 deletions(-) create mode 100644 IPython/html/static/widgets/js/widget_output.js create mode 100644 IPython/html/widgets/widget_output.py diff --git a/IPython/html/static/notebook/js/codecell.js b/IPython/html/static/notebook/js/codecell.js index ae0afda1d..fb958d26c 100644 --- a/IPython/html/static/notebook/js/codecell.js +++ b/IPython/html/static/notebook/js/codecell.js @@ -23,6 +23,7 @@ define([ 'notebook/js/codemirror-ipython' ], function(IPython, $, utils, keyboard, cell, outputarea, completer, celltoolbar, CodeMirror, cmpython, cmip) { "use strict"; + var Cell = cell.Cell; /* local util for codemirror */ @@ -79,6 +80,7 @@ define([ this.input_prompt_number = null; this.celltoolbar = null; this.output_area = null; + this.active_output_area = []; this.last_msg_id = null; this.completer = null; @@ -117,6 +119,31 @@ define([ CodeCell.prototype = Object.create(Cell.prototype); + /** + * @method get_output_area + */ + CodeCell.prototype.get_output_area = function () { + if (this.active_output_area && this.active_output_area.length > 0) { + return this.active_output_area[this.active_output_area.length-1]; + } else { + return this.output_area; + } + }; + + /** + * @method push_output_area + */ + CodeCell.prototype.push_output_area = function (output_area) { + this.active_output_area.push(output_area); + }; + + /** + * @method pop_output_area + */ + CodeCell.prototype.pop_output_area = function () { + this.active_output_area.pop(); + }; + /** * @method auto_highlight */ @@ -282,8 +309,8 @@ define([ console.log("Can't execute, kernel is not connected."); return; } - - this.output_area.clear_output(); + + this.get_output_area().clear_output(); // Clear widget area this.widget_subarea.html(''); @@ -312,6 +339,7 @@ define([ * @method get_callbacks */ CodeCell.prototype.get_callbacks = function () { + var that = this; return { shell : { reply : $.proxy(this._handle_execute_reply, this), @@ -321,8 +349,14 @@ define([ } }, iopub : { - output : $.proxy(this.output_area.handle_output, this.output_area), - clear_output : $.proxy(this.output_area.handle_clear_output, this.output_area), + output : function() { + var output_area = that.get_output_area(); + output_area.handle_output.apply(output_area, arguments); + }, + clear_output : function() { + var output_area = that.get_output_area(); + output_area.handle_clear_output.apply(output_area, arguments); + }, }, input : $.proxy(this._handle_input_request, this) }; @@ -356,7 +390,7 @@ define([ * @private */ CodeCell.prototype._handle_input_request = function (msg) { - this.output_area.append_raw_input(msg); + this.get_output_area().append_raw_input(msg); }; @@ -459,7 +493,7 @@ define([ CodeCell.prototype.clear_output = function (wait) { - this.output_area.clear_output(wait); + this.get_output_area().clear_output(wait); this.set_input_prompt(); }; diff --git a/IPython/html/static/widgets/js/init.js b/IPython/html/static/widgets/js/init.js index cfbd13465..a65ffe46f 100644 --- a/IPython/html/static/widgets/js/init.js +++ b/IPython/html/static/widgets/js/init.js @@ -9,6 +9,7 @@ define([ "widgets/js/widget_float", "widgets/js/widget_image", "widgets/js/widget_int", + "widgets/js/widget_output", "widgets/js/widget_selection", "widgets/js/widget_selectioncontainer", "widgets/js/widget_string", diff --git a/IPython/html/static/widgets/js/widget_output.js b/IPython/html/static/widgets/js/widget_output.js new file mode 100644 index 000000000..1a77b6854 --- /dev/null +++ b/IPython/html/static/widgets/js/widget_output.js @@ -0,0 +1,41 @@ +// Copyright (c) IPython Development Team. +// Distributed under the terms of the Modified BSD License. + +define([ + "widgets/js/widget", + "jquery", + 'notebook/js/outputarea', +], function(widget, $, outputarea){ + + var OutputView = widget.DOMWidgetView.extend({ + initialize: function (parameters) { + // Public constructor + OutputView.__super__.initialize.apply(this, [parameters]); + this.model.on('msg:custom', this._handle_route_msg, this); + }, + + render: function(){ + // Called when view is rendered. + this.output_area = new outputarea.OutputArea({ + selector: this.$el, + prompt_area: false, + events: this.model.widget_manager.notebook.events, + keyboard_manager: this.model.widget_manager.keyboard_manager }); + }, + + _handle_route_msg: function(content) { + var cell = this.options.cell; + if (content && cell) { + if (content.method == 'push') { + cell.push_output_area(this.output_area); + } else if (content.method == 'pop') { + cell.pop_output_area(this.output_area); + } + } + }, + }); + + return { + 'OutputView': OutputView, + }; +}); diff --git a/IPython/html/widgets/__init__.py b/IPython/html/widgets/__init__.py index 876635665..d62a3d218 100644 --- a/IPython/html/widgets/__init__.py +++ b/IPython/html/widgets/__init__.py @@ -6,6 +6,7 @@ from .widget_box import Box, Popup, FlexBox, HBox, VBox from .widget_float import FloatText, BoundedFloatText, FloatSlider, FloatProgress, FloatRangeSlider from .widget_image import Image from .widget_int import IntText, BoundedIntText, IntSlider, IntProgress, IntRangeSlider +from .widget_output import Output from .widget_selection import RadioButtons, ToggleButtons, Dropdown, Select from .widget_selectioncontainer import Tab, Accordion from .widget_string import HTML, Latex, Text, Textarea diff --git a/IPython/html/widgets/widget_output.py b/IPython/html/widgets/widget_output.py new file mode 100644 index 000000000..f07f6dd44 --- /dev/null +++ b/IPython/html/widgets/widget_output.py @@ -0,0 +1,32 @@ +"""Output class. + +Represents a widget that can be used to display output within the widget area. +""" + +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. + +from .widget import DOMWidget +import sys +from IPython.utils.traitlets import Unicode, List +from IPython.display import clear_output + +class Output(DOMWidget): + """Displays multiple widgets in a group.""" + _view_name = Unicode('OutputView', sync=True) + + def clear_output(self, *pargs, **kwargs): + with self: + clear_output(*pargs, **kwargs) + + def __enter__(self): + self._flush() + self.send({'method': 'push'}) + + def __exit__(self, exception_type, exception_value, traceback): + self._flush() + self.send({'method': 'pop'}) + + def _flush(self): + sys.stdout.flush() + sys.stderr.flush() From d033d6c1ea984799d7b9012aa7e3600ab52f2365 Mon Sep 17 00:00:00 2001 From: Jonathan Frederic Date: Thu, 9 Oct 2014 16:10:03 -0700 Subject: [PATCH 2/8] Make output widget reactive. --- IPython/html/static/notebook/js/outputarea.js | 7 +++++++ IPython/html/static/widgets/js/widget_output.js | 15 +++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/IPython/html/static/notebook/js/outputarea.js b/IPython/html/static/notebook/js/outputarea.js index ae56b948a..047198839 100644 --- a/IPython/html/static/notebook/js/outputarea.js +++ b/IPython/html/static/notebook/js/outputarea.js @@ -400,6 +400,9 @@ define([ this._append_javascript_error(err, subarea); this.element.append(toinsert); } + + // Notify others of changes. + this.element.trigger('changed'); }; @@ -832,6 +835,10 @@ define([ // them to fire if the image is never added to the page. this.element.find('img').off('load'); this.element.html(""); + + // Notify others of changes. + this.element.trigger('changed'); + this.outputs = []; this.trusted = true; this.unscroll_area(); diff --git a/IPython/html/static/widgets/js/widget_output.js b/IPython/html/static/widgets/js/widget_output.js index 1a77b6854..15a158a1c 100644 --- a/IPython/html/static/widgets/js/widget_output.js +++ b/IPython/html/static/widgets/js/widget_output.js @@ -21,6 +21,21 @@ define([ prompt_area: false, events: this.model.widget_manager.notebook.events, keyboard_manager: this.model.widget_manager.keyboard_manager }); + + // Make output area reactive. + var that = this; + this.output_area.element.on('changed', function() { + that.model.set('contents', that.output_area.element.html()); + }); + this.model.on('change:contents', function(){ + var html = this.model.get('contents'); + if (this.output_area.element.html() != html) { + this.output_area.element.html(html); + } + }, this); + + // Set initial contents. + this.output_area.element.html(this.model.get('contents')); }, _handle_route_msg: function(content) { From a12da7cee428d0f0950f8631fb086cc1819503cb Mon Sep 17 00:00:00 2001 From: Jonathan Frederic Date: Thu, 16 Oct 2014 11:05:43 -0700 Subject: [PATCH 3/8] Added comment describing output area stack. --- IPython/html/static/notebook/js/codecell.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/IPython/html/static/notebook/js/codecell.js b/IPython/html/static/notebook/js/codecell.js index fb958d26c..966e992e4 100644 --- a/IPython/html/static/notebook/js/codecell.js +++ b/IPython/html/static/notebook/js/codecell.js @@ -80,6 +80,11 @@ define([ this.input_prompt_number = null; this.celltoolbar = null; this.output_area = null; + // Keep a stack of the 'active' output areas (where active means the + // output area that recieves output). When a user activates an output + // area, it gets pushed to the stack. Then, when the output area is + // deactivated, it's popped from the stack. When the stack is empty, + // the cell's output area is used. this.active_output_area = []; this.last_msg_id = null; this.completer = null; From 6f2e02b585d5af396ed3a6a44a921a3b2d6f0811 Mon Sep 17 00:00:00 2001 From: Jonathan Frederic Date: Thu, 16 Oct 2014 11:11:31 -0700 Subject: [PATCH 4/8] Pop output now removes a specific output --- IPython/html/static/notebook/js/codecell.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/IPython/html/static/notebook/js/codecell.js b/IPython/html/static/notebook/js/codecell.js index 966e992e4..07db9edbe 100644 --- a/IPython/html/static/notebook/js/codecell.js +++ b/IPython/html/static/notebook/js/codecell.js @@ -145,8 +145,11 @@ define([ /** * @method pop_output_area */ - CodeCell.prototype.pop_output_area = function () { - this.active_output_area.pop(); + CodeCell.prototype.pop_output_area = function (output_area) { + var index = this.active_output_area.lastIndexOf(output_area); + if (index > -1) { + this.active_output_area.splice(index, 1); + } }; /** From 36f0aabb8607435c1d88438de65e021d6a230697 Mon Sep 17 00:00:00 2001 From: Jonathan Frederic Date: Thu, 16 Oct 2014 14:32:46 -0700 Subject: [PATCH 5/8] active_output_areas (plural) --- IPython/html/static/notebook/js/codecell.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/IPython/html/static/notebook/js/codecell.js b/IPython/html/static/notebook/js/codecell.js index 07db9edbe..520b98d71 100644 --- a/IPython/html/static/notebook/js/codecell.js +++ b/IPython/html/static/notebook/js/codecell.js @@ -85,7 +85,7 @@ define([ // area, it gets pushed to the stack. Then, when the output area is // deactivated, it's popped from the stack. When the stack is empty, // the cell's output area is used. - this.active_output_area = []; + this.active_output_areas = []; this.last_msg_id = null; this.completer = null; @@ -128,8 +128,8 @@ define([ * @method get_output_area */ CodeCell.prototype.get_output_area = function () { - if (this.active_output_area && this.active_output_area.length > 0) { - return this.active_output_area[this.active_output_area.length-1]; + if (this.active_output_areas && this.active_output_areas.length > 0) { + return this.active_output_areas[this.active_output_areas.length-1]; } else { return this.output_area; } @@ -139,16 +139,16 @@ define([ * @method push_output_area */ CodeCell.prototype.push_output_area = function (output_area) { - this.active_output_area.push(output_area); + this.active_output_areas.push(output_area); }; /** * @method pop_output_area */ CodeCell.prototype.pop_output_area = function (output_area) { - var index = this.active_output_area.lastIndexOf(output_area); + var index = this.active_output_areas.lastIndexOf(output_area); if (index > -1) { - this.active_output_area.splice(index, 1); + this.active_output_areas.splice(index, 1); } }; From db2ef15b8143af343f50c920695a2d06c08027d7 Mon Sep 17 00:00:00 2001 From: Jonathan Frederic Date: Fri, 14 Nov 2014 10:59:09 -0800 Subject: [PATCH 6/8] Address @carreau 's comments --- IPython/html/static/notebook/js/codecell.js | 36 +++++++++---------- .../html/static/widgets/js/widget_output.js | 3 +- 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/IPython/html/static/notebook/js/codecell.js b/IPython/html/static/notebook/js/codecell.js index 520b98d71..7490671ea 100644 --- a/IPython/html/static/notebook/js/codecell.js +++ b/IPython/html/static/notebook/js/codecell.js @@ -86,6 +86,17 @@ define([ // deactivated, it's popped from the stack. When the stack is empty, // the cell's output area is used. this.active_output_areas = []; + var that = this; + Object.defineProperty(this, 'active_output_area', { + get: function() { + if (that.active_output_areas && that.active_output_areas.length > 0) { + return that.active_output_areas[that.active_output_areas.length-1]; + } else { + return that.output_area; + } + }, + }); + this.last_msg_id = null; this.completer = null; @@ -98,8 +109,6 @@ define([ // Attributes we want to override in this subclass. this.cell_type = "code"; - - var that = this; this.element.focusout( function() { that.auto_highlight(); } ); @@ -124,17 +133,6 @@ define([ CodeCell.prototype = Object.create(Cell.prototype); - /** - * @method get_output_area - */ - CodeCell.prototype.get_output_area = function () { - if (this.active_output_areas && this.active_output_areas.length > 0) { - return this.active_output_areas[this.active_output_areas.length-1]; - } else { - return this.output_area; - } - }; - /** * @method push_output_area */ @@ -318,7 +316,7 @@ define([ return; } - this.get_output_area().clear_output(); + this.active_output_area.clear_output(); // Clear widget area this.widget_subarea.html(''); @@ -358,12 +356,10 @@ define([ }, iopub : { output : function() { - var output_area = that.get_output_area(); - output_area.handle_output.apply(output_area, arguments); + that.active_output_area.handle_output.apply(output_area, arguments); }, clear_output : function() { - var output_area = that.get_output_area(); - output_area.handle_clear_output.apply(output_area, arguments); + that.active_output_area.handle_clear_output.apply(output_area, arguments); }, }, input : $.proxy(this._handle_input_request, this) @@ -398,7 +394,7 @@ define([ * @private */ CodeCell.prototype._handle_input_request = function (msg) { - this.get_output_area().append_raw_input(msg); + this.active_output_area.append_raw_input(msg); }; @@ -501,7 +497,7 @@ define([ CodeCell.prototype.clear_output = function (wait) { - this.get_output_area().clear_output(wait); + this.active_output_area.clear_output(wait); this.set_input_prompt(); }; diff --git a/IPython/html/static/widgets/js/widget_output.js b/IPython/html/static/widgets/js/widget_output.js index 15a158a1c..a9f26f670 100644 --- a/IPython/html/static/widgets/js/widget_output.js +++ b/IPython/html/static/widgets/js/widget_output.js @@ -5,7 +5,8 @@ define([ "widgets/js/widget", "jquery", 'notebook/js/outputarea', -], function(widget, $, outputarea){ +], function(widget, $, outputarea) { + 'use strict'; var OutputView = widget.DOMWidgetView.extend({ initialize: function (parameters) { From 7a3a9e74e9948fb017e65b4e205105e85c161f17 Mon Sep 17 00:00:00 2001 From: Jonathan Frederic Date: Sat, 15 Nov 2014 09:10:04 -0800 Subject: [PATCH 7/8] Bug fix, typo --- IPython/html/static/notebook/js/codecell.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/IPython/html/static/notebook/js/codecell.js b/IPython/html/static/notebook/js/codecell.js index 7490671ea..7d06188a2 100644 --- a/IPython/html/static/notebook/js/codecell.js +++ b/IPython/html/static/notebook/js/codecell.js @@ -356,10 +356,10 @@ define([ }, iopub : { output : function() { - that.active_output_area.handle_output.apply(output_area, arguments); + that.active_output_area.handle_output.apply(that.active_output_area, arguments); }, clear_output : function() { - that.active_output_area.handle_clear_output.apply(output_area, arguments); + that.active_output_area.handle_clear_output.apply(that.active_output_area, arguments); }, }, input : $.proxy(this._handle_input_request, this) From 2ef509e505b1eb2da592d55254c68392ecfec9dc Mon Sep 17 00:00:00 2001 From: Jonathan Frederic Date: Mon, 17 Nov 2014 10:50:35 -0800 Subject: [PATCH 8/8] Add doc string to Output widget --- IPython/html/widgets/widget_output.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/IPython/html/widgets/widget_output.py b/IPython/html/widgets/widget_output.py index f07f6dd44..664bc7456 100644 --- a/IPython/html/widgets/widget_output.py +++ b/IPython/html/widgets/widget_output.py @@ -10,9 +10,27 @@ from .widget import DOMWidget import sys from IPython.utils.traitlets import Unicode, List from IPython.display import clear_output +from IPython.testing.skipdoctest import skip_doctest +@skip_doctest class Output(DOMWidget): - """Displays multiple widgets in a group.""" + """Widget used as a context manager to display output. + + This widget can capture and display stdout, stderr, and rich output. To use + it, create an instance of it and display it. Then use it as a context + manager. Any output produced while in it's context will be captured and + displayed in it instead of the standard output area. + + Example + from IPython.html import widgets + from IPython.display import display + out = widgets.Output() + display(out) + + print('prints to output area') + + with out: + print('prints to output widget')""" _view_name = Unicode('OutputView', sync=True) def clear_output(self, *pargs, **kwargs):