##// END OF EJS Templates
remove 3rd callback type from on_displayed
Jonathan Frederic -
Show More
@@ -1,416 +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
154 Parameters
155 ----------
156 [view_name]: unicode (optional kwarg)
157 Name of the view that was displayed."""
158 for handler in self._display_callbacks:
153 for handler in self._display_callbacks:
159 if callable(handler):
154 if callable(handler):
160 argspec = inspect.getargspec(handler)
155 argspec = inspect.getargspec(handler)
161 nargs = len(argspec[0])
156 nargs = len(argspec[0])
162
157
163 # Bound methods have an additional 'self' argument
158 # Bound methods have an additional 'self' argument
164 if isinstance(handler, types.MethodType):
159 if isinstance(handler, types.MethodType):
165 nargs -= 1
160 nargs -= 1
166
161
167 # Call the callback
162 # Call the callback
168 if nargs == 0:
163 if nargs == 0:
169 handler()
164 handler()
170 elif nargs == 1:
165 elif nargs == 1:
171 handler(self)
166 handler(self)
172 elif nargs == 2:
173 handler(self, kwargs.get('view_name', None))
174 else:
167 else:
175 handler(self, **kwargs)
168 handler(self, **kwargs)
176
169
177 # Public methods
170 # Public methods
178 def send_state(self, key=None):
171 def send_state(self, key=None):
179 """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.
180
173
181 Parameters
174 Parameters
182 ----------
175 ----------
183 key : unicode (optional)
176 key : unicode (optional)
184 A single property's name to sync with the frontend.
177 A single property's name to sync with the frontend.
185 """
178 """
186 self._send({"method": "update",
179 self._send({"method": "update",
187 "state": self.get_state()})
180 "state": self.get_state()})
188
181
189 def get_state(self, key=None):
182 def get_state(self, key=None):
190 """Gets the widget state, or a piece of it.
183 """Gets the widget state, or a piece of it.
191
184
192 Parameters
185 Parameters
193 ----------
186 ----------
194 key : unicode (optional)
187 key : unicode (optional)
195 A single property's name to get.
188 A single property's name to get.
196 """
189 """
197 state = {}
190 state = {}
198
191
199 # 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.
200 if key is None:
193 if key is None:
201 keys = self.keys[:]
194 keys = self.keys[:]
202 else:
195 else:
203 keys = [key]
196 keys = [key]
204 for k in keys:
197 for k in keys:
205 value = getattr(self, k)
198 value = getattr(self, k)
206
199
207 # a more elegant solution to encoding Widgets would be
200 # a more elegant solution to encoding Widgets would be
208 # 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
209 # with Widget objects, or maybe just teach the JSON
202 # with Widget objects, or maybe just teach the JSON
210 # encoder to look for a _repr_json property before giving
203 # encoder to look for a _repr_json property before giving
211 # up encoding
204 # up encoding
212 if isinstance(value, Widget):
205 if isinstance(value, Widget):
213 value = value.model_id
206 value = value.model_id
214 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):
215 # assume all elements of the list are widgets
208 # assume all elements of the list are widgets
216 value = [i.model_id for i in value]
209 value = [i.model_id for i in value]
217 state[k] = value
210 state[k] = value
218 return state
211 return state
219
212
220
213
221 def send(self, content):
214 def send(self, content):
222 """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.
223
216
224 Parameters
217 Parameters
225 ----------
218 ----------
226 content : dict
219 content : dict
227 Content of the message to send.
220 Content of the message to send.
228 """
221 """
229 self._send({"method": "custom",
222 self._send({"method": "custom",
230 "custom_content": content})
223 "custom_content": content})
231
224
232
225
233 def on_msg(self, callback, remove=False):
226 def on_msg(self, callback, remove=False):
234 """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
235
228
236 Parameters
229 Parameters
237 ----------
230 ----------
238 callback: method handler
231 callback: method handler
239 Can have a signature of:
232 Can have a signature of:
240 - callback(content)
233 - callback(content)
241 - callback(sender, content)
234 - callback(sender, content)
242 remove: bool
235 remove: bool
243 True if the callback should be unregistered."""
236 True if the callback should be unregistered."""
244 if remove and callback in self._msg_callbacks:
237 if remove and callback in self._msg_callbacks:
245 self._msg_callbacks.remove(callback)
238 self._msg_callbacks.remove(callback)
246 elif not remove and not callback in self._msg_callbacks:
239 elif not remove and not callback in self._msg_callbacks:
247 self._msg_callbacks.append(callback)
240 self._msg_callbacks.append(callback)
248
241
249
242
250 def on_displayed(self, callback, remove=False):
243 def on_displayed(self, callback, remove=False):
251 """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
252
245
253 Parameters
246 Parameters
254 ----------
247 ----------
255 callback: method handler
248 callback: method handler
256 Can have a signature of:
249 Can have a signature of:
257 - callback()
250 - callback()
258 - callback(sender)
251 - callback(sender)
259 - callback(sender, view_name)
260 - callback(sender, **kwargs)
252 - callback(sender, **kwargs)
261 kwargs from display call passed through without modification.
253 kwargs from display call passed through without modification.
262 remove: bool
254 remove: bool
263 True if the callback should be unregistered."""
255 True if the callback should be unregistered."""
264 if remove and callback in self._display_callbacks:
256 if remove and callback in self._display_callbacks:
265 self._display_callbacks.remove(callback)
257 self._display_callbacks.remove(callback)
266 elif not remove and not callback in self._display_callbacks:
258 elif not remove and not callback in self._display_callbacks:
267 self._display_callbacks.append(callback)
259 self._display_callbacks.append(callback)
268
260
269
261
270 # Support methods
262 # Support methods
271 def _repr_widget_(self, **kwargs):
263 def _repr_widget_(self, **kwargs):
272 """Function that is called when `IPython.display.display` is called on
264 """Function that is called when `IPython.display.display` is called on
273 the widget."""
265 the widget."""
274
266
275 # 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
276 # initial state is sent.
268 # initial state is sent.
277 self._send({"method": "display"})
269 self._send({"method": "display"})
278 self._handle_displayed(**kwargs)
270 self._handle_displayed(**kwargs)
279
271
280
272
281 def _open_communication(self):
273 def _open_communication(self):
282 """Opens a communication with the front-end."""
274 """Opens a communication with the front-end."""
283 # Create a comm.
275 # Create a comm.
284 if self._comm is None:
276 if self._comm is None:
285 self._comm = Comm(target_name=self.target_name)
277 self._comm = Comm(target_name=self.target_name)
286 self._comm.on_msg(self._handle_msg)
278 self._comm.on_msg(self._handle_msg)
287 self._comm.on_close(self._close_communication)
279 self._comm.on_close(self._close_communication)
288
280
289 # first update
281 # first update
290 self.send_state()
282 self.send_state()
291
283
292
284
293 def _close_communication(self):
285 def _close_communication(self):
294 """Closes a communication with the front-end."""
286 """Closes a communication with the front-end."""
295 if self._comm is not None:
287 if self._comm is not None:
296 try:
288 try:
297 self._comm.close()
289 self._comm.close()
298 finally:
290 finally:
299 self._comm = None
291 self._comm = None
300
292
301
293
302 def _send(self, msg):
294 def _send(self, msg):
303 """Sends a message to the model in the front-end"""
295 """Sends a message to the model in the front-end"""
304 self.comm.send(msg)
296 self.comm.send(msg)
305
297
306
298
307 class DOMWidget(Widget):
299 class DOMWidget(Widget):
308 visible = Bool(True, help="Whether or not the widget is visible.")
300 visible = Bool(True, help="Whether or not the widget is visible.")
309
301
310 # Private/protected declarations
302 # Private/protected declarations
311 _css = Dict() # Internal CSS property dict
303 _css = Dict() # Internal CSS property dict
312
304
313 keys = ['visible', '_css'] + Widget.keys
305 keys = ['visible', '_css'] + Widget.keys
314
306
315 def get_css(self, key, selector=""):
307 def get_css(self, key, selector=""):
316 """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
317 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
318 been set with set_css can be read.
310 been set with set_css can be read.
319
311
320 Parameters
312 Parameters
321 ----------
313 ----------
322 key: unicode
314 key: unicode
323 CSS key
315 CSS key
324 selector: unicode (optional)
316 selector: unicode (optional)
325 JQuery selector used when the CSS key/value was set.
317 JQuery selector used when the CSS key/value was set.
326 """
318 """
327 if selector in self._css and key in self._css[selector]:
319 if selector in self._css and key in self._css[selector]:
328 return self._css[selector][key]
320 return self._css[selector][key]
329 else:
321 else:
330 return None
322 return None
331
323
332
324
333 def set_css(self, *args, **kwargs):
325 def set_css(self, *args, **kwargs):
334 """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
335 views). This function has two signatures:
327 views). This function has two signatures:
336 - set_css(css_dict, [selector=''])
328 - set_css(css_dict, [selector=''])
337 - set_css(key, value, [selector=''])
329 - set_css(key, value, [selector=''])
338
330
339 Parameters
331 Parameters
340 ----------
332 ----------
341 css_dict : dict
333 css_dict : dict
342 CSS key/value pairs to apply
334 CSS key/value pairs to apply
343 key: unicode
335 key: unicode
344 CSS key
336 CSS key
345 value
337 value
346 CSS value
338 CSS value
347 selector: unicode (optional)
339 selector: unicode (optional)
348 JQuery selector to use to apply the CSS key/value.
340 JQuery selector to use to apply the CSS key/value.
349 """
341 """
350 selector = kwargs.get('selector', '')
342 selector = kwargs.get('selector', '')
351
343
352 # Signature 1: set_css(css_dict, [selector=''])
344 # Signature 1: set_css(css_dict, [selector=''])
353 if len(args) == 1:
345 if len(args) == 1:
354 if isinstance(args[0], dict):
346 if isinstance(args[0], dict):
355 for (key, value) in args[0].items():
347 for (key, value) in args[0].items():
356 self.set_css(key, value, selector=selector)
348 self.set_css(key, value, selector=selector)
357 else:
349 else:
358 raise Exception('css_dict must be a dict.')
350 raise Exception('css_dict must be a dict.')
359
351
360 # Signature 2: set_css(key, value, [selector=''])
352 # Signature 2: set_css(key, value, [selector=''])
361 elif len(args) == 2 or len(args) == 3:
353 elif len(args) == 2 or len(args) == 3:
362
354
363 # 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
364 if len(args) == 3:
356 if len(args) == 3:
365 selector = args[2]
357 selector = args[2]
366 if selector not in self._css:
358 if selector not in self._css:
367 self._css[selector] = {}
359 self._css[selector] = {}
368
360
369 # Only update the property if it has changed.
361 # Only update the property if it has changed.
370 key = args[0]
362 key = args[0]
371 value = args[1]
363 value = args[1]
372 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]):
373 self._css[selector][key] = value
365 self._css[selector][key] = value
374 self.send_state('_css') # Send new state to client.
366 self.send_state('_css') # Send new state to client.
375 else:
367 else:
376 raise Exception('set_css only accepts 1-3 arguments')
368 raise Exception('set_css only accepts 1-3 arguments')
377
369
378
370
379 def add_class(self, class_names, selector=""):
371 def add_class(self, class_names, selector=""):
380 """Add class[es] to a DOM element
372 """Add class[es] to a DOM element
381
373
382 Parameters
374 Parameters
383 ----------
375 ----------
384 class_names: unicode or list
376 class_names: unicode or list
385 Class name(s) to add to the DOM element(s).
377 Class name(s) to add to the DOM element(s).
386 selector: unicode (optional)
378 selector: unicode (optional)
387 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
388 be added to.
380 be added to.
389 """
381 """
390 class_list = class_names
382 class_list = class_names
391 if isinstance(list, class_list):
383 if isinstance(list, class_list):
392 class_list = ' '.join(class_list)
384 class_list = ' '.join(class_list)
393
385
394 self.send({"msg_type": "add_class",
386 self.send({"msg_type": "add_class",
395 "class_list": class_list,
387 "class_list": class_list,
396 "selector": selector})
388 "selector": selector})
397
389
398
390
399 def remove_class(self, class_names, selector=""):
391 def remove_class(self, class_names, selector=""):
400 """Remove class[es] from a DOM element
392 """Remove class[es] from a DOM element
401
393
402 Parameters
394 Parameters
403 ----------
395 ----------
404 class_names: unicode or list
396 class_names: unicode or list
405 Class name(s) to remove from the DOM element(s).
397 Class name(s) to remove from the DOM element(s).
406 selector: unicode (optional)
398 selector: unicode (optional)
407 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
408 be removed from.
400 be removed from.
409 """
401 """
410 class_list = class_names
402 class_list = class_names
411 if isinstance(list, class_list):
403 if isinstance(list, class_list):
412 class_list = ' '.join(class_list)
404 class_list = ' '.join(class_list)
413
405
414 self.send({"msg_type": "remove_class",
406 self.send({"msg_type": "remove_class",
415 "class_list": class_list,
407 "class_list": class_list,
416 "selector": selector})
408 "selector": selector})
General Comments 0
You need to be logged in to leave comments. Login now