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