示例#1
0
class ScrollMenu(clutter.Group, object):
    """Menu widget that contains text items."""
    __gtype_name__ = 'ScrollMenu'
    __gsignals__ = {
        'activated': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
        'selected': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
        'moved': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
    }

    MODE_SELECTION = 0
    MODE_MOTION = 1
    MODE_STOP = 2

    def __init__(self, item_gap, item_height, font_size, color_name):
        clutter.Group.__init__(self)
        self._motion_buffer = MotionBuffer()
        self._items = []
        self._item_gap = item_gap
        self._item_height = item_height
        self._item_font_size = font_size
        self._item_color_name = color_name
        self._selected_index = 1
        self._visible_items = 5
        self._event_mode = -1
        self._animation_progression = 0
        self._animation_start_index = 1
        self._animation_end_index = 1
        self._active = False
        self._motion_handler = 0

        self._timeline = clutter.Timeline(300)
        self._alpha = clutter.Alpha(self._timeline, clutter.EASE_IN_OUT_SINE)

        # preparation to pointer events handling
        self.set_reactive(True)
        self.connect('scroll-event', self._on_scroll_event)
        self.connect('button-press-event', self._on_button_press_event)
        self.connect('button-release-event', self._on_button_release_event)

    def refresh(self):
        """Refresh the menu: clip area dimensions and items positions"""
        self._set_selected_index(self._selected_index, 1)
        self._set_visible_items(self._visible_items)

    def add_item(self, text, name):
        """Creation of a new MenuItem and addition to the ScrollMenu"""
        item = ScrollMenuItem(self._alpha, text, self._item_height,
                              self._item_font_size, self._item_color_name)

        item.set_name(name)
        item.connect('notify::y', self._update_item_opacity)
        self.add(item)

        self._items.append(item)
        self._update_item_opacity(item)

    def remove_item(self, name):
        """Remove an item from the menu"""
        index = self.get_index(name)

        if index != -1:
            # if item was found, we remove it from the item list, from the
            # group and finally we delete it.
            item = self._items[index]
            self._items.remove(item)
            self.remove(item)
            del item

    def _get_active(self):
        """Active property getter"""
        return self._active

    def _set_active(self, boolean):
        """Active property setter"""
        if self._active == boolean:
            return

        self._active = boolean
        if boolean:
            self.set_opacity(255)
            self.emit('activated')
        else:
            self.set_opacity(128)

    active = property(_get_active, _set_active)

    def stop_animation(self):
        '''Stops the timeline driving menu animation.'''
        self._timeline.stop()

    def _update_behaviours(self, target):
        """Preparation of behaviours applied to menu items before animation"""
        items_len = len(self._items)
        step = 1.0 / items_len
        step_pix = self._item_gap + self._item_height
        middle_index = int(self._visible_items / 2) + 1

        for x, item in enumerate(self._items):
            item.behaviour.start_index = (x + middle_index - \
                self._selected_index) * step
            item.behaviour.end_index = (x + middle_index - target) * step

            item.behaviour.start_knot = (0.0, -step_pix)
            item.behaviour.end_knot = (0.0, (items_len - 1.0) * step_pix)

    def _display_items_at_target(self, target):
        """Menu is displayed for a particular targeted index value"""
        step = 1.0 / len(self._items)
        middle_index = int(self._visible_items / 2) + 1

        for x, item in enumerate(self._items):
            raw_index = (x + middle_index - target) * step

            if raw_index >= 0:
                index = math.modf(raw_index)[0]
            else:
                index = 1 + math.modf(raw_index)[0]

            # Calculation of new coordinates
            xx = index * (item.behaviour.end_knot[0] - \
                item.behaviour.start_knot[0]) + item.behaviour.start_knot[0]
            yy = index * (item.behaviour.end_knot[1] - \
                item.behaviour.start_knot[1]) + item.behaviour.start_knot[1]

            item.set_position(int(xx), int(yy))

    def _get_visible_items(self):
        """visible_items property getter"""
        return self._visible_items

    def _set_visible_items(self, visible_items):
        """visible_items property setter"""
        self._visible_items = visible_items
        height = visible_items * self._item_height + (visible_items - 1) * \
            self._item_gap
        self.set_clip(0, 0, self.get_width(), height)

    visible_items = property(_get_visible_items, _set_visible_items)

    def _get_selected_index(self):
        """selected_index property getter"""
        return self._selected_index

    def _set_selected_index(self, selected_index, duration=300):
        """selected_index property setter"""
        if not self._timeline.is_playing():
            items_len = len(self._items)
            self._update_behaviours(selected_index)

            # those 2 variables are used if we want to stop the timeline
            # we use them + timeline progression to calculate the current index
            # when (if) we stop
            self._animation_start_index = self._selected_index
            self._animation_end_index = selected_index

            # selected_index can be any desired value but in the end,
            # we have to rescale it to be between 0 and (items_len-1)
            if selected_index >= 0:
                self._selected_index = selected_index - \
                    math.modf(selected_index / items_len)[1] * items_len
            else:
                self._selected_index = selected_index + \
                    (math.modf(-(selected_index + 1) / items_len)[1] + 1) * \
                    items_len

            self._timeline.set_duration(duration)
            self._timeline.start()

            self.emit('moved')

    selected_index = property(_get_selected_index, _set_selected_index)

    def get_selected(self):
        """Get currently selected menuitem"""
        return self._items[int(self._selected_index)]

    def get_index(self, text):
        """Returns index of label with the text as passed or -1 if not found"""
        for item in self._items:
            if item.get_name() == text:
                return self._items.index(item)
        return -1

    def scroll_by(self, step, duration=300):
        """Generic scroll of menu items"""
        self._set_selected_index(self._selected_index + step, duration)

    def scroll_up(self, duration=300):
        """All menu items are scrolled up"""
        self.scroll_by(-1, duration)

    def scroll_down(self, duration=300):
        """All menu items are scrolled down"""
        self.scroll_by(1, duration)

    def get_opacity_for_y(self, y):
        """Calculation of actor's opacity as a function of its y coordinates"""
        opacity_first_item = 40
        opacity_selected_item = 255
        middle = int(self._visible_items / 2)

        y_medium_item = middle * (self._item_height + self._item_gap)
        a = float(opacity_selected_item - opacity_first_item)
        a /= float(y_medium_item)

        if y <= y_medium_item:
            opacity = y * a + opacity_first_item
        else:
            opacity = opacity_selected_item * 2 - opacity_first_item - a * y

        if opacity < 0:
            opacity = 0

        return int(opacity)

    def _update_item_opacity(self, item, stage=None):
        """Set opacity to actors when they are moving. Opacity is f(y)"""
        opacity = self.get_opacity_for_y(item.get_y())
        item.set_opacity(opacity)

    def _on_button_press_event(self, actor, event):
        """button-press-event handler"""
        clutter.grab_pointer(self)
        if not self.handler_is_connected(self._motion_handler):
            self._motion_handler = self.connect('motion-event',
                                                self._on_motion_event)

        if self._timeline.is_playing():
            # before we stop the timeline, store its progression
            self._animation_progression = self._timeline.get_progress()

            # A click with an animation pending should stop the animation
            self._timeline.stop()

            # go to MODE_STOP to handle correctly next button-release event
            self._event_mode = self.MODE_STOP
        else:
            # no animation pending so we're going to do either a menu_item
            # selection or a menu motion. This will be decided later, right now
            # we just take a snapshot of this button-press-event as a start.
            self._motion_buffer.start(event)
            self._event_mode = self.MODE_SELECTION

        return False

    def _on_button_release_event(self, actor, event):
        """button-release-event handler"""
        items_len = len(self._items)

        clutter.ungrab_pointer()
        if self.handler_is_connected(self._motion_handler):
            self.disconnect_by_func(self._on_motion_event)
        self._motion_buffer.compute_from_last_motion_event(event)

        if not self.active:
            self.active = True
            return

        y = event.y - self.get_y()

        if self._event_mode == self.MODE_SELECTION:
            # if we are in MODE_SELECTION it means that we want to select
            # the menu item bellow the pointer

            for index, item in enumerate(self._items):
                item_y = item.get_y()
                item_h = item.get_height()
                if (y >= item_y) and (y <= (item_y + item_h)):
                    delta1 = index - self._selected_index
                    delta2 = index - self._selected_index + items_len
                    delta3 = index - self._selected_index - items_len

                    delta = 99999
                    for i in [delta1, delta2, delta3]:
                        if math.fabs(i) < math.fabs(delta):
                            delta = i

                    self.scroll_by(delta)

                    # if delta = 0 it means we've clicked on the selected item
                    if delta == 0:
                        self.emit('selected')

        elif self._event_mode == self.MODE_MOTION:
            speed = self._motion_buffer.speed_y_from_last_motion_event
            target = self._selected_index - \
                self._motion_buffer.dy_from_start / \
                self._items[0].behaviour.path_length * items_len

            new_index = int(target - 5 * speed)
            self._selected_index = target
            self._set_selected_index(new_index, 1000)

        else:
            # If we have stopped the pending animation. Now we have to do
            # a small other one to select the closest menu-item
            current_index = self._animation_start_index + \
                (self._animation_end_index - self._animation_start_index) * \
                self._animation_progression
            self._selected_index = current_index
            target_index = int(current_index)
            self._set_selected_index(target_index, 1000)

        return False

    def _on_motion_event(self, actor, event):
        """motion-event handler"""
        # threshold in pixels = the minimum distance we have to move before we
        # consider a motion has started
        motion_threshold = 10

        self._motion_buffer.compute_from_start(event)
        if self._motion_buffer.distance_from_start > motion_threshold:
            self._motion_buffer.take_new_motion_event(event)
            self._event_mode = self.MODE_MOTION
            target = self._selected_index - \
                self._motion_buffer.dy_from_start / \
                self._items[0].behaviour.path_length * len(self._items)
            self._display_items_at_target(target)

        return False

    def _on_scroll_event(self, actor, event):
        """scroll-event handler (mouse's wheel)"""
        self.active = True

        if event.direction == clutter.SCROLL_DOWN:
            self.scroll_down(duration=150)
        else:
            self.scroll_up(duration=150)

        return False
