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