Repository: kivy/kivy

Issue:
If ScrollView doesn't have any child, 'to_local()' and 'to_parent()' don't work.
# Versions

* Python: 3.7.1
* OS: Linux Mint 18.2 Mate Edition 64bit
* Kivy: v2.0.0.dev0, git-8b53e56, 20190710
* Kivy installation method: `git clone` then `pipenv install -e .`

### Description

If `ScrollView` doesn't have any child, `to_local()` and `to_parent()` don't work.

### Code and Logs

```python
from kivy.clock import Clock
from kivy.uix.widget import Widget
from kivy.uix.scrollview import ScrollView

sv = ScrollView(pos=(100, 100, ))

Clock.tick()  # ensure ScrollView.g_translate is updated (actually not)
print(sv.to_local(0, 0))
print(sv.to_parent(0, 0))

sv.add_widget(Widget())

Clock.tick()  # ensure ScrollView.g_translate is updated
print(sv.to_local(0, 0))
print(sv.to_parent(0, 0))
```

```
(0.0, 0.0)
(0.0, 0.0)
(-100.0, -100.0)
(100.0, 100.0)
```


Discussion:

Resolving patch:
diff --git a/kivy/uix/scrollview.py b/kivy/uix/scrollview.py
--- a/kivy/uix/scrollview.py
+++ b/kivy/uix/scrollview.py
@@ -543,6 +543,7 @@ def __init__(self, **kwargs):
         fbind('scroll_y', self._update_effect_bounds)
         fbind('scroll_x', self._update_effect_bounds)
 
+        trigger_update_from_scroll()
         update_effect_widget()
         update_effect_x_bounds()
         update_effect_y_bounds()
@@ -1028,6 +1029,7 @@ def update_from_scroll(self, *largs):
         if the size of the content changes.
         '''
         if not self._viewport:
+            self.g_translate.xy = self.pos
             return
         vp = self._viewport
 


Test patch:
diff --git a/kivy/tests/test_screen.py b/kivy/tests/test_screen.py
--- a/kivy/tests/test_screen.py
+++ b/kivy/tests/test_screen.py
@@ -1,6 +1,6 @@
 import pytest
 
-TRANSITION_CLASS_NAMES = (
+transition_cls_names = (
     'ShaderTransition', 'SlideTransition', 'SwapTransition', 'FadeTransition',
     'WipeTransition', 'FallOutTransition', 'RiseInTransition', 'NoTransition',
     'CardTransition',
@@ -22,7 +22,7 @@ def test_switch_to():
     manager.switch_to(screen2)
 
 
-@pytest.mark.parametrize('transition_cls_name', TRANSITION_CLASS_NAMES)
+@pytest.mark.parametrize('transition_cls_name', transition_cls_names)
 def test_switching_does_not_affect_a_list_of_screens(transition_cls_name):
     import kivy.uix.screenmanager as sm
     transition_cls = getattr(sm, transition_cls_name)
diff --git a/kivy/tests/test_uix_translate_coordinates.py b/kivy/tests/test_uix_translate_coordinates.py
new file mode 100644
--- /dev/null
+++ b/kivy/tests/test_uix_translate_coordinates.py
@@ -0,0 +1,74 @@
+import pytest
+import functools
+
+non_relative_type_widget_cls_names = ('Widget', )
+relative_type_widget_cls_names = (
+    'RelativeLayout', 'Scatter', 'ScrollView',
+)
+all_widget_cls_names = \
+    non_relative_type_widget_cls_names + \
+    relative_type_widget_cls_names
+
+
+@functools.lru_cache(maxsize=1)
+def get_relative_type_widget_classes():
+    from kivy.factory import Factory
+    return tuple(
+        Factory.get(cls_name)
+        for cls_name in relative_type_widget_cls_names
+    )
+
+
+def is_relative_type(widget):
+    return isinstance(widget, get_relative_type_widget_classes())
+
+
+@pytest.mark.parametrize('widget_cls_name', relative_type_widget_cls_names)
+def test_to_local_and_to_parent__relative(widget_cls_name):
+    from kivy.clock import Clock
+    from kivy.factory import Factory
+    widget = Factory.get(widget_cls_name)(pos=(100, 100))
+    Clock.tick()
+    assert widget.to_local(0, 0) == (-100, -100)
+    assert widget.to_parent(0, 0) == (100, 100)
+
+
+@pytest.mark.parametrize('widget_cls_name', non_relative_type_widget_cls_names)
+def test_to_local_and_to_parent__not_relative(widget_cls_name):
+    from kivy.clock import Clock
+    from kivy.factory import Factory
+    widget = Factory.get(widget_cls_name)(pos=(100, 100))
+    Clock.tick()
+    assert widget.to_local(0, 0) == (0, 0)
+    assert widget.to_parent(0, 0) == (0, 0)
+
+
+@pytest.mark.parametrize('root_widget_cls_name', all_widget_cls_names)
+@pytest.mark.parametrize('target_widget_cls_name', all_widget_cls_names)
+def test_to_window_and_to_widget(root_widget_cls_name, target_widget_cls_name):
+    from kivy.clock import Clock
+    from textwrap import dedent
+    from kivy.lang import Builder
+    root = Builder.load_string(dedent('''
+        {}:
+            pos: 100, 0
+
+            # In case the root widget is ScrollView, this cushion is needed,
+            # because ScrollView's direct child is always at pos(0, 0)
+            Widget:
+                pos: 0, 0
+
+                {}:
+                    id: target
+                    pos: 0, 100
+        ''').format(root_widget_cls_name, target_widget_cls_name))
+    Clock.tick()
+    target = root.ids.target
+    if is_relative_type(root):
+        assert target.to_window(*target.pos) == (100, 100)
+        assert target.to_widget(0, 0) == \
+            ((-100, -100) if is_relative_type(target) else (-100, 0))
+    else:
+        assert target.to_window(*target.pos) == (0, 100)
+        assert target.to_widget(0, 0) == \
+            ((0, -100) if is_relative_type(target) else (0, 0))