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