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