##// END OF EJS Templates
Changed button to use custom messages instead of state to communicate events.
Jonathan Frederic -
Show More
@@ -1,603 +1,603 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",
23 define(["components/underscore/underscore-min",
24 "components/backbone/backbone-min",
24 "components/backbone/backbone-min",
25 ], function(underscore, backbone){
25 ], function(underscore, backbone){
26
26
27
27
28 //--------------------------------------------------------------------
28 //--------------------------------------------------------------------
29 // WidgetModel class
29 // WidgetModel class
30 //--------------------------------------------------------------------
30 //--------------------------------------------------------------------
31 var WidgetModel = Backbone.Model.extend({
31 var WidgetModel = Backbone.Model.extend({
32 constructor: function(comm_manager, comm, widget_manager) {
32 constructor: function(comm_manager, comm, widget_manager) {
33 this.comm_manager = comm_manager;
33 this.comm_manager = comm_manager;
34 this.widget_manager = widget_manager;
34 this.widget_manager = widget_manager;
35 this.pending_msgs = 0;
35 this.pending_msgs = 0;
36 this.msg_throttle = 3;
36 this.msg_throttle = 3;
37 this.msg_buffer = null;
37 this.msg_buffer = null;
38 this.views = {};
38 this.views = {};
39
39
40 // Remember comm associated with the model.
40 // Remember comm associated with the model.
41 this.comm = comm;
41 this.comm = comm;
42 comm.model = this;
42 comm.model = this;
43
43
44 // Hook comm messages up to model.
44 // Hook comm messages up to model.
45 comm.on_close($.proxy(this._handle_comm_closed, this));
45 comm.on_close($.proxy(this._handle_comm_closed, this));
46 comm.on_msg($.proxy(this._handle_comm_msg, this));
46 comm.on_msg($.proxy(this._handle_comm_msg, this));
47
47
48 return Backbone.Model.apply(this);
48 return Backbone.Model.apply(this);
49 },
49 },
50
50
51
51
52 update_other_views: function(caller) {
52 update_other_views: function(caller) {
53 this.last_modified_view = caller;
53 this.last_modified_view = caller;
54 this.save(this.changedAttributes(), {patch: true});
54 this.save(this.changedAttributes(), {patch: true});
55
55
56 for (var cell in this.views) {
56 for (var cell in this.views) {
57 var views = this.views[cell];
57 var views = this.views[cell];
58 for (var view_index in views) {
58 for (var view_index in views) {
59 var view = views[view_index];
59 var view = views[view_index];
60 if (view !== caller) {
60 if (view !== caller) {
61 view.update();
61 view.update();
62 }
62 }
63 }
63 }
64 }
64 }
65 },
65 },
66
66
67 send: function(content) {
67 send: function(content) {
68
68
69 // Used the last modified view as the sender of the message. This
69 // Used the last modified view as the sender of the message. This
70 // will insure that any python code triggered by the sent message
70 // will insure that any python code triggered by the sent message
71 // can create and display widgets and output.
71 // can create and display widgets and output.
72 var cell = null;
72 var cell = null;
73 if (this.last_modified_view != undefined &&
73 if (this.last_modified_view != undefined &&
74 this.last_modified_view.cell != undefined) {
74 this.last_modified_view.cell != undefined) {
75 cell = this.last_modified_view.cell;
75 cell = this.last_modified_view.cell;
76 }
76 }
77 var callbacks = this._make_callbacks(cell);
77 var callbacks = this._make_callbacks(cell);
78 var data = {custom_content: content};
78 var data = {'custom_content': content};
79 this.comm.send(data, callbacks);
79 this.comm.send(data, callbacks);
80 },
80 },
81
81
82
82
83 on_view_displayed: function (callback) {
83 on_view_displayed: function (callback) {
84 this._view_displayed_callback = callback;
84 this._view_displayed_callback = callback;
85 },
85 },
86
86
87
87
88 on_close: function (callback) {
88 on_close: function (callback) {
89 this._close_callback = callback;
89 this._close_callback = callback;
90 },
90 },
91
91
92
92
93 on_msg: function (callback) {
93 on_msg: function (callback) {
94 this._msg_callback = callback;
94 this._msg_callback = callback;
95 },
95 },
96
96
97
97
98 _handle_custom_msg: function (content) {
98 _handle_custom_msg: function (content) {
99 if (this._msg_callback) {
99 if (this._msg_callback) {
100 try {
100 try {
101 this._msg_callback(content);
101 this._msg_callback(content);
102 } catch (e) {
102 } catch (e) {
103 console.log("Exception in widget model msg callback", e, content);
103 console.log("Exception in widget model msg callback", e, content);
104 }
104 }
105 }
105 }
106 },
106 },
107
107
108
108
109 // Handle when a widget is closed.
109 // Handle when a widget is closed.
110 _handle_comm_closed: function (msg) {
110 _handle_comm_closed: function (msg) {
111 this._execute_views_method('remove');
111 this._execute_views_method('remove');
112 delete this.comm.model; // Delete ref so GC will collect widget model.
112 delete this.comm.model; // Delete ref so GC will collect widget model.
113 },
113 },
114
114
115
115
116 // Handle incomming comm msg.
116 // Handle incomming comm msg.
117 _handle_comm_msg: function (msg) {
117 _handle_comm_msg: function (msg) {
118 var method = msg.content.data.method;
118 var method = msg.content.data.method;
119 switch (method){
119 switch (method){
120 case 'display':
120 case 'display':
121
121
122 // Try to get the cell.
122 // Try to get the cell.
123 var cell = this._get_msg_cell(msg.parent_header.msg_id);
123 var cell = this._get_msg_cell(msg.parent_header.msg_id);
124 if (cell == null) {
124 if (cell == null) {
125 console.log("Could not determine where the display" +
125 console.log("Could not determine where the display" +
126 " message was from. Widget will not be displayed")
126 " message was from. Widget will not be displayed")
127 } else {
127 } else {
128 this._display_view(msg.content.data.view_name,
128 this._display_view(msg.content.data.view_name,
129 msg.content.data.parent,
129 msg.content.data.parent,
130 cell);
130 cell);
131 }
131 }
132 break;
132 break;
133 case 'update':
133 case 'update':
134 this._handle_update(msg.content.data.state);
134 this._handle_update(msg.content.data.state);
135 break;
135 break;
136 case 'add_class':
136 case 'add_class':
137 case 'remove_class':
137 case 'remove_class':
138 var selector = msg.content.data.selector;
138 var selector = msg.content.data.selector;
139 var class_list = msg.content.data.class_list;
139 var class_list = msg.content.data.class_list;
140 this._execute_views_method(method, selector, class_list);
140 this._execute_views_method(method, selector, class_list);
141 break;
141 break;
142 case 'custom':
142 case 'custom':
143 this._handle_custom_msg(msg.content.data.custom_content);
143 this._handle_custom_msg(msg.content.data.custom_content);
144 break;
144 break;
145 }
145 }
146 },
146 },
147
147
148
148
149 // Handle when a widget is updated via the python side.
149 // Handle when a widget is updated via the python side.
150 _handle_update: function (state) {
150 _handle_update: function (state) {
151 this.updating = true;
151 this.updating = true;
152 try {
152 try {
153 for (var key in state) {
153 for (var key in state) {
154 if (state.hasOwnProperty(key)) {
154 if (state.hasOwnProperty(key)) {
155 if (key == "_css"){
155 if (key == "_css"){
156
156
157 // Set the css value of the model as an attribute
157 // Set the css value of the model as an attribute
158 // instead of a backbone trait because we are only
158 // instead of a backbone trait because we are only
159 // interested in backend css -> frontend css. In
159 // interested in backend css -> frontend css. In
160 // other words, if the css dict changes in the
160 // other words, if the css dict changes in the
161 // frontend, we don't need to push the changes to
161 // frontend, we don't need to push the changes to
162 // the backend.
162 // the backend.
163 this.css = state[key];
163 this.css = state[key];
164 } else {
164 } else {
165 this.set(key, state[key]);
165 this.set(key, state[key]);
166 }
166 }
167 }
167 }
168 }
168 }
169 this.id = this.comm.comm_id;
169 this.id = this.comm.comm_id;
170 this.save();
170 this.save();
171 } finally {
171 } finally {
172 this.updating = false;
172 this.updating = false;
173 }
173 }
174 },
174 },
175
175
176
176
177 _handle_status: function (cell, msg) {
177 _handle_status: function (cell, msg) {
178 //execution_state : ('busy', 'idle', 'starting')
178 //execution_state : ('busy', 'idle', 'starting')
179 if (msg.content.execution_state=='idle') {
179 if (msg.content.execution_state=='idle') {
180
180
181 // Send buffer if this message caused another message to be
181 // Send buffer if this message caused another message to be
182 // throttled.
182 // throttled.
183 if (this.msg_buffer != null &&
183 if (this.msg_buffer != null &&
184 this.msg_throttle == this.pending_msgs) {
184 this.msg_throttle == this.pending_msgs) {
185
185
186 var cell = this._get_msg_cell(msg.parent_header.msg_id);
186 var cell = this._get_msg_cell(msg.parent_header.msg_id);
187 var callbacks = this._make_callbacks(cell);
187 var callbacks = this._make_callbacks(cell);
188 var data = {sync_method: 'update', sync_data: this.msg_buffer};
188 var data = {sync_method: 'update', sync_data: this.msg_buffer};
189 this.comm.send(data, callbacks);
189 this.comm.send(data, callbacks);
190 this.msg_buffer = null;
190 this.msg_buffer = null;
191 } else {
191 } else {
192
192
193 // Only decrease the pending message count if the buffer
193 // Only decrease the pending message count if the buffer
194 // doesn't get flushed (sent).
194 // doesn't get flushed (sent).
195 --this.pending_msgs;
195 --this.pending_msgs;
196 }
196 }
197 }
197 }
198 },
198 },
199
199
200
200
201 // Custom syncronization logic.
201 // Custom syncronization logic.
202 _handle_sync: function (method, options) {
202 _handle_sync: function (method, options) {
203 var model_json = this.toJSON();
203 var model_json = this.toJSON();
204
204
205 // Only send updated state if the state hasn't been changed
205 // Only send updated state if the state hasn't been changed
206 // during an update.
206 // during an update.
207 if (!this.updating) {
207 if (!this.updating) {
208 if (this.pending_msgs >= this.msg_throttle) {
208 if (this.pending_msgs >= this.msg_throttle) {
209 // The throttle has been exceeded, buffer the current msg so
209 // The throttle has been exceeded, buffer the current msg so
210 // it can be sent once the kernel has finished processing
210 // it can be sent once the kernel has finished processing
211 // some of the existing messages.
211 // some of the existing messages.
212 if (method=='patch') {
212 if (method=='patch') {
213 if (this.msg_buffer == null) {
213 if (this.msg_buffer == null) {
214 this.msg_buffer = $.extend({}, model_json); // Copy
214 this.msg_buffer = $.extend({}, model_json); // Copy
215 }
215 }
216 for (var attr in options.attrs) {
216 for (var attr in options.attrs) {
217 this.msg_buffer[attr] = options.attrs[attr];
217 this.msg_buffer[attr] = options.attrs[attr];
218 }
218 }
219 } else {
219 } else {
220 this.msg_buffer = $.extend({}, model_json); // Copy
220 this.msg_buffer = $.extend({}, model_json); // Copy
221 }
221 }
222
222
223 } else {
223 } else {
224 // We haven't exceeded the throttle, send the message like
224 // We haven't exceeded the throttle, send the message like
225 // normal. If this is a patch operation, just send the
225 // normal. If this is a patch operation, just send the
226 // changes.
226 // changes.
227 var send_json = model_json;
227 var send_json = model_json;
228 if (method=='patch') {
228 if (method=='patch') {
229 send_json = {};
229 send_json = {};
230 for (var attr in options.attrs) {
230 for (var attr in options.attrs) {
231 send_json[attr] = options.attrs[attr];
231 send_json[attr] = options.attrs[attr];
232 }
232 }
233 }
233 }
234
234
235 var data = {sync_method: method, sync_data: send_json};
235 var data = {sync_method: method, sync_data: send_json};
236
236
237 var cell = null;
237 var cell = null;
238 if (this.last_modified_view != undefined && this.last_modified_view != null) {
238 if (this.last_modified_view != undefined && this.last_modified_view != null) {
239 cell = this.last_modified_view.cell;
239 cell = this.last_modified_view.cell;
240 }
240 }
241
241
242 var callbacks = this._make_callbacks(cell);
242 var callbacks = this._make_callbacks(cell);
243 this.comm.send(data, callbacks);
243 this.comm.send(data, callbacks);
244 this.pending_msgs++;
244 this.pending_msgs++;
245 }
245 }
246 }
246 }
247
247
248 // Since the comm is a one-way communication, assume the message
248 // Since the comm is a one-way communication, assume the message
249 // arrived.
249 // arrived.
250 return model_json;
250 return model_json;
251 },
251 },
252
252
253
253
254 _handle_view_displayed: function(view) {
254 _handle_view_displayed: function(view) {
255 if (this._view_displayed_callback) {
255 if (this._view_displayed_callback) {
256 try {
256 try {
257 this._view_displayed_callback(view)
257 this._view_displayed_callback(view)
258 } catch (e) {
258 } catch (e) {
259 console.log("Exception in widget model view displayed callback", e, view, this);
259 console.log("Exception in widget model view displayed callback", e, view, this);
260 }
260 }
261 }
261 }
262 },
262 },
263
263
264
264
265 _execute_views_method: function (/* method_name, [argument0], [argument1], [...] */) {
265 _execute_views_method: function (/* method_name, [argument0], [argument1], [...] */) {
266 var method_name = arguments[0];
266 var method_name = arguments[0];
267 var args = null;
267 var args = null;
268 if (arguments.length > 1) {
268 if (arguments.length > 1) {
269 args = [].splice.call(arguments,1);
269 args = [].splice.call(arguments,1);
270 }
270 }
271
271
272 for (var cell in this.views) {
272 for (var cell in this.views) {
273 var views = this.views[cell];
273 var views = this.views[cell];
274 for (var view_index in views) {
274 for (var view_index in views) {
275 var view = views[view_index];
275 var view = views[view_index];
276 var method = view[method_name];
276 var method = view[method_name];
277 if (args === null) {
277 if (args === null) {
278 method.apply(view);
278 method.apply(view);
279 } else {
279 } else {
280 method.apply(view, args);
280 method.apply(view, args);
281 }
281 }
282 }
282 }
283 }
283 }
284 },
284 },
285
285
286
286
287 // Create view that represents the model.
287 // Create view that represents the model.
288 _display_view: function (view_name, parent_comm_id, cell) {
288 _display_view: function (view_name, parent_comm_id, cell) {
289 var new_views = [];
289 var new_views = [];
290
290
291 // Try creating and adding the view to it's parent.
291 // Try creating and adding the view to it's parent.
292 var displayed = false;
292 var displayed = false;
293 if (parent_comm_id != undefined) {
293 if (parent_comm_id != undefined) {
294 var parent_comm = this.comm_manager.comms[parent_comm_id];
294 var parent_comm = this.comm_manager.comms[parent_comm_id];
295 var parent_model = parent_comm.model;
295 var parent_model = parent_comm.model;
296 var parent_views = parent_model.views[cell];
296 var parent_views = parent_model.views[cell];
297 for (var parent_view_index in parent_views) {
297 for (var parent_view_index in parent_views) {
298 var parent_view = parent_views[parent_view_index];
298 var parent_view = parent_views[parent_view_index];
299 if (parent_view.display_child != undefined) {
299 if (parent_view.display_child != undefined) {
300 var view = this._create_view(view_name, cell);
300 var view = this._create_view(view_name, cell);
301 if (view != null) {
301 if (view != null) {
302 new_views.push(view);
302 new_views.push(view);
303 parent_view.display_child(view);
303 parent_view.display_child(view);
304 displayed = true;
304 displayed = true;
305 this._handle_view_displayed(view);
305 this._handle_view_displayed(view);
306 }
306 }
307 }
307 }
308 }
308 }
309 }
309 }
310
310
311 // If no parent view is defined or exists. Add the view's
311 // If no parent view is defined or exists. Add the view's
312 // element to cell's widget div.
312 // element to cell's widget div.
313 if (!displayed) {
313 if (!displayed) {
314 var view = this._create_view(view_name, cell);
314 var view = this._create_view(view_name, cell);
315 if (view != null) {
315 if (view != null) {
316 new_views.push(view);
316 new_views.push(view);
317
317
318 if (cell.widget_subarea != undefined && cell.widget_subarea != null) {
318 if (cell.widget_subarea != undefined && cell.widget_subarea != null) {
319 cell.widget_area.show();
319 cell.widget_area.show();
320 cell.widget_subarea.append(view.$el);
320 cell.widget_subarea.append(view.$el);
321 this._handle_view_displayed(view);
321 this._handle_view_displayed(view);
322 }
322 }
323 }
323 }
324 }
324 }
325
325
326 // Force the new view(s) to update their selves
326 // Force the new view(s) to update their selves
327 for (var view_index in new_views) {
327 for (var view_index in new_views) {
328 var view = new_views[view_index];
328 var view = new_views[view_index];
329 view.update();
329 view.update();
330 }
330 }
331 },
331 },
332
332
333
333
334 // Create a view
334 // Create a view
335 _create_view: function (view_name, cell) {
335 _create_view: function (view_name, cell) {
336 var view_type = this.widget_manager.widget_view_types[view_name];
336 var view_type = this.widget_manager.widget_view_types[view_name];
337 if (view_type != undefined && view_type != null) {
337 if (view_type != undefined && view_type != null) {
338 var view = new view_type({model: this});
338 var view = new view_type({model: this});
339 view.render();
339 view.render();
340 if (this.views[cell]==undefined) {
340 if (this.views[cell]==undefined) {
341 this.views[cell] = []
341 this.views[cell] = []
342 }
342 }
343 this.views[cell].push(view);
343 this.views[cell].push(view);
344 view.cell = cell;
344 view.cell = cell;
345
345
346 // Handle when the view element is remove from the page.
346 // Handle when the view element is remove from the page.
347 var that = this;
347 var that = this;
348 view.$el.on("remove", function(){
348 view.$el.on("remove", function(){
349 var index = that.views[cell].indexOf(view);
349 var index = that.views[cell].indexOf(view);
350 if (index > -1) {
350 if (index > -1) {
351 that.views[cell].splice(index, 1);
351 that.views[cell].splice(index, 1);
352 }
352 }
353 view.remove(); // Clean-up view
353 view.remove(); // Clean-up view
354 if (that.views[cell].length()==0) {
354 if (that.views[cell].length()==0) {
355 delete that.views[cell];
355 delete that.views[cell];
356 }
356 }
357
357
358 // Close the comm if there are no views left.
358 // Close the comm if there are no views left.
359 if (that.views.length()==0) {
359 if (that.views.length()==0) {
360 if (that._close_callback) {
360 if (that._close_callback) {
361 try {
361 try {
362 that._close_callback(that)
362 that._close_callback(that)
363 } catch (e) {
363 } catch (e) {
364 console.log("Exception in widget model close callback", e, that);
364 console.log("Exception in widget model close callback", e, that);
365 }
365 }
366 }
366 }
367 that.comm.close();
367 that.comm.close();
368 delete that.comm.model; // Delete ref so GC will collect widget model.
368 delete that.comm.model; // Delete ref so GC will collect widget model.
369 }
369 }
370 });
370 });
371 return view;
371 return view;
372 }
372 }
373 return null;
373 return null;
374 },
374 },
375
375
376
376
377 // Build a callback dict.
377 // Build a callback dict.
378 _make_callbacks: function (cell) {
378 _make_callbacks: function (cell) {
379 var callbacks = {};
379 var callbacks = {};
380 if (cell != null) {
380 if (cell != null) {
381
381
382 // Try to get output handlers
382 // Try to get output handlers
383 var handle_output = null;
383 var handle_output = null;
384 var handle_clear_output = null;
384 var handle_clear_output = null;
385 if (cell.output_area != undefined && cell.output_area != null) {
385 if (cell.output_area != undefined && cell.output_area != null) {
386 handle_output = $.proxy(cell.output_area.handle_output, cell.output_area);
386 handle_output = $.proxy(cell.output_area.handle_output, cell.output_area);
387 handle_clear_output = $.proxy(cell.output_area.handle_clear_output, cell.output_area);
387 handle_clear_output = $.proxy(cell.output_area.handle_clear_output, cell.output_area);
388 }
388 }
389
389
390 // Create callback dict usign what is known
390 // Create callback dict usign what is known
391 var that = this;
391 var that = this;
392 callbacks = {
392 callbacks = {
393 iopub : {
393 iopub : {
394 output : handle_output,
394 output : handle_output,
395 clear_output : handle_clear_output,
395 clear_output : handle_clear_output,
396
396
397 status : function(msg){
397 status : function(msg){
398 that._handle_status(cell, msg);
398 that._handle_status(cell, msg);
399 },
399 },
400
400
401 // Special function only registered by widget messages.
401 // Special function only registered by widget messages.
402 // Allows us to get the cell for a message so we know
402 // Allows us to get the cell for a message so we know
403 // where to add widgets if the code requires it.
403 // where to add widgets if the code requires it.
404 get_cell : function() {
404 get_cell : function() {
405 return cell;
405 return cell;
406 },
406 },
407 },
407 },
408 };
408 };
409 }
409 }
410 return callbacks;
410 return callbacks;
411 },
411 },
412
412
413
413
414 // Get the output area corresponding to the msg_id.
414 // Get the output area corresponding to the msg_id.
415 // cell is an instance of IPython.Cell
415 // cell is an instance of IPython.Cell
416 _get_msg_cell: function (msg_id) {
416 _get_msg_cell: function (msg_id) {
417
417
418 // First, check to see if the msg was triggered by cell execution.
418 // First, check to see if the msg was triggered by cell execution.
419 var cell = this.widget_manager.get_msg_cell(msg_id);
419 var cell = this.widget_manager.get_msg_cell(msg_id);
420 if (cell != null) {
420 if (cell != null) {
421 return cell;
421 return cell;
422 }
422 }
423
423
424 // Second, check to see if a get_cell callback was defined
424 // Second, check to see if a get_cell callback was defined
425 // for the message. get_cell callbacks are registered for
425 // for the message. get_cell callbacks are registered for
426 // widget messages, so this block is actually checking to see if the
426 // widget messages, so this block is actually checking to see if the
427 // message was triggered by a widget.
427 // message was triggered by a widget.
428 var kernel = this.comm_manager.kernel;
428 var kernel = this.comm_manager.kernel;
429 var callbacks = kernel.get_callbacks_for_msg(msg_id);
429 var callbacks = kernel.get_callbacks_for_msg(msg_id);
430 if (callbacks != undefined &&
430 if (callbacks != undefined &&
431 callbacks.iopub != undefined &&
431 callbacks.iopub != undefined &&
432 callbacks.iopub.get_cell != undefined) {
432 callbacks.iopub.get_cell != undefined) {
433
433
434 return callbacks.iopub.get_cell();
434 return callbacks.iopub.get_cell();
435 }
435 }
436
436
437 // Not triggered by a cell or widget (no get_cell callback
437 // Not triggered by a cell or widget (no get_cell callback
438 // exists).
438 // exists).
439 return null;
439 return null;
440 },
440 },
441
441
442 });
442 });
443
443
444
444
445 //--------------------------------------------------------------------
445 //--------------------------------------------------------------------
446 // WidgetView class
446 // WidgetView class
447 //--------------------------------------------------------------------
447 //--------------------------------------------------------------------
448 var WidgetView = Backbone.View.extend({
448 var WidgetView = Backbone.View.extend({
449
449
450 initialize: function() {
450 initialize: function() {
451 this.visible = true;
451 this.visible = true;
452 this.model.on('change',this.update,this);
452 this.model.on('change',this.update,this);
453 },
453 },
454
454
455 add_class: function(selector, class_list){
455 add_class: function(selector, class_list){
456 var elements = this._get_selector_element(selector);
456 var elements = this._get_selector_element(selector);
457 if (elements.length > 0) {
457 if (elements.length > 0) {
458 elements.addClass(class_list);
458 elements.addClass(class_list);
459 }
459 }
460 },
460 },
461
461
462 remove_class: function(selector, class_list){
462 remove_class: function(selector, class_list){
463 var elements = this._get_selector_element(selector);
463 var elements = this._get_selector_element(selector);
464 if (elements.length > 0) {
464 if (elements.length > 0) {
465 elements.removeClass(class_list);
465 elements.removeClass(class_list);
466 }
466 }
467 },
467 },
468
468
469 update: function() {
469 update: function() {
470 if (this.model.get('visible') != undefined) {
470 if (this.model.get('visible') != undefined) {
471 if (this.visible != this.model.get('visible')) {
471 if (this.visible != this.model.get('visible')) {
472 this.visible = this.model.get('visible');
472 this.visible = this.model.get('visible');
473 if (this.visible) {
473 if (this.visible) {
474 this.$el.show();
474 this.$el.show();
475 } else {
475 } else {
476 this.$el.hide();
476 this.$el.hide();
477 }
477 }
478 }
478 }
479 }
479 }
480
480
481 if (this.model.css != undefined) {
481 if (this.model.css != undefined) {
482 for (var selector in this.model.css) {
482 for (var selector in this.model.css) {
483 if (this.model.css.hasOwnProperty(selector)) {
483 if (this.model.css.hasOwnProperty(selector)) {
484
484
485 // Apply the css traits to all elements that match the selector.
485 // Apply the css traits to all elements that match the selector.
486 var elements = this._get_selector_element(selector);
486 var elements = this._get_selector_element(selector);
487 if (elements.length > 0) {
487 if (elements.length > 0) {
488 var css_traits = this.model.css[selector];
488 var css_traits = this.model.css[selector];
489 for (var css_key in css_traits) {
489 for (var css_key in css_traits) {
490 if (css_traits.hasOwnProperty(css_key)) {
490 if (css_traits.hasOwnProperty(css_key)) {
491 elements.css(css_key, css_traits[css_key]);
491 elements.css(css_key, css_traits[css_key]);
492 }
492 }
493 }
493 }
494 }
494 }
495 }
495 }
496 }
496 }
497 }
497 }
498 },
498 },
499
499
500 _get_selector_element: function(selector) {
500 _get_selector_element: function(selector) {
501 // Get the elements via the css selector. If the selector is
501 // Get the elements via the css selector. If the selector is
502 // blank, apply the style to the $el_to_style element. If
502 // blank, apply the style to the $el_to_style element. If
503 // the $el_to_style element is not defined, use apply the
503 // the $el_to_style element is not defined, use apply the
504 // style to the view's element.
504 // style to the view's element.
505 var elements = this.$el.find(selector);
505 var elements = this.$el.find(selector);
506 if (selector===undefined || selector===null || selector=='') {
506 if (selector===undefined || selector===null || selector=='') {
507 if (this.$el_to_style == undefined) {
507 if (this.$el_to_style == undefined) {
508 elements = this.$el;
508 elements = this.$el;
509 } else {
509 } else {
510 elements = this.$el_to_style;
510 elements = this.$el_to_style;
511 }
511 }
512 }
512 }
513 return elements;
513 return elements;
514 },
514 },
515 });
515 });
516
516
517
517
518 //--------------------------------------------------------------------
518 //--------------------------------------------------------------------
519 // WidgetManager class
519 // WidgetManager class
520 //--------------------------------------------------------------------
520 //--------------------------------------------------------------------
521 var WidgetManager = function(){
521 var WidgetManager = function(){
522 this.comm_manager = null;
522 this.comm_manager = null;
523 this.widget_model_types = {};
523 this.widget_model_types = {};
524 this.widget_view_types = {};
524 this.widget_view_types = {};
525
525
526 var that = this;
526 var that = this;
527 Backbone.sync = function(method, model, options, error) {
527 Backbone.sync = function(method, model, options, error) {
528 var result = model._handle_sync(method, options);
528 var result = model._handle_sync(method, options);
529 if (options.success) {
529 if (options.success) {
530 options.success(result);
530 options.success(result);
531 }
531 }
532 };
532 };
533 }
533 }
534
534
535
535
536 WidgetManager.prototype.attach_comm_manager = function (comm_manager) {
536 WidgetManager.prototype.attach_comm_manager = function (comm_manager) {
537 this.comm_manager = comm_manager;
537 this.comm_manager = comm_manager;
538
538
539 // Register already register widget model types with the comm manager.
539 // Register already register widget model types with the comm manager.
540 for (var widget_model_name in this.widget_model_types) {
540 for (var widget_model_name in this.widget_model_types) {
541 this.comm_manager.register_target(widget_model_name, $.proxy(this._handle_com_open, this));
541 this.comm_manager.register_target(widget_model_name, $.proxy(this._handle_com_open, this));
542 }
542 }
543 }
543 }
544
544
545
545
546 WidgetManager.prototype.register_widget_model = function (widget_model_name, widget_model_type) {
546 WidgetManager.prototype.register_widget_model = function (widget_model_name, widget_model_type) {
547 // Register the widget with the comm manager. Make sure to pass this object's context
547 // Register the widget with the comm manager. Make sure to pass this object's context
548 // in so `this` works in the call back.
548 // in so `this` works in the call back.
549 if (this.comm_manager!=null) {
549 if (this.comm_manager!=null) {
550 this.comm_manager.register_target(widget_model_name, $.proxy(this._handle_com_open, this));
550 this.comm_manager.register_target(widget_model_name, $.proxy(this._handle_com_open, this));
551 }
551 }
552 this.widget_model_types[widget_model_name] = widget_model_type;
552 this.widget_model_types[widget_model_name] = widget_model_type;
553 }
553 }
554
554
555
555
556 WidgetManager.prototype.register_widget_view = function (widget_view_name, widget_view_type) {
556 WidgetManager.prototype.register_widget_view = function (widget_view_name, widget_view_type) {
557 this.widget_view_types[widget_view_name] = widget_view_type;
557 this.widget_view_types[widget_view_name] = widget_view_type;
558 }
558 }
559
559
560
560
561 WidgetManager.prototype.get_msg_cell = function (msg_id) {
561 WidgetManager.prototype.get_msg_cell = function (msg_id) {
562 if (IPython.notebook != undefined && IPython.notebook != null) {
562 if (IPython.notebook != undefined && IPython.notebook != null) {
563 return IPython.notebook.get_msg_cell(msg_id);
563 return IPython.notebook.get_msg_cell(msg_id);
564 }
564 }
565 }
565 }
566
566
567
567
568 WidgetManager.prototype.on_create_widget = function (callback) {
568 WidgetManager.prototype.on_create_widget = function (callback) {
569 this._create_widget_callback = callback;
569 this._create_widget_callback = callback;
570 }
570 }
571
571
572
572
573 WidgetManager.prototype._handle_create_widget = function (widget_model) {
573 WidgetManager.prototype._handle_create_widget = function (widget_model) {
574 if (this._create_widget_callback) {
574 if (this._create_widget_callback) {
575 try {
575 try {
576 this._create_widget_callback(widget_model);
576 this._create_widget_callback(widget_model);
577 } catch (e) {
577 } catch (e) {
578 console.log("Exception in WidgetManager callback", e, widget_model);
578 console.log("Exception in WidgetManager callback", e, widget_model);
579 }
579 }
580 }
580 }
581 }
581 }
582
582
583
583
584 WidgetManager.prototype._handle_com_open = function (comm, msg) {
584 WidgetManager.prototype._handle_com_open = function (comm, msg) {
585 var widget_type_name = msg.content.target_name;
585 var widget_type_name = msg.content.target_name;
586 var widget_model = new this.widget_model_types[widget_type_name](this.comm_manager, comm, this);
586 var widget_model = new this.widget_model_types[widget_type_name](this.comm_manager, comm, this);
587 this._handle_create_widget(widget_model);
587 this._handle_create_widget(widget_model);
588 }
588 }
589
589
590
590
591 //--------------------------------------------------------------------
591 //--------------------------------------------------------------------
592 // Init code
592 // Init code
593 //--------------------------------------------------------------------
593 //--------------------------------------------------------------------
594 IPython.WidgetManager = WidgetManager;
594 IPython.WidgetManager = WidgetManager;
595 IPython.WidgetModel = WidgetModel;
595 IPython.WidgetModel = WidgetModel;
596 IPython.WidgetView = WidgetView;
596 IPython.WidgetView = WidgetView;
597
597
598 if (IPython.widget_manager==undefined || IPython.widget_manager==null) {
598 if (IPython.widget_manager==undefined || IPython.widget_manager==null) {
599 IPython.widget_manager = new WidgetManager();
599 IPython.widget_manager = new WidgetManager();
600 }
600 }
601
601
602 return IPython.widget_manager;
602 return IPython.widget_manager;
603 });
603 });
@@ -1,56 +1,60 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 // ButtonWidget
9 // ButtonWidget
10 //============================================================================
10 //============================================================================
11
11
12 /**
12 /**
13 * @module IPython
13 * @module IPython
14 * @namespace IPython
14 * @namespace IPython
15 **/
15 **/
16
16
17 define(["notebook/js/widget"], function(widget_manager){
17 define(["notebook/js/widget"], function(widget_manager){
18
18
19 var ButtonWidgetModel = IPython.WidgetModel.extend({});
19 var ButtonWidgetModel = IPython.WidgetModel.extend({});
20 widget_manager.register_widget_model('ButtonWidgetModel', ButtonWidgetModel);
20 widget_manager.register_widget_model('ButtonWidgetModel', ButtonWidgetModel);
21
21
22 var ButtonView = IPython.WidgetView.extend({
22 var ButtonView = IPython.WidgetView.extend({
23
23
24 // Called when view is rendered.
24 // Called when view is rendered.
25 render : function(){
25 render : function(){
26 var that = this;
26 var that = this;
27 this.setElement($("<button />")
27 this.setElement($("<button />")
28 .addClass('btn')
28 .addClass('btn'));
29 .click(function() {
30 that.model.set('clicks', that.model.get('clicks') + 1);
31 that.model.update_other_views(that);
32 }));
33
29
34 this.update(); // Set defaults.
30 this.update(); // Set defaults.
35 },
31 },
36
32
37 // Handles: Backend -> Frontend Sync
33 // Handles: Backend -> Frontend Sync
38 // Frontent -> Frontend Sync
34 // Frontent -> Frontend Sync
39 update : function(){
35 update : function(){
40 var description = this.model.get('description');
36 var description = this.model.get('description');
41 description = description.replace(/ /g, '&nbsp;', 'm');
37 description = description.replace(/ /g, '&nbsp;', 'm');
42 description = description.replace(/\n/g, '<br>\n', 'm');
38 description = description.replace(/\n/g, '<br>\n', 'm');
43 if (description.length == 0) {
39 if (description.length == 0) {
44 this.$el.html('&nbsp;'); // Preserve button height
40 this.$el.html('&nbsp;'); // Preserve button height
45 } else {
41 } else {
46 this.$el.html(description);
42 this.$el.html(description);
47 }
43 }
48
44
49 return IPython.WidgetView.prototype.update.call(this);
45 return IPython.WidgetView.prototype.update.call(this);
50 },
46 },
47
48 events: {
49 'click': '_handle_click',
50 },
51
51
52 _handle_click: function(){
53 this.model.last_modified_view = this; // For callbacks.
54 this.model.send({event: 'click'});
55 },
52 });
56 });
53
57
54 widget_manager.register_widget_view('ButtonView', ButtonView);
58 widget_manager.register_widget_view('ButtonView', ButtonView);
55
59
56 });
60 });
@@ -1,414 +1,412 b''
1 """Base Widget class. Allows user to create widgets in the backend that render
1 """Base Widget class. Allows user to create widgets in the backend that render
2 in the IPython notebook frontend.
2 in the IPython notebook frontend.
3 """
3 """
4 #-----------------------------------------------------------------------------
4 #-----------------------------------------------------------------------------
5 # Copyright (c) 2013, the IPython Development Team.
5 # Copyright (c) 2013, the IPython Development Team.
6 #
6 #
7 # Distributed under the terms of the Modified BSD License.
7 # Distributed under the terms of the Modified BSD License.
8 #
8 #
9 # The full license is in the file COPYING.txt, distributed with this software.
9 # The full license is in the file COPYING.txt, distributed with this software.
10 #-----------------------------------------------------------------------------
10 #-----------------------------------------------------------------------------
11
11
12 #-----------------------------------------------------------------------------
12 #-----------------------------------------------------------------------------
13 # Imports
13 # Imports
14 #-----------------------------------------------------------------------------
14 #-----------------------------------------------------------------------------
15 from copy import copy
15 from copy import copy
16 from glob import glob
16 from glob import glob
17 import uuid
17 import uuid
18 import sys
18 import sys
19 import os
19 import os
20 import inspect
20 import inspect
21 import types
21 import types
22
22
23 import IPython
23 import IPython
24 from IPython.kernel.comm import Comm
24 from IPython.kernel.comm import Comm
25 from IPython.config import LoggingConfigurable
25 from IPython.config import LoggingConfigurable
26 from IPython.utils.traitlets import Unicode, Dict, List, Instance, Bool
26 from IPython.utils.traitlets import Unicode, Dict, List, Instance, Bool
27 from IPython.display import Javascript, display
27 from IPython.display import Javascript, display
28 from IPython.utils.py3compat import string_types
28 from IPython.utils.py3compat import string_types
29
29
30
31 #-----------------------------------------------------------------------------
30 #-----------------------------------------------------------------------------
32 # Classes
31 # Classes
33 #-----------------------------------------------------------------------------
32 #-----------------------------------------------------------------------------
34 class Widget(LoggingConfigurable):
33 class Widget(LoggingConfigurable):
35
34
36 # Shared declarations
35 # Shared declarations
37 _keys = []
36 _keys = []
38
37
39 # Public declarations
38 # Public declarations
40 target_name = Unicode('widget', help="""Name of the backbone model
39 target_name = Unicode('widget', help="""Name of the backbone model
41 registered in the frontend to create and sync this widget with.""")
40 registered in the frontend to create and sync this widget with.""")
42 default_view_name = Unicode(help="""Default view registered in the frontend
41 default_view_name = Unicode(help="""Default view registered in the frontend
43 to use to represent the widget.""")
42 to use to represent the widget.""")
44 parent = Instance('IPython.html.widgets.widget.Widget')
43 parent = Instance('IPython.html.widgets.widget.Widget')
45 visible = Bool(True, help="Whether or not the widget is visible.")
44 visible = Bool(True, help="Whether or not the widget is visible.")
46
45
47 def _parent_changed(self, name, old, new):
46 def _parent_changed(self, name, old, new):
48 if self._displayed:
47 if self._displayed:
49 raise Exception('Parent cannot be set because widget has been displayed.')
48 raise Exception('Parent cannot be set because widget has been displayed.')
50 elif new == self:
49 elif new == self:
51 raise Exception('Parent cannot be set to self.')
50 raise Exception('Parent cannot be set to self.')
52 else:
51 else:
53
52
54 # Parent/child association
53 # Parent/child association
55 if new is not None and not self in new._children:
54 if new is not None and not self in new._children:
56 new._children.append(self)
55 new._children.append(self)
57 if old is not None and self in old._children:
56 if old is not None and self in old._children:
58 old._children.remove(self)
57 old._children.remove(self)
59
58
60 # Private/protected declarations
59 # Private/protected declarations
61 _property_lock = (None, None) # Last updated (key, value) from the front-end. Prevents echo.
60 _property_lock = (None, None) # Last updated (key, value) from the front-end. Prevents echo.
62 _css = Dict() # Internal CSS property dict
61 _css = Dict() # Internal CSS property dict
63 _displayed = False
62 _displayed = False
64 _comm = None
63 _comm = None
65
64
66
65
67 def __init__(self, **kwargs):
66 def __init__(self, **kwargs):
68 """Public constructor
67 """Public constructor
69
68
70 Parameters
69 Parameters
71 ----------
70 ----------
72 parent : Widget instance (optional)
71 parent : Widget instance (optional)
73 Widget that this widget instance is child of. When the widget is
72 Widget that this widget instance is child of. When the widget is
74 displayed in the frontend, it's corresponding view will be made
73 displayed in the frontend, it's corresponding view will be made
75 child of the parent's view if the parent's view exists already. If
74 child of the parent's view if the parent's view exists already. If
76 the parent's view is displayed, it will automatically display this
75 the parent's view is displayed, it will automatically display this
77 widget's default view as it's child. The default view can be set
76 widget's default view as it's child. The default view can be set
78 via the default_view_name property.
77 via the default_view_name property.
79 """
78 """
80 self._children = []
79 self._children = []
81 self._display_callbacks = []
80 self._display_callbacks = []
82 self._msg_callbacks = []
81 self._msg_callbacks = []
83 super(Widget, self).__init__(**kwargs)
82 super(Widget, self).__init__(**kwargs)
84
83
85 # Register after init to allow default values to be specified
84 # Register after init to allow default values to be specified
86 self.on_trait_change(self._handle_property_changed, self.keys)
85 self.on_trait_change(self._handle_property_changed, self.keys)
87
86
88
87
89 def __del__(self):
88 def __del__(self):
90 """Object disposal"""
89 """Object disposal"""
91 self.close()
90 self.close()
92
91
93
92
94 def close(self):
93 def close(self):
95 """Close method. Closes the widget which closes the underlying comm.
94 """Close method. Closes the widget which closes the underlying comm.
96 When the comm is closed, all of the widget views are automatically
95 When the comm is closed, all of the widget views are automatically
97 removed from the frontend."""
96 removed from the frontend."""
98 try:
97 try:
99 self._comm.close()
98 self._comm.close()
100 del self._comm
99 del self._comm
101 except:
100 except:
102 pass # Comm doesn't exist and/or is already closed.
101 pass # Comm doesn't exist and/or is already closed.
103
102
104
103
105 # Properties
104 # Properties
106 def _get_keys(self):
105 def _get_keys(self):
107 keys = ['visible', '_css']
106 keys = ['visible', '_css']
108 keys.extend(self._keys)
107 keys.extend(self._keys)
109 return keys
108 return keys
110 keys = property(_get_keys)
109 keys = property(_get_keys)
111
110
112
111
113 # Event handlers
112 # Event handlers
114 def _handle_msg(self, msg):
113 def _handle_msg(self, msg):
115 """Called when a msg is recieved from the frontend"""
114 """Called when a msg is recieved from the frontend"""
116 data = msg['content']['data']
115 data = msg['content']['data']
117
116
118 # Handle backbone sync methods CREATE, PATCH, and UPDATE
117 # Handle backbone sync methods CREATE, PATCH, and UPDATE
119 if 'sync_method' in data and 'sync_data' in data:
118 if 'sync_method' in data and 'sync_data' in data:
120 sync_method = data['sync_method']
119 sync_method = data['sync_method']
121 sync_data = data['sync_data']
120 sync_data = data['sync_data']
122 self._handle_recieve_state(sync_data) # handles all methods
121 self._handle_recieve_state(sync_data) # handles all methods
123
122
124 # Handle a custom msg from the front-end
123 # Handle a custom msg from the front-end
125 if 'custom_content' in data:
124 if 'custom_content' in data:
126 self._handle_custom_msg(data['custom_content'])
125 self._handle_custom_msg(data['custom_content'])
127
126
128
127
129 def _handle_custom_msg(self, content):
128 def _handle_custom_msg(self, content):
130 """Called when a custom msg is recieved."""
129 """Called when a custom msg is recieved."""
131 for handler in self._msg_callbacks:
130 for handler in self._msg_callbacks:
132 if callable(handler):
131 if callable(handler):
133 argspec = inspect.getargspec(handler)
132 argspec = inspect.getargspec(handler)
134 nargs = len(argspec[0])
133 nargs = len(argspec[0])
135
134
136 # Bound methods have an additional 'self' argument
135 # Bound methods have an additional 'self' argument
137 if isinstance(handler, types.MethodType):
136 if isinstance(handler, types.MethodType):
138 nargs -= 1
137 nargs -= 1
139
138
140 # Call the callback
139 # Call the callback
141 if nargs == 1:
140 if nargs == 1:
142 handler(content)
141 handler(content)
143 elif nargs == 2:
142 elif nargs == 2:
144 handler(self, content)
143 handler(self, content)
145 else:
144 else:
146 raise TypeError('Widget msg callback must ' \
145 raise TypeError('Widget msg callback must ' \
147 'accept 1 or 2 arguments, not %d.' % nargs)
146 'accept 1 or 2 arguments, not %d.' % nargs)
148
147
149
148
150 def _handle_recieve_state(self, sync_data):
149 def _handle_recieve_state(self, sync_data):
151 """Called when a state is recieved from the frontend."""
150 """Called when a state is recieved from the frontend."""
152 # Use _keys instead of keys - Don't get retrieve the css from the client side.
151 # Use _keys instead of keys - Don't get retrieve the css from the client side.
153 for name in self._keys:
152 for name in self._keys:
154 if name in sync_data:
153 if name in sync_data:
155 try:
154 try:
156 self._property_lock = (name, sync_data[name])
155 self._property_lock = (name, sync_data[name])
157 setattr(self, name, sync_data[name])
156 setattr(self, name, sync_data[name])
158 finally:
157 finally:
159 self._property_lock = (None, None)
158 self._property_lock = (None, None)
160
159
161
160
162 def _handle_property_changed(self, name, old, new):
161 def _handle_property_changed(self, name, old, new):
163 """Called when a proeprty has been changed."""
162 """Called when a proeprty has been changed."""
164 # Make sure this isn't information that the front-end just sent us.
163 # Make sure this isn't information that the front-end just sent us.
165 if self._property_lock[0] != name and self._property_lock[1] != new \
164 if self._property_lock[0] != name and self._property_lock[1] != new \
166 and self._comm is not None:
165 and self._comm is not None:
167 # TODO: Validate properties.
166 # TODO: Validate properties.
168 # Send new state to frontend
167 # Send new state to frontend
169 self.send_state(key=name)
168 self.send_state(key=name)
170
169
171
170
172 def _handle_close(self):
171 def _handle_close(self):
173 """Called when the comm is closed by the frontend."""
172 """Called when the comm is closed by the frontend."""
174 self._comm = None
173 self._comm = None
175
174
176
175
177 def _handle_displayed(self, view_name):
176 def _handle_displayed(self, view_name):
178 """Called when a view has been displayed for this widget instance
177 """Called when a view has been displayed for this widget instance
179
178
180 Parameters
179 Parameters
181 ----------
180 ----------
182 view_name: unicode
181 view_name: unicode
183 Name of the view that was displayed."""
182 Name of the view that was displayed."""
184 for handler in self._display_callbacks:
183 for handler in self._display_callbacks:
185 if callable(handler):
184 if callable(handler):
186 argspec = inspect.getargspec(handler)
185 argspec = inspect.getargspec(handler)
187 nargs = len(argspec[0])
186 nargs = len(argspec[0])
188
187
189 # Bound methods have an additional 'self' argument
188 # Bound methods have an additional 'self' argument
190 if isinstance(handler, types.MethodType):
189 if isinstance(handler, types.MethodType):
191 nargs -= 1
190 nargs -= 1
192
191
193 # Call the callback
192 # Call the callback
194 if nargs == 0:
193 if nargs == 0:
195 handler()
194 handler()
196 elif nargs == 1:
195 elif nargs == 1:
197 handler(self)
196 handler(self)
198 elif nargs == 2:
197 elif nargs == 2:
199 handler(self, view_name)
198 handler(self, view_name)
200 else:
199 else:
201 raise TypeError('Widget display callback must ' \
200 raise TypeError('Widget display callback must ' \
202 'accept 0-2 arguments, not %d.' % nargs)
201 'accept 0-2 arguments, not %d.' % nargs)
203
202
204
203
205 # Public methods
204 # Public methods
206 def send_state(self, key=None):
205 def send_state(self, key=None):
207 """Sends the widget state, or a piece of it, to the frontend.
206 """Sends the widget state, or a piece of it, to the frontend.
208
207
209 Parameters
208 Parameters
210 ----------
209 ----------
211 key : unicode (optional)
210 key : unicode (optional)
212 A single property's name to sync with the frontend.
211 A single property's name to sync with the frontend.
213 """
212 """
214 if self._comm is not None:
213 if self._comm is not None:
215 state = {}
214 state = {}
216
215
217 # If a key is provided, just send the state of that key.
216 # If a key is provided, just send the state of that key.
218 keys = []
217 keys = []
219 if key is None:
218 if key is None:
220 keys.extend(self.keys)
219 keys.extend(self.keys)
221 else:
220 else:
222 keys.append(key)
221 keys.append(key)
223 for key in self.keys:
222 for key in self.keys:
224 try:
223 try:
225 state[key] = getattr(self, key)
224 state[key] = getattr(self, key)
226 except Exception as e:
225 except Exception as e:
227 pass # Eat errors, nom nom nom
226 pass # Eat errors, nom nom nom
228 self._comm.send({"method": "update",
227 self._comm.send({"method": "update",
229 "state": state})
228 "state": state})
230
229
231
230
232 def get_css(self, key, selector=""):
231 def get_css(self, key, selector=""):
233 """Get a CSS property of the widget. Note, this function does not
232 """Get a CSS property of the widget. Note, this function does not
234 actually request the CSS from the front-end; Only properties that have
233 actually request the CSS from the front-end; Only properties that have
235 been set with set_css can be read.
234 been set with set_css can be read.
236
235
237 Parameters
236 Parameters
238 ----------
237 ----------
239 key: unicode
238 key: unicode
240 CSS key
239 CSS key
241 selector: unicode (optional)
240 selector: unicode (optional)
242 JQuery selector used when the CSS key/value was set.
241 JQuery selector used when the CSS key/value was set.
243 """
242 """
244 if selector in self._css and key in self._css[selector]:
243 if selector in self._css and key in self._css[selector]:
245 return self._css[selector][key]
244 return self._css[selector][key]
246 else:
245 else:
247 return None
246 return None
248
247
249
248
250 def set_css(self, *args, **kwargs):
249 def set_css(self, *args, **kwargs):
251 """Set one or more CSS properties of the widget (shared among all of the
250 """Set one or more CSS properties of the widget (shared among all of the
252 views). This function has two signatures:
251 views). This function has two signatures:
253 - set_css(css_dict, [selector=''])
252 - set_css(css_dict, [selector=''])
254 - set_css(key, value, [selector=''])
253 - set_css(key, value, [selector=''])
255
254
256 Parameters
255 Parameters
257 ----------
256 ----------
258 css_dict : dict
257 css_dict : dict
259 CSS key/value pairs to apply
258 CSS key/value pairs to apply
260 key: unicode
259 key: unicode
261 CSS key
260 CSS key
262 value
261 value
263 CSS value
262 CSS value
264 selector: unicode (optional)
263 selector: unicode (optional)
265 JQuery selector to use to apply the CSS key/value.
264 JQuery selector to use to apply the CSS key/value.
266 """
265 """
267 selector = kwargs.get('selector', '')
266 selector = kwargs.get('selector', '')
268
267
269 # Signature 1: set_css(css_dict, [selector=''])
268 # Signature 1: set_css(css_dict, [selector=''])
270 if len(args) == 1:
269 if len(args) == 1:
271 if isinstance(args[0], dict):
270 if isinstance(args[0], dict):
272 for (key, value) in args[0].items():
271 for (key, value) in args[0].items():
273 self.set_css(key, value, selector=selector)
272 self.set_css(key, value, selector=selector)
274 else:
273 else:
275 raise Exception('css_dict must be a dict.')
274 raise Exception('css_dict must be a dict.')
276
275
277 # Signature 2: set_css(key, value, [selector=''])
276 # Signature 2: set_css(key, value, [selector=''])
278 elif len(args) == 2 or len(args) == 3:
277 elif len(args) == 2 or len(args) == 3:
279
278
280 # Selector can be a positional arg if it's the 3rd value
279 # Selector can be a positional arg if it's the 3rd value
281 if len(args) == 3:
280 if len(args) == 3:
282 selector = args[2]
281 selector = args[2]
283 if selector not in self._css:
282 if selector not in self._css:
284 self._css[selector] = {}
283 self._css[selector] = {}
285
284
286 # Only update the property if it has changed.
285 # Only update the property if it has changed.
287 key = args[0]
286 key = args[0]
288 value = args[1]
287 value = args[1]
289 if not (key in self._css[selector] and value in self._css[selector][key]):
288 if not (key in self._css[selector] and value in self._css[selector][key]):
290 self._css[selector][key] = value
289 self._css[selector][key] = value
291 self.send_state('_css') # Send new state to client.
290 self.send_state('_css') # Send new state to client.
292 else:
291 else:
293 raise Exception('set_css only accepts 1-3 arguments')
292 raise Exception('set_css only accepts 1-3 arguments')
294
293
295
294
296 def add_class(self, class_name, selector=""):
295 def add_class(self, class_name, selector=""):
297 """Add class[es] to a DOM element
296 """Add class[es] to a DOM element
298
297
299 Parameters
298 Parameters
300 ----------
299 ----------
301 class_name: unicode
300 class_name: unicode
302 Class name(s) to add to the DOM element(s). Multiple class names
301 Class name(s) to add to the DOM element(s). Multiple class names
303 must be space separated.
302 must be space separated.
304 selector: unicode (optional)
303 selector: unicode (optional)
305 JQuery selector to select the DOM element(s) that the class(es) will
304 JQuery selector to select the DOM element(s) that the class(es) will
306 be added to.
305 be added to.
307 """
306 """
308 self._comm.send({"method": "add_class",
307 self._comm.send({"method": "add_class",
309 "class_list": class_name,
308 "class_list": class_name,
310 "selector": selector})
309 "selector": selector})
311
310
312
311
313 def remove_class(self, class_name, selector=""):
312 def remove_class(self, class_name, selector=""):
314 """Remove class[es] from a DOM element
313 """Remove class[es] from a DOM element
315
314
316 Parameters
315 Parameters
317 ----------
316 ----------
318 class_name: unicode
317 class_name: unicode
319 Class name(s) to remove from the DOM element(s). Multiple class
318 Class name(s) to remove from the DOM element(s). Multiple class
320 names must be space separated.
319 names must be space separated.
321 selector: unicode (optional)
320 selector: unicode (optional)
322 JQuery selector to select the DOM element(s) that the class(es) will
321 JQuery selector to select the DOM element(s) that the class(es) will
323 be removed from.
322 be removed from.
324 """
323 """
325 self._comm.send({"method": "remove_class",
324 self._comm.send({"method": "remove_class",
326 "class_list": class_name,
325 "class_list": class_name,
327 "selector": selector})
326 "selector": selector})
328
327
329
328
330 def send(self, content):
329 def send(self, content):
331 """Sends a custom msg to the widget model in the front-end.
330 """Sends a custom msg to the widget model in the front-end.
332
331
333 Parameters
332 Parameters
334 ----------
333 ----------
335 content : dict
334 content : dict
336 Content of the message to send.
335 Content of the message to send.
337 """
336 """
338 if self._comm is not None:
337 if self._comm is not None:
339 self._comm.send({"method": "custom",
338 self._comm.send({"method": "custom",
340 "custom_content": content})
339 "custom_content": content})
341
340
342
341
343 def on_msg(self, callback, remove=False):
342 def on_msg(self, callback, remove=False):
344 """Register a callback for when a custom msg is recieved from the front-end
343 """Register a callback for when a custom msg is recieved from the front-end
345
344
346 Parameters
345 Parameters
347 ----------
346 ----------
348 callback: method handler
347 callback: method handler
349 Can have a signature of:
348 Can have a signature of:
350 - callback(content)
349 - callback(content)
351 - callback(sender, content)
350 - callback(sender, content)
352 remove: bool
351 remove: bool
353 True if the callback should be unregistered."""
352 True if the callback should be unregistered."""
354 if remove and callback in self._msg_callbacks:
353 if remove and callback in self._msg_callbacks:
355 self._msg_callbacks.remove(callback)
354 self._msg_callbacks.remove(callback)
356 elif not remove and not callback in self._msg_callbacks:
355 elif not remove and not callback in self._msg_callbacks:
357 self._msg_callbacks.append(callback)
356 self._msg_callbacks.append(callback)
358
357
359
358
360 def on_displayed(self, callback, remove=False):
359 def on_displayed(self, callback, remove=False):
361 """Register a callback to be called when the widget has been displayed
360 """Register a callback to be called when the widget has been displayed
362
361
363 Parameters
362 Parameters
364 ----------
363 ----------
365 callback: method handler
364 callback: method handler
366 Can have a signature of:
365 Can have a signature of:
367 - callback()
366 - callback()
368 - callback(sender)
367 - callback(sender)
369 - callback(sender, view_name)
368 - callback(sender, view_name)
370 remove: bool
369 remove: bool
371 True if the callback should be unregistered."""
370 True if the callback should be unregistered."""
372 if remove and callback in self._display_callbacks:
371 if remove and callback in self._display_callbacks:
373 self._display_callbacks.remove(callback)
372 self._display_callbacks.remove(callback)
374 elif not remove and not callback in self._display_callbacks:
373 elif not remove and not callback in self._display_callbacks:
375 self._display_callbacks.append(callback)
374 self._display_callbacks.append(callback)
376
375
377
376
378 # Support methods
377 # Support methods
379 def _repr_widget_(self, view_name=None):
378 def _repr_widget_(self, view_name=None):
380 """Function that is called when `IPython.display.display` is called on
379 """Function that is called when `IPython.display.display` is called on
381 the widget.
380 the widget.
382
381
383 Parameters
382 Parameters
384 ----------
383 ----------
385 view_name: unicode (optional)
384 view_name: unicode (optional)
386 View to display in the frontend. Overrides default_view_name."""
385 View to display in the frontend. Overrides default_view_name."""
387
388 if not view_name:
386 if not view_name:
389 view_name = self.default_view_name
387 view_name = self.default_view_name
390
388
391 # Create a comm.
389 # Create a comm.
392 if self._comm is None:
390 if self._comm is None:
393 self._comm = Comm(target_name=self.target_name)
391 self._comm = Comm(target_name=self.target_name)
394 self._comm.on_msg(self._handle_msg)
392 self._comm.on_msg(self._handle_msg)
395 self._comm.on_close(self._handle_close)
393 self._comm.on_close(self._handle_close)
396
394
397 # Make sure model is syncronized
395 # Make sure model is syncronized
398 self.send_state()
396 self.send_state()
399
397
400 # Show view.
398 # Show view.
401 if self.parent is None or self.parent._comm is None:
399 if self.parent is None or self.parent._comm is None:
402 self._comm.send({"method": "display", "view_name": view_name})
400 self._comm.send({"method": "display", "view_name": view_name})
403 else:
401 else:
404 self._comm.send({"method": "display",
402 self._comm.send({"method": "display",
405 "view_name": view_name,
403 "view_name": view_name,
406 "parent": self.parent._comm.comm_id})
404 "parent": self.parent._comm.comm_id})
407 self._displayed = True
405 self._displayed = True
408 self._handle_displayed(view_name)
406 self._handle_displayed(view_name)
409
407
410 # Now display children if any.
408 # Now display children if any.
411 for child in self._children:
409 for child in self._children:
412 if child != self:
410 if child != self:
413 child._repr_widget_()
411 child._repr_widget_()
414 return None
412 return None
@@ -1,80 +1,93 b''
1 """ButtonWidget class.
1 """ButtonWidget class.
2
2
3 Represents a button in the frontend using a widget. Allows user to listen for
3 Represents a button in the frontend using a widget. Allows user to listen for
4 click events on the button and trigger backend code when the clicks are fired.
4 click events on the button and trigger backend code when the clicks are fired.
5 """
5 """
6 #-----------------------------------------------------------------------------
6 #-----------------------------------------------------------------------------
7 # Copyright (c) 2013, the IPython Development Team.
7 # Copyright (c) 2013, the IPython Development Team.
8 #
8 #
9 # Distributed under the terms of the Modified BSD License.
9 # Distributed under the terms of the Modified BSD License.
10 #
10 #
11 # The full license is in the file COPYING.txt, distributed with this software.
11 # The full license is in the file COPYING.txt, distributed with this software.
12 #-----------------------------------------------------------------------------
12 #-----------------------------------------------------------------------------
13
13
14 #-----------------------------------------------------------------------------
14 #-----------------------------------------------------------------------------
15 # Imports
15 # Imports
16 #-----------------------------------------------------------------------------
16 #-----------------------------------------------------------------------------
17 import inspect
17 import inspect
18 import types
18 import types
19
19
20 from .widget import Widget
20 from .widget import Widget
21 from IPython.utils.traitlets import Unicode, Bool, Int
21 from IPython.utils.traitlets import Unicode, Bool, Int
22
22
23 #-----------------------------------------------------------------------------
23 #-----------------------------------------------------------------------------
24 # Classes
24 # Classes
25 #-----------------------------------------------------------------------------
25 #-----------------------------------------------------------------------------
26 class ButtonWidget(Widget):
26 class ButtonWidget(Widget):
27 target_name = Unicode('ButtonWidgetModel')
27 target_name = Unicode('ButtonWidgetModel')
28 default_view_name = Unicode('ButtonView')
28 default_view_name = Unicode('ButtonView')
29
29
30 # Keys
30 # Keys
31 _keys = ['clicks', 'description', 'disabled']
31 _keys = ['description', 'disabled']
32 clicks = Int(0, help="Number of times the button has been clicked.")
33 description = Unicode('', help="Description of the button (label).")
32 description = Unicode('', help="Description of the button (label).")
34 disabled = Bool(False, help="Enable or disable user changes.")
33 disabled = Bool(False, help="Enable or disable user changes.")
35
34
36
35
37 def __init__(self, **kwargs):
36 def __init__(self, **kwargs):
38 self._click_handlers = []
39 super(ButtonWidget, self).__init__(**kwargs)
37 super(ButtonWidget, self).__init__(**kwargs)
38
39 self._click_handlers = []
40 self.on_msg(self._handle_button_msg)
40
41
41
42
42 def on_click(self, callback, remove=False):
43 def on_click(self, callback, remove=False):
43 """Register a callback to execute when the button is clicked. The
44 """Register a callback to execute when the button is clicked. The
44 callback can either accept no parameters or one sender parameter:
45 callback can either accept no parameters or one sender parameter:
45 - callback()
46 - callback()
46 - callback(sender)
47 - callback(sender)
47 If the callback has a sender parameter, the ButtonWidget instance that
48 If the callback has a sender parameter, the ButtonWidget instance that
48 called the callback will be passed into the method as the sender.
49 called the callback will be passed into the method as the sender.
49
50
50 Parameters
51 Parameters
51 ----------
52 ----------
52 remove : bool (optional)
53 remove : bool (optional)
53 Set to true to remove the callback from the list of callbacks."""
54 Set to true to remove the callback from the list of callbacks."""
54 if remove:
55 if remove:
55 self._click_handlers.remove(callback)
56 self._click_handlers.remove(callback)
56 elif not callback in self._click_handlers:
57 elif not callback in self._click_handlers:
57 self._click_handlers.append(callback)
58 self._click_handlers.append(callback)
58
59
59
60
60 def _clicks_changed(self, name, old, new):
61 def _handle_button_msg(self, content):
61 """Handles when the clicks property has been changed. Fires on_click
62 """Hanlde a msg from the front-end
63
64 Parameters
65 ----------
66 content: dict
67 Content of the msg."""
68 if 'event' in content and content['event'] == 'click':
69 self._handle_click()
70
71
72 def _handle_click(self):
73 """Handles when the button has been clicked. Fires on_click
62 callbacks when appropriate."""
74 callbacks when appropriate."""
63 if new > old:
75
64 for handler in self._click_handlers:
76 for handler in self._click_handlers:
65 if callable(handler):
77 if callable(handler):
66 argspec = inspect.getargspec(handler)
78 argspec = inspect.getargspec(handler)
67 nargs = len(argspec[0])
79 nargs = len(argspec[0])
68
80
69 # Bound methods have an additional 'self' argument
81 # Bound methods have an additional 'self' argument
70 if isinstance(handler, types.MethodType):
82 if isinstance(handler, types.MethodType):
71 nargs -= 1
83 nargs -= 1
72
84
73 # Call the callback
85 # Call the callback
74 if nargs == 0:
86 if nargs == 0:
75 handler()
87 handler()
76 elif nargs == 1:
88 elif nargs == 1:
77 handler(self)
89 handler(self)
78 else:
90 else:
79 raise TypeError('ButtonWidget click callback must ' \
91 raise TypeError('ButtonWidget click callback must ' \
80 'accept 0 or 1 arguments.')
92 'accept 0 or 1 arguments.')
93
General Comments 0
You need to be logged in to leave comments. Login now