# -*- coding: utf-8 -*-
# Moovida - Home multimedia server
# Copyright (C) 2006-2009 Fluendo Embedded S.L. (www.fluendo.com).
# All rights reserved.
#
# This file is available under one of two license agreements.
#
# This file is licensed under the GPL version 3.
# See "LICENSE.GPL" in the root of this distribution including a special
# exception to use Moovida with Fluendo's plugins.
#
# The GPL part of Moovida is also available under a commercial licensing
# agreement from Fluendo.
# See "LICENSE.Moovida" in the root directory of this distribution package
# for details on that license.

from elisa.core.input_event import EventValue

from elisa.plugins.pigment.widgets.widget import Widget
from elisa.plugins.pigment.widgets.const import STATE_DISABLED, STATE_NORMAL
from elisa.plugins.pigment.widgets.theme import ValueWithUnit
from elisa.core.utils import notifying_list

from elisa.plugins.pigment.graph.group import Group
from elisa.plugins.pigment.graph.image import Image
from elisa.plugins.pigment.animation.implicit import AnimatedObject, REPLACE
from elisa.plugins.pigment.animation.animation import DECELERATE
from elisa.plugins.pigment import maths

import pgm
import gobject

import math
import time as mod_time

from twisted.internet import reactor

class List(Widget):
    """
    Abstract list widget. It displays data stored in the L{model} list. Items
    of L{model} are rendered into widgets instances of the widget class passed
    to the constructor using the specified L{renderer} function.

    Usage example for a list of strings rendered in Text drawables::

        l = List(Text)

        def renderer(item, widget):
            widget.label = str(item)

        model = range(100)

        l.set_renderer(renderer)
        l.set_model(model)


    Optionally a L{cleaner} function can be specified that would have the
    responsibility to undo what the L{renderer} function did thus leaving the
    widgets in a clean state.

    Example reusing the list defined above::

        def cleaner(widget):
            widget.label = ""

        l.set_cleaner(cleaner)


    Emit the signals:
      - item-activated: when an item of the list is activated
      - selected-item-changed: when the selected item in the list changes

    @ivar selected_item_index:    index of the currently selected item in the
                                  L{model}
    @type selected_item_index:    C{int}
    @ivar visible_range_size:     number of rendered items
    @type visible_range_size:     C{float}
    @ivar model:                  list of data items that is rendered by the
                                  list widget
    @type model:                  C{list}
    @ivar drag_motion_resolution: minimum time between 2 drag events in milliseconds
    @type drag_motion_resolution: C{int}
    @ivar drag_threshold:         amount of movement needed to activate dragging
                                  in canvas coordinates
    @type drag_threshold:         C{float}
    @ivar animation_enabled:      C{True} if the list is animated, C{False} otherwise
    @type animation_enabled:      C{bool}
    @ivar spacing:                DOCME
    @type spacing:                L{elisa.plugins.pigment.widgets.theme.ValueWithUnit}
    @ivar render_empty_items:     True to fill in the list with empty widgets
                                  when the model has less elements than
                                  visible_range_size
    @type render_empty_items:     C{bool}
    @ivar start_offset:           DOCME
    @type start_offset:           C{int}
    @ivar focus_before_activate:  DOCME
    @type focus_before_activate:  C{bool}
    """

    __gsignals__ = {
        'item-activated': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
                (gobject.TYPE_PYOBJECT,)),
        'selected-item-changed': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
                (gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT)),
    }

    model = []
    drag_motion_resolution = 15
    drag_threshold = 0.05
    animation_enabled = True
    widget_signals = {'activated': 'item-activated'}
    render_empty_items = False
    start_offset = 0
    focus_before_activate = False

    def __init__(self, widget_class, visible_range_size=7, widget_args=[], widget_kwargs={}):
        """
        @param widget_class: widget type used to render the items of the model
        @type widget_class: type inheriting from
                            L{elisa.plugins.pigment.widgets.widget.Widget}
        @param visible_range_size: DOCME
        @type visible_range_size:  int
        """
        super(List, self).__init__()
        # dictionary holding currently pending rendering calls
        # key:   child widget to be rendered
        # value: gobject source id as returned by gobject.idle_add
        self._render_calls = {}
        # dictionary holding the list of widgets that have been previously
        # rendered and not yet cleaned
        # key:   child widget previously rendered
        # value: True
        self._dirty_widgets = {}
        self._render_all_call = None

        # widget instances used for rendering the model data
        self._widgets = []
        self._widget_class = widget_class
        self._widget_args = widget_args
        self._widget_kwargs = widget_kwargs
        self._renderer = None
        self._cleaner = None

        self._spacing = ValueWithUnit(0.0)
        self._selected_item_index = -1
        self._visible_range_start = 0.0
        self._visible_range_size = 0

        # selector
        self._selector = None
        self._animated_selector = None

        self._last_drag_motion = 0

        # signals support and mouse dragging support
        self._drag_zone = Image()
        self.add(self._drag_zone, forward_signals=True)
        self._drag_zone.bg_color = (0, 0, 0, 0)
        self._drag_zone.visible = True

        # current dragging state
        self._dragging = False
        self._drag_accum = 0

        # animation support
        self.animated.setup_next_animations(duration=300,
                                            transformation=DECELERATE)
        self.animated.mode = REPLACE

        self.update_style_properties(self.style.get_items())

        self.visible_range_size = visible_range_size

    # Public API

    def set_model(self, model):
        """
        Set the list of items that are rendered by the list widget. Further
        updates to the model are automatically picked up by the list widget.

        @param model: list of items that should be rendered by the list widget
        @type model: list
        """
        self.model = model

        if isinstance(self.model, notifying_list.List):
            # FIXME: it should save the connection ids in order to disconnect later
            self.model.notifier.connect('items-changed', self._on_items_changed)
            self.model.notifier.connect('items-deleted', self._on_items_deleted)
            self.model.notifier.connect('items-inserted', self._on_items_inserted)
            self.model.notifier.connect('items-reordered', self._on_items_reordered)

        self._render_all_widgets()
        self._layout_all_widgets()
        self._set_selector_visibility()

        # no item from self.model was selected, select the first one by default
        if self.selected_item_index == -1 and len(self.model) > 0:
            self.selected_item_index = 0

    def set_renderer(self, renderer):
        """
        Function used by the list widget to render an item of the model. It
        takes an item of the model as the first argument and an instance of
        the widget class passed to the constructor of the list widget as the
        second argument.

        @param renderer: function called to render an item of the model to the
                         widget used for rendering
        @type renderer: callable
        """
        self._renderer = renderer
        self._render_all_widgets()
        self._layout_all_widgets()

    def set_cleaner(self, cleaner):
        """
        Function used by the list widget to clean a widget that was previously
        used for rendering by the renderer function passed using L{set_renderer}.
        It takes as first and only argument an instance of the widget class
        passed to the constructor of the list widget.

        Important: it will be called only on widgets for which the renderer has
        already been called.

        @param cleaner: function called to clean a widget used for rendering
        @type cleaner: callable
        """
        self._cleaner = cleaner

    def set_selector(self, selector):
        if self._selector:
            self._selector.clean()

        self._selector = selector

        self._animated_selector = AnimatedObject(self._selector)
        settings = {'duration': 200,
                    'transformation': DECELERATE}
        self._animated_selector.setup_next_animations(**settings)

        self.add(self._selector)

        self._prepare_selector()
        self._layout_selector()
        self._set_selector_visibility()

        if self.focus:
            self._selector.opacity = 255
        else:
            self._selector.opacity = 0

    # Implementation methods

    def _connect_widget(self, widget):
        for widget_signal, local_signal in self.widget_signals.iteritems():
            self._connect_widget_signal_to_own_with_model(widget,
                    widget_signal, local_signal)

    def _connect_widget_signal_to_own_with_model(self, widget, widget_signal,
                                                 local_signal):
        """
        @param widget: the widget to connect to
        @type widget:  elisa.plugins.pigment.widgets.widget.Widget
        @param widget_signal: the name of the signal of the widget
        @type widget_signal:  str
        @param local_signal: the name of the signal of the list
        @type local_signal:  str
        """
        widget.connect(widget_signal, self._emit_item_clicked, local_signal)

    def _emit_item_clicked(self, widget, *args):
        name = args[-1]
        index = self._widgets.index(widget)
        real_index = self._item_index_from_widget_index(index)
        model = self.model[real_index]

        # the first activation of an item makes it selected if it was not
        # focused
        if name == "item-activated":
            if self.focus_before_activate and not widget.focus:
                self.selected_item_index = real_index
                return True

        # select the item
        self.selected_item_index = real_index
        self.emit(name, model)
        return True

    def _on_items_changed(self, notifier, index, items):
        widget_index = self._widget_index_from_item_index(index)
        start = max(widget_index, 0)
        end = min(widget_index+len(items), len(self._widgets))
        self._render_widgets(start, end)

    def _on_items_deleted(self, notifier, index, items):
        # update self.selected_item_index so that it is always corresponding
        # to an actual item
        if self.selected_item_index >= len(self.model):
            self.selected_item_index = len(self.model)-1

        widget_index = self._widget_index_from_item_index(index)
        start = max(widget_index, 0)
        end = len(self._widgets)
        self._render_widgets(start, end)
        self._layout_all_widgets()

        self._set_selector_visibility()

    def _on_items_inserted(self, notifier, index, items):
        if len(items) == len(self.model):
            # case where the list was empty before that insertion
            # check that self.selected_item_index corresponds to an actual
            # item
            start = self._selected_to_range_start(self.selected_item_index)
            self.visible_range_start = start

            if self.selected_item_index == -1:
                # no item from self.model was selected, select the first one by default
                self.selected_item_index = 0
            elif self.selected_item_index >= len(self.model):
                self.selected_item_index = len(self.model)-1
        else:
            # update self.selected_item_index if the inserted items were before the
            # selected one so that the selected item stays selected
            if index <= self.selected_item_index and len(self.model) > 1:
                self.selected_item_index += len(items)

        widget_index = self._widget_index_from_item_index(index)
        start = max(widget_index, 0)
        end = len(self._widgets)
        self._render_widgets(start, end)
        self._layout_all_widgets()

        self._set_selector_visibility()

    def _on_items_reordered(self, notifier):
        self._render_all_widgets()

    def _move_widgets_from_beginning_to_end(self, number):
        moved = self._widgets[:number]
        self._widgets[:number] = []
        self._widgets[len(self._widgets):] = moved

    def _move_widgets_from_end_to_beginning(self, number):
        moved = self._widgets[-number:]
        self._widgets[-number:] = []
        self._widgets[:0] = moved

    def _item_index_from_widget_index(self, widget_index):
        # if that formula changes please reflect it in:
        # - _widget_index_from_item_index
        # - _layout_all_widgets
        item_index = widget_index+int(self._visible_range_start)
        return item_index

    def _widget_index_from_item_index(self, item_index):
        widget_index = item_index-int(self._visible_range_start)
        return widget_index

    def _widget_from_item_index(self, item_index):
        if item_index < 0:
            return None
        widget_index = self._widget_index_from_item_index(item_index)
        if widget_index >= 0 and widget_index < len(self._widgets):
            return self._widgets[widget_index]
        else:
            return None

    def _render_widgets(self, start, end):
        if self._renderer is None:
            return

        item_index = self._item_index_from_widget_index(start)
        for widget in self._widgets[start:end+1]:
            # clean the widget if a rendering was performed on it previously and
            # a custom cleaner was set using "set_cleaner"
            if self._cleaner is not None:
                try:
                    self._dirty_widgets.pop(widget)
                except KeyError:
                    pass
                else:
                    self._cleaner(widget)

            if item_index >= 0 and item_index < len(self.model):
                # cancel a previous rendering of the widget if there is one
                # scheduled
                try:
                    previous_idle = self._render_calls.pop(widget)
                except KeyError:
                    pass
                else:
                    gobject.source_remove(previous_idle)

                # schedule a future rendering in the mainloop
                item = self.model[item_index]
                idle = gobject.idle_add(self._render_widget, item, widget)
                self._render_calls[widget] = idle

            item_index += 1

    def _render_widget(self, item, widget):
        try:
            self._render_calls.pop(widget)
        except KeyError:
            # the scheduled rendering has been cancelled between the call from the
            # main loop of _render_widget and now; do not render
            pass
        else:
            # call the custom renderer set using "set_renderer"
            self._renderer(item, widget)
            if self._cleaner is not None:
                self._dirty_widgets[widget] = True

    def _render_all_widgets(self):
        if self._render_all_call is not None:
            return

        def render():
            self._render_all_call = None
            self._render_widgets(0, len(self._widgets)-1)

        self._render_all_call = reactor.callLater(0, render)

    def visible_range_start__get(self):
        return self._visible_range_start

    def visible_range_start__set(self, visible_range_start):
        old_start = self._visible_range_start
        self._visible_range_start = visible_range_start
        delta = int(math.modf(visible_range_start)[1]-math.modf(old_start)[1])

        if abs(delta) >= len(self._widgets):
            self._render_all_widgets()
        elif delta >= 1:
            self._move_widgets_from_beginning_to_end(delta)
            self._render_widgets(len(self._widgets)-delta,
                                 len(self._widgets)-1)
        elif delta <= -1:
            self._move_widgets_from_end_to_beginning(-delta)
            self._render_widgets(0, -delta-1)

        if visible_range_start != old_start:
            self._layout_all_widgets()

    visible_range_start = property(visible_range_start__get, visible_range_start__set)

    def visible_range_size__get(self):
        return self._visible_range_size

    def visible_range_size__set(self, visible_range_size):
        assert (visible_range_size != 0)
        if visible_range_size == self._visible_range_size:
            return
        self._visible_range_size = visible_range_size

        # set visible range start accordingly
        self._visible_range_start = self._selected_to_range_start(self._selected_item_index)

        self._create_and_render_all()
        self._queue_layout()

    def _create_and_render_all(self):
        if not self.is_mapped:
            return
        # FIXME: this is very suboptimal, we should not re-create all the
        # widgets, only those that are needed
        self._destroy_widgets()
        self._create_widgets()

        # FIXME: this is very suboptimal, we should not re-render all the
        # widgets, only those that are needed
        self._render_all_widgets()

        # Reset the focus after re-creating all the widgets.
        if self.focus:
            self._forward_focus_to_selected_item()

    visible_range_size = property(visible_range_size__get, visible_range_size__set)

    def _create_widgets(self):
        nb_widgets = int(math.ceil(self._visible_range_size)+1)
        for i in xrange(nb_widgets):
            widget = self._widget_class(*self._widget_args, **self._widget_kwargs)
            self._widgets.append(widget)
            self._connect_widget(widget)
            self.add(widget, forward_signals=True)

    def _destroy_widgets(self):
        # FIXME: the renderer might still be using or referencing the widgets
        for widget in self._widgets:
            # eventually cancel the current rendering of the widget
            try:
                idle = self._render_calls.pop(widget)
            except KeyError:
                pass
            else:
                gobject.source_remove(idle)
            if widget.parent != None:
                self.remove(widget)
            widget.clean()
        self._widgets[:] = []

    def selected_item_index__get(self):
        return self._selected_item_index

    def selected_item_index__set(self, index):
        self._stop_deceleration()
        if len(self.model) == 0:
            # if there is no data let any positive index so that it is
            # possible to preselect an item that is planned to be added
            index = max(index, 0)
        else:
            index = maths.clamp(index, 0, len(self.model)-1)
        index = int(round(index))

        # do nothing if requested index equals current index
        if index == self._selected_item_index:
            return

        visible_range_start = self._selected_to_range_start(index)

        prev_selected = self._selected_item_index
        self._selected_item_index = index

        if self.animation_enabled and len(self.model) > 0 and self.is_mapped:
            self.animated.visible_range_start = visible_range_start
        else:
            if self.animation_enabled:
                self.animated.stop_animation_for_attribute('visible_range_start')
            self.visible_range_start = visible_range_start

        try:
            item = self.model[index]
        except IndexError:
            item = None
        try:
            prev_item = self.model[prev_selected]
        except IndexError:
            prev_item = None
        gobject.idle_add(self.emit, 'selected-item-changed', item, prev_item)

        # if newly selected item's widget is visible (inside the visible
        # range) pass it the focus; in other cases it will be given the focus
        # just before rendering. See _render_widgets()
        if self.focus:
            if not self._forward_focus_to_selected_item():
                # no widget got the focus: pass the focus to the list itself
                self._accept_focus()

    selected_item_index = property(selected_item_index__get, selected_item_index__set)

    def _selected_to_range_start(self, selected):
        start = self._animated.visible_range_start
        threshold = 2
        offset = self.start_offset

        if selected <= threshold-offset or \
           len(self.model) <= self.visible_range_size-self.start_offset*2:
            visible_range_start = -offset
        elif selected >= len(self.model)-threshold+offset:
            visible_range_start = len(self.model)-self.visible_range_size+offset
        else:
            # only moves the list if newly selected item is not visible
            if selected <= start+threshold:
                visible_range_start = selected-threshold
            elif selected >= start+self._visible_range_size-threshold:
                visible_range_start = selected-self._visible_range_size+1+threshold
            else:
                visible_range_start = start

        return visible_range_start

    def _range_start_to_selected(self, range_start):
        half_size = (self.visible_range_size-1.0)/2.0
        selected = range_start + half_size
        selected = int(round(selected))
        selected = maths.clamp(selected, 0, len(self.model)-1)
        return selected

    def spacing__get(self):
        return self._spacing

    def spacing__set(self, spacing):
        self._spacing = spacing
        self._queue_layout()

    spacing = property(spacing__get, spacing__set)


    def _queue_layout(self):
        if not self.is_mapped:
            return
        self._prepare_all_widgets()
        self._layout_all_widgets()
        self._prepare_selector()
        self._layout_selector()

    def _prepare_all_widgets(self):
        pass

    def _prepare_selector(self):
        pass

    def _layout_widget(self, widget, position):
        raise NotImplementedError

    def _set_widget_state(self, widget, disabled):
        # set the final state of the widget depending on whether or not
        # it renders an item from self.model
        if disabled:
            if widget.state != STATE_DISABLED:
                widget.state = STATE_DISABLED
        else:
            if widget.state == STATE_DISABLED:
                widget.state = STATE_NORMAL

    def _layout_all_widgets(self):
        if not self.is_mapped:
            return

        # we assume that the widgets are already created and stored in
        # self._widgets as well as loaded with their corresponding data from
        # self.model

        delta, start = math.modf(self._visible_range_start)
        start = int(start)

        # compute upper and lower bounds within self._widgets of widgets that
        # are rendering an item of the model
        # remark: lower and upper do not exactly define the range of widgets
        # but potentially a wider range (e.g. upper can easily be more than
        # len(self._widgets)). This works because given a list 'l' an an
        # integer 'n' with n >= len(l) then l[:n] == l and l[n:] == [],
        # which is not entirely intuitive since l[n] would raise an IndexError.
        lower = max(0, -start)
        upper = max(0, len(self.model)-start)

        # layout widgets in range [0, 1, 2, ..., lower] that is empty widgets
        # that are *before* the first visible item; it happens when dragging
        # the list off boundaries
        for widget in self._widgets[:lower]:
            widget.visible = False

        # layout widgets in range [lower, ..., upper] that is visible and active
        # widgets
        position = lower-delta
        for widget in self._widgets[lower:upper]:
            self._set_widget_state(widget, False)
            self._layout_widget(widget, position)
            widget.visible = True
            position += 1.0

        # layout widgets in range [upper, ..., len(widgets)]
        # if 'render_empty_items' is set to True and if we are displaying
        # very few items, less than the visible_range_size then they are
        # disabled and displayed as usual; otherwise they are made invisible
        disabling = self.render_empty_items and \
                    len(self.model) < self._visible_range_size

        if disabling:
            position = upper-delta
            for widget in self._widgets[upper:]:
                self._set_widget_state(widget, True)
                self._layout_widget(widget, position)
                widget.visible = True
                position += 1.0
        else:
            for widget in self._widgets[upper:]:
                widget.visible = False

        # give the focus to the widget that corresponds to the selected item
        if self.focus:
            self._forward_focus_to_selected_item()

    def _layout_selector(self):
        if not self._selector:
            return

        visible_range_start = self._selected_to_range_start(self.selected_item_index)
        selected_item_index = self.selected_item_index - visible_range_start

        if self._selector.visible and self._selector.opacity != 0:
            selector = self._animated_selector
        else:
            selector = self._selector
            selector.z = 1.0

        self._actual_layout_selector(selector, selected_item_index)

    def _set_selector_opacity(self):
        if self._selector is None:
            return

        if self.focus:
            self._animated_selector.opacity = 255
        else:
            self._animated_selector.opacity = 0

    def _set_selector_visibility(self):
        if not self._selector:
            return

        # FIXME: visibility of the selector should be dynamic depending on how
        # the content of self.model evolves
        if len(self.model) == 0:
            self._selector.visible = False
        else:
            self._selector.visible = True

    def _forward_focus_to_selected_item(self):
        widget = self._widget_from_item_index(self.selected_item_index)
        if widget != None and widget.parent != None:
            return widget.set_focus()
        else:
            return False

    def set_focus(self):
        # Overridden from elisa.plugins.pigment.widgets.widget.Widget
        focus_accepted = self._forward_focus_to_selected_item()
        if focus_accepted:
            return True

        # This happens when the list doesn't contain any element or when the
        # selected widget did not accept the focus.
        self._accept_focus()
        return True

    def compute_opacity(self, index):
        indexes =   [-0.1, 0.0, 0.1, 0.2, 0.6, 0.7, 0.8, 0.9, 1.0]
        opacities = [   0,  25, 127, 255, 255, 204, 153, 102,   0]

        # We assume that the list of 'opacities' starts with a single 0, then is
        # increasing up to 255, then decreasing to a single 0 again.
        # 'min' is the number of items in the increasing segment including 0
        # (increasing segment: [0, 25, 127])
        # 'max' is the number of items in decreasing segment including 0
        # (decreasing segment: [204, 153, 102, 0])
        min = 3
        max = 4

        item_index = int(index + self._visible_range_start)

        s1 = item_index+1+self.start_offset
        if s1 <= min-1:
            l = min-s1
            opacities[min-l:min] = [255]*l
        elif not (item_index >= len(self.model)):
            # special case: do not set opacity to maximum if the opacity is
            # computed for a widget that is not representing a model
            # typically the case when render_empty_items is set to True
            s2 = len(self.model)-item_index+self.start_offset
            if s2 <= max-1:
                l = max-s2
                opacities[-max:-max+l] = [255]*l

        # indexes are defined to work in [0.0, 1.0] whereas
        # index is given inside [0.0, self._visible_range_size]
        index = index/self._visible_range_size
        opacity = self._piecewise_interpolation(indexes, opacities, index)

        return opacity

    def _piecewise_interpolation(self, x, y, factor):
        # clamp after lower and upper limits
        if factor < x[0]:
            return y[0]
        elif factor > x[-1]:
            return y[-1]
        else:
            # general case: looking for the segment where factor belongs
            i = 0
            while factor > x[i+1]:
                i += 1

            # factor must be between 0.0 and 1.0
            new_factor = (factor - x[i]) / (x[i+1] - x[i])
            return maths.lerp(y[i], y[i+1], new_factor)

    # Overridden from Group

    def do_mapped(self):
        if len(self._widgets) == 0:
            self._create_and_render_all()
        self._queue_layout()

    def regenerate(self):
        self._queue_layout()
        super(List, self).regenerate()


    def handle_input(self, manager, event):
        if event.value == EventValue.KEY_OK:
            self.emit('item-activated', self.model[self.selected_item_index])
            return True

        return super(List, self).handle_input(manager, event)

    # Signals support methods
    def do_focus(self, focus):
        self._set_selector_opacity()

    def do_scrolled(self, x, y, z, direction, time):
        if direction == pgm.SCROLL_UP:
            if self._selected_item_index > 0:
                self.selected_item_index -= 1
        else:
            if self._selected_item_index < (len(self.model) - 1):
                self.selected_item_index += 1
        return True

    def do_drag_begin(self, x, y, z, button, time, pressure):
        # do not allow dragging when there are not enough items
        if len(self.model) <= self._visible_range_size-self.start_offset*2:
            return True

        if self._selector != None:
            self._animated_selector.opacity = 0

        self._dragging = True

        self._initial = (x, y, time)
        self.animated.stop_animations()
        self._stop_deceleration()

        self._drag_accum = 0

        self.speed = 0.0
        return True

    def do_drag_end(self, x, y, z, button, time):
        if not self._dragging:
            return True

        self._dragging = False

        self._current_time = mod_time.time()
        self._deceleration_source = gobject.timeout_add(17, self._decelerate)

        self._set_selector_opacity()

        return True

    def do_selected_item_changed(self, item, prev_item):
        self._layout_selector()

    # Inertia support methods
    # FIXME: they should be part of the animation framework

    deceleration = 8.0
    def _decelerate(self):
        self._previous_time = self._current_time
        self._current_time = mod_time.time()
        delta = self._current_time - self._previous_time

        if self.speed > 0.0:
            self.speed -= self.deceleration*delta
            if self.speed > 0.0:
                self.visible_range_start -= self.speed*delta

                # block the movement if it reaches the first item
                if self._range_start_to_selected(self._visible_range_start) > 0:
                    return True

        elif self.speed < 0.0:
            self.speed += self.deceleration*delta
            if self.speed < 0.0:
                self.visible_range_start -= self.speed*delta

                # block the movement if it reaches the last item
                if self._range_start_to_selected(self._visible_range_start) < len(self.model)-1:
                    return True

        final_item_index = self._range_start_to_selected(self._visible_range_start)
        if final_item_index != self.selected_item_index:
            self.selected_item_index = final_item_index
        else:
            final_range_start = self._selected_to_range_start(final_item_index)
            self.animated.visible_range_start = final_range_start

        return False

    def _stop_deceleration(self):
        try:
            gobject.source_remove(self._deceleration_source)
        except AttributeError:
            pass

    @classmethod
    def _demo_widget(cls, *args, **kwargs):
        from elisa.plugins.pigment.graph.text import Text
        from elisa.plugins.pigment.graph.image import Image
        from elisa.plugins.pigment.widgets.const import STATE_SELECTED
        from elisa.core.utils.notifying_list import List
        from elisa.core.utils.profiling import profileit
        import time
        import random

        class DemoText(Widget):
            def __init__(self):
                super(DemoText, self).__init__()
                self.text = Text()
                self.add(self.text)
                self.text.alignment = pgm.TEXT_ALIGN_CENTER
                self.text.bg_color = (70, 70, 70, 255)
                self.text.visible = True

            def do_state_changed(self, previous_state):
                if self.state == STATE_SELECTED:
                    self.text.bg_color = (23, 131, 173, 255)
                else:
                    self.text.bg_color = (70, 70, 70, 255)

        widget = cls(DemoText)
        # workaround the fact that DemoText is not inheriting from Button
        # and therefore does not have an 'activated' signal
        widget.widget_signals = {}
        widget.visible = True

        model = List(range(1000))
        def renderer(item, widget):
            widget.text.label = str(item)

        widget.set_renderer(renderer)
        widget.set_model(model)

        widget.position = (20.0, 20.0, 0.0)

        # make drag zone visible to be able to see the boundaries of the list
        # widget
        widget._drag_zone.bg_color = (180, 190, 10, 140)

        selector = Image()
        selector.bg_color = (255, 0, 0, 100)
#        widget.set_selector(selector)

        def item_clicked_cb(self, item):
            index = self.model.index(item)

        def key_press_event_cb(self, viewport, event, grid):
            if event.keyval == pgm.keysyms.a:
                self.model.insert(self.selected_item_index+1, "test" + str(random.randint(0, 10000)))
            elif event.keyval == pgm.keysyms.d:
                item = self.model[self.selected_item_index]
                self.model.remove(item)
            elif event.keyval == pgm.keysyms.p:
                @profileit
                def profilethat():
                    for i in range(500):
                        widget.selected_item_index += 1
                profilethat()

        widget.connect('item-activated', item_clicked_cb)
        widget.connect('key-press-event', key_press_event_cb)

        return widget

    def clean(self):
        for widget, idle in self._render_calls.iteritems():
            gobject.source_remove(idle)
        self._render_calls = {}

        if self._render_all_call is not None:
            self._render_all_call.cancel()
            self._render_all_call = None

        for widget in self._widgets:
            if widget.parent != None:
                self.remove(widget)
            widget.clean()
        self._widgets=[]
        self.animation_enabled = False

        if self._selector is not None:
            self._selector.clean()
            self._selector = None
        if self._animated_selector is not None:
            self._animated_selector.stop_animations()
            self._animated_selector = None

        self._drag_zone.clean()
        self._drag_zone = None

        return super(List, self).clean()

