How to add widgets to a label in scrollview Kivy - python-3.x

I'm trying to make a chat page which looks like a standard messaging app on a phone (Whatsapp, Imessage etc). To do this I have created a screen which contains ScrollView and have added a label widget to it.
To get the desired effect of the Whatsapp/Imessage look I think I then want a label widget to be added to the label on the ScrollView everytime the send button is pressed, this will then add the text of the text input box onto the screen.
Is it possible to add a label to another label? Every question I've looked at so far has been about adding a widget to a BoxLayout or GridLayout.
I'm also not sure on how I'll get the position of the label to change everytime the button is pressed but one step at a time!
kv file:
WindowManager:
ChatPage:
<ChatPage>:
name: "chat_page"
layout_content: layout_content
NavigationLayout:
id: nav_layout
MDNavigationDrawer:
NavigationDrawerIconButton:
text: "Test"
FloatLayout:
MDToolbar:
pos_hint: {'top': 1}
md_bg_color: 0.2, 0.6, 1, 1
ScrollView:
size_hint: 1, 0.6
pos_hint: {"top" : 0.8, "bottom" : 0.5}
GridLayout:
id: layout_content
cols: 1
size_hint_y: None
height: self.minimum_height
canvas:
Color:
rgba: (1, 1, 1, 1)
Rectangle:
size: self.size
pos: self.pos
Label:
text_size: self.width - 20, None
size_hint_y: None
height: self.texture_size[1]
color: 0,0,0,1
BoxLayout:
TextInput:
id: msg
hint_text: "Type your message here"
pos_hint: {"x": 0, "top": 0.15}
size_hint: 0.75, 0.15
Button:
text: "Send"
background_normal: ""
background_color: 0, 0.6, 0, 1
pos_hint: {"x": 0.75, "top": 0.15}
size_hint: 0.25, 0.15
on_release: root.btn_press()
<SmoothLabel#Label>:
background_color: 0,0,0,0
background_normal: ""
back_color: 1,0,1,1
border_radius: [6]
canvas.before:
Color:
rgba: 0.2,0.6,1,1 #This changes the label colour
RoundedRectangle:
size: self.size
pos: self.pos
radius: self.border_radius
py file:
import kivy
from kivy.app import App
from kivy.properties import ObjectProperty, StringProperty, NumericProperty, ListProperty
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.lang import Builder
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.core.window import Window
from kivymd.theming import ThemeManager
from kivy.uix.scrollview import ScrollView
Window.clearcolor = (1,1,1,1)
class SmoothLabel(Label):
pass
class WindowManager(ScreenManager):
pass
class ChatPage(Screen):
layout_content = ObjectProperty(None)
def btn_press(self):
if self.ids.msg.text:
self.layout_content.add_widget(SmoothLabel(text=self.ids.msg.text, size_hint_x=0.5, size_hint_y=0.1, pos_hint={"x": 0.1, "top": 0.8}, background_color=(0.2, 0.6, 1, 1)))
self.ids.msg.text = ""
else:
pass
class MyApp(App):
theme_cls = ThemeManager()
def build(self):
kv = Builder.load_file("kivy.kv")
sm = WindowManager()
screens = [ChatPage(name="chat_page")]
for screen in screens:
sm.add_widget(screen)
sm.current = "chat_page"
return sm
if __name__ == '__main__':
MyApp().run()
Thanks

Made a few changes to your code (in addition to removing all the kivyMD) to get it to work:
First, I changed the SmoothLabel rule in your kv to:
<SmoothLabel>:
size_hint: None, None
size: self.texture_size
background_color: 0,0,0,0
background_normal: ""
back_color: 1,0,1,1
border_radius: [6]
canvas.before:
Color:
rgba: 0.2,0.6,1,1 #This changes the label colour
RoundedRectangle:
size: self.size
pos: self.pos
radius: self.border_radius
Took out the #Label since that is already specified in your python code, and added the size_hint and size (otherwise, the GridLayout decides the size of the SmoothLabel children).
Also, removed the
background_color=(0.2, 0.6, 1, 1)
from the SmoothLabel creation (that was throwing an exception for me), so the ChatPage definition now looks like:
class ChatPage(Screen):
layout_content = ObjectProperty(None)
def btn_press(self):
if self.ids.msg.text:
self.layout_content.add_widget(SmoothLabel(text=self.ids.msg.text))
self.ids.msg.text = ""
else:
pass
The Label widget has no background_color attribute. And, since I added size_hint to the rule, I removed it from the SmoothLabel() call. Also, the GridLayout does not support pos_hint.
That was enough to get the code working for me.

