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