Kivy Popup Dynamic Height - python

I'm looking to make a popup on the python-side that has a dynamic height.
So far, I have this within the screens __init__ class. The kv file has another widget that called the popup on_release. Anyways, I have found that this produces a popup with very wonky formatting:
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.popup import Popup
from kivy.uix.button import Button
from kivy.uix.gridlayout import GridLayout
from kivy.uix.screenmanager import Screen, ScreenManager
kv = '''
ScreenManagement:
id: 'manager'
BrokenPopup:
name: 'broken'
manager: 'manager'
<BrokenPopup>:
BoxLayout:
Button:
text: 'Test'
on_release: root.p.open()
'''
class ScreenManagement(ScreenManager):
pass
class BrokenPopup(Screen):
def __init__(self, **kwargs):
super(BrokenPopup,self).__init__(**kwargs)
self.p = Popup(auto_dismiss=False, size_hint_x=.6, size_hint_y=None, title='A popup')
self.g = GridLayout(cols=1, spacing=10)
self.g.add_widget(Button(text='Test1', size_hint_y=None, height=32))
self.g.add_widget(Button(text='Test2', size_hint_y=None, height=32))
self.g.bind(minimum_height=self.g.setter('height'))
self.p.add_widget(self.g)
self.p.bind(height=self.g.setter('height')) #<- this does not work to change the popup height!
class TheApp(App):
def build(self):
return Builder.load_string(kv)
TheApp().run()
The popup size is set to fit only one widget, leaving the second button (and all others that may be included) to float beyond the confines of the popup border.
How should I change the code so that all of the widgets fit within the confines of the popup? I am trying to do that by dynamically setting the height of the popup, however that is not proving effective.

I have modified your code to do what I think you want. Basically it adds the minimum_height from the GridLayout, that is added to your Popup, to the calculated height of the title and the dividing bar. The first Button in the GridLayout now adds another Button to the GridLayout for testing.
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.popup import Popup
from kivy.uix.button import Button
from kivy.uix.gridlayout import GridLayout
from kivy.uix.screenmanager import Screen, ScreenManager
kv = '''
ScreenManagement:
id: 'manager'
BrokenPopup:
name: 'broken'
manager: 'manager'
<BrokenPopup>:
BoxLayout:
Button:
text: 'Test'
on_release: root.p.open()
'''
class ScreenManagement(ScreenManager):
pass
class BrokenPopup(Screen):
def __init__(self, **kwargs):
super(BrokenPopup,self).__init__(**kwargs)
self.popup_title_height = None
self.p = Popup(auto_dismiss=False, size_hint_x=.6, size_hint_y=None, title='A popup')
self.g = GridLayout(cols=1, spacing=10)
self.g.bind(minimum_height=self.fix_size)
self.g.add_widget(Button(text='Test1', size_hint_y=None, height=32, on_release=self.add_one))
self.g.add_widget(Button(text='Test2', size_hint_y=None, height=32))
self.p.add_widget(self.g)
def add_one(self, *args):
self.g.add_widget(Button(text='Another', size_hint_y=None, height=32))
def get_popup_title_height(self):
height = 0
popupGrid = self.p.children[0]
height += popupGrid.padding[1] + popupGrid.padding[3]
for child in popupGrid.children:
if isinstance(child, BoxLayout):
continue
else:
height += child.height + popupGrid.spacing[1]
self.popup_title_height = height
def fix_size(self, *args):
if self.popup_title_height is None:
self.get_popup_title_height()
self.p.height = self.g.minimum_height + self.popup_title_height
class TheApp(App):
def build(self):
return Builder.load_string(kv)
TheApp().run()
I cheated a bit by looking at the code for Popup and the style.kv file to see how the Popup is displayed. So, if any of that is changed, this may not work.

