Repository: kivy/kivy

Issue:
TextInput handles misbehave in rotated Scatter
TextInput handles do not rotate with parent Scatter, which makes them behave strange

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

runTouchApp(Builder.load_string('''
Scatter:
    size: 300, 300
    pos: 300, 300
    rotation: 90
    TextInput:
        text: 'Try to use the selection handles on this text'
'''))
```



Discussion:

Resolving patch:
diff --git a/kivy/uix/textinput.py b/kivy/uix/textinput.py
--- a/kivy/uix/textinput.py
+++ b/kivy/uix/textinput.py
@@ -210,9 +210,20 @@ def _textinput_clear_cache(*l):
 class Selector(ButtonBehavior, Image):
     # Internal class for managing the selection Handles.
 
-    def on_touch_down(self, touch):
-        self._touch_diff = self.top - touch.y
-        return super(Selector, self).on_touch_down(touch)
+    window = ObjectProperty()
+
+    def __init__(self, **kwargs):
+        super(Selector, self).__init__(**kwargs)
+        self.window.bind(on_touch_down=self.on_window_touch_down)
+
+    def on_window_touch_down(self, win, touch):
+        try:
+            touch.push()
+            touch.apply_transform_2d(self.to_widget)
+            self._touch_diff = self.top - touch.y
+            return super(Selector, self).on_touch_down(touch)
+        finally:
+            touch.pop()
 
 
 class TextInputCutCopyPaste(Bubble):
@@ -948,6 +959,10 @@ def on_touch_down(self, touch):
         if self.disabled:
             return
 
+        for child in self.children[:]:
+            if child.on_touch_down(touch):
+                return True
+
         touch_pos = touch.pos
         if not self.collide_point(*touch_pos):
             if self._keyboard_mode == 'multi':
@@ -1052,13 +1067,14 @@ def on_touch_up(self, touch):
                 if handle_middle is None:
                     self._handle_middle = handle_middle = Selector(
                         source=self.handle_image_middle,
+                        window=win,
                         size_hint=(None, None),
                         size=('45dp', '45dp'))
                     handle_middle.bind(on_press=self._handle_pressed,
                                        on_touch_move=self._handle_move,
                                        on_release=self._handle_released)
                 if not self._handle_middle.parent and self.text:
-                    self._win.add_widget(handle_middle)
+                    self.add_widget(handle_middle, canvas='after')
                 self._position_handles(mode='middle')
             return True
 
@@ -1075,9 +1091,7 @@ def _handle_released(self, instance):
 
         self._update_selection()
         self._show_cut_copy_paste(
-            (instance.x + ((1 if instance is self._handle_left else -1)
-                * self._bubble.width / 2) if self._bubble else instance.x,
-                instance.top + self.line_height), self._win, pos_in_window=True)
+	        (instance.center_x, instance.top + self.line_height), self._win)
 
     def _handle_move(self, instance, touch):
         if touch.grab_current != instance:
@@ -1087,7 +1101,7 @@ def _handle_move(self, instance, touch):
         handle_left = self._handle_left
         handle_middle = self._handle_middle
 
-        x, y = self.to_widget(*touch.pos)
+        x, y = touch.pos
         cursor = get_cursor(
             x,
             y + instance._touch_diff + (self.line_height / 2))
@@ -1116,12 +1130,11 @@ def _position_handles(self, *args, **kwargs):
         mode = kwargs.get('mode', 'both')
 
         lh = self.line_height
-        to_win = self.to_window
 
         handle_middle = self._handle_middle
         if handle_middle:
             hp_mid = self.cursor_pos
-            pos = to_win(*hp_mid)
+            pos = hp_mid
             handle_middle.x = pos[0] - handle_middle.width / 2
             handle_middle.top = pos[1] - lh
         if mode[0] == 'm':
@@ -1133,20 +1146,20 @@ def _position_handles(self, *args, **kwargs):
 
         if not self._win:
             self._set_window()
-        self._win.remove_widget(self._handle_middle)
+        self.remove_widget(self._handle_middle)
 
         handle_left = self._handle_left
         if not handle_left:
             return
         hp_left = group[2].pos
-        handle_left.pos = to_win(*hp_left)
+        handle_left.pos = hp_left
         handle_left.x -= handle_left.width
         handle_left.y -= handle_left.height
 
         handle_right = self._handle_right
         last_rect = group[-1]
         hp_right = last_rect.pos[0], last_rect.pos[1]
-        x, y = to_win(*hp_right)
+        x, y = hp_right
         handle_right.x = x + last_rect.size[0]
         handle_right.y = y - handle_right.height
 
@@ -1154,9 +1167,9 @@ def _hide_handles(self, win=None):
         win = win or self._win
         if win is None:
             return
-        self._win.remove_widget(self._handle_right)
-        self._win.remove_widget(self._handle_left)
-        self._win.remove_widget(self._handle_middle)
+        self.remove_widget(self._handle_right)
+        self.remove_widget(self._handle_left)
+        self.remove_widget(self._handle_middle)
 
     def _show_handles(self, dt):
         if not self.use_handles or not self.text:
@@ -1172,6 +1185,7 @@ def _show_handles(self, dt):
         if self._handle_left is None:
             self._handle_left = handle_left = Selector(
                 source=self.handle_image_left,
+                window=win,
                 size_hint=(None, None),
                 size=('45dp', '45dp'))
             handle_left.bind(on_press=self._handle_pressed,
@@ -1179,6 +1193,7 @@ def _show_handles(self, dt):
                              on_release=self._handle_released)
             self._handle_right = handle_right = Selector(
                 source=self.handle_image_right,
+                window=win,
                 size_hint=(None, None),
                 size=('45dp', '45dp'))
             handle_right.bind(on_press=self._handle_pressed,
@@ -1194,8 +1209,8 @@ def _show_handles(self, dt):
         self._trigger_position_handles()
         if self.selection_from != self.selection_to:
             self._handle_left.opacity = self._handle_right.opacity = 0
-            win.add_widget(self._handle_left)
-            win.add_widget(self._handle_right)
+            self.add_widget(self._handle_left, canvas='after')
+            self.add_widget(self._handle_right, canvas='after')
             anim = Animation(opacity=1, d=.4)
             anim.start(self._handle_right)
             anim.start(self._handle_left)
@@ -1214,7 +1229,7 @@ def _show_cut_copy_paste(self, pos, win, parent_changed=False,
             self._win.bind(size=lambda *args: self._hide_cut_copy_paste(win))
             self.bind(cursor_pos=lambda *args: self._hide_cut_copy_paste(win))
         else:
-            win.remove_widget(bubble)
+            self.remove_widget(bubble)
             if not self.parent:
                 return
         if parent_changed:
@@ -1224,44 +1239,50 @@ def _show_cut_copy_paste(self, pos, win, parent_changed=False,
         lh, ls = self.line_height, self.line_spacing
 
         x, y = pos
-        t_pos = (x, y) if pos_in_window else self.to_window(x, y)
+        t_pos = self.to_window(x, y)
         bubble_size = bubble.size
+        bubble_hw = bubble_size[0] / 2.
         win_size = win.size
-        bubble.pos = (t_pos[0] - bubble_size[0] / 2., t_pos[1] + inch(.25))
-        bubble_pos = bubble.pos
+        bubble_pos = (t_pos[0], t_pos[1] + inch(.25))
 
-        if bubble_pos[0] < 0:
+        if (bubble_pos[0] - bubble_hw) < 0:
             # bubble beyond left of window
-            if bubble.pos[1] > (win_size[1] - bubble_size[1]):
+            if bubble_pos[1] > (win_size[1] - bubble_size[1]):
                 # bubble above window height
-                bubble.pos = (0, (t_pos[1]) - (bubble_size[1] + lh + ls))
+                bubble_pos = (bubble_hw, (t_pos[1]) - (lh + ls + inch(.25)))
                 bubble.arrow_pos = 'top_left'
             else:
-                bubble.pos = (0, bubble_pos[1])
+                bubble_pos = (bubble_hw, bubble_pos[1])
                 bubble.arrow_pos = 'bottom_left'
-        elif bubble.right > win_size[0]:
+        elif (bubble_pos[0] + bubble_hw) > win_size[0]:
             # bubble beyond right of window
             if bubble_pos[1] > (win_size[1] - bubble_size[1]):
                 # bubble above window height
-                bubble.pos = (win_size[0] - bubble_size[0],
-                             (t_pos[1]) - (bubble_size[1] + lh + ls))
+                bubble_pos = (win_size[0] - bubble_hw,
+                             (t_pos[1]) - (lh + ls + inch(.25)))
                 bubble.arrow_pos = 'top_right'
             else:
-                bubble.right = win_size[0]
+                bubble_pos = (win_size[0] - bubble_hw, bubble_pos[1])
                 bubble.arrow_pos = 'bottom_right'
         else:
             if bubble_pos[1] > (win_size[1] - bubble_size[1]):
                 # bubble above window height
-                bubble.pos = (bubble_pos[0],
-                             (t_pos[1]) - (bubble_size[1] + lh + ls))
+                bubble_pos = (bubble_pos[0],
+                             (t_pos[1]) - (lh + ls + inch(.25)))
                 bubble.arrow_pos = 'top_mid'
             else:
                 bubble.arrow_pos = 'bottom_mid'
 
+        bubble_pos = self.to_widget(*bubble_pos)
+        bubble.center_x = bubble_pos[0]
+        if bubble.arrow_pos[0] == 't':
+            bubble.top = bubble_pos[1]
+        else:
+            bubble.y = bubble_pos[1]
         bubble.mode = mode
         Animation.cancel_all(bubble)
         bubble.opacity = 0
-        win.add_widget(bubble)
+        self.add_widget(bubble, canvas='after')
         Animation(opacity=1, d=.225).start(bubble)
 
     def _hide_cut_copy_paste(self, win=None):
@@ -1969,7 +1990,7 @@ def _keyboard_on_key_down(self, window, keycode, text, modifiers):
 
             self._hide_handles(win)
             self._hide_cut_copy_paste(win)
-            self._win.remove_widget(self._handle_middle)
+            self.remove_widget(self._handle_middle)
 
             # check for command modes
             if ord(text[0]) == 1:
diff --git a/kivy/uix/widget.py b/kivy/uix/widget.py
--- a/kivy/uix/widget.py
+++ b/kivy/uix/widget.py
@@ -264,7 +264,7 @@ class Widget(WidgetBase):
         Adding a `__del__` method to a class derived from Widget with python
         prior to 3.4 will disable automatic garbage collection for instances
         of that class. This is because the Widget class creates reference
-        cycles, thereby `preventing garbage collection 
+        cycles, thereby `preventing garbage collection
         <https://docs.python.org/2/library/gc.html#gc.garbage>`_.
 
     .. versionchanged:: 1.0.9
