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