예제 #1
0
    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)
예제 #2
0
    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))
예제 #3
0
    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)
예제 #4
0
    def __init__(self, stage, width, height):
        gobject.GObject.__init__(self)

        self._motion_buffer = MotionBuffer()
        self._event_mode = self.MODE_NONE
        self._motion_handler = 0

        self.stage = stage  # Stage that displays textures
        # Stage background color when not playing
        self.bgcolor = stage.get_color()
        self.stage_width = width  # Stage width used for video resizing
        self.stage_height = height  # Stage height used for video resizing
        self.ratio = MediaPlayer.NATIVE  # Video texture ratio

        self.audio_skip_step = 10  # Audio skip step in seconds
        self.video_skip_step = 60  # Video skip step in seconds
        self.playlist = None  # Current play list
        self.media = None  # Current media (Playable object)
        self.shuffle = False  # Shuffle mode
        self.repeat = False  # Repeat mode
        self.is_playing = False  # Is media player currently playing
        self.is_reactive_allowed = False  # Is the video_texture reactive

        self.logger = Logger().getLogger('client.MediaPlayer')

        self._internal_callback_timeout_key = None

        self.video_texture = cluttergst.VideoTexture()
        self.pipeline = self.video_texture.get_pipeline()
        self.pipeline.set_property("volume", 0.5)
        self._volume = 10
        self.bus = self.pipeline.get_bus()
        self.bus.add_signal_watch()
        self.bus.connect('message', self._on_gst_message)

        self.video_texture.set_reactive(True)
        self.video_texture.connect('size-change', self._on_size_change)
        self.video_texture.connect('scroll-event', self._on_scroll_event)
        self.video_texture.connect('button-press-event',
                                   self._on_button_press_event)
        self.video_texture.connect('button-release-event',
                                   self._on_button_release_event)
예제 #5
0
    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))
예제 #6
0
    def __init__(self, stage, width, height):
        gobject.GObject.__init__(self)

        self._motion_buffer = MotionBuffer()
        self._event_mode = self.MODE_NONE
        self._motion_handler = 0

        self.stage = stage  # Stage that displays textures
        # Stage background color when not playing
        self.bgcolor = stage.get_color()
        self.stage_width = width  # Stage width used for video resizing
        self.stage_height = height  # Stage height used for video resizing
        self.ratio = MediaPlayer.NATIVE  # Video texture ratio

        self.audio_skip_step = 10  # Audio skip step in seconds
        self.video_skip_step = 60  # Video skip step in seconds
        self.playlist = None  # Current play list
        self.media = None  # Current media (Playable object)
        self.shuffle = False  # Shuffle mode
        self.repeat = False  # Repeat mode
        self.is_playing = False  # Is media player currently playing
        self.is_reactive_allowed = False  # Is the video_texture reactive

        self.logger = Logger().getLogger("client.MediaPlayer")

        self._internal_callback_timeout_key = None

        self.video_texture = cluttergst.VideoTexture()
        self.pipeline = self.video_texture.get_pipeline()
        self.pipeline.set_property("volume", 0.5)
        self._volume = 10
        self.bus = self.pipeline.get_bus()
        self.bus.add_signal_watch()
        self.bus.connect("message", self._on_gst_message)

        self.video_texture.set_reactive(True)
        self.video_texture.connect("size-change", self._on_size_change)
        self.video_texture.connect("scroll-event", self._on_scroll_event)
        self.video_texture.connect("button-press-event", self._on_button_press_event)
        self.video_texture.connect("button-release-event", self._on_button_release_event)
예제 #7
0
    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))
