kivy/kivy — pull request #1780 by matham (2014-01-17 10:54:10+00:00)
Title: Fix issues with incorrect touch coordinates

This fixes https://github.com/kivy/kivy/issues/1772. Following is a summary of what I learned about how touch works with different widgets and transformations. Sorry for the long essay, but I don' think all of the following was very clear from the docs. I think this pr should fix these issues once and for all (assuming my understanding is correct)

For a widget, self.pos is calculated using to_local and family. Only for the special widgets (relativelayout, scatter, scrollview), to_local and to_parent are overwritten (try grep) from the default defined in Widget, so for these widgets their child's pos is relative to their x, y.

This means that when collide, which returns self.x <= x <= self.right... in base Widget is called by a child of a special class on the touch, collide will be wrong because x, and self.x are in different coordinates. So that's why in their on_touch_xxx before they propogate the touch to their children by calling super, the touch is converted to local coordinates (the child's parent coordinates), which is relative to their child's parent for these special classes. So now both touch and pos are local (see at the bottom for the difference/explanation between local and relative).

The problem with scrollview is that it does it inconsistently and incorrectly, hence all the bugs so far.

Also, carousel, dragbehavior both originated from scrollview (I believe). Whoever implemented them probably didn't think about the above and just copied the code, so they also convert the touch before calling super. But it's harmless because it doesn't do anything for this class(I did DragBehavior, and I definitly didn't know this, I even mentioned my confusion in the pr...).

In conclusion, this pr makes scrollview consistent and removes the thouch transformation from carousel and dragbehavior. And therefore also fixes the filechooser bug (which was because the touch was not transformed by scrollview before calling super().__on_touch_up()).

__How touch works__

What this means, though, is that in on_touch_xxx, touch is always relative to the pos of the last of these special classes in it parent stack (or Window if none), just like pos and hence is in parent coordinates. This is why touch.apply_transform_2d(self.to_local) is called when transfroming touch by these special classes, because the touch is already in parent coordinates, not in Window coordinates so to_local is correct.

Might it not be better to redefine collide for these widgets so that collide is correct instead of having to transform the touches? I.e. make the touch always be in Window coordinates. In any case, I really think the above should be clearly documented so other people know how it works and what to expect.

There's also something which I only now just got and is not instanly obvious; that is that __local and relative coordinates are not the same thing__ at all. Relative coordinates is always the distance between the coordinates and the x, y position of the widget in question. While local and parent coordinates is the distance between the coordinates and the x, y position of the last special class in the parent stack (where local counts itself in the parent stack if it's special, while parent only includes its special parents).

So, for a parent p, and child c: if p is a normal class, then the coordinates expressed in local or parent coordinates for either p or c's pov are identical. But if p is a special class; from p's pov, local coordinate are relative to it's pos and might be different than when expressed in its parent coordinates. But from c's pov, the coordinates are identical in either system. What on_touch_xxx gets, is always parent coordinates. So to_local has to be used to get it into local coords.

You can use the following code to test. Check the output printed::

from kivy.base import runTouchApp
from kivy.lang import Builder

kv = '''
BoxLayout:
Button:
text: 'left'
on_state: print('left', self.state)
ScrollView:
BoxLayout:
size_hint_x: None
width: root.width
Button:
text: 'middle'
on_state: print('middle', self.state)
ScrollView:
Button:
id: right
size_hint_x: None
width: root.width
text: 'right'
on_state: print('right', self.state)
'''

if __name__ == '__main__':
runTouchApp(Builder.load_string(kv))