Search code examples
pythonuser-interfacekivypopupkivy-language

Allow kivy popup (or progress bar) to show before continuing


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


Solution

  • 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.