I have found a solution for my original problem that is influenced by John Anderson's answer. I'll provide a walkthrough below for how I came to this solution.
1) Here's a photo of my original problem; I needed to dynamically set the popup height based on the widgets that are assigned to it. Before finding the below solution, my popup looked like this with the code in the OP:
As you can see, the widgets go beyond the borders of the popup.
2) I found something interesting while looking inside the popup widget with the inspector tool.
python '/path/to/your/file.py' -m inspector
Using control-e, I can click widgets and inspect their attributes. I clicked the popup button and cycled through the parent widgets until I found the popup widget.
As you can see in the photo, the popup widget has one child: a grid layout. Here are the children of that grid layout:
Interestingly, the grid layout contains:
One label, with a height of 33
One line, with a height of 4
A box layout, which contains the contents of the popup
2 units of spacing between these three widgets
12 units of padding all-around; so 24 additional units to consider for the height
3) In my solution, I have hard-written the default heights of the label, the line widget, and all default popup spacing and padding. Then, I cycle through the children inside the box layout, and add their heights. I also add 10 to those children heights, as the gridlayout that contains all of these widgets uses a spacing of 10.
Solution:
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.popup import Popup
from kivy.uix.button import Button
from kivy.uix.gridlayout import GridLayout
from kivy.uix.screenmanager import Screen, ScreenManager
kv = '''
ScreenManagement:
id: 'manager'
BrokenPopup:
name: 'broken'
manager: 'manager'
<BrokenPopup>:
BoxLayout:
Button:
text: 'Test'
on_release: root.p.open()
'''
class ScreenManagement(ScreenManager):
pass
class BrokenPopup(Screen):
def __init__(self, **kwargs):
super(BrokenPopup,self).__init__(**kwargs)
self.p = Popup(auto_dismiss=False, size_hint_x=.6, size_hint_y=None, title='A popup')
self.g = GridLayout(cols=1, spacing=10, padding=[0,10,0,-5])
self.g.bind(minimum_height=self.fix_popup_height) # <- here's the magic
self.g.add_widget(Button(text='Test1', size_hint_y=None, height=32))
self.g.add_widget(Button(text='Test2', size_hint_y=None, height=32))
self.p.add_widget(self.g)
def fix_popup_height(self, grid, *args):
# a generalized function that, when bound to minimum_height for a grid with popup widgets,
# this will set the height of the popup
height = 0
height += 33 # for popup label
height += 4 # for popup line widget
height += 24 # for popup padding
height += 2 # for spacing between main popup widgets
for child in grid.children:
height += child.height + 10 # adds 10 for the spacing between each child
grid.parent.parent.parent.height = height # sets popup height
pass
class TheApp(App):
def build(self):
return Builder.load_string(kv)
TheApp().run()
Notable changes from the OP:
Bind the minimum_height of the widget container to the fix_popup_height() function; this will trigger each time a widget is added to the popup.
Declare the fix_popup_height() within the screen class.
Here's the fixed popup:

Related

i want to add a checkbox to buttons

from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.uix.checkbox import CheckBox
class Ckbox(BoxLayout):
orientation = "vertical"
cc =[]
def __init__(self):
super().__init__()
for i in range(5):
self.bb = Button()
self.cq = CheckBox()
self.cc.append(self.cq)
self.add_widget(self.bb)
self.add_widget(self.cq)
class Ckboxapp(App):
def build(self):
return Ckbox()
Ckboxapp().run()
from the above code, i get this
but i want the checkbox and button to be together so i add the button variable to the add widget as self.bb.add_widget(self.cq) and got this
the second image show the checkbox attached to the button but only showing on the last button only. i want itlike in the second iamge but i want it to be on all the buttons. what can i do?
If you put each CheckBox/Button pair in another BoxLayout, I think you will get what you want. Try this:
class Ckbox(BoxLayout):
orientation = "vertical"
def __init__(self):
super().__init__()
for i in range(5):
bl = BoxLayout()
bb = Button(text='Button ' + str(i), size_hint_x=0.85)
cq = CheckBox(size_hint_x=0.15)
bl.add_widget(cq)
bl.add_widget(bb)
self.add_widget(bl)

Kivy Dropdown not opening in ScreenManager but open in screen

