Skip to content

Instantly share code, notes, and snippets.

@MaddoxRauch
Last active August 3, 2022 12:00
Show Gist options
  • Save MaddoxRauch/c7ab4df15b5f4cbfd7fad831d31e3248 to your computer and use it in GitHub Desktop.
Save MaddoxRauch/c7ab4df15b5f4cbfd7fad831d31e3248 to your computer and use it in GitHub Desktop.
Kivy Multi-Expression Button (on_long_press, on_single_press, on_double_press events)
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
MultiExpressionButton extends the Kivy Button object and adds three different events; on_single_press,
on_double_press, and on_double_press. DOUBLE_PRESS_TIME determines how long it will wait for the second press before
concluding it is a single press and LONG_PRESS_TIME determines how long the button must be held down to be considered
a long press.
"""
from kivy.uix.button import Button
from kivy.clock import Clock
import timeit
__author__ = "Mark Rauch Richards"
DOUBLE_TAP_TIME = 0.2 # Change time in seconds
LONG_PRESSED_TIME = 0.3 # Change time in seconds
class MultiExpressionButton(Button):
def __init__(self, **kwargs):
super(MultiExpressionButton, self).__init__(**kwargs)
self.start = 0
self.single_hit = 0
self.press_state = False
self.register_event_type('on_single_press')
self.register_event_type('on_double_press')
self.register_event_type('on_long_press')
def on_touch_down(self, touch):
if self.collide_point(touch.x, touch.y):
self.start = timeit.default_timer()
if touch.is_double_tap:
self.press_state = True
self.single_hit.cancel()
self.dispatch('on_double_press')
else:
return super(MultiExpressionButton, self).on_touch_down(touch)
def on_touch_up(self, touch):
if self.press_state is False:
if self.collide_point(touch.x, touch.y):
stop = timeit.default_timer()
awaited = stop - self.start
def not_double(time):
nonlocal awaited
if awaited > LONG_PRESSED_TIME:
self.dispatch('on_long_press')
else:
self.dispatch('on_single_press')
self.single_hit = Clock.schedule_once(not_double, DOUBLE_TAP_TIME)
else:
return super(MultiExpressionButton, self).on_touch_down(touch)
else:
self.press_state = False
def on_single_press(self):
pass
def on_double_press(self):
pass
def on_long_press(self):
pass
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Code to demonstrate the different events that can be done with the multiexpressionbutton object.
"""
from kivy.app import App
from kivy.uix.gridlayout import GridLayout
import multiexpressionbutton as meb
__author__ = "Mark Rauch Richards"
class MainWindow(GridLayout):
def __init__(self, **kwargs):
super(MainWindow, self).__init__(**kwargs)
self.cols = 1
self.rows = 1
button = meb.MultiExpressionButton(text="Click ME")
button.bind(on_long_press=self.long_action)
button.bind(on_single_press=self.single_action)
button.bind(on_double_press=self.double_action)
self.add_widget(button)
@staticmethod
def long_action(instance):
print('long press')
@staticmethod
def single_action(instance):
print('single press')
@staticmethod
def double_action(instance):
print('double press')
class Test(App):
def build(self):
self.title = 'Test'
main = MainWindow()
return main
if __name__ == '__main__':
Test().run()
@MaddoxRauch
Copy link
Author

Add multiexpressionbutton.py to your kivy project and use import statement. Use test.py to see how it works. Test currently displays in console whether it was double pressed, single pressed, or long pressed. The default times for these can be changed using the constants at the top.

@stgpepper
Copy link

This is very nice. Thank you.

However I am curious: in multiexpressionbutton.py what purpose does line 36 self.single_hit.cancel() have? This line gave me error every time I tried to double click on a button inside list of multiexpressionbutton objects. Commenting it out gave me no visible errors or disadvantages. cancel method is not defined within multiexpressionbutton-class or any other module imported.

I'll probably come back though if I run into errors in the future with it, but for now seems that I can go by without this particular line of code.

@stgpepper
Copy link

I encountered a bug when using Scrollview with buttons in for loop. When button was binded by long press and I scrolled up or down my list of buttons it would consider them as long presses and execute function binded. I solved this by adding if not touch.is_mouse_scrolling: statement in the first line of on_touch_down and on_touch_up methods. I would make pull request, but think that is not possible in gist, as far as I know anyway.

@MaddoxRauch
Copy link
Author

Hi, @stgpepper. I appreciate your comments. It's been a while since I wrote this, but here's my best guess. The self.single_hit.cancel() method is there for the Clock.schedule_once() instance. This clock is used to determine if the press is a long press or just a single press. Without it you might end up with several clock instances running at the same time (one for every time a button is pressed) and eating up resources. You can check this by adding gc.get_count() method to the event handler and see how many objects reside in memory as you click the button.

As far as the issue with the Scrollview goes, I appreciate your findings and reporting it to me. Unfortunately, I believe you are correct about pull requests on Gists, as they are not actually projects, just code snippets. You are more than welcome to modify the code and make your own Gist as you see fit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment