##// END OF EJS Templates
allowing widet id to be set externally
Sylvain Corlay -
Show More
@@ -1,446 +1,450
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
17 17 from IPython.core.getipython import get_ipython
18 18 from IPython.kernel.comm import Comm
19 19 from IPython.config import LoggingConfigurable
20 20 from IPython.utils.traitlets import Unicode, Dict, Instance, Bool, List, Tuple, Int
21 21 from IPython.utils.py3compat import string_types
22 22
23 23 #-----------------------------------------------------------------------------
24 24 # Classes
25 25 #-----------------------------------------------------------------------------
26 26 class CallbackDispatcher(LoggingConfigurable):
27 27 """A structure for registering and running callbacks"""
28 28 callbacks = List()
29 29
30 30 def __call__(self, *args, **kwargs):
31 31 """Call all of the registered callbacks."""
32 32 value = None
33 33 for callback in self.callbacks:
34 34 try:
35 35 local_value = callback(*args, **kwargs)
36 36 except Exception as e:
37 37 ip = get_ipython()
38 38 if ip is None:
39 39 self.log.warn("Exception in callback %s: %s", callback, e, exc_info=True)
40 40 else:
41 41 ip.showtraceback()
42 42 else:
43 43 value = local_value if local_value is not None else value
44 44 return value
45 45
46 46 def register_callback(self, callback, remove=False):
47 47 """(Un)Register a callback
48 48
49 49 Parameters
50 50 ----------
51 51 callback: method handle
52 52 Method to be registered or unregistered.
53 53 remove=False: bool
54 54 Whether to unregister the callback."""
55 55
56 56 # (Un)Register the callback.
57 57 if remove and callback in self.callbacks:
58 58 self.callbacks.remove(callback)
59 59 elif not remove and callback not in self.callbacks:
60 60 self.callbacks.append(callback)
61 61
62 62 def _show_traceback(method):
63 63 """decorator for showing tracebacks in IPython"""
64 64 def m(self, *args, **kwargs):
65 65 try:
66 66 return(method(self, *args, **kwargs))
67 67 except Exception as e:
68 68 ip = get_ipython()
69 69 if ip is None:
70 70 self.log.warn("Exception in widget method %s: %s", method, e, exc_info=True)
71 71 else:
72 72 ip.showtraceback()
73 73 return m
74 74
75 75 class Widget(LoggingConfigurable):
76 76 #-------------------------------------------------------------------------
77 77 # Class attributes
78 78 #-------------------------------------------------------------------------
79 79 _widget_construction_callback = None
80 80 widgets = {}
81 81
82 82 @staticmethod
83 83 def on_widget_constructed(callback):
84 84 """Registers a callback to be called when a widget is constructed.
85 85
86 86 The callback must have the following signature:
87 87 callback(widget)"""
88 88 Widget._widget_construction_callback = callback
89 89
90 90 @staticmethod
91 91 def _call_widget_constructed(widget):
92 92 """Static method, called when a widget is constructed."""
93 93 if Widget._widget_construction_callback is not None and callable(Widget._widget_construction_callback):
94 94 Widget._widget_construction_callback(widget)
95 95
96 96 #-------------------------------------------------------------------------
97 97 # Traits
98 98 #-------------------------------------------------------------------------
99 99 _model_name = Unicode('WidgetModel', help="""Name of the backbone model
100 100 registered in the front-end to create and sync this widget with.""")
101 101 _view_name = Unicode(help="""Default view registered in the front-end
102 102 to use to represent the widget.""", sync=True)
103 103 comm = Instance('IPython.kernel.comm.Comm')
104 104
105 105 msg_throttle = Int(3, sync=True, help="""Maximum number of msgs the
106 106 front-end can send before receiving an idle msg from the back-end.""")
107 107
108 108 keys = List()
109 109 def _keys_default(self):
110 110 return [name for name in self.traits(sync=True)]
111 111
112 112 _property_lock = Tuple((None, None))
113 113
114 114 _display_callbacks = Instance(CallbackDispatcher, ())
115 115 _msg_callbacks = Instance(CallbackDispatcher, ())
116 116
117 117 #-------------------------------------------------------------------------
118 118 # (Con/de)structor
119 119 #-------------------------------------------------------------------------
120 120 def __init__(self, **kwargs):
121 121 """Public constructor"""
122 self._model_id = kwargs.pop('model_id', None)
122 123 super(Widget, self).__init__(**kwargs)
123 124
124 125 self.on_trait_change(self._handle_property_changed, self.keys)
125 126 Widget._call_widget_constructed(self)
126 127 self.open()
127 128
128 129 def __del__(self):
129 130 """Object disposal"""
130 131 self.close()
131 132
132 133 #-------------------------------------------------------------------------
133 134 # Properties
134 135 #-------------------------------------------------------------------------
135 136
136 137 def open(self):
137 138 """Open a comm to the frontend if one isn't already open."""
138 139 if self.comm is None:
139 # Create a comm.
140 if self._model_id is None:
140 141 self.comm = Comm(target_name=self._model_name)
142 self._model_id = self.model_id
143 else:
144 self.comm = Comm(target_name=self._model_name, comm_id=self._model_id)
141 145 self.comm.on_msg(self._handle_msg)
142 146 Widget.widgets[self.model_id] = self
143 147
144 148 # first update
145 149 self.send_state()
146 150
147 151 @property
148 152 def model_id(self):
149 153 """Gets the model id of this widget.
150 154
151 155 If a Comm doesn't exist yet, a Comm will be created automagically."""
152 156 return self.comm.comm_id
153 157
154 158 #-------------------------------------------------------------------------
155 159 # Methods
156 160 #-------------------------------------------------------------------------
157 161
158 162 def close(self):
159 163 """Close method.
160 164
161 165 Closes the underlying comm.
162 166 When the comm is closed, all of the widget views are automatically
163 167 removed from the front-end."""
164 168 if self.comm is not None:
165 169 Widget.widgets.pop(self.model_id, None)
166 170 self.comm.close()
167 171 self.comm = None
168 172
169 173 def send_state(self, key=None):
170 174 """Sends the widget state, or a piece of it, to the front-end.
171 175
172 176 Parameters
173 177 ----------
174 178 key : unicode (optional)
175 179 A single property's name to sync with the front-end.
176 180 """
177 181 self._send({
178 182 "method" : "update",
179 183 "state" : self.get_state()
180 184 })
181 185
182 186 def get_state(self, key=None):
183 187 """Gets the widget state, or a piece of it.
184 188
185 189 Parameters
186 190 ----------
187 191 key : unicode (optional)
188 192 A single property's name to get.
189 193 """
190 194 keys = self.keys if key is None else [key]
191 195 state = {}
192 196 for k in keys:
193 197 f = self.trait_metadata(k, 'to_json')
194 198 if f is None:
195 199 f = self._trait_to_json
196 200 value = getattr(self, k)
197 201 state[k] = f(value)
198 202 return state
199 203
200 204 def send(self, content):
201 205 """Sends a custom msg to the widget model in the front-end.
202 206
203 207 Parameters
204 208 ----------
205 209 content : dict
206 210 Content of the message to send.
207 211 """
208 212 self._send({"method": "custom", "content": content})
209 213
210 214 def on_msg(self, callback, remove=False):
211 215 """(Un)Register a custom msg receive callback.
212 216
213 217 Parameters
214 218 ----------
215 219 callback: callable
216 220 callback will be passed two arguments when a message arrives::
217 221
218 222 callback(widget, content)
219 223
220 224 remove: bool
221 225 True if the callback should be unregistered."""
222 226 self._msg_callbacks.register_callback(callback, remove=remove)
223 227
224 228 def on_displayed(self, callback, remove=False):
225 229 """(Un)Register a widget displayed callback.
226 230
227 231 Parameters
228 232 ----------
229 233 callback: method handler
230 234 Must have a signature of::
231 235
232 236 callback(widget, **kwargs)
233 237
234 238 kwargs from display are passed through without modification.
235 239 remove: bool
236 240 True if the callback should be unregistered."""
237 241 self._display_callbacks.register_callback(callback, remove=remove)
238 242
239 243 #-------------------------------------------------------------------------
240 244 # Support methods
241 245 #-------------------------------------------------------------------------
242 246 @contextmanager
243 247 def _lock_property(self, key, value):
244 248 """Lock a property-value pair.
245 249
246 250 NOTE: This, in addition to the single lock for all state changes, is
247 251 flawed. In the future we may want to look into buffering state changes
248 252 back to the front-end."""
249 253 self._property_lock = (key, value)
250 254 try:
251 255 yield
252 256 finally:
253 257 self._property_lock = (None, None)
254 258
255 259 def _should_send_property(self, key, value):
256 260 """Check the property lock (property_lock)"""
257 261 return key != self._property_lock[0] or \
258 262 value != self._property_lock[1]
259 263
260 264 # Event handlers
261 265 @_show_traceback
262 266 def _handle_msg(self, msg):
263 267 """Called when a msg is received from the front-end"""
264 268 data = msg['content']['data']
265 269 method = data['method']
266 270 if not method in ['backbone', 'custom']:
267 271 self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method)
268 272
269 273 # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
270 274 if method == 'backbone' and 'sync_data' in data:
271 275 sync_data = data['sync_data']
272 276 self._handle_receive_state(sync_data) # handles all methods
273 277
274 278 # Handle a custom msg from the front-end
275 279 elif method == 'custom':
276 280 if 'content' in data:
277 281 self._handle_custom_msg(data['content'])
278 282
279 283 def _handle_receive_state(self, sync_data):
280 284 """Called when a state is received from the front-end."""
281 285 for name in self.keys:
282 286 if name in sync_data:
283 287 f = self.trait_metadata(name, 'from_json')
284 288 if f is None:
285 289 f = self._trait_from_json
286 290 value = f(sync_data[name])
287 291 with self._lock_property(name, value):
288 292 setattr(self, name, value)
289 293
290 294 def _handle_custom_msg(self, content):
291 295 """Called when a custom msg is received."""
292 296 self._msg_callbacks(self, content)
293 297
294 298 def _handle_property_changed(self, name, old, new):
295 299 """Called when a property has been changed."""
296 300 # Make sure this isn't information that the front-end just sent us.
297 301 if self._should_send_property(name, new):
298 302 # Send new state to front-end
299 303 self.send_state(key=name)
300 304
301 305 def _handle_displayed(self, **kwargs):
302 306 """Called when a view has been displayed for this widget instance"""
303 307 self._display_callbacks(self, **kwargs)
304 308
305 309 def _trait_to_json(self, x):
306 310 """Convert a trait value to json
307 311
308 312 Traverse lists/tuples and dicts and serialize their values as well.
309 313 Replace any widgets with their model_id
310 314 """
311 315 if isinstance(x, dict):
312 316 return {k: self._trait_to_json(v) for k, v in x.items()}
313 317 elif isinstance(x, (list, tuple)):
314 318 return [self._trait_to_json(v) for v in x]
315 319 elif isinstance(x, Widget):
316 320 return "IPY_MODEL_" + x.model_id
317 321 else:
318 322 return x # Value must be JSON-able
319 323
320 324 def _trait_from_json(self, x):
321 325 """Convert json values to objects
322 326
323 327 Replace any strings representing valid model id values to Widget references.
324 328 """
325 329 if isinstance(x, dict):
326 330 return {k: self._trait_from_json(v) for k, v in x.items()}
327 331 elif isinstance(x, (list, tuple)):
328 332 return [self._trait_from_json(v) for v in x]
329 333 elif isinstance(x, string_types) and x.startswith('IPY_MODEL_') and x[10:] in Widget.widgets:
330 334 # we want to support having child widgets at any level in a hierarchy
331 335 # trusting that a widget UUID will not appear out in the wild
332 336 return Widget.widgets[x]
333 337 else:
334 338 return x
335 339
336 340 def _ipython_display_(self, **kwargs):
337 341 """Called when `IPython.display.display` is called on the widget."""
338 342 # Show view. By sending a display message, the comm is opened and the
339 343 # initial state is sent.
340 344 self._send({"method": "display"})
341 345 self._handle_displayed(**kwargs)
342 346
343 347 def _send(self, msg):
344 348 """Sends a message to the model in the front-end."""
345 349 self.comm.send(msg)
346 350
347 351
348 352 class DOMWidget(Widget):
349 353 visible = Bool(True, help="Whether the widget is visible.", sync=True)
350 354 _css = List(sync=True) # Internal CSS property list: (selector, key, value)
351 355
352 356 def get_css(self, key, selector=""):
353 357 """Get a CSS property of the widget.
354 358
355 359 Note: This function does not actually request the CSS from the
356 360 front-end; Only properties that have been set with set_css can be read.
357 361
358 362 Parameters
359 363 ----------
360 364 key: unicode
361 365 CSS key
362 366 selector: unicode (optional)
363 367 JQuery selector used when the CSS key/value was set.
364 368 """
365 369 if selector in self._css and key in self._css[selector]:
366 370 return self._css[selector][key]
367 371 else:
368 372 return None
369 373
370 374 def set_css(self, dict_or_key, value=None, selector=''):
371 375 """Set one or more CSS properties of the widget.
372 376
373 377 This function has two signatures:
374 378 - set_css(css_dict, selector='')
375 379 - set_css(key, value, selector='')
376 380
377 381 Parameters
378 382 ----------
379 383 css_dict : dict
380 384 CSS key/value pairs to apply
381 385 key: unicode
382 386 CSS key
383 387 value:
384 388 CSS value
385 389 selector: unicode (optional, kwarg only)
386 390 JQuery selector to use to apply the CSS key/value. If no selector
387 391 is provided, an empty selector is used. An empty selector makes the
388 392 front-end try to apply the css to a default element. The default
389 393 element is an attribute unique to each view, which is a DOM element
390 394 of the view that should be styled with common CSS (see
391 395 `$el_to_style` in the Javascript code).
392 396 """
393 397 if value is None:
394 398 css_dict = dict_or_key
395 399 else:
396 400 css_dict = {dict_or_key: value}
397 401
398 402 for (key, value) in css_dict.items():
399 403 # First remove the selector/key pair from the css list if it exists.
400 404 # Then add the selector/key pair and new value to the bottom of the
401 405 # list.
402 406 self._css = [x for x in self._css if not (x[0]==selector and x[1]==key)]
403 407 self._css += [(selector, key, value)]
404 408 self.send_state('_css')
405 409
406 410 def add_class(self, class_names, selector=""):
407 411 """Add class[es] to a DOM element.
408 412
409 413 Parameters
410 414 ----------
411 415 class_names: unicode or list
412 416 Class name(s) to add to the DOM element(s).
413 417 selector: unicode (optional)
414 418 JQuery selector to select the DOM element(s) that the class(es) will
415 419 be added to.
416 420 """
417 421 class_list = class_names
418 422 if isinstance(class_list, (list, tuple)):
419 423 class_list = ' '.join(class_list)
420 424
421 425 self.send({
422 426 "msg_type" : "add_class",
423 427 "class_list" : class_list,
424 428 "selector" : selector
425 429 })
426 430
427 431 def remove_class(self, class_names, selector=""):
428 432 """Remove class[es] from a DOM element.
429 433
430 434 Parameters
431 435 ----------
432 436 class_names: unicode or list
433 437 Class name(s) to remove from the DOM element(s).
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 class_list = class_names
439 443 if isinstance(class_list, (list, tuple)):
440 444 class_list = ' '.join(class_list)
441 445
442 446 self.send({
443 447 "msg_type" : "remove_class",
444 448 "class_list" : class_list,
445 449 "selector" : selector,
446 450 })
General Comments 0
You need to be logged in to leave comments. Login now