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