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