Animating objects via ID in kivy - kivy

I am trying to animate a graphic in kivy. Since all my inputs will be coming from the keyboard, I need to have object references in python, however I still want to set up the widgets in the kv file. To do this I found the only way to trigger anything from python was using IDs. However when trying to start an animation via ID the object doesn't move. Printing the x coordinate shows a change, although I don't see it move.
Here is my main.py:
import threading
import time
import keyboard
from kivy.app import App, ObjectProperty
from kivy.uix.screenmanager import Screen, ScreenManager, NoTransition
from kivy.uix.floatlayout import FloatLayout
from kivy.animation import Animation, AnimationTransition
class MainScreen(Screen):
pass
class OtherScreen(Screen):
pass
def left_animate(object):
anim = Animation(x=object.x - 40, transition='in_back')
anim.start(object)
class RootWidget(FloatLayout):
def __init__(self, **kwargs):
super(RootWidget, self).__init__(**kwargs)
self.screenman = ScreenManager()
self.screenman.add_widget(MainScreen(name="main"))
self.screenman.add_widget(OtherScreen(name="other"))
self.add_widget(self.screenman)
x = threading.Thread(target=self.keyboard_thread)
x.start()
def keyboard_thread(self):
print("Thread started")
while True:
if keyboard.is_pressed('h'):
self.key_event('h')
elif keyboard.is_pressed('j'):
self.key_event('j')
elif keyboard.is_pressed('left'):
self.left_pressed()
def left_pressed(self):
if self.screenman.current == 'main':
anim = Animation(x = 40)
print(self.ids.main.ids.img_ok.x)
anim.start(self.ids.main.ids.img_ok)
def key_event(self, key):
if key == 'h':
self.screenman.current = "main"
else:
self.screenman.current = "other"
class TestApp(App):
def build(self):
return RootWidget()
if __name__ == '__main__':
TestApp().run()
and this is the .kv file:
<RootWidget>:
ScreenManager:
MainScreen:
id: main
OtherScreen:
id: other
<MainScreen>:
canvas.before:
Color:
rgba: 0, 0, 0, 1
canvas:
Rectangle:
pos: root.pos
size: root.size
Image:
id: img_ok
source: '../graphics/icons8-abstimmung-80.png'
pos: 200, 200
<OtherScreen>:
Button:
text: 'Go to main screen'
on_press: root.manager.current = 'main'

The first problem is that you are building two ScreenManagers. One is built in the __init__() method of RootWidget. The other is being built by kv when it evaluates the <RootWidget>: rule in your .kv file. Both of those ScreenManagers fill the RootWidget and one obscures the other. I believe that your Animation is moving the Image widget, but it is underneath and not visible.
I suggest that you start fixing this by eliminating:
self.screenman = ScreenManager()
self.screenman.add_widget(MainScreen(name="main"))
self.screenman.add_widget(OtherScreen(name="other"))
self.add_widget(self.screenman)
from the __init__() method. That will force additional changes to your code.

Related

Kivy text input autocompleting Wikipedia addresses

