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