I'm having issues to get a kivy.DropDown widget to work with a screen manager.
I am using the dropdown code that the kivy documentation provides, and add it to a screen widget, which I then add to a screen manager to display. The following code should reproduce the problem by itself.
import kivy
kivy.require('1.10.1')
from kivy.app import App
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.uix.dropdown import DropDown
from kivy.uix.button import Button
from kivy.uix.anchorlayout import AnchorLayout
class MyScreen(Screen):
def __init__(self, **kwargs):
super(MyScreen, self).__init__(**kwargs)
anchor = AnchorLayout()
anchor.anchor_x = "center"
anchor.anchor_y = "center"
anchor.size = self.size
anchor.pos = self.pos
dropdown = DropDown()
for index in range(10):
# When adding widgets, we need to specify the height manually
# (disabling the size_hint_y) so the dropdown can calculate
# the area it needs.
btn = Button(text='Value %d' % index, size_hint_y=None, height=44)
# for each button, attach a callback that will call the select() method
# on the dropdown. We'll pass the text of the button as the data of the
# selection.
btn.bind(on_release=lambda btn: dropdown.select(btn.text))
# then add the button inside the dropdown
dropdown.add_widget(btn)
# create a big main button
mainbutton = Button(text='Hello', size_hint=(None, None))
# show the dropdown menu when the main button is released
# note: all the bind() calls pass the instance of the caller (here, the
# mainbutton instance) as the first argument of the callback (here,
# dropdown.open.).
mainbutton.bind(on_release=dropdown.open)
# one last thing, listen for the selection in the dropdown list and
# assign the data to the button text.
dropdown.bind(on_select=lambda instance, x: setattr(mainbutton, 'text', x))
anchor.add_widget(mainbutton)
self.add_widget(anchor)
sm = ScreenManager() # transition = NoTransition())
sm.add_widget(MyScreen(name='screen'))
class MyApp(App):
def build(self):
return sm
if __name__ == '__main__':
MyApp().run()
Why is it that if put in a screen widget inside a ScreenManager, the dropdown widget does not work? Clarifications are welcome.
PS:
For anybody finding this issue, you can use the spinner widget to have the same functionality already implemented.
I believe your problem is due to garbage collection. The dropdown reference in your __init__() method is not saved (The bind uses a weakref which will not prevent garbage collection). So I think all you need to do is replace your dropdown local variable with a self.dropdown instance variable as:
import kivy
kivy.require('1.10.1')
from kivy.app import App
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.uix.dropdown import DropDown
from kivy.uix.button import Button
from kivy.uix.anchorlayout import AnchorLayout
class MyScreen(Screen):
def __init__(self, **kwargs):
super(MyScreen, self).__init__(**kwargs)
anchor = AnchorLayout()
anchor.anchor_x = "center"
anchor.anchor_y = "center"
anchor.size = self.size
anchor.pos = self.pos
self.dropdown = DropDown()
for index in range(10):
# When adding widgets, we need to specify the height manually
# (disabling the size_hint_y) so the dropdown can calculate
# the area it needs.
btn = Button(text='Value %d' % index, size_hint_y=None, height=44)
# for each button, attach a callback that will call the select() method
# on the dropdown. We'll pass the text of the button as the data of the
# selection.
btn.bind(on_release=lambda btn: self.dropdown.select(btn.text))
# then add the button inside the dropdown
self.dropdown.add_widget(btn)
# create a big main button
mainbutton = Button(text='Hello', size_hint=(None, None))
# show the dropdown menu when the main button is released
# note: all the bind() calls pass the instance of the caller (here, the
# mainbutton instance) as the first argument of the callback (here,
# dropdown.open.).
mainbutton.bind(on_release=self.dropdown.open)
# one last thing, listen for the selection in the dropdown list and
# assign the data to the button text.
self.dropdown.bind(on_select=lambda instance, x: setattr(mainbutton, 'text', x))
anchor.add_widget(mainbutton)
self.add_widget(anchor)
sm = ScreenManager() # transition = NoTransition())
sm.add_widget(MyScreen(name='screen'))
class MyApp(App):
def build(self):
return sm
if __name__ == '__main__':
MyApp().run()

Kivy mapview - Place button widgets ontop of mapview widget

I'm trying to add some widgets to a Kivy mapview like this
mapview = self.mapWidget(self.lat, self.lon)
touchBarbtn1 = Button(text='Unlock')
touchBarbtn1.bind(on_press=lambda x: self.centerOnUser())
mapview.add_widget(touchBarbtn1)
But no matter what layout options I give it, it's always stuck in the bottom left corner. Even if I add more widgets they all just stack on top of each other in the bottom left corner. My Goal is to make a button on the top right corner to center the map on the user.
Use Kivy Anchor Layout.
If you are going to add more buttons, my suggestion is to use Kivy Drop-Down List.
Example
main.py
from kivy.garden.mapview import MapView
from kivy.app import App
from kivy.uix.anchorlayout import AnchorLayout
from kivy.uix.button import Button
from kivy.properties import NumericProperty
class RootWidget(AnchorLayout):
lat = NumericProperty(50.6394)
lon = NumericProperty(3.057)
def __init__(self, **kwargs):
super(RootWidget, self).__init__(**kwargs)
self.anchor_x = 'right'
self.anchor_y = 'top'
mapview = MapView(zoom=11, lat=self.lat, lon=self.lon)
self.add_widget(mapview)
touchBarbtn1 = Button(text='Unlock', size_hint=(0.1, 0.1))
touchBarbtn1.bind(on_press=lambda x: self.centerOnUser())
self.add_widget(touchBarbtn1)
def centerOnUser(self):
pass
class MapViewApp(App):
def build(self):
return RootWidget()
MapViewApp().run()
Output

Adding spacing between dropdown items in kivy