I would like to implement text input, that is able to autocomplete
Wikipedia addresses.
For example, what you start typing dog, you have do so far
and it will suggest:
https://en.wikipedia.org/wiki/Do
https://en.wikipedia.org/wiki/Donald_Trump
https://en.wikipedia.org/wiki/Dog
https://en.wikipedia.org/wiki/Dominican_Republic
...
as in Wikipedia search engine
And would be fine, if it could solve somehow disambiguation pages as well, but it's not necessary
The structure of your application has to be broken into two parts, the first is you GUI and the second is the logic that checks for wikipedia pages.
The GUI part consists of a TextInput, a binding that will call the function that checks for wikipedia pages and finally a way to display these results. Since you haven't specified how to display these results, I will just assume that they are to be displayed in a Label.
The GUI might look something like this:
# main.py
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.textinput import TextInput
from kivy.properties import ListProperty
from wiki_recommendations import WikiSearcher
class SearchBar(TextInput):
articles = ListProperty()
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.bind(text=self.on_text)
self.bind(articles=self.on_articles)
def on_text(self, *args):
WikiSearcher().get_search_results(self.text, self)
def on_articles(self, *args):
label = self.parent.ids['results']
label.text = '' # Reset the text
for article in self.articles:
label.text = label.text + '\n' + article
kv = """
BoxLayout:
orientation: 'vertical'
padding: self.width * 0.1
spacing: self.height * 0.1
SearchBar
size_hint: 1, 0.2
multiline: False
font_size: self.height*0.8
Label:
id: results
size_hint: 1, 0.8
"""
class SearchApp(App):
def build(self):
return Builder.load_string(kv)
if __name__ == '__main__':
SearchApp().run()
Our SearchBar object has two bindings. The first calls the on_text callback when the user inputs text. This initialises a search, passing the text the user submitted as an argument. The second binding is to a ListProperty that fires the on_articles function when the articles property receives data from WikiSearcher(). This function updates the text property of the Label.
The logic that checks the wikipedia pages should look something like this:
# wiki_recommendations.py
from bs4 import BeautifulSoup
import requests
import threading
def thread(function):
def wrap(*args, **kwargs):
t = threading.Thread(target=function, args=args, kwargs=kwargs, daemon=True)
t.start()
return t
return wrap
class WikiSearcher:
url = 'https://en.wikipedia.org/wiki/Special:Search'
#thread
def get_search_results(self, text: str, root):
"""
This function uses the BeautifulSoup library and the requests library to get the top Wikipedia articls
:param text: The text to be searched.
:param root: The object that calls this function - useful for returning a result.
:return:
"""
# Web scraping happens here
top_articles = [] # The results of your web scraping
root.articles = top_articles
I won't code the web scraping in this answer, as to do so would take a load of work! I have however, noticed that there exists a special wikipedia search page (see url property). When searching "hello" into the bar the web address returned is:
https://en.wikipedia.org/w/index.php?search=hello&title=Special%3ASearch&profile=advanced&fulltext=1&advancedSearch-current=%7B%7D&ns0=1
The key here is the search=hello bit of the URL. Perhaps you can manipulate the URL so that this snippet changes in accordance with the text argument passed to the get_search_results() function. Something like:
url = f"https://en.wikipedia.org/w/index.php?search={text}&title=Special%3ASearch&profile=advanced&fulltext=1&advancedSearch-current=%7B%7D&ns0=1"
but I'll leave that with you to figure out, as this is beyond the remit of Kivy.
You absolutely should use a thread when calling this function, otherwise it will cause the GUI to stutter, resulting in a really poor looking user interface.
Finally, when this function has done its thing, the last thing it should do is update the SearchBar.articles attribute. This is where the root argument comes in, which provides a convenient way to access this class across files. Updating root.articles in-turn calls on_articles which displays these articles in a Label.
Okay, here are some of the improvements to the solution that I have already posted, but since I have made quite a few changes I thought I would post another answer. Here is the final code:
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.textinput import TextInput
from kivy.properties import ListProperty
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.behaviors import ButtonBehavior
from kivy.uix.label import Label
import webbrowser
from wiki_recommendations import WikiSearcher
class SearchBar(TextInput):
articles = ListProperty()
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.bind(text=self.on_text)
self.bind(articles=self.on_articles)
def on_text(self, *args):
WikiSearcher().get_search_results(self.text, self)
def on_articles(self, *args):
self.parent.ids['recommendations'].update_recommendations(self.articles)
class SearchItem(ButtonBehavior, Label):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.url = ''
def on_release(self):
webbrowser.open(self.url)
class Recommendations(BoxLayout):
def update_recommendations(self, recommendations: list):
for (search_item, recommendation) in zip(self.children, recommendations):
search_item.text = recommendation
search_item.url = 'https://en.wikipedia.org/wiki/' + recommendation
kv = """
<SearchItem>:
canvas.before:
Color:
rgba: [0.8, 0.8, 0.8, 1] if self.state == 'normal' else [30/255, 139/255, 195/255, 1]
Rectangle:
size: self.size
pos: self.pos
Color:
rgba: 0, 0, 0, 1
Line:
rectangle: self.x, self.y, self.width, self.height
color: 0, 0, 0, 1
BoxLayout:
canvas.before:
Color:
rgba: 1, 1, 1, 1
Rectangle:
size: self.size
pos: self.pos
orientation: 'vertical'
padding: self.width * 0.1
spacing: self.height * 0.1
SearchBar:
size_hint: 1, 0.2
multiline: False
font_size: self.height*0.8
Recommendations:
id: recommendations
orientation: 'vertical'
SearchItem
SearchItem
SearchItem
SearchItem
SearchItem
"""
class SearchApp(App):
def build(self):
return Builder.load_string(kv)
if __name__ == '__main__':
SearchApp().run()
I have created a custom SearchItem object which is essentially a minimalist button, but on pressing it you are redirected to the appropriate wikipedia page. To be honest you could have just used a Button object, but from my personal experience I prefer the freedoms and flexibility of creating a Label endowed with the ButtonBehavior behaviour.
Alongside this is a wiki_reccomendation.py file:
import wikipedia
import threading
def thread(function):
def wrap(*args, **kwargs):
t = threading.Thread(target=function, args=args, kwargs=kwargs, daemon=True)
t.start()
return t
return wrap
class WikiSearcher:
url = 'https://en.wikipedia.org/wiki/Special:Search'
#thread
def get_search_results(self, text: str, root):
"""
Gets the top Wikipedia articles
:param text: The text to be searched.
:param root: The object that calls this function - useful for returning a result.
:return:
"""
root.articles = wikipedia.search(text)
This remains mostly unchanged. Here is what the app looks like now:
As the user types in the search bar the WikiSearcher object is instantiated and the get_search_results() is called. This updates the articles property of the SearchBar which in turn updates the children of Recommendations (a BoxLayout). These children are essentially just Buttons which direct the user to the recommended page when they are pressed.
I will leave you with making it look pretty - if that is important to you - but I think that is it. Note this only displays the top 5 recommended articles. You can add and remove recommendations (to have more or less than 5) using the clear_widgets() and add_widgets() methods, but simply updating the ones on the screen is much faster.

