##// END OF EJS Templates
Added support for multiple model views in one widget area.
Jonathan Frederic -
Show More
@@ -1,351 +1,363 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 "use strict";
18 "use strict";
19
19
20 // Use require.js 'define' method so that require.js is intelligent enough to
20 // Use require.js 'define' method so that require.js is intelligent enough to
21 // syncronously load everything within this file when it is being 'required'
21 // syncronously load everything within this file when it is being 'required'
22 // elsewhere.
22 // elsewhere.
23 define(["../../components/underscore/underscore-min.js",
23 define(["../../components/underscore/underscore-min.js",
24 "../../components/backbone/backbone-min.js",
24 "../../components/backbone/backbone-min.js",
25 ], function(){
25 ], function(){
26
26
27 // Only run once on a notebook.
27 // Only run once on a notebook.
28 if (IPython.notebook.widget_manager == undefined) {
28 if (IPython.notebook.widget_manager == undefined) {
29
29
30 //--------------------------------------------------------------------
30 //--------------------------------------------------------------------
31 // WidgetModel class
31 // WidgetModel class
32 //--------------------------------------------------------------------
32 //--------------------------------------------------------------------
33 var WidgetModel = Backbone.Model.extend({
33 var WidgetModel = Backbone.Model.extend({
34 constructor: function(comm_manager, comm, widget_view_types) {
34 constructor: function(comm_manager, comm, widget_view_types) {
35 this.comm_manager = comm_manager;
35 this.comm_manager = comm_manager;
36 this.widget_view_types = widget_view_types;
36 this.widget_view_types = widget_view_types;
37 this.pending_msgs = 0;
37 this.pending_msgs = 0;
38 this.msg_throttle = 3;
38 this.msg_throttle = 3;
39 this.msg_buffer = {};
39 this.msg_buffer = {};
40 this.views = {};
40 this.views = {};
41
41
42 // Remember comm associated with the model.
42 // Remember comm associated with the model.
43 this.comm = comm;
43 this.comm = comm;
44 comm.model = this;
44 comm.model = this;
45
45
46 // Hook comm messages up to model.
46 // Hook comm messages up to model.
47 comm.on_close($.proxy(this.handle_comm_closed, this));
47 comm.on_close($.proxy(this.handle_comm_closed, this));
48 comm.on_msg($.proxy(this.handle_comm_msg, this));
48 comm.on_msg($.proxy(this.handle_comm_msg, this));
49
49
50 return Backbone.Model.apply(this);
50 return Backbone.Model.apply(this);
51 },
51 },
52
52
53
53
54 update_other_views: function(caller) {
54 update_other_views: function(caller) {
55 this.last_modified_view = caller;
55 this.last_modified_view = caller;
56 this.save(this.changedAttributes(), {patch: true});
56 this.save(this.changedAttributes(), {patch: true});
57
57
58 for (var cell_index in this.views) {
58 for (var cell_index in this.views) {
59 var view = this.views[cell_index];
59 var views = this.views[cell_index];
60 if (view !== caller) {
60 for (var view_index in views) {
61 view.update();
61 var view = views[view_index];
62 if (view !== caller) {
63 view.update();
64 }
62 }
65 }
63 }
66 }
64 },
67 },
65
68
66
69
67 handle_status: function (output_area, msg) {
70 handle_status: function (output_area, msg) {
68 //execution_state : ('busy', 'idle', 'starting')
71 //execution_state : ('busy', 'idle', 'starting')
69 if (msg.content.execution_state=='idle') {
72 if (msg.content.execution_state=='idle') {
70
73
71 // Send buffer if this message caused another message to be
74 // Send buffer if this message caused another message to be
72 // throttled.
75 // throttled.
73 if (this.msg_throttle == this.pending_msgs &&
76 if (this.msg_throttle == this.pending_msgs &&
74 this.msg_buffer.length > 0) {
77 this.msg_buffer.length > 0) {
75
78
76 var output_area = this._get_msg_output_area(msg);
79 var output_area = this._get_msg_output_area(msg);
77 var callbacks = this._make_callbacks(output_area);
80 var callbacks = this._make_callbacks(output_area);
78 var data = {sync_method: 'patch', sync_data: this.msg_buffer};
81 var data = {sync_method: 'patch', sync_data: this.msg_buffer};
79 comm.send(data, callbacks);
82 comm.send(data, callbacks);
80 this.msg_buffer = {};
83 this.msg_buffer = {};
81 } else {
84 } else {
82
85
83 // Only decrease the pending message count if the buffer
86 // Only decrease the pending message count if the buffer
84 // doesn't get flushed (sent).
87 // doesn't get flushed (sent).
85 --this.pending_msgs;
88 --this.pending_msgs;
86 }
89 }
87 }
90 }
88 },
91 },
89
92
90
93
91 // Custom syncronization logic.
94 // Custom syncronization logic.
92 handle_sync: function (method, options) {
95 handle_sync: function (method, options) {
93 var model_json = this.toJSON();
96 var model_json = this.toJSON();
94
97
95 // Only send updated state if the state hasn't been changed
98 // Only send updated state if the state hasn't been changed
96 // during an update.
99 // during an update.
97 if (!this.updating) {
100 if (!this.updating) {
98 if (this.pending_msgs >= this.msg_throttle) {
101 if (this.pending_msgs >= this.msg_throttle) {
99 // The throttle has been exceeded, buffer the current msg so
102 // The throttle has been exceeded, buffer the current msg so
100 // it can be sent once the kernel has finished processing
103 // it can be sent once the kernel has finished processing
101 // some of the existing messages.
104 // some of the existing messages.
102 if (method=='patch') {
105 if (method=='patch') {
103 for (var attr in options.attrs) {
106 for (var attr in options.attrs) {
104 this.msg_buffer[attr] = options.attrs[attr];
107 this.msg_buffer[attr] = options.attrs[attr];
105 }
108 }
106 } else {
109 } else {
107 this.msg_buffer = $.extend({}, model_json); // Copy
110 this.msg_buffer = $.extend({}, model_json); // Copy
108 }
111 }
109
112
110 } else {
113 } else {
111 // We haven't exceeded the throttle, send the message like
114 // We haven't exceeded the throttle, send the message like
112 // normal. If this is a patch operation, just send the
115 // normal. If this is a patch operation, just send the
113 // changes.
116 // changes.
114 var send_json = model_json;
117 var send_json = model_json;
115 if (method=='patch') {
118 if (method=='patch') {
116 send_json = {};
119 send_json = {};
117 for (var attr in options.attrs) {
120 for (var attr in options.attrs) {
118 send_json[attr] = options.attrs[attr];
121 send_json[attr] = options.attrs[attr];
119 }
122 }
120 }
123 }
121
124
122 var data = {sync_method: method, sync_data: send_json};
125 var data = {sync_method: method, sync_data: send_json};
123 var output_area = this._get_view_output_area(this.last_modified_view);
126 var output_area = this._get_view_output_area(this.last_modified_view);
124 var callbacks = this._make_callbacks(output_area);
127 var callbacks = this._make_callbacks(output_area);
125 this.comm.send(data, callbacks);
128 this.comm.send(data, callbacks);
126 this.pending_msgs++;
129 this.pending_msgs++;
127 }
130 }
128 }
131 }
129
132
130 // Since the comm is a one-way communication, assume the message
133 // Since the comm is a one-way communication, assume the message
131 // arrived.
134 // arrived.
132 return model_json;
135 return model_json;
133 },
136 },
134
137
135
138
136 // Handle incomming comm msg.
139 // Handle incomming comm msg.
137 handle_comm_msg: function (msg) {
140 handle_comm_msg: function (msg) {
138 var method = msg.content.data.method;
141 var method = msg.content.data.method;
139 switch (method){
142 switch (method){
140 case 'display':
143 case 'display':
141
144
142 ////////////////////////// TODO: Get cell index via currently executing cell.
145 ////////////////////////// TODO: Get cell index via currently executing cell.
143 var cell_index = IPython.notebook.get_selected_index()-1;
146 var cell_index = IPython.notebook.get_selected_index()-1;
144
147
145 this.display_view(msg.content.data.view_name,
148 this.display_view(msg.content.data.view_name,
146 msg.content.data.parent,
149 msg.content.data.parent,
147 cell_index);
150 cell_index);
148 break;
151 break;
149 case 'update':
152 case 'update':
150 this.handle_update(msg.content.data.state);
153 this.handle_update(msg.content.data.state);
151 break;
154 break;
152 }
155 }
153 },
156 },
154
157
155
158
156 // Handle when a widget is updated via the python side.
159 // Handle when a widget is updated via the python side.
157 handle_update: function (state) {
160 handle_update: function (state) {
158 this.updating = true;
161 this.updating = true;
159 try {
162 try {
160 for (var key in state) {
163 for (var key in state) {
161 if (state.hasOwnProperty(key)) {
164 if (state.hasOwnProperty(key)) {
162 if (key == "_css"){
165 if (key == "_css"){
163 this.css = state[key];
166 this.css = state[key];
164 } else {
167 } else {
165 this.set(key, state[key]);
168 this.set(key, state[key]);
166 }
169 }
167 }
170 }
168 }
171 }
169 this.id = this.comm.comm_id;
172 this.id = this.comm.comm_id;
170 this.save();
173 this.save();
171 } finally {
174 } finally {
172 this.updating = false;
175 this.updating = false;
173 }
176 }
174 },
177 },
175
178
176
179
177 // Handle when a widget is closed.
180 // Handle when a widget is closed.
178 handle_comm_closed: function (msg) {
181 handle_comm_closed: function (msg) {
179 for (var cell_index in this.views) {
182 for (var cell_index in this.views) {
180 var view = this.views[cell_index];
183 var views = this.views[cell_index];
181 view.remove();
184 for (var view_index in views) {
185 var view = views[view_index];
186 view.remove();
187 }
182 }
188 }
183 },
189 },
184
190
185
191
186 // Create view that represents the model.
192 // Create view that represents the model.
187 display_view: function (view_name, parent_comm_id, cell_index) {
193 display_view: function (view_name, parent_comm_id, cell_index) {
188 var view = new this.widget_view_types[view_name]({model: this});
194 var view = new this.widget_view_types[view_name]({model: this});
189 view.render();
195 view.render();
190 this.views[cell_index] = view;
196 if (this.views[cell_index]==undefined) {
197 this.views[cell_index] = []
198 }
199 this.views[cell_index].push(view);
191 view.cell_index = cell_index;
200 view.cell_index = cell_index;
192
201
193 // Handle when the view element is remove from the page.
202 // Handle when the view element is remove from the page.
194 var that = this;
203 var that = this;
195 view.$el.on("remove", function(){
204 view.$el.on("remove", function(){
196 var index = that.views.indexOf(view);
205 var index = that.views[cell_index].indexOf(view);
197 if (index > -1) {
206 if (index > -1) {
198 that.views.splice(index, 1);
207 that.views[cell_index].splice(index, 1);
199 }
208 }
200 view.remove(); // Clean-up view
209 view.remove(); // Clean-up view
210 if (that.views[cell_index].length()==0) {
211 delete that.views[cell_index];
212 }
201
213
202 // Close the comm if there are no views left.
214 // Close the comm if there are no views left.
203 if (that.views.length()==0) {
215 if (that.views.length()==0) {
204 that.comm.close();
216 that.comm.close();
205 }
217 }
206 });
218 });
207
219
208 var displayed = false;
220 var displayed = false;
209 if (parent_comm_id != undefined) {
221 if (parent_comm_id != undefined) {
210 var parent_comm = this.comm_manager.comms[parent_comm_id];
222 var parent_comm = this.comm_manager.comms[parent_comm_id];
211 var parent_model = parent_comm.model;
223 var parent_model = parent_comm.model;
212 var parent_view = parent_model.views[cell_index];
224 var parent_view = parent_model.views[cell_index];
213 if (parent_view.display_child != undefined) {
225 if (parent_view.display_child != undefined) {
214 parent_view.display_child(view);
226 parent_view.display_child(view);
215 displayed = true;
227 displayed = true;
216 }
228 }
217 }
229 }
218
230
219 if (!displayed) {
231 if (!displayed) {
220 // No parent view is defined or exists. Add the view's
232 // No parent view is defined or exists. Add the view's
221 // element to cell's widget div.
233 // element to cell's widget div.
222 var cell = IPython.notebook.get_cell(cell_index);
234 var cell = IPython.notebook.get_cell(cell_index);
223 cell.element.find('.widget_area').find('.widget_subarea')
235 cell.element.find('.widget_area').find('.widget_subarea')
224 .append(view.$el)
236 .append(view.$el)
225 .parent().show(); // Show the widget_area (parent of widget_subarea)
237 .parent().show(); // Show the widget_area (parent of widget_subarea)
226
238
227 }
239 }
228
240
229 // Update the view based on the model contents.
241 // Update the view based on the model contents.
230 view.update();
242 view.update();
231 },
243 },
232
244
233
245
234 // Build a callback dict.
246 // Build a callback dict.
235 _make_callbacks: function (output_area) {
247 _make_callbacks: function (output_area) {
236 var callbacks = {};
248 var callbacks = {};
237 if (output_area != null) {
249 if (output_area != null) {
238 var that = this;
250 var that = this;
239 callbacks = {
251 callbacks = {
240 iopub : {
252 iopub : {
241 output : $.proxy(output_area.handle_output, output_area),
253 output : $.proxy(output_area.handle_output, output_area),
242 clear_output : $.proxy(output_area.handle_clear_output, output_area),
254 clear_output : $.proxy(output_area.handle_clear_output, output_area),
243 status : function(msg){
255 status : function(msg){
244 that.handle_status(output_area, msg);
256 that.handle_status(output_area, msg);
245 },
257 },
246 },
258 },
247 };
259 };
248 }
260 }
249 return callbacks;
261 return callbacks;
250 },
262 },
251
263
252
264
253 // Get the cell output area corresponding to the view.
265 // Get the cell output area corresponding to the view.
254 _get_view_output_area: function (view) {
266 _get_view_output_area: function (view) {
255 return this._get_cell_output_area(view.cell_index);
267 return this._get_cell_output_area(view.cell_index);
256 },
268 },
257
269
258
270
259 // Get the cell output area corresponding to the cell id.
271 // Get the cell output area corresponding to the cell id.
260 _get_cell_output_area: function (cell_id) {
272 _get_cell_output_area: function (cell_id) {
261 var cell = IPython.notebook.get_cell(cell_id)
273 var cell = IPython.notebook.get_cell(cell_id)
262 return cell.output_area;
274 return cell.output_area;
263 },
275 },
264 });
276 });
265
277
266
278
267 //--------------------------------------------------------------------
279 //--------------------------------------------------------------------
268 // WidgetView class
280 // WidgetView class
269 //--------------------------------------------------------------------
281 //--------------------------------------------------------------------
270 var WidgetView = Backbone.View.extend({
282 var WidgetView = Backbone.View.extend({
271
283
272 initialize: function() {
284 initialize: function() {
273 this.model.on('change',this.update,this);
285 this.model.on('change',this.update,this);
274 },
286 },
275
287
276 update: function() {
288 update: function() {
277 if (this.model.css != undefined) {
289 if (this.model.css != undefined) {
278 for (var selector in this.model.css) {
290 for (var selector in this.model.css) {
279 if (this.model.css.hasOwnProperty(selector)) {
291 if (this.model.css.hasOwnProperty(selector)) {
280
292
281 // Get the elements via the css selector. If the selector is
293 // Get the elements via the css selector. If the selector is
282 // blank, assume the current element is the target.
294 // blank, assume the current element is the target.
283 var elements = this.$el.find(selector);
295 var elements = this.$el.find(selector);
284 if (selector=='') {
296 if (selector=='') {
285 elements = this.$el;
297 elements = this.$el;
286 }
298 }
287
299
288 // Apply the css traits to all elements that match the selector.
300 // Apply the css traits to all elements that match the selector.
289 if (elements.length>0){
301 if (elements.length>0){
290 var css_traits = this.model.css[selector];
302 var css_traits = this.model.css[selector];
291 for (var css_key in css_traits) {
303 for (var css_key in css_traits) {
292 if (css_traits.hasOwnProperty(css_key)) {
304 if (css_traits.hasOwnProperty(css_key)) {
293 elements.css(css_key, css_traits[css_key]);
305 elements.css(css_key, css_traits[css_key]);
294 }
306 }
295 }
307 }
296 }
308 }
297 }
309 }
298 }
310 }
299 }
311 }
300 },
312 },
301 });
313 });
302
314
303
315
304 //--------------------------------------------------------------------
316 //--------------------------------------------------------------------
305 // WidgetManager class
317 // WidgetManager class
306 //--------------------------------------------------------------------
318 //--------------------------------------------------------------------
307 var WidgetManager = function(comm_manager){
319 var WidgetManager = function(comm_manager){
308 this.comm_manager = comm_manager;
320 this.comm_manager = comm_manager;
309 this.widget_model_types = {};
321 this.widget_model_types = {};
310 this.widget_view_types = {};
322 this.widget_view_types = {};
311
323
312 var that = this;
324 var that = this;
313 Backbone.sync = function(method, model, options, error) {
325 Backbone.sync = function(method, model, options, error) {
314 var result = model.handle_sync(method, options);
326 var result = model.handle_sync(method, options);
315 if (options.success) {
327 if (options.success) {
316 options.success(result);
328 options.success(result);
317 }
329 }
318 };
330 };
319 }
331 }
320
332
321
333
322 WidgetManager.prototype.register_widget_model = function (widget_model_name, widget_model_type) {
334 WidgetManager.prototype.register_widget_model = function (widget_model_name, widget_model_type) {
323 // Register the widget with the comm manager. Make sure to pass this object's context
335 // Register the widget with the comm manager. Make sure to pass this object's context
324 // in so `this` works in the call back.
336 // in so `this` works in the call back.
325 this.comm_manager.register_target(widget_model_name, $.proxy(this.handle_com_open, this));
337 this.comm_manager.register_target(widget_model_name, $.proxy(this.handle_com_open, this));
326 this.widget_model_types[widget_model_name] = widget_model_type;
338 this.widget_model_types[widget_model_name] = widget_model_type;
327 }
339 }
328
340
329
341
330 WidgetManager.prototype.register_widget_view = function (widget_view_name, widget_view_type) {
342 WidgetManager.prototype.register_widget_view = function (widget_view_name, widget_view_type) {
331 this.widget_view_types[widget_view_name] = widget_view_type;
343 this.widget_view_types[widget_view_name] = widget_view_type;
332 }
344 }
333
345
334
346
335 WidgetManager.prototype.handle_com_open = function (comm, msg) {
347 WidgetManager.prototype.handle_com_open = function (comm, msg) {
336 var widget_type_name = msg.content.target_name;
348 var widget_type_name = msg.content.target_name;
337 var widget_model = new this.widget_model_types[widget_type_name](this.comm_manager, comm, this.widget_view_types);
349 var widget_model = new this.widget_model_types[widget_type_name](this.comm_manager, comm, this.widget_view_types);
338 }
350 }
339
351
340
352
341 //--------------------------------------------------------------------
353 //--------------------------------------------------------------------
342 // Init code
354 // Init code
343 //--------------------------------------------------------------------
355 //--------------------------------------------------------------------
344 IPython.WidgetManager = WidgetManager;
356 IPython.WidgetManager = WidgetManager;
345 IPython.WidgetModel = WidgetModel;
357 IPython.WidgetModel = WidgetModel;
346 IPython.WidgetView = WidgetView;
358 IPython.WidgetView = WidgetView;
347
359
348 IPython.notebook.widget_manager = new WidgetManager(IPython.notebook.kernel.comm_manager);
360 IPython.notebook.widget_manager = new WidgetManager(IPython.notebook.kernel.comm_manager);
349
361
350 };
362 };
351 });
363 });
General Comments 0
You need to be logged in to leave comments. Login now