示例#2
0
class GridMenu(Base, clutter.Group):
    """
    GridMenu widget.

    A core widget to handle MenuItem in a grid with a cursor.
    This widget provides all the necessary logic to move items and the cursor.
    """
    __gsignals__ = {
        'activated': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
        'moved': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
        'selected': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
    }

    MODE_NONE = 0
    MODE_SELECT = 1
    MODE_SEEK = 2

    def __init__(self, x=0, y=0, item_width=0.2, item_height=0.1):
        Base.__init__(self)
        clutter.Group.__init__(self)

        self.motion_duration = 100  # Default duration of animations ms.
        self.cursor_below = True  # Is the cursor below items?
        self._active = None
        self._is_vertical = True
        self.items = []

        # Items dimensions variable: relative, absolute, center
        self._item_width = item_width
        self._item_height = item_height
        self._item_width_abs = self.get_abs_x(item_width)
        self._item_height_abs = self.get_abs_y(item_height)
        self._dx = int(self._item_width_abs / 2)
        self._dy = int(self._item_height_abs / 2)

        # Default cursor's index.
        self._selected_index = 0

        # Grid dimensions: real, visible.
        self.items_per_row = 10
        self.items_per_col = 10
        self._visible_rows = 3
        self._visible_cols = 5

        # The moving_group is a Clutter group containing all the items.
        self._moving_group_x = 0
        self._moving_group_y = 0
        self._moving_group = clutter.Group()
        self.add(self._moving_group)

        # The moving_group is translated using a `BehaviourPath`.
        self._moving_group_timeline = clutter.Timeline(200)
        moving_group_alpha = clutter.Alpha(self._moving_group_timeline,
                                           clutter.EASE_IN_OUT_SINE)
        moving_group_path = clutter.Path()
        self._moving_group_behaviour = clutter.BehaviourPath(
            moving_group_alpha, moving_group_path)
        self._moving_group_behaviour.apply(self._moving_group)

        # The cursor is an Actor that can be added and moved on the menu.
        # The cusor is always located in the visible (clipped) area of the menu.
        self._cursor_x = 0
        self._cursor_y = 0
        self._cursor = None
        self._cursor_timeline = clutter.Timeline(200)
        cursor_alpha = clutter.Alpha(self._cursor_timeline,
                                     clutter.EASE_IN_SINE)
        cursor_path = clutter.Path()
        self._cursor_behaviour = clutter.BehaviourPath(cursor_alpha,
                                                       cursor_path)

        # A MotionBuffer is used to compute useful information about the
        # cursor's motion. It's used when moving the cursor with a pointer.
        self._motion_buffer = MotionBuffer()
        self._event_mode = self.MODE_NONE
        self._motion_handler = 0
        self._seek_step_x = 0
        self._seek_step_y = 0
        gobject.timeout_add(200, self._internal_timer_callback)

        #XXX: Samuel Buffet
        # This rectangle is used to grab events as it turns out that their
        # might be a bug in clutter 0.8 or python-clutter 0.8.
        # It may be avoided with next release of clutter.
        self._event_rect = clutter.Rectangle()
        self._event_rect.set_opacity(0)
        self.add(self._event_rect)
        self._event_rect.set_reactive(True)
        self._event_rect.connect('button-press-event',
                                 self._on_button_press_event)
        self._event_rect.connect('button-release-event',
                                 self._on_button_release_event)
        self._event_rect.connect('scroll-event', self._on_scroll_event)

        self.set_position(self.get_abs_x(x), self.get_abs_y(y))

    @property
    def count(self):
        """Return the number of items."""
        return len(self.items)

    @property
    def on_top(self):
        """Return True if the selected item is currently on the top."""
        selected_row = self._index_to_xy(self._selected_index)[1]
        if selected_row == 0:
            return True
        else:
            return False

    @property
    def on_bottom(self):
        """Return True if the selected item is currently on the bottom."""
        selected_row = self._index_to_xy(self._selected_index)[1]
        if self._is_vertical:
            end_row = self._index_to_xy(self.count - 1)[1]
            if selected_row == end_row:
                return True
            else:
                return False
        else:
            if selected_row == self.items_per_col - 1:
                return True
            else:
                return False

    @property
    def on_left(self):
        """Return True if the selected item is currently on the left."""
        selected_col = self._index_to_xy(self._selected_index)[0]
        if selected_col == 0:
            return True
        else:
            return False

    @property
    def on_right(self):
        """Return True if the selected item is currently on the right."""
        selected_col = self._index_to_xy(self._selected_index)[0]
        if not self._is_vertical:
            end_col = self._index_to_xy(self.count - 1)[0]
            if selected_col == end_col:
                return True
            else:
                return False
        else:
            if selected_col == self.items_per_row - 1:
                return True
            else:
                return False

    @property
    def selected_item(self):
        """Return the selected MenuItem."""
        if self.count == 0:
            return None
        else:
            return self.items[self._selected_index]

    @property
    def selected_userdata(self):
        """Return userdata of the MenuItem."""
        item = self.selected_item
        if item is None:
            return None
        else:
            return item.userdata

    def _get_active(self):
        """Active property getter."""
        return self._active

    def _set_active(self, boolean):
        """Active property setter."""
        if self._active == boolean:
            return

        self._active = boolean
        if boolean:
            if self._cursor is not None:
                self._cursor.show()
            if self.selected_item is not None:
                self.selected_item.animate_in()
            self.emit('activated')
            self.set_opacity(255)
        else:
            if self._cursor is not None:
                self._cursor.hide()
            if self.selected_item is not None:
                self.selected_item.animate_out()
            self.set_opacity(128)

    active = property(_get_active, _set_active)

    def _get_horizontal(self):
        """horizontal property getter."""
        return not self._is_vertical

    def _set_horizontal(self, boolean):
        """horizontal property setter."""
        self._is_vertical = not boolean

    horizontal = property(_get_horizontal, _set_horizontal)

    def _get_vertical(self):
        """vertical property getter."""
        return self._is_vertical

    def _set_vertical(self, boolean):
        """vertical property setter."""
        self._is_vertical = boolean

    vertical = property(_get_vertical, _set_vertical)

    def _get_selected_index(self):
        """selected_index property getter."""
        return self._selected_index

    def _set_selected_index(self, index, duration=None):
        """selected_index property setter."""
        # Xc, Yc : coordinates of the menu's cursor on the array of items.
        # xc, yc : coordinates of the menu's cursor relative to the menu.
        # xm, ym : coordinates of the moving_group relative to the menu.
        # Xc = xc - xm
        # Yc = yc - ym

        if self._selected_index == index or \
           index < 0 or \
           index > self.count - 1 or \
           self._moving_group_timeline.is_playing() or \
           self._cursor_timeline.is_playing():
            return

        # Start select/unselect animations on both items.
        self.items[self._selected_index].animate_out()
        self.items[index].animate_in()

        # Get the cursor's coordinate on the array.
        # /!\ Those coordinates are NOT pixels but refer to the array of items.
        (Xc, Yc) = self._index_to_xy(index)

        xm = self._moving_group_x
        ym = self._moving_group_y

        xc = Xc + xm
        yc = Yc + ym

        # If the targeted cursor's position is on the last visible column then
        # the moving_group is translated by -1 on the x axis and the translation
        # of the cursor is reduce by 1 to stay on the column before the last
        # one. This is not done if the last column has been selected.
        if xc == self.visible_cols - 1 and \
            xm > self.visible_cols -self.items_per_row:
            xc -= 1
            xm -= 1

        # If the targeted cursor's position is on the first visible column then
        # the moving_group is translated by +1 on the x axis and the translation
        # of the cursor is raised by 1 to stay on the column after the first
        # one. This is not done if the first column has been selected.
        if xc == 0 and xm < 0:
            xc += 1
            xm += 1

        # If the targeted cursor's position is on the last visible row then
        # the moving_group is translated by -1 on the y axis and the translation
        # of the cursor is reduce by 1 to stay on the row before the last
        # one. This is not done if the last row has been selected.
        if yc == self.visible_rows - 1 and \
            ym > self.visible_rows -self.items_per_col:
            yc -= 1
            ym -= 1

        # If the targeted cursor's position is on the first visible row then
        # the moving_group is translated by +1 on the y axis and the translation
        # of the cursor is raised by 1 to stay on the row after the first
        # one. This is not done if the last row has been selected.
        if yc == 0 and ym < 0:
            yc += 1
            ym += 1

        if duration is None:
            duration = self.motion_duration

        self._move_cursor(xc, yc, duration)
        self._move_moving_group(xm, ym, duration)
        self._selected_index = index

        self.emit('moved')

    selected_index = property(_get_selected_index, _set_selected_index)

    def _get_visible_rows(self):
        """visible_rows property getter."""
        return self._visible_rows

    def _set_visible_rows(self, visible_rows):
        """visible_rows property setter."""
        self._visible_rows = visible_rows
        self._clip()

    visible_rows = property(_get_visible_rows, _set_visible_rows)

    def _get_visible_cols(self):
        """visible_cols property getter."""
        return self._visible_cols

    def _set_visible_cols(self, visible_cols):
        """visible_cols property setter."""
        self._visible_cols = visible_cols
        self._clip()

    visible_cols = property(_get_visible_cols, _set_visible_cols)

    def _get_cursor(self):
        """cursor property getter."""
        return self._cursor

    def _set_cursor(self, cursor):
        """cursor property setter."""
        if self._cursor is not None:
            self.remove(self._cursor)

        self._cursor = cursor

        if self._cursor is not None:
            self.add(self._cursor)
            if self._active:
                self._cursor.show()
            else:
                self._cursor.hide()

            if self.cursor_below:
                self._cursor.lower_bottom()
            else:
                self._cursor.raise_top()

            self._cursor.set_size(int(self._item_width_abs),
                                  int(self._item_height_abs))
            self._cursor.set_anchor_point(self._dx, self._dy)
            self._cursor.set_position(self._dx, self._dy)

            self._cursor_behaviour.apply(self._cursor)

    cursor = property(_get_cursor, _set_cursor)

    def _clip(self):
        """Updates the clipping region."""
        self.set_clip(0, 0, self._visible_cols * self._item_width_abs,
                      self._visible_rows * self._item_height_abs)

        self._event_rect.set_size(self._visible_cols * self._item_width_abs,
                                  self._visible_rows * self._item_height_abs)

    def stop_animation(self):
        """Stops the timelines driving menu animation."""
        self._moving_group_timeline.stop()
        self._cursor_timeline.stop()

    def raw_add_item(self, item):
        """A method to add an item in the menu."""
        self._moving_group.add(item)
        self.items.append(item)

        (x, y) = self._index_to_xy(self.count - 1)

        item.move_anchor_point(self._dx, self._dy)
        item.set_position(x * self._item_width_abs + self._dx,
                          y * self._item_height_abs + self._dy)

        if self._is_vertical:
            self.items_per_col = y + 1
        else:
            self.items_per_row = x + 1

        if self.cursor_below:
            item.raise_top()
        else:
            item.lower_bottom()

    def _index_to_xy(self, index):
        """Return the coordinates of an element associated to its index."""
        if self._is_vertical:
            r = index / float(self.items_per_row)
            y = int(math.modf(r)[1])
            x = int(index - y * self.items_per_row)
        else:
            r = index / float(self.items_per_col)
            x = int(math.modf(r)[1])
            y = int(index - x * self.items_per_col)

        return (x, y)

    def _move_moving_group(self, x, y, duration):
        """Moves the moving_group to x, y coordinates."""
        if (x, y) == (self._moving_group_x, self._moving_group_y):
            return

        path = clutter.Path()
        path.add_move_to(self._moving_group_x * self._item_width_abs,
                         self._moving_group_y * self._item_height_abs)
        path.add_line_to(x * self._item_width_abs, y * self._item_height_abs)
        self._moving_group_behaviour.set_path(path)

        self._moving_group_x, self._moving_group_y = x, y
        self._moving_group_timeline.set_duration(duration)
        self._moving_group_timeline.start()

    def _move_cursor(self, x, y, duration):
        """
        Moves the cursor to x, y coordinates.
        The motion is applied to the center of the cursor.
        """
        if (x, y) == (self._cursor_x, self._cursor_y):
            return

        path = clutter.Path()
        path.add_move_to(self._cursor_x * self._item_width_abs + self._dx,
                         self._cursor_y * self._item_height_abs + self._dy)
        path.add_line_to(x * self._item_width_abs + self._dx,
                         y * self._item_height_abs + self._dy)
        self._cursor_behaviour.set_path(path)

        self._cursor_x, self._cursor_y = x, y
        self._cursor_timeline.set_duration(duration)
        self._cursor_timeline.start()

    def up(self):
        """Move the menu's cursor up changing the selected_index property."""
        if not self.on_top:
            if self._is_vertical:
                self.selected_index -= self.items_per_row
            else:
                self.selected_index -= 1

    def down(self):
        """Move the menu's cursor down changing the selected_index property."""
        if not self.on_bottom:
            if self._is_vertical:
                self.selected_index += self.items_per_row
            else:
                self.selected_index += 1

    def right(self):
        """Move the menu's cursor right changing the selected_index property."""
        if not self.on_right:
            if self._is_vertical:
                self.selected_index += 1
            else:
                self.selected_index += self.items_per_col

    def left(self):
        """Move the menu's cursor left changing the selected_index property."""
        if not self.on_left:
            if self._is_vertical:
                self.selected_index -= 1
            else:
                self.selected_index -= self.items_per_col

    def _internal_timer_callback(self):
        """
        This callback is used to move the cursor if the SEEK mode is activated.
        """
        if self._event_mode == self.MODE_SEEK:
            if self._seek_step_x == 1:
                self.right()
            if self._seek_step_x == -1:
                self.left()
            if self._seek_step_y == 1:
                self.down()
            if self._seek_step_y == -1:
                self.up()

        return True

    def _on_button_press_event(self, actor, event):
        """button-press-event handler."""
        clutter.grab_pointer(self._event_rect)
        if not self._event_rect.handler_is_connected(self._motion_handler):
            self._motion_handler = self._event_rect.connect(
                'motion-event', self._on_motion_event)

        (x_menu, y_menu) = self.get_transformed_position()
        (x_moving_group, y_moving_group) = self._moving_group.get_position()

        # Events coordinates are relative to the stage.
        # So they need to be computed relatively to the moving group.
        x = event.x - x_menu - x_moving_group
        y = event.y - y_menu - y_moving_group

        x_grid = int(x / self._item_width_abs)
        y_grid = int(y / self._item_height_abs)

        if self._is_vertical:
            new_index = y_grid * self.items_per_row + x_grid
        else:
            new_index = x_grid * self.items_per_col + y_grid

        (delta_x, delta_y) = self._index_to_xy(self._selected_index)

        delta_x -= x_grid
        delta_y -= y_grid

        # Correction factor due to the fact that items are not necessary square,
        # but most probably rectangles. So the distance in the grid coordinates
        # must be corrected by a factor to have a real distance in pixels on the
        # screen.
        correction = float(self._item_width_abs) / float(self._item_height_abs)
        correction *= correction
        distance = math.sqrt(delta_x**2 * correction + delta_y**2)

        # Computation of the duration of animations, scaling grid steps to ms.
        duration = int(distance * 50)

        if self.selected_index == new_index and \
            self.active and \
            not self._cursor_timeline.is_playing() and \
            not self._moving_group_timeline.is_playing():
            self._event_mode = self.MODE_SELECT
        else:
            self.active = True
            self._event_mode = self.MODE_NONE

        self._set_selected_index(new_index, duration)

        self._motion_buffer.start(event)

        return False

    def _on_button_release_event(self, actor, event):
        """button-release-event handler."""
        clutter.ungrab_pointer()
        if self._event_rect.handler_is_connected(self._motion_handler):
            self._event_rect.disconnect_by_func(self._on_motion_event)

        if self._event_mode == self.MODE_SELECT:
            self.emit('selected')

        self._event_mode = self.MODE_NONE

        return True

    def _on_motion_event(self, actor, event):
        """motion-event handler"""
        # threshold in pixels = the minimum distance we have to move before we
        # consider a motion has started
        motion_threshold = 20

        self._seek_step_x = 0
        self._seek_step_y = 0
        self._motion_buffer.compute_from_start(event)
        self._motion_buffer.compute_from_last_motion_event(event)

        if self._motion_buffer.distance_from_start > motion_threshold:
            self._event_mode = self.MODE_SEEK
            self._motion_buffer.take_new_motion_event(event)
            dx = self._motion_buffer.dx_from_last_motion_event
            dy = self._motion_buffer.dy_from_last_motion_event

            if math.fabs(dx) > math.fabs(dy):
                self._seek_step_x = dx > 0 and 1 or -1
            else:
                self._seek_step_y = dy > 0 and 1 or -1

        return False

    def _on_scroll_event(self, actor, event):
        """scroll-event handler (mouse's wheel)."""
        if not self.active:
            self.active = True
            return

        if event.direction == clutter.SCROLL_DOWN:
            self.down()
        else:
            self.up()

        return False
示例#3
0
class ScrollArea(Base, clutter.Group):
    """Wrapper of a clutter Group that allows for scrolling. ScrollArea
    modifies the width of the content and it assumes that the content uses
    percent modification (read: not default clutter objects)."""
    __gsignals__ = {
        'activated': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
        'moving': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
    }

    MODE_SELECTION = 0
    MODE_MOTION = 1
    MODE_STOP = 2
    STEP_SIZE_PERCENT = 0.04

    def __init__(self, x, y, width, height, content):
        Base.__init__(self)
        clutter.Group.__init__(self)

        self._motion_buffer = MotionBuffer()
        self._offset = 0  # Drives the content motion.
        self._offset_max = 0  # Maximum value of offset (equal on bottom).
        self._old_offset = 0  # Stores the old value of offset on motions.
        self._motion_handler = 0
        self._active = None

        self.step_size = self.get_abs_y(self.STEP_SIZE_PERCENT)

        # Allowed area for the widget's scrolling content.
        self.area_width = self.get_abs_x(width)
        self.area_height = self.get_abs_y(height)

        # Create content position indicator
        self.indicator = ListIndicator(3 * width / 4, height, 0.2, 0.045,
                                       ListIndicator.VERTICAL)
        self.indicator.hide_position()
        self.indicator.set_maximum(2)
        self.add(self.indicator)

        # A clipped Group to receive the content.
        self._fixed_group = clutter.Group()
        self._fixed_group.set_clip(0, 0, self.area_width, self.area_height)
        self.add(self._fixed_group)
        self.content = None

        self._motion_timeline = clutter.Timeline(500)
        self._motion_timeline.connect('completed',
                                      self._motion_timeline_callback, None)
        self._motion_alpha = clutter.Alpha(self._motion_timeline,
                                           clutter.EASE_OUT_SINE)
        self._motion_behaviour = LoopedPathBehaviour(self._motion_alpha)

        self.set_content(content)

        self.active = None

        # Preparation to pointer events handling.
        self.set_reactive(True)
        self.connect('button-press-event', self._on_button_press_event)
        self.connect('button-release-event', self._on_button_release_event)
        self.connect('scroll-event', self._on_scroll_event)

        self.set_position(self.get_abs_x(x), self.get_abs_y(y))

    @property
    def on_top(self):
        """True if we're on top."""
        return self._offset == 0

    @property
    def on_bottom(self):
        """True if we're on bottom."""
        return self._offset == self._offset_max

    def _get_active(self):
        """Active property getter."""
        return self._active

    def _set_active(self, boolean):
        """Active property setter."""
        if self._active == boolean:
            return

        self._active = boolean
        if boolean:
            # Show indicator if there is need for scrolling.
            if self._offset_max >= 0:
                self.indicator.show()

            self.set_opacity(255)
            self.emit('activated')
        else:
            self.indicator.hide()
            self.set_opacity(128)

    active = property(_get_active, _set_active)

    def _get_offset(self):
        """Get current offset value."""
        return self._offset

    def _set_offset(self, integer):
        """Set current offset value."""
        if self._offset == integer:
            return

        self._offset = integer

        if self._offset < 0:
            self._offset = 0
        elif self._offset > self._offset_max:
            self._offset = self._offset_max

        self.content.set_position(0, -self._offset)

        # Indicator updates.
        if self.on_top:
            self.indicator.set_current(1)
        elif self.on_bottom:
            self.indicator.set_current(2)

    offset = property(_get_offset, _set_offset)

    def set_content(self, content):
        """Set content into scroll area."""
        if self.content is not None:
            self._fixed_group.remove(self.content)
            self._motion_behaviour.remove(self.content)

        self.content = content
        self._fixed_group.add(content)

        self._offset_max = self.content.get_height() - self.area_height

        self._motion_behaviour.apply(self.content)

    def stop_animation(self):
        """Stops the timeline driving animation."""
        self._motion_timeline.stop()

    def scroll_to_top(self):
        """Scroll content back to top."""
        self.offset = 0

    def scroll_to_bottom(self):
        """Scroll content as much as possible."""
        self.offset = self._offset_max

    def scroll_up(self):
        """Scroll up by one step size."""
        self.offset -= self.step_size

    def scroll_down(self):
        """Scroll down by one step size."""
        self.offset += self.step_size

    def scroll_page_up(self):
        """Scroll up by one page. Page is a scroll area height."""
        self.offset -= self.area_height

    def scroll_page_down(self):
        self.offset += self.area_height

    def _update_motion_behaviour(self, target):
        """Preparation of looped behaviour applied to the content."""
        self._motion_behaviour.start_knot = (0.0, -self.offset)
        self._motion_behaviour.end_knot = (0.0, -target)
        self._motion_behaviour.start_index = 0.0
        # Need to set the end index to 0.9999. Indeed the LoopedPathBehaviour
        # uses an index in [0, 1[. So index = 1 is equivalent to index = 0, the
        # Actor will the be placed on the start_knot.
        self._motion_behaviour.end_index = 0.9999

    def _on_button_press_event(self, actor, event):
        """button-press-event handler."""
        clutter.grab_pointer(self)
        if not self.handler_is_connected(self._motion_handler):
            self._motion_handler = self.connect('motion-event',
                                                self._on_motion_event)

        if self._motion_timeline.is_playing():
            # A click with an animation pending should stop the animation.
            self._motion_timeline.stop()

            # Go to MODE_STOP to handle correctly next button-release event.
            self._event_mode = self.MODE_STOP
            self.offset = -self.content.get_y()
        else:
            # No animation pending so we're going to do nothing or to move
            # all the content.
            self._old_offset = self.offset
            self._motion_buffer.start(event)
            self._event_mode = self.MODE_SELECTION

        return False

    def _on_button_release_event(self, actor, event):
        """button-release-event handler."""
        clutter.ungrab_pointer()
        if self.handler_is_connected(self._motion_handler):
            self.disconnect_by_func(self._on_motion_event)

        self._motion_buffer.compute_from_last_motion_event(event)

        if not self.active:
            self.active = True
            return

        if self._event_mode == self.MODE_MOTION:
            speed = self._motion_buffer.speed_y_from_last_motion_event

            # Calculation of the new target according to vertical speed.
            target = self.offset - speed * 200

            if target < 0:
                target = 0
            elif target > self._offset_max:
                target = self._offset_max

            self._update_motion_behaviour(target)
            self._motion_timeline.start()

        return False

    def _on_motion_event(self, actor, event):
        """motion-event handler."""
        # Minimum distance we to move before we consider a motion has started.
        motion_threshold = 10

        self._motion_buffer.compute_from_start(event)
        if self._motion_buffer.distance_from_start > motion_threshold:
            self._motion_buffer.take_new_motion_event(event)
            self._event_mode = self.MODE_MOTION
            self.offset = self._old_offset - self._motion_buffer.dy_from_start

        return False

    def _on_scroll_event(self, actor, event):
        """scroll-event handler (mouse's wheel)."""
        if not self.active:
            self.active = True
            return

        # Do not scroll if there is no need.
        if self._offset_max < 0:
            return False

        if event.direction == clutter.SCROLL_DOWN:
            self.scroll_down()
        else:
            self.scroll_up()

        self.emit('moving')

        return False

    def _motion_timeline_callback(self, timeline, screen):
        """Code executed when the animation is finished."""
        self.offset = -self.content.get_y()
class MotionBufferTest(EntertainerTest):
    """Test for entertainerlib.gui.widgets.motion_buffer"""

    def setUp(self):
        """Set up the test."""
        EntertainerTest.setUp(self)

        self.buffer = MotionBuffer()

    def tearDown(self):
        """Clean up after the test."""
        EntertainerTest.tearDown(self)

    def test_create(self):
        """Test correct MotionBuffer initialization."""
        self.assertTrue(isinstance(self.buffer, MotionBuffer))

    def test_computations_from_start(self):
        """Test all values on a 3 events motion, computed from start."""
        self.buffer.start(self._create_first_event())
        self.buffer.take_new_motion_event(self._create_second_event())
        self.buffer.compute_from_start(self._create_third_event())

        self.assertEqual(self.buffer.dt_from_start, 2)
        self.assertEqual(self.buffer.dx_from_start, 10)
        self.assertEqual(self.buffer.dy_from_start, 10)
        self.assertAlmostEqual(self.buffer.distance_from_start, 14.142135624)

    def test_computations_from_last_event(self):
        """Test all values on a 3 events motion, computed from last event."""
        self.buffer.start(self._create_first_event())
        self.buffer.take_new_motion_event(self._create_second_event())
        self.buffer.compute_from_last_motion_event(self._create_third_event())

        self.assertEqual(self.buffer.dt_from_last_motion_event, 1)
        self.assertEqual(self.buffer.dx_from_last_motion_event, 10)
        self.assertEqual(self.buffer.dy_from_last_motion_event, 0)
        self.assertEqual(self.buffer.distance_from_last_motion_event, 10.0)
        self.assertEqual(self.buffer.speed_x_from_last_motion_event, 10.0)
        self.assertEqual(self.buffer.speed_y_from_last_motion_event, 0.0)
        self.assertEqual(self.buffer.speed_from_last_motion_event, 10.0)
        self.assertAlmostEqual(self.buffer.dt_ema, 0.3333333333)
        self.assertAlmostEqual(self.buffer.dx_ema, 3.3333333333)
        self.assertAlmostEqual(self.buffer.dy_ema, 0.0)
        self.assertAlmostEqual(self.buffer.distance_ema, 3.3333333333)
        self.assertAlmostEqual(self.buffer.speed_x_ema, 3.3333333333)
        self.assertAlmostEqual(self.buffer.speed_y_ema, 0.0)
        self.assertAlmostEqual(self.buffer.speed_ema, 3.3333333333)

    def _create_first_event(self):
        """Create a virtual pointer event."""
        event = MockPointerEvent()
        event.x = 100
        event.y = 100
        event.time = 0
        return event

    def _create_second_event(self):
        """Create a virtual pointer event."""
        event = MockPointerEvent()
        event.x = 100
        event.y = 110
        event.time = 1
        return event

    def _create_third_event(self):
        """Create a virtual pointer event."""
        event = MockPointerEvent()
        event.x = 110
        event.y = 110
        event.time = 2
        return event
示例#5
0
class GridMenu(Base, clutter.Group):
    """
    GridMenu widget.

    A core widget to handle MenuItem in a grid with a cursor.
    This widget provides all the necessary logic to move items and the cursor.
    """
    __gsignals__ = {
        'activated' : ( gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, () ),
        'moved' : ( gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, () ),
        'selected' : ( gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, () ),
    }

    MODE_NONE = 0
    MODE_SELECT = 1
    MODE_SEEK = 2

    def __init__(self, x=0, y=0, item_width=0.2, item_height=0.1):
        Base.__init__(self)
        clutter.Group.__init__(self)

        self.motion_duration = 100          # Default duration of animations ms.
        self.cursor_below = True            # Is the cursor below items?
        self._active = None
        self._is_vertical = True
        self.items = []

        # Items dimensions variable: relative, absolute, center
        self._item_width = item_width
        self._item_height = item_height
        self._item_width_abs = self.get_abs_x(item_width)
        self._item_height_abs = self.get_abs_y(item_height)
        self._dx = int(self._item_width_abs / 2)
        self._dy = int(self._item_height_abs / 2)

        # Default cursor's index.
        self._selected_index = 0

        # Grid dimensions: real, visible.
        self.items_per_row = 10
        self.items_per_col = 10
        self._visible_rows = 3
        self._visible_cols = 5

        # The moving_group is a Clutter group containing all the items.
        self._moving_group_x = 0
        self._moving_group_y = 0
        self._moving_group = clutter.Group()
        self.add(self._moving_group)

        # The moving_group is translated using a `BehaviourPath`.
        self._moving_group_timeline = clutter.Timeline(200)
        moving_group_alpha = clutter.Alpha(self._moving_group_timeline,
            clutter.EASE_IN_OUT_SINE)
        moving_group_path = clutter.Path()
        self._moving_group_behaviour = clutter.BehaviourPath(moving_group_alpha,
            moving_group_path)
        self._moving_group_behaviour.apply(self._moving_group)

        # The cursor is an Actor that can be added and moved on the menu.
        # The cusor is always located in the visible (clipped) area of the menu.
        self._cursor_x = 0
        self._cursor_y = 0
        self._cursor = None
        self._cursor_timeline = clutter.Timeline(200)
        cursor_alpha = clutter.Alpha(self._cursor_timeline,
            clutter.EASE_IN_SINE)
        cursor_path = clutter.Path()
        self._cursor_behaviour = clutter.BehaviourPath(cursor_alpha,
            cursor_path)

        # A MotionBuffer is used to compute useful information about the
        # cursor's motion. It's used when moving the cursor with a pointer.
        self._motion_buffer = MotionBuffer()
        self._event_mode = self.MODE_NONE
        self._motion_handler = 0
        self._seek_step_x = 0
        self._seek_step_y = 0
        gobject.timeout_add(200, self._internal_timer_callback)

        #XXX: Samuel Buffet
        # This rectangle is used to grab events as it turns out that their
        # might be a bug in clutter 0.8 or python-clutter 0.8.
        # It may be avoided with next release of clutter.
        self._event_rect = clutter.Rectangle()
        self._event_rect.set_opacity(0)
        self.add(self._event_rect)
        self._event_rect.set_reactive(True)
        self._event_rect.connect('button-press-event',
            self._on_button_press_event)
        self._event_rect.connect('button-release-event',
            self._on_button_release_event)
        self._event_rect.connect('scroll-event', self._on_scroll_event)

        self.set_position(self.get_abs_x(x), self.get_abs_y(y))

    @property
    def count(self):
        """Return the number of items."""
        return len(self.items)

    @property
    def on_top(self):
        """Return True if the selected item is currently on the top."""
        selected_row = self._index_to_xy(self._selected_index)[1]
        if selected_row == 0:
            return True
        else:
            return False

    @property
    def on_bottom(self):
        """Return True if the selected item is currently on the bottom."""
        selected_row = self._index_to_xy(self._selected_index)[1]
        if self._is_vertical:
            end_row = self._index_to_xy(self.count - 1)[1]
            if selected_row == end_row:
                return True
            else:
                return False
        else:
            if selected_row == self.items_per_col - 1:
                return True
            else:
                return False

    @property
    def on_left(self):
        """Return True if the selected item is currently on the left."""
        selected_col = self._index_to_xy(self._selected_index)[0]
        if selected_col == 0:
            return True
        else:
            return False

    @property
    def on_right(self):
        """Return True if the selected item is currently on the right."""
        selected_col = self._index_to_xy(self._selected_index)[0]
        if not self._is_vertical:
            end_col = self._index_to_xy(self.count - 1)[0]
            if selected_col == end_col:
                return True
            else:
                return False
        else:
            if selected_col == self.items_per_row - 1:
                return True
            else:
                return False

    @property
    def selected_item(self):
        """Return the selected MenuItem."""
        if self.count == 0:
            return None
        else:
            return self.items[self._selected_index]

    @property
    def selected_userdata(self):
        """Return userdata of the MenuItem."""
        item = self.selected_item
        if item is None:
            return None
        else:
            return item.userdata

    def _get_active(self):
        """Active property getter."""
        return self._active

    def _set_active(self, boolean):
        """Active property setter."""
        if self._active == boolean:
            return

        self._active = boolean
        if boolean:
            if self._cursor is not None:
                self._cursor.show()
            if  self.selected_item is not None:
                self.selected_item.animate_in()
            self.emit('activated')
            self.set_opacity(255)
        else:
            if self._cursor is not None:
                self._cursor.hide()
            if  self.selected_item is not None:
                self.selected_item.animate_out()
            self.set_opacity(128)

    active = property(_get_active, _set_active)

    def _get_horizontal(self):
        """horizontal property getter."""
        return not self._is_vertical

    def _set_horizontal(self, boolean):
        """horizontal property setter."""
        self._is_vertical = not boolean

    horizontal = property(_get_horizontal, _set_horizontal)

    def _get_vertical(self):
        """vertical property getter."""
        return self._is_vertical

    def _set_vertical(self, boolean):
        """vertical property setter."""
        self._is_vertical = boolean

    vertical = property(_get_vertical, _set_vertical)

    def _get_selected_index(self):
        """selected_index property getter."""
        return self._selected_index

    def _set_selected_index(self, index, duration=None):
        """selected_index property setter."""
        # Xc, Yc : coordinates of the menu's cursor on the array of items.
        # xc, yc : coordinates of the menu's cursor relative to the menu.
        # xm, ym : coordinates of the moving_group relative to the menu.
        # Xc = xc - xm
        # Yc = yc - ym

        if self._selected_index == index or \
           index < 0 or \
           index > self.count - 1 or \
           self._moving_group_timeline.is_playing() or \
           self._cursor_timeline.is_playing():
            return

        # Start select/unselect animations on both items.
        self.items[self._selected_index].animate_out()
        self.items[index].animate_in()

        # Get the cursor's coordinate on the array.
        # /!\ Those coordinates are NOT pixels but refer to the array of items.
        (Xc, Yc) = self._index_to_xy(index)

        xm = self._moving_group_x
        ym = self._moving_group_y

        xc = Xc + xm
        yc = Yc + ym

        # If the targeted cursor's position is on the last visible column then
        # the moving_group is translated by -1 on the x axis and the translation
        # of the cursor is reduce by 1 to stay on the column before the last
        # one. This is not done if the last column has been selected.
        if xc == self.visible_cols - 1 and \
            xm > self.visible_cols -self.items_per_row:
            xc -= 1
            xm -= 1

        # If the targeted cursor's position is on the first visible column then
        # the moving_group is translated by +1 on the x axis and the translation
        # of the cursor is raised by 1 to stay on the column after the first
        # one. This is not done if the first column has been selected.
        if xc == 0 and xm < 0:
            xc += 1
            xm += 1

        # If the targeted cursor's position is on the last visible row then
        # the moving_group is translated by -1 on the y axis and the translation
        # of the cursor is reduce by 1 to stay on the row before the last
        # one. This is not done if the last row has been selected.
        if yc == self.visible_rows - 1 and \
            ym > self.visible_rows -self.items_per_col:
            yc -= 1
            ym -= 1

        # If the targeted cursor's position is on the first visible row then
        # the moving_group is translated by +1 on the y axis and the translation
        # of the cursor is raised by 1 to stay on the row after the first
        # one. This is not done if the last row has been selected.
        if yc == 0 and ym < 0:
            yc += 1
            ym += 1

        if duration is None:
            duration = self.motion_duration

        self._move_cursor(xc, yc, duration)
        self._move_moving_group(xm, ym, duration)
        self._selected_index = index

        self.emit('moved')

    selected_index = property(_get_selected_index, _set_selected_index)

    def _get_visible_rows(self):
        """visible_rows property getter."""
        return self._visible_rows

    def _set_visible_rows(self, visible_rows):
        """visible_rows property setter."""
        self._visible_rows = visible_rows
        self._clip()

    visible_rows = property(_get_visible_rows, _set_visible_rows)

    def _get_visible_cols(self):
        """visible_cols property getter."""
        return self._visible_cols

    def _set_visible_cols(self, visible_cols):
        """visible_cols property setter."""
        self._visible_cols = visible_cols
        self._clip()

    visible_cols = property(_get_visible_cols, _set_visible_cols)

    def _get_cursor(self):
        """cursor property getter."""
        return self._cursor

    def _set_cursor(self, cursor):
        """cursor property setter."""
        if self._cursor is not None:
            self.remove(self._cursor)

        self._cursor = cursor

        if self._cursor is not None:
            self.add(self._cursor)
            if self._active:
                self._cursor.show()
            else:
                self._cursor.hide()

            if self.cursor_below:
                self._cursor.lower_bottom()
            else:
                self._cursor.raise_top()

            self._cursor.set_size(int(self._item_width_abs),
                int(self._item_height_abs))
            self._cursor.set_anchor_point(self._dx, self._dy)
            self._cursor.set_position(self._dx, self._dy)

            self._cursor_behaviour.apply(self._cursor)

    cursor = property(_get_cursor, _set_cursor)

    def _clip(self):
        """Updates the clipping region."""
        self.set_clip(0, 0, self._visible_cols * self._item_width_abs,
            self._visible_rows * self._item_height_abs)

        self._event_rect.set_size(self._visible_cols * self._item_width_abs,
            self._visible_rows * self._item_height_abs)

    def stop_animation(self):
        """Stops the timelines driving menu animation."""
        self._moving_group_timeline.stop()
        self._cursor_timeline.stop()

    def raw_add_item(self, item):
        """A method to add an item in the menu."""
        self._moving_group.add(item)
        self.items.append(item)

        (x, y) = self._index_to_xy(self.count - 1)

        item.move_anchor_point(self._dx, self._dy)
        item.set_position(x * self._item_width_abs + self._dx,
            y * self._item_height_abs + self._dy)

        if self._is_vertical:
            self.items_per_col = y + 1
        else:
            self.items_per_row = x + 1

        if self.cursor_below:
            item.raise_top()
        else:
            item.lower_bottom()

    def _index_to_xy(self, index):
        """Return the coordinates of an element associated to its index."""
        if self._is_vertical:
            r = index / float(self.items_per_row)
            y = int(math.modf(r)[1])
            x = int(index - y * self.items_per_row)
        else:
            r = index / float(self.items_per_col)
            x = int(math.modf(r)[1])
            y = int(index - x * self.items_per_col)

        return (x, y)

    def _move_moving_group(self, x, y, duration):
        """Moves the moving_group to x, y coordinates."""
        if (x, y) == (self._moving_group_x, self._moving_group_y):
            return

        path = clutter.Path()
        path.add_move_to(
            self._moving_group_x * self._item_width_abs,
            self._moving_group_y * self._item_height_abs)
        path.add_line_to(
            x * self._item_width_abs,
            y * self._item_height_abs)
        self._moving_group_behaviour.set_path(path)

        self._moving_group_x, self._moving_group_y = x, y
        self._moving_group_timeline.set_duration(duration)
        self._moving_group_timeline.start()

    def _move_cursor(self, x, y, duration):
        """
        Moves the cursor to x, y coordinates.
        The motion is applied to the center of the cursor.
        """
        if (x, y) == (self._cursor_x, self._cursor_y):
            return

        path = clutter.Path()
        path.add_move_to(
            self._cursor_x * self._item_width_abs + self._dx,
            self._cursor_y * self._item_height_abs + self._dy)
        path.add_line_to(
            x * self._item_width_abs + self._dx,
            y * self._item_height_abs + self._dy)
        self._cursor_behaviour.set_path(path)

        self._cursor_x, self._cursor_y = x, y
        self._cursor_timeline.set_duration(duration)
        self._cursor_timeline.start()

    def up(self):
        """Move the menu's cursor up changing the selected_index property."""
        if not self.on_top:
            if self._is_vertical:
                self.selected_index -= self.items_per_row
            else:
                self.selected_index -= 1

    def down(self):
        """Move the menu's cursor down changing the selected_index property."""
        if not self.on_bottom:
            if self._is_vertical:
                self.selected_index += self.items_per_row
            else:
                self.selected_index += 1

    def right(self):
        """Move the menu's cursor right changing the selected_index property."""
        if not self.on_right:
            if self._is_vertical:
                self.selected_index += 1
            else:
                self.selected_index += self.items_per_col

    def left(self):
        """Move the menu's cursor left changing the selected_index property."""
        if not self.on_left:
            if self._is_vertical:
                self.selected_index -= 1
            else:
                self.selected_index -= self.items_per_col

    def _internal_timer_callback(self):
        """
        This callback is used to move the cursor if the SEEK mode is activated.
        """
        if self._event_mode == self.MODE_SEEK:
            if self._seek_step_x == 1:
                self.right()
            if self._seek_step_x == -1:
                self.left()
            if self._seek_step_y == 1:
                self.down()
            if self._seek_step_y == -1:
                self.up()

        return True

    def _on_button_press_event(self, actor, event):
        """button-press-event handler."""
        clutter.grab_pointer(self._event_rect)
        if not self._event_rect.handler_is_connected(self._motion_handler):
            self._motion_handler = self._event_rect.connect('motion-event',
                self._on_motion_event)

        (x_menu, y_menu) = self.get_transformed_position()
        (x_moving_group, y_moving_group) = self._moving_group.get_position()

        # Events coordinates are relative to the stage.
        # So they need to be computed relatively to the moving group.
        x = event.x - x_menu - x_moving_group
        y = event.y - y_menu - y_moving_group

        x_grid = int(x / self._item_width_abs)
        y_grid = int(y / self._item_height_abs)

        if self._is_vertical:
            new_index = y_grid * self.items_per_row + x_grid
        else:
            new_index = x_grid * self.items_per_col + y_grid

        (delta_x, delta_y) = self._index_to_xy(self._selected_index)

        delta_x -= x_grid
        delta_y -= y_grid

        # Correction factor due to the fact that items are not necessary square,
        # but most probably rectangles. So the distance in the grid coordinates
        # must be corrected by a factor to have a real distance in pixels on the
        # screen.
        correction = float(self._item_width_abs) / float(self._item_height_abs)
        correction *= correction
        distance = math.sqrt(delta_x ** 2 * correction + delta_y ** 2)

        # Computation of the duration of animations, scaling grid steps to ms.
        duration = int(distance * 50)

        if self.selected_index == new_index and \
            self.active and \
            not self._cursor_timeline.is_playing() and \
            not self._moving_group_timeline.is_playing():
            self._event_mode = self.MODE_SELECT
        else:
            self.active = True
            self._event_mode = self.MODE_NONE

        self._set_selected_index(new_index, duration)

        self._motion_buffer.start(event)

        return False

    def _on_button_release_event(self, actor, event):
        """button-release-event handler."""
        clutter.ungrab_pointer()
        if self._event_rect.handler_is_connected(self._motion_handler):
            self._event_rect.disconnect_by_func(self._on_motion_event)

        if self._event_mode == self.MODE_SELECT:
            self.emit('selected')

        self._event_mode = self.MODE_NONE

        return True

    def _on_motion_event(self, actor, event):
        """motion-event handler"""
        # threshold in pixels = the minimum distance we have to move before we
        # consider a motion has started
        motion_threshold = 20

        self._seek_step_x = 0
        self._seek_step_y = 0
        self._motion_buffer.compute_from_start(event)
        self._motion_buffer.compute_from_last_motion_event(event)

        if self._motion_buffer.distance_from_start > motion_threshold:
            self._event_mode = self.MODE_SEEK
            self._motion_buffer.take_new_motion_event(event)
            dx = self._motion_buffer.dx_from_last_motion_event
            dy = self._motion_buffer.dy_from_last_motion_event

            if math.fabs(dx) > math.fabs(dy):
                self._seek_step_x = dx > 0 and 1 or -1
            else:
                self._seek_step_y = dy > 0 and 1 or -1

        return False

    def _on_scroll_event(self, actor, event):
        """scroll-event handler (mouse's wheel)."""
        if not self.active:
            self.active = True
            return

        if event.direction == clutter.SCROLL_DOWN:
            self.down()
        else:
            self.up()

        return False
class MotionBufferTest(EntertainerTest):
    """Test for entertainerlib.gui.widgets.motion_buffer"""
    def setUp(self):
        '''Set up the test.'''
        EntertainerTest.setUp(self)

        self.buffer = MotionBuffer()

    def tearDown(self):
        '''Clean up after the test.'''
        EntertainerTest.tearDown(self)

    def test_create(self):
        '''Test correct MotionBuffer initialization.'''
        self.assertTrue(isinstance(self.buffer, MotionBuffer))

    def test_computations_from_start(self):
        '''Test all values on a 3 events motion, computed from start.'''
        self.buffer.start(self._create_first_event())
        self.buffer.take_new_motion_event(self._create_second_event())
        self.buffer.compute_from_start(self._create_third_event())

        self.assertEqual(self.buffer.dt_from_start, 2)
        self.assertEqual(self.buffer.dx_from_start, 10)
        self.assertEqual(self.buffer.dy_from_start, 10)
        self.assertAlmostEqual(self.buffer.distance_from_start, 14.142135624)

    def test_computations_from_last_event(self):
        '''Test all values on a 3 events motion, computed from last event.'''
        self.buffer.start(self._create_first_event())
        self.buffer.take_new_motion_event(self._create_second_event())
        self.buffer.compute_from_last_motion_event(self._create_third_event())

        self.assertEqual(self.buffer.dt_from_last_motion_event, 1)
        self.assertEqual(self.buffer.dx_from_last_motion_event, 10)
        self.assertEqual(self.buffer.dy_from_last_motion_event, 0)
        self.assertEqual(self.buffer.distance_from_last_motion_event, 10.0)
        self.assertEqual(self.buffer.speed_x_from_last_motion_event, 10.0)
        self.assertEqual(self.buffer.speed_y_from_last_motion_event, 0.0)
        self.assertEqual(self.buffer.speed_from_last_motion_event, 10.0)
        self.assertAlmostEqual(self.buffer.dt_ema, 0.3333333333)
        self.assertAlmostEqual(self.buffer.dx_ema, 3.3333333333)
        self.assertAlmostEqual(self.buffer.dy_ema, 0.0)
        self.assertAlmostEqual(self.buffer.distance_ema, 3.3333333333)
        self.assertAlmostEqual(self.buffer.speed_x_ema, 3.3333333333)
        self.assertAlmostEqual(self.buffer.speed_y_ema, 0.0)
        self.assertAlmostEqual(self.buffer.speed_ema, 3.3333333333)

    def _create_first_event(self):
        '''Create a virtual pointer event.'''
        event = MockPointerEvent()
        event.x = 100
        event.y = 100
        event.time = 0
        return event

    def _create_second_event(self):
        '''Create a virtual pointer event.'''
        event = MockPointerEvent()
        event.x = 100
        event.y = 110
        event.time = 1
        return event

    def _create_third_event(self):
        '''Create a virtual pointer event.'''
        event = MockPointerEvent()
        event.x = 110
        event.y = 110
        event.time = 2
        return event
示例#7
0
class ScrollArea(Base, clutter.Group):
    """Wrapper of a clutter Group that allows for scrolling. ScrollArea
    modifies the width of the content and it assumes that the content uses
    percent modification (read: not default clutter objects)."""
    __gsignals__ = {
        'activated' : ( gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, () ),
        'moving' : ( gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, () ),
        }

    MODE_SELECTION = 0
    MODE_MOTION = 1
    MODE_STOP = 2
    STEP_SIZE_PERCENT = 0.04

    def __init__(self, x, y, width, height, content):
        Base.__init__(self)
        clutter.Group.__init__(self)

        self._motion_buffer = MotionBuffer()
        self._offset = 0        # Drives the content motion.
        self._offset_max = 0    # Maximum value of offset (equal on bottom).
        self._old_offset = 0    # Stores the old value of offset on motions.
        self._motion_handler = 0
        self._active = None

        self.step_size = self.get_abs_y(self.STEP_SIZE_PERCENT)

        # Allowed area for the widget's scrolling content.
        self.area_width = self.get_abs_x(width)
        self.area_height = self.get_abs_y(height)

        # Create content position indicator
        self.indicator = ListIndicator(3 * width / 4, height, 0.2, 0.045,
            ListIndicator.VERTICAL)
        self.indicator.hide_position()
        self.indicator.set_maximum(2)
        self.add(self.indicator)

        # A clipped Group to receive the content.
        self._fixed_group = clutter.Group()
        self._fixed_group.set_clip(0, 0, self.area_width, self.area_height)
        self.add(self._fixed_group)
        self.content = None

        self._motion_timeline = clutter.Timeline(500)
        self._motion_timeline.connect('completed',
            self._motion_timeline_callback, None)
        self._motion_alpha = clutter.Alpha(self._motion_timeline,
            clutter.EASE_OUT_SINE)
        self._motion_behaviour = LoopedPathBehaviour(self._motion_alpha)

        self.set_content(content)

        self.active = None

        # Preparation to pointer events handling.
        self.set_reactive(True)
        self.connect('button-press-event', self._on_button_press_event)
        self.connect('button-release-event', self._on_button_release_event)
        self.connect('scroll-event', self._on_scroll_event)

        self.set_position(self.get_abs_x(x), self.get_abs_y(y))

    @property
    def on_top(self):
        """True if we're on top."""
        return self._offset == 0

    @property
    def on_bottom(self):
        """True if we're on bottom."""
        return self._offset == self._offset_max

    def _get_active(self):
        """Active property getter."""
        return self._active

    def _set_active(self, boolean):
        """Active property setter."""
        if self._active == boolean:
            return

        self._active = boolean
        if boolean:
            # Show indicator if there is need for scrolling.
            if self._offset_max >= 0:
                self.indicator.show()

            self.set_opacity(255)
            self.emit('activated')
        else:
            self.indicator.hide()
            self.set_opacity(128)

    active = property(_get_active, _set_active)

    def _get_offset(self):
        """Get current offset value."""
        return self._offset

    def _set_offset(self, integer):
        """Set current offset value."""
        if self._offset == integer:
            return

        self._offset = integer

        if self._offset < 0:
            self._offset = 0
        elif self._offset > self._offset_max:
            self._offset = self._offset_max

        self.content.set_position(0, - self._offset)

        # Indicator updates.
        if self.on_top:
            self.indicator.set_current(1)
        elif self.on_bottom:
            self.indicator.set_current(2)

    offset = property(_get_offset, _set_offset)

    def set_content(self, content):
        """Set content into scroll area."""
        if self.content is not None:
            self._fixed_group.remove(self.content)
            self._motion_behaviour.remove(self.content)

        self.content = content
        self._fixed_group.add(content)

        self._offset_max = self.content.get_height() - self.area_height

        self._motion_behaviour.apply(self.content)

    def stop_animation(self):
        """Stops the timeline driving animation."""
        self._motion_timeline.stop()

    def scroll_to_top(self):
        """Scroll content back to top."""
        self.offset = 0

    def scroll_to_bottom(self):
        """Scroll content as much as possible."""
        self.offset = self._offset_max

    def scroll_up(self):
        """Scroll up by one step size."""
        self.offset -= self.step_size

    def scroll_down(self):
        """Scroll down by one step size."""
        self.offset += self.step_size

    def scroll_page_up(self):
        """Scroll up by one page. Page is a scroll area height."""
        self.offset -= self.area_height

    def scroll_page_down(self):
        self.offset += self.area_height

    def _update_motion_behaviour(self, target):
        """Preparation of looped behaviour applied to the content."""
        self._motion_behaviour.start_knot = (0.0, -self.offset)
        self._motion_behaviour.end_knot = (0.0, -target)
        self._motion_behaviour.start_index = 0.0
        # Need to set the end index to 0.9999. Indeed the LoopedPathBehaviour
        # uses an index in [0, 1[. So index = 1 is equivalent to index = 0, the
        # Actor will the be placed on the start_knot.
        self._motion_behaviour.end_index = 0.9999

    def _on_button_press_event(self, actor, event):
        """button-press-event handler."""
        clutter.grab_pointer(self)
        if not self.handler_is_connected(self._motion_handler):
            self._motion_handler = self.connect('motion-event',
                self._on_motion_event)

        if self._motion_timeline.is_playing():
            # A click with an animation pending should stop the animation.
            self._motion_timeline.stop()

            # Go to MODE_STOP to handle correctly next button-release event.
            self._event_mode = self.MODE_STOP
            self.offset = -self.content.get_y()
        else:
            # No animation pending so we're going to do nothing or to move
            # all the content.
            self._old_offset = self.offset
            self._motion_buffer.start(event)
            self._event_mode = self.MODE_SELECTION

        return False

    def _on_button_release_event(self, actor, event):
        """button-release-event handler."""
        clutter.ungrab_pointer()
        if self.handler_is_connected(self._motion_handler):
            self.disconnect_by_func(self._on_motion_event)

        self._motion_buffer.compute_from_last_motion_event(event)

        if not self.active:
            self.active = True
            return

        if self._event_mode == self.MODE_MOTION:
            speed = self._motion_buffer.speed_y_from_last_motion_event

            # Calculation of the new target according to vertical speed.
            target = self.offset - speed * 200

            if target < 0:
                target = 0
            elif target > self._offset_max:
                target = self._offset_max

            self._update_motion_behaviour(target)
            self._motion_timeline.start()

        return False

    def _on_motion_event(self, actor, event):
        """motion-event handler."""
        # Minimum distance we to move before we consider a motion has started.
        motion_threshold = 10

        self._motion_buffer.compute_from_start(event)
        if self._motion_buffer.distance_from_start > motion_threshold:
            self._motion_buffer.take_new_motion_event(event)
            self._event_mode = self.MODE_MOTION
            self.offset = self._old_offset - self._motion_buffer.dy_from_start

        return False

    def _on_scroll_event(self, actor, event):
        """scroll-event handler (mouse's wheel)."""
        if not self.active:
            self.active = True
            return

        # Do not scroll if there is no need.
        if self._offset_max < 0:
            return False

        if event.direction == clutter.SCROLL_DOWN:
            self.scroll_down()
        else:
            self.scroll_up()

        self.emit('moving')

        return False

    def _motion_timeline_callback(self, timeline, screen):
        """Code executed when the animation is finished."""
        self.offset = -self.content.get_y()
示例#8
0
class ScrollMenu(clutter.Group, object):
    """Menu widget that contains text items."""

    __gtype_name__ = "ScrollMenu"
    __gsignals__ = {
        "activated": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
        "selected": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
        "moved": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
    }

    MODE_SELECTION = 0
    MODE_MOTION = 1
    MODE_STOP = 2

    def __init__(self, item_gap, item_height, font_size, color_name):
        clutter.Group.__init__(self)
        self._motion_buffer = MotionBuffer()
        self._items = []
        self._item_gap = item_gap
        self._item_height = item_height
        self._item_font_size = font_size
        self._item_color_name = color_name
        self._selected_index = 1
        self._visible_items = 5
        self._event_mode = -1
        self._animation_progression = 0
        self._animation_start_index = 1
        self._animation_end_index = 1
        self._active = False
        self._motion_handler = 0

        self._timeline = clutter.Timeline(300)
        self._alpha = clutter.Alpha(self._timeline, clutter.EASE_IN_OUT_SINE)

        # preparation to pointer events handling
        self.set_reactive(True)
        self.connect("scroll-event", self._on_scroll_event)
        self.connect("button-press-event", self._on_button_press_event)
        self.connect("button-release-event", self._on_button_release_event)

    def refresh(self):
        """Refresh the menu: clip area dimensions and items positions"""
        self._set_selected_index(self._selected_index, 1)
        self._set_visible_items(self._visible_items)

    def add_item(self, text, name):
        """Creation of a new MenuItem and addition to the ScrollMenu"""
        item = ScrollMenuItem(self._alpha, text, self._item_height, self._item_font_size, self._item_color_name)

        item.set_name(name)
        item.connect("notify::y", self._update_item_opacity)
        self.add(item)

        self._items.append(item)
        self._update_item_opacity(item)

    def remove_item(self, name):
        """Remove an item from the menu"""
        index = self.get_index(name)

        if index != -1:
            # if item was found, we remove it from the item list, from the
            # group and finally we delete it.
            item = self._items[index]
            self._items.remove(item)
            self.remove(item)
            del item

    def _get_active(self):
        """Active property getter"""
        return self._active

    def _set_active(self, boolean):
        """Active property setter"""
        if self._active == boolean:
            return

        self._active = boolean
        if boolean:
            self.set_opacity(255)
            self.emit("activated")
        else:
            self.set_opacity(128)

    active = property(_get_active, _set_active)

    def stop_animation(self):
        """Stops the timeline driving menu animation."""
        self._timeline.stop()

    def _update_behaviours(self, target):
        """Preparation of behaviours applied to menu items before animation"""
        items_len = len(self._items)
        step = 1.0 / items_len
        step_pix = self._item_gap + self._item_height
        middle_index = int(self._visible_items / 2) + 1

        for x, item in enumerate(self._items):
            item.behaviour.start_index = (x + middle_index - self._selected_index) * step
            item.behaviour.end_index = (x + middle_index - target) * step

            item.behaviour.start_knot = (0.0, -step_pix)
            item.behaviour.end_knot = (0.0, (items_len - 1.0) * step_pix)

    def _display_items_at_target(self, target):
        """Menu is displayed for a particular targeted index value"""
        step = 1.0 / len(self._items)
        middle_index = int(self._visible_items / 2) + 1

        for x, item in enumerate(self._items):
            raw_index = (x + middle_index - target) * step

            if raw_index >= 0:
                index = math.modf(raw_index)[0]
            else:
                index = 1 + math.modf(raw_index)[0]

            # Calculation of new coordinates
            xx = index * (item.behaviour.end_knot[0] - item.behaviour.start_knot[0]) + item.behaviour.start_knot[0]
            yy = index * (item.behaviour.end_knot[1] - item.behaviour.start_knot[1]) + item.behaviour.start_knot[1]

            item.set_position(int(xx), int(yy))

    def _get_visible_items(self):
        """visible_items property getter"""
        return self._visible_items

    def _set_visible_items(self, visible_items):
        """visible_items property setter"""
        self._visible_items = visible_items
        height = visible_items * self._item_height + (visible_items - 1) * self._item_gap
        self.set_clip(0, 0, self.get_width(), height)

    visible_items = property(_get_visible_items, _set_visible_items)

    def _get_selected_index(self):
        """selected_index property getter"""
        return self._selected_index

    def _set_selected_index(self, selected_index, duration=300):
        """selected_index property setter"""
        if not self._timeline.is_playing():
            items_len = len(self._items)
            self._update_behaviours(selected_index)

            # those 2 variables are used if we want to stop the timeline
            # we use them + timeline progression to calculate the current index
            # when (if) we stop
            self._animation_start_index = self._selected_index
            self._animation_end_index = selected_index

            # selected_index can be any desired value but in the end,
            # we have to rescale it to be between 0 and (items_len-1)
            if selected_index >= 0:
                self._selected_index = selected_index - math.modf(selected_index / items_len)[1] * items_len
            else:
                self._selected_index = (
                    selected_index + (math.modf(-(selected_index + 1) / items_len)[1] + 1) * items_len
                )

            self._timeline.set_duration(duration)
            self._timeline.start()

            self.emit("moved")

    selected_index = property(_get_selected_index, _set_selected_index)

    def get_selected(self):
        """Get currently selected menuitem"""
        return self._items[int(self._selected_index)]

    def get_index(self, text):
        """Returns index of label with the text as passed or -1 if not found"""
        for item in self._items:
            if item.get_name() == text:
                return self._items.index(item)
        return -1

    def scroll_by(self, step, duration=300):
        """Generic scroll of menu items"""
        self._set_selected_index(self._selected_index + step, duration)

    def scroll_up(self, duration=300):
        """All menu items are scrolled up"""
        self.scroll_by(-1, duration)

    def scroll_down(self, duration=300):
        """All menu items are scrolled down"""
        self.scroll_by(1, duration)

    def get_opacity_for_y(self, y):
        """Calculation of actor's opacity as a function of its y coordinates"""
        opacity_first_item = 40
        opacity_selected_item = 255
        middle = int(self._visible_items / 2)

        y_medium_item = middle * (self._item_height + self._item_gap)
        a = float(opacity_selected_item - opacity_first_item)
        a /= float(y_medium_item)

        if y <= y_medium_item:
            opacity = y * a + opacity_first_item
        else:
            opacity = opacity_selected_item * 2 - opacity_first_item - a * y

        if opacity < 0:
            opacity = 0

        return int(opacity)

    def _update_item_opacity(self, item, stage=None):
        """Set opacity to actors when they are moving. Opacity is f(y)"""
        opacity = self.get_opacity_for_y(item.get_y())
        item.set_opacity(opacity)

    def _on_button_press_event(self, actor, event):
        """button-press-event handler"""
        clutter.grab_pointer(self)
        if not self.handler_is_connected(self._motion_handler):
            self._motion_handler = self.connect("motion-event", self._on_motion_event)

        if self._timeline.is_playing():
            # before we stop the timeline, store its progression
            self._animation_progression = self._timeline.get_progress()

            # A click with an animation pending should stop the animation
            self._timeline.stop()

            # go to MODE_STOP to handle correctly next button-release event
            self._event_mode = self.MODE_STOP
        else:
            # no animation pending so we're going to do either a menu_item
            # selection or a menu motion. This will be decided later, right now
            # we just take a snapshot of this button-press-event as a start.
            self._motion_buffer.start(event)
            self._event_mode = self.MODE_SELECTION

        return False

    def _on_button_release_event(self, actor, event):
        """button-release-event handler"""
        items_len = len(self._items)

        clutter.ungrab_pointer()
        if self.handler_is_connected(self._motion_handler):
            self.disconnect_by_func(self._on_motion_event)
        self._motion_buffer.compute_from_last_motion_event(event)

        if not self.active:
            self.active = True
            return

        y = event.y - self.get_y()

        if self._event_mode == self.MODE_SELECTION:
            # if we are in MODE_SELECTION it means that we want to select
            # the menu item bellow the pointer

            for index, item in enumerate(self._items):
                item_y = item.get_y()
                item_h = item.get_height()
                if (y >= item_y) and (y <= (item_y + item_h)):
                    delta1 = index - self._selected_index
                    delta2 = index - self._selected_index + items_len
                    delta3 = index - self._selected_index - items_len

                    delta = 99999
                    for i in [delta1, delta2, delta3]:
                        if math.fabs(i) < math.fabs(delta):
                            delta = i

                    self.scroll_by(delta)

                    # if delta = 0 it means we've clicked on the selected item
                    if delta == 0:
                        self.emit("selected")

        elif self._event_mode == self.MODE_MOTION:
            speed = self._motion_buffer.speed_y_from_last_motion_event
            target = (
                self._selected_index
                - self._motion_buffer.dy_from_start / self._items[0].behaviour.path_length * items_len
            )

            new_index = int(target - 5 * speed)
            self._selected_index = target
            self._set_selected_index(new_index, 1000)

        else:
            # If we have stopped the pending animation. Now we have to do
            # a small other one to select the closest menu-item
            current_index = (
                self._animation_start_index
                + (self._animation_end_index - self._animation_start_index) * self._animation_progression
            )
            self._selected_index = current_index
            target_index = int(current_index)
            self._set_selected_index(target_index, 1000)

        return False

    def _on_motion_event(self, actor, event):
        """motion-event handler"""
        # threshold in pixels = the minimum distance we have to move before we
        # consider a motion has started
        motion_threshold = 10

        self._motion_buffer.compute_from_start(event)
        if self._motion_buffer.distance_from_start > motion_threshold:
            self._motion_buffer.take_new_motion_event(event)
            self._event_mode = self.MODE_MOTION
            target = self._selected_index - self._motion_buffer.dy_from_start / self._items[
                0
            ].behaviour.path_length * len(self._items)
            self._display_items_at_target(target)

        return False

    def _on_scroll_event(self, actor, event):
        """scroll-event handler (mouse's wheel)"""
        self.active = True

        if event.direction == clutter.SCROLL_DOWN:
            self.scroll_down(duration=150)
        else:
            self.scroll_up(duration=150)

        return False