Related
I'm creating a Password Manager using Python as main language, SQLite for database query and Kivy for GUI. Password manager is a program or application that allows you to store passwords and other login information in a safe location.
This piece of code is responsible for displaying all of the user's added passwords as an MD List in the password menu screen, each bit of which can be clicked and will take the user to the selected password screen where they can manage it (edit, delete or just copy it for personal use).
This is what the password menu screen looks like:
And this is what the screen for the selected password looks like:
P.s. Don't mind the awful design, it's a hastily made one for further testing and debugging.
The problematic part of this code is the password selected screen label, where it now stands for "Suiii".
self.list_of_pass is a variable that stores a list of all passwords (e.g. Netflix, Google and so on). These are entered here using function get_list_of_passwords() from a separate file.
Then with a For loop we create N items of MD List, depending on the number of passwords in the database of a given user. Each item is given a text name and a function self.go_to_chosen_password(i) that will be executed when the item is clicked. The parameter of this function is i, which is supposed to give the function a unique identifier (password name) for future execution of the function. But the problem is that since "Suiii" is the last item in the list, no matter which item in the MD List is clicked, i, which was still on "Suiii" after the end of the for loop, will be responsible for the function parameter, and will therefore only output it.
Any idea how I can fix this and make the selected password screen display the exact password that was clicked in the password menu screen?
Here is a part of main.py:
...
class PasswordsMenuScreen(Screen):
# import from Get_Data_From_Db_to_Display.py
def on_enter(self):
self.list_of_pass = get_list_of_passwords(...)
for i in self.list_of_pass:
self.item = OneLineListItem(text = i)
self.item.bind(on_press = lambda x: self.go_to_chosen_password(i))
self.ids.pass_list.add_widget(self.item)
def go_to_chosen_password(self, passw):
self.manager.get_screen('chosen_password_screen').ids.name.text = passw
self.manager.transition.direction = 'right'
self.manager.current = 'chosen_password_screen'
class ChosenPasswordScreen(Screen):
pass
...
And here is a part of design.kv:
...
<PasswordsMenuScreen>:
canvas.before:
Color:
rgba: (0, 0, 1, 1)
Rectangle:
pos: self.pos
size: self.size
GridLayout:
cols: 1
Label:
text: 'Passwords Lab'
font_size: '35sp'
size_hint_y: None
height: 150
size_hint_x: None
width: 1500
GridLayout:
cols: 2
padding: 15, 15
spacing: 20, 20
GridLayout:
cols: 2
ScrollView:
MDList:
id: pass_list
font_size: '23sp'
Button:
text: '+'
size_hint_y: None
height: 80
size_hint_x: None
width: 80
on_press: root.go_to_adding_password_menu()
GridLayout:
cols: 1
size_hint: 0.2, 0.15
padding: 10, 10
spacing: 10, 0
Button:
text: 'Main Menu'
background_color: 1, 1, 1, 0
opacity: 1 if self.state == 'normal' else 0.5
color: 0.1, 0.7, 1, 1
on_press: root.go_to_main_menu()
<ChosenPasswordScreen>:
canvas.before:
Color:
rgba: (0, 0, 1, 1)
Rectangle:
pos: self.pos
size: self.size
GridLayout:
cols: 1
GridLayout:
cols: 1
padding: 15, 15
spacing: 30, 30
Label:
id: name
text: ''
font_size: '35sp'
GridLayout:
cols: 1
padding: 25, 25
spacing: 30, 30
Label:
text: 'Login'
font_size: '20sp'
TextInput:
id: login
size_hint: 1, 0.5
Label:
text: 'Password'
font_size: '20sp'
TextInput:
id: password
size_hint: 1, 0.5
GridLayout:
cols: 2
Button:
text: 'Edit'
size_hint: 0.2, 0.4
pos_hint: {'center_x': 0.5, 'center_y': 0.6}
Button:
text: 'Delete'
size_hint: 0.2, 0.4
pos_hint: {'center_x': 0.5, 'center_y': 0.6}
GridLayout:
cols: 1
size_hint: 0.2, 0.2 # 20% of window space
padding: 10, 10
spacing: 10, 0
Button:
text: 'Back'
background_color: 0, 1, 1, 0
opacity: 1 if self.state == 'normal' else 0.5
color: 0.1, 0.7, 1, 1
on_press: root.go_back_to_passwords_menu()
...
I've already tried not creating a new variable for the item in the list during for loop and writing everything on one line, but unfortunately this has the same result:
main.py
class PasswordsMenuScreen(Screen):
# import from Get_Data_From_Db_to_Display.py
def on_enter(self):
self.list_of_pass = get_list_of_passwords(...)
for i in self.list_of_pass:
self.ids.pass_list.add_widget(OneLineListItem(text = i, on_press = lambda x: self.go_to_chosen_password(i)))
def go_to_chosen_password(self, passw):
self.manager.get_screen('chosen_password_screen').ids.name.text = passw
self.manager.transition.direction = 'right'
self.manager.current = 'chosen_password_screen'
class ChosenPasswordScreen(Screen):
pass
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)
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)
I'm back with my bartender!
So now everything works, I can control the pumps, and I can update everything as I want. Now I want to add a popup or a progress bar after clicking which drink to get.
My problem is that the popup window doesn't appear until it shortly shows up after the function 'pour_drink' has finished. As I understand it, it has to do with not giving the popup enough time to show up before doing the rest of the tasks. I have tried the time.sleep() but that pauses everything, and I've tried looking for other solutions, but to no avail.
Now the code is not a nice code, I have used the evil global variables, but it works, so I'm happy with that and you don't need to give feedback on the general structure of the code (though it is welcome if you want to look through all the poorly written code). Also, ignore all the comments.. they are of no help and I will make better ones before finishing the project.
The function pour_drink is in the class DrinkMenu, and I have written the function popup_func for the popup window. So if anyone has a nice (or ugly) solution I am all ears.. this project is taking time away from school work and I am dumb enough to let it.
EDIT: Clarification: I want to add a popup window (or a progress bar), preferably with a cancel button to cancel the drink being poured.
The function that handles the pumps for the pouring of the drink is the 'pour_drink' in the class drinkMenu. So what I want is for the popup window to show up when the pour function is accessed and then disappear when it is done.
And if possible it would be nice with a cancel button that makes the program jump out from the function pour_drink, but this is not really necessary if it is too difficult to implement.
So far I have tried playing around with multiprocessing but I couldn't make it work. The popup_func in the drinkMenu class is from my attempt at splitting the two functions pour_drink and popup_func into two processes. But the problem persisted and the popup window only shows up briefly after the pour_drink function is finished.
Thanks for the help!
.py file:
#Necessary files: bartender.py, bartenderkv.kv pump_config.py, drinks_doc.py
from kivy.core.window import Window
#Uncomment to go directly to fullscreen
Window.fullscreen = 'auto'
#Everything needed for kivy
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.lang import Builder
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.core.window import Window
from kivy.uix.button import Button
from kivy.clock import mainthread
from functools import partial
from kivy.uix.popup import Popup
from kivy.uix.label import Label
#Import drink list
from drinks_doc import drink_list, drink_options
from pump_config import config
pins_ingredients = {}
#import gpio control library
import RPi.GPIO as GPIO
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
import time
#Define the different screens
class MainMenu(Screen):
def use_last(self,*args,**kwargs):
global pins_ingredients
#Write a function that use the last used settings,
#i.e that the pumps have corresponding ingredients.
try:
from pump_config import config
for pmp in config:
pins_ingredients[config[str(pmp)]['value']] = [config[str(pmp)]['pin'],config[str(pmp)]['flowrate']]
self.load_available_drinks(pins_ingredients.keys())
self.parent.current = "DrinkMenu"
except:
bttn = Button(text = "Close", font_size=24, size_hint_x = 0.5, size_hint_y = 0.5)
self.popup = Popup(title='No Previous Settings Found!', title_size = '32sp',
content=bttn,
auto_dismiss=False)
bttn.bind(on_release = self.close_n_open)
self.popup.open()
def close_n_open(self,*args,**kwargs):
self.popup.dismiss()
def load_available_drinks(self, ingredients_list):
global drinks
drinks = []
for dri in drink_list:
dr = list(dri["ingredients"].keys())
result = all(elem in ingredients_list for elem in dr)
if result:
drinks.append(dri["name"])
class DrinkMenu(Screen):
#mainthread
def on_enter(self):
self.buttons = []
self.ids.drinks.clear_widgets()
for btn in range(len(drinks)):
self.buttons.append(Button(text=str(drinks[btn])))
self.buttons[btn].bind(on_press = self.pour_drink)
self.ids.drinks.add_widget(self.buttons[btn])
def popup_func(self, onoff):
if onoff == "ON":
self.popup = Popup(title='No Previous Settings Found!', title_size = '32sp',
auto_dismiss=False)
self.popup.open()
elif onoff == "OFF":
self.popup.dismiss()
for k in range(10):
pass
def pour_drink(self, button):
self.popup_func("ON")
#The pins_ingredient dictionary, each value has a list, first number is pin on raspberry pi, the second is the flowrate in ml/min =>
#temp_ingredients = drink_list[button.text]
for num in range(len(drink_list)):
if drink_list[num]['name'] == str(button.text):
temp_ingredients = drink_list[num]['ingredients']
for ing in temp_ingredients:
flow_rate = pins_ingredients[ing][1]
amount = temp_ingredients[ing]
temp_ingredients[ing] = amount/(flow_rate/60)
continue
#The pins_ingredients has ingredients as keys, list as value
#with first pin on RPi, second flowrate of that pump
#The temp_ingredients has ingredients as keys, and a list as
#value, first: how long the pump needs to be on for,
#second is primed for "ON"
ings = len(list(temp_ingredients.keys()))
#print(temp_ingredients)
start = time.time()
state = True
for ing in temp_ingredients:
GPIO.output(pins_ingredients[str(ing)][0],GPIO.HIGH)
deleted = []
while state:
for ing in temp_ingredients:
if ing not in deleted:
if time.time()-start >= temp_ingredients[ing]:
#print(temp_ingredients[ing])
GPIO.output(pins_ingredients[str(ing)][0],GPIO.LOW)
ings -= 1
deleted.append(ing)
if ings == 0:
state = False
self.popup_func("OFF")
class TopUpMenu(Screen):
global pins_ingredients
#mainthread
def on_enter(self):
global pins_ingredients
#print(pins_ingredients)
listed = list(pins_ingredients.keys())
self.buttons = []
self.ids.topupdrinks.clear_widgets()
counter = 0
for btn in range(len(listed)):
if str(listed[btn]) != "None":
self.buttons.append(Button(text=str(listed[btn])))
self.buttons[counter].bind(on_press = partial(self.top_up, str(listed[btn]), "ON"))
self.buttons[counter].bind(on_release = partial(self.top_up,str(listed[btn]),"OFF"))
self.ids.topupdrinks.add_widget(self.buttons[counter])
counter += 1
def top_up(self, *args, **kwargs):
global pins_ingredients
#If state is "ON" set pump pin high
#If state is "OFF" set pump pin low
ingredient = str(args[0])
state = str(args[1])
if state == "ON":
#Set the pin high to turn on corresponding pump
#print(str(ingredient)+ " ON")
GPIO.output(pins_ingredients[ingredient][0],GPIO.HIGH)
if state == "OFF":
#Set the pin low to turn off corresponding pump
#print(str(ingredient)+" OFF")
GPIO.output(pins_ingredients[ingredient][0],GPIO.LOW)
class LoadNewIngredients(Screen):
global drinks
global ingredients
global pins_ingredients
#I want to use the ingredients list in the kivy file
temp_list = [None]*len(list(config.keys()))
def anydup(self, thelist):
seen = set()
for x in thelist:
if x != None:
if x in seen: return True
seen.add(x)
return False
def get_ingredients(self,*args,**kwargs):
global ingredients
ingredients = []
for drink in range(len(drink_list)):
ings = list(drink_list[drink]['ingredients'].keys())
for ing in range(len(ings)):
elem = ings[ing]
if elem not in ingredients:
ingredients.append(elem)
return ingredients
def spinner_clicked(self, ident, value):
if ident == 1:
self.temp_list[0] = str(value)
if ident == 2:
self.temp_list[1] = str(value)
if ident == 3:
self.temp_list[2] = str(value)
if ident == 4:
self.temp_list[3] = str(value)
if ident == 5:
self.temp_list[4] = str(value)
if ident == 6:
self.temp_list[5] = str(value)
if ident == 7:
self.temp_list[6] = str(value)
if ident == 8:
self.temp_list[7] = str(value)
if ident == 9:
self.temp_list[8] = str(value)
def continued(self,*args,**kwargs):
global pins_ingredients
if self.temp_list[0] != "Pump_1":
config["pump_1"]["value"] = self.temp_list[0]
else:
config["pump_1"]["value"] = None
if self.temp_list[1] != "Pump_2":
config["pump_2"]["value"] = self.temp_list[1]
else:
config["pump_2"]["value"] = None
if self.temp_list[2] != "Pump_3":
config["pump_3"]["value"] = self.temp_list[2]
else:
config["pump_3"]["value"] = None
if self.temp_list[3] != "Pump_4":
config["pump_4"]["value"] = self.temp_list[3]
else:
config["pump_4"]["value"] = None
if self.temp_list[4] != "Pump_5":
config["pump_5"]["value"] = self.temp_list[4]
else:
config["pump_5"]["value"] = None
if self.temp_list[5] != "Pump_6":
config["pump_6"]["value"] = self.temp_list[5]
else:
config["pump_6"]["value"] = None
if self.temp_list[6] != "Pump_7":
config["pump_7"]["value"] = self.temp_list[6]
else:
config["pump_7"]["value"] = None
if self.temp_list[7] != "Pump_8":
config["pump_8"]["value"] = self.temp_list[7]
else:
config["pump_8"]["value"] = None
if self.temp_list[8] != "Pump_8":
config["pump_9"]["value"] = self.temp_list[8]
else:
config["pump_9"]["value"] = None
if not self.anydup(self.temp_list):
#print(self.temp_list)
pins_ingredients = {}
filehandler = open('pump_config.py', 'wt')
filehandler.write("config = " + str(config))
filehandler.close()
for pmp in config:
if config[str(pmp)]['value'] != None:
pins_ingredients[config[str(pmp)]['value']] = [config[str(pmp)]['pin'],config[str(pmp)]['flowrate']]
#print(pins_ingredients)
self.load_available_drinks(pins_ingredients.keys())
self.ids.spinner_id_1.text = "Pump_1"
self.ids.spinner_id_2.text = "Pump_2"
self.ids.spinner_id_3.text = "Pump_3"
self.ids.spinner_id_4.text = "Pump_4"
self.ids.spinner_id_5.text = "Pump_5"
self.ids.spinner_id_6.text = "Pump_6"
self.ids.spinner_id_7.text = "Pump_7"
self.ids.spinner_id_8.text = "Pump_8"
self.ids.spinner_id_8.text = "Pump_9"
self.parent.current = "DrinkMenu"
else:
bttn = Button(text = "Ok, I'm sorry!", font_size=24, size_hint_x = 0.5, size_hint_y = 0.5)
self.popup = Popup(title='There can be NO duplicate ingredients!', title_size = '32sp',
content=bttn,
auto_dismiss=False)
bttn.bind(on_release = self.close_n_open)
self.popup.open()
self.parent.current = "LoadNewIngredients"
def close_n_open(self,*args,**kwargs):
self.popup.dismiss()
def load_available_drinks(self, ingredients_list):
global drinks
drinks = []
for dri in drink_list:
dr = list(dri["ingredients"].keys())
result = all(elem in ingredients_list for elem in dr)
if result:
drinks.append(dri["name"])
#Define the ScreenManager
class MenuManager(ScreenManager):
pass
#Designate the .kv design file
kv = Builder.load_file('bartenderkv.kv')
class BartenderApp(App):
def build(self):
return kv
if __name__ == '__main__':
#set up all gpio pins
for pmp in config:
GPIO.setup(config[pmp]['pin'],GPIO.OUT)
GPIO.output(config[pmp]['pin'],GPIO.LOW)
BartenderApp().run()
and for completeness: .kv file:
#Need to define everything, the ScreenManager is the entity that keeps tabs
#on all the different menu windows
#This is for the popup, lets you instansiate a class from anywhere
#:import Factory kivy.factory.Factory
#:import ScrollView kivy.uix.scrollview
MenuManager:
MainMenu:
LoadNewIngredients:
DrinkMenu:
TopUpMenu:
<MainMenu>:
#name defines the signature, referenced when we want to move to it
name: "MainMenu"
GridLayout:
rows: 3
size: root.width, root.height
padding: 10
spacing: 10
Label:
text: "Main Menu"
font_size: 32
GridLayout:
cols: 2
size: root.width, root.height
spacing: 10
Button:
text: "Use Last Settings"
font_size: 32
on_press: root.use_last()
#on_release: app.root.current = "DrinkMenu"
Button:
text: "Load New Ingredients"
font_size: 32
on_release: app.root.current = "LoadNewIngredients"
Button:
text: "See Permissable Ingredients"
font_size: 32
#on_press: print("It Works")
on_release: Factory.PermissablePopup().open()
<LoadNewIngredients>:
name: "LoadNewIngredients"
GridLayout:
cols: 2
size: root.width, root.height
padding: 10
spacing: 10
#size hint sets relative sized, x-dir, y-dir
GridLayout:
size: root.width, root.height
size_hint_x: 0.2
rows: 2
Button:
text: "Continue"
font_size: 24
on_release: root.continued()
Button:
text: "Main Menu"
font_size: 24
on_release: app.root.current = "MainMenu"
GridLayout:
id: choices
rows: 3
cols: 6
orientation: 'tb-lr'
Label:
id: pump_1
text: "Pump 1"
font_size: 24
Label:
id: pump_2
text: "Pump 2"
font_size: 24
Label:
id: pump_3
text: "Pump 3"
font_size: 24
#Spinner is the easy drop down version in kivy, lets see how it looks.
Spinner:
id: spinner_id_1
text: "Pump_1"
#This references the get_ingredients function in the main p
values: root.get_ingredients()
on_text: root.spinner_clicked(1,spinner_id_1.text)
Spinner:
id: spinner_id_2
text: "Pump_2"
values: root.get_ingredients()
on_text: root.spinner_clicked(2,spinner_id_2.text)
Spinner:
id: spinner_id_3
text: "Pump_3"
values: root.get_ingredients()
on_text: root.spinner_clicked(3,spinner_id_3.text)
Label:
id: pump_4
text: "Pump 4"
font_size: 24
Label:
id: pump_5
text: "Pump 5"
font_size: 24
Label:
id: pump_6
text: "Pump 6"
font_size: 24
Spinner:
id: spinner_id_4
text: "Pump_4"
values: root.get_ingredients()
on_text: root.spinner_clicked(4,spinner_id_4.text)
#Spinner is the drop down version, lets see how it looks.
Spinner:
id: spinner_id_5
text: "Pump_5"
values: root.get_ingredients()
on_text: root.spinner_clicked(5,spinner_id_5.text)
Spinner:
id: spinner_id_6
text: "Pump_6"
values: root.get_ingredients()
on_text: root.spinner_clicked(6,spinner_id_6.text)
Label:
id: pump_7
text: "Pump 7"
font_size: 24
Label:
id: pump_8
text: "Pump 8"
font_size: 24
Label:
id: pump_9
text: "Pump 9"
font_size: 24
Spinner:
id: spinner_id_7
text: "Pump_7"
values: root.get_ingredients()
on_text: root.spinner_clicked(7,spinner_id_7.text)
Spinner:
id: spinner_id_8
text: "Pump_8"
values: root.get_ingredients()
on_text: root.spinner_clicked(8,spinner_id_8.text)
Spinner:
id: spinner_id_9
text: "Pump_9"
values: root.get_ingredients()
on_text: root.spinner_clicked(9,spinner_id_9.text)
<DrinkMenu>:
name: "DrinkMenu"
GridLayout:
cols: 2
width: root.width
height: self.minimum_height
padding: 10
spacing: 10
GridLayout:
height: root.height
size_hint_x: 0.4
rows: 2
Button:
text: "Top Up"
font_size: 24
on_release: app.root.current = "TopUpMenu"
Button:
text: "Main Menu"
font_size: 24
on_release: app.root.current = "MainMenu"
ScrollView:
size_hint_y: 0.1
pos_hint: {'x':0, 'y': 0.11}
do_scroll_x: False
do_scroll_y: True
GridLayout:
id: drinks
orientation: 'lr-tb'
size_hint_y: None
size_hint_x: 1.0
cols: 3
height: self.minimum_height
#row_default_height changes the buttons height
row_default_height: 150
row_force_default: True
<TopUpMenu>:
name: "TopUpMenu"
GridLayout:
cols: 2
width: root.width
height: self.minimum_height
padding: 10
spacing: 10
GridLayout:
height: root.height
size_hint_x: 0.4
rows: 1
Button:
text: "Back"
font_size: 24
on_release: app.root.current = "DrinkMenu"
ScrollView:
size_hint_y: 0.1
pos_hint: {'x':0, 'y': 0.11}
do_scroll_x: False
do_scroll_y: True
GridLayout:
id: topupdrinks
orientation: 'lr-tb'
size_hint_y: None
size_hint_x: 1.0
cols: 3
height: self.minimum_height
#row_default_height changes the buttons height
row_default_height: 150
row_force_default: True
#Create a rounded button, the #Button is what it inherits
<RoundedButton#Button>
background_color: (0,0,0,0)
background_normal: ''
canvas.before:
Color:
rgba:
(48/255,84/255,150/255,1)\
if self.state == 'normal' else (0.6,0.6,1,1) # Color is red if button is not pressed, otherwise color is green
RoundedRectangle:
size: self.size
pos: self.pos
radius: [58]
#Create a home button
#Create a drink button
#Create a new ingredients button
#Create a use last settings button
#Create a permissable ingredients button
<PermissablePopup#Popup>
auto_dismiss: False
#size_hint: 0.6,0.2
#pos_hint: {"x":0.2, "top":0.9}
title: "Permissable Ingredients"
GridLayout:
rows: 2
size: root.width, root.height
spacing: 10
GridLayout:
cols: 2
Label:
text: "Sodas"
font_size: 32
#Add list of sodas
Label:
text: "Alcohol"
font_size: 32
#Add list of alcohols
Button:
text: "Done"
font_size: 24
on_release: root.dismiss()
You could use a decorator to display the popup then run the function in a thread. Here is a self-contained example (try running this to see the output for yourself):
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.popup import Popup
from kivy.properties import BooleanProperty
import threading
from time import sleep
def show_popup(function):
def wrap(app, *args, **kwargs):
popup = CustomPopup() # Instantiate CustomPopup (could add some kwargs if you wish)
app.done = False # Reset the app.done BooleanProperty
app.bind(done=popup.dismiss) # When app.done is set to True, then popup.dismiss is fired
popup.open() # Show popup
t = threading.Thread(target=function, args=[app, popup, *args], kwargs=kwargs) # Create thread
t.start() # Start thread
return t
return wrap
class CustomPopup(Popup):
pass
kv = Builder.load_string( # Generic kv stuff
"""
<CustomPopup>:
size_hint: .8, .4
auto_dismiss: False
progress: 0
text: ''
title: "Drink Progress"
BoxLayout:
orientation: 'vertical'
Label:
text: root.text
size_hint: 1, 0.8
ProgressBar:
value: root.progress
FloatLayout:
Button:
text: 'Pour me a Drink!'
on_release: app.mix_drinks()
"""
)
class MyMainApp(App):
done = BooleanProperty(False)
def build(self):
return kv
#show_popup
def mix_drinks(self, popup): # Decorate function to show popup and run the code below in a thread
popup.text = 'Slicing limes...'
sleep(1)
popup.progress = 20
popup.text = 'Muddling sugar...' # Changing the popup attributes as the function runs!
sleep(2)
popup.progress = 50
popup.text = 'Pouring rum...'
sleep(2)
popup.progress = 80
popup.text = 'Adding ice...'
sleep(1)
popup.progress = 100
popup.text = 'Done!'
sleep(0.5)
self.done = True
if __name__ == '__main__':
MyMainApp().run()
The App.mix_drinks function runs in a thread while updating the progress bar and label in the CustomPopup instance.
Adding a cancel button is a different kind of beast. If you really wanted to do this you could kill the thread (see Is there any way to kill a Thread?) but I can't think of an easy way to do this.
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.