##// END OF EJS Templates
Merge pull request #5963 from jdfreder/viewids...
Min RK -
r17186:c38a6218 merge
parent child Browse files
Show More
@@ -1,213 +1,214 b''
1 //----------------------------------------------------------------------------
1 //----------------------------------------------------------------------------
2 // Copyright (C) 2013 The IPython Development Team
2 // Copyright (C) 2013 The IPython Development Team
3 //
3 //
4 // Distributed under the terms of the BSD License. The full license is in
4 // Distributed under the terms of the BSD License. The full license is in
5 // the file COPYING, distributed as part of this software.
5 // the file COPYING, distributed as part of this software.
6 //----------------------------------------------------------------------------
6 //----------------------------------------------------------------------------
7
7
8 //============================================================================
8 //============================================================================
9 // WidgetModel, WidgetView, and WidgetManager
9 // WidgetModel, WidgetView, and WidgetManager
10 //============================================================================
10 //============================================================================
11 /**
11 /**
12 * Base Widget classes
12 * Base Widget classes
13 * @module IPython
13 * @module IPython
14 * @namespace IPython
14 * @namespace IPython
15 * @submodule widget
15 * @submodule widget
16 */
16 */
17
17
18 (function () {
18 (function () {
19 "use strict";
19 "use strict";
20
20
21 // Use require.js 'define' method so that require.js is intelligent enough to
21 // Use require.js 'define' method so that require.js is intelligent enough to
22 // syncronously load everything within this file when it is being 'required'
22 // syncronously load everything within this file when it is being 'required'
23 // elsewhere.
23 // elsewhere.
24 define(["underscore",
24 define(["underscore",
25 "backbone",
25 "backbone",
26 ], function (_, Backbone) {
26 ], function (_, Backbone) {
27
27
28 //--------------------------------------------------------------------
28 //--------------------------------------------------------------------
29 // WidgetManager class
29 // WidgetManager class
30 //--------------------------------------------------------------------
30 //--------------------------------------------------------------------
31 var WidgetManager = function (comm_manager) {
31 var WidgetManager = function (comm_manager) {
32 // Public constructor
32 // Public constructor
33 WidgetManager._managers.push(this);
33 WidgetManager._managers.push(this);
34
34
35 // Attach a comm manager to the
35 // Attach a comm manager to the
36 this.comm_manager = comm_manager;
36 this.comm_manager = comm_manager;
37 this._models = {}; /* Dictionary of model ids and model instances */
37 this._models = {}; /* Dictionary of model ids and model instances */
38
38
39 // Register already-registered widget model types with the comm manager.
39 // Register already-registered widget model types with the comm manager.
40 var that = this;
40 var that = this;
41 _.each(WidgetManager._model_types, function(model_type, model_name) {
41 _.each(WidgetManager._model_types, function(model_type, model_name) {
42 that.comm_manager.register_target(model_name, $.proxy(that._handle_comm_open, that));
42 that.comm_manager.register_target(model_name, $.proxy(that._handle_comm_open, that));
43 });
43 });
44 };
44 };
45
45
46 //--------------------------------------------------------------------
46 //--------------------------------------------------------------------
47 // Class level
47 // Class level
48 //--------------------------------------------------------------------
48 //--------------------------------------------------------------------
49 WidgetManager._model_types = {}; /* Dictionary of model type names (target_name) and model types. */
49 WidgetManager._model_types = {}; /* Dictionary of model type names (target_name) and model types. */
50 WidgetManager._view_types = {}; /* Dictionary of view names and view types. */
50 WidgetManager._view_types = {}; /* Dictionary of view names and view types. */
51 WidgetManager._managers = []; /* List of widget managers */
51 WidgetManager._managers = []; /* List of widget managers */
52
52
53 WidgetManager.register_widget_model = function (model_name, model_type) {
53 WidgetManager.register_widget_model = function (model_name, model_type) {
54 // Registers a widget model by name.
54 // Registers a widget model by name.
55 WidgetManager._model_types[model_name] = model_type;
55 WidgetManager._model_types[model_name] = model_type;
56
56
57 // Register the widget with the comm manager. Make sure to pass this object's context
57 // Register the widget with the comm manager. Make sure to pass this object's context
58 // in so `this` works in the call back.
58 // in so `this` works in the call back.
59 _.each(WidgetManager._managers, function(instance, i) {
59 _.each(WidgetManager._managers, function(instance, i) {
60 if (instance.comm_manager !== null) {
60 if (instance.comm_manager !== null) {
61 instance.comm_manager.register_target(model_name, $.proxy(instance._handle_comm_open, instance));
61 instance.comm_manager.register_target(model_name, $.proxy(instance._handle_comm_open, instance));
62 }
62 }
63 });
63 });
64 };
64 };
65
65
66 WidgetManager.register_widget_view = function (view_name, view_type) {
66 WidgetManager.register_widget_view = function (view_name, view_type) {
67 // Registers a widget view by name.
67 // Registers a widget view by name.
68 WidgetManager._view_types[view_name] = view_type;
68 WidgetManager._view_types[view_name] = view_type;
69 };
69 };
70
70
71 //--------------------------------------------------------------------
71 //--------------------------------------------------------------------
72 // Instance level
72 // Instance level
73 //--------------------------------------------------------------------
73 //--------------------------------------------------------------------
74 WidgetManager.prototype.display_view = function(msg, model) {
74 WidgetManager.prototype.display_view = function(msg, model) {
75 // Displays a view for a particular model.
75 // Displays a view for a particular model.
76 var cell = this.get_msg_cell(msg.parent_header.msg_id);
76 var cell = this.get_msg_cell(msg.parent_header.msg_id);
77 if (cell === null) {
77 if (cell === null) {
78 console.log("Could not determine where the display" +
78 console.log("Could not determine where the display" +
79 " message was from. Widget will not be displayed");
79 " message was from. Widget will not be displayed");
80 } else {
80 } else {
81 var view = this.create_view(model, {cell: cell});
81 var view = this.create_view(model, {cell: cell});
82 if (view === null) {
82 if (view === null) {
83 console.error("View creation failed", model);
83 console.error("View creation failed", model);
84 }
84 }
85 if (cell.widget_subarea) {
85 if (cell.widget_subarea) {
86 cell.widget_area.show();
86 cell.widget_area.show();
87 this._handle_display_view(view);
87 this._handle_display_view(view);
88 cell.widget_subarea.append(view.$el);
88 cell.widget_subarea.append(view.$el);
89 view.trigger('displayed');
89 }
90 }
90 }
91 }
91 };
92 };
92
93
93 WidgetManager.prototype._handle_display_view = function (view) {
94 WidgetManager.prototype._handle_display_view = function (view) {
94 // Have the IPython keyboard manager disable its event
95 // Have the IPython keyboard manager disable its event
95 // handling so the widget can capture keyboard input.
96 // handling so the widget can capture keyboard input.
96 // Note, this is only done on the outer most widgets.
97 // Note, this is only done on the outer most widgets.
97 IPython.keyboard_manager.register_events(view.$el);
98 IPython.keyboard_manager.register_events(view.$el);
98
99
99 if (view.additional_elements) {
100 if (view.additional_elements) {
100 for (var i = 0; i < view.additional_elements.length; i++) {
101 for (var i = 0; i < view.additional_elements.length; i++) {
101 IPython.keyboard_manager.register_events(view.additional_elements[i]);
102 IPython.keyboard_manager.register_events(view.additional_elements[i]);
102 }
103 }
103 }
104 }
104 };
105 };
105
106
106 WidgetManager.prototype.create_view = function(model, options, view) {
107 WidgetManager.prototype.create_view = function(model, options, view) {
107 // Creates a view for a particular model.
108 // Creates a view for a particular model.
108 var view_name = model.get('_view_name');
109 var view_name = model.get('_view_name');
109 var ViewType = WidgetManager._view_types[view_name];
110 var ViewType = WidgetManager._view_types[view_name];
110 if (ViewType) {
111 if (ViewType) {
111
112
112 // If a view is passed into the method, use that view's cell as
113 // If a view is passed into the method, use that view's cell as
113 // the cell for the view that is created.
114 // the cell for the view that is created.
114 options = options || {};
115 options = options || {};
115 if (view !== undefined) {
116 if (view !== undefined) {
116 options.cell = view.options.cell;
117 options.cell = view.options.cell;
117 }
118 }
118
119
119 // Create and render the view...
120 // Create and render the view...
120 var parameters = {model: model, options: options};
121 var parameters = {model: model, options: options};
121 view = new ViewType(parameters);
122 view = new ViewType(parameters);
122 view.render();
123 view.render();
123 model.on('destroy', view.remove, view);
124 model.on('destroy', view.remove, view);
124 return view;
125 return view;
125 }
126 }
126 return null;
127 return null;
127 };
128 };
128
129
129 WidgetManager.prototype.get_msg_cell = function (msg_id) {
130 WidgetManager.prototype.get_msg_cell = function (msg_id) {
130 var cell = null;
131 var cell = null;
131 // First, check to see if the msg was triggered by cell execution.
132 // First, check to see if the msg was triggered by cell execution.
132 if (IPython.notebook) {
133 if (IPython.notebook) {
133 cell = IPython.notebook.get_msg_cell(msg_id);
134 cell = IPython.notebook.get_msg_cell(msg_id);
134 }
135 }
135 if (cell !== null) {
136 if (cell !== null) {
136 return cell;
137 return cell;
137 }
138 }
138 // Second, check to see if a get_cell callback was defined
139 // Second, check to see if a get_cell callback was defined
139 // for the message. get_cell callbacks are registered for
140 // for the message. get_cell callbacks are registered for
140 // widget messages, so this block is actually checking to see if the
141 // widget messages, so this block is actually checking to see if the
141 // message was triggered by a widget.
142 // message was triggered by a widget.
142 var kernel = this.comm_manager.kernel;
143 var kernel = this.comm_manager.kernel;
143 if (kernel) {
144 if (kernel) {
144 var callbacks = kernel.get_callbacks_for_msg(msg_id);
145 var callbacks = kernel.get_callbacks_for_msg(msg_id);
145 if (callbacks && callbacks.iopub &&
146 if (callbacks && callbacks.iopub &&
146 callbacks.iopub.get_cell !== undefined) {
147 callbacks.iopub.get_cell !== undefined) {
147 return callbacks.iopub.get_cell();
148 return callbacks.iopub.get_cell();
148 }
149 }
149 }
150 }
150
151
151 // Not triggered by a cell or widget (no get_cell callback
152 // Not triggered by a cell or widget (no get_cell callback
152 // exists).
153 // exists).
153 return null;
154 return null;
154 };
155 };
155
156
156 WidgetManager.prototype.callbacks = function (view) {
157 WidgetManager.prototype.callbacks = function (view) {
157 // callback handlers specific a view
158 // callback handlers specific a view
158 var callbacks = {};
159 var callbacks = {};
159 if (view && view.options.cell) {
160 if (view && view.options.cell) {
160
161
161 // Try to get output handlers
162 // Try to get output handlers
162 var cell = view.options.cell;
163 var cell = view.options.cell;
163 var handle_output = null;
164 var handle_output = null;
164 var handle_clear_output = null;
165 var handle_clear_output = null;
165 if (cell.output_area) {
166 if (cell.output_area) {
166 handle_output = $.proxy(cell.output_area.handle_output, cell.output_area);
167 handle_output = $.proxy(cell.output_area.handle_output, cell.output_area);
167 handle_clear_output = $.proxy(cell.output_area.handle_clear_output, cell.output_area);
168 handle_clear_output = $.proxy(cell.output_area.handle_clear_output, cell.output_area);
168 }
169 }
169
170
170 // Create callback dict using what is known
171 // Create callback dict using what is known
171 var that = this;
172 var that = this;
172 callbacks = {
173 callbacks = {
173 iopub : {
174 iopub : {
174 output : handle_output,
175 output : handle_output,
175 clear_output : handle_clear_output,
176 clear_output : handle_clear_output,
176
177
177 // Special function only registered by widget messages.
178 // Special function only registered by widget messages.
178 // Allows us to get the cell for a message so we know
179 // Allows us to get the cell for a message so we know
179 // where to add widgets if the code requires it.
180 // where to add widgets if the code requires it.
180 get_cell : function () {
181 get_cell : function () {
181 return cell;
182 return cell;
182 },
183 },
183 },
184 },
184 };
185 };
185 }
186 }
186 return callbacks;
187 return callbacks;
187 };
188 };
188
189
189 WidgetManager.prototype.get_model = function (model_id) {
190 WidgetManager.prototype.get_model = function (model_id) {
190 // Look-up a model instance by its id.
191 // Look-up a model instance by its id.
191 var model = this._models[model_id];
192 var model = this._models[model_id];
192 if (model !== undefined && model.id == model_id) {
193 if (model !== undefined && model.id == model_id) {
193 return model;
194 return model;
194 }
195 }
195 return null;
196 return null;
196 };
197 };
197
198
198 WidgetManager.prototype._handle_comm_open = function (comm, msg) {
199 WidgetManager.prototype._handle_comm_open = function (comm, msg) {
199 // Handle when a comm is opened.
200 // Handle when a comm is opened.
200 var that = this;
201 var that = this;
201 var model_id = comm.comm_id;
202 var model_id = comm.comm_id;
202 var widget_type_name = msg.content.target_name;
203 var widget_type_name = msg.content.target_name;
203 var widget_model = new WidgetManager._model_types[widget_type_name](this, model_id, comm);
204 var widget_model = new WidgetManager._model_types[widget_type_name](this, model_id, comm);
204 widget_model.on('comm:close', function () {
205 widget_model.on('comm:close', function () {
205 delete that._models[model_id];
206 delete that._models[model_id];
206 });
207 });
207 this._models[model_id] = widget_model;
208 this._models[model_id] = widget_model;
208 };
209 };
209
210
210 IPython.WidgetManager = WidgetManager;
211 IPython.WidgetManager = WidgetManager;
211 return IPython.WidgetManager;
212 return IPython.WidgetManager;
212 });
213 });
213 }());
214 }());
@@ -1,452 +1,480 b''
1 //----------------------------------------------------------------------------
1 //----------------------------------------------------------------------------
2 // Copyright (C) 2013 The IPython Development Team
2 // Copyright (C) 2013 The IPython Development Team
3 //
3 //
4 // Distributed under the terms of the BSD License. The full license is in
4 // Distributed under the terms of the BSD License. The full license is in
5 // the file COPYING, distributed as part of this software.
5 // the file COPYING, distributed as part of this software.
6 //----------------------------------------------------------------------------
6 //----------------------------------------------------------------------------
7
7
8 //============================================================================
8 //============================================================================
9 // Base Widget Model and View classes
9 // Base Widget Model and View classes
10 //============================================================================
10 //============================================================================
11
11
12 /**
12 /**
13 * @module IPython
13 * @module IPython
14 * @namespace IPython
14 * @namespace IPython
15 **/
15 **/
16
16
17 define(["widgets/js/manager",
17 define(["widgets/js/manager",
18 "underscore",
18 "underscore",
19 "backbone"],
19 "backbone"],
20 function(WidgetManager, _, Backbone){
20 function(WidgetManager, _, Backbone){
21
21
22 var WidgetModel = Backbone.Model.extend({
22 var WidgetModel = Backbone.Model.extend({
23 constructor: function (widget_manager, model_id, comm) {
23 constructor: function (widget_manager, model_id, comm) {
24 // Constructor
24 // Constructor
25 //
25 //
26 // Creates a WidgetModel instance.
26 // Creates a WidgetModel instance.
27 //
27 //
28 // Parameters
28 // Parameters
29 // ----------
29 // ----------
30 // widget_manager : WidgetManager instance
30 // widget_manager : WidgetManager instance
31 // model_id : string
31 // model_id : string
32 // An ID unique to this model.
32 // An ID unique to this model.
33 // comm : Comm instance (optional)
33 // comm : Comm instance (optional)
34 this.widget_manager = widget_manager;
34 this.widget_manager = widget_manager;
35 this._buffered_state_diff = {};
35 this._buffered_state_diff = {};
36 this.pending_msgs = 0;
36 this.pending_msgs = 0;
37 this.msg_buffer = null;
37 this.msg_buffer = null;
38 this.key_value_lock = null;
38 this.key_value_lock = null;
39 this.id = model_id;
39 this.id = model_id;
40 this.views = [];
40 this.views = [];
41
41
42 if (comm !== undefined) {
42 if (comm !== undefined) {
43 // Remember comm associated with the model.
43 // Remember comm associated with the model.
44 this.comm = comm;
44 this.comm = comm;
45 comm.model = this;
45 comm.model = this;
46
46
47 // Hook comm messages up to model.
47 // Hook comm messages up to model.
48 comm.on_close($.proxy(this._handle_comm_closed, this));
48 comm.on_close($.proxy(this._handle_comm_closed, this));
49 comm.on_msg($.proxy(this._handle_comm_msg, this));
49 comm.on_msg($.proxy(this._handle_comm_msg, this));
50 }
50 }
51 return Backbone.Model.apply(this);
51 return Backbone.Model.apply(this);
52 },
52 },
53
53
54 send: function (content, callbacks) {
54 send: function (content, callbacks) {
55 // Send a custom msg over the comm.
55 // Send a custom msg over the comm.
56 if (this.comm !== undefined) {
56 if (this.comm !== undefined) {
57 var data = {method: 'custom', content: content};
57 var data = {method: 'custom', content: content};
58 this.comm.send(data, callbacks);
58 this.comm.send(data, callbacks);
59 this.pending_msgs++;
59 this.pending_msgs++;
60 }
60 }
61 },
61 },
62
62
63 _handle_comm_closed: function (msg) {
63 _handle_comm_closed: function (msg) {
64 // Handle when a widget is closed.
64 // Handle when a widget is closed.
65 this.trigger('comm:close');
65 this.trigger('comm:close');
66 delete this.comm.model; // Delete ref so GC will collect widget model.
66 delete this.comm.model; // Delete ref so GC will collect widget model.
67 delete this.comm;
67 delete this.comm;
68 delete this.model_id; // Delete id from model so widget manager cleans up.
68 delete this.model_id; // Delete id from model so widget manager cleans up.
69 _.each(this.views, function(view, i) {
69 _.each(this.views, function(view, i) {
70 view.remove();
70 view.remove();
71 });
71 });
72 },
72 },
73
73
74 _handle_comm_msg: function (msg) {
74 _handle_comm_msg: function (msg) {
75 // Handle incoming comm msg.
75 // Handle incoming comm msg.
76 var method = msg.content.data.method;
76 var method = msg.content.data.method;
77 switch (method) {
77 switch (method) {
78 case 'update':
78 case 'update':
79 this.apply_update(msg.content.data.state);
79 this.apply_update(msg.content.data.state);
80 break;
80 break;
81 case 'custom':
81 case 'custom':
82 this.trigger('msg:custom', msg.content.data.content);
82 this.trigger('msg:custom', msg.content.data.content);
83 break;
83 break;
84 case 'display':
84 case 'display':
85 this.widget_manager.display_view(msg, this);
85 this.widget_manager.display_view(msg, this);
86 this.trigger('displayed');
87 break;
86 break;
88 }
87 }
89 },
88 },
90
89
91 apply_update: function (state) {
90 apply_update: function (state) {
92 // Handle when a widget is updated via the python side.
91 // Handle when a widget is updated via the python side.
93 var that = this;
92 var that = this;
94 _.each(state, function(value, key) {
93 _.each(state, function(value, key) {
95 that.key_value_lock = [key, value];
94 that.key_value_lock = [key, value];
96 try {
95 try {
97 WidgetModel.__super__.set.apply(that, [key, that._unpack_models(value)]);
96 WidgetModel.__super__.set.apply(that, [key, that._unpack_models(value)]);
98 } finally {
97 } finally {
99 that.key_value_lock = null;
98 that.key_value_lock = null;
100 }
99 }
101 });
100 });
102 },
101 },
103
102
104 _handle_status: function (msg, callbacks) {
103 _handle_status: function (msg, callbacks) {
105 // Handle status msgs.
104 // Handle status msgs.
106
105
107 // execution_state : ('busy', 'idle', 'starting')
106 // execution_state : ('busy', 'idle', 'starting')
108 if (this.comm !== undefined) {
107 if (this.comm !== undefined) {
109 if (msg.content.execution_state ==='idle') {
108 if (msg.content.execution_state ==='idle') {
110 // Send buffer if this message caused another message to be
109 // Send buffer if this message caused another message to be
111 // throttled.
110 // throttled.
112 if (this.msg_buffer !== null &&
111 if (this.msg_buffer !== null &&
113 (this.get('msg_throttle') || 3) === this.pending_msgs) {
112 (this.get('msg_throttle') || 3) === this.pending_msgs) {
114 var data = {method: 'backbone', sync_method: 'update', sync_data: this.msg_buffer};
113 var data = {method: 'backbone', sync_method: 'update', sync_data: this.msg_buffer};
115 this.comm.send(data, callbacks);
114 this.comm.send(data, callbacks);
116 this.msg_buffer = null;
115 this.msg_buffer = null;
117 } else {
116 } else {
118 --this.pending_msgs;
117 --this.pending_msgs;
119 }
118 }
120 }
119 }
121 }
120 }
122 },
121 },
123
122
124 callbacks: function(view) {
123 callbacks: function(view) {
125 // Create msg callbacks for a comm msg.
124 // Create msg callbacks for a comm msg.
126 var callbacks = this.widget_manager.callbacks(view);
125 var callbacks = this.widget_manager.callbacks(view);
127
126
128 if (callbacks.iopub === undefined) {
127 if (callbacks.iopub === undefined) {
129 callbacks.iopub = {};
128 callbacks.iopub = {};
130 }
129 }
131
130
132 var that = this;
131 var that = this;
133 callbacks.iopub.status = function (msg) {
132 callbacks.iopub.status = function (msg) {
134 that._handle_status(msg, callbacks);
133 that._handle_status(msg, callbacks);
135 };
134 };
136 return callbacks;
135 return callbacks;
137 },
136 },
138
137
139 set: function(key, val, options) {
138 set: function(key, val, options) {
140 // Set a value.
139 // Set a value.
141 var return_value = WidgetModel.__super__.set.apply(this, arguments);
140 var return_value = WidgetModel.__super__.set.apply(this, arguments);
142
141
143 // Backbone only remembers the diff of the most recent set()
142 // Backbone only remembers the diff of the most recent set()
144 // operation. Calling set multiple times in a row results in a
143 // operation. Calling set multiple times in a row results in a
145 // loss of diff information. Here we keep our own running diff.
144 // loss of diff information. Here we keep our own running diff.
146 this._buffered_state_diff = $.extend(this._buffered_state_diff, this.changedAttributes() || {});
145 this._buffered_state_diff = $.extend(this._buffered_state_diff, this.changedAttributes() || {});
147 return return_value;
146 return return_value;
148 },
147 },
149
148
150 sync: function (method, model, options) {
149 sync: function (method, model, options) {
151 // Handle sync to the back-end. Called when a model.save() is called.
150 // Handle sync to the back-end. Called when a model.save() is called.
152
151
153 // Make sure a comm exists.
152 // Make sure a comm exists.
154 var error = options.error || function() {
153 var error = options.error || function() {
155 console.error('Backbone sync error:', arguments);
154 console.error('Backbone sync error:', arguments);
156 };
155 };
157 if (this.comm === undefined) {
156 if (this.comm === undefined) {
158 error();
157 error();
159 return false;
158 return false;
160 }
159 }
161
160
162 // Delete any key value pairs that the back-end already knows about.
161 // Delete any key value pairs that the back-end already knows about.
163 var attrs = (method === 'patch') ? options.attrs : model.toJSON(options);
162 var attrs = (method === 'patch') ? options.attrs : model.toJSON(options);
164 if (this.key_value_lock !== null) {
163 if (this.key_value_lock !== null) {
165 var key = this.key_value_lock[0];
164 var key = this.key_value_lock[0];
166 var value = this.key_value_lock[1];
165 var value = this.key_value_lock[1];
167 if (attrs[key] === value) {
166 if (attrs[key] === value) {
168 delete attrs[key];
167 delete attrs[key];
169 }
168 }
170 }
169 }
171
170
172 // Only sync if there are attributes to send to the back-end.
171 // Only sync if there are attributes to send to the back-end.
173 attrs = this._pack_models(attrs);
172 attrs = this._pack_models(attrs);
174 if (_.size(attrs) > 0) {
173 if (_.size(attrs) > 0) {
175
174
176 // If this message was sent via backbone itself, it will not
175 // If this message was sent via backbone itself, it will not
177 // have any callbacks. It's important that we create callbacks
176 // have any callbacks. It's important that we create callbacks
178 // so we can listen for status messages, etc...
177 // so we can listen for status messages, etc...
179 var callbacks = options.callbacks || this.callbacks();
178 var callbacks = options.callbacks || this.callbacks();
180
179
181 // Check throttle.
180 // Check throttle.
182 if (this.pending_msgs >= (this.get('msg_throttle') || 3)) {
181 if (this.pending_msgs >= (this.get('msg_throttle') || 3)) {
183 // The throttle has been exceeded, buffer the current msg so
182 // The throttle has been exceeded, buffer the current msg so
184 // it can be sent once the kernel has finished processing
183 // it can be sent once the kernel has finished processing
185 // some of the existing messages.
184 // some of the existing messages.
186
185
187 // Combine updates if it is a 'patch' sync, otherwise replace updates
186 // Combine updates if it is a 'patch' sync, otherwise replace updates
188 switch (method) {
187 switch (method) {
189 case 'patch':
188 case 'patch':
190 this.msg_buffer = $.extend(this.msg_buffer || {}, attrs);
189 this.msg_buffer = $.extend(this.msg_buffer || {}, attrs);
191 break;
190 break;
192 case 'update':
191 case 'update':
193 case 'create':
192 case 'create':
194 this.msg_buffer = attrs;
193 this.msg_buffer = attrs;
195 break;
194 break;
196 default:
195 default:
197 error();
196 error();
198 return false;
197 return false;
199 }
198 }
200 this.msg_buffer_callbacks = callbacks;
199 this.msg_buffer_callbacks = callbacks;
201
200
202 } else {
201 } else {
203 // We haven't exceeded the throttle, send the message like
202 // We haven't exceeded the throttle, send the message like
204 // normal.
203 // normal.
205 var data = {method: 'backbone', sync_data: attrs};
204 var data = {method: 'backbone', sync_data: attrs};
206 this.comm.send(data, callbacks);
205 this.comm.send(data, callbacks);
207 this.pending_msgs++;
206 this.pending_msgs++;
208 }
207 }
209 }
208 }
210 // Since the comm is a one-way communication, assume the message
209 // Since the comm is a one-way communication, assume the message
211 // arrived. Don't call success since we don't have a model back from the server
210 // arrived. Don't call success since we don't have a model back from the server
212 // this means we miss out on the 'sync' event.
211 // this means we miss out on the 'sync' event.
213 this._buffered_state_diff = {};
212 this._buffered_state_diff = {};
214 },
213 },
215
214
216 save_changes: function(callbacks) {
215 save_changes: function(callbacks) {
217 // Push this model's state to the back-end
216 // Push this model's state to the back-end
218 //
217 //
219 // This invokes a Backbone.Sync.
218 // This invokes a Backbone.Sync.
220 this.save(this._buffered_state_diff, {patch: true, callbacks: callbacks});
219 this.save(this._buffered_state_diff, {patch: true, callbacks: callbacks});
221 },
220 },
222
221
223 _pack_models: function(value) {
222 _pack_models: function(value) {
224 // Replace models with model ids recursively.
223 // Replace models with model ids recursively.
225 if (value instanceof Backbone.Model) {
224 if (value instanceof Backbone.Model) {
226 return value.id;
225 return value.id;
227
226
228 } else if ($.isArray(value)) {
227 } else if ($.isArray(value)) {
229 var packed = [];
228 var packed = [];
230 var that = this;
229 var that = this;
231 _.each(value, function(sub_value, key) {
230 _.each(value, function(sub_value, key) {
232 packed.push(that._pack_models(sub_value));
231 packed.push(that._pack_models(sub_value));
233 });
232 });
234 return packed;
233 return packed;
235
234
236 } else if (value instanceof Object) {
235 } else if (value instanceof Object) {
237 var packed = {};
236 var packed = {};
238 var that = this;
237 var that = this;
239 _.each(value, function(sub_value, key) {
238 _.each(value, function(sub_value, key) {
240 packed[key] = that._pack_models(sub_value);
239 packed[key] = that._pack_models(sub_value);
241 });
240 });
242 return packed;
241 return packed;
243
242
244 } else {
243 } else {
245 return value;
244 return value;
246 }
245 }
247 },
246 },
248
247
249 _unpack_models: function(value) {
248 _unpack_models: function(value) {
250 // Replace model ids with models recursively.
249 // Replace model ids with models recursively.
251 if ($.isArray(value)) {
250 if ($.isArray(value)) {
252 var unpacked = [];
251 var unpacked = [];
253 var that = this;
252 var that = this;
254 _.each(value, function(sub_value, key) {
253 _.each(value, function(sub_value, key) {
255 unpacked.push(that._unpack_models(sub_value));
254 unpacked.push(that._unpack_models(sub_value));
256 });
255 });
257 return unpacked;
256 return unpacked;
258
257
259 } else if (value instanceof Object) {
258 } else if (value instanceof Object) {
260 var unpacked = {};
259 var unpacked = {};
261 var that = this;
260 var that = this;
262 _.each(value, function(sub_value, key) {
261 _.each(value, function(sub_value, key) {
263 unpacked[key] = that._unpack_models(sub_value);
262 unpacked[key] = that._unpack_models(sub_value);
264 });
263 });
265 return unpacked;
264 return unpacked;
266
265
267 } else {
266 } else {
268 var model = this.widget_manager.get_model(value);
267 var model = this.widget_manager.get_model(value);
269 if (model) {
268 if (model) {
270 return model;
269 return model;
271 } else {
270 } else {
272 return value;
271 return value;
273 }
272 }
274 }
273 }
275 },
274 },
276
275
277 });
276 });
278 WidgetManager.register_widget_model('WidgetModel', WidgetModel);
277 WidgetManager.register_widget_model('WidgetModel', WidgetModel);
279
278
280
279
281 var WidgetView = Backbone.View.extend({
280 var WidgetView = Backbone.View.extend({
282 initialize: function(parameters) {
281 initialize: function(parameters) {
283 // Public constructor.
282 // Public constructor.
284 this.model.on('change',this.update,this);
283 this.model.on('change',this.update,this);
285 this.options = parameters.options;
284 this.options = parameters.options;
286 this.child_views = [];
285 this.child_model_views = {};
286 this.child_views = {};
287 this.model.views.push(this);
287 this.model.views.push(this);
288 this.id = this.id || IPython.utils.uuid();
288 },
289 },
289
290
290 update: function(){
291 update: function(){
291 // Triggered on model change.
292 // Triggered on model change.
292 //
293 //
293 // Update view to be consistent with this.model
294 // Update view to be consistent with this.model
294 },
295 },
295
296
296 create_child_view: function(child_model, options) {
297 create_child_view: function(child_model, options) {
297 // Create and return a child view.
298 // Create and return a child view.
298 //
299 //
299 // -given a model and (optionally) a view name if the view name is
300 // -given a model and (optionally) a view name if the view name is
300 // not given, it defaults to the model's default view attribute.
301 // not given, it defaults to the model's default view attribute.
301
302
302 // TODO: this is hacky, and makes the view depend on this cell attribute and widget manager behavior
303 // TODO: this is hacky, and makes the view depend on this cell attribute and widget manager behavior
303 // it would be great to have the widget manager add the cell metadata
304 // it would be great to have the widget manager add the cell metadata
304 // to the subview without having to add it here.
305 // to the subview without having to add it here.
305 var child_view = this.model.widget_manager.create_view(child_model, options || {}, this);
306 options = $.extend({ parent: this }, options || {});
306 this.child_views[child_model.id] = child_view;
307 var child_view = this.model.widget_manager.create_view(child_model, options, this);
308
309 // Associate the view id with the model id.
310 if (this.child_model_views[child_model.id] === undefined) {
311 this.child_model_views[child_model.id] = [];
312 }
313 this.child_model_views[child_model.id].push(child_view.id);
314
315 // Remember the view by id.
316 this.child_views[child_view.id] = child_view;
307 return child_view;
317 return child_view;
308 },
318 },
309
319
310 delete_child_view: function(child_model, options) {
320 pop_child_view: function(child_model) {
311 // Delete a child view that was previously created using create_child_view.
321 // Delete a child view that was previously created using create_child_view.
312 var view = this.child_views[child_model.id];
322 var view_ids = this.child_model_views[child_model.id];
313 if (view !== undefined) {
323 if (view_ids !== undefined) {
314 delete this.child_views[child_model.id];
324
315 view.remove();
325 // Only delete the first view in the list.
326 var view_id = view_ids[0];
327 var view = this.child_views[view_id];
328 delete this.child_views[view_id];
329 view_ids.splice(0,1);
316 child_model.views.pop(view);
330 child_model.views.pop(view);
331
332 // Remove the view list specific to this model if it is empty.
333 if (view_ids.length === 0) {
334 delete this.child_model_views[child_model.id];
335 }
336 return view;
317 }
337 }
338 return null;
318 },
339 },
319
340
320 do_diff: function(old_list, new_list, removed_callback, added_callback) {
341 do_diff: function(old_list, new_list, removed_callback, added_callback) {
321 // Difference a changed list and call remove and add callbacks for
342 // Difference a changed list and call remove and add callbacks for
322 // each removed and added item in the new list.
343 // each removed and added item in the new list.
323 //
344 //
324 // Parameters
345 // Parameters
325 // ----------
346 // ----------
326 // old_list : array
347 // old_list : array
327 // new_list : array
348 // new_list : array
328 // removed_callback : Callback(item)
349 // removed_callback : Callback(item)
329 // Callback that is called for each item removed.
350 // Callback that is called for each item removed.
330 // added_callback : Callback(item)
351 // added_callback : Callback(item)
331 // Callback that is called for each item added.
352 // Callback that is called for each item added.
332
353
354 // Walk the lists until an unequal entry is found.
355 var i;
356 for (i = 0; i < new_list.length; i++) {
357 if (i < old_list.length || new_list[i] !== old_list[i]) {
358 break;
359 }
360 }
333
361
334 // removed items
362 // Remove the non-matching items from the old list.
335 _.each(_.difference(old_list, new_list), function(item, index, list) {
363 for (var j = i; j < old_list.length; j++) {
336 removed_callback(item);
364 removed_callback(old_list[j]);
337 }, this);
365 }
338
366
339 // added items
367 // Add the rest of the new list items.
340 _.each(_.difference(new_list, old_list), function(item, index, list) {
368 for (i; i < new_list.length; i++) {
341 added_callback(item);
369 added_callback(new_list[i]);
342 }, this);
370 }
343 },
371 },
344
372
345 callbacks: function(){
373 callbacks: function(){
346 // Create msg callbacks for a comm msg.
374 // Create msg callbacks for a comm msg.
347 return this.model.callbacks(this);
375 return this.model.callbacks(this);
348 },
376 },
349
377
350 render: function(){
378 render: function(){
351 // Render the view.
379 // Render the view.
352 //
380 //
353 // By default, this is only called the first time the view is created
381 // By default, this is only called the first time the view is created
354 },
382 },
355
383
356 send: function (content) {
384 send: function (content) {
357 // Send a custom msg associated with this view.
385 // Send a custom msg associated with this view.
358 this.model.send(content, this.callbacks());
386 this.model.send(content, this.callbacks());
359 },
387 },
360
388
361 touch: function () {
389 touch: function () {
362 this.model.save_changes(this.callbacks());
390 this.model.save_changes(this.callbacks());
363 },
391 },
364 });
392 });
365
393
366
394
367 var DOMWidgetView = WidgetView.extend({
395 var DOMWidgetView = WidgetView.extend({
368 initialize: function (options) {
396 initialize: function (options) {
369 // Public constructor
397 // Public constructor
370
398
371 // In the future we may want to make changes more granular
399 // In the future we may want to make changes more granular
372 // (e.g., trigger on visible:change).
400 // (e.g., trigger on visible:change).
373 this.model.on('change', this.update, this);
401 this.model.on('change', this.update, this);
374 this.model.on('msg:custom', this.on_msg, this);
402 this.model.on('msg:custom', this.on_msg, this);
375 DOMWidgetView.__super__.initialize.apply(this, arguments);
403 DOMWidgetView.__super__.initialize.apply(this, arguments);
376 },
404 },
377
405
378 on_msg: function(msg) {
406 on_msg: function(msg) {
379 // Handle DOM specific msgs.
407 // Handle DOM specific msgs.
380 switch(msg.msg_type) {
408 switch(msg.msg_type) {
381 case 'add_class':
409 case 'add_class':
382 this.add_class(msg.selector, msg.class_list);
410 this.add_class(msg.selector, msg.class_list);
383 break;
411 break;
384 case 'remove_class':
412 case 'remove_class':
385 this.remove_class(msg.selector, msg.class_list);
413 this.remove_class(msg.selector, msg.class_list);
386 break;
414 break;
387 }
415 }
388 },
416 },
389
417
390 add_class: function (selector, class_list) {
418 add_class: function (selector, class_list) {
391 // Add a DOM class to an element.
419 // Add a DOM class to an element.
392 this._get_selector_element(selector).addClass(class_list);
420 this._get_selector_element(selector).addClass(class_list);
393 },
421 },
394
422
395 remove_class: function (selector, class_list) {
423 remove_class: function (selector, class_list) {
396 // Remove a DOM class from an element.
424 // Remove a DOM class from an element.
397 this._get_selector_element(selector).removeClass(class_list);
425 this._get_selector_element(selector).removeClass(class_list);
398 },
426 },
399
427
400 update: function () {
428 update: function () {
401 // Update the contents of this view
429 // Update the contents of this view
402 //
430 //
403 // Called when the model is changed. The model may have been
431 // Called when the model is changed. The model may have been
404 // changed by another view or by a state update from the back-end.
432 // changed by another view or by a state update from the back-end.
405 // The very first update seems to happen before the element is
433 // The very first update seems to happen before the element is
406 // finished rendering so we use setTimeout to give the element time
434 // finished rendering so we use setTimeout to give the element time
407 // to render
435 // to render
408 var e = this.$el;
436 var e = this.$el;
409 var visible = this.model.get('visible');
437 var visible = this.model.get('visible');
410 setTimeout(function() {e.toggle(visible);},0);
438 setTimeout(function() {e.toggle(visible);},0);
411
439
412 var css = this.model.get('_css');
440 var css = this.model.get('_css');
413 if (css === undefined) {return;}
441 if (css === undefined) {return;}
414 for (var i = 0; i < css.length; i++) {
442 for (var i = 0; i < css.length; i++) {
415 // Apply the css traits to all elements that match the selector.
443 // Apply the css traits to all elements that match the selector.
416 var selector = css[i][0];
444 var selector = css[i][0];
417 var elements = this._get_selector_element(selector);
445 var elements = this._get_selector_element(selector);
418 if (elements.length > 0) {
446 if (elements.length > 0) {
419 var trait_key = css[i][1];
447 var trait_key = css[i][1];
420 var trait_value = css[i][2];
448 var trait_value = css[i][2];
421 elements.css(trait_key ,trait_value);
449 elements.css(trait_key ,trait_value);
422 }
450 }
423 }
451 }
424 },
452 },
425
453
426 _get_selector_element: function (selector) {
454 _get_selector_element: function (selector) {
427 // Get the elements via the css selector.
455 // Get the elements via the css selector.
428
456
429 // If the selector is blank, apply the style to the $el_to_style
457 // If the selector is blank, apply the style to the $el_to_style
430 // element. If the $el_to_style element is not defined, use apply
458 // element. If the $el_to_style element is not defined, use apply
431 // the style to the view's element.
459 // the style to the view's element.
432 var elements;
460 var elements;
433 if (!selector) {
461 if (!selector) {
434 if (this.$el_to_style === undefined) {
462 if (this.$el_to_style === undefined) {
435 elements = this.$el;
463 elements = this.$el;
436 } else {
464 } else {
437 elements = this.$el_to_style;
465 elements = this.$el_to_style;
438 }
466 }
439 } else {
467 } else {
440 elements = this.$el.find(selector);
468 elements = this.$el.find(selector);
441 }
469 }
442 return elements;
470 return elements;
443 },
471 },
444 });
472 });
445
473
446 IPython.WidgetModel = WidgetModel;
474 IPython.WidgetModel = WidgetModel;
447 IPython.WidgetView = WidgetView;
475 IPython.WidgetView = WidgetView;
448 IPython.DOMWidgetView = DOMWidgetView;
476 IPython.DOMWidgetView = DOMWidgetView;
449
477
450 // Pass through WidgetManager namespace.
478 // Pass through WidgetManager namespace.
451 return WidgetManager;
479 return WidgetManager;
452 });
480 });
@@ -1,325 +1,321 b''
1 //----------------------------------------------------------------------------
1 //----------------------------------------------------------------------------
2 // Copyright (C) 2013 The IPython Development Team
2 // Copyright (C) 2013 The IPython Development Team
3 //
3 //
4 // Distributed under the terms of the BSD License. The full license is in
4 // Distributed under the terms of the BSD License. The full license is in
5 // the file COPYING, distributed as part of this software.
5 // the file COPYING, distributed as part of this software.
6 //----------------------------------------------------------------------------
6 //----------------------------------------------------------------------------
7
7
8 //============================================================================
8 //============================================================================
9 // ContainerWidget
9 // ContainerWidget
10 //============================================================================
10 //============================================================================
11
11
12 /**
12 /**
13 * @module IPython
13 * @module IPython
14 * @namespace IPython
14 * @namespace IPython
15 **/
15 **/
16
16
17 define(["widgets/js/widget"], function(WidgetManager) {
17 define(["widgets/js/widget"], function(WidgetManager) {
18
18
19 var ContainerView = IPython.DOMWidgetView.extend({
19 var ContainerView = IPython.DOMWidgetView.extend({
20 render: function(){
20 render: function(){
21 // Called when view is rendered.
21 // Called when view is rendered.
22 this.$el.addClass('widget-container')
22 this.$el.addClass('widget-container')
23 .addClass('vbox');
23 .addClass('vbox');
24 this.children={};
24 this.update_children([], this.model.get('children'));
25 this.update_children([], this.model.get('_children'));
25 this.model.on('change:children', function(model, value, options) {
26 this.model.on('change:_children', function(model, value, options) {
26 this.update_children(model.previous('children'), value);
27 this.update_children(model.previous('_children'), value);
28 }, this);
27 }, this);
29 this.update();
28 this.update();
30
29
31 // Trigger model displayed events for any models that are child to
30 // Trigger model displayed events for any models that are child to
32 // this model when this model is displayed.
31 // this model when this model is displayed.
33 var that = this;
32 var that = this;
34 this.model.on('displayed', function(){
33 this.on('displayed', function(){
35 that.is_displayed = true;
34 that.is_displayed = true;
36 for (var property in that.child_views) {
35 for (var property in that.child_views) {
37 if (that.child_views.hasOwnProperty(property)) {
36 if (that.child_views.hasOwnProperty(property)) {
38 that.child_views[property].model.trigger('displayed');
37 that.child_views[property].trigger('displayed');
39 }
38 }
40 }
39 }
41 });
40 });
42 },
41 },
43
42
44 update_children: function(old_list, new_list) {
43 update_children: function(old_list, new_list) {
45 // Called when the children list changes.
44 // Called when the children list changes.
46 this.do_diff(old_list,
45 this.do_diff(old_list,
47 new_list,
46 new_list,
48 $.proxy(this.remove_child_model, this),
47 $.proxy(this.remove_child_model, this),
49 $.proxy(this.add_child_model, this));
48 $.proxy(this.add_child_model, this));
50 },
49 },
51
50
52 remove_child_model: function(model) {
51 remove_child_model: function(model) {
53 // Called when a model is removed from the children list.
52 // Called when a model is removed from the children list.
54 this.child_views[model.id].remove();
53 this.pop_child_view(model).remove();
55 this.delete_child_view(model);
56 },
54 },
57
55
58 add_child_model: function(model) {
56 add_child_model: function(model) {
59 // Called when a model is added to the children list.
57 // Called when a model is added to the children list.
60 var view = this.create_child_view(model);
58 var view = this.create_child_view(model);
61 this.$el.append(view.$el);
59 this.$el.append(view.$el);
62
60
63 // Trigger the displayed event if this model is displayed.
61 // Trigger the displayed event if this model is displayed.
64 if (this.is_displayed) {
62 if (this.is_displayed) {
65 model.trigger('displayed');
63 view.trigger('displayed');
66 }
64 }
67 },
65 },
68
66
69 update: function(){
67 update: function(){
70 // Update the contents of this view
68 // Update the contents of this view
71 //
69 //
72 // Called when the model is changed. The model may have been
70 // Called when the model is changed. The model may have been
73 // changed by another view or by a state update from the back-end.
71 // changed by another view or by a state update from the back-end.
74 return ContainerView.__super__.update.apply(this);
72 return ContainerView.__super__.update.apply(this);
75 },
73 },
76 });
74 });
77
75
78 WidgetManager.register_widget_view('ContainerView', ContainerView);
76 WidgetManager.register_widget_view('ContainerView', ContainerView);
79
77
80 var PopupView = IPython.DOMWidgetView.extend({
78 var PopupView = IPython.DOMWidgetView.extend({
81 render: function(){
79 render: function(){
82 // Called when view is rendered.
80 // Called when view is rendered.
83 var that = this;
81 var that = this;
84 this.children={};
85
82
86 this.$el.on("remove", function(){
83 this.$el.on("remove", function(){
87 that.$backdrop.remove();
84 that.$backdrop.remove();
88 });
85 });
89 this.$backdrop = $('<div />')
86 this.$backdrop = $('<div />')
90 .appendTo($('#notebook-container'))
87 .appendTo($('#notebook-container'))
91 .addClass('modal-dialog')
88 .addClass('modal-dialog')
92 .css('position', 'absolute')
89 .css('position', 'absolute')
93 .css('left', '0px')
90 .css('left', '0px')
94 .css('top', '0px');
91 .css('top', '0px');
95 this.$window = $('<div />')
92 this.$window = $('<div />')
96 .appendTo(this.$backdrop)
93 .appendTo(this.$backdrop)
97 .addClass('modal-content widget-modal')
94 .addClass('modal-content widget-modal')
98 .mousedown(function(){
95 .mousedown(function(){
99 that.bring_to_front();
96 that.bring_to_front();
100 });
97 });
101
98
102 // Set the elements array since the this.$window element is not child
99 // Set the elements array since the this.$window element is not child
103 // of this.$el and the parent widget manager or other widgets may
100 // of this.$el and the parent widget manager or other widgets may
104 // need to know about all of the top-level widgets. The IPython
101 // need to know about all of the top-level widgets. The IPython
105 // widget manager uses this to register the elements with the
102 // widget manager uses this to register the elements with the
106 // keyboard manager.
103 // keyboard manager.
107 this.additional_elements = [this.$window];
104 this.additional_elements = [this.$window];
108
105
109 this.$title_bar = $('<div />')
106 this.$title_bar = $('<div />')
110 .addClass('popover-title')
107 .addClass('popover-title')
111 .appendTo(this.$window)
108 .appendTo(this.$window)
112 .mousedown(function(){
109 .mousedown(function(){
113 that.bring_to_front();
110 that.bring_to_front();
114 });
111 });
115 this.$close = $('<button />')
112 this.$close = $('<button />')
116 .addClass('close icon-remove')
113 .addClass('close icon-remove')
117 .css('margin-left', '5px')
114 .css('margin-left', '5px')
118 .appendTo(this.$title_bar)
115 .appendTo(this.$title_bar)
119 .click(function(){
116 .click(function(){
120 that.hide();
117 that.hide();
121 event.stopPropagation();
118 event.stopPropagation();
122 });
119 });
123 this.$minimize = $('<button />')
120 this.$minimize = $('<button />')
124 .addClass('close icon-arrow-down')
121 .addClass('close icon-arrow-down')
125 .appendTo(this.$title_bar)
122 .appendTo(this.$title_bar)
126 .click(function(){
123 .click(function(){
127 that.popped_out = !that.popped_out;
124 that.popped_out = !that.popped_out;
128 if (!that.popped_out) {
125 if (!that.popped_out) {
129 that.$minimize
126 that.$minimize
130 .removeClass('icon-arrow-down')
127 .removeClass('icon-arrow-down')
131 .addClass('icon-arrow-up');
128 .addClass('icon-arrow-up');
132
129
133 that.$window
130 that.$window
134 .draggable('destroy')
131 .draggable('destroy')
135 .resizable('destroy')
132 .resizable('destroy')
136 .removeClass('widget-modal modal-content')
133 .removeClass('widget-modal modal-content')
137 .addClass('docked-widget-modal')
134 .addClass('docked-widget-modal')
138 .detach()
135 .detach()
139 .insertBefore(that.$show_button);
136 .insertBefore(that.$show_button);
140 that.$show_button.hide();
137 that.$show_button.hide();
141 that.$close.hide();
138 that.$close.hide();
142 } else {
139 } else {
143 that.$minimize
140 that.$minimize
144 .addClass('icon-arrow-down')
141 .addClass('icon-arrow-down')
145 .removeClass('icon-arrow-up');
142 .removeClass('icon-arrow-up');
146
143
147 that.$window
144 that.$window
148 .removeClass('docked-widget-modal')
145 .removeClass('docked-widget-modal')
149 .addClass('widget-modal modal-content')
146 .addClass('widget-modal modal-content')
150 .detach()
147 .detach()
151 .appendTo(that.$backdrop)
148 .appendTo(that.$backdrop)
152 .draggable({handle: '.popover-title', snap: '#notebook, .modal', snapMode: 'both'})
149 .draggable({handle: '.popover-title', snap: '#notebook, .modal', snapMode: 'both'})
153 .resizable()
150 .resizable()
154 .children('.ui-resizable-handle').show();
151 .children('.ui-resizable-handle').show();
155 that.show();
152 that.show();
156 that.$show_button.show();
153 that.$show_button.show();
157 that.$close.show();
154 that.$close.show();
158 }
155 }
159 event.stopPropagation();
156 event.stopPropagation();
160 });
157 });
161 this.$title = $('<div />')
158 this.$title = $('<div />')
162 .addClass('widget-modal-title')
159 .addClass('widget-modal-title')
163 .html("&nbsp;")
160 .html("&nbsp;")
164 .appendTo(this.$title_bar);
161 .appendTo(this.$title_bar);
165 this.$body = $('<div />')
162 this.$body = $('<div />')
166 .addClass('modal-body')
163 .addClass('modal-body')
167 .addClass('widget-modal-body')
164 .addClass('widget-modal-body')
168 .addClass('widget-container')
165 .addClass('widget-container')
169 .addClass('vbox')
166 .addClass('vbox')
170 .appendTo(this.$window);
167 .appendTo(this.$window);
171
168
172 this.$show_button = $('<button />')
169 this.$show_button = $('<button />')
173 .html("&nbsp;")
170 .html("&nbsp;")
174 .addClass('btn btn-info widget-modal-show')
171 .addClass('btn btn-info widget-modal-show')
175 .appendTo(this.$el)
172 .appendTo(this.$el)
176 .click(function(){
173 .click(function(){
177 that.show();
174 that.show();
178 });
175 });
179
176
180 this.$window.draggable({handle: '.popover-title', snap: '#notebook, .modal', snapMode: 'both'});
177 this.$window.draggable({handle: '.popover-title', snap: '#notebook, .modal', snapMode: 'both'});
181 this.$window.resizable();
178 this.$window.resizable();
182 this.$window.on('resize', function(){
179 this.$window.on('resize', function(){
183 that.$body.outerHeight(that.$window.innerHeight() - that.$title_bar.outerHeight());
180 that.$body.outerHeight(that.$window.innerHeight() - that.$title_bar.outerHeight());
184 });
181 });
185
182
186 this.$el_to_style = this.$body;
183 this.$el_to_style = this.$body;
187 this._shown_once = false;
184 this._shown_once = false;
188 this.popped_out = true;
185 this.popped_out = true;
189
186
190 this.update_children([], this.model.get('_children'));
187 this.update_children([], this.model.get('children'));
191 this.model.on('change:_children', function(model, value, options) {
188 this.model.on('change:children', function(model, value, options) {
192 this.update_children(model.previous('_children'), value);
189 this.update_children(model.previous('children'), value);
193 }, this);
190 }, this);
194 this.update();
191 this.update();
195
192
196 // Trigger model displayed events for any models that are child to
193 // Trigger model displayed events for any models that are child to
197 // this model when this model is displayed.
194 // this model when this model is displayed.
198 this.model.on('displayed', function(){
195 this.on('displayed', function(){
199 that.is_displayed = true;
196 that.is_displayed = true;
200 for (var property in that.child_views) {
197 for (var property in that.child_views) {
201 if (that.child_views.hasOwnProperty(property)) {
198 if (that.child_views.hasOwnProperty(property)) {
202 that.child_views[property].model.trigger('displayed');
199 that.child_views[property].trigger('displayed');
203 }
200 }
204 }
201 }
205 });
202 });
206 },
203 },
207
204
208 hide: function() {
205 hide: function() {
209 // Called when the modal hide button is clicked.
206 // Called when the modal hide button is clicked.
210 this.$window.hide();
207 this.$window.hide();
211 this.$show_button.removeClass('btn-info');
208 this.$show_button.removeClass('btn-info');
212 },
209 },
213
210
214 show: function() {
211 show: function() {
215 // Called when the modal show button is clicked.
212 // Called when the modal show button is clicked.
216 this.$show_button.addClass('btn-info');
213 this.$show_button.addClass('btn-info');
217 this.$window.show();
214 this.$window.show();
218 if (this.popped_out) {
215 if (this.popped_out) {
219 this.$window.css("positon", "absolute");
216 this.$window.css("positon", "absolute");
220 this.$window.css("top", "0px");
217 this.$window.css("top", "0px");
221 this.$window.css("left", Math.max(0, (($('body').outerWidth() - this.$window.outerWidth()) / 2) +
218 this.$window.css("left", Math.max(0, (($('body').outerWidth() - this.$window.outerWidth()) / 2) +
222 $(window).scrollLeft()) + "px");
219 $(window).scrollLeft()) + "px");
223 this.bring_to_front();
220 this.bring_to_front();
224 }
221 }
225 },
222 },
226
223
227 bring_to_front: function() {
224 bring_to_front: function() {
228 // Make the modal top-most, z-ordered about the other modals.
225 // Make the modal top-most, z-ordered about the other modals.
229 var $widget_modals = $(".widget-modal");
226 var $widget_modals = $(".widget-modal");
230 var max_zindex = 0;
227 var max_zindex = 0;
231 $widget_modals.each(function (index, el){
228 $widget_modals.each(function (index, el){
232 var zindex = parseInt($(el).css('z-index'));
229 var zindex = parseInt($(el).css('z-index'));
233 if (!isNaN(zindex)) {
230 if (!isNaN(zindex)) {
234 max_zindex = Math.max(max_zindex, zindex);
231 max_zindex = Math.max(max_zindex, zindex);
235 }
232 }
236 });
233 });
237
234
238 // Start z-index of widget modals at 2000
235 // Start z-index of widget modals at 2000
239 max_zindex = Math.max(max_zindex, 2000);
236 max_zindex = Math.max(max_zindex, 2000);
240
237
241 $widget_modals.each(function (index, el){
238 $widget_modals.each(function (index, el){
242 $el = $(el);
239 $el = $(el);
243 if (max_zindex == parseInt($el.css('z-index'))) {
240 if (max_zindex == parseInt($el.css('z-index'))) {
244 $el.css('z-index', max_zindex - 1);
241 $el.css('z-index', max_zindex - 1);
245 }
242 }
246 });
243 });
247 this.$window.css('z-index', max_zindex);
244 this.$window.css('z-index', max_zindex);
248 },
245 },
249
246
250 update_children: function(old_list, new_list) {
247 update_children: function(old_list, new_list) {
251 // Called when the children list is modified.
248 // Called when the children list is modified.
252 this.do_diff(old_list,
249 this.do_diff(old_list,
253 new_list,
250 new_list,
254 $.proxy(this.remove_child_model, this),
251 $.proxy(this.remove_child_model, this),
255 $.proxy(this.add_child_model, this));
252 $.proxy(this.add_child_model, this));
256 },
253 },
257
254
258 remove_child_model: function(model) {
255 remove_child_model: function(model) {
259 // Called when a child is removed from children list.
256 // Called when a child is removed from children list.
260 this.child_views[model.id].remove();
257 this.pop_child_view(model).remove();
261 this.delete_child_view(model);
262 },
258 },
263
259
264 add_child_model: function(model) {
260 add_child_model: function(model) {
265 // Called when a child is added to children list.
261 // Called when a child is added to children list.
266 var view = this.create_child_view(model);
262 var view = this.create_child_view(model);
267 this.$body.append(view.$el);
263 this.$body.append(view.$el);
268
264
269 // Trigger the displayed event if this model is displayed.
265 // Trigger the displayed event if this model is displayed.
270 if (this.is_displayed) {
266 if (this.is_displayed) {
271 model.trigger('displayed');
267 view.trigger('displayed');
272 }
268 }
273 },
269 },
274
270
275 update: function(){
271 update: function(){
276 // Update the contents of this view
272 // Update the contents of this view
277 //
273 //
278 // Called when the model is changed. The model may have been
274 // Called when the model is changed. The model may have been
279 // changed by another view or by a state update from the back-end.
275 // changed by another view or by a state update from the back-end.
280 var description = this.model.get('description');
276 var description = this.model.get('description');
281 if (description.trim().length === 0) {
277 if (description.trim().length === 0) {
282 this.$title.html("&nbsp;"); // Preserve title height
278 this.$title.html("&nbsp;"); // Preserve title height
283 } else {
279 } else {
284 this.$title.text(description);
280 this.$title.text(description);
285 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$title.get(0)]);
281 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$title.get(0)]);
286 }
282 }
287
283
288 var button_text = this.model.get('button_text');
284 var button_text = this.model.get('button_text');
289 if (button_text.trim().length === 0) {
285 if (button_text.trim().length === 0) {
290 this.$show_button.html("&nbsp;"); // Preserve button height
286 this.$show_button.html("&nbsp;"); // Preserve button height
291 } else {
287 } else {
292 this.$show_button.text(button_text);
288 this.$show_button.text(button_text);
293 }
289 }
294
290
295 if (!this._shown_once) {
291 if (!this._shown_once) {
296 this._shown_once = true;
292 this._shown_once = true;
297 this.show();
293 this.show();
298 }
294 }
299
295
300 return PopupView.__super__.update.apply(this);
296 return PopupView.__super__.update.apply(this);
301 },
297 },
302
298
303 _get_selector_element: function(selector) {
299 _get_selector_element: function(selector) {
304 // Get an element view a 'special' jquery selector. (see widget.js)
300 // Get an element view a 'special' jquery selector. (see widget.js)
305 //
301 //
306 // Since the modal actually isn't within the $el in the DOM, we need to extend
302 // Since the modal actually isn't within the $el in the DOM, we need to extend
307 // the selector logic to allow the user to set css on the modal if need be.
303 // the selector logic to allow the user to set css on the modal if need be.
308 // The convention used is:
304 // The convention used is:
309 // "modal" - select the modal div
305 // "modal" - select the modal div
310 // "modal [selector]" - select element(s) within the modal div.
306 // "modal [selector]" - select element(s) within the modal div.
311 // "[selector]" - select elements within $el
307 // "[selector]" - select elements within $el
312 // "" - select the $el_to_style
308 // "" - select the $el_to_style
313 if (selector.substring(0, 5) == 'modal') {
309 if (selector.substring(0, 5) == 'modal') {
314 if (selector == 'modal') {
310 if (selector == 'modal') {
315 return this.$window;
311 return this.$window;
316 } else {
312 } else {
317 return this.$window.find(selector.substring(6));
313 return this.$window.find(selector.substring(6));
318 }
314 }
319 } else {
315 } else {
320 return PopupView.__super__._get_selector_element.apply(this, [selector]);
316 return PopupView.__super__._get_selector_element.apply(this, [selector]);
321 }
317 }
322 },
318 },
323 });
319 });
324 WidgetManager.register_widget_view('PopupView', PopupView);
320 WidgetManager.register_widget_view('PopupView', PopupView);
325 });
321 });
@@ -1,311 +1,312 b''
1 //----------------------------------------------------------------------------
1 //----------------------------------------------------------------------------
2 // Copyright (C) 2013 The IPython Development Team
2 // Copyright (C) 2013 The IPython Development Team
3 //
3 //
4 // Distributed under the terms of the BSD License. The full license is in
4 // Distributed under the terms of the BSD License. The full license is in
5 // the file COPYING, distributed as part of this software.
5 // the file COPYING, distributed as part of this software.
6 //----------------------------------------------------------------------------
6 //----------------------------------------------------------------------------
7
7
8 //============================================================================
8 //============================================================================
9 // IntWidget
9 // IntWidget
10 //============================================================================
10 //============================================================================
11
11
12 /**
12 /**
13 * @module IPython
13 * @module IPython
14 * @namespace IPython
14 * @namespace IPython
15 **/
15 **/
16
16
17 define(["widgets/js/widget"], function(WidgetManager){
17 define(["widgets/js/widget"], function(WidgetManager){
18
18
19 var IntSliderView = IPython.DOMWidgetView.extend({
19 var IntSliderView = IPython.DOMWidgetView.extend({
20 render : function(){
20 render : function(){
21 // Called when view is rendered.
21 // Called when view is rendered.
22 this.$el
22 this.$el
23 .addClass('widget-hbox-single');
23 .addClass('widget-hbox-single');
24 this.$label = $('<div />')
24 this.$label = $('<div />')
25 .appendTo(this.$el)
25 .appendTo(this.$el)
26 .addClass('widget-hlabel')
26 .addClass('widget-hlabel')
27 .hide();
27 .hide();
28
28
29 this.$slider = $('<div />')
29 this.$slider = $('<div />')
30 .slider({})
30 .slider({})
31 .addClass('slider');
31 .addClass('slider');
32 // Put the slider in a container
32 // Put the slider in a container
33 this.$slider_container = $('<div />')
33 this.$slider_container = $('<div />')
34 .addClass('widget-hslider')
34 .addClass('widget-hslider')
35 .append(this.$slider);
35 .append(this.$slider);
36 this.$el_to_style = this.$slider_container; // Set default element to style
36 this.$el_to_style = this.$slider_container; // Set default element to style
37 this.$el.append(this.$slider_container);
37 this.$el.append(this.$slider_container);
38
38
39 this.$readout = $('<div/>')
39 this.$readout = $('<div/>')
40 .appendTo(this.$el)
40 .appendTo(this.$el)
41 .addClass('widget-hreadout')
41 .addClass('widget-hreadout')
42 .hide();
42 .hide();
43
43
44 // Set defaults.
44 // Set defaults.
45 this.update();
45 this.update();
46 },
46 },
47
47
48 update : function(options){
48 update : function(options){
49 // Update the contents of this view
49 // Update the contents of this view
50 //
50 //
51 // Called when the model is changed. The model may have been
51 // Called when the model is changed. The model may have been
52 // changed by another view or by a state update from the back-end.
52 // changed by another view or by a state update from the back-end.
53 if (options === undefined || options.updated_view != this) {
53 if (options === undefined || options.updated_view != this) {
54 // JQuery slider option keys. These keys happen to have a
54 // JQuery slider option keys. These keys happen to have a
55 // one-to-one mapping with the corrosponding keys of the model.
55 // one-to-one mapping with the corrosponding keys of the model.
56 var jquery_slider_keys = ['step', 'max', 'min', 'disabled'];
56 var jquery_slider_keys = ['step', 'max', 'min', 'disabled'];
57 var that = this;
57 var that = this;
58 that.$slider.slider({});
58 _.each(jquery_slider_keys, function(key, i) {
59 _.each(jquery_slider_keys, function(key, i) {
59 var model_value = that.model.get(key);
60 var model_value = that.model.get(key);
60 if (model_value !== undefined) {
61 if (model_value !== undefined) {
61 that.$slider.slider("option", key, model_value);
62 that.$slider.slider("option", key, model_value);
62 }
63 }
63 });
64 });
64
65
65 // WORKAROUND FOR JQUERY SLIDER BUG.
66 // WORKAROUND FOR JQUERY SLIDER BUG.
66 // The horizontal position of the slider handle
67 // The horizontal position of the slider handle
67 // depends on the value of the slider at the time
68 // depends on the value of the slider at the time
68 // of orientation change. Before applying the new
69 // of orientation change. Before applying the new
69 // workaround, we set the value to the minimum to
70 // workaround, we set the value to the minimum to
70 // make sure that the horizontal placement of the
71 // make sure that the horizontal placement of the
71 // handle in the vertical slider is always
72 // handle in the vertical slider is always
72 // consistent.
73 // consistent.
73 var orientation = this.model.get('orientation');
74 var orientation = this.model.get('orientation');
74 var value = this.model.get('min');
75 var value = this.model.get('min');
75 this.$slider.slider('option', 'value', value);
76 this.$slider.slider('option', 'value', value);
76 this.$slider.slider('option', 'orientation', orientation);
77 this.$slider.slider('option', 'orientation', orientation);
77 value = this.model.get('value');
78 value = this.model.get('value');
78 this.$slider.slider('option', 'value', value);
79 this.$slider.slider('option', 'value', value);
79 this.$readout.text(value);
80 this.$readout.text(value);
80
81
81 // Use the right CSS classes for vertical & horizontal sliders
82 // Use the right CSS classes for vertical & horizontal sliders
82 if (orientation=='vertical') {
83 if (orientation=='vertical') {
83 this.$slider_container
84 this.$slider_container
84 .removeClass('widget-hslider')
85 .removeClass('widget-hslider')
85 .addClass('widget-vslider');
86 .addClass('widget-vslider');
86 this.$el
87 this.$el
87 .removeClass('widget-hbox-single')
88 .removeClass('widget-hbox-single')
88 .addClass('widget-vbox-single');
89 .addClass('widget-vbox-single');
89 this.$label
90 this.$label
90 .removeClass('widget-hlabel')
91 .removeClass('widget-hlabel')
91 .addClass('widget-vlabel');
92 .addClass('widget-vlabel');
92 this.$readout
93 this.$readout
93 .removeClass('widget-hreadout')
94 .removeClass('widget-hreadout')
94 .addClass('widget-vreadout');
95 .addClass('widget-vreadout');
95
96
96 } else {
97 } else {
97 this.$slider_container
98 this.$slider_container
98 .removeClass('widget-vslider')
99 .removeClass('widget-vslider')
99 .addClass('widget-hslider');
100 .addClass('widget-hslider');
100 this.$el
101 this.$el
101 .removeClass('widget-vbox-single')
102 .removeClass('widget-vbox-single')
102 .addClass('widget-hbox-single');
103 .addClass('widget-hbox-single');
103 this.$label
104 this.$label
104 .removeClass('widget-vlabel')
105 .removeClass('widget-vlabel')
105 .addClass('widget-hlabel');
106 .addClass('widget-hlabel');
106 this.$readout
107 this.$readout
107 .removeClass('widget-vreadout')
108 .removeClass('widget-vreadout')
108 .addClass('widget-hreadout');
109 .addClass('widget-hreadout');
109 }
110 }
110
111
111 var description = this.model.get('description');
112 var description = this.model.get('description');
112 if (description.length === 0) {
113 if (description.length === 0) {
113 this.$label.hide();
114 this.$label.hide();
114 } else {
115 } else {
115 this.$label.text(description);
116 this.$label.text(description);
116 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$label.get(0)]);
117 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$label.get(0)]);
117 this.$label.show();
118 this.$label.show();
118 }
119 }
119
120
120 var readout = this.model.get('readout');
121 var readout = this.model.get('readout');
121 if (readout) {
122 if (readout) {
122 this.$readout.show();
123 this.$readout.show();
123 } else {
124 } else {
124 this.$readout.hide();
125 this.$readout.hide();
125 }
126 }
126 }
127 }
127 return IntSliderView.__super__.update.apply(this);
128 return IntSliderView.__super__.update.apply(this);
128 },
129 },
129
130
130 events: {
131 events: {
131 // Dictionary of events and their handlers.
132 // Dictionary of events and their handlers.
132 "slide" : "handleSliderChange"
133 "slide" : "handleSliderChange"
133 },
134 },
134
135
135 handleSliderChange: function(e, ui) {
136 handleSliderChange: function(e, ui) {
136 // Called when the slider value is changed.
137 // Called when the slider value is changed.
137
138
138 // Calling model.set will trigger all of the other views of the
139 // Calling model.set will trigger all of the other views of the
139 // model to update.
140 // model to update.
140 var actual_value = this._validate_slide_value(ui.value);
141 var actual_value = this._validate_slide_value(ui.value);
141 this.model.set('value', actual_value, {updated_view: this});
142 this.model.set('value', actual_value, {updated_view: this});
142 this.$readout.text(actual_value);
143 this.$readout.text(actual_value);
143 this.touch();
144 this.touch();
144 },
145 },
145
146
146 _validate_slide_value: function(x) {
147 _validate_slide_value: function(x) {
147 // Validate the value of the slider before sending it to the back-end
148 // Validate the value of the slider before sending it to the back-end
148 // and applying it to the other views on the page.
149 // and applying it to the other views on the page.
149
150
150 // Double bit-wise not truncates the decimel (int cast).
151 // Double bit-wise not truncates the decimel (int cast).
151 return ~~x;
152 return ~~x;
152 },
153 },
153 });
154 });
154 WidgetManager.register_widget_view('IntSliderView', IntSliderView);
155 WidgetManager.register_widget_view('IntSliderView', IntSliderView);
155
156
156
157
157 var IntTextView = IPython.DOMWidgetView.extend({
158 var IntTextView = IPython.DOMWidgetView.extend({
158 render : function(){
159 render : function(){
159 // Called when view is rendered.
160 // Called when view is rendered.
160 this.$el
161 this.$el
161 .addClass('widget-hbox-single');
162 .addClass('widget-hbox-single');
162 this.$label = $('<div />')
163 this.$label = $('<div />')
163 .appendTo(this.$el)
164 .appendTo(this.$el)
164 .addClass('widget-hlabel')
165 .addClass('widget-hlabel')
165 .hide();
166 .hide();
166 this.$textbox = $('<input type="text" />')
167 this.$textbox = $('<input type="text" />')
167 .addClass('form-control')
168 .addClass('form-control')
168 .addClass('widget-numeric-text')
169 .addClass('widget-numeric-text')
169 .appendTo(this.$el);
170 .appendTo(this.$el);
170 this.$el_to_style = this.$textbox; // Set default element to style
171 this.$el_to_style = this.$textbox; // Set default element to style
171 this.update(); // Set defaults.
172 this.update(); // Set defaults.
172 },
173 },
173
174
174 update : function(options){
175 update : function(options){
175 // Update the contents of this view
176 // Update the contents of this view
176 //
177 //
177 // Called when the model is changed. The model may have been
178 // Called when the model is changed. The model may have been
178 // changed by another view or by a state update from the back-end.
179 // changed by another view or by a state update from the back-end.
179 if (options === undefined || options.updated_view != this) {
180 if (options === undefined || options.updated_view != this) {
180 var value = this.model.get('value');
181 var value = this.model.get('value');
181 if (this._parse_value(this.$textbox.val()) != value) {
182 if (this._parse_value(this.$textbox.val()) != value) {
182 this.$textbox.val(value);
183 this.$textbox.val(value);
183 }
184 }
184
185
185 if (this.model.get('disabled')) {
186 if (this.model.get('disabled')) {
186 this.$textbox.attr('disabled','disabled');
187 this.$textbox.attr('disabled','disabled');
187 } else {
188 } else {
188 this.$textbox.removeAttr('disabled');
189 this.$textbox.removeAttr('disabled');
189 }
190 }
190
191
191 var description = this.model.get('description');
192 var description = this.model.get('description');
192 if (description.length === 0) {
193 if (description.length === 0) {
193 this.$label.hide();
194 this.$label.hide();
194 } else {
195 } else {
195 this.$label.text(description);
196 this.$label.text(description);
196 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$label.get(0)]);
197 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$label.get(0)]);
197 this.$label.show();
198 this.$label.show();
198 }
199 }
199 }
200 }
200 return IntTextView.__super__.update.apply(this);
201 return IntTextView.__super__.update.apply(this);
201 },
202 },
202
203
203 events: {
204 events: {
204 // Dictionary of events and their handlers.
205 // Dictionary of events and their handlers.
205 "keyup input" : "handleChanging",
206 "keyup input" : "handleChanging",
206 "paste input" : "handleChanging",
207 "paste input" : "handleChanging",
207 "cut input" : "handleChanging",
208 "cut input" : "handleChanging",
208
209
209 // Fires only when control is validated or looses focus.
210 // Fires only when control is validated or looses focus.
210 "change input" : "handleChanged"
211 "change input" : "handleChanged"
211 },
212 },
212
213
213 handleChanging: function(e) {
214 handleChanging: function(e) {
214 // Handles and validates user input.
215 // Handles and validates user input.
215
216
216 // Try to parse value as a int.
217 // Try to parse value as a int.
217 var numericalValue = 0;
218 var numericalValue = 0;
218 if (e.target.value !== '') {
219 if (e.target.value !== '') {
219 var trimmed = e.target.value.trim();
220 var trimmed = e.target.value.trim();
220 if (!(['-', '-.', '.', '+.', '+'].indexOf(trimmed) >= 0)) {
221 if (!(['-', '-.', '.', '+.', '+'].indexOf(trimmed) >= 0)) {
221 numericalValue = this._parse_value(e.target.value);
222 numericalValue = this._parse_value(e.target.value);
222 }
223 }
223 }
224 }
224
225
225 // If parse failed, reset value to value stored in model.
226 // If parse failed, reset value to value stored in model.
226 if (isNaN(numericalValue)) {
227 if (isNaN(numericalValue)) {
227 e.target.value = this.model.get('value');
228 e.target.value = this.model.get('value');
228 } else if (!isNaN(numericalValue)) {
229 } else if (!isNaN(numericalValue)) {
229 if (this.model.get('max') !== undefined) {
230 if (this.model.get('max') !== undefined) {
230 numericalValue = Math.min(this.model.get('max'), numericalValue);
231 numericalValue = Math.min(this.model.get('max'), numericalValue);
231 }
232 }
232 if (this.model.get('min') !== undefined) {
233 if (this.model.get('min') !== undefined) {
233 numericalValue = Math.max(this.model.get('min'), numericalValue);
234 numericalValue = Math.max(this.model.get('min'), numericalValue);
234 }
235 }
235
236
236 // Apply the value if it has changed.
237 // Apply the value if it has changed.
237 if (numericalValue != this.model.get('value')) {
238 if (numericalValue != this.model.get('value')) {
238
239
239 // Calling model.set will trigger all of the other views of the
240 // Calling model.set will trigger all of the other views of the
240 // model to update.
241 // model to update.
241 this.model.set('value', numericalValue, {updated_view: this});
242 this.model.set('value', numericalValue, {updated_view: this});
242 this.touch();
243 this.touch();
243 }
244 }
244 }
245 }
245 },
246 },
246
247
247 handleChanged: function(e) {
248 handleChanged: function(e) {
248 // Applies validated input.
249 // Applies validated input.
249 if (this.model.get('value') != e.target.value) {
250 if (this.model.get('value') != e.target.value) {
250 e.target.value = this.model.get('value');
251 e.target.value = this.model.get('value');
251 }
252 }
252 },
253 },
253
254
254 _parse_value: function(value) {
255 _parse_value: function(value) {
255 // Parse the value stored in a string.
256 // Parse the value stored in a string.
256 return parseInt(value);
257 return parseInt(value);
257 },
258 },
258 });
259 });
259 WidgetManager.register_widget_view('IntTextView', IntTextView);
260 WidgetManager.register_widget_view('IntTextView', IntTextView);
260
261
261
262
262 var ProgressView = IPython.DOMWidgetView.extend({
263 var ProgressView = IPython.DOMWidgetView.extend({
263 render : function(){
264 render : function(){
264 // Called when view is rendered.
265 // Called when view is rendered.
265 this.$el
266 this.$el
266 .addClass('widget-hbox-single');
267 .addClass('widget-hbox-single');
267 this.$label = $('<div />')
268 this.$label = $('<div />')
268 .appendTo(this.$el)
269 .appendTo(this.$el)
269 .addClass('widget-hlabel')
270 .addClass('widget-hlabel')
270 .hide();
271 .hide();
271 this.$progress = $('<div />')
272 this.$progress = $('<div />')
272 .addClass('progress')
273 .addClass('progress')
273 .addClass('widget-progress')
274 .addClass('widget-progress')
274 .appendTo(this.$el);
275 .appendTo(this.$el);
275 this.$el_to_style = this.$progress; // Set default element to style
276 this.$el_to_style = this.$progress; // Set default element to style
276 this.$bar = $('<div />')
277 this.$bar = $('<div />')
277 .addClass('progress-bar')
278 .addClass('progress-bar')
278 .css('width', '50%')
279 .css('width', '50%')
279 .appendTo(this.$progress);
280 .appendTo(this.$progress);
280 this.update(); // Set defaults.
281 this.update(); // Set defaults.
281 },
282 },
282
283
283 update : function(){
284 update : function(){
284 // Update the contents of this view
285 // Update the contents of this view
285 //
286 //
286 // Called when the model is changed. The model may have been
287 // Called when the model is changed. The model may have been
287 // changed by another view or by a state update from the back-end.
288 // changed by another view or by a state update from the back-end.
288 var value = this.model.get('value');
289 var value = this.model.get('value');
289 var max = this.model.get('max');
290 var max = this.model.get('max');
290 var min = this.model.get('min');
291 var min = this.model.get('min');
291 var percent = 100.0 * (value - min) / (max - min);
292 var percent = 100.0 * (value - min) / (max - min);
292 this.$bar.css('width', percent + '%');
293 this.$bar.css('width', percent + '%');
293
294
294 var description = this.model.get('description');
295 var description = this.model.get('description');
295 if (description.length === 0) {
296 if (description.length === 0) {
296 this.$label.hide();
297 this.$label.hide();
297 } else {
298 } else {
298 this.$label.text(description);
299 this.$label.text(description);
299 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$label.get(0)]);
300 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$label.get(0)]);
300 this.$label.show();
301 this.$label.show();
301 }
302 }
302 return ProgressView.__super__.update.apply(this);
303 return ProgressView.__super__.update.apply(this);
303 },
304 },
304 });
305 });
305 WidgetManager.register_widget_view('ProgressView', ProgressView);
306 WidgetManager.register_widget_view('ProgressView', ProgressView);
306
307
307
308
308 // Return the slider and text views so they can be inheritted to create the
309 // Return the slider and text views so they can be inheritted to create the
309 // float versions.
310 // float versions.
310 return [IntSliderView, IntTextView];
311 return [IntSliderView, IntTextView];
311 });
312 });
@@ -1,273 +1,272 b''
1 //----------------------------------------------------------------------------
1 //----------------------------------------------------------------------------
2 // Copyright (C) 2013 The IPython Development Team
2 // Copyright (C) 2013 The IPython Development Team
3 //
3 //
4 // Distributed under the terms of the BSD License. The full license is in
4 // Distributed under the terms of the BSD License. The full license is in
5 // the file COPYING, distributed as part of this software.
5 // the file COPYING, distributed as part of this software.
6 //----------------------------------------------------------------------------
6 //----------------------------------------------------------------------------
7
7
8 //============================================================================
8 //============================================================================
9 // SelectionContainerWidget
9 // SelectionContainerWidget
10 //============================================================================
10 //============================================================================
11
11
12 /**
12 /**
13 * @module IPython
13 * @module IPython
14 * @namespace IPython
14 * @namespace IPython
15 **/
15 **/
16
16
17 define(["widgets/js/widget"], function(WidgetManager){
17 define(["widgets/js/widget"], function(WidgetManager){
18
18
19 var AccordionView = IPython.DOMWidgetView.extend({
19 var AccordionView = IPython.DOMWidgetView.extend({
20 render: function(){
20 render: function(){
21 // Called when view is rendered.
21 // Called when view is rendered.
22 var guid = 'panel-group' + IPython.utils.uuid();
22 var guid = 'panel-group' + IPython.utils.uuid();
23 this.$el
23 this.$el
24 .attr('id', guid)
24 .attr('id', guid)
25 .addClass('panel-group');
25 .addClass('panel-group');
26 this.containers = [];
26 this.containers = [];
27 this.model_containers = {};
27 this.model_containers = {};
28 this.update_children([], this.model.get('_children'));
28 this.update_children([], this.model.get('children'));
29 this.model.on('change:_children', function(model, value, options) {
29 this.model.on('change:children', function(model, value, options) {
30 this.update_children(model.previous('_children'), value);
30 this.update_children(model.previous('children'), value);
31 }, this);
31 }, this);
32 this.model.on('change:selected_index', function(model, value, options) {
32 this.model.on('change:selected_index', function(model, value, options) {
33 this.update_selected_index(model.previous('selected_index'), value, options);
33 this.update_selected_index(model.previous('selected_index'), value, options);
34 }, this);
34 }, this);
35 this.model.on('change:_titles', function(model, value, options) {
35 this.model.on('change:_titles', function(model, value, options) {
36 this.update_titles(value);
36 this.update_titles(value);
37 }, this);
37 }, this);
38 var that = this;
38 var that = this;
39 this.model.on('displayed', function() {
39 this.on('displayed', function() {
40 this.update_titles();
40 this.update_titles();
41 // Trigger model displayed events for any models that are child to
41 // Trigger model displayed events for any models that are child to
42 // this model when this model is displayed.
42 // this model when this model is displayed.
43 that.is_displayed = true;
43 that.is_displayed = true;
44 for (var property in that.child_views) {
44 for (var property in that.child_views) {
45 if (that.child_views.hasOwnProperty(property)) {
45 if (that.child_views.hasOwnProperty(property)) {
46 that.child_views[property].model.trigger('displayed');
46 that.child_views[property].trigger('displayed');
47 }
47 }
48 }
48 }
49 }, this);
49 }, this);
50 },
50 },
51
51
52 update_titles: function(titles) {
52 update_titles: function(titles) {
53 // Set tab titles
53 // Set tab titles
54 if (!titles) {
54 if (!titles) {
55 titles = this.model.get('_titles');
55 titles = this.model.get('_titles');
56 }
56 }
57
57
58 var that = this;
58 var that = this;
59 _.each(titles, function(title, page_index) {
59 _.each(titles, function(title, page_index) {
60 var accordian = that.containers[page_index];
60 var accordian = that.containers[page_index];
61 if (accordian !== undefined) {
61 if (accordian !== undefined) {
62 accordian
62 accordian
63 .find('.panel-heading')
63 .find('.panel-heading')
64 .find('.accordion-toggle')
64 .find('.accordion-toggle')
65 .text(title);
65 .text(title);
66 }
66 }
67 });
67 });
68 },
68 },
69
69
70 update_selected_index: function(old_index, new_index, options) {
70 update_selected_index: function(old_index, new_index, options) {
71 // Only update the selection if the selection wasn't triggered
71 // Only update the selection if the selection wasn't triggered
72 // by the front-end. It must be triggered by the back-end.
72 // by the front-end. It must be triggered by the back-end.
73 if (options === undefined || options.updated_view != this) {
73 if (options === undefined || options.updated_view != this) {
74 this.containers[old_index].find('.panel-collapse').collapse('hide');
74 this.containers[old_index].find('.panel-collapse').collapse('hide');
75 if (0 <= new_index && new_index < this.containers.length) {
75 if (0 <= new_index && new_index < this.containers.length) {
76 this.containers[new_index].find('.panel-collapse').collapse('show');
76 this.containers[new_index].find('.panel-collapse').collapse('show');
77 }
77 }
78 }
78 }
79 },
79 },
80
80
81 update_children: function(old_list, new_list) {
81 update_children: function(old_list, new_list) {
82 // Called when the children list is modified.
82 // Called when the children list is modified.
83 this.do_diff(old_list,
83 this.do_diff(old_list,
84 new_list,
84 new_list,
85 $.proxy(this.remove_child_model, this),
85 $.proxy(this.remove_child_model, this),
86 $.proxy(this.add_child_model, this));
86 $.proxy(this.add_child_model, this));
87 },
87 },
88
88
89 remove_child_model: function(model) {
89 remove_child_model: function(model) {
90 // Called when a child is removed from children list.
90 // Called when a child is removed from children list.
91 var accordion_group = this.model_containers[model.id];
91 var accordion_group = this.model_containers[model.id];
92 this.containers.splice(accordion_group.container_index, 1);
92 this.containers.splice(accordion_group.container_index, 1);
93 delete this.model_containers[model.id];
93 delete this.model_containers[model.id];
94 accordion_group.remove();
94 accordion_group.remove();
95 this.delete_child_view(model);
95 this.pop_child_view(model);
96 },
96 },
97
97
98 add_child_model: function(model) {
98 add_child_model: function(model) {
99 // Called when a child is added to children list.
99 // Called when a child is added to children list.
100 var view = this.create_child_view(model);
100 var view = this.create_child_view(model);
101 var index = this.containers.length;
101 var index = this.containers.length;
102 var uuid = IPython.utils.uuid();
102 var uuid = IPython.utils.uuid();
103 var accordion_group = $('<div />')
103 var accordion_group = $('<div />')
104 .addClass('panel panel-default')
104 .addClass('panel panel-default')
105 .appendTo(this.$el);
105 .appendTo(this.$el);
106 var accordion_heading = $('<div />')
106 var accordion_heading = $('<div />')
107 .addClass('panel-heading')
107 .addClass('panel-heading')
108 .appendTo(accordion_group);
108 .appendTo(accordion_group);
109 var that = this;
109 var that = this;
110 var accordion_toggle = $('<a />')
110 var accordion_toggle = $('<a />')
111 .addClass('accordion-toggle')
111 .addClass('accordion-toggle')
112 .attr('data-toggle', 'collapse')
112 .attr('data-toggle', 'collapse')
113 .attr('data-parent', '#' + this.$el.attr('id'))
113 .attr('data-parent', '#' + this.$el.attr('id'))
114 .attr('href', '#' + uuid)
114 .attr('href', '#' + uuid)
115 .click(function(evt){
115 .click(function(evt){
116
116
117 // Calling model.set will trigger all of the other views of the
117 // Calling model.set will trigger all of the other views of the
118 // model to update.
118 // model to update.
119 that.model.set("selected_index", index, {updated_view: that});
119 that.model.set("selected_index", index, {updated_view: that});
120 that.touch();
120 that.touch();
121 })
121 })
122 .text('Page ' + index)
122 .text('Page ' + index)
123 .appendTo(accordion_heading);
123 .appendTo(accordion_heading);
124 var accordion_body = $('<div />', {id: uuid})
124 var accordion_body = $('<div />', {id: uuid})
125 .addClass('panel-collapse collapse')
125 .addClass('panel-collapse collapse')
126 .appendTo(accordion_group);
126 .appendTo(accordion_group);
127 var accordion_inner = $('<div />')
127 var accordion_inner = $('<div />')
128 .addClass('panel-body')
128 .addClass('panel-body')
129 .appendTo(accordion_body);
129 .appendTo(accordion_body);
130 var container_index = this.containers.push(accordion_group) - 1;
130 var container_index = this.containers.push(accordion_group) - 1;
131 accordion_group.container_index = container_index;
131 accordion_group.container_index = container_index;
132 this.model_containers[model.id] = accordion_group;
132 this.model_containers[model.id] = accordion_group;
133 accordion_inner.append(view.$el);
133 accordion_inner.append(view.$el);
134
134
135 this.update();
135 this.update();
136 this.update_titles();
136 this.update_titles();
137
137
138 // Trigger the displayed event if this model is displayed.
138 // Trigger the displayed event if this model is displayed.
139 if (this.is_displayed) {
139 if (this.is_displayed) {
140 model.trigger('displayed');
140 view.trigger('displayed');
141 }
141 }
142 },
142 },
143 });
143 });
144 WidgetManager.register_widget_view('AccordionView', AccordionView);
144 WidgetManager.register_widget_view('AccordionView', AccordionView);
145
145
146
146
147 var TabView = IPython.DOMWidgetView.extend({
147 var TabView = IPython.DOMWidgetView.extend({
148 initialize: function() {
148 initialize: function() {
149 // Public constructor.
149 // Public constructor.
150 this.containers = [];
150 this.containers = [];
151 TabView.__super__.initialize.apply(this, arguments);
151 TabView.__super__.initialize.apply(this, arguments);
152 },
152 },
153
153
154 render: function(){
154 render: function(){
155 // Called when view is rendered.
155 // Called when view is rendered.
156 var uuid = 'tabs'+IPython.utils.uuid();
156 var uuid = 'tabs'+IPython.utils.uuid();
157 var that = this;
157 var that = this;
158 this.$tabs = $('<div />', {id: uuid})
158 this.$tabs = $('<div />', {id: uuid})
159 .addClass('nav')
159 .addClass('nav')
160 .addClass('nav-tabs')
160 .addClass('nav-tabs')
161 .appendTo(this.$el);
161 .appendTo(this.$el);
162 this.$tab_contents = $('<div />', {id: uuid + 'Content'})
162 this.$tab_contents = $('<div />', {id: uuid + 'Content'})
163 .addClass('tab-content')
163 .addClass('tab-content')
164 .appendTo(this.$el);
164 .appendTo(this.$el);
165 this.containers = [];
165 this.containers = [];
166 this.update_children([], this.model.get('_children'));
166 this.update_children([], this.model.get('children'));
167 this.model.on('change:_children', function(model, value, options) {
167 this.model.on('change:children', function(model, value, options) {
168 this.update_children(model.previous('_children'), value);
168 this.update_children(model.previous('children'), value);
169 }, this);
169 }, this);
170
170
171 // Trigger model displayed events for any models that are child to
171 // Trigger model displayed events for any models that are child to
172 // this model when this model is displayed.
172 // this model when this model is displayed.
173 this.model.on('displayed', function(){
173 this.on('displayed', function(){
174 that.is_displayed = true;
174 that.is_displayed = true;
175 for (var property in that.child_views) {
175 for (var property in that.child_views) {
176 if (that.child_views.hasOwnProperty(property)) {
176 if (that.child_views.hasOwnProperty(property)) {
177 that.child_views[property].model.trigger('displayed');
177 that.child_views[property].trigger('displayed');
178 }
178 }
179 }
179 }
180 });
180 });
181 },
181 },
182
182
183 update_children: function(old_list, new_list) {
183 update_children: function(old_list, new_list) {
184 // Called when the children list is modified.
184 // Called when the children list is modified.
185 this.do_diff(old_list,
185 this.do_diff(old_list,
186 new_list,
186 new_list,
187 $.proxy(this.remove_child_model, this),
187 $.proxy(this.remove_child_model, this),
188 $.proxy(this.add_child_model, this));
188 $.proxy(this.add_child_model, this));
189 },
189 },
190
190
191 remove_child_model: function(model) {
191 remove_child_model: function(model) {
192 // Called when a child is removed from children list.
192 // Called when a child is removed from children list.
193 var view = this.child_views[model.id];
193 var view = this.pop_child_view(model);
194 this.containers.splice(view.parent_tab.tab_text_index, 1);
194 this.containers.splice(view.parent_tab.tab_text_index, 1);
195 view.parent_tab.remove();
195 view.parent_tab.remove();
196 view.parent_container.remove();
196 view.parent_container.remove();
197 view.remove();
197 view.remove();
198 this.delete_child_view(model);
199 },
198 },
200
199
201 add_child_model: function(model) {
200 add_child_model: function(model) {
202 // Called when a child is added to children list.
201 // Called when a child is added to children list.
203 var view = this.create_child_view(model);
202 var view = this.create_child_view(model);
204 var index = this.containers.length;
203 var index = this.containers.length;
205 var uuid = IPython.utils.uuid();
204 var uuid = IPython.utils.uuid();
206
205
207 var that = this;
206 var that = this;
208 var tab = $('<li />')
207 var tab = $('<li />')
209 .css('list-style-type', 'none')
208 .css('list-style-type', 'none')
210 .appendTo(this.$tabs);
209 .appendTo(this.$tabs);
211 view.parent_tab = tab;
210 view.parent_tab = tab;
212
211
213 var tab_text = $('<a />')
212 var tab_text = $('<a />')
214 .attr('href', '#' + uuid)
213 .attr('href', '#' + uuid)
215 .attr('data-toggle', 'tab')
214 .attr('data-toggle', 'tab')
216 .text('Page ' + index)
215 .text('Page ' + index)
217 .appendTo(tab)
216 .appendTo(tab)
218 .click(function (e) {
217 .click(function (e) {
219
218
220 // Calling model.set will trigger all of the other views of the
219 // Calling model.set will trigger all of the other views of the
221 // model to update.
220 // model to update.
222 that.model.set("selected_index", index, {updated_view: this});
221 that.model.set("selected_index", index, {updated_view: this});
223 that.touch();
222 that.touch();
224 that.select_page(index);
223 that.select_page(index);
225 });
224 });
226 tab.tab_text_index = this.containers.push(tab_text) - 1;
225 tab.tab_text_index = this.containers.push(tab_text) - 1;
227
226
228 var contents_div = $('<div />', {id: uuid})
227 var contents_div = $('<div />', {id: uuid})
229 .addClass('tab-pane')
228 .addClass('tab-pane')
230 .addClass('fade')
229 .addClass('fade')
231 .append(view.$el)
230 .append(view.$el)
232 .appendTo(this.$tab_contents);
231 .appendTo(this.$tab_contents);
233 view.parent_container = contents_div;
232 view.parent_container = contents_div;
234
233
235 // Trigger the displayed event if this model is displayed.
234 // Trigger the displayed event if this model is displayed.
236 if (this.is_displayed) {
235 if (this.is_displayed) {
237 model.trigger('displayed');
236 view.trigger('displayed');
238 }
237 }
239 },
238 },
240
239
241 update: function(options) {
240 update: function(options) {
242 // Update the contents of this view
241 // Update the contents of this view
243 //
242 //
244 // Called when the model is changed. The model may have been
243 // Called when the model is changed. The model may have been
245 // changed by another view or by a state update from the back-end.
244 // changed by another view or by a state update from the back-end.
246 if (options === undefined || options.updated_view != this) {
245 if (options === undefined || options.updated_view != this) {
247 // Set tab titles
246 // Set tab titles
248 var titles = this.model.get('_titles');
247 var titles = this.model.get('_titles');
249 var that = this;
248 var that = this;
250 _.each(titles, function(title, page_index) {
249 _.each(titles, function(title, page_index) {
251 var tab_text = that.containers[page_index];
250 var tab_text = that.containers[page_index];
252 if (tab_text !== undefined) {
251 if (tab_text !== undefined) {
253 tab_text.text(title);
252 tab_text.text(title);
254 }
253 }
255 });
254 });
256
255
257 var selected_index = this.model.get('selected_index');
256 var selected_index = this.model.get('selected_index');
258 if (0 <= selected_index && selected_index < this.containers.length) {
257 if (0 <= selected_index && selected_index < this.containers.length) {
259 this.select_page(selected_index);
258 this.select_page(selected_index);
260 }
259 }
261 }
260 }
262 return TabView.__super__.update.apply(this);
261 return TabView.__super__.update.apply(this);
263 },
262 },
264
263
265 select_page: function(index) {
264 select_page: function(index) {
266 // Select a page.
265 // Select a page.
267 this.$tabs.find('li')
266 this.$tabs.find('li')
268 .removeClass('active');
267 .removeClass('active');
269 this.containers[index].tab('show');
268 this.containers[index].tab('show');
270 },
269 },
271 });
270 });
272 WidgetManager.register_widget_view('TabView', TabView);
271 WidgetManager.register_widget_view('TabView', TabView);
273 });
272 });
@@ -1,62 +1,33 b''
1 """ContainerWidget class.
1 """ContainerWidget class.
2
2
3 Represents a container that can be used to group other widgets.
3 Represents a container that can be used to group other widgets.
4 """
4 """
5 #-----------------------------------------------------------------------------
5
6 # Copyright (c) 2013, the IPython Development Team.
6 # Copyright (c) IPython Development Team.
7 #
8 # Distributed under the terms of the Modified BSD License.
7 # Distributed under the terms of the Modified BSD License.
9 #
10 # The full license is in the file COPYING.txt, distributed with this software.
11 #-----------------------------------------------------------------------------
12
8
13 #-----------------------------------------------------------------------------
14 # Imports
15 #-----------------------------------------------------------------------------
16 from .widget import DOMWidget
9 from .widget import DOMWidget
17 from IPython.utils.traitlets import Unicode, Tuple, TraitError
10 from IPython.utils.traitlets import Unicode, Tuple, TraitError
18
11
19 #-----------------------------------------------------------------------------
20 # Classes
21 #-----------------------------------------------------------------------------
22
23 class ContainerWidget(DOMWidget):
12 class ContainerWidget(DOMWidget):
24 _view_name = Unicode('ContainerView', sync=True)
13 _view_name = Unicode('ContainerView', sync=True)
25
14
26 # Child widgets in the container.
15 # Child widgets in the container.
27 # Using a tuple here to force reassignment to update the list.
16 # Using a tuple here to force reassignment to update the list.
28 # When a proper notifying-list trait exists, that is what should be used here.
17 # When a proper notifying-list trait exists, that is what should be used here.
29 children = Tuple()
18 children = Tuple(sync=True)
30 _children = Tuple(sync=True)
31
32
19
33 def __init__(self, **kwargs):
20 def __init__(self, **kwargs):
34 super(ContainerWidget, self).__init__(**kwargs)
21 super(ContainerWidget, self).__init__(**kwargs)
35 self.on_displayed(ContainerWidget._fire_children_displayed)
22 self.on_displayed(ContainerWidget._fire_children_displayed)
36
23
37 def _fire_children_displayed(self):
24 def _fire_children_displayed(self):
38 for child in self._children:
25 for child in self.children:
39 child._handle_displayed()
26 child._handle_displayed()
40
27
41 def _children_changed(self, name, old, new):
42 """Validate children list.
43
44 Makes sure only one instance of any given model can exist in the
45 children list.
46 An excellent post on uniqifiers is available at
47 http://www.peterbe.com/plog/uniqifiers-benchmark
48 which provides the inspiration for using this implementation. Below
49 I've implemented the `f5` algorithm using Python comprehensions."""
50 if new is not None:
51 seen = {}
52 def add_item(i):
53 seen[i.model_id] = True
54 return i
55 self._children = [add_item(i) for i in new if not i.model_id in seen]
56
57
28
58 class PopupWidget(ContainerWidget):
29 class PopupWidget(ContainerWidget):
59 _view_name = Unicode('PopupView', sync=True)
30 _view_name = Unicode('PopupView', sync=True)
60
31
61 description = Unicode(sync=True)
32 description = Unicode(sync=True)
62 button_text = Unicode(sync=True)
33 button_text = Unicode(sync=True)
General Comments 0
You need to be logged in to leave comments. Login now