illright
illright

Reputation: 4043

Strange scroll behaviour in Kivy

I have a ScrollView and a Bubble, that partially overlaps it and contains a GridLayout
Questions:

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, or to temporarily move the widget 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:

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

Upvotes: 0

Views: 623

Answers (1)

Peter Badida
Peter Badida

Reputation: 12199

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())

Upvotes: 1

Related Questions