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