So I have managed to get widgets of varying height into a recycleview and items can be added, this looked perfect on my PC but was very wrong when running on android. I re-built the app with buildozer to make sure it wasn't something to do with that, I put together the demo program seen below and ran it on both platforms, and experienced the same result. I have used the kivy inspector and not been able to see any values that are not what I didn't manually program. And unless I have missed one I have used dp(X) or 'Xdp' whenever needed.
Any help or tips will be apreciated, thank you :)
main.py
from random import randint
from kivy.app import App
from kivy.uix.label import Label
from kivy.clock import Clock
from kivy.graphics import Color, Rectangle
from kivy.uix.recycleview import RecycleView
from kivy.uix.boxlayout import BoxLayout
from kivy.properties import StringProperty, ListProperty, NumericProperty
class NewPostGrid(BoxLayout):
votes_ = StringProperty()
message_id_ = StringProperty()
text_ = StringProperty()
group_ = StringProperty()
_size = ListProperty()
class SizeLabel(Label):
pass
class RV(RecycleView):
distance_to_top = NumericProperty()
scrollable_distance = NumericProperty()
def __init__(self, **kwargs):
super().__init__(**kwargs)
App.get_running_app().rv_data_list = []
def generate_post(self): # This is only to test posts with different line height
e = ['Test post ID: ', str(App.get_running_app().message_id_num)]
for i in range(randint(1, 8)): e.append('\n')
e.append('end of post')
return "".join(e)
def add(self):
l = len(App.get_running_app().rv_data_list)
text = self.generate_post()
sl = SizeLabel(text=text)
sl.texture_update()
print(sl.text)
App.get_running_app().rv_data_list.extend([{'message_id_': str(App.get_running_app().message_id_num),
'text_': text,
'_size': sl.texture_size,
'group_': str(App.get_running_app().message_id_num),
'votes_': str(20)}])
App.get_running_app().message_id_num = App.get_running_app().message_id_num + 1
def on_scrollable_distance(self, *args):
if self.scroll_y > 0:
self.scroll_y = (self.scrollable_distance - self.distance_to_top) / self.scrollable_distance
def on_scroll_y(self, *args):
self.distance_to_top = (1 - self.scroll_y) * self.scrollable_distance
#def adjust_vote_state(self, id_):
# for d in self.data:
# if d['text_'] == id_.text_:
# d['state'] == id_.ids.button_up.state
# id_.state = id_.ids.button_up.state
class DemoApp(App):
# One post format = {'message_id':0, 'text':'post_test_here','_size':[0,0], '_group':str(0), '_score':20}
# Text fromat string = [font=Nunito-Bold.ttf][color=161616]Someone:[/color][/font]\n
message_id_num = 0
rv_data_list = ListProperty()
pending_data = ListProperty()
def build(self):
#Clock.schedule_interval(self.add_log, .01)
Clock.schedule_interval(self.flush_pending_data, .250)
def up_vote(self, button, mode): # Not part of the problem
if button.state == 'down':
if mode == 'all':
print("+1 upvote for message index:" + str(button.parent.parent.message_id) + ' in all posts')
else:
print("+1 upvote for message index:" + str(button.parent.parent.message_id) + ' in top posts')
def down_vote(self, button, mode): # Not part of the problem
if button.state == 'down':
if mode == 'all':
print("-1 upvote for message index:" + str(button.parent.parent.message_id) + ' in all posts')
else:
print("-1 upvote for message index:" + str(button.parent.parent.message_id) + ' in top posts')
def flush_pending_data(self, *args):
if self.pending_data:
pending_data, self.pending_data = self.pending_data, []
self.rv_data_list.extend(pending_data)
def generate_post(self): # This is only to test posts with different line height
e = ['Test post ID: ', str(self.message_id_num)]
for i in range(randint(1, 8)): e.append('\n')
e.append('end of post')
return "".join(e)
def add_log(self, dt):
for i in range(10):
text = self.generate_post()
sl = SizeLabel(text=text)
sl.texture_update()
self.pending_data.append({'message_id_': str(self.message_id_num),
'text_': text,
'_size': sl.texture_size,
'group_': str(self.message_id_num),
'votes_': str(20)})
self.message_id_num = self.message_id_num + 1
if __name__ == '__main__':
DemoApp().run()
demo.kv
<SizeLabel>:
padding: "10dp", "12dp"
size_hint: 0.9, None
height: self.texture_size[1]
font_size: "12dp"
text_size: self.width, None
color: 0,0,0,1
multiline: True
markup: True
<NewPostGrid>:
spacing: "6dp"
message_id: root.message_id_
BoxLayout:
id: voting_menu
orientation: "vertical"
spacing: "2dp"
size_hint: .2, None
height: label.height
ToggleButton:
id: button_up
on_state: app.up_vote(self, 'all')
group: root.group_
#state: root.vote_state_up
text: "UP"
color: (1,1,1,1) if self.state=='normal' else (.8,0,0,1)
font_size: "10dp"
size_hint: 1, .3
background_color: .2, .2, .2, 0
#on_release: app.root.ids.rv.adjust_vote_state(root)
canvas.before:
Color:
rgba: (.1,.1,.1,1)
RoundedRectangle:
pos: self.pos
size: self.size
radius: [6,]
canvas:
Color:
rgba: (.2,.2,.2,1)
Line:
width: 1.4
rounded_rectangle:(self.x,self.y,self.width,self.height, 5)
Label:
id: vote_count
text: root.votes_
size_hint: 1, .4
multiline: False
ToggleButton:
id: button_down
on_state: app.down_vote(self, 'all')
group: root.group_
#state: root.vote_state_down
text: "DOWN"
color: (1,1,1,1) if self.state=='normal' else (.8,0,0,1)
font_size: "10dp"
size_hint: 1, .3
background_color: .2, .2, .2, 0
canvas.before:
Color:
rgba: (.1,.1,.1,1)
RoundedRectangle:
pos: self.pos
size: self.size
radius: [6,]
canvas:
Color:
rgba: (.2,.2,.2,1)
Line:
width: 1.4
rounded_rectangle:(self.x,self.y,self.width,self.height, 5)
Label:
id: label
text: root.text_
padding: "10dp", "12dp"
size_hint: .9, None
height: self.texture_size[1]
font_size: "12dp"
text_size: self.width, None
color: 0,0,0,1
multiline: True
markup: True
#on_texture_size: root.update_message_size(root.message_id, self.texture_size)
pos: self.x, self.y
canvas.before:
Color:
rgba: (0.8, 0.8, 0.8, 1)
RoundedRectangle:
size: self.texture_size
radius: [5, 5, 5, 5]
pos: self.x, self.y
canvas:
Color:
rgba:0.8,0,0,1
Line:
width: 1.4
rounded_rectangle:(self.x,self.y,self.width,self.height, 5)
BoxLayout:
orientation: 'vertical'
BoxLayout:
size_hint: 1, .1
orientation: 'horizontal'
Button:
text: 'Add widget to RV list'
on_release: rv.add()
ToggleButton:
id: active
text: 'active'
on_state: app.add_log(self)
RV:
id: rv
viewclass: 'NewPostGrid' # The view class is TwoButtons, defined above.
data: app.rv_data_list # the data is a list of dicts defined below in the RV class.
scroll_type: ['bars', 'content']
#on_scroll_y: app.fill_data(self, box)
scrollable_distance: box.height - self.height
bar_width: dp(2)
RecycleBoxLayout:
id: box
# This layout is used to hold the Recycle widgets
# default_size: None, dp(48) # This sets the height of the BoxLayout that holds a TwoButtons instance.
key_size: '_size'
default_size_hint: 1, None
size_hint_y: None
spacing: '16dp'
height: self.minimum_height # To scroll you need to set the layout height.
orientation: 'vertical'
padding: ['10dp', '20dp']
Image of app running on windows
Video of app running on android
Video of full app running (The code for the recycleview is identical)
Related
After a question that was answered yesterday, this code can now set the height of the layout/widgets within the recycle view correctly. However, after 1 or 2 updates to the list, the widgets overlap. This is vital to my app working and it is super close.
I read in the Kivy documentation that the recycle view reuses widgets, which is why it is more efficient than the scroll view. However, I have not been able to understand what it means by reusing them, or how it may affect my program.
I would encourage anyone, who thinks they may be able to help, to run the code below to get a better understanding of what I have attempted to describe, but any comments, pointers, or solutions will be massively apreciated.
What currently works:
The new layouts that contain widgets are added to the recycle view successfully
The layouts resize correctly to fit the amount of text they contain
What doesn't currently work
The layouts with widgets moving to the correct position and not overlapping
main.py
from kivy.app import App
from kivy.properties import ListProperty
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.recycleview.views import RecycleDataViewBehavior
example_string = 'jshdfjhsdkjfhjkshfjkshjfhsjkdhfkjshfkjshdfjkshjkdfhsjkdhfkjshfjhsjkfhskjhfjkhfkjhdjfkhsjdkhfkjshdkjhsdjhfsjkdhfjkshdjkfhsdkjhfjksdhfkjshdf)£++£(0'
class MASTER(BoxLayout):
def example_button(self, Button): # Demo of list updating
global example_string # I know
example_string += ')+££+(test\n\n\nsdhfsjdgfj)£++£(0'
temp = []
id_num = 0
for post in example_string.split(')+££+('): # The string is an example of the input data
temp.append({'message_id':id_num, 'text':('[font=Nunito-Bold.ttf][color=161616]Someone:[/color][/font]\n' + post.split(')£++£(')[0]), '_group':str(id_num), '_score':int(post.split(')£++£(')[1])})
id_num = id_num + 1
App.get_running_app().posts = temp
class DemoApp(App):
# One post format = {'message_id':0, 'text':'post_test_here','_size':[0,0], '_group':str(0), '_score':20}
# Text fromat string = [font=Nunito-Bold.ttf][color=161616]Someone:[/color][/font]\n
posts = ListProperty([])
def up_vote(self, button, mode): # Not part of the problem
if button.state == 'down':
if mode == 'all':
print("+1 upvote for message index:" + str(button.parent.parent.message_id) + ' in all posts')
else:
print("+1 upvote for message index:" + str(button.parent.parent.message_id) + ' in top posts')
def down_vote(self, button, mode): # Not part of the problem
if button.state == 'down':
if mode == 'all':
print("-1 upvote for message index:" + str(button.parent.parent.message_id) + ' in all posts')
else:
print("-1 upvote for message index:" + str(button.parent.parent.message_id) + ' in top posts')
if __name__ == '__main__':
DemoApp().run()
demo.kv
MASTER:
<MASTER>:
Button:
text: 'Add items'
on_press: root.example_button(self)
RecycleView:
viewclass: 'PostGrid'
scroll_y: 1
id: rv
data: app.posts
canvas.before:
Color:
rgba: 0, 0, 0, 1
Rectangle:
pos: self.pos
size: self.size
RecycleBoxLayout:
id: box
default_size_hint: 1, None
default_size: None, dp(50) #
size_hint_y: None
padding: ["10dp", "16dp"]
spacing: "20dp"
height: self.minimum_height
orientation: 'vertical'
<PostGrid#BoxLayout>:
message_id: -1
orientation: "horizontal"
text: ''
_group: ''
_score: 0
spacing: "6dp"
text_size: None, None
size_hint_y: None
height: self.minimum_height
BoxLayout:
id: voting_menu
orientation: "vertical"
spacing: "2dp"
size_hint: .2, None
height: label.height # This binding will force voting_menu to resize.
# size: self.size # I don't think it has any effect.
ToggleButton:
id: button_up
on_state: app.up_vote(self, 'all')
group: str(root._group)
text: "UP"
color: (1,1,1,1) if self.state=='normal' else (.8,0,0,1)
font_size: "10dp"
size_hint: 1, .3
background_color: .2, .2, .2, 0
canvas.before:
Color:
rgba: (.1,.1,.1,1)
RoundedRectangle:
pos: self.pos
size: self.size
radius: [6,]
canvas:
Color:
rgba: .2,.2,.2,1
Line:
width: 1.4
rounded_rectangle:(self.x,self.y,self.width,self.height, 5)
Label:
id: vote_count
text: str(root._score)
size_hint: 1, .4
multiline: False
ToggleButton:
id: button_down
on_state: app.down_vote(self, 'all')
group: str(root._group)
text: "DOWN"
color: (1,1,1,1) if self.state=='normal' else (.8,0,0,1)
font_size: "10dp"
size_hint: 1, .3
background_color: .2, .2, .2, 0
canvas.before:
Color:
rgba: (.1,.1,.1,1)
RoundedRectangle:
pos: self.pos
size: self.size
radius: [6,]
canvas:
Color:
rgba: (.2,.2,.2,1)
Line:
width: 1.4
rounded_rectangle:(self.x,self.y,self.width,self.height, 5)
Label:
id: label # For reference.
text: root.text
padding: "10dp", "12dp"
size_hint: .9, None
height: self.texture_size[1]
font_size: "12dp"
text_size: self.width, None
color: 0,0,0,1
multiline: True
markup: True
# on_texture_size: app.update_message_size(root.message_id, self.texture_size)
pos: self.x, self.y
canvas.before:
Color:
rgba: (0.8, 0.8, 0.8, 1)
RoundedRectangle:
size: self.texture_size
radius: [5, 5, 5, 5]
pos: self.x, self.y
canvas:
Color:
rgba:0.8,0,0,1
Line:
width: 1.4
rounded_rectangle:(self.x,self.y,self.width,self.height, 5)
Image of overlap
Guesses
Adding items instead of updating all of the items stored in 'posts' at once
Finding a way to refresh the recycleview to update the widgets
Creating a custom widget in place of the widgets in layouts for every item in the recycleview
This is a strange problem. I noticed that you are using self.minimum_height and label.height to try to adjust the size, and I believe your code should work. But it only seems to work sometimes. I have added a hack to work around this problem by setting the height of the PostGrid instances via python code. Here is that hack:
class MASTER(BoxLayout):
def example_button(self, Button): # Demo of list updating
global example_string # I know
example_string += ')+££+(test\n\n\nsdhfsjdgfj)£++£(0'
temp = []
id_num = 0
for post in example_string.split(')+££+('): # The string is an example of the input data
temp.append({'message_id': id_num, 'text': (
'[font=Roboto-Bold.ttf][color=161616]Someone:[/color][/font]\n' + post.split(')£++£(')[0]),
'_group': str(id_num), '_score': int(post.split(')£++£(')[1])})
id_num = id_num + 1
App.get_running_app().posts = temp
Clock.schedule_once(self.fix_sizes)
def fix_sizes(self, dt):
rbl = self.ids.box
for w in rbl.walk():
if isinstance(w, Factory.PostGrid):
w.height = max(w.ids.label.height, w.ids.voting_menu.height)
My App has 3 screens to navigate through with the 'ActionBar'. I have named each screen in kv file, one is 'track' the other 'pess'. These are the two I need help with.
I created a radiobutton widget in my 'pess' screen class (pictures below) So I could add email address.
In my 'track' screen class I have all the data I'd like to send over email but I think I'm over complicating things. I want to link the active radiobutton from my 'pess' screen to my 'track' screen def send function...
my Main.py looks like this
class ScreenGenerator(ScreenManager):
pass
class PessQuestions(Screen):
email = {}
path = ''
def loadEmails(self, *args):
self.path = App.get_running_app().user_data_dir + '/'
try:
with open(self.path + 'email.json', 'r') as data:
self.email = json.load(data)
except FileNotFoundError:
pass
def saveEmails(self, *args):
print(self.ids.name.text, type(self.ids.name.text))
print(self.ids.address.text, type(self.ids.address.text))
self.email[self.ids.name.text] = self.ids.address.text
self.path = App.get_running_app().user_data_dir + '/'
with open(self.path + 'email.json', 'w') as email_address_json:
json.dump(self.email, email_address_json)
print('saveEmails')
self.ids.address.text = ''
self.ids.name.text = ''
self.ids.info.clear_widgets()
self.on_pre_enter() # refresh screen
def delete_email(self, check):
address = check.ids.who_name.text
del self.email[address]
self.ids.info.remove_widget(check)
self.saveEmails()
##########################################################
########### PRE ENTER ####################################
def on_pre_enter(self):
self.ids.info.clear_widgets()
self.loadEmails()
try:
for name, mail in self.email.items():
self.ids.info.add_widget(Checkboxes(name=name, mail=mail, email=self.email))
except ValueError:
print('VALUE ERROR: Label.text accept only "str"')
self.ids.pess_date.text = strftime('[b]%A[/b], %B %Y') # time and date
##########################################################
########### PRE LEAVE ####################################
def on_pre_leave(self):
self.ids.info.clear_widgets()
self.saveEmails()
class Checkboxes(BoxLayout):
def __init__(self, name='', mail='', email='', **kwargs):
super().__init__(**kwargs)
self.ids.who_name.text = name
self.ids.who_email.text = mail
def on_checkbox_Active(self, checkboxInstance, isActive):
address = ''
if isActive:
'''
Email is used for recipient
'''
name = self.ids.who_name.text
email = self.ids.who_email.text
print('Email Set!')
return email
else:
'''
Nothing happens, email is not used
'''
##########################################################
######## TRACKERS WINDOWS ################################
##########################################################
class Trackers(Screen):
email = {'davi': 'example#gmail.com'}
storage = {}
pess_storage = {}
path = ''
def on_pre_enter(self):
self.path = App.get_running_app().user_data_dir + '/'
self.loadData()
for tracker, num in self.storage.items():
self.ids.track.add_widget(Tracker(text=tracker, number=num))
def on_pre_leave(self):
self.ids.track.clear_widgets()
self.saveData()
def saveData(self, *args):
with open(self.path + 'data.json', 'w') as data:
json.dump(self.storage, data)
def loadData(self, *args):
try:
with open(self.path + 'data.json', 'r') as data:
self.storage = json.load(data)
print(self.storage)
except FileNotFoundError:
pass
def savePESS(self, PESS):
self.pess_storage['physical'] = PESS.ids.phy_text.text
self.pess_storage['emotional'] = PESS.ids.emo_text.text
self.pess_storage['spiritual'] = PESS.ids.spi_text.text
self.pess_storage['sexual'] = PESS.ids.sex_text.text
self.pess_storage['comment'] = PESS.ids.comment.text
def save_text(self, tracker, text_input, *args):
tracker.ids.label.text = text_input.text
self.storage[text_input.text] = '0'
self.saveData()
self.loadData()
print('testing')
def send(self, PESS):
self.path = App.get_running_app().user_data_dir + '/'
with open(self.path + 'bat', 'r') as bat:
login = bat.read()
davi = self.email['davi']
mail = PESS.ids.who_email.text
#kevin = self.email['kevin'] # instead of this I'd like to have this variable linked to
gmail = GMail(davi, login)
day = strftime("%a, %b %d, %Y")
tracker_message = 'PESS\n'
subject_message = f"Subject: PESS {day}"
for pess, txt in self.pess_storage.items():
tracker_message += f"\t{pess.title()}: {txt}\n"
for tracker, numbers in self.storage.items():
tracker_message += f"\n{numbers} ---> {tracker}\n"
message = f"{subject_message}\n\n{tracker_message}"
msg = Message(f'PESS | {day}', to=mail, text=message)
gmail.send(msg)
self.sent_confirmation(f'{self.ids.who_name.text}: {mail}')
def sent_confirmation(self, recipient):
box = BoxLayout(orientation='vertical', padding=40, spacing=5)
pop = Popup(title='Email Sent', content=box, size_hint=(None,None), size=(self.width, self.width/2))
info = Label(text=f'Congrats, {recipient} has recieved an email.')
box.add_widget(info)
pop.open()
self.saveData()
class Tracker(BoxLayout):
def __init__(self, text='', number='', **kwargs):
super().__init__(**kwargs)
self.ids.label.text = text
self.ids.count_add.text = number
class Pess(App):
def build(self):
Config.set('graphics', 'width', '600')
Config.set('graphics', 'height', '800')
from kivy.core.window import Window
Window.clearcolor = get_color_from_hex('#262829')
return ScreenGenerator()
if __name__ == '__main__':
Pess().run()
kv file
#: import rgba kivy.utils.get_color_from_hex
#: import CheckBox kivy.uix.checkbox
<Label>:
font_size: '17dp'
<MenuButton#ButtonBehavior+Label>:
canvas.before:
Color:
rgba: 0.1, 0.5, 0.7, 1
Ellipse:
pos: self.pos
size: self.height, self.height
Ellipse:
pos: self.x + self.width - self.height, self.y
size: self.height, self.height
Rectangle:
pos: self.x + self.height / 2, self.y
size: self.width - self.height, self.height
<RoundButton#ButtonBehavior+Label>:
canvas.before:
Color:
rgba: 0.8, 0.3, 0.1, 1
Ellipse:
pos: self.width / 2.265, self.y + 130
size: self.height - self.height / 1.5, self.height / 3
<MenuButton2#ButtonBehavior+Label>:
canvas.before:
Color:
rgba: 0.8, 0.3, 0.1, 1
Ellipse:
pos: self.pos
size: self.height, self.height
Ellipse:
pos: self.x + self.width - self.height, self.y
size: self.height, self.height
Rectangle:
pos: self.x + self.height / 2, self.y
size: self.width - self.height, self.height
<FloatButton#ButtonBehavior+FloatLayout>:
id: float_root
size_hint: (None, None)
text: '[b]+[/b]'
font_size: '48dp'
btn_size: (140,140)
size: (140,140)
bg_color: (0.8, 0.3, 0.1, 1)
pos_hint: {'x': 5.4, 'y': .17}
Button:
text: float_root.text
font_size: '14dp'
#allow_stretch: True
markup: True
size_hint: (None, None)
size: float_root.btn_size
pos_hint: float_root.pos_hint
background_normal: ''
background_color: (0,1,0,0)
canvas.before:
Color:
rgba: float_root.bg_color
Ellipse:
pos: self.pos
size: self.size
<TrackerButton#Button>:
background_color: 0,0,0,0
<ScreenGenerator>:
Menu:
name: 'menu'
Trackers:
name: 'track'
PessQuestions:
name: 'pess'
<Menu>:
BoxLayout:
orientation: 'vertical'
padding: 20
spacing: 30
Image:
source: 'book.png'
allow_strech: True
Label:
canvas.before:
Color:
rgba: (1,1,1,.7)
Rectangle:
size: self.size
pos: self.pos
size_hint_y: None
height: 1
MenuButton:
text: 'Enter Trackers'
height: dp(60)
size_hint_y: None
background_image: ''
background_color: rgba('#637075')
on_release: app.root.current = 'track'
MenuButton:
text: 'Enter PESS'
height: dp(60)
size_hint_y: None
background_image: ''
background_color: rgba('#637075')
on_release: app.root.current = 'pess'
Label:
canvas.before:
Color:
rgba: (1,1,1,.7)
Rectangle:
size: self.size
pos: self.pos
size_hint_y: None
height: 1
Label:
id: time
text: ''
markup: True
<PessQuestions>:
BoxLayout:
orientation: 'vertical'
pos_hint: {'top': 1}
ActionBar:
height: self.minimum_height + dp(50)
size_hint_y: None
background_image: ''
background_color: rgba('#ffffff') # rgba('#0B3242')
ActionView:
ActionPrevious:
title: '[b]PESS[/b]'
font_size: '17dp'
color: rgba('#AFB7BA')
markup: True
on_release: app.root.current = 'track'
ActionButton:
text: 'SEND'
color: rgba('#AFB7BA')
on_release: app.root.get_screen('track').send(root) # if .send() function is on another class outside of 'class PessQuestions' use the .get_screen() to locate the right screen class
#on_release: app.root.send()
ActionButton:
text: 'TRACKER'
color: rgba('#AFB7BA')
on_release: app.root.current = 'track'
ActionButton:
text: 'HOME'
color: rgba('#AFB7BA')
on_release: app.root.current = 'menu'
BoxLayout:
orientation: 'vertical'
padding: dp(10)
spacing: dp(12)
pos_hint: {'top': 1}
TextInput:
id: phy_text
hint_text: 'Physical'
size_hint_y: None
height: self.minimum_height + dp(7)
multiline: False
TextInput:
id: emo_text
hint_text: 'Emotional'
size_hint_y: None
height: self.minimum_height + dp(7)
multiline: False
TextInput:
id: spi_text
hint_text: 'Spiritual'
size_hint_y: None
height: self.minimum_height + dp(7)
multiline: False
TextInput:
id: sex_text
hint_text: 'Sexual'
size_hint_y: None
height: self.minimum_height + dp(7)
multiline: False
TextInput:
id: comment
hint_text: 'Leave a comment'
size_hint_y: None
height: self.minimum_height + dp(100)
MenuButton:
id: save_pess
text: 'Save PESS'
height: dp(60)
size_hint_y: None
background_image: ''
on_press: self.text = 'SAVED!'
on_release: app.root.get_screen('track').savePESS(root)
MenuButton2:
text: 'Clear PESS'
height: dp(60)
size_hint_y: None
background_image: ''
on_press: root.ids.save_pess.text = 'Save PESS'
on_release: root.resetPESS()
MenuButton:
text: 'Send Report'
color: rgba('#AFB7BA')
height: dp(60)
size_hint_y: None
background_image: ''
on_release: app.root.get_screen('track').send(root) # if .send() function is on another class outside of 'class PessQuestions' use the .get_screen() to locate the right screen class
#on_release: app.root.send()
Label:
canvas.before:
Color:
rgba: (1,1,1,.7)
Rectangle:
size: self.size
pos: self.pos
size_hint_y: None
height: 1
Label:
text: '[b]Physical, Emotional, Spiritual, Sexual[/b] | These principles when lived with a full purpose of heart will invite the Holy Ghost into your lives.'
markup: True
text_size: self.width, None
size_hint: 1, None
height: self.texture_size[1]
Label:
id: pess_date
text: ''
markup: True
Label:
canvas.before:
Color:
rgba: (1,1,1,.7)
Rectangle:
size: self.size
pos: self.pos
size_hint_y: None
height: 1
# Label & Checkbox creation | for selecting email address
Label:
text: "EMAIL"
text_size: self.width, None
size_hint: 1, None
height: self.texture_size[1]
ScrollView:
BoxLayout:
id: info
orientation: 'vertical'
padding: dp(5)
spacing: dp(7)
#size_hint_y: None
#height: self.minimum_height + dp(50)
TextInput:
id: name
hint_text: 'Name'
size_hint_y: None
height: self.minimum_height + dp(7)
multiline: False
TextInput:
id: address
hint_text: 'Email Address'
size_hint_y: None
height: self.minimum_height + dp(7)
multiline: False
MenuButton:
id: save_email
text: 'Save Email'
height: dp(60)
size_hint_y: None
background_image: ''
on_release: root.saveEmails()
<Checkboxes>:
CheckBox:
group: 'emails'
color: .294, .761, .623
on_active: root.on_checkbox_Active(self, self.active)
#on_active: app.root.get_screen('pess').on_checkbox_Active(self, self.active)
size_hint_x: .50
Label:
id: who_name
text: ''
text_size: self.width, None
halign: 'left'
Label:
id: who_email
text: ''
text_size: self.width, None
halign: 'left'
Button:
text: '[b]X[/b]'
markup: True
size_hint_x: None
width: dp(20)
on_release: app.root.get_screen('pess').delete_email(root)
<Trackers>:
BoxLayout:
orientation: 'vertical'
ActionBar:
height: self.minimum_height + dp(50)
size_hint_y: None
background_image: ''
background_color: rgba('#ffffff') #rgba('#0B3242')
ActionView:
ActionPrevious:
title: '[b]TRACKERS[/b]'
font_size: '17dp'
color: rgba('#AFB7BA')
markup: True
on_release: app.root.current = 'pess'
ActionButton:
text: 'RESET'
color: rgba('#AFB7BA')
on_release: root.reset_pop()
ActionButton:
text: 'PESS'
color: rgba('#AFB7BA')
on_release: app.root.current = 'pess'
ActionButton:
text: 'HOME'
color: rgba('#AFB7BA')
on_release: app.root.current = 'menu'
ScrollView:
BoxLayout:
id: track
orientation: 'vertical'
padding: dp(10)
spacing: dp(15)
size_hint_y: None
height: self.minimum_height
BoxLayout:
size_hint_y: None
height: dp(100)
Button:
text: '+'
font_size: '56dp'
size_hint_y: None
background_image: ''
background_color: (0,0,0,0)
on_release: root.addWidget()
#BoxLayout:
#FloatButton:
# pos_hint_x: None
# pos_hint_y: None
# on_release: root.addWidget()
<Tracker>:
count_add: count_add
name: name
size_hint_y: None
height: 130
canvas.before:
Color:
rgba: 0.1, 0.5, 0.7, 1
Rectangle:
pos: self.pos[0] + self.height/2, self.pos[1]
size: self.size[0] - self.height, self.height
Ellipse:
pos: self.pos[0], self.pos[1]
size: self.height, self.height
Ellipse:
pos: self.pos[0] + self.width - self.height, self.pos[1]
size: self.height, self.height
TrackerButton:
text: '[b]X[/b]'
markup: True
size_hint_x: None
width: 120
on_release: app.root.get_screen('track').delete_storage(root)
Label:
id: name
canvas.before:
Color:
rgba: (1,1,1,.7)
Rectangle:
size: self.size
pos: self.pos
size_hint_x: None
width: 1
TrackerButton:
id: label
font_size: '16dp'
on_release: app.root.get_screen('track').change_name(root)
TrackerButton:
id: count_add
font_size: '16dp'
text: '0'
size_hint_x: None
width: 120
on_release: app.root.get_screen('track').add_num(root)
Label:
canvas.before:
Color:
rgba: (1,1,1,.7)
Rectangle:
size: self.size
pos: self.pos
size_hint_x: None
width: 1
TrackerButton:
text: '[b]-[/b]'
font_size: '24dp'
markup: True
size_hint_x: None
width: 120
on_release: app.root.get_screen('track').subtract_num(root)
I had my code running really well, but all of the email sending information was hardcoded. I wanted to give the user options to input an email address and send it to that address.
I added the class Checkboxes which get's created every time I add a name and email address and hit the "Save Email" It's grouped so they are radio buttons.
I can't seem to connect the email address to my send function on the class Trackers... I hope all of this makes some sense. I might be overly complicating things for myself. I could use some help please. Thank you.
Just came across this question and since I am short on time I wont be able to help you completely. If you ever want to access widgets inside and outside of your class screens you need to have a shared parent for the both of them. In your main py file you can have something like this defined.
class MainInterface(BoxLayout):
def __init__(self, **kwargs):
super(MainInterface, self).__init__(**kwargs)
class WindowManager(ScreenManager):
pass
class HomeWindow(Screen):
...
class TrackerWindow(Screen):
...
sm = WindowManager()
screens = [HomeWindow(name="Home"), TrackerWindow(name="TrackerWindow")]
for screen in screens:
sm.add_widget(screen)
class MyMainApp(App):
def build(self):
root = MainInterface()
return root
The way you use your kv file from now on will be different from what you are used to. Your kv file can look something like this. I'll be using different example uses for one button to save time for myself.
<MainInterface>:
WindowManager:
id: screenmanager
HomeWindow:
id: homewindow
...
Button:
pos_hint:{"x":0,"top":1}
size_hint: 0.125, 0.1
text: "Some Stuff"
font_size: (root.width**1 + root.height**1) / 10**2
on_release:
root.ids['trackerwindow'].send(YourData)
root.ids['homewindow'].access_user_inputs(self.parent.user_inputs)
root.ids['screenmanager'].transition.direction = "left"
root.ids['screenmanager'].current="TrackerWindow"
TrackerWindow:
id: trackerwindow
...
notice how on the homescreen button to get information from its own screen you can't just use root.access_user_inputs(root.user_inputs) you created a parent tree so you use self.parent if you want to access a function from your windowmanager you would do a similar set up but use self.parent.parent. I hope this helps and if you have questions please ask and I'll be able to further help you later.
I recently updated kivyMD from 0.102.1 to 0.103.0 and i'm getting the following error:
raise FactoryException('Unknown class <%s>' % name)
kivy.factory.FactoryException: Unknown class
But when go back to version 0.102.1 everything works just fine. I'll post the code below but I just wanted to know if I wanted to update to 0.103.0 what do I need to change? I've tried to do some research but unable to find something that will fix the problem.
.py
#imported from kivy framework
from kivy.app import App
from kivymd.app import MDApp
from kivy.app import App
from datetime import datetime
from datetime import timedelta
from kivy.clock import Clock
from kivy.lang import Builder
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.uix.image import Image
import weatherGrab
from weatherGrab import weatherCheck
import os
#1st screen
class Login_Screen(Screen):
#Takes user input from the .KV file, converts it to string and verifies if its a saved login
def verify_credentials(self):
Username = self.ids.Username_userInput
Password = self.ids.Password_userInput
Username_string = Username.text
Password_string = Password.text
#Allows the variable to be used in another class and method
Login_Screen.verify_credentials.GGG = Username.text
#checks if user input is equal to temp database
if self.ids["Username_userInput"].text == "Username" and self.ids["Password_userInput"].text == "Password":
self.manager.current = "Homepage_Screen"
print("USER LOGIN DATA: " + Username_string +" "+Password_string)
weatherGrab.weatherCheck()
#print('CURRENT WEATHER - London','\n',weatherCheck.currentWeather,'\n' ,weatherCheck.temp)
#print('HERE',var1)
#x = dir(platform) prints all function from that lib
#after log in, sets user input to empty so the next user can login
def clean(self):
self.ids["Username_userInput"].text = ""
self.ids["Password_userInput"].text = ""
#saving user inputs to a text file (temp database)
def save_data(self):
Username = self.ids.Username_userInput
Password = self.ids.Password_userInput
Username_string = Username.text
Password_string = Password.text
TextDoc = open("UserLogin.txt", "a")
username = Username_string
password = Password_string
if username == '' and password == '':
pass
else:
TextDoc.write("\n"+username+","+password)
TextDoc.close()
#loads data from the file (debugging/testing)
def load_data(self):
f = open("UserLogin.txt", "r")
print("FROM TEXT FILE:"+ f.read())
#2nd screen
class Homepage_Screen(Screen):
#Event dispatcher to load data when user enters the second screen
def on_enter(self):
self.load_info()
self.weather()
def weather(self):
self.ids["temp"].text = weatherCheck.temp
self.ids["date"].text = weatherCheck.currentDate
#Text prints in termial for testing/debugging
def load_info(self):
print("PASSING USERNAME TO HOMEPAGE: "+Login_Screen.verify_credentials.GGG)
self.ids["USERNAME"].text = "Welcome"+" "+Login_Screen.verify_credentials.GGG
#Saves User input from the notes textinput
def save_notes(self):
UserNotes = self.ids.UserInput_notes
UserNote = UserNotes.text
TextDoc = open("UserLogin.txt", "a")
TextDoc.write("," + UserNote)
TextDoc.close()
#3rd screen
class Application_Screen(Screen):
pass
#4th screen
class Logout_Screen(Screen):
def newline(self):
TextDoc = open("UserLogin.txt", "a")
TextDoc.write("\n")
TextDoc.close()
def forcequit(self):
exit()
#class for all screens
class ScreenManagement(ScreenManager):
pass
class MainApp(MDApp):
def build(self):
#declaring time from python, and making it refresh every second
self.now = datetime.now()
Clock.schedule_interval(self.update_clock, 1)
def update_clock(self, *args):
self.now = self.now + timedelta(seconds=1)
#Allows for time to be shown on all screens
self.root.get_screen("Homepage_Screen").ids["CurrentTime"].text = self.now.strftime("%H:%M:%S")
self.root.get_screen("Logout_Screen").ids["CurrentTime"].text = self.now.strftime("%H:%M:%S")
self.root.get_screen("Login_Screen").ids["CurrentTime"].text = self.now.strftime("%H:%M:%S")
#print(self.now.strftime("%H:%M:%S"))
MainApp().run()
.kv
#:kivy 1.0
#:import hex kivy.utils.get_color_from_hex
#styles that will apply to all intences for each tag
<MDRaisedButton>:
font_size:18
<Label>:
color: 0,0,0,1
#declaring screen managers and printing them in this order
ScreenManagement:
Login_Screen:
name:"Login_Screen"
id: woow
Homepage_Screen:
name:"Homepage_Screen"
Application_Screen:
name:"Application_Screen"
Logout_Screen:
name:"Logout_Screen"
#style for login screen
<Login_Screen>:
#background color
FloatLayout:
spacing: 10
canvas.before:
Color:
rgba: hex('#eff3fa')
Rectangle:
size: self.size
pos: self.pos
#Navbar
MDToolbar:
id: fb
pos_hint: {'center_x': 0.5, 'top':1.0}
size_hint_y:None
height: 50
title: "Virtual Assistant"
md_bg_color: hex('#132843')
Label:
id: CurrentTime
font_size:18
size_hint_x: .1
color: (1,1,1,1)
#login container
#background color/positioning
BoxLayout:
spacing: 10
orientation: 'vertical'
padding: 50
canvas.before:
Color:
rgba: hex('#000')
Rectangle:
size: self.size
pos: self.pos
pos_hint: {'center_x': 0.5, 'center_y': 0.5}
size_hint: 0.33, None
size_hint_min_x: 200
height: self.minimum_height
#LOGIN CONTENT
#logo
Image:
source: 'Logo.png'
size_hint: None, None
size: 100, 100
pos_hint: {'center_x': 0.5, 'center_y': 0.0}
MDTextField:
id: Username_userInput
hint_text:"Username"
text:"Username"
line_color_normal: 0,0,0,1
MDTextField:
id: Password_userInput
hint_text:"Password"
text:"Password"
password: True
line_color_normal: 0,0,0,1
#color_mode:'accent'
Button:
text:"Login"
size_hint_y: None
height: 60
padding: 10,10
background_color: (2.08, 2.40, 1.92,1)
size_hint: 0.40, None
pos_hint: {'center_x': 0.5, 'center_y': 0.0}
on_press: root.verify_credentials() , root.save_data(), root.clean(), root.load_data()
#style for Homepage screen
<Homepage_Screen>:
#SIDEBAR BUTTONS
NavigationLayout:
id: nav_layout
MDNavigationDrawer:
drawer_logo:'Logo.png'
NavigationDrawerSubheader:
id: USERNAME
NavigationDrawerIconButton:
icon:"home"
text:"Homepage"
theme_text_color: 'Custom'
on_release:
screen_manager.current = "Homepage"
NavigationDrawerIconButton:
icon:'application'
text: "Application"
on_release:
screen_manager.current = "Application"
NavigationDrawerIconButton:
icon: 'dictionary'
text: "Dictionary"
on_release:
screen_manager.current = "Dictionary"
MDRectangleFlatIconButton:
icon:'logout'
text: "Logout"
#on_press: root.clean()
on_release: app.root.current = "Logout_Screen"
size_hint: 1, None
font_size:20
text_color: 0,0,0,1
#BACKGROUND
FloatLayout:
spacing: 10
canvas.before:
Color:
rgba: hex('#eff3fa')
Rectangle:
size: self.size
pos: self.pos
#NAVBAR
MDToolbar:
id: fb
pos_hint: {'center_x': 0.5, 'top':1.0}
size_hint_y:None
height: 50
title: "Virtual Assistant"
md_bg_color: hex('#132843')
left_action_items: [['menu', lambda x: root.ids.nav_layout.toggle_nav_drawer()]]
Label:
id: CurrentTime
size_hint_x: .1
font_size:18
color: (1,1,1,1)
#SIDEBAR SCREEN STYLE
ScreenManager:
id: screen_manager
#HOMEPAGE SCREEN STYLE
Screen:
name: "Homepage"
BoxLayout:
spacing: 10
orientation: 'vertical'
padding: 10
canvas.before:
Color:
rgba: (1,1,1,.8)
Rectangle:
size: self.size
pos: self.pos
pos_hint: {'center_x': 0.5, 'top': (root.height - fb.height)/root.height}
size_hint: 0.3, None
height: 35
GridLayout:
rows: 3
cols: 3
padding: 10
spacing: 15
#pos_hint: {"center_x":0.6, "center_y":0.0}
Label:
id:date
color: (0,0,0,1)
size_hint_x: 2
Label:
id:temp
color: (0,0,0,1)
size_hint_x: 2
Label:
text:'icon'
color: (0,0,0,1)
size_hint_x: 2
BoxLayout:
orientation: 'horizontal'
spacing: 10
padding: 10
canvas.before:
Color:
rgba: (1,1,1,1)
Rectangle:
size: self.size
pos: self.pos
size_hint: 0.8, None
height: 60
pos_hint: {'center_x': .5, 'center_y':.5}
TextInput:
height:60
size_hint: 5, None
focus: True
multiline: False
font_size: 25
padding_y:15
background_color: 1,1,1,0
foreground_color: 0,0,0,1
pos_hint:{'center_x':0, 'center_y':0.5}
MDRectangleFlatIconButton:
icon:'search-web'
height:45
text_color: 0,0,0,1
md_bg_color: hex("#D0F0C0")
pos_hint: {'center_x': 0.5, 'center_y': 0.5}
Label:
text:'Search'
font_size:18
Problem:
Is there a way to fire an event if a multiline = True TextInput lost its focus?
Background:
I have tried on_touch_up function. But it returns the instances of all my TextInputs, not just the current widget. I tried text_validate_unfocus = False, with the same result.
Code:
import kivy
kivy.require("1.10.1")
from kivy.app import App
from kivy.uix.floatlayout import FloatLayout
from kivy.properties import ObjectProperty, NumericProperty
from kivy.uix.textinput import TextInput
from kivy.uix.bubble import Bubble
from kivy.lang import Builder
from kivy.storage.jsonstore import JsonStore
Builder.load_string('''
#: import Window kivy.core.window.Window
<Button>:
background_normal: ''
<Label>:
canvas.before:
Color:
rgba: (0,0.59,0.36,1)
Rectangle:
pos: self.pos
size: self.size
<TextInput>:
hint_text: 'Nuwe nota'
font_size: self.height / 4.5 if self.focus else self.height / 3
background_normal: ''
background_active: ''
foreground_color: (0,0.61,0.36,1) if self.focus else (0.71,0.75,0.71,1)
unfocus_on_touch: False
canvas.after:
Color:
rgb: (0,0,0,1)
Line:
points: self.pos[0] , self.pos[1], self.pos[0] + self.size[0], self.pos[1]
size_hint_y: None
height: Window.height / 6 if self.focus else Window.height / 12
<ChoiceBubble>:
orientation: 'horizontal'
size_hint: (None, None)
size: (160, 120)
pos_hint: {'top': 0.2, 'right': 0.8}
arrow_pos: 'top_left'
BubbleButton:
text: 'Save'
BubbleButton:
text: 'Encrypt..'
BubbleButton:
text: 'Delete'
on_release: root.del_txt_input()
<Notation>:
canvas:
Color:
rgba: (0,0.43,0.37,1)
Rectangle:
pos: self.pos
size: self.size
Label:
pos_hint: {'top': 1, 'right': 0.8}
size_hint: [0.8, None]
height: Window.height / 15
Button:
color: (0,0,0,1)
pos_hint: {'top': 1, 'right': 0.9}
size_hint: [0.1, None]
height: Window.height / 15
Image:
source: 'gear_2.png'
center_y: self.parent.center_y
center_x: self.parent.center_x
size: self.parent.width /1.5, self.parent.height/ 1.5
allow_stretch: True
Button:
color: (0,0,0,1)
pos_hint: {'top': 1, 'right': 1}
size_hint: [0.1, None]
height: Window.height / 15
on_release: root.add_input()
Image:
source: 'plus_text12354.png'
center_y: self.parent.center_y
center_x: self.parent.center_x
size: self.parent.width /1.5, self.parent.height/ 1.5
allow_stretch: True
ScrollView:
size_hint_y: None
size: Window.width, Window.height
pos_hint: {'top': 0.92, 'right': 1}
GridLayout:
id: text_holder
cols: 1
pos_hint: {'top': 0.92, 'right': 1}
padding: 4
size_hint_x: 1
size_hint_y: None
height: self.minimum_height
''')
class ChoiceBubble(Bubble):
pass
class TextInput(TextInput):
got_txt = ObjectProperty(None)
def on_touch_up(self, touch):
if not self.collide_point(*touch.pos):
self.text_validate_unfocus = False
note = Notation()
note.show_bubble
self.got_txt=note.que_txt_input(self)
return super(TextInput, self).on_touch_up(touch)
class Notation(FloatLayout):
which_txt = ObjectProperty(None)
new_txt = ObjectProperty(None)
cnt = NumericProperty(0)
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.the_file=JsonStore('txt_input.json')
self.cnt = self.the_file.count()
lst = self.the_file.keys
def add_input(self):
txt_hld = self.ids.text_holder
self.cnt += 1
self.new_txt = TextInput(id=str(self.cnt))
self.the_file.put(str(self.cnt), the_id=str(self.cnt), the_input='')
txt_hld.add_widget(self.new_txt)
def que_txt_input(self, instance):
self.which_txt = instance
print(instance.text, instance)
return instance
def del_txt_input(self):
print(self.which_txt)
def the_file(self, notestore):
self.notestore = notestore
def show_bubble(self):
self.add_widget(ChoiceBubble())
def get_store(self):
the_keys = list(self.the_file.keys)
print(the_keys)
return the_keys
class theNoteApp(App):
title = 'My Notes'
def build(self):
return Notation()
if __name__ == '__main__':
theNoteApp().run()
Desired Result:
Once the focus is lost I want a bubble widget to be added to the top of my root class. This will give the user the option to save, encrypt or delete the TextInput id and text that just lost its focus, to a JSON file.
Problems
Multiple instances of object, Notation. One from build() method (return Notation()) in App class, and another instance created in on_touch_up() method (note = Notation()) whenever on_touch_up event is fired. Those instances created in on_touch_up() method does not has a visible view i.e. it won't show up in the window.
AttributeError: 'ChoiceBubble' object has no attribute 'del_txt_input'
Text in ChoiceBubble not visible i.e. the default text color is white on white background.
Solutions
Use App.get_running_app().root to get the instantiated root.
Replace root.del_txt_input() with app.root.del_txt_input()
Add color: 0, 0, 0, 1 to class rule, <Label>:
Use on_focus event to display BubbleButton
Snippets - kv
<Label>:
color: 0, 0, 0, 1
...
<ChoiceBubble>:
...
BubbleButton:
text: 'Delete'
on_release: app.root.del_txt_input()
Snippets - py file
class TextInput(TextInput):
got_txt = ObjectProperty(None)
def on_focus(self, instance, value):
if not value: # defocused
note = App.get_running_app().root
note.show_bubble()
self.got_txt = note.que_txt_input(self)
Output
The on_focus event will fire when the focus boolean changes.
I have tried on_touch_up function. But it returns the instances of all my TextInputs, not just the current widget.
This is because you didn't write any code that would limit it to the widget you care about, you can do this if you want to.
I want to make a simple game where you tap and you earn money. I made some code which does that however I don't know how to remove the label. Right now all it does is add 1 to the money variable and make new label.
.py
money = 0
class GameScreen(Screen):
def money(self):
global money
money += 1
self.add_widget(Label(text=str(money), color=(1,0,0,1), font_size=(45),size_hint=(0.2,0.1), pos_hint={"center_x":0.5, "center_y":0.9}))
print(money)
.kv
<GameScreen>:
name: "GameScreen"
canvas:
Color:
rgb: 1, 1, 1
Rectangle:
pos: self.pos
size: self.size
Button:
size: self.texture_size
on_release: root.money()
text: "Press"
font_size: 50
color: 1,1,1,1
background_color: (0,0,0,1)
background_normal: ""
background_down: ""
size_hint: None, None
pos_hint: {"center_x":0.5, "center_y":0.6}
width: self.texture_size[0] + dp(10)
height: self.texture_size[1] + dp(10)
Removing a Label to place a widget with another text consumes recourses in an unavoidable way, you only have to update the text. So you must add the label the first time and then update the text. On the other hand it is recommended that the name of variables, classes and functions are not the same. And try to avoid using global variables because they are difficult to debug.
Making those changes we obtain the following code:
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.label import Label
from kivy.uix.screenmanager import Screen
class GameScreen(Screen):
def __init__(self, **args):
Screen.__init__(self, **args)
self.money = 0
self.label = Label(text=str(self.money), color=(1,0,0,1), font_size=(45),size_hint=(0.2,0.1), pos_hint={"center_x":0.5, "center_y":0.9})
self.add_widget(self.label)
def add_money(self):
self.money += 1
self.label.text = str(self.money)
Builder.load_string('''
<GameScreen>:
name: "GameScreen"
canvas:
Color:
rgb: 1, 1, 1
Rectangle:
pos: self.pos
size: self.size
Button:
size: self.texture_size
on_release: root.add_money()
text: "Press"
font_size: 50
color: 1,1,1,1
background_color: (0,0,0,1)
background_normal: ""
background_down: ""
size_hint: None, None
pos_hint: {"center_x":0.5, "center_y":0.6}
width: self.texture_size[0] + dp(10)
height: self.texture_size[1] + dp(10)
''')
class TestApp(App):
def build(self):
return GameScreen()
if __name__ == '__main__':
TestApp().run()