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