Repository: kivy/kivy

Issue:
Window resize recursion error while using ScrollView
<!--
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: 3.6
* OS: Linux (Debian Jessie 8.2)
* Kivy: v1.10.1.dev0, git-3f01ba0, 20180228

* Kivy installation method: 
#### 1. Install system dependencies for Kivy
#### 2. pip install Cython==0.27.3
#### 3. pip install git+https://github.com/kivy/kivy.git@master

### Description

I'm using a large custom widget that draws a grid/spots and this is put inside a ScrollView, everything works as intended until the window is resized and a recursion error occurs.


### Code and Logs

```python


[INFO   ] [Logger      ] Record log in /home/xxxx/.kivy/logs/kivy_18-03-01_17.txt
[INFO   ] [Kivy        ] v1.10.1.dev0, git-3f01ba0, 20180228
[INFO   ] [Python      ] v3.6.3 (default, Nov  9 2017, 06:46:20) 
[GCC 4.9.2]
[INFO   ] [Factory     ] 194 symbols loaded
[INFO   ] [Image       ] Providers: img_tex, img_dds, img_sdl2, img_gif (img_pil, img_ffpyplayer ignored)
[INFO   ] [OSC         ] using <multiprocessing> for socket
[INFO   ] [Window      ] Provider: sdl2(['window_egl_rpi'] ignored)
[INFO   ] [GL          ] Using the "OpenGL" graphics system
[INFO   ] [GL          ] Backend used <gl>
[INFO   ] [GL          ] OpenGL version <b'4.5.0 NVIDIA 375.82'>
[INFO   ] [GL          ] OpenGL vendor <b'NVIDIA Corporation'>
[INFO   ] [GL          ] OpenGL renderer <b'GeForce GTX 1050 Ti/PCIe/SSE2'>
[INFO   ] [GL          ] OpenGL parsed version: 4, 5
[INFO   ] [GL          ] Shading version <b'4.50 NVIDIA'>
[INFO   ] [GL          ] Texture max size <32768>
[INFO   ] [GL          ] Texture max units <32>
[INFO   ] [Window      ] auto add sdl2 input provider
[INFO   ] [Window      ] virtual keyboard not allowed, single mode, not docked
[INFO   ] [Base        ] Start application main loop
[INFO   ] [Base        ] Leaving application in progress...



   File "kivy/properties.pyx", line 478, in kivy.properties.Property.__set__
   File "kivy/properties.pyx", line 516, in kivy.properties.Property.set
   File "kivy/properties.pyx", line 571, in kivy.properties.Property.dispatch
   File "kivy/_event.pyx", line 1214, in kivy._event.EventObservers.dispatch
   File "kivy/_event.pyx", line 1120, in kivy._event.EventObservers._dispatch
   File "/home/xxxx/.virtualenvs/kivytest36/lib/python3.6/site-packages/kivy/effects/scroll.py", line 108, in on_value
     self.scroll = self.value
   File "kivy/properties.pyx", line 478, in kivy.properties.Property.__set__
   File "kivy/properties.pyx", line 516, in kivy.properties.Property.set
   File "kivy/properties.pyx", line 571, in kivy.properties.Property.dispatch
   File "kivy/_event.pyx", line 1214, in kivy._event.EventObservers.dispatch
   File "kivy/_event.pyx", line 1120, in kivy._event.EventObservers._dispatch
   File "/home/xxxx/.virtualenvs/kivytest36/lib/python3.6/site-packages/kivy/uix/scrollview.py", line 576, in _update_effect_x
     self.scroll_x = -sx
   File "kivy/properties.pyx", line 478, in kivy.properties.Property.__set__
   File "kivy/properties.pyx", line 516, in kivy.properties.Property.set
   File "kivy/properties.pyx", line 571, in kivy.properties.Property.dispatch
   File "kivy/_event.pyx", line 1214, in kivy._event.EventObservers.dispatch
   File "kivy/_event.pyx", line 1120, in kivy._event.EventObservers._dispatch
   File "/home/xxxx/.virtualenvs/kivytest36/lib/python3.6/site-packages/kivy/uix/scrollview.py", line 557, in _update_effect_bounds
     self._update_effect_x_bounds()
   File "/home/xxxx/.virtualenvs/kivytest36/lib/python3.6/site-packages/kivy/uix/scrollview.py", line 544, in _update_effect_x_bounds
     self.effect_x.value = self.effect_x.min * self.scroll_x

```


