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