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