Callum
Callum

Reputation: 321

Is there a way to have a TextInput box that searches automatically searches a list of addresses?

I would like a user to be able to start typing an address in a TextInput box and as they start typing it is searching through a list and brining up matching options. Is this something that can be done in kivy?

I can find an example if it's not clear enough but they are seen everywhere on most websites where you've got to fil in an address (e.g delivery address)

Upvotes: 0

Views: 1282

Answers (3)

qua-non
qua-non

Reputation: 4182

You should use triggers(cancels previous call if it happens in quick succession ensuring responsive UI and waits till user is done typing.) showing a dropdown on every change of text instead of doing heavy processing/creating widgets in on_text.

Plus you can use on_text_validate event which get's called on a single line textinput when pressing enter to the text completion like so below::

import re

from kivy.clock import Clock
from kivy.factory import Factory
from kivy.properties import ListProperty, StringProperty, ObjectProperty
from kivy.lang import Builder

Builder.load_string('''
<DDNButton@Button>
   size_hint_y: None
   height: dp(45)
''')

class ComboBox(Factory.TextInput):

   options = ListProperty(('', ))
   _options = ListProperty(('', ))
   option_cls = ObjectProperty(Factory.DDNButton)

   select = StringProperty('')


   def __init__(self, **kw):
       ddn = self.drop_down = Factory.DropDown()
       ddn.bind(on_select=self.set_select)
       super().__init__(**kw)
       self.trigger_dropdown = Clock.create_trigger(self.drop_down_triggered, 1/2)
       self.write_tab = False
       self.multiline = False

   def on_text_validate(self):
       if not self._options:
           return

       # print(self.text, self._options[-1])
       if not self.text in self._options[-1]:
           return

       self.set_select(self, self._options[-1])

   def on_options(self, instance, value):
       self._options = value

   def on__options(self, instance, value):
       ddn = self.drop_down
       ddn.clear_widgets()
       for option in value:
           widg = self.option_cls(text=option)
           widg.bind(on_release=lambda btn: ddn.select(btn.text))
           ddn.add_widget(widg)

   def set_select(self, *args):
       # print('on_select', args)
       if self.text != args[1]:
           self.select = args[1]
           self.text = args[1]
           self.drop_down.dismiss()

   def on_text(self, instance, value):
       self.trigger_dropdown()

   def drop_down_triggered(self, dt):
       value = self.text
       instance = self
       # print(f'on_text {instance} "{value}"')
       if value == '':
           instance._options = self.options
           return
       else:
           # print(f'on_text {instance} "{value}" on_else')
           if value in self.options:
               self.drop_down.pos = 0, -1000
               return

           r = re.compile(f".*{value}", re.IGNORECASE)
           match = filter(r.match, instance.options)
           #using a set to remove duplicates, if any.
           instance._options = list(set(match))
           # print(instance._options)

           instance.drop_down.dismiss()
           # print(instance.parent, instance.pos)
           Clock.schedule_once(lambda dt: instance.drop_down.open(instance), .1)

   def on_touch_up(self, touch):
       # print('focus', value, self.text)
       if touch.grab_current == self:
           self.text = ''
           self.drop_down.open(self)
       # else:
       #     self.drop_down.dismiss()

if __name__ == '__main__':
   from kivy.app import App
   class MyApp(App):
       def build(self):
           return Builder.load_string('''
FloatLayout:
   BoxLayout:
       size_hint: .5, None
       pos: 0, root.top - self.height
       ComboBox:
           options: ['Hello', 'World']
       ComboBox:
           options: ['Hello', 'World']
       Button


''')
   MyApp().run()

Upvotes: 0

John Anderson
John Anderson

Reputation: 39072

Here is another approach that uses a TextInput with a DropDown:

# -*- encoding: utf-8 -*-
"""
Chooser
=======

Uses TextInput with a DropDown to choose from a list of choices

The 'choicesfile' attribute can be used to specify a file of possible choices (one per line)
The 'choiceslist' attribute can be used to provide a list of choices

When typing in the TextInput, a DropDown will show the possible choices
and a suggestion will be shown in the TextInput for the first choice.
Hitting enter will select the suggested choice.

"""


from kivy.properties import ListProperty, StringProperty
from kivy.uix.button import Button
from kivy.uix.dropdown import DropDown
from kivy.uix.textinput import TextInput


