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