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