From c38a6218b8285b7c3fe3157653c6cf13a18e6483 2014-07-10 14:59:01 From: Min RK Date: 2014-07-10 14:59:01 Subject: [PATCH] Merge pull request #5963 from jdfreder/viewids Allow widgets to display more than once within container widgets. --- diff --git a/IPython/html/static/widgets/js/manager.js b/IPython/html/static/widgets/js/manager.js index d1af78c..4aba4d2 100644 --- a/IPython/html/static/widgets/js/manager.js +++ b/IPython/html/static/widgets/js/manager.js @@ -86,6 +86,7 @@ cell.widget_area.show(); this._handle_display_view(view); cell.widget_subarea.append(view.$el); + view.trigger('displayed'); } } }; diff --git a/IPython/html/static/widgets/js/widget.js b/IPython/html/static/widgets/js/widget.js index 1f48bee..6c711a9 100644 --- a/IPython/html/static/widgets/js/widget.js +++ b/IPython/html/static/widgets/js/widget.js @@ -83,7 +83,6 @@ function(WidgetManager, _, Backbone){ break; case 'display': this.widget_manager.display_view(msg, this); - this.trigger('displayed'); break; } }, @@ -283,8 +282,10 @@ function(WidgetManager, _, Backbone){ // Public constructor. this.model.on('change',this.update,this); this.options = parameters.options; - this.child_views = []; + this.child_model_views = {}; + this.child_views = {}; this.model.views.push(this); + this.id = this.id || IPython.utils.uuid(); }, update: function(){ @@ -302,19 +303,39 @@ function(WidgetManager, _, Backbone){ // TODO: this is hacky, and makes the view depend on this cell attribute and widget manager behavior // it would be great to have the widget manager add the cell metadata // to the subview without having to add it here. - var child_view = this.model.widget_manager.create_view(child_model, options || {}, this); - this.child_views[child_model.id] = child_view; + options = $.extend({ parent: this }, options || {}); + var child_view = this.model.widget_manager.create_view(child_model, options, this); + + // Associate the view id with the model id. + if (this.child_model_views[child_model.id] === undefined) { + this.child_model_views[child_model.id] = []; + } + this.child_model_views[child_model.id].push(child_view.id); + + // Remember the view by id. + this.child_views[child_view.id] = child_view; return child_view; }, - delete_child_view: function(child_model, options) { + pop_child_view: function(child_model) { // Delete a child view that was previously created using create_child_view. - var view = this.child_views[child_model.id]; - if (view !== undefined) { - delete this.child_views[child_model.id]; - view.remove(); + var view_ids = this.child_model_views[child_model.id]; + if (view_ids !== undefined) { + + // Only delete the first view in the list. + var view_id = view_ids[0]; + var view = this.child_views[view_id]; + delete this.child_views[view_id]; + view_ids.splice(0,1); child_model.views.pop(view); + + // Remove the view list specific to this model if it is empty. + if (view_ids.length === 0) { + delete this.child_model_views[child_model.id]; + } + return view; } + return null; }, do_diff: function(old_list, new_list, removed_callback, added_callback) { @@ -330,16 +351,23 @@ function(WidgetManager, _, Backbone){ // added_callback : Callback(item) // Callback that is called for each item added. + // Walk the lists until an unequal entry is found. + var i; + for (i = 0; i < new_list.length; i++) { + if (i < old_list.length || new_list[i] !== old_list[i]) { + break; + } + } - // removed items - _.each(_.difference(old_list, new_list), function(item, index, list) { - removed_callback(item); - }, this); + // Remove the non-matching items from the old list. + for (var j = i; j < old_list.length; j++) { + removed_callback(old_list[j]); + } - // added items - _.each(_.difference(new_list, old_list), function(item, index, list) { - added_callback(item); - }, this); + // Add the rest of the new list items. + for (i; i < new_list.length; i++) { + added_callback(new_list[i]); + } }, callbacks: function(){ diff --git a/IPython/html/static/widgets/js/widget_container.js b/IPython/html/static/widgets/js/widget_container.js index f7cad2a..02bcf03 100644 --- a/IPython/html/static/widgets/js/widget_container.js +++ b/IPython/html/static/widgets/js/widget_container.js @@ -21,21 +21,20 @@ define(["widgets/js/widget"], function(WidgetManager) { // Called when view is rendered. this.$el.addClass('widget-container') .addClass('vbox'); - this.children={}; - this.update_children([], this.model.get('_children')); - this.model.on('change:_children', function(model, value, options) { - this.update_children(model.previous('_children'), value); + this.update_children([], this.model.get('children')); + this.model.on('change:children', function(model, value, options) { + this.update_children(model.previous('children'), value); }, this); this.update(); // Trigger model displayed events for any models that are child to // this model when this model is displayed. var that = this; - this.model.on('displayed', function(){ + this.on('displayed', function(){ that.is_displayed = true; for (var property in that.child_views) { if (that.child_views.hasOwnProperty(property)) { - that.child_views[property].model.trigger('displayed'); + that.child_views[property].trigger('displayed'); } } }); @@ -51,8 +50,7 @@ define(["widgets/js/widget"], function(WidgetManager) { remove_child_model: function(model) { // Called when a model is removed from the children list. - this.child_views[model.id].remove(); - this.delete_child_view(model); + this.pop_child_view(model).remove(); }, add_child_model: function(model) { @@ -62,7 +60,7 @@ define(["widgets/js/widget"], function(WidgetManager) { // Trigger the displayed event if this model is displayed. if (this.is_displayed) { - model.trigger('displayed'); + view.trigger('displayed'); } }, @@ -81,7 +79,6 @@ define(["widgets/js/widget"], function(WidgetManager) { render: function(){ // Called when view is rendered. var that = this; - this.children={}; this.$el.on("remove", function(){ that.$backdrop.remove(); @@ -187,19 +184,19 @@ define(["widgets/js/widget"], function(WidgetManager) { this._shown_once = false; this.popped_out = true; - this.update_children([], this.model.get('_children')); - this.model.on('change:_children', function(model, value, options) { - this.update_children(model.previous('_children'), value); + this.update_children([], this.model.get('children')); + this.model.on('change:children', function(model, value, options) { + this.update_children(model.previous('children'), value); }, this); this.update(); // Trigger model displayed events for any models that are child to // this model when this model is displayed. - this.model.on('displayed', function(){ + this.on('displayed', function(){ that.is_displayed = true; for (var property in that.child_views) { if (that.child_views.hasOwnProperty(property)) { - that.child_views[property].model.trigger('displayed'); + that.child_views[property].trigger('displayed'); } } }); @@ -257,8 +254,7 @@ define(["widgets/js/widget"], function(WidgetManager) { remove_child_model: function(model) { // Called when a child is removed from children list. - this.child_views[model.id].remove(); - this.delete_child_view(model); + this.pop_child_view(model).remove(); }, add_child_model: function(model) { @@ -268,7 +264,7 @@ define(["widgets/js/widget"], function(WidgetManager) { // Trigger the displayed event if this model is displayed. if (this.is_displayed) { - model.trigger('displayed'); + view.trigger('displayed'); } }, diff --git a/IPython/html/static/widgets/js/widget_int.js b/IPython/html/static/widgets/js/widget_int.js index f7a6c6e..6a94205 100644 --- a/IPython/html/static/widgets/js/widget_int.js +++ b/IPython/html/static/widgets/js/widget_int.js @@ -55,6 +55,7 @@ define(["widgets/js/widget"], function(WidgetManager){ // one-to-one mapping with the corrosponding keys of the model. var jquery_slider_keys = ['step', 'max', 'min', 'disabled']; var that = this; + that.$slider.slider({}); _.each(jquery_slider_keys, function(key, i) { var model_value = that.model.get(key); if (model_value !== undefined) { diff --git a/IPython/html/static/widgets/js/widget_selectioncontainer.js b/IPython/html/static/widgets/js/widget_selectioncontainer.js index e7981de..7dd49e0 100644 --- a/IPython/html/static/widgets/js/widget_selectioncontainer.js +++ b/IPython/html/static/widgets/js/widget_selectioncontainer.js @@ -25,9 +25,9 @@ define(["widgets/js/widget"], function(WidgetManager){ .addClass('panel-group'); this.containers = []; this.model_containers = {}; - this.update_children([], this.model.get('_children')); - this.model.on('change:_children', function(model, value, options) { - this.update_children(model.previous('_children'), value); + this.update_children([], this.model.get('children')); + this.model.on('change:children', function(model, value, options) { + this.update_children(model.previous('children'), value); }, this); this.model.on('change:selected_index', function(model, value, options) { this.update_selected_index(model.previous('selected_index'), value, options); @@ -36,14 +36,14 @@ define(["widgets/js/widget"], function(WidgetManager){ this.update_titles(value); }, this); var that = this; - this.model.on('displayed', function() { + this.on('displayed', function() { this.update_titles(); // Trigger model displayed events for any models that are child to // this model when this model is displayed. that.is_displayed = true; for (var property in that.child_views) { if (that.child_views.hasOwnProperty(property)) { - that.child_views[property].model.trigger('displayed'); + that.child_views[property].trigger('displayed'); } } }, this); @@ -92,7 +92,7 @@ define(["widgets/js/widget"], function(WidgetManager){ this.containers.splice(accordion_group.container_index, 1); delete this.model_containers[model.id]; accordion_group.remove(); - this.delete_child_view(model); + this.pop_child_view(model); }, add_child_model: function(model) { @@ -137,7 +137,7 @@ define(["widgets/js/widget"], function(WidgetManager){ // Trigger the displayed event if this model is displayed. if (this.is_displayed) { - model.trigger('displayed'); + view.trigger('displayed'); } }, }); @@ -163,18 +163,18 @@ define(["widgets/js/widget"], function(WidgetManager){ .addClass('tab-content') .appendTo(this.$el); this.containers = []; - this.update_children([], this.model.get('_children')); - this.model.on('change:_children', function(model, value, options) { - this.update_children(model.previous('_children'), value); + this.update_children([], this.model.get('children')); + this.model.on('change:children', function(model, value, options) { + this.update_children(model.previous('children'), value); }, this); // Trigger model displayed events for any models that are child to // this model when this model is displayed. - this.model.on('displayed', function(){ + this.on('displayed', function(){ that.is_displayed = true; for (var property in that.child_views) { if (that.child_views.hasOwnProperty(property)) { - that.child_views[property].model.trigger('displayed'); + that.child_views[property].trigger('displayed'); } } }); @@ -190,12 +190,11 @@ define(["widgets/js/widget"], function(WidgetManager){ remove_child_model: function(model) { // Called when a child is removed from children list. - var view = this.child_views[model.id]; + var view = this.pop_child_view(model); this.containers.splice(view.parent_tab.tab_text_index, 1); view.parent_tab.remove(); view.parent_container.remove(); view.remove(); - this.delete_child_view(model); }, add_child_model: function(model) { @@ -234,7 +233,7 @@ define(["widgets/js/widget"], function(WidgetManager){ // Trigger the displayed event if this model is displayed. if (this.is_displayed) { - model.trigger('displayed'); + view.trigger('displayed'); } }, diff --git a/IPython/html/widgets/widget_container.py b/IPython/html/widgets/widget_container.py index f70fa6b..dce2a2f 100644 --- a/IPython/html/widgets/widget_container.py +++ b/IPython/html/widgets/widget_container.py @@ -2,58 +2,29 @@ Represents a container that can be used to group other widgets. """ -#----------------------------------------------------------------------------- -# Copyright (c) 2013, the IPython Development Team. -# + +# Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. -# -# The full license is in the file COPYING.txt, distributed with this software. -#----------------------------------------------------------------------------- -#----------------------------------------------------------------------------- -# Imports -#----------------------------------------------------------------------------- from .widget import DOMWidget from IPython.utils.traitlets import Unicode, Tuple, TraitError -#----------------------------------------------------------------------------- -# Classes -#----------------------------------------------------------------------------- - class ContainerWidget(DOMWidget): _view_name = Unicode('ContainerView', sync=True) # Child widgets in the container. # Using a tuple here to force reassignment to update the list. # When a proper notifying-list trait exists, that is what should be used here. - children = Tuple() - _children = Tuple(sync=True) - + children = Tuple(sync=True) def __init__(self, **kwargs): super(ContainerWidget, self).__init__(**kwargs) self.on_displayed(ContainerWidget._fire_children_displayed) def _fire_children_displayed(self): - for child in self._children: + for child in self.children: child._handle_displayed() - def _children_changed(self, name, old, new): - """Validate children list. - - Makes sure only one instance of any given model can exist in the - children list. - An excellent post on uniqifiers is available at - http://www.peterbe.com/plog/uniqifiers-benchmark - which provides the inspiration for using this implementation. Below - I've implemented the `f5` algorithm using Python comprehensions.""" - if new is not None: - seen = {} - def add_item(i): - seen[i.model_id] = True - return i - self._children = [add_item(i) for i in new if not i.model_id in seen] - class PopupWidget(ContainerWidget): _view_name = Unicode('PopupView', sync=True)