##// END OF EJS Templates
Added on_display callback
Jonathan Frederic -
Show More
@@ -1,316 +1,361 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
20
21 import IPython
21 import IPython
22 from IPython.kernel.comm import Comm
22 from IPython.kernel.comm import Comm
23 from IPython.config import LoggingConfigurable
23 from IPython.config import LoggingConfigurable
24 from IPython.utils.traitlets import Unicode, Dict, List, Instance, Bool
24 from IPython.utils.traitlets import Unicode, Dict, List, Instance, Bool
25 from IPython.display import Javascript, display
25 from IPython.display import Javascript, display
26 from IPython.utils.py3compat import string_types
26 from IPython.utils.py3compat import string_types
27
27
28 #-----------------------------------------------------------------------------
28 #-----------------------------------------------------------------------------
29 # Shared
29 # Shared
30 #-----------------------------------------------------------------------------
30 #-----------------------------------------------------------------------------
31 def init_widget_js():
31 def init_widget_js():
32 path = os.path.split(os.path.abspath( __file__ ))[0]
32 path = os.path.split(os.path.abspath( __file__ ))[0]
33 for filepath in glob(os.path.join(path, "*.py")):
33 for filepath in glob(os.path.join(path, "*.py")):
34 filename = os.path.split(filepath)[1]
34 filename = os.path.split(filepath)[1]
35 name = filename.rsplit('.', 1)[0]
35 name = filename.rsplit('.', 1)[0]
36 if not (name == 'widget' or name == '__init__') and name.startswith('widget_'):
36 if not (name == 'widget' or name == '__init__') and name.startswith('widget_'):
37 # Remove 'widget_' from the start of the name before compiling the path.
37 # Remove 'widget_' from the start of the name before compiling the path.
38 js_path = 'static/notebook/js/widgets/%s.js' % name[7:]
38 js_path = 'static/notebook/js/widgets/%s.js' % name[7:]
39 display(Javascript(data='$.getScript($("body").data("baseProjectUrl") + "%s");' % js_path), exclude="text/plain")
39 display(Javascript(data='$.getScript($("body").data("baseProjectUrl") + "%s");' % js_path), exclude="text/plain")
40
40
41
41
42 #-----------------------------------------------------------------------------
42 #-----------------------------------------------------------------------------
43 # Classes
43 # Classes
44 #-----------------------------------------------------------------------------
44 #-----------------------------------------------------------------------------
45 class Widget(LoggingConfigurable):
45 class Widget(LoggingConfigurable):
46
46
47 # Shared declarations
47 # Shared declarations
48 _keys = []
48 _keys = []
49
49
50 # Public declarations
50 # Public declarations
51 target_name = Unicode('widget', help="""Name of the backbone model
51 target_name = Unicode('widget', help="""Name of the backbone model
52 registered in the frontend to create and sync this widget with.""")
52 registered in the frontend to create and sync this widget with.""")
53 default_view_name = Unicode(help="""Default view registered in the frontend
53 default_view_name = Unicode(help="""Default view registered in the frontend
54 to use to represent the widget.""")
54 to use to represent the widget.""")
55 parent = Instance('IPython.html.widgets.widget.Widget')
55 parent = Instance('IPython.html.widgets.widget.Widget')
56 visible = Bool(True, help="Whether or not the widget is visible.")
56 visible = Bool(True, help="Whether or not the widget is visible.")
57
57
58 def _parent_changed(self, name, old, new):
58 def _parent_changed(self, name, old, new):
59 if self._displayed:
59 if self._displayed:
60 raise Exception('Parent cannot be set because widget has been displayed.')
60 raise Exception('Parent cannot be set because widget has been displayed.')
61 elif new == self:
61 elif new == self:
62 raise Exception('Parent cannot be set to self.')
62 raise Exception('Parent cannot be set to self.')
63 else:
63 else:
64
64
65 # Parent/child association
65 # Parent/child association
66 if new is not None and not self in new._children:
66 if new is not None and not self in new._children:
67 new._children.append(self)
67 new._children.append(self)
68 if old is not None and self in old._children:
68 if old is not None and self in old._children:
69 old._children.remove(self)
69 old._children.remove(self)
70
70
71 # Private/protected declarations
71 # Private/protected declarations
72 _property_lock = False
72 _property_lock = False
73 _css = Dict() # Internal CSS property dict
73 _css = Dict() # Internal CSS property dict
74 _add_class = List() # Used to add a js class to a DOM element (call#, selector, class_name)
74 _add_class = List() # Used to add a js class to a DOM element (call#, selector, class_name)
75 _remove_class = List() # Used to remove a js class from a DOM element (call#, selector, class_name)
75 _remove_class = List() # Used to remove a js class from a DOM element (call#, selector, class_name)
76 _displayed = False
76 _displayed = False
77 _comm = None
77 _comm = None
78
78
79
79
80 def __init__(self, **kwargs):
80 def __init__(self, **kwargs):
81 """Public constructor
81 """Public constructor
82
82
83 Parameters
83 Parameters
84 ----------
84 ----------
85 parent : Widget instance (optional)
85 parent : Widget instance (optional)
86 Widget that this widget instance is child of. When the widget is
86 Widget that this widget instance is child of. When the widget is
87 displayed in the frontend, it's corresponding view will be made
87 displayed in the frontend, it's corresponding view will be made
88 child of the parent's view if the parent's view exists already. If
88 child of the parent's view if the parent's view exists already. If
89 the parent's view is displayed, it will automatically display this
89 the parent's view is displayed, it will automatically display this
90 widget's default view as it's child. The default view can be set
90 widget's default view as it's child. The default view can be set
91 via the default_view_name property.
91 via the default_view_name property.
92 """
92 """
93 self._children = []
93 self._children = []
94 self._add_class = [0]
94 self._add_class = [0]
95 self._remove_class = [0]
95 self._remove_class = [0]
96 self._display_callbacks = []
96 super(Widget, self).__init__(**kwargs)
97 super(Widget, self).__init__(**kwargs)
97
98
98 # Register after init to allow default values to be specified
99 # Register after init to allow default values to be specified
99 self.on_trait_change(self._handle_property_changed, self.keys)
100 self.on_trait_change(self._handle_property_changed, self.keys)
100
101
101
102
102 def __del__(self):
103 def __del__(self):
103 """Object disposal"""
104 """Object disposal"""
104 self.close()
105 self.close()
105
106
106
107
107 def close(self):
108 def close(self):
108 """Close method. Closes the widget which closes the underlying comm.
109 """Close method. Closes the widget which closes the underlying comm.
109 When the comm is closed, all of the widget views are automatically
110 When the comm is closed, all of the widget views are automatically
110 removed from the frontend."""
111 removed from the frontend."""
111 self._comm.close()
112 self._comm.close()
112 del self._comm
113 del self._comm
113
114
114
115
115 # Properties
116 # Properties
116 def _get_keys(self):
117 def _get_keys(self):
117 keys = ['visible', '_css', '_add_class', '_remove_class']
118 keys = ['visible', '_css', '_add_class', '_remove_class']
118 keys.extend(self._keys)
119 keys.extend(self._keys)
119 return keys
120 return keys
120 keys = property(_get_keys)
121 keys = property(_get_keys)
121
122
122
123
123 # Event handlers
124 # Event handlers
124 def _handle_msg(self, msg):
125 def _handle_msg(self, msg):
125 """Called when a msg is recieved from the frontend"""
126 """Called when a msg is recieved from the frontend"""
126 # Handle backbone sync methods CREATE, PATCH, and UPDATE
127 # Handle backbone sync methods CREATE, PATCH, and UPDATE
127 sync_method = msg['content']['data']['sync_method']
128 sync_method = msg['content']['data']['sync_method']
128 sync_data = msg['content']['data']['sync_data']
129 sync_data = msg['content']['data']['sync_data']
129 self._handle_recieve_state(sync_data) # handles all methods
130 self._handle_recieve_state(sync_data) # handles all methods
130
131
131
132
132 def _handle_recieve_state(self, sync_data):
133 def _handle_recieve_state(self, sync_data):
133 """Called when a state is recieved from the frontend."""
134 """Called when a state is recieved from the frontend."""
134 self._property_lock = True
135 self._property_lock = True
135 try:
136 try:
136
137
137 # Use _keys instead of keys - Don't get retrieve the css from the client side.
138 # Use _keys instead of keys - Don't get retrieve the css from the client side.
138 for name in self._keys:
139 for name in self._keys:
139 if name in sync_data:
140 if name in sync_data:
140 setattr(self, name, sync_data[name])
141 setattr(self, name, sync_data[name])
141 finally:
142 finally:
142 self._property_lock = False
143 self._property_lock = False
143
144
144
145
145 def _handle_property_changed(self, name, old, new):
146 def _handle_property_changed(self, name, old, new):
146 """Called when a proeprty has been changed."""
147 """Called when a proeprty has been changed."""
147 if not self._property_lock and self._comm is not None:
148 if not self._property_lock and self._comm is not None:
148 # TODO: Validate properties.
149 # TODO: Validate properties.
149 # Send new state to frontend
150 # Send new state to frontend
150 self.send_state(key=name)
151 self.send_state(key=name)
151
152
152
153
153 def _handle_close(self):
154 def _handle_close(self):
154 """Called when the comm is closed by the frontend."""
155 """Called when the comm is closed by the frontend."""
155 self._comm = None
156 self._comm = None
156
157
157
158
158 # Public methods
159 # Public methods
159 def send_state(self, key=None):
160 def send_state(self, key=None):
160 """Sends the widget state, or a piece of it, to the frontend.
161 """Sends the widget state, or a piece of it, to the frontend.
161
162
162 Parameters
163 Parameters
163 ----------
164 ----------
164 key : unicode (optional)
165 key : unicode (optional)
165 A single property's name to sync with the frontend.
166 A single property's name to sync with the frontend.
166 """
167 """
167 if self._comm is not None:
168 if self._comm is not None:
168 state = {}
169 state = {}
169
170
170 # If a key is provided, just send the state of that key.
171 # If a key is provided, just send the state of that key.
171 keys = []
172 keys = []
172 if key is None:
173 if key is None:
173 keys.extend(self.keys)
174 keys.extend(self.keys)
174 else:
175 else:
175 keys.append(key)
176 keys.append(key)
176 for key in self.keys:
177 for key in self.keys:
177 try:
178 try:
178 state[key] = getattr(self, key)
179 state[key] = getattr(self, key)
179 except Exception as e:
180 except Exception as e:
180 pass # Eat errors, nom nom nom
181 pass # Eat errors, nom nom nom
181 self._comm.send({"method": "update",
182 self._comm.send({"method": "update",
182 "state": state})
183 "state": state})
183
184
184
185
185 def get_css(self, key, selector=""):
186 def get_css(self, key, selector=""):
186 """Get a CSS property of the widget. Note, this function does not
187 """Get a CSS property of the widget. Note, this function does not
187 actually request the CSS from the front-end; Only properties that have
188 actually request the CSS from the front-end; Only properties that have
188 been set with set_css can be read.
189 been set with set_css can be read.
189
190
190 Parameters
191 Parameters
191 ----------
192 ----------
192 key: unicode
193 key: unicode
193 CSS key
194 CSS key
194 selector: unicode (optional)
195 selector: unicode (optional)
195 JQuery selector used when the CSS key/value was set.
196 JQuery selector used when the CSS key/value was set.
196 """
197 """
197 if selector in self._css and key in self._css[selector]:
198 if selector in self._css and key in self._css[selector]:
198 return self._css[selector][key]
199 return self._css[selector][key]
199 else:
200 else:
200 return None
201 return None
201
202
202
203
203 def set_css(self, *args, **kwargs):
204 def set_css(self, *args, **kwargs):
204 """Set one or more CSS properties of the widget (shared among all of the
205 """Set one or more CSS properties of the widget (shared among all of the
205 views). This function has two signatures:
206 views). This function has two signatures:
206 - set_css(css_dict, [selector=''])
207 - set_css(css_dict, [selector=''])
207 - set_css(key, value, [selector=''])
208 - set_css(key, value, [selector=''])
208
209
209 Parameters
210 Parameters
210 ----------
211 ----------
211 css_dict : dict
212 css_dict : dict
212 CSS key/value pairs to apply
213 CSS key/value pairs to apply
213 key: unicode
214 key: unicode
214 CSS key
215 CSS key
215 value
216 value
216 CSS value
217 CSS value
217 selector: unicode (optional)
218 selector: unicode (optional)
218 JQuery selector to use to apply the CSS key/value.
219 JQuery selector to use to apply the CSS key/value.
219 """
220 """
220 selector = kwargs.get('selector', '')
221 selector = kwargs.get('selector', '')
221
222
222 # Signature 1: set_css(css_dict, [selector=''])
223 # Signature 1: set_css(css_dict, [selector=''])
223 if len(args) == 1:
224 if len(args) == 1:
224 if isinstance(args[0], dict):
225 if isinstance(args[0], dict):
225 for (key, value) in args[0].items():
226 for (key, value) in args[0].items():
226 self.set_css(key, value, selector=selector)
227 self.set_css(key, value, selector=selector)
227 else:
228 else:
228 raise Exception('css_dict must be a dict.')
229 raise Exception('css_dict must be a dict.')
229
230
230 # Signature 2: set_css(key, value, [selector=''])
231 # Signature 2: set_css(key, value, [selector=''])
231 elif len(args) == 2 or len(args) == 3:
232 elif len(args) == 2 or len(args) == 3:
232
233
233 # Selector can be a positional arg if it's the 3rd value
234 # Selector can be a positional arg if it's the 3rd value
234 if len(args) == 3:
235 if len(args) == 3:
235 selector = args[2]
236 selector = args[2]
236 if selector not in self._css:
237 if selector not in self._css:
237 self._css[selector] = {}
238 self._css[selector] = {}
238
239
239 # Only update the property if it has changed.
240 # Only update the property if it has changed.
240 key = args[0]
241 key = args[0]
241 value = args[1]
242 value = args[1]
242 if not (key in self._css[selector] and value in self._css[selector][key]):
243 if not (key in self._css[selector] and value in self._css[selector][key]):
243 self._css[selector][key] = value
244 self._css[selector][key] = value
244 self.send_state('_css') # Send new state to client.
245 self.send_state('_css') # Send new state to client.
245 else:
246 else:
246 raise Exception('set_css only accepts 1-3 arguments')
247 raise Exception('set_css only accepts 1-3 arguments')
247
248
248
249
249 def add_class(self, class_name, selector=""):
250 def add_class(self, class_name, selector=""):
250 """Add class[es] to a DOM element
251 """Add class[es] to a DOM element
251
252
252 Parameters
253 Parameters
253 ----------
254 ----------
254 class_name: unicode
255 class_name: unicode
255 Class name(s) to add to the DOM element(s). Multiple class names
256 Class name(s) to add to the DOM element(s). Multiple class names
256 must be space separated.
257 must be space separated.
257 selector: unicode (optional)
258 selector: unicode (optional)
258 JQuery selector to select the DOM element(s) that the class(es) will
259 JQuery selector to select the DOM element(s) that the class(es) will
259 be added to.
260 be added to.
260 """
261 """
261 self._add_class = [self._add_class[0] + 1, selector, class_name]
262 self._add_class = [self._add_class[0] + 1, selector, class_name]
262 self.send_state(key='_add_class')
263 self.send_state(key='_add_class')
263
264
264
265
265 def remove_class(self, class_name, selector=""):
266 def remove_class(self, class_name, selector=""):
266 """Remove class[es] from a DOM element
267 """Remove class[es] from a DOM element
267
268
268 Parameters
269 Parameters
269 ----------
270 ----------
270 class_name: unicode
271 class_name: unicode
271 Class name(s) to remove from the DOM element(s). Multiple class
272 Class name(s) to remove from the DOM element(s). Multiple class
272 names must be space separated.
273 names must be space separated.
273 selector: unicode (optional)
274 selector: unicode (optional)
274 JQuery selector to select the DOM element(s) that the class(es) will
275 JQuery selector to select the DOM element(s) that the class(es) will
275 be removed from.
276 be removed from.
276 """
277 """
277 self._remove_class = [self._remove_class[0] + 1, selector, class_name]
278 self._remove_class = [self._remove_class[0] + 1, selector, class_name]
278 self.send_state(key='_remove_class')
279 self.send_state(key='_remove_class')
279
280
280
281
282 def on_displayed(self, callback, remove=False):
283 """Register a callback to be called when the widget has been displayed
284
285 callback: method handler
286 Can have a signature of:
287 - callback()
288 - callback(sender)
289 - callback(sender, view_name)
290 remove: bool
291 True if the callback should be unregistered."""
292 if remove:
293 self._display_callbacks.remove(callback)
294 elif not callback in self._display_callbacks:
295 self._display_callbacks.append(callback)
296
297
298 def handle_displayed(self, view_name):
299 """Called when a view has been displayed for this widget instance
300
301 view_name: unicode
302 Name of the view that was displayed."""
303 for handler in self._display_callbacks:
304 if callable(handler):
305 argspec = inspect.getargspec(handler)
306 nargs = len(argspec[0])
307
308 # Bound methods have an additional 'self' argument
309 if isinstance(handler, types.MethodType):
310 nargs -= 1
311
312 # Call the callback
313 if nargs == 0:
314 handler()
315 elif nargs == 1:
316 handler(self)
317 elif nargs == 2:
318 handler(self, view_name)
319 else:
320 raise TypeError('Widget display callback must ' \
321 'accept 0-2 arguments, not %d.' % nargs)
322
323
324
281 # Support methods
325 # Support methods
282 def _repr_widget_(self, view_name=None):
326 def _repr_widget_(self, view_name=None):
283 """Function that is called when `IPython.display.display` is called on
327 """Function that is called when `IPython.display.display` is called on
284 the widget.
328 the widget.
285
329
286 Parameters
330 Parameters
287 ----------
331 ----------
288 view_name: unicode (optional)
332 view_name: unicode (optional)
289 View to display in the frontend. Overrides default_view_name."""
333 View to display in the frontend. Overrides default_view_name."""
290
334
291 if not view_name:
335 if not view_name:
292 view_name = self.default_view_name
336 view_name = self.default_view_name
293
337
294 # Create a comm.
338 # Create a comm.
295 if self._comm is None:
339 if self._comm is None:
296 self._comm = Comm(target_name=self.target_name)
340 self._comm = Comm(target_name=self.target_name)
297 self._comm.on_msg(self._handle_msg)
341 self._comm.on_msg(self._handle_msg)
298 self._comm.on_close(self._handle_close)
342 self._comm.on_close(self._handle_close)
299
343
300 # Make sure model is syncronized
344 # Make sure model is syncronized
301 self.send_state()
345 self.send_state()
302
346
303 # Show view.
347 # Show view.
304 if self.parent is None or self.parent._comm is None:
348 if self.parent is None or self.parent._comm is None:
305 self._comm.send({"method": "display", "view_name": view_name})
349 self._comm.send({"method": "display", "view_name": view_name})
306 else:
350 else:
307 self._comm.send({"method": "display",
351 self._comm.send({"method": "display",
308 "view_name": view_name,
352 "view_name": view_name,
309 "parent": self.parent._comm.comm_id})
353 "parent": self.parent._comm.comm_id})
310 self._displayed = True
354 self._displayed = True
355 self.handle_displayed(view_name)
311
356
312 # Now display children if any.
357 # Now display children if any.
313 for child in self._children:
358 for child in self._children:
314 if child != self:
359 if child != self:
315 child._repr_widget_()
360 child._repr_widget_()
316 return None
361 return None
General Comments 0
You need to be logged in to leave comments. Login now