class Chooser(TextInput):
    choicesfile = StringProperty()
    choiceslist = ListProperty([])

    def __init__(self, **kwargs):
        self.choicesfile = kwargs.pop('choicesfile', '')  # each line of file is one possible choice
        self.choiceslist = kwargs.pop('choiceslist', [])  # list of choices
        super(Chooser, self).__init__(**kwargs)
        self.multiline = False
        self.halign = 'left'
        self.bind(choicesfile=self.load_choices)
        self.bind(text=self.on_text)
        self.load_choices()
        self.dropdown = None

    def open_dropdown(self, *args):
        if self.dropdown:
            self.dropdown.open(self)

    def load_choices(self):
        if self.choicesfile:
            with open(self.choicesfile) as fd:
                for line in fd:
                    self.choiceslist.append(line.strip('\n'))
        self.values = []

    def keyboard_on_key_down(self, window, keycode, text, modifiers):
        if self.suggestion_text and keycode[0] == ord('\r'):  # enter selects current suggestion
            self.suggestion_text = ' '  # setting suggestion_text to '' screws everything
            self.text = self.values[0]
            if self.dropdown:
                self.dropdown.dismiss()
                self.dropdown = None
        else:
            super(Chooser, self).keyboard_on_key_down(window, keycode, text, modifiers)

    def on_text(self, chooser, text):
        if self.dropdown:
            self.dropdown.dismiss()
            self.dropdown = None
        if text == '':
            return
        values = []
        for addr in self.choiceslist:
            if addr.startswith(text):
                values.append(addr)
        self.values = values
        if len(values) > 0:
            if len(self.text) < len(self.values[0]):
                self.suggestion_text = self.values[0][len(self.text):]
            else:
                self.suggestion_text = ' '  # setting suggestion_text to '' screws everything
            self.dropdown = DropDown()
            for val in self.values:
                self.dropdown.add_widget(Button(text=val, size_hint_y=None, height=48, on_release=self.do_choose))
            self.dropdown.open(self)

    def do_choose(self, butt):
        self.text = butt.text
        if self.dropdown:
            self.dropdown.dismiss()
            self.dropdown = None

if __name__ == '__main__':
    from kivy.app import App
    from kivy.uix.relativelayout import RelativeLayout

    class TestApp(App):
        def build(self):
            layout = RelativeLayout()
            choices = ['Abba', 'dabba', 'doo']
            chooser = Chooser(choiceslist=choices, hint_text='Enter one of Fred\'s words', size_hint=(0.5,None), height=30, pos_hint={'center_x':0.5, 'center_y':0.5})
            layout.add_widget(chooser)
            return layout


    TestApp().run()

Upvotes: 1

John Anderson
John Anderson

Reputation: 39072

Here is a start for another approach that uses Spinner:

from kivy.app import App
from kivy.properties import StringProperty, ListProperty
from kivy.uix.behaviors import FocusBehavior
from kivy.uix.relativelayout import RelativeLayout
from kivy.uix.spinner import Spinner


class AddressChooser(FocusBehavior, Spinner):
    addressfile = StringProperty()
    addresslist = ListProperty([])

    def __init__(self, **kwargs):
        self.addressfile = kwargs.pop('addressfile', '')
        self.sync_height = True
        super(AddressChooser, self).__init__(**kwargs)
        self.modifiers = []
        self.bind(addressfile=self.load_addresses)
        self.load_addresses()

    def on_parent(self, widget, parent):
        self.focus = True

    def load_addresses(self):
        if self.addressfile:
            with open(self.addressfile) as fd:
                for line in fd:
                    self.addresslist.append(line)
        else:
            self.addresslist = []
        self.values = []
        if len(self.text) > 0:
            self.on_text(self, self.text)

    def on_text(self, chooser, text):
        values = []
        for addr in self.addresslist:
            if addr.startswith(text):
                values.append(addr)
        self.values = values
        self.is_open = True

    def keyboard_on_key_up(self, window, keycode):
        if keycode[0] == 304:
            self.modifiers.remove('shift')
        super(AddressChooser, self).keyboard_on_key_up(window, keycode)

    def keyboard_on_key_down(self, window, keycode, text, modifiers):
        if keycode[0] == 304:   # shift
            self.modifiers.append('shift')
        elif keycode[0] == 8 and len(self.text) > 0:   # backspace
            self.text = self.text[:-1]
        else:
            if 'shift' in self.modifiers:
                self.text += text.upper()
            else:
                self.text += text
        super(AddressChooser, self).keyboard_on_key_down(window, keycode, text, modifiers)


class TestApp(App):
    def build(self):
        layout = RelativeLayout()
        chooser = AddressChooser(addressfile='adresses.txt', size_hint=(0.5,None), height=50, pos_hint={'center_x':0.5, 'center_y':0.5})
        layout.add_widget(chooser)
        return layout

if __name__ == '__main__':
    TestApp().run()

Upvotes: 1

Related Questions