Search code examples
pythonfilterkivyprediction

Predictive Text MDDropdown with Python's filter Method


I am trying to build a predictive text MDDropdown on a KivyMD application. I have an MDTextField in which the user will enter text. As the user writes down his/her search, the MDDropdown should show the items which include the text written down. On selection of any given item, the full text of the selection should be added to the TextField.

I have tried Python's filter method to do such task, however I have not succeeded. The MDDropdown is never opened when text is entered. Also, I have noticed that the filter function looks for exact matches. Is there a way to filter partial parts of a string?

The Python code is the following:

from kivy.uix.screenmanager import ScreenManager, Screen
from kivymd.app import MDApp
from kivymd.uix.button import MDIconButton, MDTooltip
from kivymd.uix.menu import MDDropdownMenu
from kivy.clock import Clock


class TooltipMDIconButton(MDIconButton, MDTooltip):
    pass

# HOJA DE ACTIVIDADES ################################
class IngActivWindow(Screen):
    menu_cliente = None

    def cliente_dropdown(self):

        clientes = ['Amazon', 'Adidas','Banana Republic', 'Calvin Klein','Coach', 'DKNY', 'Microsoft','Nike', 'Sony']

        if len(self.ids.name_client.text) != 0:
            def filtar_clientes(cliente):
                filtrar_valor = self.ids.name_client.text
                if cliente in filtrar_valor:
                    return True
                else:
                    return False
            filtered = filter(lambda l: str(self.ids.name_client.text) in l, clientes)

            for valores in filtered:
                print(valores)

            # Create the drop down menu
            for cliente in filtered:
                menu_items = [{"text": f"{valores}"} for valores in filtered]
                self.menu_cliente = MDDropdownMenu(
                    caller=self.ids.name_client,
                    items=menu_items,
                    width_mult=5,
                )
                self.menu_cliente.open()
                self.menu_cliente.bind(on_release=self.set_item_cliente)

    def open_clientes_list(self):
        clientes = ['Amazon', 'Adidas','Banana Republic', 'Calvin Klein','Coach', 'DKNY', 'Microsoft','Nike', 'Sony']
        # Create the drop down menu
        menu_items = [{"text": f"{cliente}"} for cliente in clientes]
        self.menu_cliente = MDDropdownMenu(
            caller=self.ids.name_client,
            items=menu_items,
            width_mult=5,
        )
        self.menu_cliente.open()
        self.menu_cliente.bind(on_release=self.set_item_cliente)

    def set_item_cliente(self, instance_menu, instance_menu_item):
        def set_item_cliente(interval):
            self.ids.name_client.text = instance_menu_item.text
            instance_menu.dismiss()

        Clock.schedule_once(set_item_cliente, 0.5)


# WINDOW MANAGER ################################
class WindowManager(ScreenManager):
    pass


# MAIN CLASS ################################
class PredictiveDropdown(MDApp):


    def build(self):
        return WindowManager()


if __name__ == "__main__":
    PredictiveDropdown().run()

Kivy code:

<WindowManager>:
    id: screen_manager

    IngActivWindow:
        id: ingActiv
        name: 'ingActiv'


<IngActivWindow>:
    MDBoxLayout:
        orientation: 'horizontal'
        size_hint: 1, 1
        pos_hint: {'center_x':0.5 , 'center_y':0.5}
        padding: '20dp'
        MDTextField:
            id: name_client
            pos_hint: {"x":0, 'center_y': 0.5}
            write_tab: False
            size_hint: 0.45, None
            halign: 'left'
            valign: 'center'
            hint_text:'Seleccionar Cliente o Prospecto'
            on_text: root.cliente_dropdown()

        TooltipMDIconButton:
            id: client_list
            pos_hint: {"x":0, 'center_y': 0.5}
            valign: 'center'
            icon: 'arrow-down-drop-circle-outline'
            tooltip_text: 'Haga clic para visualizar la lista completa de clientes'
            on_release: root.open_clientes_list()

Any suggestions of how to achieve the desired task? What am I doing wrong?

Thanks a lot in advance.

EDIT Is there a way to filter by position of the character as well? I changed the filtered variable code. Now when I type, I do get a printed list. However if I write 'a' I get all of the elements with an a, such as 'Calvin Klein'. I would like for the program to print only 'Amazon' and 'Adidas'. I already modified my code accordingly.


Solution

  • The DropDown is never opened because you are using the iterator that filter produces when you print the results of the filter. Once an iterator is used, it cannot be reused. To fix that, simply remove the lines:

            for valores in filtered:
                print(valores)
    

    Consider using regular expressions. instead of filter to get your filtered list.

    Two other issues. You are opening the DropDown in a loop for valores in filtered:, but I cannot see why you are doing that in a loop. And since you are triggering the cliente_dropdown() method using on_text in the kv, it will be triggered again when your set_item_cliente() method changes the text. On way to fix that is to disable the cliente_dropdown() method when dropdown is changing the text.

    Here is a modified version of your IngActivWindow class that addresses those issues:

    class IngActivWindow(Screen):
        menu_cliente = None
        do_dropdown = True
    
        def cliente_dropdown(self):
            # do not open a dropdown if the text was changed by the dropdown
            if not self.do_dropdown:
                return
    
            if self.menu_cliente:
                # if dropdown is already open, close it
                self.menu_cliente.dismiss()
                self.menu_cliente = None
    
            clientes = ['Amazon', 'Adidas','Banana Republic', 'Calvin Klein','Coach', 'DKNY', 'Microsoft','Nike', 'Sony']
            filtered_list = []
    
            if len(self.ids.name_client.text) != 0:
                prefix = self.ids.name_client.text.title()
                filtered = filter(lambda l: l.startswith(prefix), clientes)
                menu_items = [{"text": f"{valores}"} for valores in filtered]
                if len(menu_items) < 1:
                    return
                self.menu_cliente = MDDropdownMenu(
                    caller=self.ids.name_client,
                    callback=self.set_item_cliente,  # use callback instead of bind
                    items=menu_items,
                    width_mult=5,
                )
                self.menu_cliente.open()
    
        def open_clientes_list(self):
            clientes = ['Amazon', 'Adidas','Banana Republic', 'Calvin Klein','Coach', 'DKNY', 'Microsoft','Nike', 'Sony']
            # Create the drop down menu
            menu_items = [{"text": f"{cliente}"} for cliente in clientes]
            self.menu_cliente = MDDropdownMenu(
                caller=self.ids.name_client,
                items=menu_items,
                width_mult=5,
            )
            self.menu_cliente.open()
            self.menu_cliente.bind(on_release=self.set_item_cliente)
    
        def set_item_cliente(self, instance_menu_item):
            def set_item_cliente(interval):
                self.do_dropdown = False
                self.ids.name_client.text = instance_menu_item.text
                self.menu_cliente.dismiss()
                self.menu_cliente = None
                self.do_dropdown = True
    
            Clock.schedule_once(set_item_cliente, 0.5)