Kivy, reusing a toggle button layout, but assigning different functions to the buttons

I have created a custom toggle button layout that I would like to reuse multiple times. I would like to reuse it as it contains extensive formatting. I am using generated uuid's to assign the groups so that the multiple instances don't interfere with each other.
Here my test.kv:
#:kivy 2.0.0
#:import uuid uuid
<ExampleToggle#BoxLayout>:
uuid: uuid.uuid4()
orientation: 'horizontal'
ToggleButton:
id: run
text: 'RUN'
group: root.uuid
on_release: root.on_run()
ToggleButton:
id: stop
text: 'STOP'
group: root.uuid
on_release: root.on_stop()
<TestDisplay>:
BoxLayout:
orientation: 'vertical'
ExampleToggle:
on_run: print('A')
on_stop: print('B')
ExampleToggle:
on_run: print('C')
on_stop: print('D')
Here is my test.py:
import kivy
kivy.require('2.0.0')
from kivy.app import App
from kivy.properties import ObjectProperty
from kivy.uix.widget import Widget
class ExampleToggle():
on_run = ObjectProperty(None)
on_stop = ObjectProperty(None)
def on_run(self, *args):
# Dummy function
pass
def on_stop(self, *args):
# Dummy function
pass
class TestDisplay(Widget):
pass
class TestApp(App):
def __init__(self, **kwargs):
super().__init__(**kwargs)
def build(self):
return TestDisplay()
if __name__ == '__main__':
TestApp().run()
I would like to be able to create multiple copies of the ExampleToggle widget and assign different functions to the buttons. I have no issue assigning a function to the on_release event of the individual buttons, however if I do that then it is the same event for every instance of this widget. I would like to be able to assign different functions every time I reuse the widget.
I feel I am either missing something ridiculously simple, or I am going down the wrong path. I have tried multiple different methods, and spent a ton of time reading and researching... Any help would be greatly appreciated.
You just need to provide a way for your functions called to be set at runtime.
For instance, you could have f1 = ObjectProperty() and f2 = ObjectProperty() in your ExampleToggle Python code, then on_release: root.f1() etc. in your kv code. To set the functions, use f1: do_something_a in kv, or your_exampletoggle.f1 = do_something_a in python, where do_something_a is a function.
After a long break I came back to this and worked out the answer. Here is my working solution for others who are curious, or for myself in the future. I had to build the toggle layout in python and then use it in the .kv file. The key was using the 'self.dispatch()' method. I found this from reading through the kivy source code for buttons.
test.kv:
#:kivy 2.0.0
#:import uuid uuid
#:import ExampleToggle test
<TestDisplay>:
BoxLayout:
orientation: 'vertical'
ExampleToggle:
on_run: print('A')
on_stop: print('B')
ExampleToggle:
on_run: print('C')
on_stop: print('D')
test.py:
from uuid import uuid4
import kivy
kivy.require('2.0.0')
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.uix.togglebutton import ToggleButton
from kivy.uix.boxlayout import BoxLayout
class ExampleToggle(BoxLayout):
def __init__(self, **kwargs):
self.register_event_type('on_run')
self.register_event_type('on_stop')
super(ExampleToggle, self).__init__(**kwargs)
self.groupid = uuid4()
self.orientation = 'horizontal'
self.size_hint = (None, .1)
self.width: self.height * 5
self.state = None
rbtn = ToggleButton(text='RUN',
group=self.groupid,
allow_no_selection=False)
sbtn = ToggleButton(text='STOP',
group=self.groupid,
allow_no_selection=False)
self.add_widget(rbtn)
self.add_widget(sbtn)
rbtn.bind(on_release=self.run)
sbtn.bind(on_release=self.stop)
def run(self, *args):
if self.state != 'RUN':
self.dispatch('on_run')
self.state = 'RUN'
def stop(self, *args):
if self.state != 'STOP':
self.dispatch('on_stop')
self.state = 'STOP'
def on_run(self):
pass
def on_stop(self):
pass
class TestDisplay(Widget):
pass
class TestApp(App):
def __init__(self, **kwargs):
super().__init__(**kwargs)
def build(self):
return TestDisplay()
if __name__ == '__main__':
TestApp().run()

