Kivy: How to separate background touch from a widget touch? - python

In my app, I want to handle background touches and widget touches separately. The Widget documentation ignores how to prevent bubbling from .kv events. Here's a little test case:
from kivy.app import App
class TestApp(App):
def on_background_touch(self):
print("Background Touched")
return True
def on_button_touch(self):
print("Button Touched")
if __name__ == "__main__":
TestApp().run()
And the .kv:
#:kivy 1.8.0
BoxLayout:
orientation: "vertical"
on_touch_down: app.on_background_touch()
padding: 50, 50
Button:
text: "Touch me!"
on_touch_down: app.on_button_touch()
The result: touching either the background or button triggers both handlers. Should I perform collision detection, or is there another way?

You should perform collision detection. For instance, in a class definition:
class YourWidget(SomeWidget):
def on_touch_down(self, touch):
if self.collide_point(*touch.pos):
do_stuff()
Edit: Actually, your method won't work anyway because the Button overlaps the BoxLayout. I would probably instead create a BoxLayout subclass and override on_touch_down, calling super first then if it returns False (indicating the touch hasn't been used yet) doing the BoxLayout interaction.

I wanted a solution that allows me to bind events from .kv files. #inclement solution won't allow me to do that because once you bind the event from .kv, you can't return True anymore to tell the parent you handled the event:
Button:
# you can't return True here, neither from the handler itself
on_touch_down: app.button_touched()
So what I've done is to perform collision detection at the parent, emitting a custom on_really_touch_down only if it doesn't hit any children, and performing collision detection yet again at the child, because all children receive the touch regardless of whatever (it's a mess, I know). Here's the complete solution (requires Kivy >= 1.9.0, because of the usage walk method):
from kivy.app import App
from kivy.uix.button import Button
from kivy.uix.boxlayout import BoxLayout
class CustomTouchMixin(object):
def __init__(self, *args, **kwargs):
super(CustomTouchMixin, self).__init__(*args, **kwargs)
self.register_event_type("on_really_touch_down")
def on_really_touch_down(self, touch):
pass
class CustomTouchWidgetMixin(CustomTouchMixin):
def on_touch_down(self, touch):
if self.collide_point(*touch.pos):
self.dispatch("on_really_touch_down", touch)
return super(CustomTouchWidgetMixin, self).on_touch_down(touch)
class CustomTouchLayoutMixin(CustomTouchMixin):
def on_touch_down(self, touch):
for child in self.walk():
if child is self: continue
if child.collide_point(*touch.pos):
# let the touch propagate to children
return super(CustomTouchLayoutMixin, self).on_touch_down(touch)
else:
super(CustomTouchLayoutMixin, self).dispatch("on_really_touch_down", touch)
return True
class TouchHandlerBoxLayout(CustomTouchLayoutMixin, BoxLayout):
pass
class TouchAwareButton(CustomTouchWidgetMixin, Button):
pass
class TestApp(App):
def on_background_touch(self):
print("Background Touched")
def on_button_touch(self, button_text):
print("'{}' Touched".format(button_text))
if __name__ == "__main__":
TestApp().run()
The .kv:
#:kivy 1.9.0
TouchHandlerBoxLayout:
padding: 50, 50
on_really_touch_down: app.on_background_touch()
TouchAwareButton:
text: "Button One"
on_really_touch_down: app.on_button_touch(self.text)
TouchAwareButton:
text: "Button Two"
on_really_touch_down: app.on_button_touch(self.text)
So this allows me to bind touches from .kv.

Methods for binding touch events via .kv file/string syntax are possible, here's an example that modifies the caller's background when collisions are detected.
<cLabel#Label>:
padding: 5, 10
default_background_color: 0, 0, 0, 0
selected_background_color: 0, 1, 0, 1
on_touch_down:
## First & second arguments passed when touches happen
caller = args[0]
touch = args[1]
## True or False for collisions & caller state
caller_touched = caller.collide_point(*touch.pos)
background_defaulted = caller.background_color == caller.default_background_color
## Modify caller state if touched
if caller_touched and background_defaulted: caller.background_color = self.selected_background_color
elif caller_touched and not background_defaulted: caller.background_color = caller.default_background_color
background_color: 0, 0, 0, 0
canvas.before:
Color:
rgba: self.background_color
Rectangle:
pos: self.pos
size: self.size
And for completeness, here's how to use the above code within a layout that is touch activated only if none of the children (or grandchildren and so on) have also collided with the same event.
<cGrid#GridLayout>:
on_touch_down:
caller = args[0]
touch = args[1]
caller_touched = caller.collide_point(*touch.pos)
spawn_touched = [x.collide_point(*touch.pos) for x in self.walk(restrict = True) if x is not self]
## Do stuff if touched and none of the spawn have been touched
if caller_touched and True not in spawn_touched: print('caller -> {0}\ntouch -> {1}'.format(caller, touch))
cols: 2
size_hint_y: None
height: sorted([x.height + x.padding[1] for x in self.children])[-1]
cLabel:
text: 'Foo'
size_hint_y: None
height: self.texture_size[1]
cLabel:
text: 'Bar'
size_hint_y: None
height: self.texture_size[1] * 2
I may have gotten the texture_size's backwards, or perhaps not, but the height trickery can be ignored for the most part as it's purpose is to aid in making the parent layout more clickable.
The color changing and printing of caller & touch objects should be replaced with do_stuff() or similar methods, as they're there to make the example self contained, and show another way handling caller saved state when touched.

Related

Python, Kivy: Problem with calling functions from different classes/screens

I'm having trouble with correctly calling functions from different classes.
I am making a simple game which calculates the score using the amount of time it takes to clear a level. There's a stopwatch running in the background and I want to add a pause button that popup menu, and a resume button inside this popup menu.
The problem is that when calling the pause function from within the popup menu, it will also be returned inside the popup, instead of inside the main widget.
Here is a simplified version of the code:
import kivy
from kivy.app import App
from kivy.lang import Builder
from kivy.properties import NumericProperty
from kivy.uix.widget import Widget
from kivy.uix.popup import Popup
from kivy.clock import Clock
root_widget = Builder.load_file('app.kv')
class ExampleWidget(Widget):
time = NumericProperty(0)
paused = False
stop = False
# Keeping time
def increment_time(self, interval):
self.time += .1
print(self.time) # To check if stopwatch is running or not
# Stop should mean that the stopwatch must reset when it starts again.
# When paused it should resume when it starts again
def stop_start_or_pause(self):
# stop stopwatch
if self.stop:
Clock.unschedule(self.increment_time)
print('Stopped')
# Make sure time is 0 when restarting
elif not self.stop and not self.paused:
# Keeping time
self.time = 0
Clock.schedule_interval(self.increment_time, .1)
# Pause stopwatch
elif self.paused:
Clock.unschedule(self.increment_time)
print("!!", self.time) # To make it easier to see if stopwatch actually resumes where it left off
print('unscheduled') # Just to confirm and to make it a bit easier to see
# resume stopwatch
elif not self.paused:
Clock.schedule_interval(self.increment_time, .1)
class PopupMenu(Popup):
example = ExampleWidget()
class MyApp(App):
ExampleWidget = ExampleWidget()
def build(self):
return ExampleWidget()
MyApp().run()
.kv file:
#:import Factory kivy.factory.Factory
<PopupMenu#Popup>
auto_dismiss: False
size_hint_y: .8
size_hint_x: .9
title: 'Pause'
example: app.ExampleWidget
BoxLayout:
Button:
text: 'resume'
on_press: root.example.paused = False
on_release: root.dismiss(); root.example.stop_start_or_pause()
size: self.size
<ExampleWidget>:
GridLayout:
col: 2
rows: 3
size: root.size
Button:
text: 'start'
size: self.size
on_press: root.stop = False; root.stop_start_or_pause()
Button:
text: 'stop'
size: self.size
on_press: root.stop = True; root.stop_start_or_pause()
Button:
text: 'Pause menu'
size: self.size
on_press: root.paused = True
on_release: Factory.PopupMenu().open(); root.stop_start_or_pause()
Label:
text: str(round(root.time))
size: self.size
I tried making a function and using Clock.schedule.interval() to keep checking if paused == True, but it keeps returning:
AttributeError: 'float' object has no attribute 'stopped'
This didn't seem like efficient solution anyways, so I didn't want to spend too much time on this function. I also tried to find 'stupid' mistakes (I.e. ',' instead of '.') but that was before I realised that the resume button returned a 'second' stopwatch instead of updating the one I actually wanted to use.
I hope that someone can help, and that my question is clear. English is not my first language so I sometimes have a hard time finding the best way to explain/ask questions.
Thank you in advance!
If I understand your question, the problem is with your MyApp class:
class MyApp(App):
ExampleWidget = ExampleWidget()
def build(self):
return ExampleWidget()
This code is creating two instances of ExampleWidget. One is returned in the build() method, and one is saved as the ExampleWidget attribute of MyApp. Now, when you use the ExampleWidget attribute of MyApp, you are not referencing the ExampleWidget that is the root of your GUI, so it has no effect on what appears on the screen. The fix is to just creat a single instance of ExampleWidget, like this:
class MyApp(App):
ExampleWidget = ExampleWidget()
def build(self):
return self.ExampleWidget

Kivy: return to home screen on inactivity or button push?

I have an application with multiple screens (using ScreenManager), and would like to return to the home screen from any of the other screens: either on >30s inactivity, or on the click of a 'home' button.
Button:
I could add a button to every screen in my .kv file, and bind the following on_press method:
on_press:
screen_manager.transition.direction = 'left'
screen_manager.transition.duration = 0.5
screen_manager.current = 'main_screen'
Is there a better way to add a button on all screens, rather than repeating this on every screen? This would also require me to make sure the button is on the exact same position on each screen.
Automatic check:
When on a screen, is there a way to check how long the screen has not been touched, and if that time is more than 30s then call the method from the button above?
I try to split my graphic code in the .kv file and the code logic in the python file, so I am happy with answers involving both.
Thanks a lot for any pointers, eager to learn more kivy!
You can create a custom button just for that purpose in your kv file:
<MyReturnButton#Button>:
text: 'Return'
pos_hint: {'center_x':0.5, 'y':0}
size_hint: None, None
size: self.texture_size
on_press:
screen_manager.transition.direction = 'left'
screen_manager.transition.duration = 0.5
screen_manager.current = 'main_screen'
Then, just add:
MyReturnButton:
To the kv rule for any screen where you want that Button. Of course, the size, pos_hint, and any other properties can be set to your choice.
As for the automatic screen change, you can use Clock.schedule_once to set a timer in the on_enter method of Screen, and use the on_touch_down method to reset the timer:
def callbackTo2(*args):
screen_manager.current = 'main_screen'
class ScreenOne(Screen):
def __init__(self, *args):
super(ScreenOne, self).__init__(name='ScreenOne')
self.timer = None
def on_enter(self, *args):
print('on_enter ScreenOne:')
# start the timer for 30 seconds
self.timer = Clock.schedule_once(callbackTo2, 30)
def on_leave(self, *args):
# cancel the timer
self.timer.cancel()
self.timer = None
def on_touch_down(self, touch):
if self.timer is not None:
self.timer.cancel()
# reset the timer
self.timer = Clock.schedule_once(callbackTo2, 30)
return super(ScreenOne, self).on_touch_down(touch)

Kivy : How can i reset checkbox active value

When I switch between screens i want to clear the check boxes marked.Marked boxes remain checked when i change screens.
I think my problem will be solved if I find a way to change the activation of checkboxes when I switch to another screen.
But I don't know how to do that.
My Code also has a certain number of checkboxes I choose only six of them. The functions in my main file are to calculate them.
My main.py
class SkillChose(Screen):
checkboxvalues = {}
for i in range(1, 21):
checkboxvalues["s{}".format(i)] = -2
def __init__(self,**kwargs):
super(SkillChose,self).__init__(**kwargs)
self.click_count = 0
self.skills=[]
def click_plus(self,check,id):
if check is True:
self.click_count+=1
self.checkboxvalues[id]=1
return True
def click_extraction(self,id):
if self.checkboxvalues[id]==1:self.click_count-=1
self.checkboxvalues[id]=0
return False
def control(self,id):
if id==0:return False
count=0
for open in self.checkboxvalues.values():
if open==1:
count+=1
for i,j in self.checkboxvalues.items():
print(i,j)
if count<6:
return True
else:
return False
my.kv file
<SkillChose>:
name:"skill"
BoxLayout
ScrollView:
size: self.size
GridLayout:
id: grid
size_hint_y: None
row_default_height: '50sp'
height: self.minimum_height
cols:2
Label:
Label:
Label:
text:"skill1"
CheckBox:
value:"s1"
active:(root.click_plus(self.active,self.value) if root.control(self.value) else False ) if self.active else root.click_extraction(self.value)
Label:
text:"skill2"
CheckBox:
value:"s2"
active:(root.click_plus(self.active,self.value) if root.control(self.value) else False ) if self.active else root.click_extraction(self.value)
Label:
text:"skill3"
CheckBox:
value:"s3"
active:(root.click_plus(self.active,self.value) if root.control(self.value) else False ) if self.active else root.click_extraction(self.value)
The following enhancements (kv file & Python script) are required to clear the CheckBox's attribute active when leaving a screen.
kv file
Use ScreenManager on_leave event to invoke a callback e.g. reset_checkbox()
Snippets - kv file
<SkillChose>:
name:"skill"
on_leave: root.reset_checkbox()
BoxLayout:
...
Py file
Add import statement, from kivy.uix.checkbox import CheckBox
Use for loop to traverse the children of GridLayout: via ids.grid
Use isinstance() function to check for CheckBox widget
Snippets - Py file
class SkillChose(Screen):
...
def reset_checkbox(self):
for child in reversed(self.ids.grid.children):
if isinstance(child, CheckBox):
child.active = False
...

How can I detect when touch is in the children widget in kivy

How I can detect when touch position in a children widget of game grid? When I want call children method mark_label(). Thank you.
class GameGrid(GridLayout):
def on_touch_move(self, touch):
#which label is collision
print(str(touch.pos))
class StartScreen(Screen):
level = Level(mode, 1)
def __init__(self,**kwargs):
super().__init__(**kwargs)
self.create_level()
def create_level(self):
self.ids.game_grid.clear_widgets()
labels = self.level.get_letters_label()
for f in range(len(labels)):
self.ids.game_grid.add_widget(labels[f])
Use self.collide_points() method to check for collision of the touch with the widget of interest.
Snippets
class CreateLabel(Label):
def on_touch_down(self, touch):
if self.collide_point(*touch.pos):
# TODO
# call method mark_label()
if touch.button == "right":
print("Right mouse clicked on {}".format(self.text))
elif touch.button == "left":
print("Left mouse clicked on {}".format(self.text))
else:
print(self.id)
return True
return super(CreateLabel, self).on_touch_down(touch)
Programming Guide » Events and Properties » Dispatching a Property event
If the touch falls inside of our widget, we change the value of
pressed to touch.pos and return True, indicating that we have consumed
the touch and don’t want it to propagate any further. ... Finally,
if the touch falls outside our widget, we call the original event
using super(…) and return the result. This allows the touch event
propagation to continue as it would normally have occurred.
Example
main.py
from kivy.app import App
from kivy.uix.gridlayout import GridLayout
from kivy.uix.label import Label
class CreateLabel(Label):
def on_touch_down(self, touch):
if self.collide_point(*touch.pos):
if touch.button == "right":
print("Right mouse clicked on {}".format(self.text))
elif touch.button == "left":
print("Left mouse clicked on {}".format(self.text))
else:
print(self.id)
return True
return super(CreateLabel, self).on_touch_down(touch)
class RootWidget(GridLayout):
def __init__(self, **kwargs):
super(RootWidget, self).__init__(**kwargs)
self.build_board()
def build_board(self):
# make 9 label in a grid
for i in range(0, 9):
label = CreateLabel(id=str(i), text="Label {}".format(i))
self.add_widget(label)
class TestApp(App):
def build(self):
return RootWidget()
if __name__ == '__main__':
TestApp().run()
test.kv
#:kivy 1.10.0
<CreateLabel>:
canvas.before:
Color:
rgba: 0, 1, 1, 0.5 # 50% blue
Rectangle:
size: self.size
pos: self.pos
font_size: 30
on_touch_down: self.on_touch_down
<RootWidget>:
rows: 3
cols: 3
row_force_default: True
row_default_height: 150
col_force_default: True
col_default_width: 150
padding: [10]
spacing: [10]
Output

Dynamically resizing a kivy label (and button) on the python side

How do I dynamically resize the a label or button, in particular, the text_size and height, depending on the amount of text, at run-time?
I am aware that this question has already been answered in one way with this question:
Dynamically resizing a Label within a Scrollview?
And I reflect that example in part of my code.
The problem is dynamically resizing the labels and buttons at run-time. Using, for example:
btn = Button(text_size=(self.width, self.height), text='blah blah')
...and so on, only makes the program think (and logically so) that the "self" is referring to the class which is containing the button.
So, how do I dynamically resize these attributes in the python language, not kivy?
My example code:
import kivy
kivy.require('1.7.2') # replace with your current kivy version !
from kivy.app import App
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.properties import ObjectProperty
from kivy.uix.button import Button
from kivy.uix.gridlayout import GridLayout
i = range(20)
long_text = 'sometimes the search result could be rather long \
sometimes the search result could be rather long \
sometimes the search result could be rather long '
class ButtonILike(Button):
def get_text(self):
return long_text
class HomeScreen(Screen):
scroll_view = ObjectProperty(None)
def __init__(self, *args, **kwargs):
super(HomeScreen, self).__init__(*args, **kwargs)
layout1 = GridLayout(cols=1, spacing=0, size_hint=(1, None), \
row_force_default=False, row_default_height=40)
layout1.bind(minimum_height=layout1.setter('height'),
minimum_width=layout1.setter('width'))
layout1.add_widget(ButtonILike())
for result in i:
btn1 = Button(font_name="data/fonts/DejaVuSans.ttf", \
size_hint=(1, None), valign='middle',)#, \
#height=self.texture_size[1], text_size=(self.width-10, None))
btn1.height = btn1.texture_size[1]
btn1.text_size = (btn1.width-20, layout1.row_default_height)
btn1.text = long_text
btn2 = Button(font_name="data/fonts/DejaVuSans.ttf", \
size_hint=(1, None), valign='middle')
btn2.bind(text_size=(btn2.width-20, None))
btn2.text = 'or short'
layout1.add_widget(btn1)
layout1.add_widget(btn2)
scrollview1 = self.scroll_view
scrollview1.clear_widgets()
scrollview1.add_widget(layout1)
class mybuttonsApp(App):
def build(self):
return HomeScreen()
if __name__ == '__main__':
mybuttonsApp().run()
And the kv file:
#:kivy 1.7.2
<ButtonILike>:
text_size: self.width-10, None
size_hint: (1, None)
height: self.texture_size[1]
text: root.get_text()
#on_release: root.RunSearchButton_pressed()
<HomeScreen>:
scroll_view: scrollviewID
AnchorLayout:
size_hint: 1, .1
pos_hint: {'x': 0, 'y': .9}
anchor_x: 'center'
anchor_y: 'center'
Label:
text: 'Button Tester'
ScrollView:
id: scrollviewID
orientation: 'vertical'
pos_hint: {'x': 0, 'y': 0}
size_hint: 1, .9
bar_width: '8dp'
You can see that I added the button from the kv file which displays all the behavior that I want at the top of the list. Resize your window while running it, and you can see the magic. And, of course, changing the text_size also makes it possible for me to align text.
I simply have not been able to achieve the same behavior on the python side. My app requires that the buttons be created at run-time. I think the answer might lie with "bind()", though admittedly, I'm not sure I used it correctly in my attempts or that I understand it fully. You can see that I tried with "btn2", which I thought would've thrown the text to the left (since halign defaults to left), but didn't seem to do anything.
I appreciate the help.
I think the best way is to set Label's/Button's size to texture_size:
Label:
text: "test"
size_hint: None, None
size: self.texture_size
canvas.before: # for testing purposes
Color:
rgb: 0, 1, 0
Rectangle:
pos: self.pos
size: self.size
My answer is slightly different from #martin's - I only want to modify the height.
def my_height_callback(obj, texture: Texture):
if texture:
obj.height = max(texture.size[1], 100)
class MyButton(Button):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.size_hint = (1, None)
self.bind(texture=my_height_callback)
When the text is rendered the texture property of the button gets set. That texture's height is then pushed to the button's height via the callback. Calling max() allows for a minimum height to be set. This works fine with labels as well.
btn2.bind(text_size=(btn2.width-20, None))
As with your other question, the problem is that you have the syntax of bind wrong. You must pass a function, but you just wrote a tuple, and bind can't do anything useful with that - it certainly doesn't know you happened to write btn2.width there.
Also, the syntax is that bind calls the function when the given property changes. That's the opposite of what you want - you need to change the text_size when btn2.width changes, not call a function when text_size changes
I think something like the following would work. instance and value are the default arguments we ignored in the other question.
def setting_function(instance, value):
btn2.text_size = (value-20, None)
btn1.bind(width=setting_function)
I was looking to resize both the text_size width and height, the latter specifically with regard to the documented behaviour of kivy.Label that vertical alignment of text in a label cannot be achieved without doing this first. Further, I needed to do it in python, not .kv.
class WrappedVAlignedLabel(Label):
def __init__(self,**kwargs):
super().__init__(**kwargs)
self.bind(height=lambda *x:self.setter('text_size')(self, (self.width, self.height)))
strangely, binding on width instead of height would only set text_size[0], I guess due to some order of rendering self.height wasn't yet computed, so the setting of text_size[1] wasn't happening. Whereas binding on height gets them both.

Categories