##// END OF EJS Templates
Re-decoupled comm_id from widget models
Jonathan Frederic -
Show More
@@ -1,208 +1,210 b''
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2013 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // WidgetModel, WidgetView, and WidgetManager
10 10 //============================================================================
11 11 /**
12 12 * Base Widget classes
13 13 * @module IPython
14 14 * @namespace IPython
15 15 * @submodule widget
16 16 */
17 17
18 18 (function () {
19 19 "use strict";
20 20
21 21 // Use require.js 'define' method so that require.js is intelligent enough to
22 22 // syncronously load everything within this file when it is being 'required'
23 23 // elsewhere.
24 24 define(["underscore",
25 25 "backbone",
26 26 ], function (underscore, backbone) {
27 27
28 28 //--------------------------------------------------------------------
29 29 // WidgetManager class
30 30 //--------------------------------------------------------------------
31 31 var WidgetManager = function () {
32 32 this.comm_manager = null;
33 33 this.widget_model_types = {};
34 34 this.widget_view_types = {};
35 35 this._model_instances = {};
36 36
37 37 Backbone.sync = function (method, model, options, error) {
38 38 var result = model._handle_sync(method, options);
39 39 if (options.success) {
40 40 options.success(result);
41 41 }
42 42 };
43 43 };
44 44
45 45
46 46 WidgetManager.prototype.attach_comm_manager = function (comm_manager) {
47 47 this.comm_manager = comm_manager;
48 48
49 49 // Register already-registered widget model types with the comm manager.
50 50 for (var widget_model_name in this.widget_model_types) {
51 51 this.comm_manager.register_target(widget_model_name, $.proxy(this._handle_comm_open, this));
52 52 }
53 53 };
54 54
55 55
56 56 WidgetManager.prototype.register_widget_model = function (widget_model_name, widget_model_type) {
57 57 // Register the widget with the comm manager. Make sure to pass this object's context
58 58 // in so `this` works in the call back.
59 59 if (this.comm_manager !== null) {
60 60 this.comm_manager.register_target(widget_model_name, $.proxy(this._handle_comm_open, this));
61 61 }
62 62 this.widget_model_types[widget_model_name] = widget_model_type;
63 63 };
64 64
65 65
66 66 WidgetManager.prototype.register_widget_view = function (widget_view_name, widget_view_type) {
67 67 this.widget_view_types[widget_view_name] = widget_view_type;
68 68 };
69
70
69 71 WidgetManager.prototype.handle_msg = function(msg, model) {
70 72 var method = msg.content.data.method;
71 73 switch (method) {
72 74 case 'display':
73 75 var cell = this.get_msg_cell(msg.parent_header.msg_id);
74 76 if (cell === null) {
75 77 console.log("Could not determine where the display" +
76 78 " message was from. Widget will not be displayed");
77 79 } else {
78 80 var view = this.create_view(model,
79 81 msg.content.data.view_name, cell);
80 82 if (view !== undefined
81 83 && cell.widget_subarea !== undefined
82 84 && cell.widget_subarea !== null) {
83 85 cell.widget_area.show();
84 86 cell.widget_subarea.append(view.$el);
85 87 }
86 88 }
87 89 break;
88 90 }
89 91 }
90 92
91 93 WidgetManager.prototype.create_view = function(model, view_name, cell) {
92 94 view_name = view_name || model.get('default_view_name');
93 95 var ViewType = this.widget_view_types[view_name];
94 96 if (ViewType !== undefined && ViewType !== null) {
95 97 var view = new ViewType({model: model, widget_manager: this, cell: cell});
96 98 view.render();
97 99 model.views.push(view);
98 100 model.on('destroy', view.remove, view);
99 101 /*
100 102 // TODO: handle view deletion. Don't forget to delete child views
101 103 var that = this;
102 104 view.$el.on("remove", function () {
103 105 var index = that.views.indexOf(view);
104 106 if (index > -1) {
105 107 that.views.splice(index, 1);
106 108 }
107 109 view.remove(); // Clean-up view
108 110
109 111 // Close the comm if there are no views left.
110 112 if (that.views.length() === 0) {
111 113 //trigger comm close event?
112 114 }
113 115
114 116
115 117 if (that.comm !== undefined) {
116 118 that.comm.close();
117 119 delete that.comm.model; // Delete ref so GC will collect widget model.
118 120 delete that.comm;
119 121 }
120 delete that.widget_id; // Delete id from model so widget manager cleans up.
122 delete that.model_id; // Delete id from model so widget manager cleans up.
121 123 });
122 124 */
123 125 return view;
124 126 }
125 127 },
126 128
127 129 WidgetManager.prototype.get_msg_cell = function (msg_id) {
128 130 var cell = null;
129 131 // First, check to see if the msg was triggered by cell execution.
130 132 if (IPython.notebook !== undefined && IPython.notebook !== null) {
131 133 cell = IPython.notebook.get_msg_cell(msg_id);
132 134 }
133 135 if (cell !== null) {
134 136 return cell
135 137 }
136 138 // Second, check to see if a get_cell callback was defined
137 139 // for the message. get_cell callbacks are registered for
138 140 // widget messages, so this block is actually checking to see if the
139 141 // message was triggered by a widget.
140 142 var kernel = this.get_kernel();
141 143 if (kernel !== undefined && kernel !== null) {
142 144 var callbacks = kernel.get_callbacks_for_msg(msg_id);
143 145 if (callbacks !== undefined &&
144 146 callbacks.iopub !== undefined &&
145 147 callbacks.iopub.get_cell !== undefined) {
146 148
147 149 return callbacks.iopub.get_cell();
148 150 }
149 151 }
150 152
151 153 // Not triggered by a cell or widget (no get_cell callback
152 154 // exists).
153 155 return null;
154 156 };
155 157
156 158
157 WidgetManager.prototype.get_model = function (widget_id) {
158 var model = this._model_instances[widget_id];
159 if (model !== undefined && model.id == widget_id) {
159 WidgetManager.prototype.get_model = function (model_id) {
160 var model = this._model_instances[model_id];
161 if (model !== undefined && model.id == model_id) {
160 162 return model;
161 163 }
162 164 return null;
163 165 };
164 166
165 167
166 168 WidgetManager.prototype.get_kernel = function () {
167 169 if (this.comm_manager === null) {
168 170 return null;
169 171 } else {
170 172 return this.comm_manager.kernel;
171 173 }
172 174 };
173 175
174 176
175 177 WidgetManager.prototype.on_create_widget = function (callback) {
176 178 this._create_widget_callback = callback;
177 179 };
178 180
179 181
180 182 WidgetManager.prototype._handle_create_widget = function (widget_model) {
181 183 if (this._create_widget_callback) {
182 184 try {
183 185 this._create_widget_callback(widget_model);
184 186 } catch (e) {
185 187 console.log("Exception in WidgetManager callback", e, widget_model);
186 188 }
187 189 }
188 190 };
189 191
190 192
191 193 WidgetManager.prototype._handle_comm_open = function (comm, msg) {
192 194 var widget_type_name = msg.content.target_name;
193 195 var widget_model = new this.widget_model_types[widget_type_name](this, comm.comm_id, comm);
194 this._model_instances[comm.comm_id] = widget_model;
196 this._model_instances[comm.comm_id] = widget_model; // comm_id == model_id
195 197 this._handle_create_widget(widget_model);
196 198 };
197 199
198 200 //--------------------------------------------------------------------
199 201 // Init code
200 202 //--------------------------------------------------------------------
201 203 IPython.WidgetManager = WidgetManager;
202 204 if (IPython.widget_manager === undefined || IPython.widget_manager === null) {
203 205 IPython.widget_manager = new WidgetManager();
204 206 }
205 207
206 208 return IPython.widget_manager;
207 209 });
208 210 }());
@@ -1,345 +1,345 b''
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2013 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // Base Widget Model and View classes
10 10 //============================================================================
11 11
12 12 /**
13 13 * @module IPython
14 14 * @namespace IPython
15 15 **/
16 16
17 17 define(["notebook/js/widgetmanager",
18 18 "underscore",
19 19 "backbone"],
20 20 function(widget_manager, underscore, backbone){
21 21
22 22 //--------------------------------------------------------------------
23 23 // WidgetModel class
24 24 //--------------------------------------------------------------------
25 25 var WidgetModel = Backbone.Model.extend({
26 constructor: function (widget_manager, widget_id, comm) {
26 constructor: function (widget_manager, model_id, comm) {
27 27 this.widget_manager = widget_manager;
28 28 this.pending_msgs = 0;
29 29 this.msg_throttle = 3;
30 30 this.msg_buffer = null;
31 this.id = widget_id;
31 this.id = model_id;
32 32 this.views = [];
33 33
34 34 if (comm !== undefined) {
35 35 // Remember comm associated with the model.
36 36 this.comm = comm;
37 37 comm.model = this;
38 38
39 39 // Hook comm messages up to model.
40 40 comm.on_close($.proxy(this._handle_comm_closed, this));
41 41 comm.on_msg($.proxy(this._handle_comm_msg, this));
42 42 }
43 43 return Backbone.Model.apply(this);
44 44 },
45 45
46 46 send: function (content, callbacks) {
47 47 if (this.comm !== undefined) {
48 48 var data = {method: 'custom', custom_content: content};
49 49 this.comm.send(data, callbacks);
50 50 }
51 51 },
52 52
53 53 // Handle when a widget is closed.
54 54 _handle_comm_closed: function (msg) {
55 55 this.trigger('comm:close');
56 56 delete this.comm.model; // Delete ref so GC will collect widget model.
57 57 delete this.comm;
58 delete this.widget_id; // Delete id from model so widget manager cleans up.
58 delete this.model_id; // Delete id from model so widget manager cleans up.
59 59 // TODO: Handle deletion, like this.destroy(), and delete views, etc.
60 60 },
61 61
62 62
63 63 // Handle incoming comm msg.
64 64 _handle_comm_msg: function (msg) {
65 65 var method = msg.content.data.method;
66 66 switch (method) {
67 67 case 'update':
68 68 this.apply_update(msg.content.data.state);
69 69 break;
70 70 case 'custom':
71 71 this.trigger('msg:custom', msg.content.data.custom_content);
72 72 break;
73 73 default:
74 74 // pass on to widget manager
75 75 this.widget_manager.handle_msg(msg, this);
76 76 }
77 77 },
78 78
79 79
80 80 // Handle when a widget is updated via the python side.
81 81 apply_update: function (state) {
82 82 this.updating = true;
83 83 try {
84 84 for (var key in state) {
85 85 if (state.hasOwnProperty(key)) {
86 86 this.set(key, state[key]);
87 87 }
88 88 }
89 89 //TODO: are there callbacks that make sense in this case? If so, attach them here as an option
90 90 this.save();
91 91 } finally {
92 92 this.updating = false;
93 93 }
94 94 },
95 95
96 96
97 97 _handle_status: function (msg, callbacks) {
98 98 //execution_state : ('busy', 'idle', 'starting')
99 99 if (this.comm !== undefined && msg.content.execution_state ==='idle') {
100 100 // Send buffer if this message caused another message to be
101 101 // throttled.
102 102 if (this.msg_buffer !== null &&
103 103 this.msg_throttle === this.pending_msgs) {
104 104 var data = {method: 'backbone', sync_method: 'update', sync_data: this.msg_buffer};
105 105 this.comm.send(data, callbacks);
106 106 this.msg_buffer = null;
107 107 } else {
108 108 // Only decrease the pending message count if the buffer
109 109 // doesn't get flushed (sent).
110 110 --this.pending_msgs;
111 111 }
112 112 }
113 113 },
114 114
115 115
116 116 // Custom syncronization logic.
117 117 _handle_sync: function (method, options) {
118 118 var model_json = this.toJSON();
119 119 var attr;
120 120
121 121 // Only send updated state if the state hasn't been changed
122 122 // during an update.
123 123 if (this.comm !== undefined) {
124 124 if (!this.updating) {
125 125 if (this.pending_msgs >= this.msg_throttle) {
126 126 // The throttle has been exceeded, buffer the current msg so
127 127 // it can be sent once the kernel has finished processing
128 128 // some of the existing messages.
129 129 if (method=='patch') {
130 130 if (this.msg_buffer === null) {
131 131 this.msg_buffer = $.extend({}, model_json); // Copy
132 132 }
133 133 for (attr in options.attrs) {
134 134 this.msg_buffer[attr] = options.attrs[attr];
135 135 }
136 136 } else {
137 137 this.msg_buffer = $.extend({}, model_json); // Copy
138 138 }
139 139
140 140 } else {
141 141 // We haven't exceeded the throttle, send the message like
142 142 // normal. If this is a patch operation, just send the
143 143 // changes.
144 144 var send_json = model_json;
145 145 if (method =='patch') {
146 146 send_json = {};
147 147 for (attr in options.attrs) {
148 148 send_json[attr] = options.attrs[attr];
149 149 }
150 150 }
151 151
152 152 var data = {method: 'backbone', sync_method: method, sync_data: send_json};
153 153 this.comm.send(data, options.callbacks);
154 154 this.pending_msgs++;
155 155 }
156 156 }
157 157 }
158 158
159 159 // Since the comm is a one-way communication, assume the message
160 160 // arrived.
161 161 return model_json;
162 162 },
163 163
164 164 });
165 165
166 166
167 167 //--------------------------------------------------------------------
168 168 // WidgetView class
169 169 //--------------------------------------------------------------------
170 170 var BaseWidgetView = Backbone.View.extend({
171 171 initialize: function(options) {
172 172 this.model.on('change',this.update,this);
173 173 this.widget_manager = options.widget_manager;
174 174 this.comm_manager = options.widget_manager.comm_manager;
175 175 this.cell = options.cell;
176 176 this.child_views = [];
177 177 },
178 178
179 179 update: function(){
180 180 // update view to be consistent with this.model
181 181 // triggered on model change
182 182 },
183 183
184 child_view: function(comm_id, view_name) {
184 child_view: function(model_id, view_name) {
185 185 // create and return a child view, given a comm id for a model and (optionally) a view name
186 186 // if the view name is not given, it defaults to the model's default view attribute
187 var child_model = this.comm_manager.comms[comm_id].model;
187 var child_model = this.widget_manager.get_model(model_id);
188 188 var child_view = this.widget_manager.create_view(child_model, view_name, this.cell);
189 this.child_views[comm_id] = child_view;
189 this.child_views[model_id] = child_view;
190 190 return child_view;
191 191 },
192 192
193 193 update_child_views: function(old_list, new_list) {
194 194 // this function takes an old list and new list of comm ids
195 195 // views in child_views that correspond to deleted ids are deleted
196 196 // views corresponding to added ids are added child_views
197 197
198 198 // delete old views
199 199 _.each(_.difference(old_list, new_list), function(element, index, list) {
200 200 var view = this.child_views[element];
201 201 delete this.child_views[element];
202 202 view.remove();
203 203 }, this);
204 204
205 205 // add new views
206 206 _.each(_.difference(new_list, old_list), function(element, index, list) {
207 207 // this function adds the view to the child_views dictionary
208 208 this.child_view(element);
209 209 }, this);
210 210 },
211 211
212 212
213 213
214 214 render: function(){
215 215 // render the view. By default, this is only called the first time the view is created
216 216 },
217 217 send: function (content) {
218 218 this.model.send(content, this._callbacks());
219 219 },
220 220
221 221 touch: function () {
222 222 this.model.save(this.model.changedAttributes(), {patch: true, callbacks: this._callbacks()});
223 223 },
224 224
225 225 _callbacks: function () {
226 226 // callback handlers specific to this view's cell
227 227 var callbacks = {};
228 228 var cell = this.cell;
229 229 if (cell !== null) {
230 230 // Try to get output handlers
231 231 var handle_output = null;
232 232 var handle_clear_output = null;
233 233 if (cell.output_area !== undefined && cell.output_area !== null) {
234 234 handle_output = $.proxy(cell.output_area.handle_output, cell.output_area);
235 235 handle_clear_output = $.proxy(cell.output_area.handle_clear_output, cell.output_area);
236 236 }
237 237
238 238 // Create callback dict using what is known
239 239 var that = this;
240 240 callbacks = {
241 241 iopub : {
242 242 output : handle_output,
243 243 clear_output : handle_clear_output,
244 244
245 245 status : function (msg) {
246 246 that.model._handle_status(msg, that._callbacks());
247 247 },
248 248
249 249 // Special function only registered by widget messages.
250 250 // Allows us to get the cell for a message so we know
251 251 // where to add widgets if the code requires it.
252 252 get_cell : function () {
253 253 return cell;
254 254 },
255 255 },
256 256 };
257 257 }
258 258 return callbacks;
259 259 },
260 260
261 261 });
262 262
263 263 var WidgetView = BaseWidgetView.extend({
264 264 initialize: function (options) {
265 265 // TODO: make changes more granular (e.g., trigger on visible:change)
266 266 this.model.on('change', this.update, this);
267 267 this.model.on('msg:custom', this.on_msg, this);
268 268 BaseWidgetView.prototype.initialize.apply(this, arguments);
269 269 },
270 270
271 271 on_msg: function(msg) {
272 272 switch(msg.msg_type) {
273 273 case 'add_class':
274 274 this.add_class(msg.selector, msg.class_list);
275 275 break;
276 276 case 'remove_class':
277 277 this.remove_class(msg.selector, msg.class_list);
278 278 break;
279 279 }
280 280 },
281 281
282 282 add_class: function (selector, class_list) {
283 283 var elements = this._get_selector_element(selector);
284 284 if (elements.length > 0) {
285 285 elements.addClass(class_list);
286 286 }
287 287 },
288 288
289 289 remove_class: function (selector, class_list) {
290 290 var elements = this._get_selector_element(selector);
291 291 if (elements.length > 0) {
292 292 elements.removeClass(class_list);
293 293 }
294 294 },
295 295
296 296 update: function () {
297 297 // the very first update seems to happen before the element is finished rendering
298 298 // so we use setTimeout to give the element time to render
299 299 var e = this.$el;
300 300 var visible = this.model.get('visible');
301 301 setTimeout(function() {e.toggle(visible)},0);
302 302
303 303 var css = this.model.get('_css');
304 304 if (css === undefined) {return;}
305 305 for (var selector in css) {
306 306 if (css.hasOwnProperty(selector)) {
307 307 // Apply the css traits to all elements that match the selector.
308 308 var elements = this._get_selector_element(selector);
309 309 if (elements.length > 0) {
310 310 var css_traits = css[selector];
311 311 for (var css_key in css_traits) {
312 312 if (css_traits.hasOwnProperty(css_key)) {
313 313 elements.css(css_key, css_traits[css_key]);
314 314 }
315 315 }
316 316 }
317 317 }
318 318 }
319 319 },
320 320
321 321 _get_selector_element: function (selector) {
322 322 // Get the elements via the css selector. If the selector is
323 323 // blank, apply the style to the $el_to_style element. If
324 324 // the $el_to_style element is not defined, use apply the
325 325 // style to the view's element.
326 326 var elements;
327 327 if (selector === undefined || selector === null || selector === '') {
328 328 if (this.$el_to_style === undefined) {
329 329 elements = this.$el;
330 330 } else {
331 331 elements = this.$el_to_style;
332 332 }
333 333 } else {
334 334 elements = this.$el.find(selector);
335 335 }
336 336 return elements;
337 337 },
338 338 });
339 339
340 340 IPython.WidgetModel = WidgetModel;
341 341 IPython.WidgetView = WidgetView;
342 342 IPython.BaseWidgetView = BaseWidgetView;
343 343
344 344 return widget_manager;
345 345 });
@@ -1,446 +1,450 b''
1 1 """Base Widget class. Allows user to create widgets in the backend that render
2 2 in the IPython notebook frontend.
3 3 """
4 4 #-----------------------------------------------------------------------------
5 5 # Copyright (c) 2013, the IPython Development Team.
6 6 #
7 7 # Distributed under the terms of the Modified BSD License.
8 8 #
9 9 # The full license is in the file COPYING.txt, distributed with this software.
10 10 #-----------------------------------------------------------------------------
11 11
12 12 #-----------------------------------------------------------------------------
13 13 # Imports
14 14 #-----------------------------------------------------------------------------
15 15 from copy import copy
16 16 from glob import glob
17 17 import uuid
18 18 import sys
19 19 import os
20 20 import inspect
21 21 import types
22 22
23 23 import IPython
24 24 from IPython.kernel.comm import Comm
25 25 from IPython.config import LoggingConfigurable
26 26 from IPython.utils.traitlets import Unicode, Dict, List, Instance, Bool
27 27 from IPython.display import Javascript, display
28 28 from IPython.utils.py3compat import string_types
29 29
30 30 from .widget_view import ViewWidget
31 31
32 32 #-----------------------------------------------------------------------------
33 33 # Classes
34 34 #-----------------------------------------------------------------------------
35 35
36 36 class BaseWidget(LoggingConfigurable):
37 37
38 38 # Shared declarations (Class level)
39 39 _keys = List(Unicode, default_value = [],
40 40 help="List of keys comprising the state of the model.", allow_none=False)
41 41 widget_construction_callback = None
42 42
43 43 def on_widget_constructed(callback):
44 44 """Class method, registers a callback to be called when a widget is
45 45 constructed. The callback must have the following signature:
46 46 callback(widget)"""
47 47 BaseWidget.widget_construction_callback = callback
48 48
49 49 def _handle_widget_constructed(widget):
50 50 """Class method, called when a widget is constructed."""
51 51 if BaseWidget.widget_construction_callback is not None and callable(BaseWidget.widget_construction_callback):
52 52 BaseWidget.widget_construction_callback(widget)
53 53
54 54
55 55
56 56 # Public declarations (Instance level)
57 57 target_name = Unicode('widget', help="""Name of the backbone model
58 58 registered in the frontend to create and sync this widget with.""")
59 59 default_view_name = Unicode(help="""Default view registered in the frontend
60 60 to use to represent the widget.""")
61 61
62 62 # Private/protected declarations
63 63 # todo: change this to a context manager
64 64 _property_lock = (None, None) # Last updated (key, value) from the front-end. Prevents echo.
65 65 _displayed = False
66 66 _comm = Instance('IPython.kernel.comm.Comm')
67 67
68 68 def __init__(self, **kwargs):
69 69 """Public constructor
70 70 """
71 71 self._display_callbacks = []
72 72 self._msg_callbacks = []
73 73 super(BaseWidget, self).__init__(**kwargs)
74 74
75 75 self.on_trait_change(self._handle_property_changed, self.keys)
76 76 BaseWidget._handle_widget_constructed(self)
77 77
78 78 def __del__(self):
79 79 """Object disposal"""
80 80 self.close()
81 81
82 82
83 83 def close(self):
84 84 """Close method. Closes the widget which closes the underlying comm.
85 85 When the comm is closed, all of the widget views are automatically
86 86 removed from the frontend."""
87 87 self._close_communication()
88 88
89 89
90 90 # Properties
91 91 @property
92 92 def keys(self):
93 93 keys = ['default_view_name']
94 94 keys.extend(self._keys)
95 95 return keys
96 96
97 97 @property
98 98 def comm(self):
99 99 if self._comm is None:
100 100 self._open_communication()
101 101 return self._comm
102 102
103 @property
104 def model_id(self):
105 return self._comm.comm_id
106
103 107 # Event handlers
104 108 def _handle_msg(self, msg):
105 109 """Called when a msg is recieved from the frontend"""
106 110 data = msg['content']['data']
107 111 method = data['method']
108 112
109 113 # Handle backbone sync methods CREATE, PATCH, and UPDATE
110 114 if method == 'backbone':
111 115 if 'sync_method' in data and 'sync_data' in data:
112 116 sync_method = data['sync_method']
113 117 sync_data = data['sync_data']
114 118 self._handle_recieve_state(sync_data) # handles all methods
115 119
116 120 # Handle a custom msg from the front-end
117 121 elif method == 'custom':
118 122 if 'custom_content' in data:
119 123 self._handle_custom_msg(data['custom_content'])
120 124
121 125 def _handle_custom_msg(self, content):
122 126 """Called when a custom msg is recieved."""
123 127 for handler in self._msg_callbacks:
124 128 if callable(handler):
125 129 argspec = inspect.getargspec(handler)
126 130 nargs = len(argspec[0])
127 131
128 132 # Bound methods have an additional 'self' argument
129 133 if isinstance(handler, types.MethodType):
130 134 nargs -= 1
131 135
132 136 # Call the callback
133 137 if nargs == 1:
134 138 handler(content)
135 139 elif nargs == 2:
136 140 handler(self, content)
137 141 else:
138 142 raise TypeError('Widget msg callback must ' \
139 143 'accept 1 or 2 arguments, not %d.' % nargs)
140 144
141 145
142 146 def _handle_recieve_state(self, sync_data):
143 147 """Called when a state is recieved from the frontend."""
144 148 # Use _keys instead of keys - Don't get retrieve the css from the client side.
145 149 for name in self._keys:
146 150 if name in sync_data:
147 151 try:
148 152 self._property_lock = (name, sync_data[name])
149 153 setattr(self, name, sync_data[name])
150 154 finally:
151 155 self._property_lock = (None, None)
152 156
153 157
154 158 def _handle_property_changed(self, name, old, new):
155 159 """Called when a property has been changed."""
156 160 # Make sure this isn't information that the front-end just sent us.
157 161 if self._property_lock[0] != name and self._property_lock[1] != new:
158 162 # Send new state to frontend
159 163 self.send_state(key=name)
160 164
161 165 def _handle_displayed(self, **kwargs):
162 166 """Called when a view has been displayed for this widget instance
163 167
164 168 Parameters
165 169 ----------
166 170 [view_name]: unicode (optional kwarg)
167 171 Name of the view that was displayed."""
168 172 for handler in self._display_callbacks:
169 173 if callable(handler):
170 174 argspec = inspect.getargspec(handler)
171 175 nargs = len(argspec[0])
172 176
173 177 # Bound methods have an additional 'self' argument
174 178 if isinstance(handler, types.MethodType):
175 179 nargs -= 1
176 180
177 181 # Call the callback
178 182 if nargs == 0:
179 183 handler()
180 184 elif nargs == 1:
181 185 handler(self)
182 186 elif nargs == 2:
183 187 handler(self, kwargs.get('view_name', None))
184 188 else:
185 189 handler(self, **kwargs)
186 190
187 191 # Public methods
188 192 def send_state(self, key=None):
189 193 """Sends the widget state, or a piece of it, to the frontend.
190 194
191 195 Parameters
192 196 ----------
193 197 key : unicode (optional)
194 198 A single property's name to sync with the frontend.
195 199 """
196 200 self._send({"method": "update",
197 201 "state": self.get_state()})
198 202
199 203 def get_state(self, key=None):
200 204 """Gets the widget state, or a piece of it.
201 205
202 206 Parameters
203 207 ----------
204 208 key : unicode (optional)
205 209 A single property's name to get.
206 210 """
207 211 state = {}
208 212
209 213 # If a key is provided, just send the state of that key.
210 214 if key is None:
211 215 keys = self.keys[:]
212 216 else:
213 217 keys = [key]
214 218 for k in keys:
215 219 value = getattr(self, k)
216 220
217 221 # a more elegant solution to encoding BaseWidgets would be
218 222 # to tap into the JSON encoder and teach it how to deal
219 223 # with BaseWidget objects, or maybe just teach the JSON
220 224 # encoder to look for a _repr_json property before giving
221 225 # up encoding
222 226 if isinstance(value, BaseWidget):
223 value = value.comm.comm_id
227 value = value.model_id
224 228 elif isinstance(value, list) and len(value)>0 and isinstance(value[0], BaseWidget):
225 229 # assume all elements of the list are widgets
226 value = [i.comm.comm_id for i in value]
230 value = [i.model_id for i in value]
227 231 state[k] = value
228 232 return state
229 233
230 234
231 235 def send(self, content):
232 236 """Sends a custom msg to the widget model in the front-end.
233 237
234 238 Parameters
235 239 ----------
236 240 content : dict
237 241 Content of the message to send.
238 242 """
239 243 self._send({"method": "custom",
240 244 "custom_content": content})
241 245
242 246
243 247 def on_msg(self, callback, remove=False):
244 248 """Register a callback for when a custom msg is recieved from the front-end
245 249
246 250 Parameters
247 251 ----------
248 252 callback: method handler
249 253 Can have a signature of:
250 254 - callback(content)
251 255 - callback(sender, content)
252 256 remove: bool
253 257 True if the callback should be unregistered."""
254 258 if remove and callback in self._msg_callbacks:
255 259 self._msg_callbacks.remove(callback)
256 260 elif not remove and not callback in self._msg_callbacks:
257 261 self._msg_callbacks.append(callback)
258 262
259 263
260 264 def on_displayed(self, callback, remove=False):
261 265 """Register a callback to be called when the widget has been displayed
262 266
263 267 Parameters
264 268 ----------
265 269 callback: method handler
266 270 Can have a signature of:
267 271 - callback()
268 272 - callback(sender)
269 273 - callback(sender, view_name)
270 274 - callback(sender, **kwargs)
271 275 kwargs from display call passed through without modification.
272 276 remove: bool
273 277 True if the callback should be unregistered."""
274 278 if remove and callback in self._display_callbacks:
275 279 self._display_callbacks.remove(callback)
276 280 elif not remove and not callback in self._display_callbacks:
277 281 self._display_callbacks.append(callback)
278 282
279 283
280 284 # Support methods
281 285 def _repr_widget_(self, **kwargs):
282 286 """Function that is called when `IPython.display.display` is called on
283 287 the widget.
284 288
285 289 Parameters
286 290 ----------
287 291 view_name: unicode (optional)
288 292 View to display in the frontend. Overrides default_view_name."""
289 293 view_name = kwargs.get('view_name', self.default_view_name)
290 294
291 295 # Create a communication.
292 296 self._open_communication()
293 297
294 298 # Make sure model is syncronized
295 299 self.send_state()
296 300
297 301 # Show view.
298 302 self._send({"method": "display", "view_name": view_name})
299 303 self._displayed = True
300 304 self._handle_displayed(**kwargs)
301 305
302 306
303 307 def _open_communication(self):
304 308 """Opens a communication with the front-end."""
305 309 # Create a comm.
306 310 if self._comm is None:
307 311 self._comm = Comm(target_name=self.target_name)
308 312 self._comm.on_msg(self._handle_msg)
309 313 self._comm.on_close(self._close_communication)
310 314
311 315 # first update
312 316 self.send_state()
313 317
314 318
315 319 def _close_communication(self):
316 320 """Closes a communication with the front-end."""
317 321 if self._comm is not None:
318 322 try:
319 323 self._comm.close()
320 324 finally:
321 325 self._comm = None
322 326
323 327
324 328 def _send(self, msg):
325 329 """Sends a message to the model in the front-end"""
326 330 if self._comm is not None:
327 331 self._comm.send(msg)
328 332 return True
329 333 else:
330 334 return False
331 335
332 336 class Widget(BaseWidget):
333 337 visible = Bool(True, help="Whether or not the widget is visible.")
334 338
335 339 # Private/protected declarations
336 340 _css = Dict() # Internal CSS property dict
337 341
338 342 # Properties
339 343 @property
340 344 def keys(self):
341 345 keys = ['visible', '_css']
342 346 keys.extend(super(Widget, self).keys)
343 347 return keys
344 348
345 349 def get_css(self, key, selector=""):
346 350 """Get a CSS property of the widget. Note, this function does not
347 351 actually request the CSS from the front-end; Only properties that have
348 352 been set with set_css can be read.
349 353
350 354 Parameters
351 355 ----------
352 356 key: unicode
353 357 CSS key
354 358 selector: unicode (optional)
355 359 JQuery selector used when the CSS key/value was set.
356 360 """
357 361 if selector in self._css and key in self._css[selector]:
358 362 return self._css[selector][key]
359 363 else:
360 364 return None
361 365
362 366
363 367 def set_css(self, *args, **kwargs):
364 368 """Set one or more CSS properties of the widget (shared among all of the
365 369 views). This function has two signatures:
366 370 - set_css(css_dict, [selector=''])
367 371 - set_css(key, value, [selector=''])
368 372
369 373 Parameters
370 374 ----------
371 375 css_dict : dict
372 376 CSS key/value pairs to apply
373 377 key: unicode
374 378 CSS key
375 379 value
376 380 CSS value
377 381 selector: unicode (optional)
378 382 JQuery selector to use to apply the CSS key/value.
379 383 """
380 384 selector = kwargs.get('selector', '')
381 385
382 386 # Signature 1: set_css(css_dict, [selector=''])
383 387 if len(args) == 1:
384 388 if isinstance(args[0], dict):
385 389 for (key, value) in args[0].items():
386 390 self.set_css(key, value, selector=selector)
387 391 else:
388 392 raise Exception('css_dict must be a dict.')
389 393
390 394 # Signature 2: set_css(key, value, [selector=''])
391 395 elif len(args) == 2 or len(args) == 3:
392 396
393 397 # Selector can be a positional arg if it's the 3rd value
394 398 if len(args) == 3:
395 399 selector = args[2]
396 400 if selector not in self._css:
397 401 self._css[selector] = {}
398 402
399 403 # Only update the property if it has changed.
400 404 key = args[0]
401 405 value = args[1]
402 406 if not (key in self._css[selector] and value in self._css[selector][key]):
403 407 self._css[selector][key] = value
404 408 self.send_state('_css') # Send new state to client.
405 409 else:
406 410 raise Exception('set_css only accepts 1-3 arguments')
407 411
408 412
409 413 def add_class(self, class_name, selector=""):
410 414 """Add class[es] to a DOM element
411 415
412 416 Parameters
413 417 ----------
414 418 class_name: unicode
415 419 Class name(s) to add to the DOM element(s). Multiple class names
416 420 must be space separated.
417 421 selector: unicode (optional)
418 422 JQuery selector to select the DOM element(s) that the class(es) will
419 423 be added to.
420 424 """
421 425 self.send({"msg_type": "add_class",
422 426 "class_list": class_name,
423 427 "selector": selector})
424 428
425 429
426 430 def remove_class(self, class_name, selector=""):
427 431 """Remove class[es] from a DOM element
428 432
429 433 Parameters
430 434 ----------
431 435 class_name: unicode
432 436 Class name(s) to remove from the DOM element(s). Multiple class
433 437 names must be space separated.
434 438 selector: unicode (optional)
435 439 JQuery selector to select the DOM element(s) that the class(es) will
436 440 be removed from.
437 441 """
438 442 self.send({"msg_type": "remove_class",
439 443 "class_list": class_name,
440 444 "selector": selector})
441 445
442 446
443 447 def view(self, view_name=None):
444 448 """Return a widget that can be displayed to display this widget using
445 449 a non-default view"""
446 450 return ViewWidget(self, view_name)
General Comments 0
You need to be logged in to leave comments. Login now