Leva7 Leva7 - 6 months ago 152
Python Question

Strange scroll behaviour in Kivy

I have a

ScrollView
and a
Bubble
, that partially overlaps it and contains a
GridLayout


Questions:


  • How can I hide the widget without removing it?



I've read answers to a question on that topic, the suggestions were either to combine the
disabled
and
opacity
properties, which is what I ended up using, and to move the widget temporarily off-screen. Using the first way to hide the
Bubble
, I found out that even if it is disabled, it blocks the scrolling of the view behind it, even though the documentation states that this property


Indicates whether this widget can interact with input or not


So I would assume it shouldn't have blocked the scrolling. Interesting enough, when it wasn't hidden (
disabled=False
), the scrolling passed right through it, which is even more confusing

I also had that
Bubble
before contain a
ScrollView
, which, in turn, held that
GridLayout
. The following question is not an issue anymore, but still an interesting behaviour:


  • Why did the
    Bubble
    pass the scrolling up, but didn't pass the scrolling down?



To understand what I mean, run the code, mouse over the
Bubble
and try scrolling in different directions using the mouse wheel. That is considering the
GridLayout
in the
ScrollView
contains nothing, even though it doesn't affect the behaviour

Here is the code for both questions with some instructions to get the needed behaviour:

from kivy.app import App
from kivy.lang import Builder
from kivy.core.window import Window
from kivy.uix.screenmanager import Screen
from kivy.uix.widget import Widget
from kivy.uix.textinput import TextInput
from kivy.uix.scrollview import ScrollView
from kivy.uix.gridlayout import GridLayout
from kivy.uix.bubble import Bubble
from kivy.properties import ListProperty

Builder.load_string('''
<SmileBubble>:
size_hint: None, None
pos: 220, 90
size: 175, 250

#ScrollView:
#GridLayout:
#rows: 8 # To see the second question's example, uncomment this section
# and comment out the one below
GridLayout:
rows: 8

<MessageView>:
canvas:
Color:
rgba: 1, 1, 1, 1
Rectangle:
pos: self.pos
size: self.size

<Message>:
BoxLayout:
pos: root.pos
height: self.height
TextInput:
pos: root.pos
size: root.size
id: msg

''')

class Message(Widget):
bg_color = ListProperty([0.99, 0.99, 0.99, 1])

class SmileBubble(Bubble):
def hide(self):
self.disabled = True

def show(self):
self.disabled = False

class MessageView(ScrollView):
pass

class TestApp(App):
def msg_in(self, text):
msg = Message()
msg.ids['msg'].text = text
msg.size_hint = [None, None]
msg.width = 160
self.msg_layout.add_widget(msg)

def build(self):
self.scr = Screen()
self.sv1_main = MessageView()
self.msg_layout = GridLayout(cols = 1,
size_hint_y = None)
self.msg_layout.bind(minimum_height = self.msg_layout.setter('height'))

self.smile_bbl = SmileBubble()
for i in range(10):
self.msg_in("test")

self.smile_bbl.hide() # To hide/show the Bubble, comment out this line. For the second question, comment out this line

self.scr.add_widget(self.sv1_main)
self.sv1_main.add_widget(self.msg_layout)
self.scr.add_widget(self.smile_bbl)

return self.scr

TestApp().run()


If it matters, I'm using Kivy
v1.9.2-dev0

Answer

If you don't want opacity&disabled trick (nice one) or something like y = 5000 that doesn't work well in e.g. BoxLayout - and clearly using that would result in stretching your ScrollView, I see "two" simple options though not really without removing - yet with preserving!

First one is to take all canvas instructions if you can access them, copy them somewhere and canvas.clear, but good luck finding a bug if something goes wrong.

The second one is basically the first one, but in three commands and you can't screw something up except if you forget where did the widget go(hehe):

  • copy widget
  • remove widget from where it shouldn't be visible
  • add somewhere where you can access it later

 

from kivy.lang import Builder
from kivy.base import runTouchApp
from kivy.uix.boxlayout import BoxLayout
Builder.load_string('''
<Test>:
    orientation: 'vertical'
    Button:
        on_release: root.move_me()
    BoxLayout:
        id: box
        Button:
            text: 'Hi %s!' % self.parent
    Button:
        id: box2
''')
class Test(BoxLayout):
    def move_me(self):
        if self.ids.box.children:
            button = self.ids.box.children[0]
            self.ids.box.remove_widget(button)
            self.ids.box2.add_widget(button)
        else:
            button = self.ids.box2.children[0]
            self.ids.box2.remove_widget(button)
            self.ids.box.add_widget(button)
runTouchApp(Test())

You can see that the widget is still present in a variable, you can access its properties via button.<something> as you can see in the text.

Now what may happen if you use this in e.g. Boxlayout or GridLayout in a wrong way: widgets' pos inside the parent gets updated and exactly as in a list [1, 2, 3].remove(2) the final list would be [1, 3], which in BoxLayout means dividing size into halves and not into thirds.

How to fix this? Simply, again use the trick from above, but now save more widgets - the one you want to hide + every widget that is added to parent later

my_widgets = <parent>.children[<widget you want to hide>:len(<parent>.children)-1]

which will give you a list of objects i.e. again preserving everything as is and you'll only "pop" the widget you don't want to see. Finally make a placeholder(e.g. Widget or anything transparent) with the same size(and maybe even pos, but this is calculated automatically, so...) of the widget you want to hide and:

for child in my_widgets:
    <parent>.add_widget(child)

This method(or rather my explaining) may seem a difficult, but is more simple than throwing away a widget out of parent's bounding box or trying to disabled=True, which for you resulted in not being able to scroll(otherwise I'd definitely go for that). Replace Bubble with Widget and you'll be able to scroll. Ofc it'll be placed to [0, 0], but that's not an argument as if you make it visible with Color&Rectangle, scrolling with a cursor placed on Widget works (at least for me on master with your code).

Finally: make a function from that

from kivy.lang import Builder
from kivy.base import runTouchApp
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.widget import Widget
Builder.load_string('''
<Test>:
    orientation: 'vertical'
    Button:
        text: 'hide'
        on_release: root.hide(box)
    BoxLayout:
        id: box
        Button:
            text: 'Hi %s!' % self.parent
    Button
        text: 'Retrieve'
        on_release: root.hide(root.placeholder, root.saved)
''')
class Test(BoxLayout):
    def hide(self, what, retrieve=None):
        # you'll need that object accessible and ".parent" will disappear
        parent = what.parent
        children = what.parent.children[:]

        # check for position in children list
        place = children.index(what)

        # save the widget you want to hide
        self.saved = children[place]

        # save children from the latest added to the removed one
        saved_children = children[0:place+1]

        # sizes are optional here
        self.placeholder = Widget(size_hint=[None, None],
                             size=saved_children[0].size)
        for child in saved_children:
            parent.remove_widget(child)  # here you still can use what.parent

        # here the ".parent" is not available - the reason for "parent" var.
        # add Widget instead of whatever you will "hide"
        # pass to "retrieve" the saved widget if you want it back
        parent.add_widget(self.placeholder if not retrieve else retrieve)

        # add widgets in order they were originally added to the parent
        for child in list(reversed(saved_children[:place])):
            parent.add_widget(child)
        # cleanup mess ^^
        del children, saved_children
runTouchApp(Test())