<commit_before>"""
RecycleView
===========

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::
    This module is highly experimental, its API may change in the future and
    the documentation is not complete at this time.

The view is generatad by processing the :attr:`~RecycleView.data`, essentially
a list of dicts, and using 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 'data' you pass in via a list of dicts.
* View: The View is split across layout and views and implemented by...
* Controller: The controller is implemented by :class:`RecycleViewBehavior`.

These are abstract classes and cannot be used directly. The default concrete
implementation is RecycleDataModel for M, RecycleLayout and ... for views,
and RecycleView for C.

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 added as a layout manager when added as the
child of the RecycleView. Similarly when removed. A requirement is that the
layout manager must be a sub-child of the RecycleView 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()

Please see the `examples/widgets/recycleview/basic_data.py` file for a more
complete example.

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 module documentation for more informations.
    """

    # 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):
        '''Should be called when data changes. Data changes typically indicate
        that everything has to be recomputed since the source data changed.

        This method is automatically bound to `'on_data_changed'` in
        :class:`~kivy.uix.recycleview.datamodel.RecycleDataModelBehavior` 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):
        '''Should be called when the layout changes or needs to change.
        Typically called when the data has not been changed but a layout
        parameter has and therefore the layout needs to be recomputed.
        '''
        self._refresh_flags['layout'].append(kwargs)
        self._refresh_trigger()

    def refresh_from_viewport(self, *largs):
        '''Should be called when the viewport changes and the displayed data
        must be updated. Typically 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`.
    """


class RecycleView(RecycleViewBehavior, ScrollView):
    """RecycleView is a flexible view for providing a limited window
    into a large data set.

    See 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()
<commit_msg>doc: grammar tweaks to uix/recycleview/__init__.py<commit_after>"""
RecycleView
===========

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::
    This module is highly experimental, its API may change in the future and
    the documentation is not complete at this time.

The view is generatad by processing the :attr:`~RecycleView.data`, essentially
a list of dicts, and using 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 'data' you pass in via a list of dicts.
* View: The View is split across layout and views and implemented by...
* Controller: The controller is implemented by :class:`RecycleViewBehavior`.

These are abstract classes and cannot be used directly. The default concrete
implementation is RecycleDataModel for M, RecycleLayout and ... for views,
and RecycleView for C.

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 added as a layout manager when added as the
child of the RecycleView. Similarly when removed. A requirement is that the
layout manager must be a sub-child of the RecycleView 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()

Please see the `examples/widgets/recycleview/basic_data.py` file for a more
complete example.

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 be recomputed since the source data changed.

        This method is automatically bound to `'on_data_changed'` in
        :class:`~kivy.uix.recycleview.datamodel.RecycleDataModelBehavior` 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`.
    """


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()

{'fraction_non_alphanumeric': 0.07614093166233152, 'fraction_numerical': 0.0016079451406951998, 'mean_word_length': 2.5665373587451508, 'pattern_counts': {'":': 0, '<': 7, '<?xml version=': 0, '>': 7, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3375726', 'n_tokens_mistral': 5787, 'n_tokens_neox': 5585, 'n_words': 1898}