Reputation: 321
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
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
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
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