##// END OF EJS Templates
Separate the display from the models on the python side, creating a BaseWidget class....
Jason Grout -
Show More
@@ -30,10 +30,13 b' from IPython.utils.py3compat import string_types'
30 30 #-----------------------------------------------------------------------------
31 31 # Classes
32 32 #-----------------------------------------------------------------------------
33 class Widget(LoggingConfigurable):
33
34 class BaseWidget(LoggingConfigurable):
34 35
35 36 # Shared declarations (Class level)
36 _keys = []
37 _keys = List(Unicode, help="List of keys comprising the state of the model.")
38 _children_attr = List(Unicode, help="List of keys of children objects of the model.")
39 _children_lists_attr = List(Unicode, help="List of keys containing lists of children objects of the model.")
37 40 widget_construction_callback = None
38 41
39 42 def on_widget_constructed(callback):
@@ -54,52 +57,24 b' class Widget(LoggingConfigurable):'
54 57 registered in the frontend to create and sync this widget with.""")
55 58 default_view_name = Unicode(help="""Default view registered in the frontend
56 59 to use to represent the widget.""")
57 parent = Instance('IPython.html.widgets.widget.Widget')
58 visible = Bool(True, help="Whether or not the widget is visible.")
59
60 def _parent_changed(self, name, old, new):
61 if self._displayed:
62 raise Exception('Parent cannot be set because widget has been displayed.')
63 elif new == self:
64 raise Exception('Parent cannot be set to self.')
65 else:
66
67 # Parent/child association
68 if new is not None and not self in new._children:
69 new._children.append(self)
70 if old is not None and self in old._children:
71 old._children.remove(self)
72 60
73 61 # Private/protected declarations
74 62 _property_lock = (None, None) # Last updated (key, value) from the front-end. Prevents echo.
75 _css = Dict() # Internal CSS property dict
76 63 _displayed = False
77
64 _comm = None
78 65
79 66 def __init__(self, **kwargs):
80 67 """Public constructor
81
82 Parameters
83 ----------
84 parent : Widget instance (optional)
85 Widget that this widget instance is child of. When the widget is
86 displayed in the frontend, it's corresponding view will be made
87 child of the parent's view if the parent's view exists already. If
88 the parent's view is displayed, it will automatically display this
89 widget's default view as it's child. The default view can be set
90 via the default_view_name property.
91 68 """
92 self._children = []
93 69 self._display_callbacks = []
94 70 self._msg_callbacks = []
95 super(Widget, self).__init__(**kwargs)
71 super(BaseWidget, self).__init__(**kwargs)
96 72
97 73 # Register after init to allow default values to be specified
98 self.on_trait_change(self._handle_property_changed, self.keys)
99
74 # TODO: register three different handlers, one for each list, and abstract out the common parts
75 self.on_trait_change(self._handle_property_changed, self.keys+self._children_attr+self._children_lists_attr)
100 76 Widget._handle_widget_constructed(self)
101 77
102
103 78 def __del__(self):
104 79 """Object disposal"""
105 80 self.close()
@@ -113,12 +88,17 b' class Widget(LoggingConfigurable):'
113 88
114 89
115 90 # Properties
116 def _get_keys(self):
117 keys = ['visible', '_css']
91 @property
92 def keys(self):
93 keys = ['_children_attr', '_children_lists_attr']
118 94 keys.extend(self._keys)
119 95 return keys
120 keys = property(_get_keys)
121 96
97 @property
98 def comm(self):
99 if self._comm is None:
100 self._open_communication()
101 return self._comm
122 102
123 103 # Event handlers
124 104 def _handle_msg(self, msg):
@@ -179,7 +159,6 b' class Widget(LoggingConfigurable):'
179 159 # Send new state to frontend
180 160 self.send_state(key=name)
181 161
182
183 162 def _handle_displayed(self, **kwargs):
184 163 """Called when a view has been displayed for this widget instance
185 164
@@ -206,7 +185,6 b' class Widget(LoggingConfigurable):'
206 185 else:
207 186 handler(self, **kwargs)
208 187
209
210 188 # Public methods
211 189 def send_state(self, key=None):
212 190 """Sends the widget state, or a piece of it, to the frontend.
@@ -216,119 +194,43 b' class Widget(LoggingConfigurable):'
216 194 key : unicode (optional)
217 195 A single property's name to sync with the frontend.
218 196 """
219 state = {}
220
221 # If a key is provided, just send the state of that key.
222 keys = []
223 if key is None:
224 keys.extend(self.keys)
225 else:
226 keys.append(key)
227 for key in self.keys:
228 try:
229 state[key] = getattr(self, key)
230 except Exception as e:
231 pass # Eat errors, nom nom nom
232 197 self._send({"method": "update",
233 "state": state})
198 "state": self.get_state()})
234 199
235
236 def get_css(self, key, selector=""):
237 """Get a CSS property of the widget. Note, this function does not
238 actually request the CSS from the front-end; Only properties that have
239 been set with set_css can be read.
200 def get_state(self, key=None)
201 """Gets the widget state, or a piece of it.
240 202
241 203 Parameters
242 204 ----------
243 key: unicode
244 CSS key
245 selector: unicode (optional)
246 JQuery selector used when the CSS key/value was set.
247 """
248 if selector in self._css and key in self._css[selector]:
249 return self._css[selector][key]
250 else:
251 return None
252
253
254 def set_css(self, *args, **kwargs):
255 """Set one or more CSS properties of the widget (shared among all of the
256 views). This function has two signatures:
257 - set_css(css_dict, [selector=''])
258 - set_css(key, value, [selector=''])
259
260 Parameters
261 ----------
262 css_dict : dict
263 CSS key/value pairs to apply
264 key: unicode
265 CSS key
266 value
267 CSS value
268 selector: unicode (optional)
269 JQuery selector to use to apply the CSS key/value.
205 key : unicode (optional)
206 A single property's name to get.
270 207 """
271 selector = kwargs.get('selector', '')
208 state = {}
272 209
273 # Signature 1: set_css(css_dict, [selector=''])
274 if len(args) == 1:
275 if isinstance(args[0], dict):
276 for (key, value) in args[0].items():
277 self.set_css(key, value, selector=selector)
210 # If a key is provided, just send the state of that key.
211 if key is None:
212 keys = self.keys[:]
213 children_attr = self._children_attr[:]
214 children_lists_attr = self._children_lists_attr[:]
278 215 else:
279 raise Exception('css_dict must be a dict.')
280
281 # Signature 2: set_css(key, value, [selector=''])
282 elif len(args) == 2 or len(args) == 3:
283
284 # Selector can be a positional arg if it's the 3rd value
285 if len(args) == 3:
286 selector = args[2]
287 if selector not in self._css:
288 self._css[selector] = {}
289
290 # Only update the property if it has changed.
291 key = args[0]
292 value = args[1]
293 if not (key in self._css[selector] and value in self._css[selector][key]):
294 self._css[selector][key] = value
295 self.send_state('_css') # Send new state to client.
216 keys = []
217 children_attr = []
218 children_lists_attr = []
219 if key in self._children_attr:
220 children_attr.append(key)
221 elif key in self._children_lists_attr:
222 children_lists_attr.append(key)
296 223 else:
297 raise Exception('set_css only accepts 1-3 arguments')
298
299
300 def add_class(self, class_name, selector=""):
301 """Add class[es] to a DOM element
302
303 Parameters
304 ----------
305 class_name: unicode
306 Class name(s) to add to the DOM element(s). Multiple class names
307 must be space separated.
308 selector: unicode (optional)
309 JQuery selector to select the DOM element(s) that the class(es) will
310 be added to.
311 """
312 self._send({"method": "add_class",
313 "class_list": class_name,
314 "selector": selector})
315
316
317 def remove_class(self, class_name, selector=""):
318 """Remove class[es] from a DOM element
319
320 Parameters
321 ----------
322 class_name: unicode
323 Class name(s) to remove from the DOM element(s). Multiple class
324 names must be space separated.
325 selector: unicode (optional)
326 JQuery selector to select the DOM element(s) that the class(es) will
327 be removed from.
328 """
329 self._send({"method": "remove_class",
330 "class_list": class_name,
331 "selector": selector})
224 keys.append(key)
225 for k in keys:
226 state[k] = getattr(self, k)
227 for k in children_attr:
228 # automatically create models on the browser side if they aren't already created
229 state[k] = getattr(self, k).comm.comm_id
230 for k in children_lists_attr:
231 # automatically create models on the browser side if they aren't already created
232 state[k] = [i.comm.comm_id for i in getattr(self, k)]
233 return state
332 234
333 235
334 236 def send(self, content):
@@ -398,20 +300,9 b' class Widget(LoggingConfigurable):'
398 300 self.send_state()
399 301
400 302 # Show view.
401 if self.parent is None or self.parent._comm is None:
402 303 self._send({"method": "display", "view_name": view_name})
403 else:
404 self._send({"method": "display",
405 "view_name": view_name,
406 "parent": self.parent._comm.comm_id})
407 self._handle_displayed(**kwargs)
408 304 self._displayed = True
409
410 # Now display children if any.
411 for child in self._children:
412 if child != self:
413 child._repr_widget_()
414 return None
305 self._handle_displayed(**kwargs)
415 306
416 307
417 308 def _open_communication(self):
@@ -434,8 +325,121 b' class Widget(LoggingConfigurable):'
434 325
435 326 def _send(self, msg):
436 327 """Sends a message to the model in the front-end"""
437 if hasattr(self, '_comm') and self._comm is not None:
328 if self._comm is not None:
438 329 self._comm.send(msg)
439 330 return True
440 331 else:
441 332 return False
333
334 class Widget(BaseWidget):
335
336 _children = List(Instance('IPython.html.widgets.widget.Widget'))
337 _children_lists_attr = List(Unicode, ['_children'])
338 visible = Bool(True, help="Whether or not the widget is visible.")
339
340 # Private/protected declarations
341 _css = Dict() # Internal CSS property dict
342
343 # Properties
344 @property
345 def keys(self):
346 keys = ['visible', '_css']
347 keys.extend(super(Widget, self).keys)
348 return keys
349
350 def get_css(self, key, selector=""):
351 """Get a CSS property of the widget. Note, this function does not
352 actually request the CSS from the front-end; Only properties that have
353 been set with set_css can be read.
354
355 Parameters
356 ----------
357 key: unicode
358 CSS key
359 selector: unicode (optional)
360 JQuery selector used when the CSS key/value was set.
361 """
362 if selector in self._css and key in self._css[selector]:
363 return self._css[selector][key]
364 else:
365 return None
366
367
368 def set_css(self, *args, **kwargs):
369 """Set one or more CSS properties of the widget (shared among all of the
370 views). This function has two signatures:
371 - set_css(css_dict, [selector=''])
372 - set_css(key, value, [selector=''])
373
374 Parameters
375 ----------
376 css_dict : dict
377 CSS key/value pairs to apply
378 key: unicode
379 CSS key
380 value
381 CSS value
382 selector: unicode (optional)
383 JQuery selector to use to apply the CSS key/value.
384 """
385 selector = kwargs.get('selector', '')
386
387 # Signature 1: set_css(css_dict, [selector=''])
388 if len(args) == 1:
389 if isinstance(args[0], dict):
390 for (key, value) in args[0].items():
391 self.set_css(key, value, selector=selector)
392 else:
393 raise Exception('css_dict must be a dict.')
394
395 # Signature 2: set_css(key, value, [selector=''])
396 elif len(args) == 2 or len(args) == 3:
397
398 # Selector can be a positional arg if it's the 3rd value
399 if len(args) == 3:
400 selector = args[2]
401 if selector not in self._css:
402 self._css[selector] = {}
403
404 # Only update the property if it has changed.
405 key = args[0]
406 value = args[1]
407 if not (key in self._css[selector] and value in self._css[selector][key]):
408 self._css[selector][key] = value
409 self.send_state('_css') # Send new state to client.
410 else:
411 raise Exception('set_css only accepts 1-3 arguments')
412
413
414 def add_class(self, class_name, selector=""):
415 """Add class[es] to a DOM element
416
417 Parameters
418 ----------
419 class_name: unicode
420 Class name(s) to add to the DOM element(s). Multiple class names
421 must be space separated.
422 selector: unicode (optional)
423 JQuery selector to select the DOM element(s) that the class(es) will
424 be added to.
425 """
426 self._send({"method": "add_class",
427 "class_list": class_name,
428 "selector": selector})
429
430
431 def remove_class(self, class_name, selector=""):
432 """Remove class[es] from a DOM element
433
434 Parameters
435 ----------
436 class_name: unicode
437 Class name(s) to remove from the DOM element(s). Multiple class
438 names must be space separated.
439 selector: unicode (optional)
440 JQuery selector to select the DOM element(s) that the class(es) will
441 be removed from.
442 """
443 self._send({"method": "remove_class",
444 "class_list": class_name,
445 "selector": selector})
General Comments 0
You need to be logged in to leave comments. Login now