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 __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))
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 __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 __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 __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))
class ScrollArea(Base, clutter.Group): """Wrapper of a clutter Group that allows for scrolling. ScrollArea modifies the width of the content and it assumes that the content uses percent modification (read: not default clutter objects).""" __gsignals__ = { 'activated': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), 'moving': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), } MODE_SELECTION = 0 MODE_MOTION = 1 MODE_STOP = 2 STEP_SIZE_PERCENT = 0.04 def __init__(self, x, y, width, height, content): Base.__init__(self) clutter.Group.__init__(self) self._motion_buffer = MotionBuffer() self._offset = 0 # Drives the content motion. self._offset_max = 0 # Maximum value of offset (equal on bottom). self._old_offset = 0 # Stores the old value of offset on motions. self._motion_handler = 0 self._active = None self.step_size = self.get_abs_y(self.STEP_SIZE_PERCENT) # Allowed area for the widget's scrolling content. self.area_width = self.get_abs_x(width) self.area_height = self.get_abs_y(height) # Create content position indicator self.indicator = ListIndicator(3 * width / 4, height, 0.2, 0.045, ListIndicator.VERTICAL) self.indicator.hide_position() self.indicator.set_maximum(2) self.add(self.indicator) # A clipped Group to receive the content. self._fixed_group = clutter.Group() self._fixed_group.set_clip(0, 0, self.area_width, self.area_height) self.add(self._fixed_group) self.content = None self._motion_timeline = clutter.Timeline(500) self._motion_timeline.connect('completed', self._motion_timeline_callback, None) self._motion_alpha = clutter.Alpha(self._motion_timeline, clutter.EASE_OUT_SINE) self._motion_behaviour = LoopedPathBehaviour(self._motion_alpha) self.set_content(content) self.active = None # Preparation to pointer events handling. self.set_reactive(True) self.connect('button-press-event', self._on_button_press_event) self.connect('button-release-event', self._on_button_release_event) self.connect('scroll-event', self._on_scroll_event) self.set_position(self.get_abs_x(x), self.get_abs_y(y)) @property def on_top(self): """True if we're on top.""" return self._offset == 0 @property def on_bottom(self): """True if we're on bottom.""" return self._offset == self._offset_max def _get_active(self): """Active property getter.""" return self._active def _set_active(self, boolean): """Active property setter.""" if self._active == boolean: return self._active = boolean if boolean: # Show indicator if there is need for scrolling. if self._offset_max >= 0: self.indicator.show() self.set_opacity(255) self.emit('activated') else: self.indicator.hide() self.set_opacity(128) active = property(_get_active, _set_active) def _get_offset(self): """Get current offset value.""" return self._offset def _set_offset(self, integer): """Set current offset value.""" if self._offset == integer: return self._offset = integer if self._offset < 0: self._offset = 0 elif self._offset > self._offset_max: self._offset = self._offset_max self.content.set_position(0, -self._offset) # Indicator updates. if self.on_top: self.indicator.set_current(1) elif self.on_bottom: self.indicator.set_current(2) offset = property(_get_offset, _set_offset) def set_content(self, content): """Set content into scroll area.""" if self.content is not None: self._fixed_group.remove(self.content) self._motion_behaviour.remove(self.content) self.content = content self._fixed_group.add(content) self._offset_max = self.content.get_height() - self.area_height self._motion_behaviour.apply(self.content) def stop_animation(self): """Stops the timeline driving animation.""" self._motion_timeline.stop() def scroll_to_top(self): """Scroll content back to top.""" self.offset = 0 def scroll_to_bottom(self): """Scroll content as much as possible.""" self.offset = self._offset_max def scroll_up(self): """Scroll up by one step size.""" self.offset -= self.step_size def scroll_down(self): """Scroll down by one step size.""" self.offset += self.step_size def scroll_page_up(self): """Scroll up by one page. Page is a scroll area height.""" self.offset -= self.area_height def scroll_page_down(self): self.offset += self.area_height def _update_motion_behaviour(self, target): """Preparation of looped behaviour applied to the content.""" self._motion_behaviour.start_knot = (0.0, -self.offset) self._motion_behaviour.end_knot = (0.0, -target) self._motion_behaviour.start_index = 0.0 # Need to set the end index to 0.9999. Indeed the LoopedPathBehaviour # uses an index in [0, 1[. So index = 1 is equivalent to index = 0, the # Actor will the be placed on the start_knot. self._motion_behaviour.end_index = 0.9999 def _on_button_press_event(self, actor, event): """button-press-event handler.""" clutter.grab_pointer(self) if not self.handler_is_connected(self._motion_handler): self._motion_handler = self.connect('motion-event', self._on_motion_event) if self._motion_timeline.is_playing(): # A click with an animation pending should stop the animation. self._motion_timeline.stop() # Go to MODE_STOP to handle correctly next button-release event. self._event_mode = self.MODE_STOP self.offset = -self.content.get_y() else: # No animation pending so we're going to do nothing or to move # all the content. self._old_offset = self.offset self._motion_buffer.start(event) self._event_mode = self.MODE_SELECTION return False def _on_button_release_event(self, actor, event): """button-release-event handler.""" clutter.ungrab_pointer() if self.handler_is_connected(self._motion_handler): self.disconnect_by_func(self._on_motion_event) self._motion_buffer.compute_from_last_motion_event(event) if not self.active: self.active = True return if self._event_mode == self.MODE_MOTION: speed = self._motion_buffer.speed_y_from_last_motion_event # Calculation of the new target according to vertical speed. target = self.offset - speed * 200 if target < 0: target = 0 elif target > self._offset_max: target = self._offset_max self._update_motion_behaviour(target) self._motion_timeline.start() return False def _on_motion_event(self, actor, event): """motion-event handler.""" # Minimum distance we to move before we consider a motion has started. motion_threshold = 10 self._motion_buffer.compute_from_start(event) if self._motion_buffer.distance_from_start > motion_threshold: self._motion_buffer.take_new_motion_event(event) self._event_mode = self.MODE_MOTION self.offset = self._old_offset - self._motion_buffer.dy_from_start return False def _on_scroll_event(self, actor, event): """scroll-event handler (mouse's wheel).""" if not self.active: self.active = True return # Do not scroll if there is no need. if self._offset_max < 0: return False if event.direction == clutter.SCROLL_DOWN: self.scroll_down() else: self.scroll_up() self.emit('moving') return False def _motion_timeline_callback(self, timeline, screen): """Code executed when the animation is finished.""" self.offset = -self.content.get_y()
class 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")
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))
class GridMenu(Base, clutter.Group): """ GridMenu widget. A core widget to handle MenuItem in a grid with a cursor. This widget provides all the necessary logic to move items and the cursor. """ __gsignals__ = { 'activated': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), 'moved': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), 'selected': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), } MODE_NONE = 0 MODE_SELECT = 1 MODE_SEEK = 2 def __init__(self, x=0, y=0, item_width=0.2, item_height=0.1): Base.__init__(self) clutter.Group.__init__(self) self.motion_duration = 100 # Default duration of animations ms. self.cursor_below = True # Is the cursor below items? self._active = None self._is_vertical = True self.items = [] # Items dimensions variable: relative, absolute, center self._item_width = item_width self._item_height = item_height self._item_width_abs = self.get_abs_x(item_width) self._item_height_abs = self.get_abs_y(item_height) self._dx = int(self._item_width_abs / 2) self._dy = int(self._item_height_abs / 2) # Default cursor's index. self._selected_index = 0 # Grid dimensions: real, visible. self.items_per_row = 10 self.items_per_col = 10 self._visible_rows = 3 self._visible_cols = 5 # The moving_group is a Clutter group containing all the items. self._moving_group_x = 0 self._moving_group_y = 0 self._moving_group = clutter.Group() self.add(self._moving_group) # The moving_group is translated using a `BehaviourPath`. self._moving_group_timeline = clutter.Timeline(200) moving_group_alpha = clutter.Alpha(self._moving_group_timeline, clutter.EASE_IN_OUT_SINE) moving_group_path = clutter.Path() self._moving_group_behaviour = clutter.BehaviourPath( moving_group_alpha, moving_group_path) self._moving_group_behaviour.apply(self._moving_group) # The cursor is an Actor that can be added and moved on the menu. # The cusor is always located in the visible (clipped) area of the menu. self._cursor_x = 0 self._cursor_y = 0 self._cursor = None self._cursor_timeline = clutter.Timeline(200) cursor_alpha = clutter.Alpha(self._cursor_timeline, clutter.EASE_IN_SINE) cursor_path = clutter.Path() self._cursor_behaviour = clutter.BehaviourPath(cursor_alpha, cursor_path) # A MotionBuffer is used to compute useful information about the # cursor's motion. It's used when moving the cursor with a pointer. self._motion_buffer = MotionBuffer() self._event_mode = self.MODE_NONE self._motion_handler = 0 self._seek_step_x = 0 self._seek_step_y = 0 gobject.timeout_add(200, self._internal_timer_callback) #XXX: Samuel Buffet # This rectangle is used to grab events as it turns out that their # might be a bug in clutter 0.8 or python-clutter 0.8. # It may be avoided with next release of clutter. self._event_rect = clutter.Rectangle() self._event_rect.set_opacity(0) self.add(self._event_rect) self._event_rect.set_reactive(True) self._event_rect.connect('button-press-event', self._on_button_press_event) self._event_rect.connect('button-release-event', self._on_button_release_event) self._event_rect.connect('scroll-event', self._on_scroll_event) self.set_position(self.get_abs_x(x), self.get_abs_y(y)) @property def count(self): """Return the number of items.""" return len(self.items) @property def on_top(self): """Return True if the selected item is currently on the top.""" selected_row = self._index_to_xy(self._selected_index)[1] if selected_row == 0: return True else: return False @property def on_bottom(self): """Return True if the selected item is currently on the bottom.""" selected_row = self._index_to_xy(self._selected_index)[1] if self._is_vertical: end_row = self._index_to_xy(self.count - 1)[1] if selected_row == end_row: return True else: return False else: if selected_row == self.items_per_col - 1: return True else: return False @property def on_left(self): """Return True if the selected item is currently on the left.""" selected_col = self._index_to_xy(self._selected_index)[0] if selected_col == 0: return True else: return False @property def on_right(self): """Return True if the selected item is currently on the right.""" selected_col = self._index_to_xy(self._selected_index)[0] if not self._is_vertical: end_col = self._index_to_xy(self.count - 1)[0] if selected_col == end_col: return True else: return False else: if selected_col == self.items_per_row - 1: return True else: return False @property def selected_item(self): """Return the selected MenuItem.""" if self.count == 0: return None else: return self.items[self._selected_index] @property def selected_userdata(self): """Return userdata of the MenuItem.""" item = self.selected_item if item is None: return None else: return item.userdata def _get_active(self): """Active property getter.""" return self._active def _set_active(self, boolean): """Active property setter.""" if self._active == boolean: return self._active = boolean if boolean: if self._cursor is not None: self._cursor.show() if self.selected_item is not None: self.selected_item.animate_in() self.emit('activated') self.set_opacity(255) else: if self._cursor is not None: self._cursor.hide() if self.selected_item is not None: self.selected_item.animate_out() self.set_opacity(128) active = property(_get_active, _set_active) def _get_horizontal(self): """horizontal property getter.""" return not self._is_vertical def _set_horizontal(self, boolean): """horizontal property setter.""" self._is_vertical = not boolean horizontal = property(_get_horizontal, _set_horizontal) def _get_vertical(self): """vertical property getter.""" return self._is_vertical def _set_vertical(self, boolean): """vertical property setter.""" self._is_vertical = boolean vertical = property(_get_vertical, _set_vertical) def _get_selected_index(self): """selected_index property getter.""" return self._selected_index def _set_selected_index(self, index, duration=None): """selected_index property setter.""" # Xc, Yc : coordinates of the menu's cursor on the array of items. # xc, yc : coordinates of the menu's cursor relative to the menu. # xm, ym : coordinates of the moving_group relative to the menu. # Xc = xc - xm # Yc = yc - ym if self._selected_index == index or \ index < 0 or \ index > self.count - 1 or \ self._moving_group_timeline.is_playing() or \ self._cursor_timeline.is_playing(): return # Start select/unselect animations on both items. self.items[self._selected_index].animate_out() self.items[index].animate_in() # Get the cursor's coordinate on the array. # /!\ Those coordinates are NOT pixels but refer to the array of items. (Xc, Yc) = self._index_to_xy(index) xm = self._moving_group_x ym = self._moving_group_y xc = Xc + xm yc = Yc + ym # If the targeted cursor's position is on the last visible column then # the moving_group is translated by -1 on the x axis and the translation # of the cursor is reduce by 1 to stay on the column before the last # one. This is not done if the last column has been selected. if xc == self.visible_cols - 1 and \ xm > self.visible_cols -self.items_per_row: xc -= 1 xm -= 1 # If the targeted cursor's position is on the first visible column then # the moving_group is translated by +1 on the x axis and the translation # of the cursor is raised by 1 to stay on the column after the first # one. This is not done if the first column has been selected. if xc == 0 and xm < 0: xc += 1 xm += 1 # If the targeted cursor's position is on the last visible row then # the moving_group is translated by -1 on the y axis and the translation # of the cursor is reduce by 1 to stay on the row before the last # one. This is not done if the last row has been selected. if yc == self.visible_rows - 1 and \ ym > self.visible_rows -self.items_per_col: yc -= 1 ym -= 1 # If the targeted cursor's position is on the first visible row then # the moving_group is translated by +1 on the y axis and the translation # of the cursor is raised by 1 to stay on the row after the first # one. This is not done if the last row has been selected. if yc == 0 and ym < 0: yc += 1 ym += 1 if duration is None: duration = self.motion_duration self._move_cursor(xc, yc, duration) self._move_moving_group(xm, ym, duration) self._selected_index = index self.emit('moved') selected_index = property(_get_selected_index, _set_selected_index) def _get_visible_rows(self): """visible_rows property getter.""" return self._visible_rows def _set_visible_rows(self, visible_rows): """visible_rows property setter.""" self._visible_rows = visible_rows self._clip() visible_rows = property(_get_visible_rows, _set_visible_rows) def _get_visible_cols(self): """visible_cols property getter.""" return self._visible_cols def _set_visible_cols(self, visible_cols): """visible_cols property setter.""" self._visible_cols = visible_cols self._clip() visible_cols = property(_get_visible_cols, _set_visible_cols) def _get_cursor(self): """cursor property getter.""" return self._cursor def _set_cursor(self, cursor): """cursor property setter.""" if self._cursor is not None: self.remove(self._cursor) self._cursor = cursor if self._cursor is not None: self.add(self._cursor) if self._active: self._cursor.show() else: self._cursor.hide() if self.cursor_below: self._cursor.lower_bottom() else: self._cursor.raise_top() self._cursor.set_size(int(self._item_width_abs), int(self._item_height_abs)) self._cursor.set_anchor_point(self._dx, self._dy) self._cursor.set_position(self._dx, self._dy) self._cursor_behaviour.apply(self._cursor) cursor = property(_get_cursor, _set_cursor) def _clip(self): """Updates the clipping region.""" self.set_clip(0, 0, self._visible_cols * self._item_width_abs, self._visible_rows * self._item_height_abs) self._event_rect.set_size(self._visible_cols * self._item_width_abs, self._visible_rows * self._item_height_abs) def stop_animation(self): """Stops the timelines driving menu animation.""" self._moving_group_timeline.stop() self._cursor_timeline.stop() def raw_add_item(self, item): """A method to add an item in the menu.""" self._moving_group.add(item) self.items.append(item) (x, y) = self._index_to_xy(self.count - 1) item.move_anchor_point(self._dx, self._dy) item.set_position(x * self._item_width_abs + self._dx, y * self._item_height_abs + self._dy) if self._is_vertical: self.items_per_col = y + 1 else: self.items_per_row = x + 1 if self.cursor_below: item.raise_top() else: item.lower_bottom() def _index_to_xy(self, index): """Return the coordinates of an element associated to its index.""" if self._is_vertical: r = index / float(self.items_per_row) y = int(math.modf(r)[1]) x = int(index - y * self.items_per_row) else: r = index / float(self.items_per_col) x = int(math.modf(r)[1]) y = int(index - x * self.items_per_col) return (x, y) def _move_moving_group(self, x, y, duration): """Moves the moving_group to x, y coordinates.""" if (x, y) == (self._moving_group_x, self._moving_group_y): return path = clutter.Path() path.add_move_to(self._moving_group_x * self._item_width_abs, self._moving_group_y * self._item_height_abs) path.add_line_to(x * self._item_width_abs, y * self._item_height_abs) self._moving_group_behaviour.set_path(path) self._moving_group_x, self._moving_group_y = x, y self._moving_group_timeline.set_duration(duration) self._moving_group_timeline.start() def _move_cursor(self, x, y, duration): """ Moves the cursor to x, y coordinates. The motion is applied to the center of the cursor. """ if (x, y) == (self._cursor_x, self._cursor_y): return path = clutter.Path() path.add_move_to(self._cursor_x * self._item_width_abs + self._dx, self._cursor_y * self._item_height_abs + self._dy) path.add_line_to(x * self._item_width_abs + self._dx, y * self._item_height_abs + self._dy) self._cursor_behaviour.set_path(path) self._cursor_x, self._cursor_y = x, y self._cursor_timeline.set_duration(duration) self._cursor_timeline.start() def up(self): """Move the menu's cursor up changing the selected_index property.""" if not self.on_top: if self._is_vertical: self.selected_index -= self.items_per_row else: self.selected_index -= 1 def down(self): """Move the menu's cursor down changing the selected_index property.""" if not self.on_bottom: if self._is_vertical: self.selected_index += self.items_per_row else: self.selected_index += 1 def right(self): """Move the menu's cursor right changing the selected_index property.""" if not self.on_right: if self._is_vertical: self.selected_index += 1 else: self.selected_index += self.items_per_col def left(self): """Move the menu's cursor left changing the selected_index property.""" if not self.on_left: if self._is_vertical: self.selected_index -= 1 else: self.selected_index -= self.items_per_col def _internal_timer_callback(self): """ This callback is used to move the cursor if the SEEK mode is activated. """ if self._event_mode == self.MODE_SEEK: if self._seek_step_x == 1: self.right() if self._seek_step_x == -1: self.left() if self._seek_step_y == 1: self.down() if self._seek_step_y == -1: self.up() return True def _on_button_press_event(self, actor, event): """button-press-event handler.""" clutter.grab_pointer(self._event_rect) if not self._event_rect.handler_is_connected(self._motion_handler): self._motion_handler = self._event_rect.connect( 'motion-event', self._on_motion_event) (x_menu, y_menu) = self.get_transformed_position() (x_moving_group, y_moving_group) = self._moving_group.get_position() # Events coordinates are relative to the stage. # So they need to be computed relatively to the moving group. x = event.x - x_menu - x_moving_group y = event.y - y_menu - y_moving_group x_grid = int(x / self._item_width_abs) y_grid = int(y / self._item_height_abs) if self._is_vertical: new_index = y_grid * self.items_per_row + x_grid else: new_index = x_grid * self.items_per_col + y_grid (delta_x, delta_y) = self._index_to_xy(self._selected_index) delta_x -= x_grid delta_y -= y_grid # Correction factor due to the fact that items are not necessary square, # but most probably rectangles. So the distance in the grid coordinates # must be corrected by a factor to have a real distance in pixels on the # screen. correction = float(self._item_width_abs) / float(self._item_height_abs) correction *= correction distance = math.sqrt(delta_x**2 * correction + delta_y**2) # Computation of the duration of animations, scaling grid steps to ms. duration = int(distance * 50) if self.selected_index == new_index and \ self.active and \ not self._cursor_timeline.is_playing() and \ not self._moving_group_timeline.is_playing(): self._event_mode = self.MODE_SELECT else: self.active = True self._event_mode = self.MODE_NONE self._set_selected_index(new_index, duration) self._motion_buffer.start(event) return False def _on_button_release_event(self, actor, event): """button-release-event handler.""" clutter.ungrab_pointer() if self._event_rect.handler_is_connected(self._motion_handler): self._event_rect.disconnect_by_func(self._on_motion_event) if self._event_mode == self.MODE_SELECT: self.emit('selected') self._event_mode = self.MODE_NONE return True def _on_motion_event(self, actor, event): """motion-event handler""" # threshold in pixels = the minimum distance we have to move before we # consider a motion has started motion_threshold = 20 self._seek_step_x = 0 self._seek_step_y = 0 self._motion_buffer.compute_from_start(event) self._motion_buffer.compute_from_last_motion_event(event) if self._motion_buffer.distance_from_start > motion_threshold: self._event_mode = self.MODE_SEEK self._motion_buffer.take_new_motion_event(event) dx = self._motion_buffer.dx_from_last_motion_event dy = self._motion_buffer.dy_from_last_motion_event if math.fabs(dx) > math.fabs(dy): self._seek_step_x = dx > 0 and 1 or -1 else: self._seek_step_y = dy > 0 and 1 or -1 return False def _on_scroll_event(self, actor, event): """scroll-event handler (mouse's wheel).""" if not self.active: self.active = True return if event.direction == clutter.SCROLL_DOWN: self.down() else: self.up() return False
class MotionBufferTest(EntertainerTest): """Test for entertainerlib.gui.widgets.motion_buffer""" def setUp(self): """Set up the test.""" EntertainerTest.setUp(self) self.buffer = MotionBuffer() def tearDown(self): """Clean up after the test.""" EntertainerTest.tearDown(self) def test_create(self): """Test correct MotionBuffer initialization.""" self.assertTrue(isinstance(self.buffer, MotionBuffer)) def test_computations_from_start(self): """Test all values on a 3 events motion, computed from start.""" self.buffer.start(self._create_first_event()) self.buffer.take_new_motion_event(self._create_second_event()) self.buffer.compute_from_start(self._create_third_event()) self.assertEqual(self.buffer.dt_from_start, 2) self.assertEqual(self.buffer.dx_from_start, 10) self.assertEqual(self.buffer.dy_from_start, 10) self.assertAlmostEqual(self.buffer.distance_from_start, 14.142135624) def test_computations_from_last_event(self): """Test all values on a 3 events motion, computed from last event.""" self.buffer.start(self._create_first_event()) self.buffer.take_new_motion_event(self._create_second_event()) self.buffer.compute_from_last_motion_event(self._create_third_event()) self.assertEqual(self.buffer.dt_from_last_motion_event, 1) self.assertEqual(self.buffer.dx_from_last_motion_event, 10) self.assertEqual(self.buffer.dy_from_last_motion_event, 0) self.assertEqual(self.buffer.distance_from_last_motion_event, 10.0) self.assertEqual(self.buffer.speed_x_from_last_motion_event, 10.0) self.assertEqual(self.buffer.speed_y_from_last_motion_event, 0.0) self.assertEqual(self.buffer.speed_from_last_motion_event, 10.0) self.assertAlmostEqual(self.buffer.dt_ema, 0.3333333333) self.assertAlmostEqual(self.buffer.dx_ema, 3.3333333333) self.assertAlmostEqual(self.buffer.dy_ema, 0.0) self.assertAlmostEqual(self.buffer.distance_ema, 3.3333333333) self.assertAlmostEqual(self.buffer.speed_x_ema, 3.3333333333) self.assertAlmostEqual(self.buffer.speed_y_ema, 0.0) self.assertAlmostEqual(self.buffer.speed_ema, 3.3333333333) def _create_first_event(self): """Create a virtual pointer event.""" event = MockPointerEvent() event.x = 100 event.y = 100 event.time = 0 return event def _create_second_event(self): """Create a virtual pointer event.""" event = MockPointerEvent() event.x = 100 event.y = 110 event.time = 1 return event def _create_third_event(self): """Create a virtual pointer event.""" event = MockPointerEvent() event.x = 110 event.y = 110 event.time = 2 return event
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
class GridMenu(Base, clutter.Group): """ GridMenu widget. A core widget to handle MenuItem in a grid with a cursor. This widget provides all the necessary logic to move items and the cursor. """ __gsignals__ = { 'activated' : ( gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, () ), 'moved' : ( gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, () ), 'selected' : ( gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, () ), } MODE_NONE = 0 MODE_SELECT = 1 MODE_SEEK = 2 def __init__(self, x=0, y=0, item_width=0.2, item_height=0.1): Base.__init__(self) clutter.Group.__init__(self) self.motion_duration = 100 # Default duration of animations ms. self.cursor_below = True # Is the cursor below items? self._active = None self._is_vertical = True self.items = [] # Items dimensions variable: relative, absolute, center self._item_width = item_width self._item_height = item_height self._item_width_abs = self.get_abs_x(item_width) self._item_height_abs = self.get_abs_y(item_height) self._dx = int(self._item_width_abs / 2) self._dy = int(self._item_height_abs / 2) # Default cursor's index. self._selected_index = 0 # Grid dimensions: real, visible. self.items_per_row = 10 self.items_per_col = 10 self._visible_rows = 3 self._visible_cols = 5 # The moving_group is a Clutter group containing all the items. self._moving_group_x = 0 self._moving_group_y = 0 self._moving_group = clutter.Group() self.add(self._moving_group) # The moving_group is translated using a `BehaviourPath`. self._moving_group_timeline = clutter.Timeline(200) moving_group_alpha = clutter.Alpha(self._moving_group_timeline, clutter.EASE_IN_OUT_SINE) moving_group_path = clutter.Path() self._moving_group_behaviour = clutter.BehaviourPath(moving_group_alpha, moving_group_path) self._moving_group_behaviour.apply(self._moving_group) # The cursor is an Actor that can be added and moved on the menu. # The cusor is always located in the visible (clipped) area of the menu. self._cursor_x = 0 self._cursor_y = 0 self._cursor = None self._cursor_timeline = clutter.Timeline(200) cursor_alpha = clutter.Alpha(self._cursor_timeline, clutter.EASE_IN_SINE) cursor_path = clutter.Path() self._cursor_behaviour = clutter.BehaviourPath(cursor_alpha, cursor_path) # A MotionBuffer is used to compute useful information about the # cursor's motion. It's used when moving the cursor with a pointer. self._motion_buffer = MotionBuffer() self._event_mode = self.MODE_NONE self._motion_handler = 0 self._seek_step_x = 0 self._seek_step_y = 0 gobject.timeout_add(200, self._internal_timer_callback) #XXX: Samuel Buffet # This rectangle is used to grab events as it turns out that their # might be a bug in clutter 0.8 or python-clutter 0.8. # It may be avoided with next release of clutter. self._event_rect = clutter.Rectangle() self._event_rect.set_opacity(0) self.add(self._event_rect) self._event_rect.set_reactive(True) self._event_rect.connect('button-press-event', self._on_button_press_event) self._event_rect.connect('button-release-event', self._on_button_release_event) self._event_rect.connect('scroll-event', self._on_scroll_event) self.set_position(self.get_abs_x(x), self.get_abs_y(y)) @property def count(self): """Return the number of items.""" return len(self.items) @property def on_top(self): """Return True if the selected item is currently on the top.""" selected_row = self._index_to_xy(self._selected_index)[1] if selected_row == 0: return True else: return False @property def on_bottom(self): """Return True if the selected item is currently on the bottom.""" selected_row = self._index_to_xy(self._selected_index)[1] if self._is_vertical: end_row = self._index_to_xy(self.count - 1)[1] if selected_row == end_row: return True else: return False else: if selected_row == self.items_per_col - 1: return True else: return False @property def on_left(self): """Return True if the selected item is currently on the left.""" selected_col = self._index_to_xy(self._selected_index)[0] if selected_col == 0: return True else: return False @property def on_right(self): """Return True if the selected item is currently on the right.""" selected_col = self._index_to_xy(self._selected_index)[0] if not self._is_vertical: end_col = self._index_to_xy(self.count - 1)[0] if selected_col == end_col: return True else: return False else: if selected_col == self.items_per_row - 1: return True else: return False @property def selected_item(self): """Return the selected MenuItem.""" if self.count == 0: return None else: return self.items[self._selected_index] @property def selected_userdata(self): """Return userdata of the MenuItem.""" item = self.selected_item if item is None: return None else: return item.userdata def _get_active(self): """Active property getter.""" return self._active def _set_active(self, boolean): """Active property setter.""" if self._active == boolean: return self._active = boolean if boolean: if self._cursor is not None: self._cursor.show() if self.selected_item is not None: self.selected_item.animate_in() self.emit('activated') self.set_opacity(255) else: if self._cursor is not None: self._cursor.hide() if self.selected_item is not None: self.selected_item.animate_out() self.set_opacity(128) active = property(_get_active, _set_active) def _get_horizontal(self): """horizontal property getter.""" return not self._is_vertical def _set_horizontal(self, boolean): """horizontal property setter.""" self._is_vertical = not boolean horizontal = property(_get_horizontal, _set_horizontal) def _get_vertical(self): """vertical property getter.""" return self._is_vertical def _set_vertical(self, boolean): """vertical property setter.""" self._is_vertical = boolean vertical = property(_get_vertical, _set_vertical) def _get_selected_index(self): """selected_index property getter.""" return self._selected_index def _set_selected_index(self, index, duration=None): """selected_index property setter.""" # Xc, Yc : coordinates of the menu's cursor on the array of items. # xc, yc : coordinates of the menu's cursor relative to the menu. # xm, ym : coordinates of the moving_group relative to the menu. # Xc = xc - xm # Yc = yc - ym if self._selected_index == index or \ index < 0 or \ index > self.count - 1 or \ self._moving_group_timeline.is_playing() or \ self._cursor_timeline.is_playing(): return # Start select/unselect animations on both items. self.items[self._selected_index].animate_out() self.items[index].animate_in() # Get the cursor's coordinate on the array. # /!\ Those coordinates are NOT pixels but refer to the array of items. (Xc, Yc) = self._index_to_xy(index) xm = self._moving_group_x ym = self._moving_group_y xc = Xc + xm yc = Yc + ym # If the targeted cursor's position is on the last visible column then # the moving_group is translated by -1 on the x axis and the translation # of the cursor is reduce by 1 to stay on the column before the last # one. This is not done if the last column has been selected. if xc == self.visible_cols - 1 and \ xm > self.visible_cols -self.items_per_row: xc -= 1 xm -= 1 # If the targeted cursor's position is on the first visible column then # the moving_group is translated by +1 on the x axis and the translation # of the cursor is raised by 1 to stay on the column after the first # one. This is not done if the first column has been selected. if xc == 0 and xm < 0: xc += 1 xm += 1 # If the targeted cursor's position is on the last visible row then # the moving_group is translated by -1 on the y axis and the translation # of the cursor is reduce by 1 to stay on the row before the last # one. This is not done if the last row has been selected. if yc == self.visible_rows - 1 and \ ym > self.visible_rows -self.items_per_col: yc -= 1 ym -= 1 # If the targeted cursor's position is on the first visible row then # the moving_group is translated by +1 on the y axis and the translation # of the cursor is raised by 1 to stay on the row after the first # one. This is not done if the last row has been selected. if yc == 0 and ym < 0: yc += 1 ym += 1 if duration is None: duration = self.motion_duration self._move_cursor(xc, yc, duration) self._move_moving_group(xm, ym, duration) self._selected_index = index self.emit('moved') selected_index = property(_get_selected_index, _set_selected_index) def _get_visible_rows(self): """visible_rows property getter.""" return self._visible_rows def _set_visible_rows(self, visible_rows): """visible_rows property setter.""" self._visible_rows = visible_rows self._clip() visible_rows = property(_get_visible_rows, _set_visible_rows) def _get_visible_cols(self): """visible_cols property getter.""" return self._visible_cols def _set_visible_cols(self, visible_cols): """visible_cols property setter.""" self._visible_cols = visible_cols self._clip() visible_cols = property(_get_visible_cols, _set_visible_cols) def _get_cursor(self): """cursor property getter.""" return self._cursor def _set_cursor(self, cursor): """cursor property setter.""" if self._cursor is not None: self.remove(self._cursor) self._cursor = cursor if self._cursor is not None: self.add(self._cursor) if self._active: self._cursor.show() else: self._cursor.hide() if self.cursor_below: self._cursor.lower_bottom() else: self._cursor.raise_top() self._cursor.set_size(int(self._item_width_abs), int(self._item_height_abs)) self._cursor.set_anchor_point(self._dx, self._dy) self._cursor.set_position(self._dx, self._dy) self._cursor_behaviour.apply(self._cursor) cursor = property(_get_cursor, _set_cursor) def _clip(self): """Updates the clipping region.""" self.set_clip(0, 0, self._visible_cols * self._item_width_abs, self._visible_rows * self._item_height_abs) self._event_rect.set_size(self._visible_cols * self._item_width_abs, self._visible_rows * self._item_height_abs) def stop_animation(self): """Stops the timelines driving menu animation.""" self._moving_group_timeline.stop() self._cursor_timeline.stop() def raw_add_item(self, item): """A method to add an item in the menu.""" self._moving_group.add(item) self.items.append(item) (x, y) = self._index_to_xy(self.count - 1) item.move_anchor_point(self._dx, self._dy) item.set_position(x * self._item_width_abs + self._dx, y * self._item_height_abs + self._dy) if self._is_vertical: self.items_per_col = y + 1 else: self.items_per_row = x + 1 if self.cursor_below: item.raise_top() else: item.lower_bottom() def _index_to_xy(self, index): """Return the coordinates of an element associated to its index.""" if self._is_vertical: r = index / float(self.items_per_row) y = int(math.modf(r)[1]) x = int(index - y * self.items_per_row) else: r = index / float(self.items_per_col) x = int(math.modf(r)[1]) y = int(index - x * self.items_per_col) return (x, y) def _move_moving_group(self, x, y, duration): """Moves the moving_group to x, y coordinates.""" if (x, y) == (self._moving_group_x, self._moving_group_y): return path = clutter.Path() path.add_move_to( self._moving_group_x * self._item_width_abs, self._moving_group_y * self._item_height_abs) path.add_line_to( x * self._item_width_abs, y * self._item_height_abs) self._moving_group_behaviour.set_path(path) self._moving_group_x, self._moving_group_y = x, y self._moving_group_timeline.set_duration(duration) self._moving_group_timeline.start() def _move_cursor(self, x, y, duration): """ Moves the cursor to x, y coordinates. The motion is applied to the center of the cursor. """ if (x, y) == (self._cursor_x, self._cursor_y): return path = clutter.Path() path.add_move_to( self._cursor_x * self._item_width_abs + self._dx, self._cursor_y * self._item_height_abs + self._dy) path.add_line_to( x * self._item_width_abs + self._dx, y * self._item_height_abs + self._dy) self._cursor_behaviour.set_path(path) self._cursor_x, self._cursor_y = x, y self._cursor_timeline.set_duration(duration) self._cursor_timeline.start() def up(self): """Move the menu's cursor up changing the selected_index property.""" if not self.on_top: if self._is_vertical: self.selected_index -= self.items_per_row else: self.selected_index -= 1 def down(self): """Move the menu's cursor down changing the selected_index property.""" if not self.on_bottom: if self._is_vertical: self.selected_index += self.items_per_row else: self.selected_index += 1 def right(self): """Move the menu's cursor right changing the selected_index property.""" if not self.on_right: if self._is_vertical: self.selected_index += 1 else: self.selected_index += self.items_per_col def left(self): """Move the menu's cursor left changing the selected_index property.""" if not self.on_left: if self._is_vertical: self.selected_index -= 1 else: self.selected_index -= self.items_per_col def _internal_timer_callback(self): """ This callback is used to move the cursor if the SEEK mode is activated. """ if self._event_mode == self.MODE_SEEK: if self._seek_step_x == 1: self.right() if self._seek_step_x == -1: self.left() if self._seek_step_y == 1: self.down() if self._seek_step_y == -1: self.up() return True def _on_button_press_event(self, actor, event): """button-press-event handler.""" clutter.grab_pointer(self._event_rect) if not self._event_rect.handler_is_connected(self._motion_handler): self._motion_handler = self._event_rect.connect('motion-event', self._on_motion_event) (x_menu, y_menu) = self.get_transformed_position() (x_moving_group, y_moving_group) = self._moving_group.get_position() # Events coordinates are relative to the stage. # So they need to be computed relatively to the moving group. x = event.x - x_menu - x_moving_group y = event.y - y_menu - y_moving_group x_grid = int(x / self._item_width_abs) y_grid = int(y / self._item_height_abs) if self._is_vertical: new_index = y_grid * self.items_per_row + x_grid else: new_index = x_grid * self.items_per_col + y_grid (delta_x, delta_y) = self._index_to_xy(self._selected_index) delta_x -= x_grid delta_y -= y_grid # Correction factor due to the fact that items are not necessary square, # but most probably rectangles. So the distance in the grid coordinates # must be corrected by a factor to have a real distance in pixels on the # screen. correction = float(self._item_width_abs) / float(self._item_height_abs) correction *= correction distance = math.sqrt(delta_x ** 2 * correction + delta_y ** 2) # Computation of the duration of animations, scaling grid steps to ms. duration = int(distance * 50) if self.selected_index == new_index and \ self.active and \ not self._cursor_timeline.is_playing() and \ not self._moving_group_timeline.is_playing(): self._event_mode = self.MODE_SELECT else: self.active = True self._event_mode = self.MODE_NONE self._set_selected_index(new_index, duration) self._motion_buffer.start(event) return False def _on_button_release_event(self, actor, event): """button-release-event handler.""" clutter.ungrab_pointer() if self._event_rect.handler_is_connected(self._motion_handler): self._event_rect.disconnect_by_func(self._on_motion_event) if self._event_mode == self.MODE_SELECT: self.emit('selected') self._event_mode = self.MODE_NONE return True def _on_motion_event(self, actor, event): """motion-event handler""" # threshold in pixels = the minimum distance we have to move before we # consider a motion has started motion_threshold = 20 self._seek_step_x = 0 self._seek_step_y = 0 self._motion_buffer.compute_from_start(event) self._motion_buffer.compute_from_last_motion_event(event) if self._motion_buffer.distance_from_start > motion_threshold: self._event_mode = self.MODE_SEEK self._motion_buffer.take_new_motion_event(event) dx = self._motion_buffer.dx_from_last_motion_event dy = self._motion_buffer.dy_from_last_motion_event if math.fabs(dx) > math.fabs(dy): self._seek_step_x = dx > 0 and 1 or -1 else: self._seek_step_y = dy > 0 and 1 or -1 return False def _on_scroll_event(self, actor, event): """scroll-event handler (mouse's wheel).""" if not self.active: self.active = True return if event.direction == clutter.SCROLL_DOWN: self.down() else: self.up() return False
class MotionBufferTest(EntertainerTest): """Test for entertainerlib.gui.widgets.motion_buffer""" def setUp(self): '''Set up the test.''' EntertainerTest.setUp(self) self.buffer = MotionBuffer() def tearDown(self): '''Clean up after the test.''' EntertainerTest.tearDown(self) def test_create(self): '''Test correct MotionBuffer initialization.''' self.assertTrue(isinstance(self.buffer, MotionBuffer)) def test_computations_from_start(self): '''Test all values on a 3 events motion, computed from start.''' self.buffer.start(self._create_first_event()) self.buffer.take_new_motion_event(self._create_second_event()) self.buffer.compute_from_start(self._create_third_event()) self.assertEqual(self.buffer.dt_from_start, 2) self.assertEqual(self.buffer.dx_from_start, 10) self.assertEqual(self.buffer.dy_from_start, 10) self.assertAlmostEqual(self.buffer.distance_from_start, 14.142135624) def test_computations_from_last_event(self): '''Test all values on a 3 events motion, computed from last event.''' self.buffer.start(self._create_first_event()) self.buffer.take_new_motion_event(self._create_second_event()) self.buffer.compute_from_last_motion_event(self._create_third_event()) self.assertEqual(self.buffer.dt_from_last_motion_event, 1) self.assertEqual(self.buffer.dx_from_last_motion_event, 10) self.assertEqual(self.buffer.dy_from_last_motion_event, 0) self.assertEqual(self.buffer.distance_from_last_motion_event, 10.0) self.assertEqual(self.buffer.speed_x_from_last_motion_event, 10.0) self.assertEqual(self.buffer.speed_y_from_last_motion_event, 0.0) self.assertEqual(self.buffer.speed_from_last_motion_event, 10.0) self.assertAlmostEqual(self.buffer.dt_ema, 0.3333333333) self.assertAlmostEqual(self.buffer.dx_ema, 3.3333333333) self.assertAlmostEqual(self.buffer.dy_ema, 0.0) self.assertAlmostEqual(self.buffer.distance_ema, 3.3333333333) self.assertAlmostEqual(self.buffer.speed_x_ema, 3.3333333333) self.assertAlmostEqual(self.buffer.speed_y_ema, 0.0) self.assertAlmostEqual(self.buffer.speed_ema, 3.3333333333) def _create_first_event(self): '''Create a virtual pointer event.''' event = MockPointerEvent() event.x = 100 event.y = 100 event.time = 0 return event def _create_second_event(self): '''Create a virtual pointer event.''' event = MockPointerEvent() event.x = 100 event.y = 110 event.time = 1 return event def _create_third_event(self): '''Create a virtual pointer event.''' event = MockPointerEvent() event.x = 110 event.y = 110 event.time = 2 return event
def setUp(self): '''Set up the test.''' EntertainerTest.setUp(self) self.buffer = MotionBuffer()
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')
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()
def setUp(self): """Set up the test.""" EntertainerTest.setUp(self) self.buffer = MotionBuffer()
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