Kivy Recycleview. Change label background color on motion event

Is is possible to change background color of without touch events? Please refer to below code. self.collidepoint() method always returns False. I know that even if it will return True it won't work because I should clear RV.data and build it again with new bcolor which seems to be pretty slow way.
CODE
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.recycleview import RecycleView
from kivy.uix.recycleview.views import RecycleDataViewBehavior
from kivy.uix.label import Label
from kivy.properties import BooleanProperty
from kivy.uix.recycleboxlayout import RecycleBoxLayout
from kivy.uix.behaviors import FocusBehavior
from kivy.uix.recycleview.layout import LayoutSelectionBehavior
from kivy.core.window import Window
Builder.load_string('''
<SelectableLabel>:
# Draw a background to indicate selection
bcolor: root.bcolor
canvas.before:
Color:
rgba: (0, 0, 0, 1) if self.selected else self.bcolor
Rectangle:
pos: self.pos
size: self.size
<RV>:
viewclass: 'SelectableLabel'
SelectableRecycleBoxLayout:
default_size: None, dp(56)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
multiselect: True
touch_multiselect: True
''')
class SelectableRecycleBoxLayout(FocusBehavior, LayoutSelectionBehavior,
RecycleBoxLayout):
''' Adds selection and focus behaviour to the view. '''
class SelectableLabel(RecycleDataViewBehavior, Label):
''' Add selection support to the Label '''
index = None
selected = BooleanProperty(False)
selectable = BooleanProperty(True)
bcolor = (0,0,0,1)
def __init__(self, **kwargs):
super(SelectableLabel, self).__init__(**kwargs)
Window.bind(mouse_pos=self.light_up)
def refresh_view_attrs(self, rv, index, data):
''' Catch and handle the view changes '''
self.index = index
return super(SelectableLabel, self).refresh_view_attrs(
rv, index, data)
def light_up(self, window, mouse_pos):
print('MOTION ', mouse_pos, self.collide_point(mouse_pos[0], mouse_pos[1]))
if self.collide_point(mouse_pos[0], mouse_pos[1]):
self.bcolor = (1,1,1,1)
else:
self.bcolor = (0, 0, 0, 1)
class RV(RecycleView):
def __init__(self, **kwargs):
super(RV, self).__init__(**kwargs)
self.data = [{'text': str(x)} for x in range(100)]
class TestApp(App):
def build(self):
return RV()
Window.bind(on_motion=SelectableLabel.on_motion())
if __name__ == '__main__':
TestApp().run()
There are two problems with your code. I have posted a version of your code with what I think are corrections for those problems.
The first is that bcolor in SelectableLabel needs to be a ListProperty (the kv bindings do not work otherwise).
The second is that in your light_up() method, you must convert the window coordinates into coordinates appropriate for passing to collide_point(). To do this, I save a reference to the RV instance in each SelectableLabel (as self.root). And use the to_local() to do the coordinate transformation.
Also, the Window.bind() in the build() method is unnecessary.
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.recycleview import RecycleView
from kivy.uix.recycleview.views import RecycleDataViewBehavior
from kivy.uix.label import Label
from kivy.properties import BooleanProperty, ListProperty
from kivy.uix.recycleboxlayout import RecycleBoxLayout
from kivy.uix.behaviors import FocusBehavior
from kivy.uix.recycleview.layout import LayoutSelectionBehavior
from kivy.core.window import Window
Builder.load_string('''
<SelectableLabel>:
# Draw a background to indicate selection
bcolor: root.bcolor
canvas.before:
Color:
rgba: (0, 0, 0, 1) if self.selected else self.bcolor
Rectangle:
pos: self.pos
size: self.size
<RV>:
viewclass: 'SelectableLabel'
SelectableRecycleBoxLayout:
default_size: None, dp(56)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
multiselect: True
touch_multiselect: True
''')
class SelectableRecycleBoxLayout(FocusBehavior, LayoutSelectionBehavior,
RecycleBoxLayout):
''' Adds selection and focus behaviour to the view. '''
class SelectableLabel(RecycleDataViewBehavior, Label):
''' Add selection support to the Label '''
index = None
selected = BooleanProperty(False)
selectable = BooleanProperty(True)
bcolor = ListProperty([0,0,0,1])
def __init__(self, **kwargs):
super(SelectableLabel, self).__init__(**kwargs)
self.root = App.get_running_app().root
Window.bind(mouse_pos=self.light_up)
def refresh_view_attrs(self, rv, index, data):
''' Catch and handle the view changes '''
self.index = index
return super(SelectableLabel, self).refresh_view_attrs(
rv, index, data)
def light_up(self, window, mouse_pos):
pos_in_parent = self.root.to_local(*mouse_pos)
print('MOTION ', pos_in_parent, self.collide_point(pos_in_parent[0], pos_in_parent[1]))
if self.collide_point(pos_in_parent[0], pos_in_parent[1]):
self.bcolor = (1,1,1,1)
else:
self.bcolor = (0, 0, 0, 1)
class RV(RecycleView):
def __init__(self, **kwargs):
super(RV, self).__init__(**kwargs)
self.data = [{'text': str(x)} for x in range(100)]
class TestApp(App):
def build(self):
return RV()
if __name__ == '__main__':
TestApp().run()