My problem is in accessing container property of DropDown, which is a GridLayout by default and contains it's children.
Simple app:
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.dropdown import DropDown
from kivy.uix.button import Button
from kivy.uix.label import Label
class TestApp(App):
def build(self):
root = BoxLayout(orientation='vertical')
dropdown = DropDown()
for i in range(3):
dropdown.add_widget(Button(
text=str(i),
size_hint_y=None
)) # add 3 buttons to dropdown
dropdown.container.bind(spacing=8) # this line does not work
dropdown_button = Button(size_hint_y=.2, text='Open DropDown')
dropdown_button.bind(on_release=dropdown.open)
root.add_widget(dropdown_button)
root.add_widget(Label()) # empty space under button
return root
TestApp().run()
I tried using bind method for this, but there is no result. No indentation is set. I also would like to see the solution in kivy language because I want to use dp() function for setting spacing and it's not very convenient to pass the parameter to python file for this. Thanks in advance for any help.
Instead using bind you should just set the value directly:
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.dropdown import DropDown
from kivy.uix.button import Button
from kivy.uix.widget import Widget
class TestApp(App):
def build(self):
root = BoxLayout(orientation='vertical')
dropdown = DropDown()
for i in range(3):
dropdown.add_widget(Button(
text=str(i),
size_hint_y=None
)) # add 3 buttons to dropdown
dropdown.container.spacing = 10
dropdown.container.padding = (0, 10, 0, 0)
dropdown_button = Button(size_hint_y=.2, text='Open DropDown')
dropdown_button.bind(on_release=dropdown.open)
root.add_widget(dropdown_button)
root.add_widget(Widget()) # empty space under button
return root
TestApp().run()
Using a custom container class is not supported directly. You can do it like this, but its hacky and ugly:
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.gridlayout import GridLayout
from kivy.uix.dropdown import DropDown
from kivy.uix.button import Button
from kivy.uix.widget import Widget
from kivy.lang import Builder
Builder.load_string('''
<MyContainer>:
# copied from kivy.uix.dropdown._grid_kv
size_hint_y: None
height: self.minimum_size[1]
cols: 1
# custom settings
spacing: 10
padding: (0, 10, 0, 0)
''')
class MyContainer(GridLayout):
pass
class TestApp(App):
def build(self):
root = BoxLayout(orientation='vertical')
container = MyContainer()
dropdown = DropDown(container=container)
super(DropDown, dropdown).add_widget(container)
dropdown.on_container(dropdown, container)
for i in range(3):
dropdown.add_widget(Button(
text=str(i),
size_hint_y=None
)) # add 3 buttons to dropdown
dropdown_button = Button(size_hint_y=.2, text='Open DropDown')
dropdown_button.bind(on_release=dropdown.open)
root.add_widget(dropdown_button)
root.add_widget(Widget()) # empty space under button
return root
TestApp().run()
So I'd say it'd more clean to make custom spacing class as a subclass of Widget to fill space between buttons:
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.dropdown import DropDown
from kivy.uix.button import Button
from kivy.uix.widget import Widget
from kivy.lang import Builder
Builder.load_string('''
<DropDownSpacing>:
size_hint_y: None
height: 20
''')
class DropDownSpacing(Widget):
pass
class TestApp(App):
def build(self):
root = BoxLayout(orientation='vertical')
dropdown = DropDown()
for i in range(3):
dropdown.add_widget(DropDownSpacing())
dropdown.add_widget(Button(
text=str(i),
size_hint_y=None
)) # add 3 buttons to dropdown
dropdown_button = Button(size_hint_y=.2, text='Open DropDown')
dropdown_button.bind(on_release=dropdown.open)
root.add_widget(dropdown_button)
root.add_widget(Widget()) # empty space under button
return root
TestApp().run()
This is the same you're doing in your main BoxLayout, except I prefer to use Widget class directly instead of Label with no text.

Anchor a label to the corner?

In Kivy:
from kivy.app import App
from kivy.uix.label import Label
class TestApp(App):
def build(self):
label = Label(text="TEST")
return label
TestApp().run()
My label is centred in the window:
How can I instead anchor my label to the bottom right corner of the window?
You'd think
label.halign = 'right'
label.valign = 'bottom'
would do the trick, but as the Label documentation points out,
The valign property will have no effect and halign will only have an effect if your text has newlines; a single line of text will appear to be centered even though halign is set to left (by default).
It looks like adding the label to an AnchorLayout, then shrinking the size of the label relative to its parent widget, together achieves what I want.
from kivy.app import App
from kivy.uix.label import Label
from kivy.uix.anchorlayout import AnchorLayout
class TestApp(App):
def build(self):
anchor_layout = AnchorLayout(anchor_x='right', anchor_y='bottom')
label = Label(text="TEST")
label.size_hint = (0.1, 0.1)
anchor_layout.add_widget(label)
return anchor_layout
TestApp().run()
Produces:
Set the Label's text_size to its size, e.g. in kv text_size: self.size. The text_size controls the bounding box within which text is wrapped.

Categories