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