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