예제 #8
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()
예제 #9
0
class MediaPlayer(gobject.GObject, object):
    """MediaPlayer uses Gstreamer to play all video and audio files. Entertainer
    has only one MediaPlayer object at runtime. MediaPlayer can play objects
    that implement Playable interface."""

    __gsignals__ = {
        "play": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
        "pause": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
        "stop": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
        "skip-forward": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
        "skip-backward": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
        "volume_changed": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
        "position-changed": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
        "refresh": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
    }

    # Ratio constants
    NATIVE = 0
    WIDESCREEN = 1
    NORMAL = 2
    LETTER_BOX = 3
    ZOOM = 4
    INTELLIGENT = 5

    MODE_NONE = 0
    MODE_PLAYPAUSE = 1
    MODE_SEEK = 2

    def __init__(self, stage, width, height):
        gobject.GObject.__init__(self)

        self._motion_buffer = MotionBuffer()
        self._event_mode = self.MODE_NONE
        self._motion_handler = 0

        self.stage = stage  # Stage that displays textures
        # Stage background color when not playing
        self.bgcolor = stage.get_color()
        self.stage_width = width  # Stage width used for video resizing
        self.stage_height = height  # Stage height used for video resizing
        self.ratio = MediaPlayer.NATIVE  # Video texture ratio

        self.audio_skip_step = 10  # Audio skip step in seconds
        self.video_skip_step = 60  # Video skip step in seconds
        self.playlist = None  # Current play list
        self.media = None  # Current media (Playable object)
        self.shuffle = False  # Shuffle mode
        self.repeat = False  # Repeat mode
        self.is_playing = False  # Is media player currently playing
        self.is_reactive_allowed = False  # Is the video_texture reactive

        self.logger = Logger().getLogger("client.MediaPlayer")

        self._internal_callback_timeout_key = None

        self.video_texture = cluttergst.VideoTexture()
        self.pipeline = self.video_texture.get_pipeline()
        self.pipeline.set_property("volume", 0.5)
        self._volume = 10
        self.bus = self.pipeline.get_bus()
        self.bus.add_signal_watch()
        self.bus.connect("message", self._on_gst_message)

        self.video_texture.set_reactive(True)
        self.video_texture.connect("size-change", self._on_size_change)
        self.video_texture.connect("scroll-event", self._on_scroll_event)
        self.video_texture.connect("button-press-event", self._on_button_press_event)
        self.video_texture.connect("button-release-event", self._on_button_release_event)

    def _on_gst_message(self, bus, message):
        """
        Callback function that is called every time when message occurs on
        Gstreamer messagebus.
        """
        if message.type == gst.MESSAGE_EOS:
            if self.media.get_type() == Playable.VIDEO_STREAM or self.playlist is None:
                self.stop()
            else:
                self.next()
        elif message.type == gst.MESSAGE_ERROR:
            self.video_texture.set_playing(False)
            # XXX: laymansterms - I don't know the implications of removing the
            # position property.
            # self.video_texture.set_property("position", 0)
            err, debug = message.parse_error()
            self.logger.error("Error: %(err)s, %(debug)s" % {"err": err, "debug": debug})

    def _get_volume(self):
        """volume property getter."""
        return self._volume

    def _set_volume(self, volume):
        """volume property setter."""
        self._volume = volume
        if self._volume > 20:
            self._volume = 20
        if self._volume < 0:
            self._volume = 0
        self.pipeline.set_property("volume", self._volume / 20.0)
        self.emit("volume-changed")

    volume = property(_get_volume, _set_volume)

    def volume_up(self):
        """Increase player's volume level."""
        self.volume = self._volume + 1

    def volume_down(self):
        """Decrease player's volume level."""
        self.volume = self._volume - 1

    def set_playlist(self, playlist):
        """Set new playlist to MediaPlayer."""
        if len(playlist) == 0:
            raise Exception("Empty playlist is not allowed!")
        self.playlist = playlist
        self.set_media(self.playlist.get_current(), True)

    def get_playlist(self):
        """Get current playlist."""
        return self.playlist

    def set_media(self, playable, internal_call=False):
        """
        Set media to media player. Media is an object that implements
        Playable interface. This media is played back when play() is called.
        """
        # If this function is called from this object we don't set playlist
        # to None
        if not internal_call:
            self.playlist = None

        # If player is currently playing then we stop it
        if self.is_playing:
            self.stop()

        # Update media information
        self.media = playable

        # Set up media player for media
        if self.media.get_type() == Playable.AUDIO_STREAM or self.media.get_type() == Playable.VIDEO_STREAM:
            self.video_texture.set_playing(False)
            self.video_texture.set_uri(playable.get_uri())
            # XXX: laymansterms - I don't know the implications of removing the
            # position property.
            # self.video_texture.set_property("position", 0)

    def get_media(self):
        """Get URI of the current media stream."""
        return self.media

    def has_media(self):
        """
        Has media been set to this player. == has set_media() been called 
        before.
        """
        if self.media is None:
            return False
        else:
            return True

    def get_media_type(self):
        """Get the type of the current media."""
        return self.media.get_type()

    def set_shuffle(self, boolean):
        """
        Enable or disable shuffle play. When shuffle is enabled MediaPlayer picks
        a random Playable from the current playlist.
        """
        self.shuffle = boolean

    def is_shuffle_enabled(self):
        """Is shuffle enabled?"""
        return self.shuffle

    def set_repeat(self, boolean):
        """
        Enable or disable repeat mode. When repeat is enabled the current
        playable is repeated forever.
        """
        self.repeat = boolean

    def is_repeat_enabled(self):
        """Is repeat enabled?"""
        return self.repeat

    def play(self):
        """Play current media."""
        # If current media is an audio file
        if not self.has_media():
            return

        if self.media.get_type() == Playable.AUDIO_STREAM:
            self.is_playing = True
            self.video_texture.set_playing(True)
            self.emit("play")

        # If current media is a video file
        elif self.media.get_type() == Playable.VIDEO_STREAM:
            if self.video_texture.get_parent() == None:
                self.stage.add(self.video_texture)
            self.video_texture.lower_bottom()
            self.is_playing = True
            self.stage.set_color((0, 0, 0, 0))
            self.video_texture.set_playing(True)
            self.emit("play")

        if self._internal_callback_timeout_key is not None:
            gobject.source_remove(self._internal_callback_timeout_key)
        self._internal_callback_timeout_key = gobject.timeout_add(200, self._internal_timer_callback)

    def pause(self):
        """Pause media player."""
        self.is_playing = False
        self.video_texture.set_playing(False)
        self.emit("pause")

    def stop(self):
        """Stop media player."""
        self.is_playing = False
        if self.media.get_type() == Playable.VIDEO_STREAM:
            self.stage.set_color(self.bgcolor)
            self.stage.remove(self.video_texture)
        self.video_texture.set_playing(False)
        # XXX: laymansterms - I don't know the implications of removing the
        # position property.
        # self.video_texture.set_property("position", 0)
        self.emit("stop")

        if self._internal_callback_timeout_key is not None:
            gobject.source_remove(self._internal_callback_timeout_key)

    def next(self):
        """Play next track / video from current playlist."""
        if self.playlist is not None:
            if self.shuffle:
                self.set_media(self.playlist.get_random(), True)
            elif self.playlist.has_next():
                self.set_media(self.playlist.get_next(), True)
            self.play()

    def previous(self):
        """Play previous track / video from current playlist."""
        if self.playlist is not None:
            if self.shuffle:
                self.set_media(self.playlist.get_random(), True)
            elif self.playlist.has_previous():
                self.set_media(self.playlist.get_previous(), True)
            self.play()

    def skip_forward(self):
        """Skip media stream forward."""
        if (self.media.get_type() == Playable.AUDIO_STREAM) or (self.media.get_type() == Playable.VIDEO_STREAM):
            pos_int = self.pipeline.query_position(gst.FORMAT_TIME, None)[0]
            dur = self.pipeline.query_duration(gst.FORMAT_TIME, None)[0]
            seek_ns = pos_int + (self.audio_skip_step * 1000000000)
            if seek_ns > dur:
                seek_ns = dur
            self.pipeline.seek_simple(gst.FORMAT_TIME, gst.SEEK_FLAG_FLUSH, seek_ns)
            self.emit("skip-forward")

    def skip_backward(self):
        """Skip media stream backward."""
        if (self.media.get_type() == Playable.AUDIO_STREAM) or (self.media.get_type() == Playable.VIDEO_STREAM):
            pos_int = self.pipeline.query_position(gst.FORMAT_TIME, None)[0]
            seek_ns = pos_int - (self.audio_skip_step * 1000000000)
            if seek_ns < 0:
                seek_ns = 0
            self.pipeline.seek_simple(gst.FORMAT_TIME, gst.SEEK_FLAG_FLUSH, seek_ns)
            self.emit("skip-backward")

    def get_media_position(self):
        """Get current position of the play back."""
        try:
            pos = self.pipeline.query_position(gst.FORMAT_TIME, None)[0]
            dur = self.pipeline.query_duration(gst.FORMAT_TIME, None)[0]
        except gst.QueryError:
            # This normally means that the MediaPlayer object is querying
            # before the media is playing.
            return 0
        dur_sec = dur / 1000000000.0
        pos_sec = pos / 1000000000.0
        return pos_sec / dur_sec

    def get_media_position_string(self):
        """Get current position of the play back as human readable string."""
        try:
            nanoseconds = self.pipeline.query_position(gst.FORMAT_TIME, None)[0]
            return self._convert_ns_to_human_readable(nanoseconds)
        except gst.QueryError:
            # This normally means that the MediaPlayer object is querying
            # before the media is playing.
            return "00:00"

    def set_media_position(self, position):
        """Set position of the current media."""
        if position < 0.0:
            position = 0.0

        if position > 1.0:
            position = 1.0

        if (self.media.get_type() == Playable.AUDIO_STREAM) or (self.media.get_type() == Playable.VIDEO_STREAM):
            dur = self.pipeline.query_duration(gst.FORMAT_TIME, None)[0]
            seek_ns = position * dur
            if seek_ns > dur:
                seek_ns = dur
            self.pipeline.seek_simple(gst.FORMAT_TIME, gst.SEEK_FLAG_FLUSH, seek_ns)

    def get_media_duration_string(self):
        """
        Return media duration in string format. Example 04:20
        This code is borrowed from gStreamer python tutorial.
        """
        try:
            nanoseconds = self.pipeline.query_duration(gst.FORMAT_TIME, None)[0]
            return self._convert_ns_to_human_readable(nanoseconds)
        except gst.QueryError:
            # This normally means that the MediaPlayer object is querying
            # before the media is playing.
            return "00:00"

    def get_media_title(self):
        """Returns the title of the playing media."""
        return self.media.get_title()

    def _convert_ns_to_human_readable(self, time_int):
        """
        Convert nano seconds to human readable time string.
        This code is borrowed from gStreamer python tutorial.
        """
        time_int = time_int / 1000000000
        time_str = ""
        if time_int >= 3600:
            _hours = time_int / 3600
            time_int = time_int - (_hours * 3600)
            time_str = str(_hours) + ":"
        if time_int >= 600:
            _mins = time_int / 60
            time_int = time_int - (_mins * 60)
            time_str = time_str + str(_mins) + ":"
        elif time_int >= 60:
            _mins = time_int / 60
            time_int = time_int - (_mins * 60)
            time_str = time_str + "0" + str(_mins) + ":"
        else:
            time_str = time_str + "00:"
        if time_int > 9:
            time_str = time_str + str(time_int)
        else:
            time_str = time_str + "0" + str(time_int)
        return time_str

    def _on_size_change(self, texture, width, height):
        """
        Callback for changing video texture's aspect ratio. This is called when
        video texture size changes.
        IMPORTANT NOTE FOR PYLINTers
        The texture parameter is unused, however it cannot be removed because
        this method is called as a callback by cluttergst.VideoTexture.connect()
        """
        if self.ratio == MediaPlayer.NATIVE:
            self.set_native_ratio(width, height)
        elif self.ratio == MediaPlayer.WIDESCREEN:
            self.set_widescreen_ratio(width, height)
        elif self.ratio == MediaPlayer.ZOOM:
            self.set_zoom_ratio(width, height)
        elif self.ratio == MediaPlayer.INTELLIGENT:
            self.set_intelligent_ratio(width, height)

    def set_native_ratio(self, width=None, height=None):
        """
        Do not stretch video. Use native ratio, but scale video such a way
        that it fits in the window.
        """
        self.ratio = MediaPlayer.NATIVE
        if self.has_media() and self.media.get_type() == Playable.VIDEO_STREAM:
            if width is None and height is None:
                texture_width, texture_height = self.video_texture.get_size()
            else:
                texture_width = width
                texture_height = height
            x_ratio = self.stage_width / float(texture_width)
            y_ratio = self.stage_height / float(texture_height)

            if x_ratio > y_ratio:
                self.video_texture.set_scale(
                    self.stage_height / float(texture_height), self.stage_height / float(texture_height)
                )
                new_width = int(texture_width * (self.stage_height / float(texture_height)))
                new_x = int((self.stage_width - new_width) / float(2))
                self.video_texture.set_position(new_x, 0)
            else:
                self.video_texture.set_scale(
                    self.stage_width / float(texture_width), self.stage_width / float(texture_width)
                )
                new_height = int(texture_height * (self.stage_width / float(texture_width)))
                new_y = int((self.stage_height - new_height) / float(2))
                self.video_texture.set_position(0, new_y)

    def set_widescreen_ratio(self, width=None, height=None):
        """'Stretch video to 16:9 ratio."""
        self.ratio = MediaPlayer.WIDESCREEN
        if self.has_media() and self.media.get_type() == Playable.VIDEO_STREAM:
            if width is None and height is None:
                texture_width, texture_height = self.video_texture.get_size()
            else:
                texture_width = width
                texture_height = height
            self.video_texture.set_scale(
                self.stage_width / float(texture_width), self.stage_height / float(texture_height)
            )
            self.video_texture.set_position(0, 0)

    def set_letter_box_ratio(self, width=None, height=None):
        """Set video playback into letter box mode."""
        self.ratio = MediaPlayer.LETTER_BOX
        raise Exception("width=", width, "height=", height, "set_letter_box_ratio() is NOT implemented!")

    def set_zoom_ratio(self, width=None, height=None):
        """
        Stretch video to screen such a way that video covers most of the
        screen.
        """
        self.ratio = MediaPlayer.ZOOM
        if self.has_media() and self.media.get_type() == Playable.VIDEO_STREAM:
            if width is None and height is None:
                texture_width, texture_height = self.video_texture.get_size()
            else:
                texture_width = width
                texture_height = height
            x_ratio = self.stage_width / float(texture_width)
            y_ratio = self.stage_height / float(texture_height)

            if x_ratio < y_ratio:
                self.video_texture.set_scale(
                    self.stage_height / float(texture_height), self.stage_height / float(texture_height)
                )
                new_width = int(texture_width * (self.stage_height / float(texture_height)))
                new_x = int((self.stage_width - new_width) / float(2))
                self.video_texture.set_position(new_x, 0)
            else:
                self.video_texture.set_scale(
                    self.stage_width / float(texture_width), self.stage_width / float(texture_width)
                )
                new_height = int(texture_height * (self.stage_width / float(texture_width)))
                new_y = int((self.stage_height - new_height) / float(2))
                self.video_texture.set_position(0, new_y)

    def set_intelligent_ratio(self, width=None, height=None):
        """
        This aspect ratio tries to display 4:3 on 16:9 in such a way that
        it looks good and still uses the whole screen space. It crops some of
        the image and does some stretching, but not as much as
        set_widescreen_ratio() method.
        """
        self.ratio = MediaPlayer.INTELLIGENT
        if self.has_media() and self.media.get_type() == Playable.VIDEO_STREAM:
            ratio = 1.555555555  # 14:9 Aspect ratio
            if width is None and height is None:
                texture_width, texture_height = self.video_texture.get_size()
            else:
                texture_width = width
                texture_height = height
            fake_height = self.stage_width / ratio  # Fake stage aspect ratio
            self.video_texture.set_scale(self.stage_width / float(texture_width), fake_height / float(texture_height))
            y_offset = -int((fake_height - self.stage_height) / 2)
            self.video_texture.set_position(0, y_offset)

    def get_texture(self):
        """Get media's texture. This is a video texture or album art texture."""
        if self.media.get_type() == Playable.VIDEO_STREAM:
            return clutter.Clone(self.video_texture)

        elif self.media.get_type() == Playable.AUDIO_STREAM:
            url = self.media.get_album_art_url()
            if url is not None:
                texture = Texture(url)
                return texture
            else:
                return None

    def _internal_timer_callback(self):
        """
        A `refresh` event is regulary emited if media is playing.
        And update of the media's' position if a MODE_SEEK has been started
        with the pointer.
        """
        if self.is_playing:
            self.emit("refresh")

        if self._event_mode == self.MODE_SEEK:
            position = self.get_media_position()
            position += self._seek_step
            self.set_media_position(position)
            self.emit("position-changed")

        return True

    def _on_button_press_event(self, actor, event):
        """`button-press` event handler."""
        if not self.is_reactive_allowed:
            return

        clutter.grab_pointer(self.video_texture)
        if not self.video_texture.handler_is_connected(self._motion_handler):
            self._motion_handler = self.video_texture.connect("motion-event", self._on_motion_event)

        self._motion_buffer.start(event)
        self._event_mode = self.MODE_PLAYPAUSE

    def _on_button_release_event(self, actor, event):
        """`button-press` event handler."""
        if not self.is_reactive_allowed:
            return

        clutter.ungrab_pointer()
        if self.video_texture.handler_is_connected(self._motion_handler):
            self.video_texture.disconnect_by_func(self._on_motion_event)

        if self._event_mode == self.MODE_PLAYPAUSE:
            if self.is_playing:
                self.pause()
            else:
                self.play()

        self._event_mode = self.MODE_NONE

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

        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_SEEK
            self._seek_step = float(self._motion_buffer.dx_from_start)
            self._seek_step /= self.video_texture.get_width()
            self._seek_step *= 0.01

        return False

    def _on_scroll_event(self, actor, event):
        """`scroll-event` event handler (mouse's wheel)."""
        # +/- 2% per scroll event on the position of the media stream.
        scroll_progress_ratio = 0.02

        position = self.get_media_position()

        if event.direction == clutter.SCROLL_DOWN:
            position -= scroll_progress_ratio
        else:
            position += scroll_progress_ratio

        self.set_media_position(position)
        self.emit("position-changed")
예제 #10
0
    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))
예제 #11
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
예제 #12
0
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
예제 #13
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
예제 #14
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
예제 #15
0
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
예제 #16
0
    def setUp(self):
        '''Set up the test.'''
        EntertainerTest.setUp(self)

        self.buffer = MotionBuffer()
예제 #17
0
class MediaPlayer(gobject.GObject, object):
    '''MediaPlayer uses Gstreamer to play all video and audio files. Entertainer
    has only one MediaPlayer object at runtime. MediaPlayer can play objects
    that implement Playable interface.'''

    __gsignals__ = {
        'play': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
        'pause': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
        'stop': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
        'skip-forward': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
        'skip-backward': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
        'volume_changed': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
        'position-changed': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
        'refresh': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
    }

    # Ratio constants
    NATIVE = 0
    WIDESCREEN = 1
    NORMAL = 2
    LETTER_BOX = 3
    ZOOM = 4
    INTELLIGENT = 5

    MODE_NONE = 0
    MODE_PLAYPAUSE = 1
    MODE_SEEK = 2

    def __init__(self, stage, width, height):
        gobject.GObject.__init__(self)

        self._motion_buffer = MotionBuffer()
        self._event_mode = self.MODE_NONE
        self._motion_handler = 0

        self.stage = stage  # Stage that displays textures
        # Stage background color when not playing
        self.bgcolor = stage.get_color()
        self.stage_width = width  # Stage width used for video resizing
        self.stage_height = height  # Stage height used for video resizing
        self.ratio = MediaPlayer.NATIVE  # Video texture ratio

        self.audio_skip_step = 10  # Audio skip step in seconds
        self.video_skip_step = 60  # Video skip step in seconds
        self.playlist = None  # Current play list
        self.media = None  # Current media (Playable object)
        self.shuffle = False  # Shuffle mode
        self.repeat = False  # Repeat mode
        self.is_playing = False  # Is media player currently playing
        self.is_reactive_allowed = False  # Is the video_texture reactive

        self.logger = Logger().getLogger('client.MediaPlayer')

        self._internal_callback_timeout_key = None

        self.video_texture = cluttergst.VideoTexture()
        self.pipeline = self.video_texture.get_pipeline()
        self.pipeline.set_property("volume", 0.5)
        self._volume = 10
        self.bus = self.pipeline.get_bus()
        self.bus.add_signal_watch()
        self.bus.connect('message', self._on_gst_message)

        self.video_texture.set_reactive(True)
        self.video_texture.connect('size-change', self._on_size_change)
        self.video_texture.connect('scroll-event', self._on_scroll_event)
        self.video_texture.connect('button-press-event',
                                   self._on_button_press_event)
        self.video_texture.connect('button-release-event',
                                   self._on_button_release_event)

    def _on_gst_message(self, bus, message):
        '''
        Callback function that is called every time when message occurs on
        Gstreamer messagebus.
        '''
        if message.type == gst.MESSAGE_EOS:
            if self.media.get_type() == Playable.VIDEO_STREAM \
                or self.playlist is None:
                self.stop()
            else:
                self.next()
        elif message.type == gst.MESSAGE_ERROR:
            self.video_texture.set_playing(False)
            # XXX: laymansterms - I don't know the implications of removing the
            # position property.
            #self.video_texture.set_property("position", 0)
            err, debug = message.parse_error()
            self.logger.error("Error: %(err)s, %(debug)s" % \
                {'err': err, 'debug': debug})

    def _get_volume(self):
        """volume property getter."""
        return self._volume

    def _set_volume(self, volume):
        """volume property setter."""
        self._volume = volume
        if self._volume > 20:
            self._volume = 20
        if self._volume < 0:
            self._volume = 0
        self.pipeline.set_property("volume", self._volume / 20.0)
        self.emit('volume-changed')

    volume = property(_get_volume, _set_volume)

    def volume_up(self):
        """Increase player's volume level."""
        self.volume = self._volume + 1

    def volume_down(self):
        """Decrease player's volume level."""
        self.volume = self._volume - 1

    def set_playlist(self, playlist):
        '''Set new playlist to MediaPlayer.'''
        if len(playlist) == 0:
            raise Exception("Empty playlist is not allowed!")
        self.playlist = playlist
        self.set_media(self.playlist.get_current(), True)

    def get_playlist(self):
        '''Get current playlist.'''
        return self.playlist

    def set_media(self, playable, internal_call=False):
        '''
        Set media to media player. Media is an object that implements
        Playable interface. This media is played back when play() is called.
        '''
        # If this function is called from this object we don't set playlist
        # to None
        if not internal_call:
            self.playlist = None

        # If player is currently playing then we stop it
        if self.is_playing:
            self.stop()

        # Update media information
        self.media = playable

        # Set up media player for media
        if self.media.get_type() == Playable.AUDIO_STREAM \
            or self.media.get_type() == Playable.VIDEO_STREAM:
            self.video_texture.set_playing(False)
            self.video_texture.set_uri(playable.get_uri())
            # XXX: laymansterms - I don't know the implications of removing the
            # position property.
            #self.video_texture.set_property("position", 0)

    def get_media(self):
        '''Get URI of the current media stream.'''
        return self.media

    def has_media(self):
        '''
        Has media been set to this player. == has set_media() been called 
        before.
        '''
        if self.media is None:
            return False
        else:
            return True

    def get_media_type(self):
        '''Get the type of the current media.'''
        return self.media.get_type()

    def set_shuffle(self, boolean):
        '''
        Enable or disable shuffle play. When shuffle is enabled MediaPlayer picks
        a random Playable from the current playlist.
        '''
        self.shuffle = boolean

    def is_shuffle_enabled(self):
        '''Is shuffle enabled?'''
        return self.shuffle

    def set_repeat(self, boolean):
        '''
        Enable or disable repeat mode. When repeat is enabled the current
        playable is repeated forever.
        '''
        self.repeat = boolean

    def is_repeat_enabled(self):
        '''Is repeat enabled?'''
        return self.repeat

    def play(self):
        '''Play current media.'''
        # If current media is an audio file
        if not self.has_media():
            return

        if self.media.get_type() == Playable.AUDIO_STREAM:
            self.is_playing = True
            self.video_texture.set_playing(True)
            self.emit('play')

        # If current media is a video file
        elif self.media.get_type() == Playable.VIDEO_STREAM:
            if (self.video_texture.get_parent() == None):
                self.stage.add(self.video_texture)
            self.video_texture.lower_bottom()
            self.is_playing = True
            self.stage.set_color((0, 0, 0, 0))
            self.video_texture.set_playing(True)
            self.emit('play')

        if self._internal_callback_timeout_key is not None:
            gobject.source_remove(self._internal_callback_timeout_key)
        self._internal_callback_timeout_key = \
            gobject.timeout_add(200, self._internal_timer_callback)

    def pause(self):
        '''Pause media player.'''
        self.is_playing = False
        self.video_texture.set_playing(False)
        self.emit('pause')

    def stop(self):
        '''Stop media player.'''
        self.is_playing = False
        if self.media.get_type() == Playable.VIDEO_STREAM:
            self.stage.set_color(self.bgcolor)
            self.stage.remove(self.video_texture)
        self.video_texture.set_playing(False)
        # XXX: laymansterms - I don't know the implications of removing the
        # position property.
        #self.video_texture.set_property("position", 0)
        self.emit('stop')

        if self._internal_callback_timeout_key is not None:
            gobject.source_remove(self._internal_callback_timeout_key)

    def next(self):
        '''Play next track / video from current playlist.'''
        if self.playlist is not None:
            if self.shuffle:
                self.set_media(self.playlist.get_random(), True)
            elif self.playlist.has_next():
                self.set_media(self.playlist.get_next(), True)
            self.play()

    def previous(self):
        '''Play previous track / video from current playlist.'''
        if self.playlist is not None:
            if self.shuffle:
                self.set_media(self.playlist.get_random(), True)
            elif self.playlist.has_previous():
                self.set_media(self.playlist.get_previous(), True)
            self.play()

    def skip_forward(self):
        '''Skip media stream forward.'''
        if (self.media.get_type() == Playable.AUDIO_STREAM) or \
            (self.media.get_type() == Playable.VIDEO_STREAM):
            pos_int = self.pipeline.query_position(gst.FORMAT_TIME, None)[0]
            dur = self.pipeline.query_duration(gst.FORMAT_TIME, None)[0]
            seek_ns = pos_int + (self.audio_skip_step * 1000000000)
            if seek_ns > dur:
                seek_ns = dur
            self.pipeline.seek_simple(gst.FORMAT_TIME, gst.SEEK_FLAG_FLUSH,
                                      seek_ns)
            self.emit('skip-forward')

    def skip_backward(self):
        '''Skip media stream backward.'''
        if (self.media.get_type() == Playable.AUDIO_STREAM) or \
            (self.media.get_type() == Playable.VIDEO_STREAM):
            pos_int = self.pipeline.query_position(gst.FORMAT_TIME, None)[0]
            seek_ns = pos_int - (self.audio_skip_step * 1000000000)
            if seek_ns < 0:
                seek_ns = 0
            self.pipeline.seek_simple(gst.FORMAT_TIME, gst.SEEK_FLAG_FLUSH,
                                      seek_ns)
            self.emit('skip-backward')

    def get_media_position(self):
        '''Get current position of the play back.'''
        try:
            pos = self.pipeline.query_position(gst.FORMAT_TIME, None)[0]
            dur = self.pipeline.query_duration(gst.FORMAT_TIME, None)[0]
        except gst.QueryError:
            # This normally means that the MediaPlayer object is querying
            # before the media is playing.
            return 0
        dur_sec = dur / 1000000000.0
        pos_sec = pos / 1000000000.0
        return pos_sec / dur_sec

    def get_media_position_string(self):
        '''Get current position of the play back as human readable string.'''
        try:
            nanoseconds = self.pipeline.query_position(gst.FORMAT_TIME,
                                                       None)[0]
            return self._convert_ns_to_human_readable(nanoseconds)
        except gst.QueryError:
            # This normally means that the MediaPlayer object is querying
            # before the media is playing.
            return "00:00"

    def set_media_position(self, position):
        '''Set position of the current media.'''
        if position < 0.0:
            position = 0.0

        if position > 1.0:
            position = 1.0

        if (self.media.get_type() == Playable.AUDIO_STREAM) or \
            (self.media.get_type() == Playable.VIDEO_STREAM):
            dur = self.pipeline.query_duration(gst.FORMAT_TIME, None)[0]
            seek_ns = (position * dur)
            if seek_ns > dur:
                seek_ns = dur
            self.pipeline.seek_simple(gst.FORMAT_TIME, gst.SEEK_FLAG_FLUSH,
                                      seek_ns)

    def get_media_duration_string(self):
        '''
        Return media duration in string format. Example 04:20
        This code is borrowed from gStreamer python tutorial.
        '''
        try:
            nanoseconds = self.pipeline.query_duration(gst.FORMAT_TIME,
                                                       None)[0]
            return self._convert_ns_to_human_readable(nanoseconds)
        except gst.QueryError:
            # This normally means that the MediaPlayer object is querying
            # before the media is playing.
            return "00:00"

    def get_media_title(self):
        '''Returns the title of the playing media.'''
        return self.media.get_title()

    def _convert_ns_to_human_readable(self, time_int):
        '''
        Convert nano seconds to human readable time string.
        This code is borrowed from gStreamer python tutorial.
        '''
        time_int = time_int / 1000000000
        time_str = ""
        if time_int >= 3600:
            _hours = time_int / 3600
            time_int = time_int - (_hours * 3600)
            time_str = str(_hours) + ":"
        if time_int >= 600:
            _mins = time_int / 60
            time_int = time_int - (_mins * 60)
            time_str = time_str + str(_mins) + ":"
        elif time_int >= 60:
            _mins = time_int / 60
            time_int = time_int - (_mins * 60)
            time_str = time_str + "0" + str(_mins) + ":"
        else:
            time_str = time_str + "00:"
        if time_int > 9:
            time_str = time_str + str(time_int)
        else:
            time_str = time_str + "0" + str(time_int)
        return time_str

    def _on_size_change(self, texture, width, height):
        '''
        Callback for changing video texture's aspect ratio. This is called when
        video texture size changes.
        IMPORTANT NOTE FOR PYLINTers
        The texture parameter is unused, however it cannot be removed because
        this method is called as a callback by cluttergst.VideoTexture.connect()
        '''
        if self.ratio == MediaPlayer.NATIVE:
            self.set_native_ratio(width, height)
        elif self.ratio == MediaPlayer.WIDESCREEN:
            self.set_widescreen_ratio(width, height)
        elif self.ratio == MediaPlayer.ZOOM:
            self.set_zoom_ratio(width, height)
        elif self.ratio == MediaPlayer.INTELLIGENT:
            self.set_intelligent_ratio(width, height)

    def set_native_ratio(self, width=None, height=None):
        '''
        Do not stretch video. Use native ratio, but scale video such a way
        that it fits in the window.
        '''
        self.ratio = MediaPlayer.NATIVE
        if self.has_media() and self.media.get_type() == Playable.VIDEO_STREAM:
            if width is None and height is None:
                texture_width, texture_height = self.video_texture.get_size()
            else:
                texture_width = width
                texture_height = height
            x_ratio = self.stage_width / float(texture_width)
            y_ratio = self.stage_height / float(texture_height)

            if x_ratio > y_ratio:
                self.video_texture.set_scale(
                    self.stage_height / float(texture_height),
                    self.stage_height / float(texture_height))
                new_width = int(texture_width * \
                    (self.stage_height / float(texture_height)))
                new_x = int((self.stage_width - new_width) / float(2))
                self.video_texture.set_position(new_x, 0)
            else:
                self.video_texture.set_scale(
                    self.stage_width / float(texture_width),
                    self.stage_width / float(texture_width))
                new_height = int(texture_height * \
                    (self.stage_width / float(texture_width)))
                new_y = int((self.stage_height - new_height) / float(2))
                self.video_texture.set_position(0, new_y)

    def set_widescreen_ratio(self, width=None, height=None):
        ''''Stretch video to 16:9 ratio.'''
        self.ratio = MediaPlayer.WIDESCREEN
        if self.has_media() and self.media.get_type() == Playable.VIDEO_STREAM:
            if width is None and height is None:
                texture_width, texture_height = self.video_texture.get_size()
            else:
                texture_width = width
                texture_height = height
            self.video_texture.set_scale(
                self.stage_width / float(texture_width),
                self.stage_height / float(texture_height))
            self.video_texture.set_position(0, 0)

    def set_letter_box_ratio(self, width=None, height=None):
        '''Set video playback into letter box mode.'''
        self.ratio = MediaPlayer.LETTER_BOX
        raise Exception("width=", width, "height=", height,
                        "set_letter_box_ratio() is NOT implemented!")

    def set_zoom_ratio(self, width=None, height=None):
        '''
        Stretch video to screen such a way that video covers most of the
        screen.
        '''
        self.ratio = MediaPlayer.ZOOM
        if self.has_media() and self.media.get_type() == Playable.VIDEO_STREAM:
            if width is None and height is None:
                texture_width, texture_height = self.video_texture.get_size()
            else:
                texture_width = width
                texture_height = height
            x_ratio = self.stage_width / float(texture_width)
            y_ratio = self.stage_height / float(texture_height)

            if x_ratio < y_ratio:
                self.video_texture.set_scale(
                    self.stage_height / float(texture_height),
                    self.stage_height / float(texture_height))
                new_width = int(texture_width * \
                    (self.stage_height / float(texture_height)))
                new_x = int((self.stage_width - new_width) / float(2))
                self.video_texture.set_position(new_x, 0)
            else:
                self.video_texture.set_scale(
                    self.stage_width / float(texture_width),
                    self.stage_width / float(texture_width))
                new_height = int(texture_height * \
                    (self.stage_width / float(texture_width)))
                new_y = int((self.stage_height - new_height) / float(2))
                self.video_texture.set_position(0, new_y)

    def set_intelligent_ratio(self, width=None, height=None):
        '''
        This aspect ratio tries to display 4:3 on 16:9 in such a way that
        it looks good and still uses the whole screen space. It crops some of
        the image and does some stretching, but not as much as
        set_widescreen_ratio() method.
        '''
        self.ratio = MediaPlayer.INTELLIGENT
        if self.has_media() and self.media.get_type() == Playable.VIDEO_STREAM:
            ratio = 1.555555555  # 14:9 Aspect ratio
            if width is None and height is None:
                texture_width, texture_height = self.video_texture.get_size()
            else:
                texture_width = width
                texture_height = height
            fake_height = self.stage_width / ratio  # Fake stage aspect ratio
            self.video_texture.set_scale(
                self.stage_width / float(texture_width),
                fake_height / float(texture_height))
            y_offset = -int((fake_height - self.stage_height) / 2)
            self.video_texture.set_position(0, y_offset)

    def get_texture(self):
        '''Get media's texture. This is a video texture or album art texture.'''
        if self.media.get_type() == Playable.VIDEO_STREAM:
            return clutter.Clone(self.video_texture)

        elif self.media.get_type() == Playable.AUDIO_STREAM:
            url = self.media.get_album_art_url()
            if url is not None:
                texture = Texture(url)
                return texture
            else:
                return None

    def _internal_timer_callback(self):
        '''
        A `refresh` event is regulary emited if media is playing.
        And update of the media's' position if a MODE_SEEK has been started
        with the pointer.
        '''
        if self.is_playing:
            self.emit('refresh')

        if self._event_mode == self.MODE_SEEK:
            position = self.get_media_position()
            position += self._seek_step
            self.set_media_position(position)
            self.emit('position-changed')

        return True

    def _on_button_press_event(self, actor, event):
        """`button-press` event handler."""
        if not self.is_reactive_allowed:
            return

        clutter.grab_pointer(self.video_texture)
        if not self.video_texture.handler_is_connected(self._motion_handler):
            self._motion_handler = self.video_texture.connect(
                'motion-event', self._on_motion_event)

        self._motion_buffer.start(event)
        self._event_mode = self.MODE_PLAYPAUSE

    def _on_button_release_event(self, actor, event):
        """`button-press` event handler."""
        if not self.is_reactive_allowed:
            return

        clutter.ungrab_pointer()
        if self.video_texture.handler_is_connected(self._motion_handler):
            self.video_texture.disconnect_by_func(self._on_motion_event)

        if self._event_mode == self.MODE_PLAYPAUSE:
            if self.is_playing:
                self.pause()
            else:
                self.play()

        self._event_mode = self.MODE_NONE

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

        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_SEEK
            self._seek_step = float(self._motion_buffer.dx_from_start)
            self._seek_step /= self.video_texture.get_width()
            self._seek_step *= 0.01

        return False

    def _on_scroll_event(self, actor, event):
        '''`scroll-event` event handler (mouse's wheel).'''
        # +/- 2% per scroll event on the position of the media stream.
        scroll_progress_ratio = 0.02

        position = self.get_media_position()

        if event.direction == clutter.SCROLL_DOWN:
            position -= scroll_progress_ratio
        else:
            position += scroll_progress_ratio

        self.set_media_position(position)
        self.emit('position-changed')
예제 #18
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()
예제 #19
0
    def setUp(self):
        """Set up the test."""
        EntertainerTest.setUp(self)

        self.buffer = MotionBuffer()
예제 #20
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