@@ -441,7 +441,7 @@ def on_disabled(self, instance, value):
     #
     # Tree management
     #
-    def add_widget(self, widget, index=0):
+    def add_widget(self, widget, index=0, canvas=None):
         '''Add a new widget as a child of this widget.
 
         :Parameters:
@@ -451,6 +451,11 @@ def add_widget(self, widget, index=0):
                 Index to insert the widget in the list
 
                 .. versionadded:: 1.0.5
+            `canvas`: str, defaults to None
+                Canvas to add widget's canvas to. Can be 'before', 'after' or
+                None for the default canvas.
+
+                .. versionadded:: 1.9.0
 
         >>> from kivy.uix.button import Button
         >>> from kivy.uix.slider import Slider
@@ -477,9 +482,12 @@ def add_widget(self, widget, index=0):
         if parent.disabled:
             widget.disabled = True
 
+        canvas = self.canvas.before if canvas == 'before' else \
+            self.canvas.after if canvas == 'after' else self.canvas
+
         if index == 0 or len(self.children) == 0:
             self.children.insert(0, widget)
-            self.canvas.add(widget.canvas)
+            canvas.add(widget.canvas)
         else:
             canvas = self.canvas
             children = self.children
@@ -516,7 +524,12 @@ def remove_widget(self, widget):
         if widget not in self.children:
             return
         self.children.remove(widget)
-        self.canvas.remove(widget.canvas)
+        if widget.canvas in self.canvas.children:
+            self.canvas.remove(widget.canvas)
+        elif widget.canvas in self.canvas.after.children:
+            self.canvas.after.remove(widget.canvas)
+        elif widget.canvas in self.canvas.before.children:
+            self.canvas.before.remove(widget.canvas)
         widget.parent = None
 
     def clear_widgets(self, children=None):


Test patch: