[pitivi] Remove unused imports, and separate elements and previewers from the timeline code
- From: Jean-François Fortin Tam <jfft src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [pitivi] Remove unused imports, and separate elements and previewers from the timeline code
- Date: Wed, 24 Apr 2013 18:02:01 +0000 (UTC)
commit 41248171a60f225cca3263e4a8359717a6e3a15c
Author: Mathieu Duponchelle <mathieu duponchelle epitech eu>
Date: Wed Apr 17 02:31:06 2013 +0200
Remove unused imports, and separate elements and previewers from the timeline code
pitivi/timeline/elements.py | 529 +++++++++++++++++++++++
pitivi/timeline/previewers.py | 413 ++++++++++++++++++
pitivi/timeline/timeline.py | 951 +----------------------------------------
pitivi/utils/ui.py | 7 +-
4 files changed, 951 insertions(+), 949 deletions(-)
---
diff --git a/pitivi/timeline/elements.py b/pitivi/timeline/elements.py
new file mode 100644
index 0000000..63b3fbe
--- /dev/null
+++ b/pitivi/timeline/elements.py
@@ -0,0 +1,529 @@
+"""
+Convention throughout this file:
+Every GES element which name could be mistaken with a UI element
+is prefixed with a little b, example : bTimeline
+"""
+
+import os
+
+from gi.repository import Clutter, Cogl, GES
+from pitivi.utils.timeline import Zoomable, EditingContext, Selection, SELECT, UNSELECT, Selected
+from previewers import VideoPreviewer, BORDER_WIDTH
+
+import pitivi.configure as configure
+from pitivi.utils.ui import EXPANDED_SIZE, SPACING
+
+
+def get_preview_for_object(bElement, timeline):
+ # Fixme special preview for transitions, titles
+ if not isinstance(bElement.get_parent(), GES.UriClip):
+ return Clutter.Actor()
+ track_type = bElement.get_track_type()
+ if track_type == GES.TrackType.AUDIO:
+ # FIXME: RandomAccessAudioPreviewer doesn't work yet
+ # previewers[key] = RandomAccessAudioPreviewer(instance, uri)
+ # TODO: return waveform previewer
+ return Clutter.Actor()
+ elif track_type == GES.TrackType.VIDEO:
+ if bElement.get_parent().is_image():
+ # TODO: return still image previewer
+ return Clutter.Actor()
+ else:
+ return VideoPreviewer(bElement, timeline)
+ else:
+ return Clutter.Actor()
+
+
+class RoundedRectangle(Clutter.Actor):
+ """
+ Custom actor used to draw a rectangle that can have rounded corners
+ """
+ __gtype_name__ = 'RoundedRectangle'
+
+ def __init__(self, width, height, arc, step,
+ color=None, border_color=None, border_width=0):
+ """
+ Creates a new rounded rectangle
+ """
+ Clutter.Actor.__init__(self)
+ self.props.width = width
+ self.props.height = height
+ self._arc = arc
+ self._step = step
+ self._border_width = border_width
+ self._color = color
+ self._border_color = border_color
+
+ def do_paint(self):
+ # Set a rectangle for the clipping
+ Cogl.clip_push_rectangle(0, 0, self.props.width, self.props.height)
+
+ if self._border_color:
+ # draw the rectangle for the border which is the same size as the
+ # object
+ Cogl.path_round_rectangle(0, 0, self.props.width, self.props.height,
+ self._arc, self._step)
+ Cogl.path_round_rectangle(self._border_width, self._border_width,
+ self.props.width - self._border_width,
+ self.props.height - self._border_width,
+ self._arc, self._step)
+ Cogl.path_set_fill_rule(Cogl.PathFillRule.EVEN_ODD)
+ Cogl.path_close()
+
+ # set color to border color
+ Cogl.set_source_color(self._border_color)
+ Cogl.path_fill()
+
+ if self._color:
+ # draw the content with is the same size minus the width of the border
+ # finish the clip
+ Cogl.path_round_rectangle(self._border_width, self._border_width,
+ self.props.width - self._border_width,
+ self.props.height - self._border_width,
+ self._arc, self._step)
+ Cogl.path_close()
+
+ # set the color of the filled area
+ Cogl.set_source_color(self._color)
+ Cogl.path_fill()
+
+ Cogl.clip_pop()
+
+ def get_color(self):
+ return self._color
+
+ def set_color(self, color):
+ self._color = color
+ self.queue_redraw()
+
+ def get_border_width(self):
+ return self._border_width
+
+ def set_border_width(self, width):
+ self._border_width = width
+ self.queue_redraw()
+
+ def get_border_color(color):
+ return self._border_color
+
+ def set_border_color(self, color):
+ self._border_color = color
+ self.queue_redraw()
+
+
+class TrimHandle(Clutter.Texture):
+ def __init__(self, timelineElement, isLeft):
+ Clutter.Texture.__init__(self)
+ self.set_from_file(os.path.join(configure.get_pixmap_dir(), "trimbar-normal.png"))
+ self.isLeft = isLeft
+ self.set_size(-1, EXPANDED_SIZE)
+ self.hide()
+
+ self.isSelected = False
+
+ self.timelineElement = timelineElement
+
+ self.set_reactive(True)
+
+ self.dragAction = Clutter.DragAction()
+ self.add_action(self.dragAction)
+
+ self.dragAction.connect("drag-begin", self._dragBeginCb)
+ self.dragAction.connect("drag-end", self._dragEndCb)
+ self.dragAction.connect("drag-progress", self._dragProgressCb)
+
+ self.connect("enter-event", self._enterEventCb)
+ self.connect("leave-event", self._leaveEventCb)
+ self.timelineElement.connect("enter-event", self._elementEnterEventCb)
+ self.timelineElement.connect("leave-event", self._elementLeaveEventCb)
+ self.timelineElement.bElement.selected.connect("selected-changed", self._selectedChangedCb)
+
+ #Callbacks
+
+ def _enterEventCb(self, actor, event):
+ self.timelineElement.set_reactive(False)
+ for elem in self.timelineElement.get_children():
+ elem.set_reactive(False)
+ self.set_reactive(True)
+ self.set_from_file(os.path.join(configure.get_pixmap_dir(), "trimbar-focused.png"))
+ if self.isLeft:
+
self.timelineElement.timeline._container.embed.get_window().set_cursor(Gdk.Cursor.new(Gdk.CursorType.LEFT_SIDE))
+ else:
+
self.timelineElement.timeline._container.embed.get_window().set_cursor(Gdk.Cursor.new(Gdk.CursorType.RIGHT_SIDE))
+
+ def _leaveEventCb(self, actor, event):
+ self.timelineElement.set_reactive(True)
+ for elem in self.timelineElement.get_children():
+ elem.set_reactive(True)
+ self.set_from_file(os.path.join(configure.get_pixmap_dir(), "trimbar-normal.png"))
+
self.timelineElement.timeline._container.embed.get_window().set_cursor(Gdk.Cursor.new(Gdk.CursorType.ARROW))
+
+ def _elementEnterEventCb(self, actor, event):
+ self.show()
+
+ def _elementLeaveEventCb(self, actor, event):
+ if not self.isSelected:
+ self.hide()
+
+ def _selectedChangedCb(self, selected, isSelected):
+ self.isSelected = isSelected
+ self.props.visible = isSelected
+
+ def _dragBeginCb(self, action, actor, event_x, event_y, modifiers):
+ self.timelineElement.setDragged(True)
+ elem = self.timelineElement.bElement.get_parent()
+
+ if self.isLeft:
+ edge = GES.Edge.EDGE_START
+ self._dragBeginStart = self.timelineElement.bElement.get_parent().get_start()
+ else:
+ edge = GES.Edge.EDGE_END
+ self._dragBeginStart = self.timelineElement.bElement.get_parent().get_duration() + \
+ self.timelineElement.bElement.get_parent().get_start()
+
+ self._context = EditingContext(elem,
+ self.timelineElement.timeline.bTimeline,
+ GES.EditMode.EDIT_TRIM,
+ edge,
+ set([]),
+ None)
+
+ self.dragBeginStartX = event_x
+ self.dragBeginStartY = event_y
+
+ def _dragProgressCb(self, action, actor, delta_x, delta_y):
+ # We can't use delta_x here because it fluctuates weirdly.
+ coords = self.dragAction.get_motion_coords()
+ delta_x = coords[0] - self.dragBeginStartX
+
+ new_start = self._dragBeginStart + Zoomable.pixelToNs(delta_x)
+
+ self._context.editTo(new_start,
self.timelineElement.bElement.get_parent().get_layer().get_priority())
+ return False
+
+ def _dragEndCb(self, action, actor, event_x, event_y, modifiers):
+ self.timelineElement.setDragged(False)
+ self._context.finish()
+ self.timelineElement.set_reactive(True)
+ for elem in self.timelineElement.get_children():
+ elem.set_reactive(True)
+ self.set_from_file(os.path.join(configure.get_pixmap_dir(), "trimbar-normal.png"))
+
self.timelineElement.timeline._container.embed.get_window().set_cursor(Gdk.Cursor.new(Gdk.CursorType.ARROW))
+
+
+class TimelineElement(Clutter.Actor, Zoomable):
+ def __init__(self, bElement, track, timeline):
+ """
+ @param bElement : the backend GES.TrackElement
+ @param track : the track to which the bElement belongs
+ @param timeline : the containing graphic timeline.
+ """
+ Zoomable.__init__(self)
+ Clutter.Actor.__init__(self)
+
+ self.timeline = timeline
+
+ self.bElement = bElement
+
+ self.bElement.selected = Selected()
+ self.bElement.selected.connect("selected-changed", self._selectedChangedCb)
+
+ self._createBackground(track)
+
+ self._createPreview()
+
+ self._createBorder()
+
+ self._createMarquee()
+
+ self._createHandles()
+
+ self._createGhostclip()
+
+ self.track_type = self.bElement.get_track_type() # This won't change
+
+ self.isDragged = False
+
+ size = self.bElement.get_duration()
+ self.set_size(self.nsToPixel(size), EXPANDED_SIZE, False)
+ self.set_reactive(True)
+ self._connectToEvents()
+
+ # Public API
+
+ def set_size(self, width, height, ease):
+ if ease:
+ self.save_easing_state()
+ self.set_easing_duration(600)
+ self.background.save_easing_state()
+ self.background.set_easing_duration(600)
+ self.border.save_easing_state()
+ self.border.set_easing_duration(600)
+ self.preview.save_easing_state()
+ self.preview.set_easing_duration(600)
+ try:
+ self.rightHandle.save_easing_state()
+ self.rightHandle.set_easing_duration(600)
+ except AttributeError: # Element doesnt't have handles
+ pass
+
+ self.marquee.set_size(width, height)
+ self.background.props.width = width
+ self.background.props.height = height
+ self.border.props.width = width
+ self.border.props.height = height
+ self.props.width = width
+ self.props.height = height
+ self.preview.set_size(width, height)
+ try:
+ self.rightHandle.set_position(width - self.rightHandle.props.width, 0)
+ except AttributeError: # Element doesnt't have handles
+ pass
+
+ if ease:
+ self.background.restore_easing_state()
+ self.border.restore_easing_state()
+ self.preview.restore_easing_state()
+ try:
+ self.rightHandle.restore_easing_state()
+ except AttributeError: # Element doesnt't have handles
+ pass
+ self.restore_easing_state()
+
+ def update(self, ease):
+ size = self.bElement.get_duration()
+ self.set_size(self.nsToPixel(size), EXPANDED_SIZE, ease)
+
+ def setDragged(self, dragged):
+ brother = self.timeline.findBrother(self.bElement)
+ if brother:
+ brother.isDragged = dragged
+ self.isDragged = dragged
+
+ # Internal API
+
+ def _createGhostclip(self):
+ pass
+
+ def _createBorder(self):
+ self.border = RoundedRectangle(0, 0, 5, 5)
+ color = Cogl.Color()
+ color.init_from_4ub(100, 100, 100, 255)
+ self.border.set_border_color(color)
+ self.border.set_border_width(3)
+
+ self.border.set_position(0, 0)
+ self.add_child(self.border)
+
+ def _createBackground(self, track):
+ pass
+
+ def _createHandles(self):
+ pass
+
+ def _createPreview(self):
+ self.preview = get_preview_for_object(self.bElement, self.timeline)
+ self.add_child(self.preview)
+
+ def _createMarquee(self):
+ # TODO: difference between Actor.new() and Actor()?
+ self.marquee = Clutter.Actor()
+ self.marquee.set_background_color(Clutter.Color.new(60, 60, 60, 100))
+ self.add_child(self.marquee)
+ self.marquee.props.visible = False
+
+ def _connectToEvents(self):
+ # Click
+ # We gotta go low-level cause Clutter.ClickAction["clicked"]
+ # gets emitted after Clutter.DragAction["drag-begin"]
+ self.connect("button-press-event", self._clickedCb)
+
+ # Drag and drop.
+ action = Clutter.DragAction()
+ self.add_action(action)
+ action.connect("drag-progress", self._dragProgressCb)
+ action.connect("drag-begin", self._dragBeginCb)
+ action.connect("drag-end", self._dragEndCb)
+ self.dragAction = action
+
+ def _getLayerForY(self, y):
+ if self.bElement.get_track_type() == GES.TrackType.AUDIO:
+ y -= self.nbrLayers * (EXPANDED_SIZE + SPACING)
+ priority = int(y / (EXPANDED_SIZE + SPACING))
+ return priority
+
+ # Interface (Zoomable)
+
+ def zoomChanged(self):
+ self.update(True)
+
+ # Callbacks
+
+ def _clickedCb(self, action, actor):
+ pass
+
+ def _dragBeginCb(self, action, actor, event_x, event_y, modifiers):
+ pass
+
+ def _dragProgressCb(self, action, actor, delta_x, delta_y):
+ return False
+
+ def _dragEndCb(self, action, actor, event_x, event_y, modifiers):
+ pass
+
+ def _selectedChangedCb(self, selected, isSelected):
+ self.marquee.props.visible = isSelected
+
+
+class ClipElement(TimelineElement):
+ def __init__(self, bElement, track, timeline):
+ TimelineElement.__init__(self, bElement, track, timeline)
+
+ # public API
+ def updateGhostclip(self, priority, y, isControlledByBrother):
+ # Only tricky part of the code, can be called by the linked track element.
+ if priority < 0:
+ return
+
+ # Here we make it so the calculation is the same for audio and video.
+ if self.track_type == GES.TrackType.AUDIO and not isControlledByBrother:
+ y -= self.nbrLayers * (EXPANDED_SIZE + SPACING)
+
+ # And here we take into account the fact that the pointer might actually be
+ # on the other track element, meaning we have to offset it.
+ if isControlledByBrother:
+ if self.track_type == GES.TrackType.AUDIO:
+ y += self.nbrLayers * (EXPANDED_SIZE + SPACING)
+ else:
+ y -= self.nbrLayers * (EXPANDED_SIZE + SPACING)
+
+ # Would that be a new layer ?
+ if priority == self.nbrLayers:
+ self.ghostclip.set_size(self.props.width, SPACING)
+ self.ghostclip.props.y = priority * (EXPANDED_SIZE + SPACING)
+ if self.track_type == GES.TrackType.AUDIO:
+ self.ghostclip.props.y += self.nbrLayers * (EXPANDED_SIZE + SPACING)
+ self.ghostclip.props.visible = True
+ else:
+ # No need to mockup on the same layer
+ if priority == self.bElement.get_parent().get_layer().get_priority():
+ self.ghostclip.props.visible = False
+ # We would be moving to an existing layer.
+ elif priority < self.nbrLayers:
+ self.ghostclip.set_size(self.props.width, EXPANDED_SIZE)
+ self.ghostclip.props.y = priority * (EXPANDED_SIZE + SPACING) + SPACING
+ if self.track_type == GES.TrackType.AUDIO:
+ self.ghostclip.props.y += self.nbrLayers * (EXPANDED_SIZE + SPACING)
+ self.ghostclip.props.visible = True
+
+ # private API
+
+ def _createGhostclip(self):
+ self.ghostclip = Clutter.Actor.new()
+ self.ghostclip.set_background_color(Clutter.Color.new(100, 100, 100, 50))
+ self.ghostclip.props.visible = False
+ self.timeline.add_child(self.ghostclip)
+
+ def _createHandles(self):
+ self.leftHandle = TrimHandle(self, True)
+ self.rightHandle = TrimHandle(self, False)
+ self.add_child(self.leftHandle)
+ self.add_child(self.rightHandle)
+ self.leftHandle.set_position(0, 0)
+
+ def _createBackground(self, track):
+ self.background = RoundedRectangle(0, 0, 5, 5)
+ if track.type == GES.TrackType.AUDIO:
+ color = Cogl.Color()
+ color.init_from_4ub(70, 79, 118, 255)
+ else:
+ color = Cogl.Color()
+ color.init_from_4ub(225, 232, 238, 255)
+ self.background.set_color(color)
+ self.background.set_border_width(3)
+
+ self.background.set_position(0, 0)
+ self.add_child(self.background)
+
+ # Callbacks
+ def _clickedCb(self, action, actor):
+ #TODO : Let's be more specific, masks etc ..
+ self.timeline.selection.setToObj(self.bElement, SELECT)
+
+ def _dragBeginCb(self, action, actor, event_x, event_y, modifiers):
+ self._context = EditingContext(self.bElement, self.timeline.bTimeline, GES.EditMode.EDIT_NORMAL,
GES.Edge.EDGE_NONE, self.timeline.selection.getSelectedTrackElements(), None)
+
+ # This can't change during a drag, so we can safely compute it now for drag events.
+ self.nbrLayers = len(self.timeline.bTimeline.get_layers())
+ # We can also safely find if the object has a brother element
+ self.setDragged(True)
+ self.brother = self.timeline.findBrother(self.bElement)
+ if self.brother:
+ self.brother.nbrLayers = self.nbrLayers
+
+ self._dragBeginStart = self.bElement.get_start()
+ self.dragBeginStartX = event_x
+ self.dragBeginStartY = event_y
+
+ def _dragProgressCb(self, action, actor, delta_x, delta_y):
+ # We can't use delta_x here because it fluctuates weirdly.
+ coords = self.dragAction.get_motion_coords()
+ delta_x = coords[0] - self.dragBeginStartX
+ delta_y = coords[1] - self.dragBeginStartY
+
+ y = coords[1] + self.timeline._container.point.y
+
+ priority = self._getLayerForY(y)
+
+ self.ghostclip.props.x = self.nsToPixel(self._dragBeginStart) + delta_x
+ self.updateGhostclip(priority, y, False)
+ if self.brother:
+ self.brother.ghostclip.props.x = self.nsToPixel(self._dragBeginStart) + delta_x
+ self.brother.updateGhostclip(priority, y, True)
+
+ new_start = self._dragBeginStart + self.pixelToNs(delta_x)
+
+ if not self.ghostclip.props.visible:
+ self._context.editTo(new_start, self.bElement.get_parent().get_layer().get_priority())
+ else:
+ self._context.editTo(self._dragBeginStart, self.bElement.get_parent().get_layer().get_priority())
+ return False
+
+ def _dragEndCb(self, action, actor, event_x, event_y, modifiers):
+ coords = self.dragAction.get_motion_coords()
+ delta_x = coords[0] - self.dragBeginStartX
+ new_start = self._dragBeginStart + self.pixelToNs(delta_x)
+
+ priority = self._getLayerForY(coords[1] + self.timeline._container.point.y)
+ priority = min(priority, len(self.timeline.bTimeline.get_layers()))
+
+ self.timeline._snapEndedCb()
+
+ self.setDragged(False)
+
+ self.ghostclip.props.visible = False
+ if self.brother:
+ self.brother.ghostclip.props.visible = False
+
+ priority = max(0, priority)
+ self._context.editTo(new_start, priority)
+ self._context.finish()
+
+ def _selectedChangedCb(self, selected, isSelected):
+ self.marquee.props.visible = isSelected
+
+
+class TransitionElement(TimelineElement):
+ def __init__(self, bElement, track, timeline):
+ TimelineElement.__init__(self, bElement, track, timeline)
+ self.isDragged = True
+
+ def _createBackground(self, track):
+ self.background = RoundedRectangle(0, 0, 5, 5)
+ color = Cogl.Color()
+ color.init_from_4ub(100, 100, 100, 125)
+ self.background.set_color(color)
+ self.background.set_border_width(3)
+
+ self.background.set_position(0, 0)
+ self.add_child(self.background)
diff --git a/pitivi/timeline/previewers.py b/pitivi/timeline/previewers.py
new file mode 100644
index 0000000..5c21e48
--- /dev/null
+++ b/pitivi/timeline/previewers.py
@@ -0,0 +1,413 @@
+import hashlib
+import os
+import sqlite3
+import sys
+import xdg.BaseDirectory as xdg_dirs
+
+from gi.repository import Clutter, Gst, GLib, GdkPixbuf, Cogl
+from pitivi.utils.timeline import Zoomable
+from pitivi.utils.ui import EXPANDED_SIZE, SPACING
+
+BORDER_WIDTH = 3 # For the timeline elements
+
+
+class VideoPreviewer(Clutter.ScrollActor, Zoomable):
+ def __init__(self, bElement, timeline):
+ """
+ @param bElement : the backend GES.TrackElement
+ @param track : the track to which the bElement belongs
+ @param timeline : the containing graphic timeline.
+ """
+ Zoomable.__init__(self)
+ Clutter.ScrollActor.__init__(self)
+
+ self.uri = bElement.props.uri
+
+ self.bElement = bElement
+ self.timeline = timeline
+
+ self.bElement.connect("notify::duration", self.duration_changed)
+ self.bElement.connect("notify::in-point", self._inpoint_changed_cb)
+ self.bElement.connect("notify::start", self.start_changed)
+
+ self.timeline.connect("scrolled", self._scroll_changed)
+
+ self.duration = self.bElement.props.duration
+
+ self.thumb_margin = BORDER_WIDTH
+ self.thumb_height = EXPANDED_SIZE - 2 * self.thumb_margin
+ # self.thumb_width will be set by self._setupPipeline()
+
+ # TODO: read this property from the settings
+ self.thumb_period = long(0.5 * Gst.SECOND)
+
+ # maps (quantized) times to Thumbnail objects
+ self.thumbs = {}
+
+ self.thumb_cache = ThumbnailCache(uri=self.uri)
+
+ self.wishlist = []
+
+ self._setupPipeline()
+
+ self._startThumbnailing()
+
+ self.callback_id = None
+
+ # Internal API
+
+ def _scroll_changed(self, unused):
+ self._updateWishlist()
+
+ def start_changed(self, unused_bElement, unused_value):
+ self._updateWishlist()
+
+ def _update(self, unused_msg_source=None):
+ if self.callback_id:
+ GLib.source_remove(self.callback_id)
+ self.callback_id = GLib.idle_add(self._addAllThumbnails, priority=GLib.PRIORITY_LOW)
+
+ def _setupPipeline(self):
+ """
+ Create the pipeline.
+
+ It has the form "playbin ! thumbnailsink" where thumbnailsink
+ is a Bin made out of "videorate ! capsfilter ! gdkpixbufsink"
+ """
+ # TODO: don't hardcode framerate
+ self.pipeline = Gst.parse_launch(
+ "uridecodebin uri={uri} ! "
+ "videoconvert ! "
+ "videorate ! "
+ "videoscale method=lanczos ! "
+ "capsfilter caps=video/x-raw,format=(string)RGBA,height=(int){height},"
+ "pixel-aspect-ratio=(fraction)1/1,framerate=2/1 ! "
+ "gdkpixbufsink name=gdkpixbufsink".format(uri=self.uri, height=self.thumb_height))
+
+ # get the gdkpixbufsink and the sinkpad
+ self.gdkpixbufsink = self.pipeline.get_by_name("gdkpixbufsink")
+ sinkpad = self.gdkpixbufsink.get_static_pad("sink")
+
+ self.pipeline.set_state(Gst.State.PAUSED)
+
+ # Wait for the pipeline to be prerolled so we can check the width
+ # that the thumbnails will have and set the aspect ratio accordingly
+ # as well as getting the framerate of the video:
+ change_return = self.pipeline.get_state(Gst.CLOCK_TIME_NONE)
+ if Gst.StateChangeReturn.SUCCESS == change_return[0]:
+ neg_caps = sinkpad.get_current_caps()[0]
+ self.thumb_width = neg_caps["width"]
+ else:
+ # the pipeline couldn't be prerolled so we can't determine the
+ # correct values. Set sane defaults (this should never happen)
+ self.warning("Couldn't preroll the pipeline")
+ # assume 16:9 aspect ratio
+ self.thumb_width = 16 * self.thumb_height / 9
+
+ # pop all messages from the bus so we won't be flooded with messages
+ # from the prerolling phase
+ while self.pipeline.get_bus().pop():
+ continue
+ # add a message handler that listens for the created pixbufs
+ self.pipeline.get_bus().add_signal_watch()
+ self.pipeline.get_bus().connect("message", self.bus_message_handler)
+
+ def _startThumbnailing(self):
+ self.queue = []
+ query_success, duration = self.pipeline.query_duration(Gst.Format.TIME)
+ if not query_success:
+ print("Could not determine the duration of the file {}".format(self.uri))
+ duration = self.duration
+ else:
+ self.duration = duration
+
+ current_time = 0
+ while current_time < duration:
+ self.queue.append(current_time)
+ current_time += self.thumb_period
+
+ self._create_next_thumb()
+
+ def _create_next_thumb(self):
+ if not self.queue:
+ # nothing left to do
+ self.thumb_cache.commit()
+ return
+ wish = self._get_wish()
+ if wish:
+ time = wish
+ self.queue.remove(wish)
+ else:
+ time = self.queue.pop(0)
+ # append the time to the end of the queue so that if this seek fails
+ # another try will be started later
+ self.queue.append(time)
+
+ self.pipeline.seek(1.0,
+ Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.ACCURATE,
+ Gst.SeekType.SET, time,
+ Gst.SeekType.NONE, -1)
+
+ def _addAllThumbnails(self):
+ self.remove_all_children()
+ self.thumbs = {}
+
+ thumb_duration_tmp = Zoomable.pixelToNs(self.thumb_width + self.thumb_margin)
+
+ # quantize thumb length to thumb_period
+ # TODO: replace with a call to utils.misc.quantize:
+ thumb_duration = (thumb_duration_tmp // self.thumb_period) * self.thumb_period
+ # make sure that the thumb duration after the quantization isn't smaller than before
+ if thumb_duration < thumb_duration_tmp:
+ thumb_duration += self.thumb_period
+
+ # make sure that we don't show thumbnails more often than thumb_period
+ thumb_duration = max(thumb_duration, self.thumb_period)
+
+ current_time = 0
+ while current_time < self.duration:
+ thumb = Thumbnail(self.thumb_width, self.thumb_height)
+ thumb.set_position(Zoomable.nsToPixel(current_time), self.thumb_margin)
+ self.add_child(thumb)
+ self.thumbs[current_time] = thumb
+ if current_time in self.thumb_cache:
+ gdkpixbuf = self.thumb_cache[current_time]
+ self.thumbs[current_time].set_from_gdkpixbuf(gdkpixbuf)
+ current_time += thumb_duration
+
+ self._updateWishlist()
+
+ def _inpoint_changed_cb(self, unused_bElement, unused_value):
+ position = Clutter.Point()
+ position.x = Zoomable.nsToPixel(self.bElement.props.in_point)
+ self.scroll_to_point(position)
+ self._updateWishlist()
+
+ def _updateWishlist(self):
+ """
+ Adds thumbnails for the whole clip.
+
+ Takes the zoom setting into account and removes potentially
+ existing thumbnails prior to adding the new ones.
+ """
+ self.wishlist = []
+
+ # calculate unquantized length of a thumb in nano seconds
+ thumb_duration_tmp = Zoomable.pixelToNs(self.thumb_width + self.thumb_margin)
+
+ # quantize thumb length to thumb_period
+ # TODO: replace with a call to utils.misc.quantize:
+ thumb_duration = (thumb_duration_tmp // self.thumb_period) * self.thumb_period
+ # make sure that the thumb duration after the quantization isn't smaller than before
+ if thumb_duration < thumb_duration_tmp:
+ thumb_duration += self.thumb_period
+
+ # make sure that we don't show thumbnails more often than thumb_period
+ thumb_duration = max(thumb_duration, self.thumb_period)
+
+ element_left, element_right = self._get_visible_range()
+ # TODO: replace with a call to utils.misc.quantize:
+ element_left = (element_left // thumb_duration) * thumb_duration
+
+ current_time = element_left
+ while current_time < element_right:
+ if current_time not in self.thumb_cache:
+ self.wishlist.append(current_time)
+ current_time += thumb_duration
+
+ def _get_wish(self):
+ """Returns a wish that is also in the queue or None
+ if no such wish exists"""
+ while True:
+ if not self.wishlist:
+ return None
+ wish = self.wishlist.pop(0)
+ if wish in self.queue:
+ return wish
+
+ def _setThumbnail(self, time, thumbnail):
+ # TODO: is "time" guaranteed to be nanosecond precise?
+ # => __tim says: "that's how it should be"
+ # => also see gst-plugins-good/tests/icles/gdkpixbufsink-test
+ # => Daniel: It is *not* nanosecond precise when we remove the videorate
+ # element from the pipeline
+ if time in self.queue:
+ self.queue.remove(time)
+
+ self.thumb_cache[time] = thumbnail
+
+ if time in self.thumbs:
+ self.thumbs[time].set_from_gdkpixbuf(thumbnail)
+
+ # Interface (Zoomable)
+
+ def zoomChanged(self):
+ self._update()
+
+ def _get_visible_range(self):
+ timeline_left, timeline_right = self._get_visible_timeline_range()
+ element_left = timeline_left - self.bElement.props.start + self.bElement.props.in_point
+ element_left = max(element_left, self.bElement.props.in_point)
+
+ element_right = timeline_right - self.bElement.props.start + self.bElement.props.in_point
+ element_right = min(element_right, self.bElement.props.in_point + self.bElement.props.duration)
+
+ return (element_left, element_right)
+
+ # TODO: move to Timeline or to utils
+ def _get_visible_timeline_range(self):
+ # determine the visible left edge of the timeline
+ # TODO: isn't there some easier way to get the scroll point of the ScrollActor?
+ # timeline_left = -(self.timeline.get_transform().xw - self.timeline.props.x)
+ timeline_left = self.timeline.get_scroll_point().x
+
+ # determine the width of the pipeline
+ # by intersecting the timeline's and the stage's allocation
+ timeline_allocation = self.timeline.props.allocation
+ stage_allocation = self.timeline.get_stage().props.allocation
+
+ timeline_rect = Clutter.Rect()
+ timeline_rect.init(timeline_allocation.x1,
+ timeline_allocation.y1,
+ timeline_allocation.x2 - timeline_allocation.x1,
+ timeline_allocation.y2 - timeline_allocation.y1)
+
+ stage_rect = Clutter.Rect()
+ stage_rect.init(stage_allocation.x1,
+ stage_allocation.y1,
+ stage_allocation.x2 - stage_allocation.x1,
+ stage_allocation.y2 - stage_allocation.y1)
+
+ has_intersection, intersection = timeline_rect.intersection(stage_rect)
+
+ if not has_intersection:
+ return (0, 0)
+
+ timeline_width = intersection.size.width
+
+ # determine the visible right edge of the timeline
+ timeline_right = timeline_left + timeline_width
+
+ # convert to nanoseconds
+ time_left = Zoomable.pixelToNs(timeline_left)
+ time_right = Zoomable.pixelToNs(timeline_right)
+
+ return (time_left, time_right)
+
+ # Callbacks
+
+ def bus_message_handler(self, unused_bus, message):
+ if message.type == Gst.MessageType.ELEMENT and \
+ message.src == self.gdkpixbufsink:
+ struct = message.get_structure()
+ struct_name = struct.get_name()
+ if struct_name == "preroll-pixbuf":
+ self._setThumbnail(struct.get_value("stream-time"), struct.get_value("pixbuf"))
+ elif message.type == Gst.MessageType.ASYNC_DONE:
+ self._create_next_thumb()
+ return Gst.BusSyncReply.PASS
+
+ def duration_changed(self, unused_bElement, unused_value):
+ new_duration = max(self.duration, self.bElement.props.duration)
+ if new_duration > self.duration:
+ self.duration = new_duration
+ self._update()
+
+
+class Thumbnail(Clutter.Actor):
+ def __init__(self, width, height):
+ Clutter.Actor.__init__(self)
+ image = Clutter.Image.new()
+ self.props.content = image
+ self.width = width
+ self.height = height
+ self.set_background_color(Clutter.Color.new(0, 100, 150, 100))
+ self.set_size(self.width, self.height)
+
+ def set_from_gdkpixbuf(self, gdkpixbuf):
+ row_stride = gdkpixbuf.get_rowstride()
+ pixel_data = gdkpixbuf.get_pixels()
+ alpha = gdkpixbuf.get_has_alpha()
+ if alpha:
+ self.props.content.set_data(pixel_data, Cogl.PixelFormat.RGBA_8888, self.width, self.height,
row_stride)
+ else:
+ self.props.content.set_data(pixel_data, Cogl.PixelFormat.RGB_888, self.width, self.height,
row_stride)
+
+
+# TODO: replace with utils.misc.hash_file
+def hash_file(uri):
+ """Hashes the first 256KB of the specified file"""
+ sha256 = hashlib.sha256()
+ with open(uri, "rb") as file:
+ for _ in range(1024):
+ chunk = file.read(256)
+ if not chunk:
+ break
+ sha256.update(chunk)
+ return sha256.hexdigest()
+
+# TODO: remove eventually
+autocreate = True
+
+
+# TODO: replace with pitivi.settings.get_dir
+def get_dir(path, autocreate=True):
+ if autocreate and not os.path.exists(path):
+ os.makedirs(path)
+ return path
+
+
+class ThumbnailCache(object):
+
+ """Caches thumbnails by key using LRU policy, implemented with heapq.
+
+ Uses a two stage caching mechanism. A limited number of elements are
+ held in memory, the rest is being cached on disk using an sqlite db."""
+
+ def __init__(self, uri):
+ object.__init__(self)
+ # TODO: replace with utils.misc.hash_file
+ filehash = hash_file(Gst.uri_get_location(uri))
+ # TODO: replace with pitivi.settings.xdg_cache_home()
+ cache_dir = get_dir(os.path.join(xdg_dirs.xdg_cache_home, "pitivi"), autocreate)
+ dbfile = os.path.join(get_dir(os.path.join(cache_dir, "thumbs")), filehash)
+ self.conn = sqlite3.connect(dbfile)
+ self.cur = self.conn.cursor()
+ self.cur.execute("CREATE TABLE IF NOT EXISTS Thumbs\
+ (Time INTEGER NOT NULL PRIMARY KEY,\
+ Jpeg BLOB NOT NULL)")
+
+ def __contains__(self, key):
+ # check if item is present in on disk cache
+ self.cur.execute("SELECT Time FROM Thumbs WHERE Time = ?", (key,))
+ if self.cur.fetchone():
+ return True
+ return False
+
+ def __getitem__(self, key):
+ self.cur.execute("SELECT * FROM Thumbs WHERE Time = ?", (key,))
+ row = self.cur.fetchone()
+ if row:
+ jpeg = row[1]
+ loader = GdkPixbuf.PixbufLoader.new()
+ # TODO: what do to if any of the following calls fails?
+ loader.write(jpeg)
+ loader.close()
+ pixbuf = loader.get_pixbuf()
+ return pixbuf
+ raise KeyError(key)
+
+ def __setitem__(self, key, value):
+ success, jpeg = value.save_to_bufferv("jpeg", ["quality", None], ["90"])
+ if not success:
+ self.warning("JPEG compression failed")
+ return
+ blob = sqlite3.Binary(jpeg)
+ #Replace if the key already existed
+ self.cur.execute("DELETE FROM Thumbs WHERE time=?", (key,))
+ self.cur.execute("INSERT INTO Thumbs VALUES (?,?)", (key, blob,))
+ #self.conn.commit()
+
+ def commit(self):
+ print("commit")
+ self.conn.commit()
diff --git a/pitivi/timeline/timeline.py b/pitivi/timeline/timeline.py
index 12d5999..411425e 100644
--- a/pitivi/timeline/timeline.py
+++ b/pitivi/timeline/timeline.py
@@ -1,40 +1,17 @@
from gi.repository import GtkClutter
GtkClutter.init([])
-from gi.repository import Gst
-from gi.repository import GES
-from gi.repository import GObject
-import hashlib
-import os
-import sqlite3
-import sys
-import xdg.BaseDirectory as xdg_dirs
-
-from gi.repository import Clutter, GObject, Gtk, Cogl
-
-from gi.repository import GLib
-from gi.repository import Gdk
-from gi.repository import GdkPixbuf
-
-import pitivi.configure as configure
-
-from pitivi.viewer import ViewerWidget
-
-from pitivi.utils.timeline import Zoomable, EditingContext, Selection, SELECT, UNSELECT, Selected
+from gi.repository import Gst, GES, GObject, Clutter, Gtk, GLib, Gdk
+from pitivi.utils.timeline import Zoomable, Selection, UNSELECT
from pitivi.settings import GlobalSettings
-
from pitivi.dialogs.prefs import PreferencesDialog
-
+from pitivi.utils.ui import EXPANDED_SIZE, SPACING
from ruler import ScaleRuler
-
-from datetime import datetime
-
from gettext import gettext as _
-
from pitivi.utils.pipeline import Pipeline
-
from layer import VideoLayerControl, AudioLayerControl
+from elements import ClipElement, TransitionElement
GlobalSettings.addConfigOption('edgeSnapDeadband',
section="user-interface",
@@ -63,12 +40,8 @@ PreferencesDialog.addNumericPreference('imageClipLength',
# CONSTANTS
-EXPANDED_SIZE = 65
-SPACING = 10
CONTROL_WIDTH = 250
-BORDER_WIDTH = 3 # For the timeline elements
-
# tooltip text for toolbar
DELETE = _("Delete Selected")
SPLIT = _("Split clip at playhead position")
@@ -139,500 +112,6 @@ is prefixed with a little b, example : bTimeline
"""
-class RoundedRectangle(Clutter.Actor):
- """
- Custom actor used to draw a rectangle that can have rounded corners
- """
- __gtype_name__ = 'RoundedRectangle'
-
- def __init__(self, width, height, arc, step,
- color=None, border_color=None, border_width=0):
- """
- Creates a new rounded rectangle
- """
- Clutter.Actor.__init__(self)
- self.props.width = width
- self.props.height = height
- self._arc = arc
- self._step = step
- self._border_width = border_width
- self._color = color
- self._border_color = border_color
-
- def do_paint(self):
- # Set a rectangle for the clipping
- Cogl.clip_push_rectangle(0, 0, self.props.width, self.props.height)
-
- if self._border_color:
- # draw the rectangle for the border which is the same size as the
- # object
- Cogl.path_round_rectangle(0, 0, self.props.width, self.props.height,
- self._arc, self._step)
- Cogl.path_round_rectangle(self._border_width, self._border_width,
- self.props.width - self._border_width,
- self.props.height - self._border_width,
- self._arc, self._step)
- Cogl.path_set_fill_rule(Cogl.PathFillRule.EVEN_ODD)
- Cogl.path_close()
-
- # set color to border color
- Cogl.set_source_color(self._border_color)
- Cogl.path_fill()
-
- if self._color:
- # draw the content with is the same size minus the width of the border
- # finish the clip
- Cogl.path_round_rectangle(self._border_width, self._border_width,
- self.props.width - self._border_width,
- self.props.height - self._border_width,
- self._arc, self._step)
- Cogl.path_close()
-
- # set the color of the filled area
- Cogl.set_source_color(self._color)
- Cogl.path_fill()
-
- Cogl.clip_pop()
-
- def get_color(self):
- return self._color
-
- def set_color(self, color):
- self._color = color
- self.queue_redraw()
-
- def get_border_width(self):
- return self._border_width
-
- def set_border_width(self, width):
- self._border_width = width
- self.queue_redraw()
-
- def get_border_color(color):
- return self._border_color
-
- def set_border_color(self, color):
- self._border_color = color
- self.queue_redraw()
-
-
-class TrimHandle(Clutter.Texture):
- def __init__(self, timelineElement, isLeft):
- Clutter.Texture.__init__(self)
- self.set_from_file(os.path.join(configure.get_pixmap_dir(), "trimbar-normal.png"))
- self.isLeft = isLeft
- self.set_size(-1, EXPANDED_SIZE)
- self.hide()
-
- self.isSelected = False
-
- self.timelineElement = timelineElement
-
- self.set_reactive(True)
-
- self.dragAction = Clutter.DragAction()
- self.add_action(self.dragAction)
-
- self.dragAction.connect("drag-begin", self._dragBeginCb)
- self.dragAction.connect("drag-end", self._dragEndCb)
- self.dragAction.connect("drag-progress", self._dragProgressCb)
-
- self.connect("enter-event", self._enterEventCb)
- self.connect("leave-event", self._leaveEventCb)
- self.timelineElement.connect("enter-event", self._elementEnterEventCb)
- self.timelineElement.connect("leave-event", self._elementLeaveEventCb)
- self.timelineElement.bElement.selected.connect("selected-changed", self._selectedChangedCb)
-
- #Callbacks
-
- def _enterEventCb(self, actor, event):
- self.timelineElement.set_reactive(False)
- for elem in self.timelineElement.get_children():
- elem.set_reactive(False)
- self.set_reactive(True)
- self.set_from_file(os.path.join(configure.get_pixmap_dir(), "trimbar-focused.png"))
- if self.isLeft:
-
self.timelineElement.timeline._container.embed.get_window().set_cursor(Gdk.Cursor.new(Gdk.CursorType.LEFT_SIDE))
- else:
-
self.timelineElement.timeline._container.embed.get_window().set_cursor(Gdk.Cursor.new(Gdk.CursorType.RIGHT_SIDE))
-
- def _leaveEventCb(self, actor, event):
- self.timelineElement.set_reactive(True)
- for elem in self.timelineElement.get_children():
- elem.set_reactive(True)
- self.set_from_file(os.path.join(configure.get_pixmap_dir(), "trimbar-normal.png"))
-
self.timelineElement.timeline._container.embed.get_window().set_cursor(Gdk.Cursor.new(Gdk.CursorType.ARROW))
-
- def _elementEnterEventCb(self, actor, event):
- self.show()
-
- def _elementLeaveEventCb(self, actor, event):
- if not self.isSelected:
- self.hide()
-
- def _selectedChangedCb(self, selected, isSelected):
- self.isSelected = isSelected
- self.props.visible = isSelected
-
- def _dragBeginCb(self, action, actor, event_x, event_y, modifiers):
- self.timelineElement.setDragged(True)
- elem = self.timelineElement.bElement.get_parent()
-
- if self.isLeft:
- edge = GES.Edge.EDGE_START
- self._dragBeginStart = self.timelineElement.bElement.get_parent().get_start()
- else:
- edge = GES.Edge.EDGE_END
- self._dragBeginStart = self.timelineElement.bElement.get_parent().get_duration() + \
- self.timelineElement.bElement.get_parent().get_start()
-
- self._context = EditingContext(elem,
- self.timelineElement.timeline.bTimeline,
- GES.EditMode.EDIT_TRIM,
- edge,
- set([]),
- None)
-
- self.dragBeginStartX = event_x
- self.dragBeginStartY = event_y
-
- def _dragProgressCb(self, action, actor, delta_x, delta_y):
- # We can't use delta_x here because it fluctuates weirdly.
- coords = self.dragAction.get_motion_coords()
- delta_x = coords[0] - self.dragBeginStartX
-
- new_start = self._dragBeginStart + Zoomable.pixelToNs(delta_x)
-
- self._context.editTo(new_start,
self.timelineElement.bElement.get_parent().get_layer().get_priority())
- return False
-
- def _dragEndCb(self, action, actor, event_x, event_y, modifiers):
- self.timelineElement.setDragged(False)
- self._context.finish()
- self.timelineElement.set_reactive(True)
- for elem in self.timelineElement.get_children():
- elem.set_reactive(True)
- self.set_from_file(os.path.join(configure.get_pixmap_dir(), "trimbar-normal.png"))
-
self.timelineElement.timeline._container.embed.get_window().set_cursor(Gdk.Cursor.new(Gdk.CursorType.ARROW))
-
-
-class TimelineElement(Clutter.Actor, Zoomable):
- def __init__(self, bElement, track, timeline):
- """
- @param bElement : the backend GES.TrackElement
- @param track : the track to which the bElement belongs
- @param timeline : the containing graphic timeline.
- """
- Zoomable.__init__(self)
- Clutter.Actor.__init__(self)
-
- self.timeline = timeline
-
- self.bElement = bElement
-
- self.bElement.selected = Selected()
- self.bElement.selected.connect("selected-changed", self._selectedChangedCb)
-
- self._createBackground(track)
-
- self._createPreview()
-
- self._createBorder()
-
- self._createMarquee()
-
- self._createHandles()
-
- self._createGhostclip()
-
- self.track_type = self.bElement.get_track_type() # This won't change
-
- self.isDragged = False
-
- size = self.bElement.get_duration()
- self.set_size(self.nsToPixel(size), EXPANDED_SIZE, False)
- self.set_reactive(True)
- self._connectToEvents()
-
- # Public API
-
- def set_size(self, width, height, ease):
- if ease:
- self.save_easing_state()
- self.set_easing_duration(600)
- self.background.save_easing_state()
- self.background.set_easing_duration(600)
- self.border.save_easing_state()
- self.border.set_easing_duration(600)
- self.preview.save_easing_state()
- self.preview.set_easing_duration(600)
- try:
- self.rightHandle.save_easing_state()
- self.rightHandle.set_easing_duration(600)
- except AttributeError: # Element doesnt't have handles
- pass
-
- self.marquee.set_size(width, height)
- self.background.props.width = width
- self.background.props.height = height
- self.border.props.width = width
- self.border.props.height = height
- self.props.width = width
- self.props.height = height
- self.preview.set_size(width, height)
- try:
- self.rightHandle.set_position(width - self.rightHandle.props.width, 0)
- except AttributeError: # Element doesnt't have handles
- pass
-
- if ease:
- self.background.restore_easing_state()
- self.border.restore_easing_state()
- self.preview.restore_easing_state()
- try:
- self.rightHandle.restore_easing_state()
- except AttributeError: # Element doesnt't have handles
- pass
- self.restore_easing_state()
-
- def update(self, ease):
- size = self.bElement.get_duration()
- self.set_size(self.nsToPixel(size), EXPANDED_SIZE, ease)
-
- def setDragged(self, dragged):
- brother = self.timeline.findBrother(self.bElement)
- if brother:
- brother.isDragged = dragged
- self.isDragged = dragged
-
- # Internal API
-
- def _createGhostclip(self):
- pass
-
- def _createBorder(self):
- self.border = RoundedRectangle(0, 0, 5, 5)
- color = Cogl.Color()
- color.init_from_4ub(100, 100, 100, 255)
- self.border.set_border_color(color)
- self.border.set_border_width(3)
-
- self.border.set_position(0, 0)
- self.add_child(self.border)
-
- def _createBackground(self, track):
- pass
-
- def _createHandles(self):
- pass
-
- def _createPreview(self):
- self.preview = get_preview_for_object(self.bElement, self.timeline)
- self.add_child(self.preview)
-
- def _createMarquee(self):
- # TODO: difference between Actor.new() and Actor()?
- self.marquee = Clutter.Actor()
- self.marquee.set_background_color(Clutter.Color.new(60, 60, 60, 100))
- self.add_child(self.marquee)
- self.marquee.props.visible = False
-
- def _connectToEvents(self):
- # Click
- # We gotta go low-level cause Clutter.ClickAction["clicked"]
- # gets emitted after Clutter.DragAction["drag-begin"]
- self.connect("button-press-event", self._clickedCb)
-
- # Drag and drop.
- action = Clutter.DragAction()
- self.add_action(action)
- action.connect("drag-progress", self._dragProgressCb)
- action.connect("drag-begin", self._dragBeginCb)
- action.connect("drag-end", self._dragEndCb)
- self.dragAction = action
-
- def _getLayerForY(self, y):
- if self.bElement.get_track_type() == GES.TrackType.AUDIO:
- y -= self.nbrLayers * (EXPANDED_SIZE + SPACING)
- priority = int(y / (EXPANDED_SIZE + SPACING))
- return priority
-
- # Interface (Zoomable)
-
- def zoomChanged(self):
- self.update(True)
-
- # Callbacks
-
- def _clickedCb(self, action, actor):
- pass
-
- def _dragBeginCb(self, action, actor, event_x, event_y, modifiers):
- pass
-
- def _dragProgressCb(self, action, actor, delta_x, delta_y):
- return False
-
- def _dragEndCb(self, action, actor, event_x, event_y, modifiers):
- pass
-
- def _selectedChangedCb(self, selected, isSelected):
- self.marquee.props.visible = isSelected
-
-
-class ClipElement(TimelineElement):
- def __init__(self, bElement, track, timeline):
- TimelineElement.__init__(self, bElement, track, timeline)
-
- # public API
- def updateGhostclip(self, priority, y, isControlledByBrother):
- # Only tricky part of the code, can be called by the linked track element.
- if priority < 0:
- return
-
- # Here we make it so the calculation is the same for audio and video.
- if self.track_type == GES.TrackType.AUDIO and not isControlledByBrother:
- y -= self.nbrLayers * (EXPANDED_SIZE + SPACING)
-
- # And here we take into account the fact that the pointer might actually be
- # on the other track element, meaning we have to offset it.
- if isControlledByBrother:
- if self.track_type == GES.TrackType.AUDIO:
- y += self.nbrLayers * (EXPANDED_SIZE + SPACING)
- else:
- y -= self.nbrLayers * (EXPANDED_SIZE + SPACING)
-
- # Would that be a new layer ?
- if priority == self.nbrLayers:
- self.ghostclip.set_size(self.props.width, SPACING)
- self.ghostclip.props.y = priority * (EXPANDED_SIZE + SPACING)
- if self.track_type == GES.TrackType.AUDIO:
- self.ghostclip.props.y += self.nbrLayers * (EXPANDED_SIZE + SPACING)
- self.ghostclip.props.visible = True
- else:
- # No need to mockup on the same layer
- if priority == self.bElement.get_parent().get_layer().get_priority():
- self.ghostclip.props.visible = False
- # We would be moving to an existing layer.
- elif priority < self.nbrLayers:
- self.ghostclip.set_size(self.props.width, EXPANDED_SIZE)
- self.ghostclip.props.y = priority * (EXPANDED_SIZE + SPACING) + SPACING
- if self.track_type == GES.TrackType.AUDIO:
- self.ghostclip.props.y += self.nbrLayers * (EXPANDED_SIZE + SPACING)
- self.ghostclip.props.visible = True
-
- # private API
-
- def _createGhostclip(self):
- self.ghostclip = Clutter.Actor.new()
- self.ghostclip.set_background_color(Clutter.Color.new(100, 100, 100, 50))
- self.ghostclip.props.visible = False
- self.timeline.add_child(self.ghostclip)
-
- def _createHandles(self):
- self.leftHandle = TrimHandle(self, True)
- self.rightHandle = TrimHandle(self, False)
- self.add_child(self.leftHandle)
- self.add_child(self.rightHandle)
- self.leftHandle.set_position(0, 0)
-
- def _createBackground(self, track):
- self.background = RoundedRectangle(0, 0, 5, 5)
- if track.type == GES.TrackType.AUDIO:
- color = Cogl.Color()
- color.init_from_4ub(70, 79, 118, 255)
- else:
- color = Cogl.Color()
- color.init_from_4ub(225, 232, 238, 255)
- self.background.set_color(color)
- self.background.set_border_width(3)
-
- self.background.set_position(0, 0)
- self.add_child(self.background)
-
- # Callbacks
- def _clickedCb(self, action, actor):
- #TODO : Let's be more specific, masks etc ..
- self.timeline.selection.setToObj(self.bElement, SELECT)
-
- def _dragBeginCb(self, action, actor, event_x, event_y, modifiers):
- self._context = EditingContext(self.bElement, self.timeline.bTimeline, GES.EditMode.EDIT_NORMAL,
GES.Edge.EDGE_NONE, self.timeline.selection.getSelectedTrackElements(), None)
-
- # This can't change during a drag, so we can safely compute it now for drag events.
- self.nbrLayers = len(self.timeline.bTimeline.get_layers())
- # We can also safely find if the object has a brother element
- self.setDragged(True)
- self.brother = self.timeline.findBrother(self.bElement)
- if self.brother:
- self.brother.nbrLayers = self.nbrLayers
-
- self._dragBeginStart = self.bElement.get_start()
- self.dragBeginStartX = event_x
- self.dragBeginStartY = event_y
-
- def _dragProgressCb(self, action, actor, delta_x, delta_y):
- # We can't use delta_x here because it fluctuates weirdly.
- coords = self.dragAction.get_motion_coords()
- delta_x = coords[0] - self.dragBeginStartX
- delta_y = coords[1] - self.dragBeginStartY
-
- y = coords[1] + self.timeline._container.point.y
-
- priority = self._getLayerForY(y)
-
- self.ghostclip.props.x = self.nsToPixel(self._dragBeginStart) + delta_x
- self.updateGhostclip(priority, y, False)
- if self.brother:
- self.brother.ghostclip.props.x = self.nsToPixel(self._dragBeginStart) + delta_x
- self.brother.updateGhostclip(priority, y, True)
-
- new_start = self._dragBeginStart + self.pixelToNs(delta_x)
-
- if not self.ghostclip.props.visible:
- self._context.editTo(new_start, self.bElement.get_parent().get_layer().get_priority())
- else:
- self._context.editTo(self._dragBeginStart, self.bElement.get_parent().get_layer().get_priority())
- return False
-
- def _dragEndCb(self, action, actor, event_x, event_y, modifiers):
- coords = self.dragAction.get_motion_coords()
- delta_x = coords[0] - self.dragBeginStartX
- new_start = self._dragBeginStart + self.pixelToNs(delta_x)
-
- priority = self._getLayerForY(coords[1] + self.timeline._container.point.y)
- priority = min(priority, len(self.timeline.bTimeline.get_layers()))
-
- self.timeline._snapEndedCb()
-
- self.setDragged(False)
-
- self.ghostclip.props.visible = False
- if self.brother:
- self.brother.ghostclip.props.visible = False
-
- priority = max(0, priority)
- self._context.editTo(new_start, priority)
- self._context.finish()
-
- def _selectedChangedCb(self, selected, isSelected):
- self.marquee.props.visible = isSelected
-
-
-class TransitionElement(TimelineElement):
- def __init__(self, bElement, track, timeline):
- TimelineElement.__init__(self, bElement, track, timeline)
-
- def _createBackground(self, track):
- self.background = RoundedRectangle(0, 0, 5, 5)
- color = Cogl.Color()
- color.init_from_4ub(100, 100, 100, 125)
- self.background.set_color(color)
- self.background.set_border_width(3)
-
- self.background.set_position(0, 0)
- self.add_child(self.background)
-
-
class TimelineStage(Clutter.ScrollActor, Zoomable):
__gsignals__ = {
'scrolled': (GObject.SIGNAL_RUN_FIRST, None, ())
@@ -1739,428 +1218,6 @@ class Timeline(Gtk.VBox, Zoomable):
self.project.connect("asset-added", self._doAssetAddedCb, layer)
self.project.create_asset("file://" + sys.argv[1], GES.UriClip)
-
-def get_preview_for_object(bElement, timeline):
- # Fixme special preview for transitions, titles
- if not isinstance(bElement.get_parent(), GES.UriClip):
- return Clutter.Actor()
- track_type = bElement.get_track_type()
- if track_type == GES.TrackType.AUDIO:
- # FIXME: RandomAccessAudioPreviewer doesn't work yet
- # previewers[key] = RandomAccessAudioPreviewer(instance, uri)
- # TODO: return waveform previewer
- return Clutter.Actor()
- elif track_type == GES.TrackType.VIDEO:
- if bElement.get_parent().is_image():
- # TODO: return still image previewer
- return Clutter.Actor()
- else:
- return VideoPreviewer(bElement, timeline)
- else:
- return Clutter.Actor()
-
-
-class VideoPreviewer(Clutter.ScrollActor, Zoomable):
- def __init__(self, bElement, timeline):
- """
- @param bElement : the backend GES.TrackElement
- @param track : the track to which the bElement belongs
- @param timeline : the containing graphic timeline.
- """
- Zoomable.__init__(self)
- Clutter.ScrollActor.__init__(self)
-
- self.uri = bElement.props.uri
-
- self.bElement = bElement
- self.timeline = timeline
-
- self.bElement.connect("notify::duration", self.duration_changed)
- self.bElement.connect("notify::in-point", self._inpoint_changed_cb)
- self.bElement.connect("notify::start", self.start_changed)
-
- self.timeline.connect("scrolled", self._scroll_changed)
-
- self.duration = self.bElement.props.duration
-
- self.thumb_margin = BORDER_WIDTH
- self.thumb_height = EXPANDED_SIZE - 2 * self.thumb_margin
- # self.thumb_width will be set by self._setupPipeline()
-
- # TODO: read this property from the settings
- self.thumb_period = long(0.5 * Gst.SECOND)
-
- # maps (quantized) times to Thumbnail objects
- self.thumbs = {}
-
- self.thumb_cache = ThumbnailCache(uri=self.uri)
-
- self.wishlist = []
-
- self._setupPipeline()
-
- self._startThumbnailing()
-
- self.callback_id = None
-
- # Internal API
-
- def _scroll_changed(self, unused):
- self._updateWishlist()
-
- def start_changed(self, unused_bElement, unused_value):
- self._updateWishlist()
-
- def _update(self, unused_msg_source=None):
- if self.callback_id:
- GLib.source_remove(self.callback_id)
- self.callback_id = GLib.idle_add(self._addAllThumbnails, priority=GLib.PRIORITY_LOW)
-
- def _setupPipeline(self):
- """
- Create the pipeline.
-
- It has the form "playbin ! thumbnailsink" where thumbnailsink
- is a Bin made out of "videorate ! capsfilter ! gdkpixbufsink"
- """
- # TODO: don't hardcode framerate
- self.pipeline = Gst.parse_launch(
- "uridecodebin uri={uri} ! "
- "videoconvert ! "
- "videorate ! "
- "videoscale method=lanczos ! "
- "capsfilter caps=video/x-raw,format=(string)RGBA,height=(int){height},"
- "pixel-aspect-ratio=(fraction)1/1,framerate=2/1 ! "
- "gdkpixbufsink name=gdkpixbufsink".format(uri=self.uri, height=self.thumb_height))
-
- # get the gdkpixbufsink and the sinkpad
- self.gdkpixbufsink = self.pipeline.get_by_name("gdkpixbufsink")
- sinkpad = self.gdkpixbufsink.get_static_pad("sink")
-
- self.pipeline.set_state(Gst.State.PAUSED)
-
- # Wait for the pipeline to be prerolled so we can check the width
- # that the thumbnails will have and set the aspect ratio accordingly
- # as well as getting the framerate of the video:
- change_return = self.pipeline.get_state(Gst.CLOCK_TIME_NONE)
- if Gst.StateChangeReturn.SUCCESS == change_return[0]:
- neg_caps = sinkpad.get_current_caps()[0]
- self.thumb_width = neg_caps["width"]
- else:
- # the pipeline couldn't be prerolled so we can't determine the
- # correct values. Set sane defaults (this should never happen)
- self.warning("Couldn't preroll the pipeline")
- # assume 16:9 aspect ratio
- self.thumb_width = 16 * self.thumb_height / 9
-
- # pop all messages from the bus so we won't be flooded with messages
- # from the prerolling phase
- while self.pipeline.get_bus().pop():
- continue
- # add a message handler that listens for the created pixbufs
- self.pipeline.get_bus().add_signal_watch()
- self.pipeline.get_bus().connect("message", self.bus_message_handler)
-
- def _startThumbnailing(self):
- self.queue = []
- query_success, duration = self.pipeline.query_duration(Gst.Format.TIME)
- if not query_success:
- print("Could not determine the duration of the file {}".format(self.uri))
- duration = self.duration
- else:
- self.duration = duration
-
- current_time = 0
- while current_time < duration:
- self.queue.append(current_time)
- current_time += self.thumb_period
-
- self._create_next_thumb()
-
- def _create_next_thumb(self):
- if not self.queue:
- # nothing left to do
- self.thumb_cache.commit()
- return
- wish = self._get_wish()
- if wish:
- time = wish
- self.queue.remove(wish)
- else:
- time = self.queue.pop(0)
- # append the time to the end of the queue so that if this seek fails
- # another try will be started later
- self.queue.append(time)
-
- self.pipeline.seek(1.0,
- Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.ACCURATE,
- Gst.SeekType.SET, time,
- Gst.SeekType.NONE, -1)
-
- def _addAllThumbnails(self):
- self.remove_all_children()
- self.thumbs = {}
-
- thumb_duration_tmp = Zoomable.pixelToNs(self.thumb_width + self.thumb_margin)
-
- # quantize thumb length to thumb_period
- # TODO: replace with a call to utils.misc.quantize:
- thumb_duration = (thumb_duration_tmp // self.thumb_period) * self.thumb_period
- # make sure that the thumb duration after the quantization isn't smaller than before
- if thumb_duration < thumb_duration_tmp:
- thumb_duration += self.thumb_period
-
- # make sure that we don't show thumbnails more often than thumb_period
- thumb_duration = max(thumb_duration, self.thumb_period)
-
- current_time = 0
- while current_time < self.duration:
- thumb = Thumbnail(self.thumb_width, self.thumb_height)
- thumb.set_position(Zoomable.nsToPixel(current_time), self.thumb_margin)
- self.add_child(thumb)
- self.thumbs[current_time] = thumb
- if current_time in self.thumb_cache:
- gdkpixbuf = self.thumb_cache[current_time]
- self.thumbs[current_time].set_from_gdkpixbuf(gdkpixbuf)
- current_time += thumb_duration
-
- self._updateWishlist()
-
- def _inpoint_changed_cb(self, unused_bElement, unused_value):
- position = Clutter.Point()
- position.x = Zoomable.nsToPixel(self.bElement.props.in_point)
- self.scroll_to_point(position)
- self._updateWishlist()
-
- def _updateWishlist(self):
- """
- Adds thumbnails for the whole clip.
-
- Takes the zoom setting into account and removes potentially
- existing thumbnails prior to adding the new ones.
- """
- self.wishlist = []
-
- # calculate unquantized length of a thumb in nano seconds
- thumb_duration_tmp = Zoomable.pixelToNs(self.thumb_width + self.thumb_margin)
-
- # quantize thumb length to thumb_period
- # TODO: replace with a call to utils.misc.quantize:
- thumb_duration = (thumb_duration_tmp // self.thumb_period) * self.thumb_period
- # make sure that the thumb duration after the quantization isn't smaller than before
- if thumb_duration < thumb_duration_tmp:
- thumb_duration += self.thumb_period
-
- # make sure that we don't show thumbnails more often than thumb_period
- thumb_duration = max(thumb_duration, self.thumb_period)
-
- element_left, element_right = self._get_visible_range()
- # TODO: replace with a call to utils.misc.quantize:
- element_left = (element_left // thumb_duration) * thumb_duration
-
- current_time = element_left
- while current_time < element_right:
- if current_time not in self.thumb_cache:
- self.wishlist.append(current_time)
- current_time += thumb_duration
-
- def _get_wish(self):
- """Returns a wish that is also in the queue or None
- if no such wish exists"""
- while True:
- if not self.wishlist:
- return None
- wish = self.wishlist.pop(0)
- if wish in self.queue:
- return wish
-
- def _setThumbnail(self, time, thumbnail):
- # TODO: is "time" guaranteed to be nanosecond precise?
- # => __tim says: "that's how it should be"
- # => also see gst-plugins-good/tests/icles/gdkpixbufsink-test
- # => Daniel: It is *not* nanosecond precise when we remove the videorate
- # element from the pipeline
- if time in self.queue:
- self.queue.remove(time)
-
- self.thumb_cache[time] = thumbnail
-
- if time in self.thumbs:
- self.thumbs[time].set_from_gdkpixbuf(thumbnail)
-
- # Interface (Zoomable)
-
- def zoomChanged(self):
- self._update()
-
- def _get_visible_range(self):
- timeline_left, timeline_right = self._get_visible_timeline_range()
- element_left = timeline_left - self.bElement.props.start + self.bElement.props.in_point
- element_left = max(element_left, self.bElement.props.in_point)
-
- element_right = timeline_right - self.bElement.props.start + self.bElement.props.in_point
- element_right = min(element_right, self.bElement.props.in_point + self.bElement.props.duration)
-
- return (element_left, element_right)
-
- # TODO: move to Timeline or to utils
- def _get_visible_timeline_range(self):
- # determine the visible left edge of the timeline
- # TODO: isn't there some easier way to get the scroll point of the ScrollActor?
- # timeline_left = -(self.timeline.get_transform().xw - self.timeline.props.x)
- timeline_left = self.timeline.get_scroll_point().x
-
- # determine the width of the pipeline
- # by intersecting the timeline's and the stage's allocation
- timeline_allocation = self.timeline.props.allocation
- stage_allocation = self.timeline.get_stage().props.allocation
-
- timeline_rect = Clutter.Rect()
- timeline_rect.init(timeline_allocation.x1,
- timeline_allocation.y1,
- timeline_allocation.x2 - timeline_allocation.x1,
- timeline_allocation.y2 - timeline_allocation.y1)
-
- stage_rect = Clutter.Rect()
- stage_rect.init(stage_allocation.x1,
- stage_allocation.y1,
- stage_allocation.x2 - stage_allocation.x1,
- stage_allocation.y2 - stage_allocation.y1)
-
- has_intersection, intersection = timeline_rect.intersection(stage_rect)
-
- if not has_intersection:
- return (0, 0)
-
- timeline_width = intersection.size.width
-
- # determine the visible right edge of the timeline
- timeline_right = timeline_left + timeline_width
-
- # convert to nanoseconds
- time_left = Zoomable.pixelToNs(timeline_left)
- time_right = Zoomable.pixelToNs(timeline_right)
-
- return (time_left, time_right)
-
- # Callbacks
-
- def bus_message_handler(self, unused_bus, message):
- if message.type == Gst.MessageType.ELEMENT and \
- message.src == self.gdkpixbufsink:
- struct = message.get_structure()
- struct_name = struct.get_name()
- if struct_name == "preroll-pixbuf":
- self._setThumbnail(struct.get_value("stream-time"), struct.get_value("pixbuf"))
- elif message.type == Gst.MessageType.ASYNC_DONE:
- self._create_next_thumb()
- return Gst.BusSyncReply.PASS
-
- def duration_changed(self, unused_bElement, unused_value):
- new_duration = max(self.duration, self.bElement.props.duration)
- if new_duration > self.duration:
- self.duration = new_duration
- self._update()
-
-
-class Thumbnail(Clutter.Actor):
- def __init__(self, width, height):
- Clutter.Actor.__init__(self)
- image = Clutter.Image.new()
- self.props.content = image
- self.width = width
- self.height = height
- self.set_background_color(Clutter.Color.new(0, 100, 150, 100))
- self.set_size(self.width, self.height)
-
- def set_from_gdkpixbuf(self, gdkpixbuf):
- row_stride = gdkpixbuf.get_rowstride()
- pixel_data = gdkpixbuf.get_pixels()
- alpha = gdkpixbuf.get_has_alpha()
- if alpha:
- self.props.content.set_data(pixel_data, Cogl.PixelFormat.RGBA_8888, self.width, self.height,
row_stride)
- else:
- self.props.content.set_data(pixel_data, Cogl.PixelFormat.RGB_888, self.width, self.height,
row_stride)
-
-
-# TODO: replace with utils.misc.hash_file
-def hash_file(uri):
- """Hashes the first 256KB of the specified file"""
- sha256 = hashlib.sha256()
- with open(uri, "rb") as file:
- for _ in range(1024):
- chunk = file.read(256)
- if not chunk:
- break
- sha256.update(chunk)
- return sha256.hexdigest()
-
-# TODO: remove eventually
-autocreate = True
-
-
-# TODO: replace with pitivi.settings.get_dir
-def get_dir(path, autocreate=True):
- if autocreate and not os.path.exists(path):
- os.makedirs(path)
- return path
-
-
-class ThumbnailCache(object):
-
- """Caches thumbnails by key using LRU policy, implemented with heapq.
-
- Uses a two stage caching mechanism. A limited number of elements are
- held in memory, the rest is being cached on disk using an sqlite db."""
-
- def __init__(self, uri):
- object.__init__(self)
- # TODO: replace with utils.misc.hash_file
- filehash = hash_file(Gst.uri_get_location(uri))
- # TODO: replace with pitivi.settings.xdg_cache_home()
- cache_dir = get_dir(os.path.join(xdg_dirs.xdg_cache_home, "pitivi"), autocreate)
- dbfile = os.path.join(get_dir(os.path.join(cache_dir, "thumbs")), filehash)
- self.conn = sqlite3.connect(dbfile)
- self.cur = self.conn.cursor()
- self.cur.execute("CREATE TABLE IF NOT EXISTS Thumbs\
- (Time INTEGER NOT NULL PRIMARY KEY,\
- Jpeg BLOB NOT NULL)")
-
- def __contains__(self, key):
- # check if item is present in on disk cache
- self.cur.execute("SELECT Time FROM Thumbs WHERE Time = ?", (key,))
- if self.cur.fetchone():
- return True
- return False
-
- def __getitem__(self, key):
- self.cur.execute("SELECT * FROM Thumbs WHERE Time = ?", (key,))
- row = self.cur.fetchone()
- if row:
- jpeg = row[1]
- loader = GdkPixbuf.PixbufLoader.new()
- # TODO: what do to if any of the following calls fails?
- loader.write(jpeg)
- loader.close()
- pixbuf = loader.get_pixbuf()
- return pixbuf
- raise KeyError(key)
-
- def __setitem__(self, key, value):
- success, jpeg = value.save_to_bufferv("jpeg", ["quality", None], ["90"])
- if not success:
- self.warning("JPEG compression failed")
- return
- blob = sqlite3.Binary(jpeg)
- #Replace if the key already existed
- self.cur.execute("DELETE FROM Thumbs WHERE time=?", (key,))
- self.cur.execute("INSERT INTO Thumbs VALUES (?,?)", (key, blob,))
- #self.conn.commit()
-
- def commit(self):
- print("commit")
- self.conn.commit()
-
if __name__ == "__main__":
# Basic argument handling, no need for getopt here
if len(sys.argv) < 2:
diff --git a/pitivi/utils/ui.py b/pitivi/utils/ui.py
index 79e9e6e..8655051 100644
--- a/pitivi/utils/ui.py
+++ b/pitivi/utils/ui.py
@@ -52,9 +52,12 @@ LAYER_HEIGHT_EXPANDED = 50
LAYER_HEIGHT_COLLAPSED = 15
TRACK_SPACING = 8
-SPACING = 6
+EXPANDED_SIZE = 65
+
PADDING = 6
+SPACING = 10
+
CANVAS_SPACING = 21
# Layer creation blocking time in s
@@ -197,7 +200,7 @@ def beautify_info(info):
def info_name(info):
"""Return a human-readable filename (without the path and quoting)."""
- if isinstance(info, GES.Asset):
+ if isinstance(info, GES.Asset):
filename = unquote(os.path.basename(info.get_id()))
else:
filename = unquote(os.path.basename(info.get_uri()))
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]