### Quick fix

https://github.com/kivy/kivy/blob/3f01ba0b9983ae60ca69d4d34d6b07ad3777c36a/kivy/uix/scrollview.py#L553

This function causes the recursion error, I've put a pass on it temporarily to see if it would solve the issue, it does but I'm not entirely sure if something else is negatively affected. (It doesn't appear so in my case)

```python

    def _update_effect_bounds(self, *args):
        pass
        # if not self._viewport:
        #     return
        # if self.effect_x:
        #     self._update_effect_x_bounds()
        # if self.effect_y:
        #     self._update_effect_y_bounds()

```


### Example code

```python

import kivy
kivy.require("1.10.0")
from kivy.app import App

from kivy.config import Config
Config.set('graphics', 'width', '1920')
Config.set('graphics', 'height', '1080')
from kivy.uix.gridlayout import GridLayout
from kivy.uix.scrollview import ScrollView
from kivy.uix.widget import Widget
from random import randint
from kivy.graphics import Color, Ellipse

# Steps to reproduce error
# Window opens, move scrollbars, resize window, maximize
# ** It might not happen right away.

class MyPaintWidget(Widget):
    def __init__(self, **kwargs):
        super(MyPaintWidget, self).__init__(**kwargs)

        self.size_hint=(None,None)
        self.size=(5000,5000)
        with self.canvas:
            Color(1, 1, 0)
            for x in range(5000):
                Ellipse(pos=(randint(0,self.size[0]),randint(0,self.size[1])), size=(5, 5))


class BaseLayout(GridLayout):
    def __init__(self, **kwargs):
        super(BaseLayout, self).__init__(**kwargs)


class BaseApp(App):
    def __init__(self, **kwargs):
        super(BaseApp, self).__init__(**kwargs)

    def build(self):
        base_layout = BaseLayout()

        step_base = ScrollView(
            size_hint=(1,1),
            do_scroll_y=True,
            do_scroll_x=True)
        step_base.bar_width=20
        step_base.scroll_type = ['bars']

        paint_widget = MyPaintWidget()
        step_base.add_widget(paint_widget)

        return step_base

base_app = BaseApp()
base_app.run()

```
Updating ScrollView from outside and scrolling with mouse after.
When I set ScrollView's scroll_y(x) property from outside (by some button), position of content is updated as supposed. But then when you try to scroll (with mouse wheel) after increasing scroll_y value (still in range 0-1), content and its move animation starts from place before 'outside' updating. When decreasing scroll_y and then scrolls with mouse, it works nicely though.

Test code in gist: https://gist.github.com/Goffrey/5299929
Steps to see problem:
1. Scrolls completely down with wheel.
2. Scroll UP to middle with Up button.
3. Now scroll with mouse wheel:
    \* when scrolling up - it jumps down to little scrolled height from bottom
    \* when scrolling down - it jump completely to the bottom
## <bountysource-plugin>

Want to back this issue? **[Post a bounty on it!](https://www.bountysource.com/issues/295785-updating-scrollview-from-outside-and-scrolling-with-mouse-after?utm_campaign=plugin&utm_content=tracker%2F42681&utm_medium=issues&utm_source=github)** We accept bounties via [Bountysource](https://www.bountysource.com/?utm_campaign=plugin&utm_content=tracker%2F42681&utm_medium=issues&utm_source=github).
</bountysource-plugin>



Discussion:
👋 Thanks for opening your first issue here! Be sure to follow the issue template!

So i trimmed down the example a bit, and tried using a create_trigger to fix it, it looks like it mitigate the issue quite a lot, but i still manage to hit it sometime, if i scroll *and* resize the window at the same time (quite fast, too, by toggling floating mode for the window in i3 continuously). people can test this "fix" with the following example, though i'll keep looking for a better one.

```
import kivy
kivy.require("1.10.0")  # noqa
from kivy.app import App

from kivy.config import Config
Config.set('graphics', 'width', '1920')  # noqa
Config.set('graphics', 'height', '1080')  # noqa

from kivy.uix.scrollview import ScrollView
from kivy.uix.widget import Widget
from random import randint
from kivy.graphics import Color, Ellipse
from kivy.effects.dampedscroll import DampedScrollEffect
from kivy.clock import Clock

# Steps to reproduce error
# Window opens, move scrollbars, resize window, maximize
# ** It might not happen right away.


class MyPaintWidget(Widget):
    def __init__(self, **kwargs):
        super(MyPaintWidget, self).__init__(**kwargs)

        self.size_hint = (None, None)
        self.size = (5000, 5000)
        with self.canvas:
            Color(1, 1, 0)
            for x in range(5000):
                Ellipse(
                    pos=(randint(0, self.size[0]), randint(0, self.size[1])),
                    size=(5, 5)
                )


class SafeScrollEffect(DampedScrollEffect):
    def __init__(self, **kwargs):
        super(SafeScrollEffect, self).__init__(**kwargs)
        self.on_value = Clock.create_trigger(
            super(SafeScrollEffect, self).on_value)


class BaseApp(App):
    def build(self):
        step_base = ScrollView(effect_cls=SafeScrollEffect)
        step_base.bar_width = 20
        step_base.scroll_type = ['content', 'bars']

        paint_widget = MyPaintWidget()
        step_base.add_widget(paint_widget)

        return step_base


base_app = BaseApp()
base_app.run()
```
I had the same issue with a scrollview that was being resized (by an Animation) while trying to scroll it, current workaround is to disable the scrollview while it's being animated.
I have the same problem, it occurs when I have both x and y scroll enabled, and only happens when I resize the window in y axis after I have scrolled the horizontal bar. If I don't move the horizontal bar at all since the program start, I don't get the RecursionError.
Completely the same as @rodrigocfaria's case. @tshirtman's patch has worked when `scroll_type=['content', 'bars']`, but has not worked when `scroll_type=['bars']`.
Yes, I discovered the same, dual axis scroll with scroll_type = ['bars'] breaks Kivy.

possibly the same issue here: https://github.com/spesmilo/electrum/issues/5189
Attempting to resolve. Here is what I learned at the moment: (The kivy version is 1.11.1.)

- Confirmed the situation that abnormal values ​​are assigned to _viewport, height, width of ScrollView class in the process of changing window size.

- All do_layouts of the upper widget will change the width and height of the ScrollView with value that before layout converges every time the window size is changed.
This change triggers update_effect_bounds every time.

- _viewport is reset by a “bad” widget, such as tabbed_panel, that add_widget a child again in the layout process. 
(Show tabbedpanel.py line 686. This is a pretty bad implementation and should be fixed.)
The size of _viewport at this point is a small size before optimization is performed, but ScrollView performs calculations by referring to it.

I will continue to work on it, but please let me know if you have noticed anything.
Mostly solved.

Modify ScrollView as follows:

```
def _update_effect_x(self, *args):
    vp = self._viewport
    if not vp or not self.effect_x:
        return
    sw = vp.width - self.width
    if sw < 1:
        return
    sx = self.effect_x.scroll / float(sw)
    self.scroll_x = -sx
    self._trigger_update_from_scroll()

def _update_effect_y(self, *args):
    vp = self._viewport
    if not vp or not self.effect_y:
        return
    sh = vp.height - self.height
    if sh < 1:
        return
    sy = self.effect_y.scroll / float(sh)
    self.scroll_y = -sy
    self._trigger_update_from_scroll()
```

(For your reference: 1.11.1 scrollview.py)
```
def _update_effect_x(self, *args):
    vp = self._viewport
    if not vp or not self.effect_x:
        return

    if self.effect_x.is_manual:
        sw = vp.width - self._effect_x_start_width
    else:
        sw = vp.width - self.width
    if sw < 1:
        return
    sx = self.effect_x.scroll / float(sw)
    self.scroll_x = -sx
    self._trigger_update_from_scroll()

def _update_effect_y(self, *args):
    vp = self._viewport
    if not vp or not self.effect_y:
        return
    if self.effect_y.is_manual:
        sh = vp.height - self._effect_y_start_height
    else:
        sh = vp.height - self.height
    if sh < 1:
        return
    sy = self.effect_y.scroll / float(sh)
    self.scroll_y = -sy
    self._trigger_update_from_scroll()

```

It seems that line602,616 (specialization with the is_manual flag) in scrollview.py was causing problems.
This flag seems to be True when the effect is started, but the effect may stop when the window is resized for some reason.
I was having this issue, but it's because I had created a class that inherited from ScrollView and had foolishly overridden `__len__`. 
I encountered a similar problem when updating scroll position from code. The ScrollView uses two extra properties - `_scroll_x_mouse` and `_scroll_y_mouse` - which are not updated when you set `scroll_x` or `scroll_y`.

I just submitted a pull request: https://github.com/kivy/kivy/pull/1079

Ran your test code with these changes, and the buttons work fine with the scroll wheel.

I was away. So I've checked it this morning and.. Thanks a lot! ;)

This issue still happens on version 1.8 and 1.9.

Here is some info:
https://groups.google.com/forum/?hl=en-US&fromgroups=#!topic/kivy-users/PlSwk_MXe3s

I can confirm that scrollview is jumping to the bottom in certain cases.

Hi, i checked the code to try to track the issue. It seems that the problem is on the _update_effect_x and y. Unfortunately it is a holiday in my country and I won't be able to look further to this issue until Monday, but this should give you a pointer if you want to take a look.

Thanks for revisiting this issue so fast.

Adding further info:
calling _update_effect_x_bounds() and _update_effect_y_bounds() after setting the values of scroll_x and scroll_y will remove problem of it jumping back to the old position.

But if you try to replace the scroll view when it is still moving from a long drag, the position will still bug.

ref
https://github.com/kivy/kivy/issues/3382

A possible fix is mentioned here:
https://github.com/kivy/kivy/issues/2038#issuecomment-46158938

I worked around this in my app by calling `_update_effect_y_bounds()` when scroll_y was changed from code, i'm not familiar enough with SV code to know how wrong/inefficient it is, but maybe adding it to _update_from_scroll would solve the issue in a more general way.
I just wanted to do a pull request that solves this problem and accidentaly commited the changes.
This commit makes scrollview not reset back to start position when scroll_y or scroll_x value is set from outside and then scrolled with a touch event https://github.com/kivy/kivy/commit/dce021f9d6d4b20f32e93876302c89e27e1b3f44.
It triggers _update_effect_bounds() on every scroll_x and scroll_y value change.
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.


Resolving patch:
diff --git a/kivy/uix/scrollview.py b/kivy/uix/scrollview.py
--- a/kivy/uix/scrollview.py
+++ b/kivy/uix/scrollview.py
@@ -566,8 +566,6 @@ def __init__(self, **kwargs):
         fbind('scroll_y', trigger_update_from_scroll)
         fbind('pos', trigger_update_from_scroll)
         fbind('size', trigger_update_from_scroll)
-        fbind('scroll_y', self._update_effect_bounds)
-        fbind('scroll_x', self._update_effect_bounds)
 
         trigger_update_from_scroll()
         update_effect_widget()
@@ -749,6 +747,8 @@ def on_scroll_start(self, touch, check_children=True):
                 e = self.effect_y if ud['in_bar_y'] else self.effect_x
 
             if e:
+                # make sure the effect's value is synced to scroll value
+                self._update_effect_bounds()
                 if btn in ('scrolldown', 'scrollleft'):
                     if self.smooth_scroll_end:
                         e.velocity -= m * self.smooth_scroll_end
@@ -799,12 +799,20 @@ def on_scroll_start(self, touch, check_children=True):
             'time': touch.time_start,
         }
 
-        if self.do_scroll_x and self.effect_x and not ud['in_bar_x']:
+        if (self.do_scroll_x and self.effect_x and not ud['in_bar_x']
+                and not ud['in_bar_y']):
+            # make sure the effect's value is synced to scroll value
+            self._update_effect_bounds()
+
             self._effect_x_start_width = self.width
             self.effect_x.start(touch.x)
             self._scroll_x_mouse = self.scroll_x
 
-        if self.do_scroll_y and self.effect_y and not ud['in_bar_y']:
+        if (self.do_scroll_y and self.effect_y and not ud['in_bar_x']
+                and not ud['in_bar_y']):
+            # make sure the effect's value is synced to scroll value
+            self._update_effect_bounds()
+
             self._effect_y_start_height = self.height
             self.effect_y.start(touch.y)
             self._scroll_y_mouse = self.scroll_y


Test patch: