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})