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