diff --git a/IPython/html/widgets/widget.py b/IPython/html/widgets/widget.py
index 8615c59de..5329625a6 100644
--- a/IPython/html/widgets/widget.py
+++ b/IPython/html/widgets/widget.py
@@ -30,10 +30,13 @@ from IPython.utils.py3compat import string_types
#-----------------------------------------------------------------------------
# Classes
#-----------------------------------------------------------------------------
-class Widget(LoggingConfigurable):
+
+class BaseWidget(LoggingConfigurable):
# Shared declarations (Class level)
- _keys = []
+ _keys = List(Unicode, help="List of keys comprising the state of the model.")
+ _children_attr = List(Unicode, help="List of keys of children objects of the model.")
+ _children_lists_attr = List(Unicode, help="List of keys containing lists of children objects of the model.")
widget_construction_callback = None
def on_widget_constructed(callback):
@@ -54,72 +57,49 @@ class Widget(LoggingConfigurable):
registered in the frontend to create and sync this widget with.""")
default_view_name = Unicode(help="""Default view registered in the frontend
to use to represent the widget.""")
- parent = Instance('IPython.html.widgets.widget.Widget')
- visible = Bool(True, help="Whether or not the widget is visible.")
-
- def _parent_changed(self, name, old, new):
- if self._displayed:
- raise Exception('Parent cannot be set because widget has been displayed.')
- elif new == self:
- raise Exception('Parent cannot be set to self.')
- else:
-
- # Parent/child association
- if new is not None and not self in new._children:
- new._children.append(self)
- if old is not None and self in old._children:
- old._children.remove(self)
# Private/protected declarations
_property_lock = (None, None) # Last updated (key, value) from the front-end. Prevents echo.
- _css = Dict() # Internal CSS property dict
_displayed = False
-
+ _comm = None
def __init__(self, **kwargs):
"""Public constructor
-
- Parameters
- ----------
- parent : Widget instance (optional)
- Widget that this widget instance is child of. When the widget is
- displayed in the frontend, it's corresponding view will be made
- child of the parent's view if the parent's view exists already. If
- the parent's view is displayed, it will automatically display this
- widget's default view as it's child. The default view can be set
- via the default_view_name property.
"""
- self._children = []
self._display_callbacks = []
self._msg_callbacks = []
- super(Widget, self).__init__(**kwargs)
-
- # Register after init to allow default values to be specified
- self.on_trait_change(self._handle_property_changed, self.keys)
+ super(BaseWidget, self).__init__(**kwargs)
+ # Register after init to allow default values to be specified
+ # TODO: register three different handlers, one for each list, and abstract out the common parts
+ self.on_trait_change(self._handle_property_changed, self.keys+self._children_attr+self._children_lists_attr)
Widget._handle_widget_constructed(self)
-
-
+
def __del__(self):
"""Object disposal"""
self.close()
-
+
def close(self):
"""Close method. Closes the widget which closes the underlying comm.
- When the comm is closed, all of the widget views are automatically
+ When the comm is closed, all of the widget views are automatically
removed from the frontend."""
self._close_communication()
- # Properties
- def _get_keys(self):
- keys = ['visible', '_css']
+ # Properties
+ @property
+ def keys(self):
+ keys = ['_children_attr', '_children_lists_attr']
keys.extend(self._keys)
return keys
- keys = property(_get_keys)
-
+ @property
+ def comm(self):
+ if self._comm is None:
+ self._open_communication()
+ return self._comm
+
# Event handlers
def _handle_msg(self, msg):
"""Called when a msg is recieved from the frontend"""
@@ -158,8 +138,8 @@ class Widget(LoggingConfigurable):
else:
raise TypeError('Widget msg callback must ' \
'accept 1 or 2 arguments, not %d.' % nargs)
-
-
+
+
def _handle_recieve_state(self, sync_data):
"""Called when a state is recieved from the frontend."""
# Use _keys instead of keys - Don't get retrieve the css from the client side.
@@ -170,8 +150,8 @@ class Widget(LoggingConfigurable):
setattr(self, name, sync_data[name])
finally:
self._property_lock = (None, None)
-
-
+
+
def _handle_property_changed(self, name, old, new):
"""Called when a proeprty has been changed."""
# Make sure this isn't information that the front-end just sent us.
@@ -179,7 +159,6 @@ class Widget(LoggingConfigurable):
# Send new state to frontend
self.send_state(key=name)
-
def _handle_displayed(self, **kwargs):
"""Called when a view has been displayed for this widget instance
@@ -205,8 +184,7 @@ class Widget(LoggingConfigurable):
handler(self, kwargs.get('view_name', None))
else:
handler(self, **kwargs)
-
-
+
# Public methods
def send_state(self, key=None):
"""Sends the widget state, or a piece of it, to the frontend.
@@ -216,119 +194,43 @@ class Widget(LoggingConfigurable):
key : unicode (optional)
A single property's name to sync with the frontend.
"""
+ self._send({"method": "update",
+ "state": self.get_state()})
+
+ def get_state(self, key=None)
+ """Gets the widget state, or a piece of it.
+
+ Parameters
+ ----------
+ key : unicode (optional)
+ A single property's name to get.
+ """
state = {}
# If a key is provided, just send the state of that key.
- keys = []
if key is None:
- keys.extend(self.keys)
+ keys = self.keys[:]
+ children_attr = self._children_attr[:]
+ children_lists_attr = self._children_lists_attr[:]
else:
- keys.append(key)
- for key in self.keys:
- try:
- state[key] = getattr(self, key)
- except Exception as e:
- pass # Eat errors, nom nom nom
- self._send({"method": "update",
- "state": state})
-
-
- def get_css(self, key, selector=""):
- """Get a CSS property of the widget. Note, this function does not
- actually request the CSS from the front-end; Only properties that have
- been set with set_css can be read.
-
- Parameters
- ----------
- key: unicode
- CSS key
- selector: unicode (optional)
- JQuery selector used when the CSS key/value was set.
- """
- if selector in self._css and key in self._css[selector]:
- return self._css[selector][key]
- else:
- return None
-
-
- def set_css(self, *args, **kwargs):
- """Set one or more CSS properties of the widget (shared among all of the
- views). This function has two signatures:
- - set_css(css_dict, [selector=''])
- - set_css(key, value, [selector=''])
-
- Parameters
- ----------
- css_dict : dict
- CSS key/value pairs to apply
- key: unicode
- CSS key
- value
- CSS value
- selector: unicode (optional)
- JQuery selector to use to apply the CSS key/value.
- """
- selector = kwargs.get('selector', '')
-
- # Signature 1: set_css(css_dict, [selector=''])
- if len(args) == 1:
- if isinstance(args[0], dict):
- for (key, value) in args[0].items():
- self.set_css(key, value, selector=selector)
+ keys = []
+ children_attr = []
+ children_lists_attr = []
+ if key in self._children_attr:
+ children_attr.append(key)
+ elif key in self._children_lists_attr:
+ children_lists_attr.append(key)
else:
- raise Exception('css_dict must be a dict.')
-
- # Signature 2: set_css(key, value, [selector=''])
- elif len(args) == 2 or len(args) == 3:
-
- # Selector can be a positional arg if it's the 3rd value
- if len(args) == 3:
- selector = args[2]
- if selector not in self._css:
- self._css[selector] = {}
-
- # Only update the property if it has changed.
- key = args[0]
- value = args[1]
- if not (key in self._css[selector] and value in self._css[selector][key]):
- self._css[selector][key] = value
- self.send_state('_css') # Send new state to client.
- else:
- raise Exception('set_css only accepts 1-3 arguments')
-
-
- def add_class(self, class_name, selector=""):
- """Add class[es] to a DOM element
-
- Parameters
- ----------
- class_name: unicode
- Class name(s) to add to the DOM element(s). Multiple class names
- must be space separated.
- selector: unicode (optional)
- JQuery selector to select the DOM element(s) that the class(es) will
- be added to.
- """
- self._send({"method": "add_class",
- "class_list": class_name,
- "selector": selector})
-
-
- def remove_class(self, class_name, selector=""):
- """Remove class[es] from a DOM element
-
- Parameters
- ----------
- class_name: unicode
- Class name(s) to remove from the DOM element(s). Multiple class
- names must be space separated.
- selector: unicode (optional)
- JQuery selector to select the DOM element(s) that the class(es) will
- be removed from.
- """
- self._send({"method": "remove_class",
- "class_list": class_name,
- "selector": selector})
+ keys.append(key)
+ for k in keys:
+ state[k] = getattr(self, k)
+ for k in children_attr:
+ # automatically create models on the browser side if they aren't already created
+ state[k] = getattr(self, k).comm.comm_id
+ for k in children_lists_attr:
+ # automatically create models on the browser side if they aren't already created
+ state[k] = [i.comm.comm_id for i in getattr(self, k)]
+ return state
def send(self, content):
@@ -398,20 +300,9 @@ class Widget(LoggingConfigurable):
self.send_state()
# Show view.
- if self.parent is None or self.parent._comm is None:
- self._send({"method": "display", "view_name": view_name})
- else:
- self._send({"method": "display",
- "view_name": view_name,
- "parent": self.parent._comm.comm_id})
- self._handle_displayed(**kwargs)
+ self._send({"method": "display", "view_name": view_name})
self._displayed = True
-
- # Now display children if any.
- for child in self._children:
- if child != self:
- child._repr_widget_()
- return None
+ self._handle_displayed(**kwargs)
def _open_communication(self):
@@ -434,8 +325,121 @@ class Widget(LoggingConfigurable):
def _send(self, msg):
"""Sends a message to the model in the front-end"""
- if hasattr(self, '_comm') and self._comm is not None:
+ if self._comm is not None:
self._comm.send(msg)
return True
else:
- return False
+ return False
+
+class Widget(BaseWidget):
+
+ _children = List(Instance('IPython.html.widgets.widget.Widget'))
+ _children_lists_attr = List(Unicode, ['_children'])
+ visible = Bool(True, help="Whether or not the widget is visible.")
+
+ # Private/protected declarations
+ _css = Dict() # Internal CSS property dict
+
+ # Properties
+ @property
+ def keys(self):
+ keys = ['visible', '_css']
+ keys.extend(super(Widget, self).keys)
+ return keys
+
+ def get_css(self, key, selector=""):
+ """Get a CSS property of the widget. Note, this function does not
+ actually request the CSS from the front-end; Only properties that have
+ been set with set_css can be read.
+
+ Parameters
+ ----------
+ key: unicode
+ CSS key
+ selector: unicode (optional)
+ JQuery selector used when the CSS key/value was set.
+ """
+ if selector in self._css and key in self._css[selector]:
+ return self._css[selector][key]
+ else:
+ return None
+
+
+ def set_css(self, *args, **kwargs):
+ """Set one or more CSS properties of the widget (shared among all of the
+ views). This function has two signatures:
+ - set_css(css_dict, [selector=''])
+ - set_css(key, value, [selector=''])
+
+ Parameters
+ ----------
+ css_dict : dict
+ CSS key/value pairs to apply
+ key: unicode
+ CSS key
+ value
+ CSS value
+ selector: unicode (optional)
+ JQuery selector to use to apply the CSS key/value.
+ """
+ selector = kwargs.get('selector', '')
+
+ # Signature 1: set_css(css_dict, [selector=''])
+ if len(args) == 1:
+ if isinstance(args[0], dict):
+ for (key, value) in args[0].items():
+ self.set_css(key, value, selector=selector)
+ else:
+ raise Exception('css_dict must be a dict.')
+
+ # Signature 2: set_css(key, value, [selector=''])
+ elif len(args) == 2 or len(args) == 3:
+
+ # Selector can be a positional arg if it's the 3rd value
+ if len(args) == 3:
+ selector = args[2]
+ if selector not in self._css:
+ self._css[selector] = {}
+
+ # Only update the property if it has changed.
+ key = args[0]
+ value = args[1]
+ if not (key in self._css[selector] and value in self._css[selector][key]):
+ self._css[selector][key] = value
+ self.send_state('_css') # Send new state to client.
+ else:
+ raise Exception('set_css only accepts 1-3 arguments')
+
+
+ def add_class(self, class_name, selector=""):
+ """Add class[es] to a DOM element
+
+ Parameters
+ ----------
+ class_name: unicode
+ Class name(s) to add to the DOM element(s). Multiple class names
+ must be space separated.
+ selector: unicode (optional)
+ JQuery selector to select the DOM element(s) that the class(es) will
+ be added to.
+ """
+ self._send({"method": "add_class",
+ "class_list": class_name,
+ "selector": selector})
+
+
+ def remove_class(self, class_name, selector=""):
+ """Remove class[es] from a DOM element
+
+ Parameters
+ ----------
+ class_name: unicode
+ Class name(s) to remove from the DOM element(s). Multiple class
+ names must be space separated.
+ selector: unicode (optional)
+ JQuery selector to select the DOM element(s) that the class(es) will
+ be removed from.
+ """
+ self._send({"method": "remove_class",
+ "class_list": class_name,
+ "selector": selector})