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