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