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