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