Ajout du GUI
This commit is contained in:
423
kivy/uix/recycleview/views.py
Normal file
423
kivy/uix/recycleview/views.py
Normal file
@@ -0,0 +1,423 @@
|
||||
'''
|
||||
RecycleView Views
|
||||
=================
|
||||
|
||||
.. versionadded:: 1.10.0
|
||||
|
||||
The adapter part of the RecycleView which together with the layout is the
|
||||
view part of the model-view-controller pattern.
|
||||
|
||||
The view module handles converting the data to a view using the adapter class
|
||||
which is then displayed by the layout. A view can be any Widget based class.
|
||||
However, inheriting from RecycleDataViewBehavior adds methods for converting
|
||||
the data to a view.
|
||||
|
||||
TODO:
|
||||
* Make view caches specific to each view class type.
|
||||
|
||||
'''
|
||||
|
||||
from kivy.properties import ObjectProperty
|
||||
from kivy.event import EventDispatcher
|
||||
from collections import defaultdict
|
||||
|
||||
__all__ = (
|
||||
'RecycleDataViewBehavior', 'RecycleKVIDsDataViewBehavior',
|
||||
'RecycleDataAdapter')
|
||||
|
||||
_view_base_cache = {}
|
||||
'''Cache whose keys are classes and values is a boolean indicating whether the
|
||||
class inherits from :class:`RecycleDataViewBehavior`.
|
||||
'''
|
||||
|
||||
_cached_views = defaultdict(list)
|
||||
'''A size limited cache that contains old views (instances) that are not used.
|
||||
Each key is a class whose value is the list of the instances of that class.
|
||||
'''
|
||||
# current number of unused classes in the class cache
|
||||
_cache_count = 0
|
||||
# maximum number of items in the class cache
|
||||
_max_cache_size = 1000
|
||||
|
||||
|
||||
def _clean_cache():
|
||||
'''Trims _cached_views cache to half the size of `_max_cache_size`.
|
||||
'''
|
||||
# all keys will be reduced to max_size.
|
||||
max_size = (_max_cache_size // 2) // len(_cached_views)
|
||||
global _cache_count
|
||||
for cls, instances in _cached_views.items():
|
||||
_cache_count -= max(0, len(instances) - max_size)
|
||||
del instances[max_size:]
|
||||
|
||||
|
||||
class RecycleDataViewBehavior(object):
|
||||
'''A optional base class for data views (:attr:`RecycleView`.viewclass).
|
||||
If a view inherits from this class, the class's functions will be called
|
||||
when the view needs to be updated due to a data change or layout update.
|
||||
'''
|
||||
|
||||
def refresh_view_attrs(self, rv, index, data):
|
||||
'''Called by the :class:`RecycleAdapter` when the view is initially
|
||||
populated with the values from the `data` dictionary for this item.
|
||||
|
||||
Any pos or size info should be removed because they are set
|
||||
subsequently with :attr:`refresh_view_layout`.
|
||||
|
||||
:Parameters:
|
||||
|
||||
`rv`: :class:`RecycleView` instance
|
||||
The :class:`RecycleView` that caused the update.
|
||||
`data`: dict
|
||||
The data dict used to populate this view.
|
||||
'''
|
||||
sizing_attrs = RecycleDataAdapter._sizing_attrs
|
||||
for key, value in data.items():
|
||||
if key not in sizing_attrs:
|
||||
setattr(self, key, value)
|
||||
|
||||
def refresh_view_layout(self, rv, index, layout, viewport):
|
||||
'''Called when the view's size is updated by the layout manager,
|
||||
:class:`RecycleLayoutManagerBehavior`.
|
||||
|
||||
:Parameters:
|
||||
|
||||
`rv`: :class:`RecycleView` instance
|
||||
The :class:`RecycleView` that caused the update.
|
||||
`viewport`: 4-tuple
|
||||
The coordinates of the bottom left and width height in layout
|
||||
manager coordinates. This may be larger than this view item.
|
||||
|
||||
:raises:
|
||||
`LayoutChangeException`: If the sizing or data changed during a
|
||||
call to this method, raising a `LayoutChangeException` exception
|
||||
will force a refresh. Useful when data changed and we don't want
|
||||
to layout further since it'll be overwritten again soon.
|
||||
'''
|
||||
w, h = layout.pop('size')
|
||||
if w is None:
|
||||
if h is not None:
|
||||
self.height = h
|
||||
else:
|
||||
if h is None:
|
||||
self.width = w
|
||||
else:
|
||||
self.size = w, h
|
||||
|
||||
for name, value in layout.items():
|
||||
setattr(self, name, value)
|
||||
|
||||
def apply_selection(self, rv, index, is_selected):
|
||||
pass
|
||||
|
||||
|
||||
class RecycleKVIDsDataViewBehavior(RecycleDataViewBehavior):
|
||||
"""Similar to :class:`RecycleDataViewBehavior`, except that the data keys
|
||||
can signify properties of an object named with an id in the root KV rule.
|
||||
|
||||
E.g. given a KV rule::
|
||||
|
||||
<MyRule@RecycleKVIDsDataViewBehavior+BoxLayout>:
|
||||
Label:
|
||||
id: name
|
||||
Label:
|
||||
id: value
|
||||
|
||||
Then setting the data list with
|
||||
``rv.data = [{'name.text': 'Kivy user', 'value.text': '12'}]`` would
|
||||
automatically set the corresponding labels.
|
||||
|
||||
So, if the key doesn't have a period, the named property of the root widget
|
||||
will be set to the corresponding value. If there is a period, the named
|
||||
property of the widget with the id listed before the period will be set to
|
||||
the corresponding value.
|
||||
|
||||
.. versionadded:: 2.0.0
|
||||
"""
|
||||
|
||||
def refresh_view_attrs(self, rv, index, data):
|
||||
sizing_attrs = RecycleDataAdapter._sizing_attrs
|
||||
for key, value in data.items():
|
||||
if key not in sizing_attrs:
|
||||
name, *ids = key.split('.')
|
||||
if ids:
|
||||
if len(ids) != 1:
|
||||
raise ValueError(
|
||||
f'Data key "{key}" has more than one period')
|
||||
setattr(self.ids[name], ids[0], value)
|
||||
else:
|
||||
setattr(self, name, value)
|
||||
|
||||
|
||||
class RecycleDataAdapter(EventDispatcher):
|
||||
'''The class that converts data to a view.
|
||||
|
||||
--- Internal details ---
|
||||
A view can have 3 states.
|
||||
|
||||
* It can be completely in sync with the data, which
|
||||
occurs when the view is displayed. These are stored in :attr:`views`.
|
||||
* It can be dirty, which occurs when the view is in sync with the data,
|
||||
except for the size/pos parameters which is controlled by the layout.
|
||||
This occurs when the view is not currently displayed but the data has
|
||||
not changed. These views are stored in :attr:`dirty_views`.
|
||||
* Finally the view can be dead which occurs when the data changes and
|
||||
the view was not updated or when a view is just created. Such views
|
||||
are typically added to the internal cache.
|
||||
|
||||
Typically what happens is that the layout manager lays out the data
|
||||
and then asks for views, using :meth:`set_visible_views,` for some specific
|
||||
data items that it displays.
|
||||
|
||||
These views are gotten from the current views, dirty or global cache. Then
|
||||
depending on the view state :meth:`refresh_view_attrs` is called to bring
|
||||
the view up to date with the data (except for sizing parameters). Finally,
|
||||
the layout manager gets these views, updates their size and displays them.
|
||||
'''
|
||||
|
||||
recycleview = ObjectProperty(None, allownone=True)
|
||||
'''The :class:`~kivy.uix.recycleview.RecycleViewBehavior` associated
|
||||
with this instance.
|
||||
'''
|
||||
|
||||
# internals
|
||||
views = {} # current displayed items
|
||||
# items whose attrs, except for pos/size is still accurate
|
||||
dirty_views = defaultdict(dict)
|
||||
|
||||
_sizing_attrs = {
|
||||
'size', 'width', 'height', 'size_hint', 'size_hint_x', 'size_hint_y',
|
||||
'pos', 'x', 'y', 'center', 'center_x', 'center_y', 'pos_hint',
|
||||
'size_hint_min', 'size_hint_min_x', 'size_hint_min_y', 'size_hint_max',
|
||||
'size_hint_max_x', 'size_hint_max_y'}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""
|
||||
Fix for issue https://github.com/kivy/kivy/issues/5913:
|
||||
Scrolling RV A, then Scrolling RV B, content of A and B seemed
|
||||
to be getting mixed up
|
||||
"""
|
||||
self.views = {}
|
||||
self.dirty_views = defaultdict(dict)
|
||||
super(RecycleDataAdapter, self).__init__(**kwargs)
|
||||
|
||||
def attach_recycleview(self, rv):
|
||||
'''Associates a :class:`~kivy.uix.recycleview.RecycleViewBehavior`
|
||||
with this instance. It is stored in :attr:`recycleview`.
|
||||
'''
|
||||
self.recycleview = rv
|
||||
|
||||
def detach_recycleview(self):
|
||||
'''Removes the :class:`~kivy.uix.recycleview.RecycleViewBehavior`
|
||||
associated with this instance and clears :attr:`recycleview`.
|
||||
'''
|
||||
self.recycleview = None
|
||||
|
||||
def create_view(self, index, data_item, viewclass):
|
||||
'''(internal) Creates and initializes the view for the data at `index`.
|
||||
|
||||
The returned view is synced with the data, except for the pos/size
|
||||
information.
|
||||
'''
|
||||
if viewclass is None:
|
||||
return
|
||||
|
||||
view = viewclass()
|
||||
self.refresh_view_attrs(index, data_item, view)
|
||||
return view
|
||||
|
||||
def get_view(self, index, data_item, viewclass):
|
||||
'''(internal) Returns a view instance for the data at `index`
|
||||
|
||||
It looks through the various caches and finally creates a view if it
|
||||
doesn't exist. The returned view is synced with the data, except for
|
||||
the pos/size information.
|
||||
|
||||
If found in the cache it's removed from the source
|
||||
before returning. It doesn't check the current views.
|
||||
'''
|
||||
# is it in the dirtied views?
|
||||
dirty_views = self.dirty_views
|
||||
if viewclass is None:
|
||||
return
|
||||
stale = False
|
||||
view = None
|
||||
|
||||
if viewclass in dirty_views: # get it first from dirty list
|
||||
dirty_class = dirty_views[viewclass]
|
||||
if index in dirty_class:
|
||||
# we found ourself in the dirty list, no need to update data!
|
||||
view = dirty_class.pop(index)
|
||||
elif _cached_views[viewclass]:
|
||||
# global cache has this class, update data
|
||||
view, stale = _cached_views[viewclass].pop(), True
|
||||
elif dirty_class:
|
||||
# random any dirty view element - update data
|
||||
view, stale = dirty_class.popitem()[1], True
|
||||
elif _cached_views[viewclass]: # otherwise go directly to cache
|
||||
# global cache has this class, update data
|
||||
view, stale = _cached_views[viewclass].pop(), True
|
||||
|
||||
if view is None:
|
||||
view = self.create_view(index, data_item, viewclass)
|
||||
if view is None:
|
||||
return
|
||||
|
||||
if stale:
|
||||
self.refresh_view_attrs(index, data_item, view)
|
||||
return view
|
||||
|
||||
def refresh_view_attrs(self, index, data_item, view):
|
||||
'''(internal) Syncs the view and brings it up to date with the data.
|
||||
|
||||
This method calls :meth:`RecycleDataViewBehavior.refresh_view_attrs`
|
||||
if the view inherits from :class:`RecycleDataViewBehavior`. See that
|
||||
method for more details.
|
||||
|
||||
.. note::
|
||||
Any sizing and position info is skipped when syncing with the data.
|
||||
'''
|
||||
viewclass = view.__class__
|
||||
if viewclass not in _view_base_cache:
|
||||
_view_base_cache[viewclass] = isinstance(view,
|
||||
RecycleDataViewBehavior)
|
||||
|
||||
if _view_base_cache[viewclass]:
|
||||
view.refresh_view_attrs(self.recycleview, index, data_item)
|
||||
else:
|
||||
sizing_attrs = RecycleDataAdapter._sizing_attrs
|
||||
for key, value in data_item.items():
|
||||
if key not in sizing_attrs:
|
||||
setattr(view, key, value)
|
||||
|
||||
def refresh_view_layout(self, index, layout, view, viewport):
|
||||
'''Updates the sizing information of the view.
|
||||
|
||||
viewport is in coordinates of the layout manager.
|
||||
|
||||
This method calls :meth:`RecycleDataViewBehavior.refresh_view_attrs`
|
||||
if the view inherits from :class:`RecycleDataViewBehavior`. See that
|
||||
method for more details.
|
||||
|
||||
.. note::
|
||||
Any sizing and position info is skipped when syncing with the data.
|
||||
'''
|
||||
if view.__class__ not in _view_base_cache:
|
||||
_view_base_cache[view.__class__] = isinstance(
|
||||
view, RecycleDataViewBehavior)
|
||||
|
||||
if _view_base_cache[view.__class__]:
|
||||
view.refresh_view_layout(
|
||||
self.recycleview, index, layout, viewport)
|
||||
else:
|
||||
w, h = layout.pop('size')
|
||||
if w is None:
|
||||
if h is not None:
|
||||
view.height = h
|
||||
else:
|
||||
if h is None:
|
||||
view.width = w
|
||||
else:
|
||||
view.size = w, h
|
||||
|
||||
for name, value in layout.items():
|
||||
setattr(view, name, value)
|
||||
|
||||
def make_view_dirty(self, view, index):
|
||||
'''(internal) Used to flag this view as dirty, ready to be used for
|
||||
others. See :meth:`make_views_dirty`.
|
||||
'''
|
||||
del self.views[index]
|
||||
self.dirty_views[view.__class__][index] = view
|
||||
|
||||
def make_views_dirty(self):
|
||||
'''Makes all the current views dirty.
|
||||
|
||||
Dirty views are still in sync with the corresponding data. However, the
|
||||
size information may go out of sync. Therefore a dirty view can be
|
||||
reused by the same index by just updating the sizing information.
|
||||
|
||||
Once the underlying data of this index changes, the view should be
|
||||
removed from the dirty views and moved to the global cache with
|
||||
:meth:`invalidate`.
|
||||
|
||||
This is typically called when the layout manager needs to re-layout all
|
||||
the data.
|
||||
'''
|
||||
views = self.views
|
||||
if not views:
|
||||
return
|
||||
|
||||
dirty_views = self.dirty_views
|
||||
for index, view in views.items():
|
||||
dirty_views[view.__class__][index] = view
|
||||
self.views = {}
|
||||
|
||||
def invalidate(self):
|
||||
'''Moves all the current views into the global cache.
|
||||
|
||||
As opposed to making a view dirty where the view is in sync with the
|
||||
data except for sizing information, this will completely disconnect the
|
||||
view from the data, as it is assumed the data has gone out of sync with
|
||||
the view.
|
||||
|
||||
This is typically called when the data changes.
|
||||
'''
|
||||
global _cache_count
|
||||
for view in self.views.values():
|
||||
_cached_views[view.__class__].append(view)
|
||||
_cache_count += 1
|
||||
|
||||
for cls, views in self.dirty_views.items():
|
||||
_cached_views[cls].extend(views.values())
|
||||
_cache_count += len(views)
|
||||
|
||||
if _cache_count >= _max_cache_size:
|
||||
_clean_cache()
|
||||
self.views = {}
|
||||
self.dirty_views.clear()
|
||||
|
||||
def set_visible_views(self, indices, data, viewclasses):
|
||||
'''Gets a 3-tuple of the new, remaining, and old views for the current
|
||||
viewport.
|
||||
|
||||
The new views are synced to the data except for the size/pos
|
||||
properties.
|
||||
The old views need to be removed from the layout, and the new views
|
||||
added.
|
||||
|
||||
The new views are not necessarily *new*, but are all the currently
|
||||
visible views.
|
||||
'''
|
||||
visible_views = {}
|
||||
previous_views = self.views
|
||||
ret_new = []
|
||||
ret_remain = []
|
||||
get_view = self.get_view
|
||||
|
||||
# iterate though the visible view
|
||||
# add them into the container if not already done
|
||||
for index in indices:
|
||||
view = previous_views.pop(index, None)
|
||||
if view is not None: # was current view
|
||||
visible_views[index] = view
|
||||
ret_remain.append((index, view))
|
||||
else:
|
||||
view = get_view(index, data[index],
|
||||
viewclasses[index]['viewclass'])
|
||||
if view is None:
|
||||
continue
|
||||
visible_views[index] = view
|
||||
ret_new.append((index, view))
|
||||
|
||||
old_views = previous_views.items()
|
||||
self.make_views_dirty()
|
||||
self.views = visible_views
|
||||
return ret_new, ret_remain, old_views
|
||||
|
||||
def get_visible_view(self, index):
|
||||
'''Returns the currently visible view associated with ``index``.
|
||||
|
||||
If no view is currently displayed for ``index`` it returns ``None``.
|
||||
'''
|
||||
return self.views.get(index)
|
||||
Reference in New Issue
Block a user