##// END OF EJS Templates
fix typos
Paul Ivanov -
Show More
@@ -1,465 +1,465 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 contextlib import contextmanager
16 16 import inspect
17 17 import types
18 18
19 19 from IPython.kernel.comm import Comm
20 20 from IPython.config import LoggingConfigurable
21 21 from IPython.utils.traitlets import Unicode, Dict, Instance, Bool
22 22 from IPython.utils.py3compat import string_types
23 23
24 24 #-----------------------------------------------------------------------------
25 25 # Classes
26 26 #-----------------------------------------------------------------------------
27 27 @contextmanager
28 28 def PropertyLock(instance, key, value):
29 29 instance._property_lock = (key, value)
30 30 try:
31 31 yield
32 32 finally:
33 33 del instance._property_lock
34 34
35 35 def should_send_property(instance, key, value):
36 36 return not hasattr(instance, '_property_lock') or \
37 37 key != instance._property_lock[0] or \
38 38 value != instance._property_lock[1]
39 39
40 40
41 41 class Widget(LoggingConfigurable):
42 42
43 43 # Shared declarations (Class level)
44 44 widget_construction_callback = None
45 45 widgets = []
46 46
47 47 keys = ['view_name'] # TODO: Sync = True
48 48
49 49 def on_widget_constructed(callback):
50 50 """Class method, registers a callback to be called when a widget is
51 51 constructed. The callback must have the following signature:
52 52 callback(widget)"""
53 53 Widget.widget_construction_callback = callback
54 54
55 55 def _call_widget_constructed(widget):
56 56 """Class method, called when a widget is constructed."""
57 57 if Widget.widget_construction_callback is not None and callable(Widget.widget_construction_callback):
58 58 Widget.widget_construction_callback(widget)
59 59
60 60
61 61
62 62 # Public declarations (Instance level)
63 63 target_name = Unicode('widget', help="""Name of the backbone model
64 64 registered in the frontend to create and sync this widget with.""")
65 65 # model_name
66 66 view_name = Unicode(help="""Default view registered in the frontend
67 67 to use to represent the widget.""")
68 68
69 69 # Private/protected declarations
70 70 _comm = Instance('IPython.kernel.comm.Comm')
71 71
72 72 def __init__(self, **kwargs):
73 73 """Public constructor
74 74 """
75 75 self.closed = False
76 76 self._display_callbacks = []
77 77 self._msg_callbacks = []
78 78 super(Widget, self).__init__(**kwargs)
79 79
80 80 self.on_trait_change(self._handle_property_changed, self.keys)
81 81 Widget.widgets.append(self)
82 82 Widget._call_widget_constructed(self)
83 83
84 84 def __del__(self):
85 85 """Object disposal"""
86 86 self.close()
87 87
88 88 def close(self):
89 89 """Close method. Closes the widget which closes the underlying comm.
90 90 When the comm is closed, all of the widget views are automatically
91 91 removed from the frontend."""
92 92 if not self.closed:
93 93 self.closed = True
94 94 self._close_communication()
95 95 Widget.widgets.remove(self)
96 96
97 97 @property
98 98 def comm(self):
99 99 if self._comm is None:
100 100 self._open_communication()
101 101 return self._comm
102 102
103 103 @property
104 104 def model_id(self):
105 105 return self.comm.comm_id
106 106
107 107 # Event handlers
108 108 def _handle_msg(self, msg):
109 """Called when a msg is recieved from the frontend"""
109 """Called when a msg is received from the frontend"""
110 110 data = msg['content']['data']
111 111 method = data['method']
112 112
113 113 # TODO: Log unrecog.
114 114
115 115 # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
116 116 if method == 'backbone' and 'sync_data' in data:
117 117 sync_data = data['sync_data']
118 self._handle_recieve_state(sync_data) # handles all methods
118 self._handle_receive_state(sync_data) # handles all methods
119 119
120 120 # Handle a custom msg from the front-end
121 121 elif method == 'custom':
122 122 if 'custom_content' in data:
123 123 self._handle_custom_msg(data['custom_content'])
124 124
125 125
126 def _handle_recieve_state(self, sync_data):
127 """Called when a state is recieved from the frontend."""
126 def _handle_receive_state(self, sync_data):
127 """Called when a state is received from the frontend."""
128 128 for name in self.keys:
129 129 if name in sync_data:
130 130 value = self._unpack_widgets(sync_data[name])
131 131 with PropertyLock(self, name, value):
132 132 setattr(self, name, value)
133 133
134 134
135 135 def _handle_custom_msg(self, content):
136 """Called when a custom msg is recieved."""
136 """Called when a custom msg is received."""
137 137 for handler in self._msg_callbacks:
138 138 if callable(handler):
139 139 argspec = inspect.getargspec(handler)
140 140 nargs = len(argspec[0])
141 141
142 142 # Bound methods have an additional 'self' argument
143 143 if isinstance(handler, types.MethodType):
144 144 nargs -= 1
145 145
146 146 # Call the callback
147 147 if nargs == 1:
148 148 handler(content)
149 149 elif nargs == 2:
150 150 handler(self, content)
151 151 else:
152 152 raise TypeError('Widget msg callback must ' \
153 153 'accept 1 or 2 arguments, not %d.' % nargs)
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 should_send_property(self, name, 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 for handler in self._display_callbacks:
166 166 if callable(handler):
167 167 argspec = inspect.getargspec(handler)
168 168 nargs = len(argspec[0])
169 169
170 170 # Bound methods have an additional 'self' argument
171 171 if isinstance(handler, types.MethodType):
172 172 nargs -= 1
173 173
174 174 # Call the callback
175 175 if nargs == 0:
176 176 handler()
177 177 elif nargs == 1:
178 178 handler(self)
179 179 else:
180 180 handler(self, **kwargs)
181 181
182 182 # Public methods
183 183 def send_state(self, key=None):
184 184 """Sends the widget state, or a piece of it, to the frontend.
185 185
186 186 Parameters
187 187 ----------
188 188 key : unicode (optional)
189 189 A single property's name to sync with the frontend.
190 190 """
191 191 self._send({"method": "update",
192 192 "state": self.get_state()})
193 193
194 194 def get_state(self, key=None):
195 195 """Gets the widget state, or a piece of it.
196 196
197 197 Parameters
198 198 ----------
199 199 key : unicode (optional)
200 200 A single property's name to get.
201 201 """
202 202 state = {}
203 203
204 204 # If a key is provided, just send the state of that key.
205 205 if key is None:
206 206 keys = self.keys[:]
207 207 else:
208 208 keys = [key]
209 209 for k in keys:
210 210 state[k] = self._pack_widgets(getattr(self, k))
211 211 return state
212 212
213 213
214 214 def _pack_widgets(self, values):
215 215 """This function recursively converts all widget instances to model id
216 216 strings.
217 217
218 218 Children widgets will be stored and transmitted to the front-end by
219 219 their model ids."""
220 220 if isinstance(values, dict):
221 221 new_dict = {}
222 222 for key in values.keys():
223 223 new_dict[key] = self._pack_widgets(values[key])
224 224 return new_dict
225 225 elif isinstance(values, list):
226 226 new_list = []
227 227 for value in values:
228 228 new_list.append(self._pack_widgets(value))
229 229 return new_list
230 230 elif isinstance(values, Widget):
231 231 return values.model_id
232 232 else:
233 233 return values
234 234
235 235
236 236 def _unpack_widgets(self, values):
237 237 """This function recursively converts all model id strings to widget
238 238 instances.
239 239
240 240 Children widgets will be stored and transmitted to the front-end by
241 241 their model ids."""
242 242 if isinstance(values, dict):
243 243 new_dict = {}
244 244 for key in values.keys():
245 245 new_dict[key] = self._unpack_widgets(values[key])
246 246 return new_dict
247 247 elif isinstance(values, list):
248 248 new_list = []
249 249 for value in values:
250 250 new_list.append(self._unpack_widgets(value))
251 251 return new_list
252 252 elif isinstance(values, string_types):
253 253 for widget in Widget.widgets:
254 254 if widget.model_id == values:
255 255 return widget
256 256 return values
257 257 else:
258 258 return values
259 259
260 260
261 261 def send(self, content):
262 262 """Sends a custom msg to the widget model in the front-end.
263 263
264 264 Parameters
265 265 ----------
266 266 content : dict
267 267 Content of the message to send.
268 268 """
269 269 self._send({"method": "custom",
270 270 "custom_content": content})
271 271
272 272
273 273 def on_msg(self, callback, remove=False): # TODO: Use lambdas and inspect here
274 """Register or unregister a callback for when a custom msg is recieved
274 """Register or unregister a callback for when a custom msg is received
275 275 from the front-end.
276 276
277 277 Parameters
278 278 ----------
279 279 callback: method handler
280 280 Can have a signature of:
281 281 - callback(content)
282 282 - callback(sender, content)
283 283 remove: bool
284 284 True if the callback should be unregistered."""
285 285 if remove and callback in self._msg_callbacks:
286 286 self._msg_callbacks.remove(callback)
287 287 elif not remove and not callback in self._msg_callbacks:
288 288 self._msg_callbacks.append(callback)
289 289
290 290
291 291 def on_displayed(self, callback, remove=False):
292 292 """Register or unregister a callback to be called when the widget has
293 293 been displayed.
294 294
295 295 Parameters
296 296 ----------
297 297 callback: method handler
298 298 Can have a signature of:
299 299 - callback()
300 300 - callback(sender)
301 301 - callback(sender, **kwargs)
302 302 kwargs from display call passed through without modification.
303 303 remove: bool
304 304 True if the callback should be unregistered."""
305 305 if remove and callback in self._display_callbacks:
306 306 self._display_callbacks.remove(callback)
307 307 elif not remove and not callback in self._display_callbacks:
308 308 self._display_callbacks.append(callback)
309 309
310 310
311 311 # Support methods
312 312 def _repr_widget_(self, **kwargs):
313 313 """Function that is called when `IPython.display.display` is called on
314 314 the widget."""
315 315
316 316 # Show view. By sending a display message, the comm is opened and the
317 317 # initial state is sent.
318 318 self._send({"method": "display"})
319 319 self._handle_displayed(**kwargs)
320 320
321 321
322 322 def _open_communication(self):
323 323 """Opens a communication with the front-end."""
324 324 # Create a comm.
325 325 self._comm = Comm(target_name=self.target_name)
326 326 self._comm.on_msg(self._handle_msg)
327 327 self._comm.on_close(self._close_communication)
328 328
329 329 # first update
330 330 self.send_state()
331 331
332 332
333 333 def _close_communication(self):
334 334 """Closes a communication with the front-end."""
335 335 if self._comm is not None:
336 336 try:
337 337 self._comm.close() # TODO: Check
338 338 finally:
339 339 self._comm = None
340 340
341 341
342 342 def _send(self, msg):
343 343 """Sends a message to the model in the front-end"""
344 344 self.comm.send(msg)
345 345
346 346
347 347 class DOMWidget(Widget):
348 348 visible = Bool(True, help="Whether or not the widget is visible.")
349 349
350 350 # Private/protected declarations
351 351 _css = Dict() # Internal CSS property dict
352 352
353 353 keys = ['visible', '_css'] + Widget.keys
354 354
355 355 def get_css(self, key, selector=""):
356 356 """Get a CSS property of the widget.
357 357
358 358 Note: This function does not actually request the CSS from the
359 359 front-end; Only properties that have 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.
376 376
377 377 This function has two signatures:
378 378 - set_css(css_dict, selector='')
379 379 - set_css(key, value, selector='')
380 380
381 381 Parameters
382 382 ----------
383 383 css_dict : dict
384 384 CSS key/value pairs to apply
385 385 key: unicode
386 386 CSS key
387 387 value
388 388 CSS value
389 389 selector: unicode (optional)
390 390 JQuery selector to use to apply the CSS key/value. If no selector
391 391 is provided, an empty selector is used. An empty selector makes the
392 392 front-end try to apply the css to a default element. The default
393 393 element is an attribute unique to each view, which is a DOM element
394 394 of the view that should be styled with common CSS (see
395 395 `$el_to_style` in the Javascript code).
396 396 """
397 397 selector = kwargs.get('selector', '')
398 398
399 399 # Signature 1: set_css(css_dict, selector='')
400 400 if len(args) == 1:
401 401 if isinstance(args[0], dict):
402 402 for (key, value) in args[0].items():
403 403 if not (key in self._css[selector] and value == self._css[selector][key]):
404 404 self._css[selector][key] = value
405 405 self.send_state('_css')
406 406 else:
407 407 raise Exception('css_dict must be a dict.')
408 408
409 409 # Signature 2: set_css(key, value, selector='')
410 410 elif len(args) == 2 or len(args) == 3:
411 411
412 412 # Selector can be a positional arg if it's the 3rd value
413 413 if len(args) == 3:
414 414 selector = args[2]
415 415 if selector not in self._css:
416 416 self._css[selector] = {}
417 417
418 418 # Only update the property if it has changed.
419 419 key = args[0]
420 420 value = args[1]
421 421 if not (key in self._css[selector] and value == self._css[selector][key]):
422 422 self._css[selector][key] = value
423 423 self.send_state('_css') # Send new state to client.
424 424 else:
425 425 raise Exception('set_css only accepts 1-3 arguments')
426 426
427 427
428 428 def add_class(self, class_names, selector=""):
429 429 """Add class[es] to a DOM element
430 430
431 431 Parameters
432 432 ----------
433 433 class_names: unicode or list
434 434 Class name(s) to add to the DOM element(s).
435 435 selector: unicode (optional)
436 436 JQuery selector to select the DOM element(s) that the class(es) will
437 437 be added to.
438 438 """
439 439 class_list = class_names
440 440 if isinstance(class_list, list):
441 441 class_list = ' '.join(class_list)
442 442
443 443 self.send({"msg_type": "add_class",
444 444 "class_list": class_list,
445 445 "selector": selector})
446 446
447 447
448 448 def remove_class(self, class_names, selector=""):
449 449 """Remove class[es] from a DOM element
450 450
451 451 Parameters
452 452 ----------
453 453 class_names: unicode or list
454 454 Class name(s) to remove from the DOM element(s).
455 455 selector: unicode (optional)
456 456 JQuery selector to select the DOM element(s) that the class(es) will
457 457 be removed from.
458 458 """
459 459 class_list = class_names
460 460 if isinstance(class_list, list):
461 461 class_list = ' '.join(class_list)
462 462
463 463 self.send({"msg_type": "remove_class",
464 464 "class_list": class_list,
465 465 "selector": selector})
General Comments 0
You need to be logged in to leave comments. Login now