##// 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 # Classes
31 # Classes
32 #-----------------------------------------------------------------------------
32 #-----------------------------------------------------------------------------
33 class Widget(LoggingConfigurable):
33
34 class BaseWidget(LoggingConfigurable):
34
35
35 # Shared declarations (Class level)
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 widget_construction_callback = None
40 widget_construction_callback = None
38
41
39 def on_widget_constructed(callback):
42 def on_widget_constructed(callback):
@@ -54,72 +57,49 b' class Widget(LoggingConfigurable):'
54 registered in the frontend to create and sync this widget with.""")
57 registered in the frontend to create and sync this widget with.""")
55 default_view_name = Unicode(help="""Default view registered in the frontend
58 default_view_name = Unicode(help="""Default view registered in the frontend
56 to use to represent the widget.""")
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 # Private/protected declarations
61 # Private/protected declarations
74 _property_lock = (None, None) # Last updated (key, value) from the front-end. Prevents echo.
62 _property_lock = (None, None) # Last updated (key, value) from the front-end. Prevents echo.
75 _css = Dict() # Internal CSS property dict
76 _displayed = False
63 _displayed = False
77
64 _comm = None
78
65
79 def __init__(self, **kwargs):
66 def __init__(self, **kwargs):
80 """Public constructor
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 self._display_callbacks = []
69 self._display_callbacks = []
94 self._msg_callbacks = []
70 self._msg_callbacks = []
95 super(Widget, self).__init__(**kwargs)
71 super(BaseWidget, self).__init__(**kwargs)
96
97 # Register after init to allow default values to be specified
98 self.on_trait_change(self._handle_property_changed, self.keys)
99
72
73 # Register after init to allow default values to be specified
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 Widget._handle_widget_constructed(self)
76 Widget._handle_widget_constructed(self)
101
77
102
103 def __del__(self):
78 def __del__(self):
104 """Object disposal"""
79 """Object disposal"""
105 self.close()
80 self.close()
106
81
107
82
108 def close(self):
83 def close(self):
109 """Close method. Closes the widget which closes the underlying comm.
84 """Close method. Closes the widget which closes the underlying comm.
110 When the comm is closed, all of the widget views are automatically
85 When the comm is closed, all of the widget views are automatically
111 removed from the frontend."""
86 removed from the frontend."""
112 self._close_communication()
87 self._close_communication()
113
88
114
89
115 # Properties
90 # Properties
116 def _get_keys(self):
91 @property
117 keys = ['visible', '_css']
92 def keys(self):
93 keys = ['_children_attr', '_children_lists_attr']
118 keys.extend(self._keys)
94 keys.extend(self._keys)
119 return keys
95 return keys
120 keys = property(_get_keys)
121
122
96
97 @property
98 def comm(self):
99 if self._comm is None:
100 self._open_communication()
101 return self._comm
102
123 # Event handlers
103 # Event handlers
124 def _handle_msg(self, msg):
104 def _handle_msg(self, msg):
125 """Called when a msg is recieved from the frontend"""
105 """Called when a msg is recieved from the frontend"""
@@ -158,8 +138,8 b' class Widget(LoggingConfigurable):'
158 else:
138 else:
159 raise TypeError('Widget msg callback must ' \
139 raise TypeError('Widget msg callback must ' \
160 'accept 1 or 2 arguments, not %d.' % nargs)
140 'accept 1 or 2 arguments, not %d.' % nargs)
161
141
162
142
163 def _handle_recieve_state(self, sync_data):
143 def _handle_recieve_state(self, sync_data):
164 """Called when a state is recieved from the frontend."""
144 """Called when a state is recieved from the frontend."""
165 # Use _keys instead of keys - Don't get retrieve the css from the client side.
145 # Use _keys instead of keys - Don't get retrieve the css from the client side.
@@ -170,8 +150,8 b' class Widget(LoggingConfigurable):'
170 setattr(self, name, sync_data[name])
150 setattr(self, name, sync_data[name])
171 finally:
151 finally:
172 self._property_lock = (None, None)
152 self._property_lock = (None, None)
173
153
174
154
175 def _handle_property_changed(self, name, old, new):
155 def _handle_property_changed(self, name, old, new):
176 """Called when a proeprty has been changed."""
156 """Called when a proeprty has been changed."""
177 # Make sure this isn't information that the front-end just sent us.
157 # Make sure this isn't information that the front-end just sent us.
@@ -179,7 +159,6 b' class Widget(LoggingConfigurable):'
179 # Send new state to frontend
159 # Send new state to frontend
180 self.send_state(key=name)
160 self.send_state(key=name)
181
161
182
183 def _handle_displayed(self, **kwargs):
162 def _handle_displayed(self, **kwargs):
184 """Called when a view has been displayed for this widget instance
163 """Called when a view has been displayed for this widget instance
185
164
@@ -205,8 +184,7 b' class Widget(LoggingConfigurable):'
205 handler(self, kwargs.get('view_name', None))
184 handler(self, kwargs.get('view_name', None))
206 else:
185 else:
207 handler(self, **kwargs)
186 handler(self, **kwargs)
208
187
209
210 # Public methods
188 # Public methods
211 def send_state(self, key=None):
189 def send_state(self, key=None):
212 """Sends the widget state, or a piece of it, to the frontend.
190 """Sends the widget state, or a piece of it, to the frontend.
@@ -216,119 +194,43 b' class Widget(LoggingConfigurable):'
216 key : unicode (optional)
194 key : unicode (optional)
217 A single property's name to sync with the frontend.
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 self._send({"method": "update",
197 self._send({"method": "update",
233 "state": state})
198 "state": self.get_state()})
234
199
235
200 def get_state(self, key=None)
236 def get_css(self, key, selector=""):
201 """Gets the widget state, or a piece of it.
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.
240
241 Parameters
242 ----------
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
202
260 Parameters
203 Parameters
261 ----------
204 ----------
262 css_dict : dict
205 key : unicode (optional)
263 CSS key/value pairs to apply
206 A single property's name to get.
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.
270 """
207 """
271 selector = kwargs.get('selector', '')
208 state = {}
272
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)
278 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
209
284 # Selector can be a positional arg if it's the 3rd value
210 # If a key is provided, just send the state of that key.
285 if len(args) == 3:
211 if key is None:
286 selector = args[2]
212 keys = self.keys[:]
287 if selector not in self._css:
213 children_attr = self._children_attr[:]
288 self._css[selector] = {}
214 children_lists_attr = self._children_lists_attr[:]
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.
296 else:
215 else:
297 raise Exception('set_css only accepts 1-3 arguments')
216 keys = []
298
217 children_attr = []
299
218 children_lists_attr = []
300 def add_class(self, class_name, selector=""):
219 if key in self._children_attr:
301 """Add class[es] to a DOM element
220 children_attr.append(key)
302
221 elif key in self._children_lists_attr:
303 Parameters
222 children_lists_attr.append(key)
304 ----------
223 else:
305 class_name: unicode
224 keys.append(key)
306 Class name(s) to add to the DOM element(s). Multiple class names
225 for k in keys:
307 must be space separated.
226 state[k] = getattr(self, k)
308 selector: unicode (optional)
227 for k in children_attr:
309 JQuery selector to select the DOM element(s) that the class(es) will
228 # automatically create models on the browser side if they aren't already created
310 be added to.
229 state[k] = getattr(self, k).comm.comm_id
311 """
230 for k in children_lists_attr:
312 self._send({"method": "add_class",
231 # automatically create models on the browser side if they aren't already created
313 "class_list": class_name,
232 state[k] = [i.comm.comm_id for i in getattr(self, k)]
314 "selector": selector})
233 return state
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})
332
234
333
235
334 def send(self, content):
236 def send(self, content):
@@ -398,20 +300,9 b' class Widget(LoggingConfigurable):'
398 self.send_state()
300 self.send_state()
399
301
400 # Show view.
302 # Show view.
401 if self.parent is None or self.parent._comm is None:
303 self._send({"method": "display", "view_name": view_name})
402 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 self._displayed = True
304 self._displayed = True
409
305 self._handle_displayed(**kwargs)
410 # Now display children if any.
411 for child in self._children:
412 if child != self:
413 child._repr_widget_()
414 return None
415
306
416
307
417 def _open_communication(self):
308 def _open_communication(self):
@@ -434,8 +325,121 b' class Widget(LoggingConfigurable):'
434
325
435 def _send(self, msg):
326 def _send(self, msg):
436 """Sends a message to the model in the front-end"""
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 self._comm.send(msg)
329 self._comm.send(msg)
439 return True
330 return True
440 else:
331 else:
441 return False
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