first commit

This commit is contained in:
Yura 2024-09-15 15:12:16 +03:00
commit 417e54da96
5696 changed files with 900003 additions and 0 deletions

View file

@ -0,0 +1,124 @@
from random import sample, randint
from string import ascii_lowercase
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.boxlayout import BoxLayout
kv = """
<Row@RecycleKVIDsDataViewBehavior+BoxLayout>:
canvas.before:
Color:
rgba: 0.5, 0.5, 0.5, 1
Rectangle:
size: self.size
pos: self.pos
value: ''
Label:
id: name
Label:
text: root.value
<Test>:
canvas:
Color:
rgba: 0.3, 0.3, 0.3, 1
Rectangle:
size: self.size
pos: self.pos
rv: rv
orientation: 'vertical'
GridLayout:
cols: 3
rows: 2
size_hint_y: None
height: dp(108)
padding: dp(8)
spacing: dp(16)
Button:
text: 'Populate list'
on_press: root.populate()
Button:
text: 'Sort list'
on_press: root.sort()
Button:
text: 'Clear list'
on_press: root.clear()
BoxLayout:
spacing: dp(8)
Button:
text: 'Insert new item'
on_press: root.insert(new_item_input.text)
TextInput:
id: new_item_input
size_hint_x: 0.6
hint_text: 'value'
padding: dp(10), dp(10), 0, 0
BoxLayout:
spacing: dp(8)
Button:
text: 'Update first item'
on_press: root.update(update_item_input.text)
TextInput:
id: update_item_input
size_hint_x: 0.6
hint_text: 'new value'
padding: dp(10), dp(10), 0, 0
Button:
text: 'Remove first item'
on_press: root.remove()
RecycleView:
id: rv
scroll_type: ['bars', 'content']
scroll_wheel_distance: dp(114)
bar_width: dp(10)
viewclass: 'Row'
RecycleBoxLayout:
default_size: None, dp(56)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
spacing: dp(2)
"""
Builder.load_string(kv)
class Test(BoxLayout):
def populate(self):
self.rv.data = [
{'name.text': ''.join(sample(ascii_lowercase, 6)),
'value': str(randint(0, 2000))}
for x in range(50)]
def sort(self):
self.rv.data = sorted(self.rv.data, key=lambda x: x['name.text'])
def clear(self):
self.rv.data = []
def insert(self, value):
self.rv.data.insert(0, {
'name.text': value or 'default value', 'value': 'unknown'})
def update(self, value):
if self.rv.data:
self.rv.data[0]['name.text'] = value or 'default new value'
self.rv.refresh_from_data()
def remove(self):
if self.rv.data:
self.rv.data.pop(0)
class TestApp(App):
def build(self):
return Test()
if __name__ == '__main__':
TestApp().run()

View file

@ -0,0 +1,143 @@
"""
A constantly appending log, using recycleview.
- use variable size widgets using the key_size property to cache texture_size
- keeps current position in scroll when new data is happened, unless the view
is at the very bottom, in which case it follows the log
- works well with mouse scrolling, but less nicely when using swipes,
improvements welcome.
"""
from random import sample
from string import printable
from time import asctime
from kivy.app import App
from kivy.uix.recycleview import RecycleView
from kivy.lang import Builder
from kivy.properties import NumericProperty, ListProperty
from kivy.clock import Clock
KV = """
#:import rgba kivy.utils.rgba
<LogLabel@RelativeLayout>:
# using a boxlayout here allows us to have better control of the text
# position
text: ''
index: None
Label:
y: 0
x: 5
size_hint: None, None
size: self.texture_size
padding: dp(5), dp(5)
color: rgba("#3f3e36")
text: root.text
on_texture_size: app.update_size(root.index, self.texture_size)
canvas.before:
Color:
rgba: rgba("#dbeeff")
RoundedRectangle:
pos: self.pos
size: self.size
radius: dp(5), dp(5)
BoxLayout:
orientation: 'vertical'
spacing: dp(2)
# a label to help understand what's happening with the scrolling
Label:
size_hint_y: None
height: self.texture_size[1]
text:
'''height: {height}
scrollable_distance: {scrollable_distance}
distance_to_top: {distance_to_top}
scroll_y: {scroll_y}
'''.format(
height=rv.height,
scrollable_distance=rv.scrollable_distance,
distance_to_top=rv.distance_to_top,
scroll_y=rv.scroll_y,
)
canvas.before:
Color:
rgba: rgba("#77b4ff")
RoundedRectangle:
pos: self.pos
size: self.size
radius: dp(5), dp(5)
FixedRecycleView:
id: rv
data: app.data
viewclass: 'LogLabel'
scrollable_distance: box.height - self.height
RecycleBoxLayout:
id: box
orientation: 'vertical'
size_hint_y: None
height: self.minimum_height
default_size: 0, 48
default_size_hint: 1, None
spacing: dp(1)
key_size: 'cached_size'
"""
class FixedRecycleView(RecycleView):
distance_to_top = NumericProperty()
scrollable_distance = NumericProperty()
def on_scrollable_distance(self, *args):
"""This method maintains the position in scroll, by using the saved
distance_to_top property to adjust the scroll_y property. Only if we
are currently scrolled back.
"""
if self.scroll_y > 0:
self.scroll_y = (
(self.scrollable_distance - self.distance_to_top)
/ self.scrollable_distance
)
def on_scroll_y(self, *args):
"""Save the distance_to_top everytime we scroll.
"""
self.distance_to_top = (1 - self.scroll_y) * self.scrollable_distance
class Application(App):
data = ListProperty()
def build(self):
Clock.schedule_interval(self.add_log, .1)
return Builder.load_string(KV)
def add_log(self, dt):
"""Produce random text to append in the log, with the date, we don't
want to forget when we babbled incoherently.
"""
self.data.append({
'index': len(self.data),
'text': f"[{asctime()}]: {''.join(sample(printable, 50))}",
'cached_size': (0, 0)
})
def update_size(self, index, size):
"""Maintain the size data for a log entry, so recycleview can adjust
the size computation.
As a log entry needs to be displayed to compute its size, it's by
default considered to be (0, 0) which is a good enough approximation
for such a small widget, but you might want do give a better default
value if that doesn't fit your needs.
"""
self.data[index]['cached_size'] = size
if __name__ == '__main__':
Application().run()

View file

@ -0,0 +1,149 @@
'''
A form generator, using random data, but can be data driven (json or whatever)
Shows that you can use the key_viewclass attribute of RecycleView to select a
different Widget for each item.
'''
from random import choice, choices
from string import ascii_lowercase
from kivy.app import App
from kivy.lang import Builder
from kivy import properties as P
KV = r'''
<RVTextInput,RVCheckBox,RVSpinner>:
size_hint_y: None
height: self.minimum_height
index: None
title: ''
<RVTextInput@BoxLayout>:
value: ''
Label:
text: root.title
size_hint_y: None
height: self.texture_size[1]
TextInput:
text: root.value
on_text: app.handle_update(self.text, root.index)
size_hint_y: None
height: dp(40)
multiline: False
<RVCheckBox@BoxLayout>:
value: False
Label:
text: root.title
size_hint_y: None
height: self.texture_size[1]
CheckBox:
active: root.value
on_active: app.handle_update(self.active, root.index)
size_hint_y: None
height: dp(40)
<RVSpinner@BoxLayout>:
value: ''
values: []
Label:
text: root.title
size_hint_y: None
height: self.texture_size[1]
Spinner:
text: root.value
values: root.values
size_hint_y: None
height: dp(40)
on_text: app.handle_update(self.text, root.index)
FloatLayout:
RecycleView:
id: rv
data: app.data
key_viewclass: 'widget'
size_hint_x: 1
RecycleBoxLayout:
orientation: 'vertical'
size_hint_y: None
height: self.minimum_height
default_size_hint: 1, None
'''
class Application(App):
'''A form manager demonstrating the power of RecycleView's key_viewclass
property.
'''
data = P.ListProperty()
def build(self):
root = Builder.load_string(KV)
rv = root.ids.rv
self.data = [
self.create_random_input(rv, index)
for index in range(20)
]
return root
def handle_update(self, value, index):
if None not in (index, value):
self.data[index]['value'] = value
def create_random_input(self, rv, index):
return choice((
self.create_textinput,
self.create_checkbox,
self.create_spinner
))(rv, index)
def create_spinner(self, rv, index):
"""
create a dict of data for a spinner
"""
return {
'index': index,
'widget': 'RVSpinner',
'value': '',
'values': [
letter * 5
for letter in ascii_lowercase[:5]
],
'ready': True,
}
def create_checkbox(self, rv, index):
"""
create a dict of data for a checkbox
"""
return {
'index': index,
'widget': 'RVCheckBox',
'value': choice((True, False)),
'title': ''.join(choices(ascii_lowercase, k=10)),
'ready': True,
}
def create_textinput(self, rv, index):
"""
create a dict of data for a textinput
"""
return {
'index': index,
'widget': 'RVTextInput',
'value': ''.join(choices(ascii_lowercase, k=10)),
'title': ''.join(choices(ascii_lowercase, k=10)),
'ready': True,
}
if __name__ == "__main__":
Application().run()

View file

@ -0,0 +1,196 @@
from kivy.app import App
from kivy.lang import Builder
from kivy.clock import Clock
from kivy.properties import ListProperty
from kivy.animation import Animation
from kivy.metrics import dp
KV = '''
#:import RGBA kivy.utils.rgba
<ImageButton@ButtonBehavior+Image>:
size_hint: None, None
size: self.texture_size
canvas.before:
PushMatrix
Scale:
origin: self.center
x: .75 if self.state == 'down' else 1
y: .75 if self.state == 'down' else 1
canvas.after:
PopMatrix
BoxLayout:
orientation: 'vertical'
padding: dp(5), dp(5)
RecycleView:
id: rv
data: app.messages
viewclass: 'Message'
do_scroll_x: False
RecycleBoxLayout:
id: box
orientation: 'vertical'
size_hint_y: None
size: self.minimum_size
default_size_hint: 1, None
# magic value for the default height of the message
default_size: 0, 38
key_size: '_size'
FloatLayout:
size_hint_y: None
height: 0
Button:
size_hint_y: None
height: self.texture_size[1]
opacity: 0 if not self.height else 1
text:
(
'go to last message'
if rv.height < box.height and rv.scroll_y > 0 else
''
)
pos_hint: {'pos': (0, 0)}
on_release: app.scroll_bottom()
BoxLayout:
size_hint: 1, None
size: self.minimum_size
TextInput:
id: ti
size_hint: 1, None
height: min(max(self.line_height, self.minimum_height), 150)
multiline: False
on_text_validate:
app.send_message(self)
ImageButton:
source: 'data/logo/kivy-icon-48.png'
on_release:
app.send_message(ti)
<Message@FloatLayout>:
message_id: -1
bg_color: '#223344'
side: 'left'
text: ''
size_hint_y: None
_size: 0, 0
size: self._size
text_size: None, None
opacity: min(1, self._size[0])
Label:
text: root.text
padding: 10, 10
size_hint: None, 1
size: self.texture_size
text_size: root.text_size
on_texture_size:
app.update_message_size(
root.message_id,
self.texture_size,
root.width,
)
pos_hint:
(
{'x': 0, 'center_y': .5}
if root.side == 'left' else
{'right': 1, 'center_y': .5}
)
canvas.before:
Color:
rgba: RGBA(root.bg_color)
RoundedRectangle:
size: self.texture_size
radius: dp(5), dp(5), dp(5), dp(5)
pos: self.pos
canvas.after:
Color:
Line:
rounded_rectangle: self.pos + self.texture_size + [dp(5)]
width: 1.01
'''
class MessengerApp(App):
messages = ListProperty()
def build(self):
return Builder.load_string(KV)
def add_message(self, text, side, color):
# create a message for the recycleview
self.messages.append({
'message_id': len(self.messages),
'text': text,
'side': side,
'bg_color': color,
'text_size': [None, None],
})
def update_message_size(self, message_id, texture_size, max_width):
# when the label is updated, we want to make sure the displayed size is
# proper
if max_width == 0:
return
one_line = dp(50) # a bit of hack, YMMV
# if the texture is too big, limit its size
if texture_size[0] >= max_width * 2 / 3:
self.messages[message_id] = {
**self.messages[message_id],
'text_size': (max_width * 2 / 3, None),
}
# if it was limited, but is now too small to be limited, raise the limit
elif texture_size[0] < max_width * 2 / 3 and \
texture_size[1] > one_line:
self.messages[message_id] = {
**self.messages[message_id],
'text_size': (max_width * 2 / 3, None),
'_size': texture_size,
}
# just set the size
else:
self.messages[message_id] = {
**self.messages[message_id],
'_size': texture_size,
}
@staticmethod
def focus_textinput(textinput):
textinput.focus = True
def send_message(self, textinput):
text = textinput.text
textinput.text = ''
self.add_message(text, 'right', '#223344')
self.focus_textinput(textinput)
Clock.schedule_once(lambda *args: self.answer(text), 1)
self.scroll_bottom()
def answer(self, text, *args):
self.add_message('do you really think so?', 'left', '#332211')
def scroll_bottom(self):
rv = self.root.ids.rv
box = self.root.ids.box
if rv.height < box.height:
Animation.cancel_all(rv, 'scroll_y')
Animation(scroll_y=0, t='out_quad', d=.5).start(rv)
if __name__ == '__main__':
MessengerApp().run()

View file

@ -0,0 +1,95 @@
"""Detecting and acting upon "Pull down actions" in a RecycleView
- When using overscroll or being at the to, a "pull down to refresh" message
appears
- if the user pulls down far enough, then a refresh is triggered, which adds
new elements at the top of the list.
"""
from threading import Thread
from time import sleep
from datetime import datetime
from kivy.app import App
from kivy.lang import Builder
from kivy.properties import ListProperty, BooleanProperty
from kivy.metrics import dp
from kivy.clock import mainthread
KV = r'''
FloatLayout:
Label:
opacity: 1 if app.refreshing or rv.scroll_y > 1 else 0
size_hint_y: None
pos_hint: {'top': 1}
text: 'Refreshing…' if app.refreshing else 'Pull down to refresh'
RecycleView:
id: rv
data: app.data
viewclass: 'Row'
do_scroll_y: True
do_scroll_x: False
on_scroll_y: app.check_pull_refresh(self, grid)
RecycleGridLayout:
id: grid
cols: 1
size_hint_y: None
height: self.minimum_height
default_size: 0, 36
default_size_hint: 1, None
<Row@Label>:
_id: 0
text: ''
canvas:
Line:
rectangle: self.pos + self.size
width: 0.6
'''
class Application(App):
data = ListProperty([])
refreshing = BooleanProperty()
def build(self):
self.refresh_data()
return Builder.load_string(KV)
def check_pull_refresh(self, view, grid):
"""Check the amount of overscroll to decide if we want to trigger the
refresh or not.
"""
max_pixel = dp(200)
to_relative = max_pixel / (grid.height - view.height)
if view.scroll_y <= 1.0 + to_relative or self.refreshing:
return
self.refresh_data()
def refresh_data(self):
# using a Thread to do a potentially long operation without blocking
# the UI.
self.refreshing = True
Thread(target=self._refresh_data).start()
def _refresh_data(self):
sleep(2)
update_time = datetime.now().strftime("%H:%M:%S")
self.prepend_data([
{'_id': i, 'text': '[{}] hello {}'.format(update_time, i)}
for i in range(len(self.data) + 10, len(self.data), -1)
])
@mainthread
def prepend_data(self, data):
self.data = data + self.data
self.refreshing = False
if __name__ == "__main__":
Application().run()

View file

@ -0,0 +1,109 @@
'''How to use Animation with RecycleView items?
In case you really want to use the Animation class with RecycleView, you'll
likely encounter an issue, as widgets are moved around, they are used to
represent different items, so an animation on a specific item is going to
affect others, and this will lead to really confusing results.
This example works around that by creating a "proxy" widget for the animation,
and, by putting it in the data, allowing the displayed widget to mimic the
animation. As the item always refers to its proxy, whichever widget is used to
display the item will keep in sync with the animation.
'''
from copy import copy
from kivy.app import App
from kivy.clock import triggered
from kivy.lang import Builder
from kivy.uix.widget import Widget
from kivy.animation import Animation
from kivy.uix.button import Button
from kivy.properties import (
ObjectProperty, ListProperty
)
KV = '''
<Item>:
index: None
animation_proxy: None
on_release: app.animate_item(self.index)
RecycleView:
data: app.data
viewclass: 'Item'
RecycleBoxLayout:
orientation: 'vertical'
size_hint: 1, None
height: self.minimum_height
default_size_hint: 1, None
default_size: 0, dp(40)
'''
class Item(Button):
animation_proxy = ObjectProperty(allownone=True)
_animation_proxy = None
def update_opacity(self, proxy, opacity):
# sync one animated property to the value in the proxy
self.opacity = opacity
def on_animation_proxy(self, *args):
"""When we create an animation proxy for an item, we need to bind to
the animated property to update our own.
"""
if self._animation_proxy:
self._animation_proxy.unbind(opacity=self.update_opacity)
self._animation_proxy = self.animation_proxy
if self.animation_proxy:
# when we are assigned an animation_proxy, sync our properties to
# the animated version.
self.opacity = self.animation_proxy.opacity
self.animation_proxy.bind(opacity=self.update_opacity)
else:
# if we lose our animation proxy, we need to reset the animated
# property to their default values.
self.opacity = 1
class Application(App):
data = ListProperty()
def build(self):
self.data = [
{'index': i, 'text': 'hello {}'.format(i), 'animation_proxy': None}
for i in range(1000)
]
return Builder.load_string(KV)
# the triggered decorator allows delaying the animation until after the
# blue effect on the button is removed, to avoid a flash as widgets gets
# reordered when that happens
@triggered(timeout=0.05)
def animate_item(self, index):
# the animation we actually want to do on the item, note that any
# property animated here needs to be synchronized from the proxy to the
# animated widget (in on_animation_proxy and using methods for each
# animation)
proxy = Widget(opacity=1)
item = copy(self.data[index])
animation = (
Animation(opacity=0, d=.1, t='out_quad')
+ Animation(opacity=1, d=5, t='out_quad')
)
animation.bind(on_complete=lambda *x: self.reset_animation(item))
item['animation_proxy'] = proxy
self.data[index] = item
animation.start(proxy)
def reset_animation(self, item):
# animation is complete, widget should be garbage collected
item['animation_proxy'] = None
if __name__ == "__main__":
Application().run()