how to pass widget dimensions to __init__

Apologies if this question has an obvious answer but I have been unable to find a solution for some time now. A widget in my app has a 'graph' that is defined in terms of the widget's dimensions. I can dynamically update the 'graph' from kv because I have access to the widget's dimensions there. However I would like to define a default 'graph', also in terms of the widget's size, that appears at startup. I do not know how to pass the widget's dimensions to the __init__ function. Here is my boiled down example:
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.lang import Builder
from kivy.properties import ListProperty
Builder.load_string('''
#:kivy 1.9.2
<MainWidget>:
BoxLayout:
size_hint_x: 20
orientation: 'vertical'
ToggleButton:
text: 'WF1'
state: 'down'
allow_no_selection: False
on_press:
root.line_points = [waveform.x, waveform.top, waveform.right, waveform.y]
root.event_handler()
group: 'lhs_buttons'
ToggleButton:
text: 'WF2'
allow_no_selection: False
on_press:
root.line_points = [waveform.x, waveform.y, waveform.right, waveform.top]
root.event_handler()
group: 'lhs_buttons'
BoxLayout:
size_hint_x: 80
Button:
id: waveform
canvas:
Line:
points: root.line_points
''')
class MainWidget(BoxLayout):
line_points = ListProperty()
def __init__(self, **kwargs):
super(MainWidget, self).__init__(**kwargs)
#self.line_points = [waveform.x, waveform.top, waveform.right, waveform.y]
def event_handler(self):
print "event"
class MyApp(App):
def build(self):
return MainWidget()
if __name__ == '__main__':
MyApp().run()
I suppose a partial solution would be to trigger the on_press event in __init__ for one of the buttons , but I have been unable to figure out how to do that. I am new to Python and to Kivy.
One approach will be to bind line_points like this:
<MainWidget>:
line_points: self.calc_line_points(waveform.x, waveform.y, waveform.right, waveform.top)
...
And calc_line_points will be defined such as :
def calc_line_points(self, x, y, right, top):
return [ x, top, right, y] #put more logic here ...

Kivy, passing argument using roulette and carousel

main:
# -*- coding: utf-8 -*-
import kivy
kivy.require('1.8.0')
'''
Check aida.kv for the ui design
'''
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.properties import (ObjectProperty, ListProperty, StringProperty, NumericProperty)
class Controls(BoxLayout):
timer_value = NumericProperty()
def __init__(self, **kwargs):
super(Controls, self).__init__(**kwargs)
self.roulette_time.bind(rolling_value=self.time_changed)
def time_changed(self, instance, value):
self.timer_value = value
class WeatherRoot(BoxLayout):
pass
class AidaApp(App):
pass
if __name__ == '__main__':
AidaApp().run()
kv:
# -*- coding: cp1252 -*-
#:kivy 1.8.0
#:import CyclicRoulette kivy.garden.roulette.CyclicRoulette
WeatherRoot:
<WeatherRoot#BoxLayout>:
carousel: carousel
controls: controls
BoxLayout:
orientation: "vertical"
Carousel:
id: carousel
Controls:
id: controls
<Controls>:
canvas.before:
Color:
rgba: 0.686, 0.635, 0.541, 0.5
Rectangle:
pos: self.pos
size: self.size
roulette_time: rlt_time
BoxLayout:
CyclicRoulette:
cycle: 60
density: 15
zero_indexed: True
selected_value: 5
width: 50
background_color: [0.686, 0.635, 0.541, 1]
id: rlt_time
Label:
size_hint: (1, .8)
text: format(root.timer_value)
font_size: 50
I get an error 'Controls'object has no attribute 'roulette_time
When I set an attribute I get sort of different errors about binding, etc
only works when in kv Controls set to root, but I must have a different class as a root
Please help me to solve this problem, I am stucked
You get the error Controls'object has no attribute 'roulette_time' because nowhere in your Controls class, or anywhere for that matter, do you define a roulette_time variable. Try setting roulette_time = NumericProperty(0) in the Controls class.
class Controls(BoxLayout):
roulette_time = NumericProperty(0)
Not sure why that's not working, because it should. An ObjectProperty should automatically be created to hold the reference to the CyclicRoulette instance.
But - there's an easier way to do this! You're just updating a property on your Controls class with that value anyway, so you can directly bind them. Replace this line:
roulette_time: rlt_time
with:
timer_value: rlt_time.rolling_value
and whenever the CyclicRoulettes rolling_value property changes, your timer_value on Controls will automatically be updated. You don't need to create the time_changed method or bind it to the CyclicRoulette.

Resources