Ajout du GUI
This commit is contained in:
624
kivy/uix/recycleview/__init__.py
Normal file
624
kivy/uix/recycleview/__init__.py
Normal file
@@ -0,0 +1,624 @@
|
||||
"""
|
||||
RecycleView
|
||||
===========
|
||||
|
||||
.. versionadded:: 1.10.0
|
||||
|
||||
The RecycleView provides a flexible model for viewing selected sections of
|
||||
large data sets. It aims to prevent the performance degradation that can occur
|
||||
when generating large numbers of widgets in order to display many data items.
|
||||
|
||||
.. warning::
|
||||
|
||||
Because :class:`RecycleView` reuses widgets, any state change to a single
|
||||
widget will stay with that widget as it's reused, even if the
|
||||
:attr:`~RecycleView.data` assigned to it by the :class:`RecycleView`
|
||||
changes. Unless the complete state is tracked in :attr:`~RecycleView.data`
|
||||
(see below).
|
||||
|
||||
The view is generatad by processing the :attr:`~RecycleView.data`, essentially
|
||||
a list of dicts, and uses these dicts to generate instances of the
|
||||
:attr:`~RecycleView.viewclass` as required. Its design is based on the
|
||||
MVC (`Model-view-controller
|
||||
<https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller>`_)
|
||||
pattern.
|
||||
|
||||
* Model: The model is formed by :attr:`~RecycleView.data` you pass in via a
|
||||
list of dicts.
|
||||
* View: The View is split across layout and views and implemented using
|
||||
adapters.
|
||||
* Controller: The controller determines the logical interaction and is
|
||||
implemented by :class:`RecycleViewBehavior`.
|
||||
|
||||
These are abstract classes and cannot be used directly. The default concrete
|
||||
implementations are the
|
||||
:class:`~kivy.uix.recycleview.datamodel.RecycleDataModel` for the model, the
|
||||
:class:`~kivy.uix.recyclelayout.RecycleLayout` for the view, and the
|
||||
:class:`RecycleView` for the controller.
|
||||
|
||||
When a RecycleView is instantiated, it automatically creates the views and data
|
||||
classes. However, one must manually create the layout classes and add them to
|
||||
the RecycleView.
|
||||
|
||||
A layout manager is automatically created as a
|
||||
:attr:`~RecycleViewBehavior.layout_manager` when added as the child of the
|
||||
RecycleView. Similarly when removed. A requirement is that the layout manager
|
||||
must be contained as a child somewhere within the RecycleView's widget tree so
|
||||
the view port can be found.
|
||||
|
||||
A minimal example might look something like this::
|
||||
|
||||
from kivy.app import App
|
||||
from kivy.lang import Builder
|
||||
from kivy.uix.recycleview import RecycleView
|
||||
|
||||
|
||||
Builder.load_string('''
|
||||
<RV>:
|
||||
viewclass: 'Label'
|
||||
RecycleBoxLayout:
|
||||
default_size: None, dp(56)
|
||||
default_size_hint: 1, None
|
||||
size_hint_y: None
|
||||
height: self.minimum_height
|
||||
orientation: 'vertical'
|
||||
''')
|
||||
|
||||
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()
|
||||
|
||||
In order to support selection in the view, you can add the required behaviours
|
||||
as follows::
|
||||
|
||||
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
|
||||
|
||||
Builder.load_string('''
|
||||
<SelectableLabel>:
|
||||
# Draw a background to indicate selection
|
||||
canvas.before:
|
||||
Color:
|
||||
rgba: (.0, 0.9, .1, .3) if self.selected else (0, 0, 0, 1)
|
||||
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)
|
||||
|
||||
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 on_touch_down(self, touch):
|
||||
''' Add selection on touch down '''
|
||||
if super(SelectableLabel, self).on_touch_down(touch):
|
||||
return True
|
||||
if self.collide_point(*touch.pos) and self.selectable:
|
||||
return self.parent.select_with_touch(self.index, touch)
|
||||
|
||||
def apply_selection(self, rv, index, is_selected):
|
||||
''' Respond to the selection of items in the view. '''
|
||||
self.selected = is_selected
|
||||
if is_selected:
|
||||
print("selection changed to {0}".format(rv.data[index]))
|
||||
else:
|
||||
print("selection removed for {0}".format(rv.data[index]))
|
||||
|
||||
|
||||
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()
|
||||
|
||||
|
||||
|
||||
Please see the `examples/widgets/recycleview/basic_data.py` file for a more
|
||||
complete example.
|
||||
|
||||
Viewclass State
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
Because the viewclass widgets are reused or instantiated as needed by the
|
||||
:class:`RecycleView`, the order and content of the widgets are mutable. So any
|
||||
state change to a single widget will stay with that widget, even when the data
|
||||
assigned to it from the :attr:`~RecycleView.data` dict changes, unless
|
||||
:attr:`~RecycleView.data` tracks those changes or they are manually refreshed
|
||||
when re-used.
|
||||
|
||||
There are two methods for managing state changes in viewclass widgets:
|
||||
|
||||
1. Store state in the RecycleView.data Model
|
||||
2. Generate state changes on-the-fly by catching :attr:`~RecycleView.data`
|
||||
updates and manually refreshing.
|
||||
|
||||
An example::
|
||||
|
||||
from kivy.app import App
|
||||
from kivy.lang import Builder
|
||||
from kivy.uix.boxlayout import BoxLayout
|
||||
from kivy.uix.recycleview import RecycleView
|
||||
from kivy.uix.recycleview.views import RecycleDataViewBehavior
|
||||
from kivy.properties import BooleanProperty, StringProperty
|
||||
|
||||
Builder.load_string('''
|
||||
<StatefulLabel>:
|
||||
active: stored_state.active
|
||||
CheckBox:
|
||||
id: stored_state
|
||||
active: root.active
|
||||
on_release: root.store_checkbox_state()
|
||||
Label:
|
||||
text: root.text
|
||||
Label:
|
||||
id: generate_state
|
||||
text: root.generated_state_text
|
||||
|
||||
<RV>:
|
||||
viewclass: 'StatefulLabel'
|
||||
RecycleBoxLayout:
|
||||
size_hint_y: None
|
||||
height: self.minimum_height
|
||||
orientation: 'vertical'
|
||||
''')
|
||||
|
||||
class StatefulLabel(RecycleDataViewBehavior, BoxLayout):
|
||||
text = StringProperty()
|
||||
generated_state_text = StringProperty()
|
||||
active = BooleanProperty()
|
||||
index = 0
|
||||
|
||||
'''
|
||||
To change a viewclass' state as the data assigned to it changes,
|
||||
overload the refresh_view_attrs function (inherited from
|
||||
RecycleDataViewBehavior)
|
||||
'''
|
||||
def refresh_view_attrs(self, rv, index, data):
|
||||
self.index = index
|
||||
if data['text'] == '0':
|
||||
self.generated_state_text = "is zero"
|
||||
elif int(data['text']) % 2 == 1:
|
||||
self.generated_state_text = "is odd"
|
||||
else:
|
||||
self.generated_state_text = "is even"
|
||||
super(StatefulLabel, self).refresh_view_attrs(rv, index, data)
|
||||
|
||||
'''
|
||||
To keep state changes in the viewclass with associated data,
|
||||
they can be explicitly stored in the RecycleView's data object
|
||||
'''
|
||||
def store_checkbox_state(self):
|
||||
rv = App.get_running_app().rv
|
||||
rv.data[self.index]['active'] = self.active
|
||||
|
||||
class RV(RecycleView, App):
|
||||
def __init__(self, **kwargs):
|
||||
super(RV, self).__init__(**kwargs)
|
||||
self.data = [{'text': str(x), 'active': False} for x in range(10)]
|
||||
App.get_running_app().rv = self
|
||||
|
||||
def build(self):
|
||||
return self
|
||||
|
||||
if __name__ == '__main__':
|
||||
RV().run()
|
||||
|
||||
TODO:
|
||||
- Method to clear cached class instances.
|
||||
- Test when views cannot be found (e.g. viewclass is None).
|
||||
- Fix selection goto.
|
||||
|
||||
.. warning::
|
||||
When views are re-used they may not trigger if the data remains the same.
|
||||
"""
|
||||
|
||||
__all__ = ('RecycleViewBehavior', 'RecycleView')
|
||||
|
||||
from copy import deepcopy
|
||||
|
||||
from kivy.uix.scrollview import ScrollView
|
||||
from kivy.properties import AliasProperty
|
||||
from kivy.clock import Clock
|
||||
|
||||
from kivy.uix.recycleview.layout import RecycleLayoutManagerBehavior, \
|
||||
LayoutChangeException
|
||||
from kivy.uix.recycleview.views import RecycleDataAdapter
|
||||
from kivy.uix.recycleview.datamodel import RecycleDataModelBehavior, \
|
||||
RecycleDataModel
|
||||
|
||||
|
||||
class RecycleViewBehavior(object):
|
||||
"""RecycleViewBehavior provides a behavioral model upon which the
|
||||
:class:`RecycleView` is built. Together, they offer an extensible and
|
||||
flexible way to produce views with limited windows over large data sets.
|
||||
|
||||
See the module documentation for more information.
|
||||
"""
|
||||
|
||||
# internals
|
||||
_view_adapter = None
|
||||
_data_model = None
|
||||
_layout_manager = None
|
||||
|
||||
_refresh_flags = {'data': [], 'layout': [], 'viewport': False}
|
||||
_refresh_trigger = None
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._refresh_trigger = Clock.create_trigger(self.refresh_views, -1)
|
||||
self._refresh_flags = deepcopy(self._refresh_flags)
|
||||
super(RecycleViewBehavior, self).__init__(**kwargs)
|
||||
|
||||
def get_viewport(self):
|
||||
pass
|
||||
|
||||
def save_viewport(self):
|
||||
pass
|
||||
|
||||
def restore_viewport(self):
|
||||
pass
|
||||
|
||||
def refresh_views(self, *largs):
|
||||
lm = self.layout_manager
|
||||
flags = self._refresh_flags
|
||||
if lm is None or self.view_adapter is None or self.data_model is None:
|
||||
return
|
||||
|
||||
data = self.data
|
||||
f = flags['data']
|
||||
if f:
|
||||
self.save_viewport()
|
||||
# lm.clear_layout()
|
||||
flags['data'] = []
|
||||
flags['layout'] = [{}]
|
||||
lm.compute_sizes_from_data(data, f)
|
||||
|
||||
while flags['layout']:
|
||||
# if `data` we were re-triggered so finish in the next call.
|
||||
# Otherwise go until fully laid out.
|
||||
self.save_viewport()
|
||||
if flags['data']:
|
||||
return
|
||||
flags['viewport'] = True
|
||||
f = flags['layout']
|
||||
flags['layout'] = []
|
||||
|
||||
try:
|
||||
lm.compute_layout(data, f)
|
||||
except LayoutChangeException:
|
||||
flags['layout'].append({})
|
||||
continue
|
||||
|
||||
if flags['data']: # in case that happened meanwhile
|
||||
return
|
||||
|
||||
# make sure if we were re-triggered in the loop that we won't be
|
||||
# called needlessly later.
|
||||
self._refresh_trigger.cancel()
|
||||
|
||||
self.restore_viewport()
|
||||
|
||||
if flags['viewport']:
|
||||
# TODO: make this also listen to LayoutChangeException
|
||||
flags['viewport'] = False
|
||||
viewport = self.get_viewport()
|
||||
indices = lm.compute_visible_views(data, viewport)
|
||||
lm.set_visible_views(indices, data, viewport)
|
||||
|
||||
def refresh_from_data(self, *largs, **kwargs):
|
||||
"""
|
||||
This should be called when data changes. Data changes typically
|
||||
indicate that everything should be recomputed since the source data
|
||||
changed.
|
||||
|
||||
This method is automatically bound to the
|
||||
:attr:`~RecycleDataModelBehavior.on_data_changed` method of the
|
||||
:class:`~RecycleDataModelBehavior` class and
|
||||
therefore responds to and accepts the keyword arguments of that event.
|
||||
|
||||
It can be called manually to trigger an update.
|
||||
"""
|
||||
self._refresh_flags['data'].append(kwargs)
|
||||
self._refresh_trigger()
|
||||
|
||||
def refresh_from_layout(self, *largs, **kwargs):
|
||||
"""
|
||||
This should be called when the layout changes or needs to change. It is
|
||||
typically called when a layout parameter has changed and therefore the
|
||||
layout needs to be recomputed.
|
||||
"""
|
||||
self._refresh_flags['layout'].append(kwargs)
|
||||
self._refresh_trigger()
|
||||
|
||||
def refresh_from_viewport(self, *largs):
|
||||
"""
|
||||
This should be called when the viewport changes and the displayed data
|
||||
must be updated. Neither the data nor the layout will be recomputed.
|
||||
"""
|
||||
self._refresh_flags['viewport'] = True
|
||||
self._refresh_trigger()
|
||||
|
||||
def _dispatch_prop_on_source(self, prop_name, *largs):
|
||||
# Dispatches the prop of this class when the
|
||||
# view_adapter/layout_manager property changes.
|
||||
getattr(self.__class__, prop_name).dispatch(self)
|
||||
|
||||
def _get_data_model(self):
|
||||
return self._data_model
|
||||
|
||||
def _set_data_model(self, value):
|
||||
data_model = self._data_model
|
||||
if value is data_model:
|
||||
return
|
||||
if data_model is not None:
|
||||
self._data_model = None
|
||||
data_model.detach_recycleview()
|
||||
|
||||
if value is None:
|
||||
return True
|
||||
|
||||
if not isinstance(value, RecycleDataModelBehavior):
|
||||
raise ValueError(
|
||||
'Expected object based on RecycleDataModelBehavior, got {}'.
|
||||
format(value.__class__))
|
||||
|
||||
self._data_model = value
|
||||
value.attach_recycleview(self)
|
||||
self.refresh_from_data()
|
||||
return True
|
||||
|
||||
data_model = AliasProperty(_get_data_model, _set_data_model)
|
||||
"""
|
||||
The Data model responsible for maintaining the data set.
|
||||
|
||||
data_model is an :class:`~kivy.properties.AliasProperty` that gets and sets
|
||||
the current data model.
|
||||
"""
|
||||
|
||||
def _get_view_adapter(self):
|
||||
return self._view_adapter
|
||||
|
||||
def _set_view_adapter(self, value):
|
||||
view_adapter = self._view_adapter
|
||||
if value is view_adapter:
|
||||
return
|
||||
if view_adapter is not None:
|
||||
self._view_adapter = None
|
||||
view_adapter.detach_recycleview()
|
||||
|
||||
if value is None:
|
||||
return True
|
||||
|
||||
if not isinstance(value, RecycleDataAdapter):
|
||||
raise ValueError(
|
||||
'Expected object based on RecycleAdapter, got {}'.
|
||||
format(value.__class__))
|
||||
|
||||
self._view_adapter = value
|
||||
value.attach_recycleview(self)
|
||||
self.refresh_from_layout()
|
||||
return True
|
||||
|
||||
view_adapter = AliasProperty(_get_view_adapter, _set_view_adapter)
|
||||
"""
|
||||
The adapter responsible for providing views that represent items in a data
|
||||
set.
|
||||
|
||||
view_adapter is an :class:`~kivy.properties.AliasProperty` that gets and
|
||||
sets the current view adapter.
|
||||
"""
|
||||
|
||||
def _get_layout_manager(self):
|
||||
return self._layout_manager
|
||||
|
||||
def _set_layout_manager(self, value):
|
||||
lm = self._layout_manager
|
||||
if value is lm:
|
||||
return
|
||||
|
||||
if lm is not None:
|
||||
self._layout_manager = None
|
||||
lm.detach_recycleview()
|
||||
|
||||
if value is None:
|
||||
return True
|
||||
|
||||
if not isinstance(value, RecycleLayoutManagerBehavior):
|
||||
raise ValueError(
|
||||
'Expected object based on RecycleLayoutManagerBehavior, '
|
||||
'got {}'.format(value.__class__))
|
||||
|
||||
self._layout_manager = value
|
||||
value.attach_recycleview(self)
|
||||
self.refresh_from_layout()
|
||||
return True
|
||||
|
||||
layout_manager = AliasProperty(_get_layout_manager, _set_layout_manager)
|
||||
"""
|
||||
The Layout manager responsible for positioning views within the
|
||||
:class:`RecycleView`.
|
||||
|
||||
layout_manager is an :class:`~kivy.properties.AliasProperty` that gets
|
||||
and sets the layout_manger.
|
||||
"""
|
||||
|
||||
|
||||
class RecycleView(RecycleViewBehavior, ScrollView):
|
||||
"""
|
||||
RecycleView is a flexible view for providing a limited window
|
||||
into a large data set.
|
||||
|
||||
See the module documentation for more information.
|
||||
"""
|
||||
def __init__(self, **kwargs):
|
||||
if self.data_model is None:
|
||||
kwargs.setdefault('data_model', RecycleDataModel())
|
||||
if self.view_adapter is None:
|
||||
kwargs.setdefault('view_adapter', RecycleDataAdapter())
|
||||
super(RecycleView, self).__init__(**kwargs)
|
||||
|
||||
fbind = self.fbind
|
||||
fbind('scroll_x', self.refresh_from_viewport)
|
||||
fbind('scroll_y', self.refresh_from_viewport)
|
||||
fbind('size', self.refresh_from_viewport)
|
||||
self.refresh_from_data()
|
||||
|
||||
def _convert_sv_to_lm(self, x, y):
|
||||
lm = self.layout_manager
|
||||
tree = [lm]
|
||||
parent = lm.parent
|
||||
while parent is not None and parent is not self:
|
||||
tree.append(parent)
|
||||
parent = parent.parent
|
||||
|
||||
if parent is not self:
|
||||
raise Exception(
|
||||
'The layout manager must be a sub child of the recycleview. '
|
||||
'Could not find {} in the parent tree of {}'.format(self, lm))
|
||||
|
||||
for widget in reversed(tree):
|
||||
x, y = widget.to_local(x, y)
|
||||
|
||||
return x, y
|
||||
|
||||
def get_viewport(self):
|
||||
lm = self.layout_manager
|
||||
lm_w, lm_h = lm.size
|
||||
w, h = self.size
|
||||
scroll_y = min(1, max(self.scroll_y, 0))
|
||||
scroll_x = min(1, max(self.scroll_x, 0))
|
||||
|
||||
if lm_h <= h:
|
||||
bottom = 0
|
||||
else:
|
||||
above = (lm_h - h) * scroll_y
|
||||
bottom = max(0, lm_h - above - h)
|
||||
|
||||
bottom = max(0, (lm_h - h) * scroll_y)
|
||||
left = max(0, (lm_w - w) * scroll_x)
|
||||
width = min(w, lm_w)
|
||||
height = min(h, lm_h)
|
||||
|
||||
# now convert the sv coordinates into the coordinates of the lm. In
|
||||
# case there's a relative layout type widget in the parent tree
|
||||
# between the sv and the lm.
|
||||
left, bottom = self._convert_sv_to_lm(left, bottom)
|
||||
return left, bottom, width, height
|
||||
|
||||
def save_viewport(self):
|
||||
pass
|
||||
|
||||
def restore_viewport(self):
|
||||
pass
|
||||
|
||||
def add_widget(self, widget, *args, **kwargs):
|
||||
super(RecycleView, self).add_widget(widget, *args, **kwargs)
|
||||
if (isinstance(widget, RecycleLayoutManagerBehavior) and
|
||||
not self.layout_manager):
|
||||
self.layout_manager = widget
|
||||
|
||||
def remove_widget(self, widget, *args, **kwargs):
|
||||
super(RecycleView, self).remove_widget(widget, *args, **kwargs)
|
||||
if self.layout_manager == widget:
|
||||
self.layout_manager = None
|
||||
|
||||
# or easier way to use
|
||||
def _get_data(self):
|
||||
d = self.data_model
|
||||
return d and d.data
|
||||
|
||||
def _set_data(self, value):
|
||||
d = self.data_model
|
||||
if d is not None:
|
||||
d.data = value
|
||||
|
||||
data = AliasProperty(_get_data, _set_data, bind=["data_model"])
|
||||
"""
|
||||
The data used by the current view adapter. This is a list of dicts whose
|
||||
keys map to the corresponding property names of the
|
||||
:attr:`~RecycleView.viewclass`.
|
||||
|
||||
data is an :class:`~kivy.properties.AliasProperty` that gets and sets the
|
||||
data used to generate the views.
|
||||
"""
|
||||
|
||||
def _get_viewclass(self):
|
||||
a = self.layout_manager
|
||||
return a and a.viewclass
|
||||
|
||||
def _set_viewclass(self, value):
|
||||
a = self.layout_manager
|
||||
if a:
|
||||
a.viewclass = value
|
||||
|
||||
viewclass = AliasProperty(_get_viewclass, _set_viewclass,
|
||||
bind=["layout_manager"])
|
||||
"""
|
||||
The viewclass used by the current layout_manager.
|
||||
|
||||
viewclass is an :class:`~kivy.properties.AliasProperty` that gets and sets
|
||||
the class used to generate the individual items presented in the view.
|
||||
"""
|
||||
|
||||
def _get_key_viewclass(self):
|
||||
a = self.layout_manager
|
||||
return a and a.key_viewclass
|
||||
|
||||
def _set_key_viewclass(self, value):
|
||||
a = self.layout_manager
|
||||
if a:
|
||||
a.key_viewclass = value
|
||||
|
||||
key_viewclass = AliasProperty(_get_key_viewclass, _set_key_viewclass,
|
||||
bind=["layout_manager"])
|
||||
"""
|
||||
key_viewclass is an :class:`~kivy.properties.AliasProperty` that gets and
|
||||
sets the key viewclass for the current
|
||||
:attr:`~kivy.uix.recycleview.layout_manager`.
|
||||
"""
|
||||
Reference in New Issue
Block a user