Repository: kivy/kivy

Issue:
Disabled property doesn't work for views used by RecycleView
<!--
The issue tracker is a tool to address bugs.
Please use the #kivy IRC channel on freenode or Stack Overflow for
support questions, more information at https://git.io/vM1yQ.

Before opening a new issue, make sure you do the following:
    * check that your issue isn't already filed: https://git.io/vM1iE
    * prepare a short, runnable example that reproduces the issue
    * reproduce the problem with the latest development version of Kivy
    * double-check that the issue is indeed a bug and not a support request
-->

### Versions

* Python: 2.7
* OS: Ubuntu 14.04
* Kivy: 1.10.1
* Kivy installation method: pip install git+https://github.com/kivy/kivy.git@1.10.1

### Description

Instances of `SomeView` class should be disabled when `disabled` property is set to `True` in `refresh_view_attrs` method, but that's not the case with Kivy 1.10.1 version. Related to https://github.com/kivy/kivy/pull/5537/

### Code and Logs

```python
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.button import Button
from kivy.uix.recycleview.views import RecycleDataViewBehavior


class SomeView(RecycleDataViewBehavior, Button):

    def refresh_view_attrs(self, rv, index, data):
        self.disabled = True
        self.text = 'Disabled is {}'.format(self.disabled)


kv = '''
RecycleView:
    effect_cls: 'ScrollEffect'
    scroll_type: ['bars']
    bar_width: '15dp'
    scroll_wheel_distance: '50dp'
    viewclass: 'SomeView'
    RecycleGridLayout:
        cols: 1
        default_size_hint: 1, None
        default_size: None, dp(50)
        size_hint_y: None
        height: self.minimum_height
'''


class DisabledViewApp(App):

    def build(self):
        rv = Builder.load_string(kv)
        rv.data = [{}] * 30
        return rv


if __name__ == '__main__':
    DisabledViewApp().run()
```



Discussion:
Ok, since 1.10.1, we try to be more clever about disabled management, to avoid for example, when A is a child of B, and A is disabled, to lose that state when B is disabled then enabled again.

When a widget is added to a parent, we initiate its disabled_count to the one of the parent.

But when recycleview manages its state, it constantly removes/adds widgets to itself, resetting their disabled_count state every time, which shows that this behavior is wrong. maybe we should *add* our disabled count to the child's, and *substract* when the child is removed. so the children can manage its own count.

```diff
[INFO   ] [MTD         ] </dev/input/event9> rotation set to 0
[INFO   ] [MTD         ] </dev/input/event11> rotation set to 0
diff --git a/kivy/uix/widget.py b/kivy/uix/widget.py
index 95769b375..abaf320b5 100644
--- a/kivy/uix/widget.py
+++ b/kivy/uix/widget.py
@@ -531,7 +531,7 @@ class Widget(WidgetBase):
                                   % (widget, parent))
         widget.parent = parent = self
         # Child will be disabled if added to a disabled parent.
-        widget._disabled_count = self._disabled_count
+        widget._disabled_count += self._disabled_count
 
         canvas = self.canvas.before if canvas == 'before' else \
             self.canvas.after if canvas == 'after' else self.canvas
@@ -584,6 +584,7 @@ class Widget(WidgetBase):
         elif widget.canvas in self.canvas.before.children:
             self.canvas.before.remove(widget.canvas)
         widget.parent = None
+        widget._disabled_count -= self._disabled_count
 
     def clear_widgets(self, children=None):
```

this seems to work here, but i didn't try more complex case yet.

edit: my old test case for disabled still works, nicely, https://gist.github.com/tshirtman/3db1281dc945f53a9ca9513c5252ad7f so if there is you see no flaw with that, i'll probably commit that soon.

(i probably should make it into a real test)
@tshirtman, I added your changes, but there is still something that we are missing. Check-out this case: 
```python
from kivy.app import App
from kivy.lang import Builder
from kivy.properties import StringProperty
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.recycleview.views import RecycleDataViewBehavior


class SomeView(RecycleDataViewBehavior, BoxLayout):

    key = StringProperty()
    value = StringProperty()

    def refresh_view_attrs(self, rv, index, data):
        self.disabled = data.get('disabled', False)
        self.key = 'Key{}'.format(index)
        self.value = 'Value {}'.format(index)


kv = '''
<SomeView>:
    Label:
        text: root.key
        size_hint_x: None
        text_size: self.size
        halign: 'left'
        valign: 'middle'
    TextInput:
        text: root.value
        multiline: False
        write_tab: False


GridLayout:
    cols: 1
    RecycleView:
        id: rv
        effect_cls: 'ScrollEffect'
        scroll_type: ['bars']
        bar_width: '15dp'
        scroll_wheel_distance: '50dp'
        viewclass: 'SomeView'
        RecycleGridLayout:
            id: layout
            cols: 1
            spacing: '2dp'
            default_size_hint: 1, None
            default_size: None, dp(30)
            size_hint_y: None
            height: self.minimum_height
    Button:
        size_hint_y: None
        height: '50dp'
        text: 'Layout is {}disabled'.format('' if layout.disabled else 'NOT ')
        on_release: layout.disabled = not layout.disabled
'''


class DisabledViewApp(App):

    def build(self):
        root = Builder.load_string(kv)
        data = [{} for _ in xrange(30)]
        for index in xrange(0, len(data), 5):
            data[index]['disabled'] = True
        root.ids.rv.data = data
        return root


if __name__ == '__main__':
    DisabledViewApp().run()
```

If `layout` gets disabled, then all child widgets should get disabled too. In another case when `layout` is enabled and previous state of child widget was enabled then that widget should be enabled.

In example above, every fifth widget is disabled using value from data and others are enabled/disabled by toggling `layout`'s `disabled` property.
Seems that using for: 
- `add_widget` method:
```
for _ in xrange(self._disabled_count):
    widget.inc_disabled()
```
- `remove_widget` method:
```
for _ in xrange(self._disabled_count):
    widget.dec_disabled()
```

solves this case. But, another thing I noticed is that if label is disabled, `disabled_color` doesn't kick-in. If you add: 
```
    def on_disabled(self, view, disabled):
        print(view, disabled)
```
to `SomeView` (and probably any other widget) `disabled` argument is always `None`.
Ah, that makes some sense, i guess it would be better to allow an optional `count` param to `inc_disabled` and `dec_disabled`, to void the loop and still dispatch when doing the increase. I'm not sure about the second issue, will try to check soon.
Soooo, apparently dispatching an AliasProperty has been broken all along!

https://github.com/kivy/kivy/blob/df7de2042ae4e0f820bbe91ab9f485d746c52fae/kivy/properties.pyx#L562-L579

this is the dispatch from `Property`, which `AliasProperty` simply inherits, while it doesn't store anything in `ps.value` since it's using a getter function, so this simply dispatches `None`… great catch!

patch is pretty straightforward.

```diff --git a/kivy/properties.pyx b/kivy/properties.pyx
index d27d53e25..234657674 100644
--- a/kivy/properties.pyx
+++ b/kivy/properties.pyx
@@ -1471,6 +1471,11 @@ cdef class AliasProperty(Property):
             ps.value = self.get(obj)
             self.dispatch(obj)
 
+    cpdef dispatch(self, EventDispatcher obj):
+        cdef PropertyStorage ps = obj.__storage[self._name]
+        ps.observers.dispatch(obj, ps.getter(obj), None, None, 0)
+
+
 cdef class VariableListProperty(Property):
     '''A ListProperty that allows you to work with a variable amount of
     list items and to expand them to the desired list size.
diff --git a/kivy/uix/widget.py b/kivy/uix/widget.py
index 95769b375..5f474d5b0 100644
--- a/kivy/uix/widget.py
+++ b/kivy/uix/widget.py
@@ -531,7 +531,7 @@ class Widget(WidgetBase):
                                   % (widget, parent))
         widget.parent = parent = self
         # Child will be disabled if added to a disabled parent.
-        widget._disabled_count = self._disabled_count
+        widget.inc_disabled(self._disabled_count)
 
         canvas = self.canvas.before if canvas == 'before' else \
             self.canvas.after if canvas == 'after' else self.canvas
@@ -584,6 +584,7 @@ class Widget(WidgetBase):
         elif widget.canvas in self.canvas.before.children:
             self.canvas.before.remove(widget.canvas)
         widget.parent = None
+        widget.dec_disabled(self._disabled_count)
 
     def clear_widgets(self, children=None):
         '''
@@ -1319,19 +1320,19 @@ class Widget(WidgetBase):
             else:
                 self.dec_disabled()
 
-    def inc_disabled(self):
-        self._disabled_count += 1
-        if self._disabled_count == 1:
+    def inc_disabled(self, count=1):
+        self._disabled_count += count
+        if self._disabled_count - count < 1 <= self._disabled_count:
             self.property('disabled').dispatch(self)
         for c in self.children:
-            c.inc_disabled()
+            c.inc_disabled(count)
 
-    def dec_disabled(self):
-        self._disabled_count -= 1
-        if self._disabled_count == 0:
+    def dec_disabled(self, count=1):
+        self._disabled_count -= count
+        if self._disabled_count <= 0 < self._disabled_count + count:
             self.property('disabled').dispatch(self)
         for c in self.children:
-            c.dec_disabled()
+            c.dec_disabled(count)
 
     disabled = AliasProperty(get_disabled, set_disabled)
     '''Indicates whether this widget can interact with input or not.
```

this does seem to fix both examples.

I approve solution for the first problem, but for the second one I don't see how `cache` is honored if set to `True`. `AliasProperty.set` method checks if setter has returned true-like value and then updates `ps.value`, so if value is cached then setter should return `True` to set a new value. 

Therefore, in `disptach` method, instead of: 
`ps.observers.dispatch(obj, ps.getter(obj), None, None, 0)` 
we should do:
`ps.observers.dispatch(obj, self.get(obj), None, None, 0)`

and probably update `get` method to cache the value for case when `bind_objects` is an empty list. 
```
cpdef get(self, EventDispatcher obj):
    cdef PropertyStorage ps = obj.__storage[self._name]
    if self.use_cache:
        if ps.alias_initial:
            ps.alias_initial = 0
            ps.value = ps.getter(obj)
        return ps.value
    return ps.getter(obj)
```
And maybe we should use `cache=True` more often, as there are properties with "heavy" getters.
That second one probably deserves a separate issue/fix, i'll create a PR with that updated version, thanks for your feedback!

Resolving patch:
diff --git a/kivy/uix/widget.py b/kivy/uix/widget.py
--- a/kivy/uix/widget.py
+++ b/kivy/uix/widget.py
@@ -531,7 +531,7 @@ def add_widget(self, widget, index=0, canvas=None):
                                   % (widget, parent))
         widget.parent = parent = self
         # Child will be disabled if added to a disabled parent.
-        widget._disabled_count = self._disabled_count
+        widget.inc_disabled(self._disabled_count)
 
         canvas = self.canvas.before if canvas == 'before' else \
             self.canvas.after if canvas == 'after' else self.canvas
@@ -584,6 +584,7 @@ def remove_widget(self, widget):
         elif widget.canvas in self.canvas.before.children:
             self.canvas.before.remove(widget.canvas)
         widget.parent = None
+        widget.dec_disabled(self._disabled_count)
 
     def clear_widgets(self, children=None):
         '''
@@ -1318,20 +1319,21 @@ def set_disabled(self, value):
                 self.inc_disabled()
             else:
                 self.dec_disabled()
+            return True
 
-    def inc_disabled(self):
-        self._disabled_count += 1
-        if self._disabled_count == 1:
+    def inc_disabled(self, count=1):
+        self._disabled_count += count
+        if self._disabled_count - count < 1 <= self._disabled_count:
             self.property('disabled').dispatch(self)
         for c in self.children:
-            c.inc_disabled()
+            c.inc_disabled(count)
 
-    def dec_disabled(self):
-        self._disabled_count -= 1
-        if self._disabled_count == 0:
+    def dec_disabled(self, count=1):
+        self._disabled_count -= count
+        if self._disabled_count <= 0 < self._disabled_count + count:
             self.property('disabled').dispatch(self)
         for c in self.children:
-            c.dec_disabled()
+            c.dec_disabled(count)
 
     disabled = AliasProperty(get_disabled, set_disabled)
     '''Indicates whether this widget can interact with input or not.


Test patch: