##// END OF EJS Templates
Make the widget property_lock store the JSON state, not the original object...
Jason Grout -
Show More
@@ -1,474 +1,474 b''
1 """Base Widget class. Allows user to create widgets in the back-end that render
1 """Base Widget class. Allows user to create widgets in the back-end that render
2 in the IPython notebook front-end.
2 in the IPython notebook front-end.
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 contextlib import contextmanager
15 from contextlib import contextmanager
16 import collections
16 import collections
17
17
18 from IPython.core.getipython import get_ipython
18 from IPython.core.getipython import get_ipython
19 from IPython.kernel.comm import Comm
19 from IPython.kernel.comm import Comm
20 from IPython.config import LoggingConfigurable
20 from IPython.config import LoggingConfigurable
21 from IPython.utils.traitlets import Unicode, Dict, Instance, Bool, List, Tuple, Int, Set
21 from IPython.utils.traitlets import Unicode, Dict, Instance, Bool, List, Tuple, Int, Set
22 from IPython.utils.py3compat import string_types
22 from IPython.utils.py3compat import string_types
23
23
24 #-----------------------------------------------------------------------------
24 #-----------------------------------------------------------------------------
25 # Classes
25 # Classes
26 #-----------------------------------------------------------------------------
26 #-----------------------------------------------------------------------------
27 class CallbackDispatcher(LoggingConfigurable):
27 class CallbackDispatcher(LoggingConfigurable):
28 """A structure for registering and running callbacks"""
28 """A structure for registering and running callbacks"""
29 callbacks = List()
29 callbacks = List()
30
30
31 def __call__(self, *args, **kwargs):
31 def __call__(self, *args, **kwargs):
32 """Call all of the registered callbacks."""
32 """Call all of the registered callbacks."""
33 value = None
33 value = None
34 for callback in self.callbacks:
34 for callback in self.callbacks:
35 try:
35 try:
36 local_value = callback(*args, **kwargs)
36 local_value = callback(*args, **kwargs)
37 except Exception as e:
37 except Exception as e:
38 ip = get_ipython()
38 ip = get_ipython()
39 if ip is None:
39 if ip is None:
40 self.log.warn("Exception in callback %s: %s", callback, e, exc_info=True)
40 self.log.warn("Exception in callback %s: %s", callback, e, exc_info=True)
41 else:
41 else:
42 ip.showtraceback()
42 ip.showtraceback()
43 else:
43 else:
44 value = local_value if local_value is not None else value
44 value = local_value if local_value is not None else value
45 return value
45 return value
46
46
47 def register_callback(self, callback, remove=False):
47 def register_callback(self, callback, remove=False):
48 """(Un)Register a callback
48 """(Un)Register a callback
49
49
50 Parameters
50 Parameters
51 ----------
51 ----------
52 callback: method handle
52 callback: method handle
53 Method to be registered or unregistered.
53 Method to be registered or unregistered.
54 remove=False: bool
54 remove=False: bool
55 Whether to unregister the callback."""
55 Whether to unregister the callback."""
56
56
57 # (Un)Register the callback.
57 # (Un)Register the callback.
58 if remove and callback in self.callbacks:
58 if remove and callback in self.callbacks:
59 self.callbacks.remove(callback)
59 self.callbacks.remove(callback)
60 elif not remove and callback not in self.callbacks:
60 elif not remove and callback not in self.callbacks:
61 self.callbacks.append(callback)
61 self.callbacks.append(callback)
62
62
63 def _show_traceback(method):
63 def _show_traceback(method):
64 """decorator for showing tracebacks in IPython"""
64 """decorator for showing tracebacks in IPython"""
65 def m(self, *args, **kwargs):
65 def m(self, *args, **kwargs):
66 try:
66 try:
67 return(method(self, *args, **kwargs))
67 return(method(self, *args, **kwargs))
68 except Exception as e:
68 except Exception as e:
69 ip = get_ipython()
69 ip = get_ipython()
70 if ip is None:
70 if ip is None:
71 self.log.warn("Exception in widget method %s: %s", method, e, exc_info=True)
71 self.log.warn("Exception in widget method %s: %s", method, e, exc_info=True)
72 else:
72 else:
73 ip.showtraceback()
73 ip.showtraceback()
74 return m
74 return m
75
75
76 class Widget(LoggingConfigurable):
76 class Widget(LoggingConfigurable):
77 #-------------------------------------------------------------------------
77 #-------------------------------------------------------------------------
78 # Class attributes
78 # Class attributes
79 #-------------------------------------------------------------------------
79 #-------------------------------------------------------------------------
80 _widget_construction_callback = None
80 _widget_construction_callback = None
81 widgets = {}
81 widgets = {}
82
82
83 @staticmethod
83 @staticmethod
84 def on_widget_constructed(callback):
84 def on_widget_constructed(callback):
85 """Registers a callback to be called when a widget is constructed.
85 """Registers a callback to be called when a widget is constructed.
86
86
87 The callback must have the following signature:
87 The callback must have the following signature:
88 callback(widget)"""
88 callback(widget)"""
89 Widget._widget_construction_callback = callback
89 Widget._widget_construction_callback = callback
90
90
91 @staticmethod
91 @staticmethod
92 def _call_widget_constructed(widget):
92 def _call_widget_constructed(widget):
93 """Static method, called when a widget is constructed."""
93 """Static method, called when a widget is constructed."""
94 if Widget._widget_construction_callback is not None and callable(Widget._widget_construction_callback):
94 if Widget._widget_construction_callback is not None and callable(Widget._widget_construction_callback):
95 Widget._widget_construction_callback(widget)
95 Widget._widget_construction_callback(widget)
96
96
97 #-------------------------------------------------------------------------
97 #-------------------------------------------------------------------------
98 # Traits
98 # Traits
99 #-------------------------------------------------------------------------
99 #-------------------------------------------------------------------------
100 _model_name = Unicode('WidgetModel', help="""Name of the backbone model
100 _model_name = Unicode('WidgetModel', help="""Name of the backbone model
101 registered in the front-end to create and sync this widget with.""")
101 registered in the front-end to create and sync this widget with.""")
102 _view_name = Unicode('WidgetView', help="""Default view registered in the front-end
102 _view_name = Unicode('WidgetView', help="""Default view registered in the front-end
103 to use to represent the widget.""", sync=True)
103 to use to represent the widget.""", sync=True)
104 _comm = Instance('IPython.kernel.comm.Comm')
104 _comm = Instance('IPython.kernel.comm.Comm')
105
105
106 msg_throttle = Int(3, sync=True, help="""Maximum number of msgs the
106 msg_throttle = Int(3, sync=True, help="""Maximum number of msgs the
107 front-end can send before receiving an idle msg from the back-end.""")
107 front-end can send before receiving an idle msg from the back-end.""")
108
108
109 keys = List()
109 keys = List()
110 def _keys_default(self):
110 def _keys_default(self):
111 return [name for name in self.traits(sync=True)]
111 return [name for name in self.traits(sync=True)]
112
112
113 _property_lock = Tuple((None, None))
113 _property_lock = Tuple((None, None))
114 _send_state_lock = Int(0)
114 _send_state_lock = Int(0)
115 _states_to_send = Set(allow_none=False)
115 _states_to_send = Set(allow_none=False)
116 _display_callbacks = Instance(CallbackDispatcher, ())
116 _display_callbacks = Instance(CallbackDispatcher, ())
117 _msg_callbacks = Instance(CallbackDispatcher, ())
117 _msg_callbacks = Instance(CallbackDispatcher, ())
118
118
119 #-------------------------------------------------------------------------
119 #-------------------------------------------------------------------------
120 # (Con/de)structor
120 # (Con/de)structor
121 #-------------------------------------------------------------------------
121 #-------------------------------------------------------------------------
122 def __init__(self, **kwargs):
122 def __init__(self, **kwargs):
123 """Public constructor"""
123 """Public constructor"""
124 super(Widget, self).__init__(**kwargs)
124 super(Widget, self).__init__(**kwargs)
125
125
126 self.on_trait_change(self._handle_property_changed, self.keys)
126 self.on_trait_change(self._handle_property_changed, self.keys)
127 Widget._call_widget_constructed(self)
127 Widget._call_widget_constructed(self)
128
128
129 def __del__(self):
129 def __del__(self):
130 """Object disposal"""
130 """Object disposal"""
131 self.close()
131 self.close()
132
132
133 #-------------------------------------------------------------------------
133 #-------------------------------------------------------------------------
134 # Properties
134 # Properties
135 #-------------------------------------------------------------------------
135 #-------------------------------------------------------------------------
136
136
137 @property
137 @property
138 def comm(self):
138 def comm(self):
139 """Gets the Comm associated with this widget.
139 """Gets the Comm associated with this widget.
140
140
141 If a Comm doesn't exist yet, a Comm will be created automagically."""
141 If a Comm doesn't exist yet, a Comm will be created automagically."""
142 if self._comm is None:
142 if self._comm is None:
143 # Create a comm.
143 # Create a comm.
144 self._comm = Comm(target_name=self._model_name)
144 self._comm = Comm(target_name=self._model_name)
145 self._comm.on_msg(self._handle_msg)
145 self._comm.on_msg(self._handle_msg)
146 Widget.widgets[self.model_id] = self
146 Widget.widgets[self.model_id] = self
147
147
148 # first update
148 # first update
149 self.send_state()
149 self.send_state()
150 return self._comm
150 return self._comm
151
151
152 @property
152 @property
153 def model_id(self):
153 def model_id(self):
154 """Gets the model id of this widget.
154 """Gets the model id of this widget.
155
155
156 If a Comm doesn't exist yet, a Comm will be created automagically."""
156 If a Comm doesn't exist yet, a Comm will be created automagically."""
157 return self.comm.comm_id
157 return self.comm.comm_id
158
158
159 #-------------------------------------------------------------------------
159 #-------------------------------------------------------------------------
160 # Methods
160 # Methods
161 #-------------------------------------------------------------------------
161 #-------------------------------------------------------------------------
162
162
163 def close(self):
163 def close(self):
164 """Close method.
164 """Close method.
165
165
166 Closes the underlying comm.
166 Closes the underlying comm.
167 When the comm is closed, all of the widget views are automatically
167 When the comm is closed, all of the widget views are automatically
168 removed from the front-end."""
168 removed from the front-end."""
169 if self._comm is not None:
169 if self._comm is not None:
170 Widget.widgets.pop(self.model_id, None)
170 Widget.widgets.pop(self.model_id, None)
171 self._comm.close()
171 self._comm.close()
172 self._comm = None
172 self._comm = None
173
173
174 def send_state(self, key=None):
174 def send_state(self, key=None):
175 """Sends the widget state, or a piece of it, to the front-end.
175 """Sends the widget state, or a piece of it, to the front-end.
176
176
177 Parameters
177 Parameters
178 ----------
178 ----------
179 key : unicode, or iterable (optional)
179 key : unicode, or iterable (optional)
180 A single property's name or iterable of property names to sync with the front-end.
180 A single property's name or iterable of property names to sync with the front-end.
181 """
181 """
182 self._send({
182 self._send({
183 "method" : "update",
183 "method" : "update",
184 "state" : self.get_state(key=key)
184 "state" : self.get_state(key=key)
185 })
185 })
186
186
187 def get_state(self, key=None):
187 def get_state(self, key=None):
188 """Gets the widget state, or a piece of it.
188 """Gets the widget state, or a piece of it.
189
189
190 Parameters
190 Parameters
191 ----------
191 ----------
192 key : unicode or iterable (optional)
192 key : unicode or iterable (optional)
193 A single property's name or iterable of property names to get.
193 A single property's name or iterable of property names to get.
194 """
194 """
195 if key is None:
195 if key is None:
196 keys = self.keys
196 keys = self.keys
197 elif isinstance(key, string_types):
197 elif isinstance(key, string_types):
198 keys = [key]
198 keys = [key]
199 elif isinstance(key, collections.Iterable):
199 elif isinstance(key, collections.Iterable):
200 keys = key
200 keys = key
201 else:
201 else:
202 raise ValueError("key must be a string, an iterable of keys, or None")
202 raise ValueError("key must be a string, an iterable of keys, or None")
203 state = {}
203 state = {}
204 for k in keys:
204 for k in keys:
205 f = self.trait_metadata(k, 'to_json')
205 f = self.trait_metadata(k, 'to_json', self._trait_to_json)
206 if f is None:
207 f = self._trait_to_json
208 value = getattr(self, k)
206 value = getattr(self, k)
209 state[k] = f(value)
207 state[k] = f(value)
210 return state
208 return state
211
209
212 def send(self, content):
210 def send(self, content):
213 """Sends a custom msg to the widget model in the front-end.
211 """Sends a custom msg to the widget model in the front-end.
214
212
215 Parameters
213 Parameters
216 ----------
214 ----------
217 content : dict
215 content : dict
218 Content of the message to send.
216 Content of the message to send.
219 """
217 """
220 self._send({"method": "custom", "content": content})
218 self._send({"method": "custom", "content": content})
221
219
222 def on_msg(self, callback, remove=False):
220 def on_msg(self, callback, remove=False):
223 """(Un)Register a custom msg receive callback.
221 """(Un)Register a custom msg receive callback.
224
222
225 Parameters
223 Parameters
226 ----------
224 ----------
227 callback: callable
225 callback: callable
228 callback will be passed two arguments when a message arrives::
226 callback will be passed two arguments when a message arrives::
229
227
230 callback(widget, content)
228 callback(widget, content)
231
229
232 remove: bool
230 remove: bool
233 True if the callback should be unregistered."""
231 True if the callback should be unregistered."""
234 self._msg_callbacks.register_callback(callback, remove=remove)
232 self._msg_callbacks.register_callback(callback, remove=remove)
235
233
236 def on_displayed(self, callback, remove=False):
234 def on_displayed(self, callback, remove=False):
237 """(Un)Register a widget displayed callback.
235 """(Un)Register a widget displayed callback.
238
236
239 Parameters
237 Parameters
240 ----------
238 ----------
241 callback: method handler
239 callback: method handler
242 Must have a signature of::
240 Must have a signature of::
243
241
244 callback(widget, **kwargs)
242 callback(widget, **kwargs)
245
243
246 kwargs from display are passed through without modification.
244 kwargs from display are passed through without modification.
247 remove: bool
245 remove: bool
248 True if the callback should be unregistered."""
246 True if the callback should be unregistered."""
249 self._display_callbacks.register_callback(callback, remove=remove)
247 self._display_callbacks.register_callback(callback, remove=remove)
250
248
251 #-------------------------------------------------------------------------
249 #-------------------------------------------------------------------------
252 # Support methods
250 # Support methods
253 #-------------------------------------------------------------------------
251 #-------------------------------------------------------------------------
254 @contextmanager
252 @contextmanager
255 def _lock_property(self, key, value):
253 def _lock_property(self, key, value):
256 """Lock a property-value pair.
254 """Lock a property-value pair.
257
255
256 The value should be the JSON state of the property.
257
258 NOTE: This, in addition to the single lock for all state changes, is
258 NOTE: This, in addition to the single lock for all state changes, is
259 flawed. In the future we may want to look into buffering state changes
259 flawed. In the future we may want to look into buffering state changes
260 back to the front-end."""
260 back to the front-end."""
261 self._property_lock = (key, value)
261 self._property_lock = (key, value)
262 try:
262 try:
263 yield
263 yield
264 finally:
264 finally:
265 self._property_lock = (None, None)
265 self._property_lock = (None, None)
266
266
267 @contextmanager
267 @contextmanager
268 def hold_sync(self):
268 def hold_sync(self):
269 """Hold syncing any state until the context manager is released"""
269 """Hold syncing any state until the context manager is released"""
270 # We increment a value so that this can be nested. Syncing will happen when
270 # We increment a value so that this can be nested. Syncing will happen when
271 # all levels have been released.
271 # all levels have been released.
272 self._send_state_lock += 1
272 self._send_state_lock += 1
273 try:
273 try:
274 yield
274 yield
275 finally:
275 finally:
276 self._send_state_lock -=1
276 self._send_state_lock -=1
277 if self._send_state_lock == 0:
277 if self._send_state_lock == 0:
278 self.send_state(self._states_to_send)
278 self.send_state(self._states_to_send)
279 self._states_to_send.clear()
279 self._states_to_send.clear()
280
280
281 def _should_send_property(self, key, value):
281 def _should_send_property(self, key, value):
282 """Check the property lock (property_lock)"""
282 """Check the property lock (property_lock)"""
283 if (key == self._property_lock[0] and value == self._property_lock[1]):
283 to_json = self.trait_metadata(key, 'to_json', self._trait_to_json)
284 if (key == self._property_lock[0]
285 and to_json(value) == self._property_lock[1]):
284 return False
286 return False
285 elif self._send_state_lock > 0:
287 elif self._send_state_lock > 0:
286 self._states_to_send.add(key)
288 self._states_to_send.add(key)
287 return False
289 return False
288 else:
290 else:
289 return True
291 return True
290
292
291 # Event handlers
293 # Event handlers
292 @_show_traceback
294 @_show_traceback
293 def _handle_msg(self, msg):
295 def _handle_msg(self, msg):
294 """Called when a msg is received from the front-end"""
296 """Called when a msg is received from the front-end"""
295 data = msg['content']['data']
297 data = msg['content']['data']
296 method = data['method']
298 method = data['method']
297 if not method in ['backbone', 'custom']:
299 if not method in ['backbone', 'custom']:
298 self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method)
300 self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method)
299
301
300 # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
302 # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
301 if method == 'backbone' and 'sync_data' in data:
303 if method == 'backbone' and 'sync_data' in data:
302 sync_data = data['sync_data']
304 sync_data = data['sync_data']
303 self._handle_receive_state(sync_data) # handles all methods
305 self._handle_receive_state(sync_data) # handles all methods
304
306
305 # Handle a custom msg from the front-end
307 # Handle a custom msg from the front-end
306 elif method == 'custom':
308 elif method == 'custom':
307 if 'content' in data:
309 if 'content' in data:
308 self._handle_custom_msg(data['content'])
310 self._handle_custom_msg(data['content'])
309
311
310 def _handle_receive_state(self, sync_data):
312 def _handle_receive_state(self, sync_data):
311 """Called when a state is received from the front-end."""
313 """Called when a state is received from the front-end."""
312 for name in self.keys:
314 for name in self.keys:
313 if name in sync_data:
315 if name in sync_data:
314 f = self.trait_metadata(name, 'from_json')
316 json_value = sync_data[name]
315 if f is None:
317 from_json = self.trait_metadata(name, 'from_json', self._trait_from_json)
316 f = self._trait_from_json
318 with self._lock_property(name, json_value):
317 value = f(sync_data[name])
319 setattr(self, name, from_json(json_value))
318 with self._lock_property(name, value):
319 setattr(self, name, value)
320
320
321 def _handle_custom_msg(self, content):
321 def _handle_custom_msg(self, content):
322 """Called when a custom msg is received."""
322 """Called when a custom msg is received."""
323 self._msg_callbacks(self, content)
323 self._msg_callbacks(self, content)
324
324
325 def _handle_property_changed(self, name, old, new):
325 def _handle_property_changed(self, name, old, new):
326 """Called when a property has been changed."""
326 """Called when a property has been changed."""
327 # Make sure this isn't information that the front-end just sent us.
327 # Make sure this isn't information that the front-end just sent us.
328 if self._should_send_property(name, new):
328 if self._should_send_property(name, new):
329 # Send new state to front-end
329 # Send new state to front-end
330 self.send_state(key=name)
330 self.send_state(key=name)
331
331
332 def _handle_displayed(self, **kwargs):
332 def _handle_displayed(self, **kwargs):
333 """Called when a view has been displayed for this widget instance"""
333 """Called when a view has been displayed for this widget instance"""
334 self._display_callbacks(self, **kwargs)
334 self._display_callbacks(self, **kwargs)
335
335
336 def _trait_to_json(self, x):
336 def _trait_to_json(self, x):
337 """Convert a trait value to json
337 """Convert a trait value to json
338
338
339 Traverse lists/tuples and dicts and serialize their values as well.
339 Traverse lists/tuples and dicts and serialize their values as well.
340 Replace any widgets with their model_id
340 Replace any widgets with their model_id
341 """
341 """
342 if isinstance(x, dict):
342 if isinstance(x, dict):
343 return {k: self._trait_to_json(v) for k, v in x.items()}
343 return {k: self._trait_to_json(v) for k, v in x.items()}
344 elif isinstance(x, (list, tuple)):
344 elif isinstance(x, (list, tuple)):
345 return [self._trait_to_json(v) for v in x]
345 return [self._trait_to_json(v) for v in x]
346 elif isinstance(x, Widget):
346 elif isinstance(x, Widget):
347 return "IPY_MODEL_" + x.model_id
347 return "IPY_MODEL_" + x.model_id
348 else:
348 else:
349 return x # Value must be JSON-able
349 return x # Value must be JSON-able
350
350
351 def _trait_from_json(self, x):
351 def _trait_from_json(self, x):
352 """Convert json values to objects
352 """Convert json values to objects
353
353
354 Replace any strings representing valid model id values to Widget references.
354 Replace any strings representing valid model id values to Widget references.
355 """
355 """
356 if isinstance(x, dict):
356 if isinstance(x, dict):
357 return {k: self._trait_from_json(v) for k, v in x.items()}
357 return {k: self._trait_from_json(v) for k, v in x.items()}
358 elif isinstance(x, (list, tuple)):
358 elif isinstance(x, (list, tuple)):
359 return [self._trait_from_json(v) for v in x]
359 return [self._trait_from_json(v) for v in x]
360 elif isinstance(x, string_types) and x.startswith('IPY_MODEL_') and x[10:] in Widget.widgets:
360 elif isinstance(x, string_types) and x.startswith('IPY_MODEL_') and x[10:] in Widget.widgets:
361 # we want to support having child widgets at any level in a hierarchy
361 # we want to support having child widgets at any level in a hierarchy
362 # trusting that a widget UUID will not appear out in the wild
362 # trusting that a widget UUID will not appear out in the wild
363 return Widget.widgets[x]
363 return Widget.widgets[x]
364 else:
364 else:
365 return x
365 return x
366
366
367 def _ipython_display_(self, **kwargs):
367 def _ipython_display_(self, **kwargs):
368 """Called when `IPython.display.display` is called on the widget."""
368 """Called when `IPython.display.display` is called on the widget."""
369 # Show view. By sending a display message, the comm is opened and the
369 # Show view. By sending a display message, the comm is opened and the
370 # initial state is sent.
370 # initial state is sent.
371 self._send({"method": "display"})
371 self._send({"method": "display"})
372 self._handle_displayed(**kwargs)
372 self._handle_displayed(**kwargs)
373
373
374 def _send(self, msg):
374 def _send(self, msg):
375 """Sends a message to the model in the front-end."""
375 """Sends a message to the model in the front-end."""
376 self.comm.send(msg)
376 self.comm.send(msg)
377
377
378
378
379 class DOMWidget(Widget):
379 class DOMWidget(Widget):
380 visible = Bool(True, help="Whether the widget is visible.", sync=True)
380 visible = Bool(True, help="Whether the widget is visible.", sync=True)
381 _css = List(sync=True) # Internal CSS property list: (selector, key, value)
381 _css = List(sync=True) # Internal CSS property list: (selector, key, value)
382
382
383 def get_css(self, key, selector=""):
383 def get_css(self, key, selector=""):
384 """Get a CSS property of the widget.
384 """Get a CSS property of the widget.
385
385
386 Note: This function does not actually request the CSS from the
386 Note: This function does not actually request the CSS from the
387 front-end; Only properties that have been set with set_css can be read.
387 front-end; Only properties that have been set with set_css can be read.
388
388
389 Parameters
389 Parameters
390 ----------
390 ----------
391 key: unicode
391 key: unicode
392 CSS key
392 CSS key
393 selector: unicode (optional)
393 selector: unicode (optional)
394 JQuery selector used when the CSS key/value was set.
394 JQuery selector used when the CSS key/value was set.
395 """
395 """
396 if selector in self._css and key in self._css[selector]:
396 if selector in self._css and key in self._css[selector]:
397 return self._css[selector][key]
397 return self._css[selector][key]
398 else:
398 else:
399 return None
399 return None
400
400
401 def set_css(self, dict_or_key, value=None, selector=''):
401 def set_css(self, dict_or_key, value=None, selector=''):
402 """Set one or more CSS properties of the widget.
402 """Set one or more CSS properties of the widget.
403
403
404 This function has two signatures:
404 This function has two signatures:
405 - set_css(css_dict, selector='')
405 - set_css(css_dict, selector='')
406 - set_css(key, value, selector='')
406 - set_css(key, value, selector='')
407
407
408 Parameters
408 Parameters
409 ----------
409 ----------
410 css_dict : dict
410 css_dict : dict
411 CSS key/value pairs to apply
411 CSS key/value pairs to apply
412 key: unicode
412 key: unicode
413 CSS key
413 CSS key
414 value:
414 value:
415 CSS value
415 CSS value
416 selector: unicode (optional, kwarg only)
416 selector: unicode (optional, kwarg only)
417 JQuery selector to use to apply the CSS key/value. If no selector
417 JQuery selector to use to apply the CSS key/value. If no selector
418 is provided, an empty selector is used. An empty selector makes the
418 is provided, an empty selector is used. An empty selector makes the
419 front-end try to apply the css to the top-level element.
419 front-end try to apply the css to the top-level element.
420 """
420 """
421 if value is None:
421 if value is None:
422 css_dict = dict_or_key
422 css_dict = dict_or_key
423 else:
423 else:
424 css_dict = {dict_or_key: value}
424 css_dict = {dict_or_key: value}
425
425
426 for (key, value) in css_dict.items():
426 for (key, value) in css_dict.items():
427 # First remove the selector/key pair from the css list if it exists.
427 # First remove the selector/key pair from the css list if it exists.
428 # Then add the selector/key pair and new value to the bottom of the
428 # Then add the selector/key pair and new value to the bottom of the
429 # list.
429 # list.
430 self._css = [x for x in self._css if not (x[0]==selector and x[1]==key)]
430 self._css = [x for x in self._css if not (x[0]==selector and x[1]==key)]
431 self._css += [(selector, key, value)]
431 self._css += [(selector, key, value)]
432 self.send_state('_css')
432 self.send_state('_css')
433
433
434 def add_class(self, class_names, selector=""):
434 def add_class(self, class_names, selector=""):
435 """Add class[es] to a DOM element.
435 """Add class[es] to a DOM element.
436
436
437 Parameters
437 Parameters
438 ----------
438 ----------
439 class_names: unicode or list
439 class_names: unicode or list
440 Class name(s) to add to the DOM element(s).
440 Class name(s) to add to the DOM element(s).
441 selector: unicode (optional)
441 selector: unicode (optional)
442 JQuery selector to select the DOM element(s) that the class(es) will
442 JQuery selector to select the DOM element(s) that the class(es) will
443 be added to.
443 be added to.
444 """
444 """
445 class_list = class_names
445 class_list = class_names
446 if isinstance(class_list, (list, tuple)):
446 if isinstance(class_list, (list, tuple)):
447 class_list = ' '.join(class_list)
447 class_list = ' '.join(class_list)
448
448
449 self.send({
449 self.send({
450 "msg_type" : "add_class",
450 "msg_type" : "add_class",
451 "class_list" : class_list,
451 "class_list" : class_list,
452 "selector" : selector
452 "selector" : selector
453 })
453 })
454
454
455 def remove_class(self, class_names, selector=""):
455 def remove_class(self, class_names, selector=""):
456 """Remove class[es] from a DOM element.
456 """Remove class[es] from a DOM element.
457
457
458 Parameters
458 Parameters
459 ----------
459 ----------
460 class_names: unicode or list
460 class_names: unicode or list
461 Class name(s) to remove from the DOM element(s).
461 Class name(s) to remove from the DOM element(s).
462 selector: unicode (optional)
462 selector: unicode (optional)
463 JQuery selector to select the DOM element(s) that the class(es) will
463 JQuery selector to select the DOM element(s) that the class(es) will
464 be removed from.
464 be removed from.
465 """
465 """
466 class_list = class_names
466 class_list = class_names
467 if isinstance(class_list, (list, tuple)):
467 if isinstance(class_list, (list, tuple)):
468 class_list = ' '.join(class_list)
468 class_list = ' '.join(class_list)
469
469
470 self.send({
470 self.send({
471 "msg_type" : "remove_class",
471 "msg_type" : "remove_class",
472 "class_list" : class_list,
472 "class_list" : class_list,
473 "selector" : selector,
473 "selector" : selector,
474 })
474 })
General Comments 0
You need to be logged in to leave comments. Login now