Related

Adding Kivy recycleview to screenmanager breaks example

I'm trying to combine this recycleview example with this screenmanager example so that the recycleview example can be one of the screens in my app. The app runs but all the control buttons display at the bottom of the UI (They're supposed to be at the top) and are either disabled or obscured, disallowing input. Consequently, the recycleview cannot be populated and/or viewed.
Here's my attempt:
import asyncio
from random import sample, randint
from string import ascii_lowercase
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.screenmanager import ScreenManager, Screen, FadeTransition
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.recycleview.views import RecycleDataViewBehavior
kv = '''
#:import FadeTransition kivy.uix.screenmanager.FadeTransition
#:import BoxLayout kivy.uix.boxlayout.BoxLayout
#:import RecycleDataViewBehavior kivy.uix.recycleview.views.RecycleDataViewBehavior
ScreenManagement:
transition: FadeTransition()
ContactsScreen:
name: 'contacts'
MessengerScreen:
name: 'messenger'
<Row#RecycleKVIDsDataViewBehavior+BoxLayout>:
canvas.before:
Color:
rgba: 0.5, 0.5, 0.5, 1
Rectangle:
size: self.size
pos: self.pos
value: ''
Label:
id: name
Label:
text: root.value
<ContactsScreen>:
canvas:
Color:
rgba: 0.3, 0.3, 0.3, 1
Rectangle:
size: self.size
pos: self.pos
rv: rv
orientation: 'vertical'
GridLayout:
cols: 3
rows: 2
size_hint_y: None
height: dp(108)
padding: dp(8)
spacing: dp(16)
Button:
text: 'Populate list'
on_press: root.populate()
Button:
text: 'Sort list'
on_press: root.sort()
Button:
text: 'Clear list'
on_press: root.clear()
BoxLayout:
spacing: dp(8)
Button:
text: 'Insert new item'
on_press: root.insert(new_item_input.text)
TextInput:
id: new_item_input
size_hint_x: 0.6
hint_text: 'value'
padding: dp(10), dp(10), 0, 0
BoxLayout:
spacing: dp(8)
Button:
text: 'Update first item'
on_press: root.update(update_item_input.text)
TextInput:
id: update_item_input
size_hint_x: 0.6
hint_text: 'new value'
padding: dp(10), dp(10), 0, 0
Button:
text: 'Remove first item'
on_press: root.remove()
RecycleView:
id: rv
scroll_type: ['bars', 'content']
scroll_wheel_distance: dp(114)
bar_width: dp(10)
viewclass: 'Row'
RecycleBoxLayout:
default_size: None, dp(56)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
spacing: dp(2)
<MessengerScreen>:
Button:
on_release: app.root.current = 'contacts'
text: 'back to the home screen'
font_size: 50
'''
class ContactsScreen(Screen):
def populate(self):
self.rv.data = [
{'name.text': ''.join(sample(ascii_lowercase, 6)),
'value': str(randint(0, 2000))}
for x in range(50)]
def sort(self):
self.rv.data = sorted(self.rv.data, key=lambda x: x['name.text'])
def clear(self):
self.rv.data = []
def insert(self, value):
self.rv.data.insert(0, {
'name.text': value or 'default value', 'value': 'unknown'})
def update(self, value):
if self.rv.data:
self.rv.data[0]['name.text'] = value or 'default new value'
self.rv.refresh_from_data()
def remove(self):
if self.rv.data:
self.rv.data.pop(0)
class MessengerScreen(Screen):
pass
class ScreenManagement(ScreenManager):
pass
presentation = Builder.load_string(kv)
class MainApp(App):
def build(self):
return presentation
def main_runner(self):
async def run_wrapper():
# we don't actually need to set asyncio as the lib because it is
# the default, but it doesn't hurt to be explicit
await self.async_run(async_lib='asyncio')
print('App done')
#self.other_task.cancel()
return asyncio.gather(run_wrapper())
if __name__ == '__main__':
loop = asyncio.get_event_loop()
loop.run_until_complete(MainApp().main_runner())
loop.close()
The trouble seems to come from the <Row#RecycleKVIDsDataViewBehavior+BoxLayout> directive but I can't seem to fix it no matter where I place it.
You can position the Buttons using pos_hint, like this:
GridLayout:
id: grid
pos_hint: {'center_x': 0.5, 'top': 1}
Then, you need to size the RecycleView so that it is not over the Buttons, like this:
RecycleView:
id: rv
size_hint_y: None
height: root.height - grid.height

How to use on_press event for a button that has been added dynamically kivy

I have the following code where a Button is added to a Grid Layout everytime the test_press method is called. What I'd like to be able to do is change to a different screen when one of these buttons is pressed, but I'd like each Button that's been created to go to a different screen - which screen it goes to isn't important at the moment. How would I go about implementing something like this?
from kivy.uix.scrollview import ScrollView
from kivy.uix.button import Button
from kivy.core.window import Window
from kivymd.theming import ThemeManager
import kivy
from kivy.app import App
from kivy.properties import ObjectProperty, StringProperty, NumericProperty, ListProperty
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.lang import Builder
from kivy.uix.label import Label
Window.clearcolor = (1,1,1,1)
class Finder(Screen):
layout_profiles = ObjectProperty(None)
def test_press(self):
Btn1 = Button(size_hint_y=None, height=75)
self.layout_profiles.add_widget(Btn1)
class MyApp(App):
theme_cls = ThemeManager()
def build(self):
kv = Builder.load_file("kivy.kv")
sm = WindowManager()
screens = [Finder(name="finder")]
for screen in screens:
sm.add_widget(screen)
sm.current = "information"
return sm
if __name__ == '__main__':
MyApp().run()
kv file:
<Finder>:
name: "finder"
layout_profiles: layout_profiles
NavigationLayout:
id: nav_layout
MDNavigationDrawer:
NavigationDrawerIconButton:
text: "Test"
on_release: app.root.current = "login"
FloatLayout:
MDToolbar:
pos_hint: {'top': 1}
md_bg_color: 0.2, 0.6, 1, 1
left_action_items: [['menu', lambda x: root.ids.nav_layout.toggle_nav_drawer()]]
Label:
background_normal: ""
background_color: 1, 1, 1, 1
color: 0, 0, 0, 1
text: "Holding text"
font_size: 20
multiline: True
pos_hint: {"x": 0.125, "y": 0.7}
text_size: self.width, None
size_hint: 0.75, None
height: self.texture_size[1]
halign: "center"
ScrollView:
size_hint: 0.95, 0.6
pos_hint: {"x": 0.025, "top": 0.65, "bottom": 0.2}
do_scroll_x: False
GridLayout:
cols: 1
id: layout_profiles
size_hint_y: None
spacing: 10
height: self.minimum_height
canvas:
Color:
rgba: (1, 1, 1, 1)
Rectangle:
size: self.size
pos: self.pos
BoxLayout:
cols: 1
Button:
text: "Add widget"
size_hint: 0.5, 0.1
pos_hint: {"x": 0.5, "top": 0.1}
on_release: root.test_press()
Also apologies if the title (or description!) isn't that clear, I wasn't sure how to describe it.
The "Add widget" Button is just there to mimic the eventual goal where the buttons will be added dependent on some information feeding in from a database.

Kivy widget's opacity doesn't update visually

I want to hide one widget and show another on button's click. I do it by changing opacity form 1 to 0 and from 0 to 1. The firt widget becomes invisible, but the second is not showing.
This is my python file:
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.properties import NumericProperty
from kivy.config import Config
Config.set('graphics', 'width', '1920')
Config.set('graphics', 'height', '1080')
Config.set('graphics', 'fullscreen', 1)
class FirstScreen(Screen):
pass
class SecondScreen(Screen):
pass
class MyScreenManager(ScreenManager):
pass
class PasswordEntryWidget(BoxLayout):
def checkPswd(instance, pswd):
if (pswd == "123"):
instance.parent.opacity = 0
instance.parent.disabled = True
instance.parent.parent.ids.settingsWgt.opacity = 1.0
#print(instance.parent.parent.ids.settingsWgt.size_hint)
class SettingsEntryWidget(BoxLayout):
def checkPswd(instance, pswd):
if (pswd == "123"):
print ("It's OK!")
class MyApp(App):
def build(self):
return MyScreenManager()
if __name__ == '__main__':
MyApp().run()
This is my.kv:
#:kivy 1.11.1
#:include loginWidgets.kv
<MyScreenManager>:
FirstScreen:
SecondScreen:
<FirstScreen>:
name: "FirstScreen"
canvas.before:
Rectangle:
pos: self.pos
size: self.size
source: 'img/background.png'
FloatLayout:
size: root.size
PasswordEntryWidget:
id: passwordWgt
size_hint: 0.2, 0.1
pos_hint: {'top': 0.35, 'right': 0.7}
SettingsEntryWidget:
id: settingsWgt
opacity: 0
disabled: True
size_hint: 0.2, 0.32
pos_hint: {'top': 0.35, 'right': 0.95}
<SecondScreen>:
name: "SecondScreen"
Label:
color: 1,1,1,1
font_size: 10
center_x: root.width/4
top: root.top - 50
text: "Lalalala"
And this is loginWidgets.kv:
#:kivy 1.11.1
<PasswordEntryWidget>:
orientation: "vertical"
spacing: 20
#padding: 50
TextInput:
id: adminPswdLine
multiline: False
password: True
size_hint_y: None
height: 30
font_size: self.height - 10
Button:
id: "takeAdminPswd"
size_hint_x: 0.5
pos_hint: {'center_x': 0.5}
on_press: root.checkPswd(adminPswdLine.text)
<SettingsEntryWidget>:
orientation: "vertical"
spacing: 20
#padding: 50
TextInput:
id: host
padding: [5, 2, 0, 5]
text: ""
multiline: False
password: False
size_hint_y: None
height: 30
font_size: self.height - 10
Button:
id: "takeSettings"
size_hint_x: 0.5
pos_hint: {'center_x': 0.5}
on_press: root.checkPswd(adminPswdLine.text)
As I said, the first part works well. PasswordWgt hide perfectly, but SettingsWgt doesn't become visible. When I print it's opacity after - it gives 1, so widget must be visible, but it's not. I checked ids and what elements are there and needed widget is there with this id.
Is it a bug or I do something wrong?
The problem is that your are changing the opacity of the container (FloatLayout) instead of the PasswordEntryWidget. Just change:
class PasswordEntryWidget(BoxLayout):
def checkPswd(instance, pswd):
if (pswd == "123"):
instance.parent.opacity = 0
instance.parent.disabled = True
instance.parent.parent.ids.settingsWgt.opacity = 1.0
to:
class PasswordEntryWidget(BoxLayout):
def checkPswd(instance, pswd):
if (pswd == "123"):
instance.opacity = 0
instance.disabled = True
instance.parent.parent.ids.settingsWgt.opacity = 1.0
and I think it will work as you desire.

Kivy make a grid layout inside a label or button

Is it possible to have a grid like layout inside a Label or a Button in kivy.
I have an app that takes in a CSV file with product information and I would like to populate MainScreen with rows from a CSV file.
Each row should look like this:
In the end the Label or Button should be pressable to open a pop up window for confirmation screen for quantity of the product and verify.
Is it even possible or am I approaching it from the wrong angle?
I do not have any code yet to populate the MainScreen with rows but this is how it looks so far.
To clarify. At this moment I don't need help with importing CSV files, but with the method to display it, that matches the above criteria(picture)
Code so far is as follows:
ATmain.py
from kivy.app import App
from kivy.uix.screenmanager import Screen, ScreenManager
from kivy.core.window import Window
from kivy.lang import Builder
from kivy.properties import StringProperty
Window.clearcolor = (1,1,1,1)
Window.size = (270, 480)
class LoginScreen(Screen):
input = StringProperty("")
class MainScreen(Screen):
username = StringProperty('')
class ScreenManagement(ScreenManager):
pass
presentation = Builder.load_file("app.kv")
class ATApp(App):
presentation = Builder.load_file("app.kv")
def build(self):
return presentation
if __name__ == '__main__':
ATApp().run()
app.kv:
# File name: main.py
#:import FadeTransition kivy.uix.screenmanager.FadeTransition
#:kivy 1.10.1
ScreenManagement:
transition: FadeTransition()
LoginScreen:
id: login
MainScreen:
username: login.input
<LoginScreen>:
name: "login"
canvas:
Color:
rgba: [1,1,1]
Rectangle:
pos: self.pos
size: self.size
FloatLayout:
rows:2
cols:1
background_color: 1,1,1,1
Label:
size_hint: 0.3, 0.05
pos_hint: {"x": 0.5 - 0.3/2, "center_y": 0.4}
text:"Kasutaja"
color: 0,0,0,1
TextInput:
id: input
size_hint: (0.3, None)
height: 30
pos_hint: {"x": 0.5 - 0.3/2, "center_y": 0.3}
multiline: False
Button:
id: confirm_login
text: "Login"
size_hint: 0.15, 0.07
pos_hint: {"x": 0.5 - 0.15/2, "center_y": 0.2}
background_color: 0.9,0.9,0.9,1
on_press: self.background_color = (1,0,0,1)
on_release: root.input = input.text; app.root.current = "main"
<MainScreen>:
name: "main"
canvas:
Rectangle:
pos: self.pos
size: self.size
Label:
id:name
text: root.username
color: (0,0,0,1)
size_hint_y: None
height: 30
size_hint_x: 1
pos_hint: {"right": 1, "top": 1}
BoxLayout:
orientation: "vertical"
size_hint_y: None
size_hint_x: 1
pos_hint_x: None
pos_hint_y: 1
Button:
text: "Item1"
color: (0,0,0,1)
height: self.height
size_hint_y: None
size_hint_x: 1
pos_hint: {"right": 1, "top": 0}
I would be very greatful if anyone could as much as point me in the right direction!
The kivy hack way will be to simply use a GridLayout or any layout for that matter then give your layout button properties so it is clickable like so :
from kivy.behaviors import ButtonBehavior
#then make a clickable grid
class GridButton(GridLayout, ButtonBehaviour):
def __init__(self, **kwargs):
super().__init__(**kwargs)
#Then do whatever you want to do
Another way to do it I guess would be to use the on_touch_down callback and check if the touch is within the widget's bounds

Kivy: Popup can have only one widget as content

I am having an issue with using a popup in my .kv file. I understand that a popup can only have one widget as it's content, however if I am only passing a GridLayout as a child that includes a Label and Button, shouldn't this work?
Here is my Python code:
import kivy, LabelB
from kivy.app import App
from kivy.graphics import Color, Rectangle
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.gridlayout import GridLayout
from kivy.uix.tabbedpanel import TabbedPanel
from kivy.properties import ObjectProperty, StringProperty
from kivy.lang import Builder
from kivy.uix.popup import Popup
from kivy.uix.screenmanager import ScreenManager, Screen, FadeTransition
Builder.load_file('main.kv')
class CustomPopup(Popup):
pass
class MenuScreen(Screen):
def open_popup(self):
the_popup = CustomPopup()
the_popup.open()
class SurveyScreen(Screen):
pass
sm = ScreenManager(transition=FadeTransition())
sm.add_widget(MenuScreen(name='menu'))
sm.add_widget(SurveyScreen(name='survey'))
class MainApp(App):
def build(self):
return sm
if __name__ == '__main__':
MainApp().run()
Here is my .kv file:
<CustomPopup>:
title: 'Terms of Service'
size_hint: .5, .5
auto_dismiss: False
GridLayout:
cols: 1
Label:
size_hint: .9, .9
halign: 'center'
valign: 'middle'
text: 'Insert terms of service text here'
text_size: self.width, None
Button:
text: 'Close'
on_release: root.dismiss()
<MenuScreen>:
FloatLayout:
canvas.before:
Rectangle:
source: 'menu.png'
size: self.size
pos: self.pos
Label:
pos_hint: {'x': .7, 'y': .85}
text_size: self.size
font_name: 'Arial'
font_size: 26
text: 'Sample'
bold: True
Button:
text: 'Take Survey'
size_hint: .2, .1
pos_hint: {'x': .15, 'y': .1}
on_release:
root.manager.transition.duration = 0.5
root.manager.current = 'survey'
Button:
text: 'Terms of Service'
size_hint: .2, .1
pos_hint: {'x': .6-self.size_hint_x, 'y': .1}
on_release: root.open_popup()
Button:
text: 'Quit'
size_hint: .2, .1
pos_hint: {'x': .85-self.size_hint_x, 'y': .1}
on_release: app.stop()
<SurveyScreen>:
GridLayout:
cols: 1
padding: 20
spacing: 10
Label:
text: 'WELCOME!'
font_size: 20
Label:
text: 'Some boring text'
The error is as follows: 'Popup can have only one widget as content'
Am I missing something obvious here? Thanks in advance.
Yes, it should work as you say, your code is correct.
The problem is that the loading of the .kv file is duplicated. As your App subclass is called MainApp, main.kv is loaded automatically if it is in same directory (Doc: How to load kv). On the other hand, you explicitly upload the file using Builder.load_file ('main.kv').
You must remove the call to Builder or rename MainApp/main.kv.
If you delete the call to Builder.load_file you must create the ScreenManager instance once the .kv is loaded. You can do something like:
class MainApp (App):
def build (self):
sm = ScreenManager (transition = FadeTransition ())
sm.add_widget (MenuScreen (name = 'menu'))
sm.add_widget (SurveyScreen (name = 'survey'))
return sm

Resources