##// END OF EJS Templates
Address some more review comments...
Jonathan Frederic -
Show More
@@ -1,36 +1,36
1 1 // Test the widget manager.
2 2 casper.notebook_test(function () {
3 3 var index;
4 4 var slider = {};
5 5
6 6 this.then(function () {
7 7
8 8 // Check if the WidgetManager class is defined.
9 9 this.test.assert(this.evaluate(function() {
10 10 return IPython.WidgetManager !== undefined;
11 11 }), 'WidgetManager class is defined');
12 12
13 13 // Check if the widget manager has been instantiated.
14 14 this.test.assert(this.evaluate(function() {
15 15 return IPython.notebook.kernel.widget_manager !== undefined;
16 16 }), 'Notebook widget manager instantiated');
17 17
18 18 // Try creating a widget from Javascript.
19 19 slider.id = this.evaluate(function() {
20 20 var slider = IPython.notebook.kernel.widget_manager.create_model({
21 21 model_name: 'WidgetModel',
22 22 target_name: 'IPython.html.widgets.widget_int.IntSlider',
23 23 init_state_callback: function(model) { console.log('Create success!', model); }});
24 24 return slider.id;
25 25 });
26 26 });
27 27
28 28 index = this.append_cell(
29 29 'from IPython.html.widgets import Widget\n' +
30 'widget = Widget.widgets.values()[0]\n' +
30 'widget = list(Widget.widgets.values())[0]\n' +
31 31 'print(widget.model_id)');
32 32 this.execute_cell_then(index, function(index) {
33 33 var output = this.get_output_cell(index).text.trim();
34 34 this.test.assertEquals(output, slider.id, "Widget created from the front-end.");
35 35 });
36 36 });
@@ -1,469 +1,467
1 1 """Base Widget class. Allows user to create widgets in the back-end that render
2 2 in the IPython notebook front-end.
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 contextlib import contextmanager
16 16 import collections
17 17
18 18 from IPython.core.getipython import get_ipython
19 19 from IPython.kernel.comm import Comm
20 20 from IPython.config import LoggingConfigurable
21 21 from IPython.utils.importstring import import_item
22 22 from IPython.utils.traitlets import Unicode, Dict, Instance, Bool, List, \
23 23 CaselessStrEnum, Tuple, CUnicode, Int, Set
24 24 from IPython.utils.py3compat import string_types
25 25
26 26 #-----------------------------------------------------------------------------
27 27 # Classes
28 28 #-----------------------------------------------------------------------------
29 29 class CallbackDispatcher(LoggingConfigurable):
30 30 """A structure for registering and running callbacks"""
31 31 callbacks = List()
32 32
33 33 def __call__(self, *args, **kwargs):
34 34 """Call all of the registered callbacks."""
35 35 value = None
36 36 for callback in self.callbacks:
37 37 try:
38 38 local_value = callback(*args, **kwargs)
39 39 except Exception as e:
40 40 ip = get_ipython()
41 41 if ip is None:
42 42 self.log.warn("Exception in callback %s: %s", callback, e, exc_info=True)
43 43 else:
44 44 ip.showtraceback()
45 45 else:
46 46 value = local_value if local_value is not None else value
47 47 return value
48 48
49 49 def register_callback(self, callback, remove=False):
50 50 """(Un)Register a callback
51 51
52 52 Parameters
53 53 ----------
54 54 callback: method handle
55 55 Method to be registered or unregistered.
56 56 remove=False: bool
57 57 Whether to unregister the callback."""
58 58
59 59 # (Un)Register the callback.
60 60 if remove and callback in self.callbacks:
61 61 self.callbacks.remove(callback)
62 62 elif not remove and callback not in self.callbacks:
63 63 self.callbacks.append(callback)
64 64
65 65 def _show_traceback(method):
66 66 """decorator for showing tracebacks in IPython"""
67 67 def m(self, *args, **kwargs):
68 68 try:
69 69 return(method(self, *args, **kwargs))
70 70 except Exception as e:
71 71 ip = get_ipython()
72 72 if ip is None:
73 73 self.log.warn("Exception in widget method %s: %s", method, e, exc_info=True)
74 74 else:
75 75 ip.showtraceback()
76 76 return m
77 77
78 78 class Widget(LoggingConfigurable):
79 79 #-------------------------------------------------------------------------
80 80 # Class attributes
81 81 #-------------------------------------------------------------------------
82 82 _widget_construction_callback = None
83 83 widgets = {}
84 84
85 85 @staticmethod
86 86 def on_widget_constructed(callback):
87 87 """Registers a callback to be called when a widget is constructed.
88 88
89 89 The callback must have the following signature:
90 90 callback(widget)"""
91 91 Widget._widget_construction_callback = callback
92 92
93 93 @staticmethod
94 94 def _call_widget_constructed(widget):
95 95 """Static method, called when a widget is constructed."""
96 96 if Widget._widget_construction_callback is not None and callable(Widget._widget_construction_callback):
97 97 Widget._widget_construction_callback(widget)
98 98
99 99 @staticmethod
100 100 def handle_comm_opened(comm, msg):
101 101 """Static method, called when a widget is constructed."""
102 102 target_name = msg['content']['data']['target_name']
103 103 widget_class = import_item(target_name)
104 widget = widget_class(open_comm=False)
105 widget.comm = comm
104 widget = widget_class(comm=comm)
106 105
107 106
108 107 #-------------------------------------------------------------------------
109 108 # Traits
110 109 #-------------------------------------------------------------------------
111 110 _model_module = Unicode(None, allow_none=True, help="""A requirejs module name
112 111 in which to find _model_name. If empty, look in the global registry.""")
113 112 _model_name = Unicode('WidgetModel', help="""Name of the backbone model
114 113 registered in the front-end to create and sync this widget with.""")
115 114 _view_module = Unicode(help="""A requirejs module in which to find _view_name.
116 115 If empty, look in the global registry.""", sync=True)
117 116 _view_name = Unicode(None, allow_none=True, help="""Default view registered in the front-end
118 117 to use to represent the widget.""", sync=True)
119 118 comm = Instance('IPython.kernel.comm.Comm')
120 119
121 120 msg_throttle = Int(3, sync=True, help="""Maximum number of msgs the
122 121 front-end can send before receiving an idle msg from the back-end.""")
123 122
124 123 version = Int(0, sync=True, help="""Widget's version""")
125 124 keys = List()
126 125 def _keys_default(self):
127 126 return [name for name in self.traits(sync=True)]
128 127
129 128 _property_lock = Tuple((None, None))
130 129 _send_state_lock = Int(0)
131 130 _states_to_send = Set(allow_none=False)
132 131 _display_callbacks = Instance(CallbackDispatcher, ())
133 132 _msg_callbacks = Instance(CallbackDispatcher, ())
134 133
135 134 #-------------------------------------------------------------------------
136 135 # (Con/de)structor
137 136 #-------------------------------------------------------------------------
138 137 def __init__(self, open_comm=True, **kwargs):
139 138 """Public constructor"""
140 139 self._model_id = kwargs.pop('model_id', None)
141 140 super(Widget, self).__init__(**kwargs)
142 141
143 142 Widget._call_widget_constructed(self)
144 if open_comm:
145 143 self.open()
146 144
147 145 def __del__(self):
148 146 """Object disposal"""
149 147 self.close()
150 148
151 149 #-------------------------------------------------------------------------
152 150 # Properties
153 151 #-------------------------------------------------------------------------
154 152
155 153 def open(self):
156 154 """Open a comm to the frontend if one isn't already open."""
157 155 if self.comm is None:
158 156 args = dict(target_name='ipython.widget',
159 157 data={'model_name': self._model_name,
160 158 'model_module': self._model_module})
161 159 if self._model_id is not None:
162 160 args['comm_id'] = self._model_id
163 161 self.comm = Comm(**args)
164 162
165 163 def _comm_changed(self, name, new):
166 164 """Called when the comm is changed."""
167 165 self.comm = new
168 166 self._model_id = self.model_id
169 167
170 168 self.comm.on_msg(self._handle_msg)
171 169 Widget.widgets[self.model_id] = self
172 170
173 171 # first update
174 172 self.send_state()
175 173
176 174 @property
177 175 def model_id(self):
178 176 """Gets the model id of this widget.
179 177
180 178 If a Comm doesn't exist yet, a Comm will be created automagically."""
181 179 return self.comm.comm_id
182 180
183 181 #-------------------------------------------------------------------------
184 182 # Methods
185 183 #-------------------------------------------------------------------------
186 184
187 185 def close(self):
188 186 """Close method.
189 187
190 188 Closes the underlying comm.
191 189 When the comm is closed, all of the widget views are automatically
192 190 removed from the front-end."""
193 191 if self.comm is not None:
194 192 Widget.widgets.pop(self.model_id, None)
195 193 self.comm.close()
196 194 self.comm = None
197 195
198 196 def send_state(self, key=None):
199 197 """Sends the widget state, or a piece of it, to the front-end.
200 198
201 199 Parameters
202 200 ----------
203 201 key : unicode, or iterable (optional)
204 202 A single property's name or iterable of property names to sync with the front-end.
205 203 """
206 204 self._send({
207 205 "method" : "update",
208 206 "state" : self.get_state(key=key)
209 207 })
210 208
211 209 def get_state(self, key=None):
212 210 """Gets the widget state, or a piece of it.
213 211
214 212 Parameters
215 213 ----------
216 214 key : unicode or iterable (optional)
217 215 A single property's name or iterable of property names to get.
218 216 """
219 217 if key is None:
220 218 keys = self.keys
221 219 elif isinstance(key, string_types):
222 220 keys = [key]
223 221 elif isinstance(key, collections.Iterable):
224 222 keys = key
225 223 else:
226 224 raise ValueError("key must be a string, an iterable of keys, or None")
227 225 state = {}
228 226 for k in keys:
229 227 f = self.trait_metadata(k, 'to_json', self._trait_to_json)
230 228 value = getattr(self, k)
231 229 state[k] = f(value)
232 230 return state
233 231
234 232 def set_state(self, sync_data):
235 233 """Called when a state is received from the front-end."""
236 234 for name in self.keys:
237 235 if name in sync_data:
238 236 json_value = sync_data[name]
239 237 from_json = self.trait_metadata(name, 'from_json', self._trait_from_json)
240 238 with self._lock_property(name, json_value):
241 239 setattr(self, name, from_json(json_value))
242 240
243 241 def send(self, content):
244 242 """Sends a custom msg to the widget model in the front-end.
245 243
246 244 Parameters
247 245 ----------
248 246 content : dict
249 247 Content of the message to send.
250 248 """
251 249 self._send({"method": "custom", "content": content})
252 250
253 251 def on_msg(self, callback, remove=False):
254 252 """(Un)Register a custom msg receive callback.
255 253
256 254 Parameters
257 255 ----------
258 256 callback: callable
259 257 callback will be passed two arguments when a message arrives::
260 258
261 259 callback(widget, content)
262 260
263 261 remove: bool
264 262 True if the callback should be unregistered."""
265 263 self._msg_callbacks.register_callback(callback, remove=remove)
266 264
267 265 def on_displayed(self, callback, remove=False):
268 266 """(Un)Register a widget displayed callback.
269 267
270 268 Parameters
271 269 ----------
272 270 callback: method handler
273 271 Must have a signature of::
274 272
275 273 callback(widget, **kwargs)
276 274
277 275 kwargs from display are passed through without modification.
278 276 remove: bool
279 277 True if the callback should be unregistered."""
280 278 self._display_callbacks.register_callback(callback, remove=remove)
281 279
282 280 #-------------------------------------------------------------------------
283 281 # Support methods
284 282 #-------------------------------------------------------------------------
285 283 @contextmanager
286 284 def _lock_property(self, key, value):
287 285 """Lock a property-value pair.
288 286
289 287 The value should be the JSON state of the property.
290 288
291 289 NOTE: This, in addition to the single lock for all state changes, is
292 290 flawed. In the future we may want to look into buffering state changes
293 291 back to the front-end."""
294 292 self._property_lock = (key, value)
295 293 try:
296 294 yield
297 295 finally:
298 296 self._property_lock = (None, None)
299 297
300 298 @contextmanager
301 299 def hold_sync(self):
302 300 """Hold syncing any state until the context manager is released"""
303 301 # We increment a value so that this can be nested. Syncing will happen when
304 302 # all levels have been released.
305 303 self._send_state_lock += 1
306 304 try:
307 305 yield
308 306 finally:
309 307 self._send_state_lock -=1
310 308 if self._send_state_lock == 0:
311 309 self.send_state(self._states_to_send)
312 310 self._states_to_send.clear()
313 311
314 312 def _should_send_property(self, key, value):
315 313 """Check the property lock (property_lock)"""
316 314 to_json = self.trait_metadata(key, 'to_json', self._trait_to_json)
317 315 if (key == self._property_lock[0]
318 316 and to_json(value) == self._property_lock[1]):
319 317 return False
320 318 elif self._send_state_lock > 0:
321 319 self._states_to_send.add(key)
322 320 return False
323 321 else:
324 322 return True
325 323
326 324 # Event handlers
327 325 @_show_traceback
328 326 def _handle_msg(self, msg):
329 327 """Called when a msg is received from the front-end"""
330 328 data = msg['content']['data']
331 329 method = data['method']
332 330 if not method in ['backbone', 'custom']:
333 331 self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method)
334 332
335 333 # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
336 334 if method == 'backbone' and 'sync_data' in data:
337 335 sync_data = data['sync_data']
338 336 self.set_state(sync_data) # handles all methods
339 337
340 338 # Handle a custom msg from the front-end
341 339 elif method == 'custom':
342 340 if 'content' in data:
343 341 self._handle_custom_msg(data['content'])
344 342
345 343 def _handle_custom_msg(self, content):
346 344 """Called when a custom msg is received."""
347 345 self._msg_callbacks(self, content)
348 346
349 347 def _notify_trait(self, name, old_value, new_value):
350 348 """Called when a property has been changed."""
351 349 # Trigger default traitlet callback machinery. This allows any user
352 350 # registered validation to be processed prior to allowing the widget
353 351 # machinery to handle the state.
354 352 LoggingConfigurable._notify_trait(self, name, old_value, new_value)
355 353
356 354 # Send the state after the user registered callbacks for trait changes
357 355 # have all fired (allows for user to validate values).
358 356 if self.comm is not None and name in self.keys:
359 357 # Make sure this isn't information that the front-end just sent us.
360 358 if self._should_send_property(name, new_value):
361 359 # Send new state to front-end
362 360 self.send_state(key=name)
363 361
364 362 def _handle_displayed(self, **kwargs):
365 363 """Called when a view has been displayed for this widget instance"""
366 364 self._display_callbacks(self, **kwargs)
367 365
368 366 def _trait_to_json(self, x):
369 367 """Convert a trait value to json
370 368
371 369 Traverse lists/tuples and dicts and serialize their values as well.
372 370 Replace any widgets with their model_id
373 371 """
374 372 if isinstance(x, dict):
375 373 return {k: self._trait_to_json(v) for k, v in x.items()}
376 374 elif isinstance(x, (list, tuple)):
377 375 return [self._trait_to_json(v) for v in x]
378 376 elif isinstance(x, Widget):
379 377 return "IPY_MODEL_" + x.model_id
380 378 else:
381 379 return x # Value must be JSON-able
382 380
383 381 def _trait_from_json(self, x):
384 382 """Convert json values to objects
385 383
386 384 Replace any strings representing valid model id values to Widget references.
387 385 """
388 386 if isinstance(x, dict):
389 387 return {k: self._trait_from_json(v) for k, v in x.items()}
390 388 elif isinstance(x, (list, tuple)):
391 389 return [self._trait_from_json(v) for v in x]
392 390 elif isinstance(x, string_types) and x.startswith('IPY_MODEL_') and x[10:] in Widget.widgets:
393 391 # we want to support having child widgets at any level in a hierarchy
394 392 # trusting that a widget UUID will not appear out in the wild
395 393 return Widget.widgets[x[10:]]
396 394 else:
397 395 return x
398 396
399 397 def _ipython_display_(self, **kwargs):
400 398 """Called when `IPython.display.display` is called on the widget."""
401 399 # Show view.
402 400 if self._view_name is not None:
403 401 self._send({"method": "display"})
404 402 self._handle_displayed(**kwargs)
405 403
406 404 def _send(self, msg):
407 405 """Sends a message to the model in the front-end."""
408 406 self.comm.send(msg)
409 407
410 408
411 409 class DOMWidget(Widget):
412 410 visible = Bool(True, help="Whether the widget is visible.", sync=True)
413 411 _css = Tuple(sync=True, help="CSS property list: (selector, key, value)")
414 412 _dom_classes = Tuple(sync=True, help="DOM classes applied to widget.$el.")
415 413
416 414 width = CUnicode(sync=True)
417 415 height = CUnicode(sync=True)
418 416 padding = CUnicode(sync=True)
419 417 margin = CUnicode(sync=True)
420 418
421 419 color = Unicode(sync=True)
422 420 background_color = Unicode(sync=True)
423 421 border_color = Unicode(sync=True)
424 422
425 423 border_width = CUnicode(sync=True)
426 424 border_radius = CUnicode(sync=True)
427 425 border_style = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_border-style.asp
428 426 'none',
429 427 'hidden',
430 428 'dotted',
431 429 'dashed',
432 430 'solid',
433 431 'double',
434 432 'groove',
435 433 'ridge',
436 434 'inset',
437 435 'outset',
438 436 'initial',
439 437 'inherit', ''],
440 438 default_value='', sync=True)
441 439
442 440 font_style = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_font_font-style.asp
443 441 'normal',
444 442 'italic',
445 443 'oblique',
446 444 'initial',
447 445 'inherit', ''],
448 446 default_value='', sync=True)
449 447 font_weight = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_font_weight.asp
450 448 'normal',
451 449 'bold',
452 450 'bolder',
453 451 'lighter',
454 452 'initial',
455 453 'inherit', ''] + [str(100 * (i+1)) for i in range(9)],
456 454 default_value='', sync=True)
457 455 font_size = CUnicode(sync=True)
458 456 font_family = Unicode(sync=True)
459 457
460 458 def __init__(self, *pargs, **kwargs):
461 459 super(DOMWidget, self).__init__(*pargs, **kwargs)
462 460
463 461 def _validate_border(name, old, new):
464 462 if new is not None and new != '':
465 463 if name != 'border_width' and not self.border_width:
466 464 self.border_width = 1
467 465 if name != 'border_style' and self.border_style == '':
468 466 self.border_style = 'solid'
469 467 self.on_trait_change(_validate_border, ['border_width', 'border_style', 'border_color'])
General Comments 0
You need to be logged in to leave comments. Login now