Skip to content

Instantly share code, notes, and snippets.

@akshayaurora
Created January 22, 2023 11:17
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save akshayaurora/3d1a48a3c7ae972cce7931e58077fcb1 to your computer and use it in GitHub Desktop.
Save akshayaurora/3d1a48a3c7ae972cce7931e58077fcb1 to your computer and use it in GitHub Desktop.
Date picker: Sample: Not directly usable. You need some additions with this like images for focus, glow, a theme folder in, a app.theme property etc.
'''
Custom Behaviors are defined in this module.
'''
from kivy.app import App
from kivy.animation import Animation
from kivy.lang import Builder
from kivy.clock import Clock
from kivy.config import Config
from kivy.core.window import Window
is_desktop = Config.get('kivy', 'desktop') == '1'
from kivy.event import EventDispatcher
from kivy.factory import Factory
from kivy.graphics import Rectangle, BorderImage, Color
from kivy.metrics import dp
from kivy.properties import (NumericProperty, StringProperty,
ListProperty, BooleanProperty)
from kivy.graphics import (Rectangle, Color, Ellipse, StencilPop,
StencilPush, StencilUnUse, StencilUse)
from kivy.uix.behaviors import CoverBehavior
from kivy.uix.image import Image
from kivy.uix.behaviors import FocusBehavior
class CoverImage(CoverBehavior, Image):
def __init__(self, **kwargs):
super(CoverImage, self).__init__(**kwargs)
def on_texture(self, *args, **kwargs):
texture = self._coreimage.texture
self.reference_size = texture.size
self.texture = texture
class TouchRippleBehavior(EventDispatcher):
__events__ = ('on_released',)
ripple_rad = NumericProperty(10)
ripple_pos = ListProperty([0, 0])
#141 ,188, 234
ripple_color = ListProperty((141./256., 188./256., 234./256., 1))
ripple_duration_in = NumericProperty(.3)
ripple_duration_out = NumericProperty(.3)
fade_to_alpha = NumericProperty(.3)
ripple_scale = NumericProperty(2.0)
ripple_func_in = StringProperty('out_quad')
ripple_func_out = StringProperty('in_quad')
def on_touch_down(self, touch):
if self.collide_point(touch.x, touch.y):
# self.anim_complete(self, self)
self.ripple_pos = ripple_pos = (touch.x, touch.y)
Animation.cancel_all(self, 'ripple_rad', 'ripple_color')
rc = self.ripple_color
ripple_rad = self.ripple_rad
self.ripple_color = [rc[0], rc[1], rc[2], 1.]
anim = Animation(
ripple_rad=max(self.width, self.height) * self.ripple_scale,
t=self.ripple_func_in,
ripple_color=[rc[0], rc[1], rc[2], self.fade_to_alpha],
duration=self.ripple_duration_in)
anim.bind(on_complete=self.anim_complete)
anim.start(self)
with self.canvas:
StencilPush()
Rectangle(size=self.size, pos=self.pos)
StencilUse()
self.col_instruction = Color(
rgba=self.ripple_color, group='one')
self.ellipse = Ellipse(
size=(ripple_rad, ripple_rad),
pos=(ripple_pos[0] - ripple_rad/2.,
ripple_pos[1] - ripple_rad/2.),
group='one')
StencilUnUse()
Rectangle(size=self.size, pos=self.pos)
StencilPop()
self.bind(
ripple_color=self.set_color, ripple_pos=self.set_ellipse,
ripple_rad=self.set_ellipse)
return super(TouchRippleBehavior, self).on_touch_down(touch)
def set_ellipse(self, instance, value):
ellipse = self.ellipse
ripple_pos = self.ripple_pos
ripple_rad = self.ripple_rad
ellipse.size = (ripple_rad, ripple_rad)
ellipse.pos = (
ripple_pos[0] - ripple_rad/2.,
ripple_pos[1] - ripple_rad/2.)
def set_color(self, instance, value):
self.col_instruction.rgba = value
def on_release(self):
rc = self.ripple_color
anim = Animation(
ripple_color=[rc[0], rc[1], rc[2], 0.],
t=self.ripple_func_out, duration=self.ripple_duration_out)
anim.bind(on_complete=self.anim_completed)
anim.start(self)
def anim_complete(self, anim, instance):
self.ripple_rad = 10
self.canvas.remove_group('one')
def on_released(self):
pass
def anim_completed(self, anim, instance):
self.anim_complete(anim, instance)
self.dispatch('on_released')
class HoverBehavior(object):
hover = BooleanProperty(False)
def __init__(self, *args, **kwargs):
super(HoverBehavior, self).__init__(*args, **kwargs)
Window.bind(mouse_pos=self.on_mouse_pos)
def on_mouse_pos(self, discard, pos):
if not self.get_root_window():
return
self.hover = self.collide_point(*self.to_widget(*pos))
def on_touch_up(self, touch):
super(HoverBehavior, self).on_touch_up(touch)
class HoverLookBehavior(HoverBehavior):
_animating = False
Builder.load_string('''
#:import Clock kivy.clock.Clock
<HoverLookBehavior>
hovering_opacity: 0
dx: -1000
on_hover:
if args[1]:\
self.hovering_opacity = .5;\
dx=0;\
Clock.schedule_once(self._anim_flash, .5)
if not args[1]:\
self.hovering_opacity = 0;\
self.dx = -self.height;\
self._animating = False;\
Animation.cancel_all(self);\
Clock.unschedule(self._anim_flash)
canvas.after:
Color:
rgba: 1, 1, 1, self.hovering_opacity
# Rectangle:
# source: 'data/theme/' + app.theme + '/images/glow.png'
# size: self.width - dp(18), self.height
# pos: self.x + dp(9), self.y
Rectangle
source: 'data/theme/' + app.theme + '/images/flash_white.png'
size: root.height, root.height
pos: root.dx, root.y
''')
def _anim_flash(self, dt):
if self._animating:
return
self._animating = True
Animation.cancel_all(self)
anim = Animation(dx=self.width+self.height, d=.45)
def _on_complete(self, widget):
self._animating = False
anim.bind(on_complete=_on_complete)
anim.start(self)
class FocusTrigger(FocusBehavior):
'''This defines a class that triggers the focusable item when a space/enter
key is pressed. This class can only be used with classes like
Button/ButtonBehavior that have a `trigger_action`.
'''
touched = BooleanProperty(False)
def on_touch_down(self, touch):
if self.collide_point(*touch.pos):
self.touched = True
super(FocusTrigger, self).on_touch_down(touch)
def on_touch_up(self, touch):
super(FocusTrigger, self).on_touch_up(touch)
Clock.schedule_once(self._untouch, .25)
def keyboard_on_key_down(self, window, keycode, text, modifiers):
'''Trigger action when `space` or `enter` is pressed.
'''
if keycode[1] in ('enter', 'spacebar') and not modifiers:
self.touched = True
self.trigger_action()
Clock.schedule_once(self._untouch, .25)
return True
return super(FocusTrigger, self).keyboard_on_key_down(
window, keycode, text, modifiers)
def _untouch(self, dt):
self.touched = False
class FocusLook(object):
Builder.load_string('''
<FocusLook>
focus_look_width: 0
focus_opacity: 0
on_focused:
from kivy.animation import Animation
anim = Animation(\
focus_look_width=self.width if args[1] else 0,\
focus_opacity=.4 if args[1] else 0, d=.25)
anim.start(self)
canvas.after:
Color:
rgba: 1, 1, 1, 1 if self.focus_look_width > 10 else 0
BorderImage
border: 0, 0, 36, 0
source: 'data/theme/' + app.theme + '/images/focused_overlay.png'
pos: self.pos
size: root.focus_look_width, dp(1)
Color:
rgba: 1, 1, 1, root.focus_opacity
Rectangle
source: 'data/theme/' + app.theme + '/images/glow.png'
size: self.size
pos: self.pos
''')
class FocusLookBehavior(FocusLook, FocusTrigger):
pass
class FocusArrowKeys(FocusBehavior):
'''This defines a class that allows the focusable item to react when a arrow up/down
key is pressed. This class can be used with widgets that have a
`value` property.
'''
touched = BooleanProperty(False)
def on_touch_down(self, touch):
if self.collide_point(*touch.pos):
self.touched = True
super(FocusArrowKeys, self).on_touch_down(touch)
def on_touch_up(self, touch):
super(FocusArrowKeys, self).on_touch_up(touch)
Clock.schedule_once(self._untouch, .25)
def keyboard_on_key_down(self, window, keycode, text, modifiers):
'''Trigger action when `space` or `enter` is pressed.
'''
# print(keycode, text, modifiers)
if keycode[1] in ('up', 'down', 'left', 'right'):
value = (self.step or 1) * (-1 if keycode[1] in ('down', 'left') else 1)
Clock.unschedule(self._untouch)
self.touched = True
self.value = max(self.min, min(self.max, (self.value+value)))
Clock.schedule_once(self._untouch, .25)
return True
return super(FocusArrowKeys, self).keyboard_on_key_down(
window, keycode, text, modifiers)
def _untouch(self, dt):
self.touched = False
class FocusArrowBehavior(FocusLook, FocusArrowKeys):
pass
class FocusSwitch(FocusBehavior, Factory.Switch):
touched = BooleanProperty(False)
def on_focused(self, instance, value):
anim = Animation(\
focus_look_width=self.width if value else 0,\
focus_opacity=.4 if value else 0, d=.25)
anim.start(self)
def on_touch_down(self, touch):
if self.collide_point(*touch.pos):
self.touched = True
return super().on_touch_down(touch)
def on_touch_up(self, touch):
if self.collide_point(*touch.pos):
Clock.schedule_once(self._untouch)
return super().on_touch_up(touch)
def keyboard_on_key_down(self, window, keycode, text, modifiers):
'''Trigger action when `space` or `enter` is pressed.
'''
# print(keycode, text, modifiers)
if keycode[1] in ('enter', 'spacebar'):
self.touched = True
self.active = not self.active
return True
return super(FocusSwitch, self).keyboard_on_key_down(
window, keycode, text, modifiers)
def _untouch(self, dt):
self.touched = False
def keyboard_on_key_up(self, *args):
Clock.schedule_once(self._untouch)
return super(FocusSwitch, self).keyboard_on_key_up(
*args)
class FocusSpinner(FocusLook, FocusBehavior, Factory.Spinner):
touched = BooleanProperty(False)
def on_text(self, instance, value):
if not self.disabled or not self.parent:
self.focus = is_desktop
def keyboard_on_key_down(self, window, keycode, text, modifiers):
'''Trigger action when `space` or `enter` is pressed.
'''
# print(keycode, text, modifiers)
if keycode[1] in ('enter', 'spacebar'):
self.trigger_action()
self._dropdown.children[0].children[-1].focus = is_desktop
return super(FocusSpinner, self).keyboard_on_key_down(
window, keycode, text, modifiers)
class FocusSpinnerOption(FocusLook, FocusBehavior):
def on_focused(self, instance, focus):
scroll = self.parent.parent
scroll.scroll_to(instance, padding=instance.height)
def on_touch_down(self, touch):
if self.collide_point(*touch.pos):
if self.parent and self.parent.parent and self.parent.parent.attach_to:
at = self.parent.parent.attach_to
if at.text == self.text:
at.text = ''
at.touched = True
Clock.unschedule(self._untouch)
super(FocusSpinnerOption, self).on_touch_down(touch)
def keyboard_on_key_down(self, window, keycode, text, modifiers):
'''Trigger action when `space` or `enter` is pressed.
'''
# print(keycode, text, modifiers)
if keycode[1] in ('enter', 'spacebar'):
if self.parent and self.parent.parent and self.parent.parent.attach_to:
self.parent.parent.attach_to.touched = True
Clock.unschedule(self._untouch)
self.trigger_action()
return True
if keycode[1] in ('up', 'down'):
children = self.parent.children
idx = children.index(self)
try:
lenc = len(children)
children[idx + (-1 if keycode[1] == 'down' else 1)].focus = is_desktop
except IndexError:
children[0].focus = is_desktop
return True
return super(FocusSpinnerOption, self).keyboard_on_key_down(
window, keycode, text, modifiers)
def _untouch(self, dt):
self.parent.parent.attach_to.touched = Fakse
class DownActiveBehavior(object):
def on_press(self):
path_list = self.source.split('/')
filename = path_list[-1]
path = '/'.join(path_list[:-1])
file, ext = filename.split('.')
if '_active' not in file and '_down' not in file:
filename = file + '_down.' + ext
self.source = '/'.join((path, filename))
Factory.register('DownActiveBehavior', DownActiveBehavior)
Factory.register('FocusLook', FocusLook)
Factory.register('FocusSpinnerOption', FocusSpinnerOption)
Factory.register('FocusArrowBehavior', FocusArrowBehavior)
Factory.register('FocusArrowKeys', FocusArrowKeys)
Factory.register('FocusLookBehavior', FocusLookBehavior)
Factory.register('HoverBehavior', HoverBehavior)
Factory.register('HoverLookBehavior', HoverLookBehavior)
Factory.register('TouchRippleBehavior', TouchRippleBehavior)
from kivy.clock import Clock
from kivy.uix.boxlayout import BoxLayout
from kivy.factory import Factory
from kivy.properties import ObjectProperty
from kivy.lang import Builder
class CalendarButton(Factory.ToggleButtonBehavior, Factory.FocusLookBehavior, Factory.Label):
datepicker = ObjectProperty(None)
Builder.load_string('''
<CalendarButton>
size_hint: 1, 1
disabled: True if self.text == '' else False
group: calendar
current_day_color: 1, 1, 1, 1
canvas.before:
Color:
rgba: .5, .9, .5, .5 if self.state == 'down' else 0
Rectangle:
size: self.size
pos: self.pos
canvas.after:
Color:
rgba: root.current_day_color or (1, 1, 1, 1)
Rectangle:
size: self.size
pos: self.pos
on_release: root.datepicker.day = self.text
on_touched: self.parent.parent.parent.touched = args[1]
on_parent:
root.current_day_color = 1, 1, 1, .2 if (args[1] and str(datetime.datetime.now().day) == self.text and args[1].month == datetime.datetime.now().month and str(args[1].year) == str(datetime.datetime.now().year)) else 0
''')
Month = Builder.load_string('''
<Month@Screen+Label>
text: self.name
''')
class DatePicker(Factory.FocusLookBehavior, BoxLayout):
Builder.load_string('''
<DateNTimeSpinnerOption@FocusableSpinnerOption>
<DatePicker>
orientation: 'vertical'
month: spinner_month.text
day: '12'
year: 2022
date: '{}-{}-{}'.format(self.day, self.month, self.year)
isodate: '{}-{:02d}-{:02d}'.format(int(self.year), (spinner_month.values.index(self.month)+1) if self.month else 0, int(self.day))
BoxLayout
size_hint_y: None
padding: dp(9), 0
spacing: dp(9)
height: dp(45 if not app.horiz_mode else 36)
FocusSpinner
id: spinner_month
option_cls: Factory.DateNTimeSpinnerOption
values: ('JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC')
text: self.values[datetime.datetime.now().month -1]
on_touched: root.touched = args[1]
on_text:
if args[1]:\
grid.month = self.values.index(args[1])+1;\
parent = grid.parent;\
grid.parent = None;\
grid.parent = parent
FocusSpinner
id: spinner_year
option_cls: Factory.DateNTimeSpinnerOption
text: str(datetime.datetime.now().year)
values: map(str, range(2006,datetime.datetime.now().year+1))
# on_focus: scroll.scroll_to(self)
on_touched: root.touched = args[1]
on_text:
if args[1]:\
root.year = self.text;\
grid.year = self.text;\
parent = grid.parent;\
grid.parent = None;\
grid.parent = parent
BoxLayout
orientation: 'vertical'
BoxLayout
canvas.before:
Color:
rgba: 1, 1, 1, 1
Rectangle:
source: 'data/theme/' + app.theme + '/images/glow.png'
size: self.width, dp(4)
pos: self.pos
size_hint_y: None
height: dp(32)
Label
text: 'M'
Label
text: 'T'
Label
text: 'W'
Label
text: 'T'
Label
text: 'F'
Label
text: 'S'
Label
text: 'S'
GridLayout
id: grid
cols: 7
rows: 6
spacing: dp(1)
year: datetime.datetime.now().year
month: datetime.datetime.now().month
on_parent:
month = spinner_month.values.index(root.month)
# print(month +1, root.year)
if args[1] and root.year:\
cal = calendar.Calendar();\
self.clear_widgets();\
[self.add_widget(Factory.CalendarButton(text=str(x), datepicker=root) if x!=0 else Factory.Widget())\
for x in cal.itermonthdays(int(root.year), (month+1)) ]
''')
def on_touch_up(self, touch):
super().on_touch_up(touch)
self.touched = True
Clock.unschedule(self._untouch)
def set_date(self, response):
import datetime
date_time = datetime.datetime.fromisoformat(str(response))
sm = self.ids.spinner_month
sm.text = sm.values[int(date_time.month) - 1]
self.ids.spinner_year.text = str(date_time.year)
day = str(date_time.day)
try:
for child in self.ids.grid.children:
if child.state == 'down':
child.state = 'normal'
if child.text == day:
child.state = 'down'
except AttributeError:
pass
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment