[pitivi] Port the timeline to Gtk+ and remove clutter dependency!
- From: Thibault Saunier <tsaunier src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [pitivi] Port the timeline to Gtk+ and remove clutter dependency!
- Date: Thu, 11 Jun 2015 09:15:02 +0000 (UTC)
commit 91b7b3753861c9ec5be11acc3744d1ca9ac19a89
Author: Thibault Saunier <tsaunier gnome org>
Date: Thu Dec 18 19:36:50 2014 +0100
Port the timeline to Gtk+ and remove clutter dependency!
Summary:
With help from Mathieu Duponchelle <mathieu duonchelle opencreed com>
for the keyframes
Maniphest Tasks: T31
Reviewers: Mathieu_Du
Differential Revision: http://phabricator.freedesktop.org/D111
pitivi/application.py | 4 +-
pitivi/check.py | 10 +-
pitivi/mainwindow.py | 12 +-
pitivi/project.py | 15 +-
pitivi/timeline/Makefile.am | 3 +-
pitivi/timeline/controls.py | 180 -----
pitivi/timeline/elements.py | 1678 ++++++++++++++++-------------------------
pitivi/timeline/layer.py | 253 ++++++-
pitivi/timeline/previewers.py | 346 +++------
pitivi/timeline/ruler.py | 19 +-
pitivi/timeline/timeline.py | 1523 ++++++++++++++++++++------------------
pitivi/transitions.py | 7 +-
pitivi/undo/timeline.py | 1 +
pitivi/utils/pipeline.py | 16 +-
pitivi/utils/timeline.py | 40 +-
pitivi/utils/ui.py | 90 ++-
pitivi/utils/validate.py | 147 ++++-
pitivi/utils/widgets.py | 8 +-
pitivi/viewer.py | 12 +-
tests/test_utils.py | 4 +-
20 files changed, 2118 insertions(+), 2250 deletions(-)
---
diff --git a/pitivi/application.py b/pitivi/application.py
index ac66e1f..c2eb101 100644
--- a/pitivi/application.py
+++ b/pitivi/application.py
@@ -24,8 +24,6 @@
import os
import time
-from datetime import datetime
-
from gi.repository import GObject
from gi.repository import Gio
from gi.repository import Gtk
@@ -231,6 +229,8 @@ class Pitivi(Gtk.Application, Loggable):
def _setScenarioFile(self, uri):
if 'PITIVI_SCENARIO_FILE' in os.environ:
uri = quote_uri(os.environ['PITIVI_SCENARIO_FILE'])
+ if uri:
+ project_path = path_from_uri(uri)
else:
cache_dir = get_dir(os.path.join(xdg_cache_home(), "scenarios"))
scenario_name = str(time.strftime("%Y%m%d-%H%M%S"))
diff --git a/pitivi/check.py b/pitivi/check.py
index 3cea5ec..a3d3627 100644
--- a/pitivi/check.py
+++ b/pitivi/check.py
@@ -177,7 +177,7 @@ class GstDependency(GIDependency):
return list(module.version())
-class GtkOrClutterDependency(GIDependency):
+class GtkDependency(GIDependency):
def _format_version(self, module):
return [module.MAJOR_VERSION, module.MINOR_VERSION, module.MICRO_VERSION]
@@ -254,8 +254,6 @@ def initialize_modules():
"""
from gi.repository import Gdk
Gdk.init([])
- from gi.repository import GtkClutter
- GtkClutter.init([])
import gi
if not gi.version_info >= (3, 11):
@@ -287,13 +285,13 @@ Some of our dependencies have version numbers requirements; for those without
a specific version requirement, they have the "None" value.
"""
HARD_DEPENDENCIES = [CairoDependency("1.10.0"),
- GtkOrClutterDependency("Clutter", "1.12.0"),
GstDependency("Gst", "1.4.0"),
GstDependency("GES", "1.5.0.0"),
- GtkOrClutterDependency("Gtk", "3.10.0"),
+ GtkDependency("Gtk", "3.10.0"),
ClassicDependency("numpy", None),
GIDependency("Gio", None),
- GstPluginDependency("opengl", "1.4.0")
+ GstPluginDependency("opengl", "1.4.0"),
+ ClassicDependency("matplotlib", None),
]
SOFT_DEPENDENCIES = \
diff --git a/pitivi/mainwindow.py b/pitivi/mainwindow.py
index 82f2750..95d58e5 100644
--- a/pitivi/mainwindow.py
+++ b/pitivi/mainwindow.py
@@ -48,7 +48,7 @@ from pitivi.transitions import TransitionsListWidget
from pitivi.utils.loggable import Loggable
from pitivi.utils.misc import show_user_manual, path_from_uri
from pitivi.utils.ui import info_name, beautify_time_delta, SPACING, \
- beautify_length
+ beautify_length, TIMELINE_CSS
from pitivi.viewer import ViewerContainer
@@ -144,6 +144,7 @@ class PitiviMainWindow(Gtk.ApplicationWindow, Loggable):
self.connect("destroy", self._destroyedCb)
self.uimanager = Gtk.UIManager()
+ self.setupCss()
self.builder_handler_ids = []
self.builder = Gtk.Builder()
self.add_accel_group(self.uimanager.get_accel_group())
@@ -167,6 +168,14 @@ class PitiviMainWindow(Gtk.ApplicationWindow, Loggable):
pm.connect("project-closed", self._projectManagerProjectClosedCb)
pm.connect("missing-uri", self._projectManagerMissingUriCb)
+ def setupCss(self):
+ self.css_provider = Gtk.CssProvider()
+ self.css_provider.load_from_data(TIMELINE_CSS.encode('UTF-8'))
+ screen = Gdk.Screen.get_default()
+ style_context = self.get_style_context()
+ style_context.add_provider_for_screen(screen, self.css_provider,
+ Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
+
@staticmethod
def createStockIcons():
"""
@@ -627,6 +636,7 @@ class PitiviMainWindow(Gtk.ApplicationWindow, Loggable):
comments = ["",
"GES %s" % ".".join(map(str, GES.version())),
+ "Gtk %s" % ".".join(map(str, (Gtk.MAJOR_VERSION, Gtk.MINOR_VERSION))),
"GStreamer %s" % ".".join(map(str, Gst.version()))]
abt.set_comments("\n".join(comments))
diff --git a/pitivi/project.py b/pitivi/project.py
index 7430c2e..7ef75ad 100644
--- a/pitivi/project.py
+++ b/pitivi/project.py
@@ -787,8 +787,13 @@ class Project(Loggable, GES.Project):
self._acodecsettings_cache = {}
self._has_rendering_values = False
+ self.runner = None
+ self.monitor = None
+ self._scenario = None
+
def _scenarioDoneCb(self, scenario):
- self.pipeline.setForcePositionListener(False)
+ if self.pipeline is not None:
+ self.pipeline.setForcePositionListener(False)
def setupValidateScenario(self):
from gi.repository import GstValidate
@@ -1150,8 +1155,16 @@ class Project(Loggable, GES.Project):
return self.list_assets(GES.UriClip)
def release(self):
+ if self.runner:
+ self.runner.printf()
+
if self.pipeline:
self.pipeline.release()
+
+ if self.runner:
+ self.runner = None
+ self.monitor = None
+
self.pipeline = None
self.timeline = None
diff --git a/pitivi/timeline/Makefile.am b/pitivi/timeline/Makefile.am
index cf89178..b08f86a 100644
--- a/pitivi/timeline/Makefile.am
+++ b/pitivi/timeline/Makefile.am
@@ -6,8 +6,7 @@ timeline_PYTHON = \
ruler.py \
timeline.py \
elements.py \
- previewers.py \
- controls.py
+ previewers.py
clean-local:
rm -rf *.pyc *.pyo
diff --git a/pitivi/timeline/elements.py b/pitivi/timeline/elements.py
index 18729d8..a91db08 100644
--- a/pitivi/timeline/elements.py
+++ b/pitivi/timeline/elements.py
@@ -25,1174 +25,778 @@ 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 cairo
-import math
import os
-from datetime import datetime
-import weakref
+from gi.repository import GES
+from gi.repository import Gtk
+from gi.repository import Gdk
+from gi.repository import GdkPixbuf
+from gi.repository import GstController
+from gi.repository import GObject
-from gi.repository import Clutter, Gtk, GtkClutter, GES, Gdk, Gst, GstController
-from pitivi.utils.timeline import Zoomable, EditingContext, SELECT, UNSELECT, SELECT_ADD, Selected
-from .previewers import AudioPreviewer, VideoPreviewer
+from pitivi.utils import ui
+from pitivi.utils import misc
+from pitivi import configure
+from pitivi.timeline import previewers
+from pitivi.utils.loggable import Loggable
+from pitivi.utils import timeline as timelineUtils
-import pitivi.configure as configure
-from pitivi.utils.ui import EXPANDED_SIZE, SPACING, KEYFRAME_SIZE, CONTROL_WIDTH
+from matplotlib.figure import Figure
+from matplotlib.backends.backend_gtk3cairo import FigureCanvasGTK3Cairo as FigureCanvas
+import numpy
-# Colors for keyframes and clips (RGBA)
-KEYFRAME_LINE_COLOR = (237, 212, 0, 255) # "Tango" yellow
-KEYFRAME_NORMAL_COLOR = Clutter.Color.new(0, 0, 0, 200)
-KEYFRAME_SELECTED_COLOR = Clutter.Color.new(200, 200, 200, 200)
-CLIP_SELECTED_OVERLAY_COLOR = Clutter.Color.new(60, 60, 60, 100)
-GHOST_CLIP_COLOR = Clutter.Color.new(255, 255, 255, 50)
-TRANSITION_COLOR = Clutter.Color.new(35, 85, 125, 125) # light blue
+KEYFRAME_LINE_COLOR = (237, 212, 0) # "Tango" yellow
-BORDER_NORMAL_COLOR = Clutter.Color.new(100, 100, 100, 255)
-BORDER_SELECTED_COLOR = Clutter.Color.new(200, 200, 10, 255)
+CURSORS = {
+ GES.Edge.EDGE_START: Gdk.Cursor.new(Gdk.CursorType.LEFT_SIDE),
+ GES.Edge.EDGE_END: Gdk.Cursor.new(Gdk.CursorType.RIGHT_SIDE)
+}
NORMAL_CURSOR = Gdk.Cursor.new(Gdk.CursorType.LEFT_PTR)
DRAG_CURSOR = Gdk.Cursor.new(Gdk.CursorType.HAND1)
-DRAG_LEFT_HANDLEBAR_CURSOR = Gdk.Cursor.new(Gdk.CursorType.LEFT_SIDE)
-DRAG_RIGHT_HANDLEBAR_CURSOR = Gdk.Cursor.new(Gdk.CursorType.RIGHT_SIDE)
-
-
-class Ghostclip(Clutter.Actor):
-
- """
- The concept of a ghostclip is to represent future actions without
- actually moving GESClips. They are created when the user wants
- to change a clip of layer, and when the user does a drag and drop
- from the media library.
- """
-
- def __init__(self, track_type, bElement=None):
- Clutter.Actor.__init__(self)
- self.track_type = track_type
- self.bElement = bElement
- self.set_background_color(GHOST_CLIP_COLOR)
- self.props.visible = False
- self.shouldCreateLayer = False
-
- def setNbrLayers(self, nbrLayers):
- self.nbrLayers = nbrLayers
-
- def setWidth(self, width):
- self.props.width = width
-
- def update(self, priority, y, isControlledByBrother):
- # Priority and y can be negative when dragging an asset to the ruler.
- # Priority can also be negative when dragging a linked element.
- self.priority = min(max(0, priority), self.nbrLayers)
- y = max(0, y)
-
- # 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 at the end or inserted ?
- if self.priority == self.nbrLayers or y % (EXPANDED_SIZE + SPACING) < SPACING:
- self.shouldCreateLayer = True
- self.set_size(self.props.width, SPACING)
- self.props.y = self.priority * (EXPANDED_SIZE + SPACING)
- if self.track_type == GES.TrackType.AUDIO:
- self.props.y += self.nbrLayers * (EXPANDED_SIZE + SPACING)
- self.props.visible = True
- else:
- self.shouldCreateLayer = False
- # No need to mockup on the same layer
- if self.bElement and self.priority == self.bElement.get_parent().get_layer().get_priority():
- self.props.visible = False
- # We would be moving to an existing layer.
- elif self.priority < self.nbrLayers:
- self.set_size(self.props.width, EXPANDED_SIZE)
- self.props.y = self.priority * \
- (EXPANDED_SIZE + SPACING) + SPACING
- if self.track_type == GES.TrackType.AUDIO:
- self.props.y += self.nbrLayers * (EXPANDED_SIZE + SPACING)
- self.props.visible = True
-
- def getLayerForY(self, y):
- if self.track_type == GES.TrackType.AUDIO:
- y -= self.nbrLayers * (EXPANDED_SIZE + SPACING)
- priority = int(y / (EXPANDED_SIZE + SPACING))
-
- return priority
-
-
-class TrimHandle(Clutter.Texture):
-
- def __init__(self, timelineElement, isLeft):
- Clutter.Texture.__init__(self)
-
- self.isLeft = isLeft
- self.timelineElement = weakref.proxy(timelineElement)
- self.dragAction = Clutter.DragAction()
-
- self.set_from_file(
- os.path.join(configure.get_pixmap_dir(), "trimbar-normal.png"))
- self.set_size(-1, EXPANDED_SIZE)
- self.hide()
- self.set_reactive(True)
-
- 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)
-
- def cleanup(self):
- self.disconnect_by_func(self._enterEventCb)
- self.disconnect_by_func(self._leaveEventCb)
- self.timelineElement.disconnect_by_func(self._elementEnterEventCb)
- self.timelineElement.disconnect_by_func(self._elementLeaveEventCb)
+
+
+class KeyframeCurve(FigureCanvas, Loggable):
+
+ __gsignals__ = {
+ # Signal our values changed, and a redraw will be needed
+ "plot-changed": (GObject.SIGNAL_RUN_LAST, None, ()),
+ }
+
+ def __init__(self, timeline, source):
+ figure = Figure()
+ FigureCanvas.__init__(self, figure)
+ Loggable.__init__(self)
+
+ self.__timeline = timeline
+ self.__source = source
+
+ # Curve values, basically separating source.get_values() timestamps
+ # and values.
+ self.__line_xs = []
+ self.__line_ys = []
+
+ # axisbg to None for transparency
+ self.__ax = figure.add_axes([0, 0, 1, 1], axisbg='None')
+ self.__ax.cla()
+
+ # FIXME: drawing a grid and ticks would be nice, but
+ # matplotlib is too slow for now.
+ self.__ax.grid(False)
+
+ self.__ax.tick_params(axis='both',
+ which='both',
+ bottom='off',
+ top='off',
+ right='off',
+ left='off')
+
+ # This seems to also be necessary for transparency ..
+ figure.patch.set_visible(False)
+
+ # The actual Line2D object
+ self.__line = None
+
+ # The PathCollection as returned by scatter
+ self.__keyframes = None
+
+ sizes = [100]
+ colors = ['r']
+
+ self.__keyframes = self.__ax.scatter([], [], marker='o', s=sizes,
+ c=colors, zorder=2)
+ self.__line = self.__ax.plot([], [],
+ linewidth=2.0, zorder=1)[0]
+ self.__updatePlots()
+
+ # Drag and drop logic
+ self.__dragged = False
+ self.__offset = None
+ self.__handling_motion = False
+ self.connect("event", self._eventCb)
+
+ self.mpl_connect('button_press_event', self.__mplButtonPressEventCb)
+ self.mpl_connect(
+ 'button_release_event', self.__mplButtonReleaseEventCb)
+ self.mpl_connect('motion_notify_event', self.__mplMotionEventCb)
+
+ # Private methods
+ def __updatePlots(self):
+ values = self.__source.get_all()
+
+ self.__line_xs = []
+ self.__line_ys = []
+ for value in values:
+ self.__line_xs.append(value.timestamp)
+ self.__line_ys.append(value.value)
+ self.__ax.set_xlim(self.__line_xs[0], self.__line_xs[-1])
+ self.__ax.set_ylim(0.0, 1.0)
+
+ arr = numpy.array((self.__line_xs, self.__line_ys))
+ arr = arr.transpose()
+ self.__keyframes.set_offsets(arr)
+ self.__line.set_xdata(self.__line_xs)
+ self.__line.set_ydata(self.__line_ys)
+ self.emit("plot-changed")
+
+ def __maybeCreateKeyframe(self, event):
+ result = self.__line.contains(event)
+ if result[0]:
+ self.__source.set(event.xdata, event.ydata)
+ self.__updatePlots()
# Callbacks
+ def _eventCb(self, element, event):
+ if event.type == Gdk.EventType.LEAVE_NOTIFY:
+ cursor = NORMAL_CURSOR
+ self.__timeline.get_window().set_cursor(cursor)
+ elif event.type == Gdk.EventType.MOTION_NOTIFY:
+ # We need to do that here, because mpl's callbacks can't stop
+ # signal propagation.
+ if self.__handling_motion:
+ return True
+ return False
- def _enterEventCb(self, unused_actor, unused_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(
- DRAG_LEFT_HANDLEBAR_CURSOR)
- else:
- self.timelineElement.timeline._container.embed.get_window().set_cursor(
- DRAG_RIGHT_HANDLEBAR_CURSOR)
-
- def _leaveEventCb(self, unused_actor, event):
- self.timelineElement.set_reactive(True)
- children = self.timelineElement.get_children()
-
- other_actor = self.timelineElement.timeline._container.stage.get_actor_at_pos(
- Clutter.PickMode.ALL, event.x, event.y)
- if other_actor not in children:
- self.timelineElement.hideHandles()
-
- for elem in 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(
- NORMAL_CURSOR)
-
- def _elementEnterEventCb(self, unused_actor, unused_event):
- self.show()
+ def __mplButtonPressEventCb(self, event):
+ result = self.__keyframes.contains(event)
+ if result[0]:
+ self.__handling_motion = True
+ self.__offset = \
+ self.__keyframes.get_offsets()[result[1]['ind'][0]][0]
- def _elementLeaveEventCb(self, unused_actor, unused_event):
- self.hide()
+ def __mplMotionEventCb(self, event):
+ if not self.props.visible:
+ return
- def _dragBeginCb(self, unused_action, unused_actor, event_x, event_y, unused_modifiers):
- self.dragBeginStartX = event_x
- self.dragBeginStartY = event_y
- elem = self.timelineElement.bElement.get_parent()
- self.timelineElement.setDragged(True)
+ if self.__offset is not None:
+ self.__dragged = True
+ # Check that the mouse event still is in the figure boundaries
+ if event.ydata is not None and event.xdata is not None:
+ self.__source.unset(int(self.__offset))
+ self.__source.set(event.xdata, event.ydata)
+ self.__offset = event.xdata
+ self.__updatePlots()
- 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,
- None,
- self.timelineElement.timeline._container.app.action_log)
-
- self._context.connect("clip-trim", self.clipTrimCb)
- self._context.connect("clip-trim-finished", self.clipTrimFinishedCb)
-
- def _dragProgressCb(self, unused_action, unused_actor, delta_x, unused_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.setMode(
- self.timelineElement.timeline._container.getEditionMode(isAHandle=True))
- self._context.editTo(
- new_start, self.timelineElement.bElement.get_parent().get_layer().get_priority())
- return False
+ cursor = NORMAL_CURSOR
+ result = self.__line.contains(event)
+ if result[0]:
+ cursor = DRAG_CURSOR
- def _dragEndCb(self, unused_action, unused_actor, unused_event_x, unused_event_y, unused_modifiers):
- self.timelineElement.setDragged(False)
- self._context.finish()
+ self.__timeline.get_window().set_cursor(
+ cursor)
- self.timelineElement.set_reactive(True)
- for elem in self.timelineElement.get_children():
- elem.set_reactive(True)
+ def __mplButtonReleaseEventCb(self, event):
+ self.__offset = None
+ self.__handling_motion = False
- self.set_from_file(
- os.path.join(configure.get_pixmap_dir(), "trimbar-normal.png"))
- self.timelineElement.timeline._container.embed.get_window().set_cursor(
- NORMAL_CURSOR)
+ if not self.__dragged:
+ self.__maybeCreateKeyframe(event)
+ self.__dragged = False
- def clipTrimCb(self, unused_TrimStartContext, tl_obj, position):
- # While a clip is being trimmed, ask the viewer to preview it
- self.timelineElement.timeline._container.app.gui.viewer.clipTrimPreview(
- tl_obj, position)
- def clipTrimFinishedCb(self, unused_TrimStartContext):
- # When a clip has finished trimming, tell the viewer to reset itself
- self.timelineElement.timeline._container.app.gui.viewer.clipTrimPreviewFinished(
- )
+class TimelineElement(Gtk.Layout, timelineUtils.Zoomable, Loggable):
+ def __init__(self, element, timeline):
+ super(TimelineElement, self).__init__()
+ timelineUtils.Zoomable.__init__(self)
+ Loggable.__init__(self)
-class TimelineElement(Clutter.Actor, Zoomable):
+ self.timeline = timeline
+ self._bElement = element
+ self._bElement.selected = timelineUtils.Selected()
+ self._bElement.selected.connect(
+ "selected-changed", self.__selectedChangedCb)
- """
- @ivar bElement: the backend element.
- @type bElement: GES.TrackElement
- @ivar timeline: the containing graphic timeline.
- @type timeline: TimelineStage
- """
+ self.__width = self.__height = 0
- def __init__(self, bElement, timeline):
- Zoomable.__init__(self)
- Clutter.Actor.__init__(self)
+ # Needed for effect's keyframe toggling
+ self._bElement.ui_element = self
- self.timeline = timeline
- self.bElement = bElement
- self.bElement.selected = Selected()
- self.bElement.ui_element = weakref.proxy(self)
- self.track_type = self.bElement.get_track_type() # This won't change
- self.isDragged = False
- self.lines = []
- self.keyframes = []
- self.keyframesVisible = False
- self.source = None
- self.keyframedElement = None
- self.rightHandle = None
- self.isSelected = False
- self.updating_keyframes = False
- size = self.bElement.get_duration()
+ self.props.vexpand = True
- self.background = self._createBackground()
- self.background.set_position(1, 1)
- self.add_child(self.background)
+ self.__previewer = self._getPreviewer()
+ if self.__previewer:
+ self.add(self.__previewer)
- self.preview = self._createPreview()
- self.add_child(self.preview)
+ self.__background = self._getBackground()
+ if self.__background:
+ self.add(self.__background)
- self.border = self._createBorder()
- self.add_child(self.border)
+ self.__keyframeCurve = None
+ self.show_all()
- self.set_child_below_sibling(self.border, self.background)
+ # We set up the default mixing property right here, if a binding was
+ # already set (when loading a project), it will be added later
+ # and override that one.
+ self.__controlledProperty = self._getDefaultMixingProperty()
+ if self.__controlledProperty:
+ self.__createControlBinding(self._bElement)
- self.marquee = self._createMarquee()
- self.add_child(self.marquee)
+ # Public API
+ def setSize(self, width, height):
+ width = max(0, width)
+ self.set_size_request(width, height)
+
+ if self.__previewer:
+ self.__previewer.set_size_request(width, height)
+
+ if self.__background:
+ self.__background.set_size_request(width, height)
+
+ if self.__keyframeCurve:
+ self.__keyframeCurve.set_size_request(width, height)
+
+ self.__width = width
+ self.__height = height
+
+ def showKeyframes(self, effect, prop):
+ self.__controlledProperty = prop
+ self.__createControlBinding(effect)
+
+ # Private methods
+ def __createKeyframeCurve(self, binding):
+ source = binding.props.control_source
+ values = source.get_all()
+
+ if len(values) < 2:
+ source.unset_all()
+ val = float(self.__controlledProperty.default_value) / \
+ (self.__controlledProperty.maximum -
+ self.__controlledProperty.minimum)
+ source.set(self._bElement.props.in_point, val)
+ source.set(
+ self._bElement.props.duration + self._bElement.props.in_point,
+ val)
+
+ if self.__keyframeCurve:
+ self.__keyframeCurve.disconnect_by_func(
+ self.__keyframePlotChangedCb)
+ self.remove(self.__keyframeCurve)
+
+ self.__keyframeCurve = KeyframeCurve(self.timeline, source)
+ self.__keyframeCurve.connect("plot-changed",
+ self.__keyframePlotChangedCb)
+ self.add(self.__keyframeCurve)
+ self.__keyframeCurve.set_size_request(self.__width, self.__height)
+ self.__keyframeCurve.props.visible = bool(self._bElement.selected)
+ self.queue_draw()
+
+ def __createControlBinding(self, element):
+ if self.__controlledProperty:
+ element.connect("control-binding-added",
+ self.__controlBindingAddedCb)
+ binding = \
+ element.get_control_binding(self.__controlledProperty.name)
+
+ if binding:
+ self.__createKeyframeCurve(binding)
- self._createHandles()
+ source = GstController.InterpolationControlSource()
+ source.props.mode = GstController.InterpolationMode.LINEAR
+ element.set_control_source(source,
+ self.__controlledProperty.name, "direct")
- self._linesMarker = self._createMarker()
- self._keyframesMarker = self._createMarker()
+ def __controlBindingAddedCb(self, unused_bElement, binding):
+ if binding.props.name == self.__controlledProperty.name:
+ self.__createKeyframeCurve(binding)
- self._createGhostclip()
+ # Gtk implementation
+ def do_set_property(self, property_id, value, pspec):
+ Gtk.Layout.do_set_property(self, property_id, value, pspec)
- self.update(True)
- self.set_reactive(True)
+ def do_get_preferred_width(self):
+ wanted_width = max(
+ 0, self.nsToPixel(self._bElement.props.duration) - TrimHandle.DEFAULT_WIDTH * 2)
- self._createMixingKeyframes()
+ return wanted_width, wanted_width
- self._connectToEvents()
+ def do_draw(self, cr):
+ self.propagate_draw(self.__background, cr)
- def _valueChanged(self, source, value):
- if self.updating_keyframes is True:
- return
+ if self.__previewer:
+ self.propagate_draw(self.__previewer, cr)
- self.updateKeyframes()
+ if self.__keyframeCurve and self._bElement.selected:
+ self.__keyframeCurve.draw()
+ self.propagate_draw(self.__keyframeCurve, cr)
- def _valueAddedCb(self, source, value):
- if self.updating_keyframes is True:
- return
+ def do_show_all(self):
+ for child in self.get_children():
+ if bool(self._bElement.selected) or child != self.__keyframeCurve:
+ child.show_all()
- self.updateKeyframes()
+ self.show()
- def _valueRemovedCb(self, source, value):
- if self.updating_keyframes is True:
- return
+ # Callbacks
+ def __selectedChangedCb(self, unused_bElement, selected):
+ if self.__keyframeCurve:
+ self.__keyframeCurve.props.visible = selected
- self.updateKeyframes()
+ def __keyframePlotChangedCb(self, unused_curve):
+ self.queue_draw()
- # Public API
+ # Virtual methods
+ def _getPreviewer(self):
+ """
+ Should return a GtkWidget offering a representation of the
+ medium (waveforms for audio, thumbnails for video ..).
+ This previewer will be automatically scaled to the width and
+ height of the TimelineElement.
+ """
+ return None
- 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)
- if self.rightHandle:
- self.rightHandle.save_easing_state()
- self.rightHandle.set_easing_duration(600)
-
- self.marquee.set_size(width, height)
- self.background.props.width = max(width - 2, 1)
- self.background.props.height = max(height - 2, 1)
- self.border.props.width = width
- self.border.props.height = height
- self.props.width = width
- self.props.height = height
- self.preview.set_size(max(width - 2, 1), max(height - 2, 1))
- if self.rightHandle:
- self.rightHandle.set_position(
- width - self.rightHandle.props.width, 0)
-
- if ease:
- self.background.restore_easing_state()
- self.border.restore_easing_state()
- self.preview.restore_easing_state()
- if self.rightHandle:
- self.rightHandle.restore_easing_state()
- self.restore_easing_state()
-
- def addKeyframe(self, value, timestamp):
- self.timeline._container.app.action_log.begin("Add KeyFrame")
- self.updating_keyframes = True
- self.source.set(timestamp, value)
- self.updateKeyframes()
- self.timeline._container.app.action_log.commit()
- self.updating_keyframes = False
-
- def removeKeyframe(self, kf):
- self.timeline._container.app.action_log.begin("Remove KeyFrame")
- self.updating_keyframes = True
- self.source.unset(kf.value.timestamp)
- self.keyframes = sorted(
- self.keyframes, key=lambda keyframe: keyframe.value.timestamp)
- self.updateKeyframes()
- self.timeline._container.app.action_log.commit()
- self.updating_keyframes = False
-
- def showKeyframes(self, element, propname, isDefault=False):
- binding = element.get_control_binding(propname.name)
- if not binding:
- source = GstController.InterpolationControlSource()
- source.props.mode = GstController.InterpolationMode.LINEAR
- if not (element.set_control_source(source, propname.name, "direct")):
- print("There was something like a problem captain")
- return
- binding = element.get_control_binding(propname.name)
+ def _getBackground(self):
+ """
+ Should return a GtkWidget with a unique background color.
+ """
+ return None
- self.binding = binding
- self.prop = propname
- self.keyframedElement = element
- self.source = self.binding.props.control_source
- self.source.connect("value-added", self._valueAddedCb)
- self.source.connect("value-removed", self._valueRemovedCb)
- self.source.connect("value-changed", self._valueChanged)
+ def _getDefaultMixingProperty(self):
+ """
+ Should return a controllable GObject.ParamSpec allowing to mix
+ media on different layers.
+ """
+ return None
- if isDefault:
- self.default_prop = propname
- self.default_element = element
- self.keyframesVisible = True
+class TitleSource(TimelineElement):
- self.updateKeyframes()
+ __gtype_name__ = "PitiviTitleSource"
- def hideKeyframes(self):
- for keyframe in self.keyframes:
- self.remove_child(keyframe)
- self.keyframes = []
+ def __init__(self, element, timeline):
+ super(TitleSource, self).__init__(element, timeline)
+ self.get_style_context().add_class("VideoUriSource")
- self.keyframesVisible = False
+ def _getBackground(self):
+ return VideoBackground()
- if self.isSelected:
- self.showKeyframes(self.default_element, self.default_prop)
+ def do_get_preferred_height(self):
+ return ui.LAYER_HEIGHT / 2, ui.LAYER_HEIGHT
- self.drawLines()
- def setKeyframePosition(self, keyframe, value):
- x = self.nsToPixel(
- value.timestamp - self.bElement.props.in_point) - KEYFRAME_SIZE / 2
- y = EXPANDED_SIZE - (value.value * EXPANDED_SIZE) - KEYFRAME_SIZE / 2
- keyframe.set_position(x, y)
+class VideoBackground (Gtk.Box):
- def drawLines(self, line=None):
- for line_ in self.lines:
- if line_ != line:
- self.remove_child(line_)
+ def __init__(self):
+ super(VideoBackground, self).__init__(self)
+ self.get_style_context().add_class("VideoBackground")
- if line:
- self.lines = [line]
- else:
- self.lines = []
-
- lastKeyframe = None
- for keyframe in self.keyframes:
- if lastKeyframe and (not line or lastKeyframe != line.previousKeyframe):
- self._createLine(keyframe, lastKeyframe, None)
- elif lastKeyframe:
- self._createLine(keyframe, lastKeyframe, line)
- lastKeyframe = keyframe
-
- def updateKeyframes(self):
- if not self.source:
- return
- updating = self.updating_keyframes
- self.updating_keyframes = True
- values = self.source.get_all()
- if len(values) < 2 and self.bElement.props.duration > 0:
- self.source.unset_all()
- val = float(self.prop.default_value) / \
- (self.prop.maximum - self.prop.minimum)
- self.source.set(self.bElement.props.in_point, val)
- self.source.set(
- self.bElement.props.duration + self.bElement.props.in_point, val)
-
- for keyframe in self.keyframes:
- self.remove_child(keyframe)
-
- self.keyframes = []
- values = self.source.get_all()
- values_count = len(values)
- for i, value in enumerate(values):
- has_changeable_time = i > 0 and i < values_count - 1
- keyframe = self._createKeyframe(value, has_changeable_time)
- self.keyframes.append(keyframe)
-
- self.drawLines()
- self.updating_keyframes = updating
-
- def cleanup(self):
- Zoomable.__del__(self)
- self.disconnectFromEvents()
-
- def disconnectFromEvents(self):
- self.dragAction.disconnect_by_func(self._dragProgressCb)
- self.dragAction.disconnect_by_func(self._dragBeginCb)
- self.dragAction.disconnect_by_func(self._dragEndCb)
- self.remove_action(self.dragAction)
- self.bElement.selected.disconnect_by_func(self._selectedChangedCb)
- self.bElement.disconnect_by_func(self._durationChangedCb)
- self.bElement.disconnect_by_func(self._inpointChangedCb)
- self.disconnect_by_func(self._clickedCb)
-
- # private API
-
- def _createMarker(self):
- marker = Clutter.Actor()
- self.add_child(marker)
- return marker
-
- def update(self, ease):
- start = self.bElement.get_start()
- duration = self.bElement.get_duration()
- # The calculation of the duration assumes that the start is always
- # int(pixels_float). In that case, the rounding can add up and a pixel
- # might be lost if we ignore the start of the clip.
- size = self.nsToPixel(start + duration) - self.nsToPixel(start)
- # Avoid elements to become invisible.
- size = max(size, 1)
- self.set_size(size, EXPANDED_SIZE, ease)
-
- def setDragged(self, dragged):
- brother = self.timeline.findBrother(self.bElement)
- if brother:
- brother.isDragged = dragged
- self.isDragged = dragged
-
- def _createMixingKeyframes(self):
- if self.track_type == GES.TrackType.VIDEO:
- propname = "alpha"
- else:
- propname = "volume"
-
- for spec in self.bElement.list_children_properties():
- if spec.name == propname:
- self.showKeyframes(self.bElement, spec, isDefault=True)
-
- self.hideKeyframes()
-
- def _createKeyframe(self, value, has_changeable_time):
- keyframe = Keyframe(self, value, has_changeable_time)
- self.insert_child_above(keyframe, self._keyframesMarker)
- self.setKeyframePosition(keyframe, value)
- return keyframe
-
- def _createLine(self, keyframe, lastKeyframe, line):
- if not line:
- line = Line(self, keyframe, lastKeyframe)
- self.lines.append(line)
- self.insert_child_above(line, self._linesMarker)
-
- adj = self.nsToPixel(
- keyframe.value.timestamp - lastKeyframe.value.timestamp)
- opp = (lastKeyframe.value.value - keyframe.value.value) * EXPANDED_SIZE
- hyp = math.sqrt(adj ** 2 + opp ** 2)
- if hyp < 1:
- # line length would be less than one pixel
- return
+class VideoSource(TimelineElement):
- sinX = opp / hyp
- line.props.width = hyp
- line.props.height = KEYFRAME_SIZE
- line.props.rotation_angle_z = math.degrees(math.asin(sinX))
- line.props.x = self.nsToPixel(
- lastKeyframe.value.timestamp - self.bElement.props.in_point)
- line.props.y = EXPANDED_SIZE - \
- (EXPANDED_SIZE * lastKeyframe.value.value) - KEYFRAME_SIZE / 2
- line.canvas.invalidate()
-
- def _createGhostclip(self):
- pass
+ __gtype_name__ = "PitiviVideoSource"
- def _createBorder(self):
- border = Clutter.Actor()
- border.bElement = self.bElement
- border.set_background_color(BORDER_NORMAL_COLOR)
- border.set_position(0, 0)
- return border
+ def _getBackground(self):
+ return VideoBackground()
- def _createBackground(self):
- raise NotImplementedError()
+ def do_get_preferred_height(self):
+ return ui.LAYER_HEIGHT / 2, ui.LAYER_HEIGHT
- def _createHandles(self):
- pass
- def _createPreview(self):
- if isinstance(self.bElement, GES.AudioUriSource):
- previewer = AudioPreviewer(self.bElement, self.timeline)
- previewer.startLevelsDiscoveryWhenIdle()
- return previewer
- if isinstance(self.bElement, GES.VideoUriSource):
- return VideoPreviewer(self.bElement, self.timeline)
- # TODO: GES.AudioTransition, GES.VideoTransition, GES.ImageSource,
- # GES.TitleSource
- return Clutter.Actor()
-
- def _createMarquee(self):
- marquee = Clutter.Actor()
- marquee.bElement = self.bElement
- marquee.set_background_color(CLIP_SELECTED_OVERLAY_COLOR)
- marquee.props.visible = False
- return marquee
-
- def _connectToEvents(self):
- self.dragAction = Clutter.DragAction()
- self.add_action(self.dragAction)
- self.dragAction.connect("drag-progress", self._dragProgressCb)
- self.dragAction.connect("drag-begin", self._dragBeginCb)
- self.dragAction.connect("drag-end", self._dragEndCb)
- self.bElement.selected.connect(
- "selected-changed", self._selectedChangedCb)
- self.bElement.connect("notify::duration", self._durationChangedCb)
- self.bElement.connect("notify::in-point", self._inpointChangedCb)
- # We gotta go low-level cause Clutter.ClickAction["clicked"]
- # gets emitted after Clutter.DragAction["drag-begin"]
- self.connect("button-press-event", self._clickedCb)
-
- def _getLayerForY(self, y):
- if self.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(False)
- if self.isSelected:
- self.updateKeyframes()
+class VideoUriSource(VideoSource):
- # Callbacks
+ __gtype_name__ = "PitiviUriVideoSource"
- def _clickedCb(self, action, actor):
- pass
+ def __init__(self, element, timeline):
+ super(VideoUriSource, self).__init__(element, timeline)
+ self.get_style_context().add_class("VideoUriSource")
- def _dragBeginCb(self, action, actor, event_x, event_y, modifiers):
- pass
+ def _getPreviewer(self):
+ previewer = previewers.VideoPreviewer(self._bElement)
+ previewer.get_style_context().add_class("VideoUriSource")
- def _dragProgressCb(self, unused_action, unused_actor, unused_delta_x, unused_delta_y):
- return False
+ return previewer
- def _dragEndCb(self, action, actor, event_x, event_y, modifiers):
- pass
+ def _getDefaultMixingProperty(self):
+ for spec in self._bElement.list_children_properties():
+ if spec.name == "alpha":
+ return spec
- def _durationChangedCb(self, unused_element, unused_duration):
- if self.keyframesVisible:
- self.updateKeyframes()
- def _inpointChangedCb(self, unused_element, unused_inpoint):
- if self.keyframesVisible:
- self.updateKeyframes()
+class AudioBackground (Gtk.Box):
- def _selectedChangedCb(self, unused_selected, isSelected):
- self.isSelected = isSelected
- if not isSelected:
- self.hideKeyframes()
- self.marquee.props.visible = isSelected
- color = BORDER_SELECTED_COLOR if isSelected else BORDER_NORMAL_COLOR
- self.border.set_background_color(color)
+ def __init__(self):
+ super(AudioBackground, self).__init__(self)
+ self.get_style_context().add_class("AudioBackground")
-class Gradient(Clutter.Actor):
+class AudioUriSource(TimelineElement):
- def __init__(self, rb, gb, bb, re, ge, be):
- """
- Creates a rectangle with a gradient. The first three parameters
- are the gradient's RGB values at the top, the last three params
- are the RGB values at the bottom.
- """
- Clutter.Actor.__init__(self)
- self.canvas = Clutter.Canvas()
- self.linear = cairo.LinearGradient(0, 0, 10, EXPANDED_SIZE)
- self.linear.add_color_stop_rgb(0, rb / 255., gb / 255., bb / 255.)
- self.linear.add_color_stop_rgb(1, re / 255., ge / 255., be / 255.)
- self.canvas.set_size(10, EXPANDED_SIZE)
- self.canvas.connect("draw", self._drawCb)
- self.set_content(self.canvas)
- self.canvas.invalidate()
-
- def _drawCb(self, unused_canvas, cr, unused_width, unused_height):
- cr.set_operator(cairo.OPERATOR_CLEAR)
- cr.paint()
- cr.set_operator(cairo.OPERATOR_OVER)
- cr.set_source(self.linear)
- cr.rectangle(0, 0, 10, EXPANDED_SIZE)
- cr.fill()
-
-
-class Line(Clutter.Actor):
-
- """
- A cairo line used for keyframe curves.
- """
-
- def __init__(self, timelineElement, keyframe, lastKeyframe):
- Clutter.Actor.__init__(self)
- self.timelineElement = weakref.proxy(timelineElement)
-
- self.canvas = Clutter.Canvas()
- self.canvas.set_size(1000, KEYFRAME_SIZE)
- self.canvas.connect("draw", self._drawCb)
- self.set_content(self.canvas)
- self.set_reactive(True)
-
- self.gotDragged = False
-
- 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)
+ __gtype_name__ = "PitiviAudioUriSource"
- self.connect("button-release-event", self._clickedCb)
- self.connect("motion-event", self._motionEventCb)
- self.connect("enter-event", self._enterEventCb)
- self.connect("leave-event", self._leaveEventCb)
+ def __init__(self, element, timeline):
+ super(AudioUriSource, self).__init__(element, timeline)
+ self.get_style_context().add_class("AudioUriSource")
- self.previousKeyframe = lastKeyframe
- self.nextKeyframe = keyframe
+ def do_get_preferred_height(self):
+ return ui.LAYER_HEIGHT / 2, ui.LAYER_HEIGHT
- def _drawCb(self, unused_canvas, cr, width, unused_height):
- """
- This is where we actually create the line segments for keyframe curves.
- We draw multiple lines (one-third of the height each) to add a "shadow"
- around the actual line segment to improve visibility.
- """
- cr.set_operator(cairo.OPERATOR_CLEAR)
- cr.paint()
- cr.set_operator(cairo.OPERATOR_OVER)
-
- # The "height budget" to draw line components = the tallest
- # component...
- _max_height = KEYFRAME_SIZE
-
- # While normally all three lines would have an equal height,
- # I make the shadow lines be 1/2 (3px) instead of 1/3 (2px),
- # while keeping their 1/3 position... this softens them up.
-
- # Upper shadow/border:
- cr.set_source_rgba(0, 0, 0, 0.5) # 50% transparent black color
- cr.move_to(0, _max_height / 3)
- cr.line_to(width, _max_height / 3)
- cr.set_line_width(_max_height / 3) # Special case: fuzzy 3px
- cr.stroke()
- # Lower shadow/border:
- cr.set_source_rgba(0, 0, 0, 0.5) # 50% transparent black color
- cr.move_to(0, _max_height * 2 / 3)
- cr.line_to(width, _max_height * 2 / 3)
- cr.set_line_width(_max_height / 3) # Special case: fuzzy 3px
- cr.stroke()
- # Draw the actual line in the middle.
- # Do it last, so that it gets drawn on top and remains sharp.
- cr.set_source_rgba(*KEYFRAME_LINE_COLOR)
- cr.move_to(0, _max_height / 2)
- cr.line_to(width, _max_height / 2)
- cr.set_line_width(_max_height / 3)
- cr.stroke()
-
- def transposeXY(self, x, y):
- x -= self.timelineElement.props.x + CONTROL_WIDTH - \
- self.timelineElement.timeline._scroll_point.x
- x += Zoomable.nsToPixel(self.timelineElement.bElement.props.in_point)
- y -= self.timelineElement.props.y
- return x, y
-
- def _ungrab(self):
- self.timelineElement.set_reactive(True)
- self.timelineElement.timeline._container.embed.get_window().set_cursor(
- NORMAL_CURSOR)
-
- def _clickedCb(self, unused_actor, event):
- if self.gotDragged:
- self.gotDragged = False
- return
- x, unused_y = self.transposeXY(event.x, event.y)
- timestamp = Zoomable.pixelToNs(x)
- value = self._valueAtTimestamp(timestamp)
- self.timelineElement.addKeyframe(value, timestamp)
-
- def _valueAtTimestamp(self, timestamp):
- timestamp_left = self.previousKeyframe.value.timestamp
- value_left = self.previousKeyframe.value.value
- timestamp_right = self.nextKeyframe.value.timestamp
- value_right = self.nextKeyframe.value.value
- height = value_right - value_left
- duration = timestamp_right - timestamp_left
- value = value_right - (timestamp_right - timestamp) * height / duration
- return max(0.0, min(value, 1.0))
-
- def _enterEventCb(self, unused_actor, unused_event):
- self.timelineElement.set_reactive(False)
- self.timelineElement.timeline._container.embed.get_window().set_cursor(
- DRAG_CURSOR)
-
- def _leaveEventCb(self, unused_actor, unused_event):
- self._ungrab()
-
- def _motionEventCb(self, actor, event):
- pass
+ def _getPreviewer(self):
+ previewer = previewers.AudioPreviewer(self._bElement)
+ previewer.get_style_context().add_class("AudioUriSource")
+ previewer.startLevelsDiscoveryWhenIdle()
- def _dragBeginCb(self, unused_action, unused_actor, event_x, event_y, unused_modifiers):
- self.timelineElement.timeline._container.app.action_log.begin(
- "Dragging keyframe line")
- self.dragBeginStartX = event_x
- self.dragBeginStartY = event_y
- self.origY = self.props.y
- self.previousKeyframe.startDrag(event_x, event_y, self)
- self.nextKeyframe.startDrag(event_x, event_y, self)
+ return previewer
- def _dragProgressCb(self, unused_action, unused_actor, unused_delta_x, delta_y):
- self.gotDragged = True
- coords = self.dragAction.get_motion_coords()
- delta_x = coords[0] - self.dragBeginStartX
- delta_y = coords[1] - self.dragBeginStartY
+ def _getBackground(self):
+ return AudioBackground()
- self.previousKeyframe.updateValue(0, delta_y)
- self.nextKeyframe.updateValue(0, delta_y)
+ def _getDefaultMixingProperty(self):
+ for spec in self._bElement.list_children_properties():
+ if spec.name == "volume":
+ return spec
- return False
- def _dragEndCb(self, unused_action, unused_actor, unused_event_x, unused_event_y, unused_modifiers):
- self.previousKeyframe.endDrag()
- self.nextKeyframe.endDrag()
- if self.timelineElement.timeline.getActorUnderPointer() != self:
- self._ungrab()
- self.timelineElement.timeline._container.app.action_log.commit()
+class TrimHandle(Gtk.EventBox, Loggable):
+
+ __gtype_name__ = "PitiviTrimHandle"
+ DEFAULT_WIDTH = 5
-class KeyframeMenu(GtkClutter.Actor):
+ def __init__(self, clip, edge):
+ Gtk.EventBox.__init__(self)
+ Loggable.__init__(self)
- def __init__(self, keyframe):
- GtkClutter.Actor.__init__(self)
- self.keyframe = keyframe
- vbox = Gtk.Box()
- vbox.set_orientation(Gtk.Orientation.VERTICAL)
+ self.clip = clip
+ self.get_style_context().add_class("Trimbar")
+ self.edge = edge
- button = Gtk.Button()
- button.set_label("Remove")
- button.connect("clicked", self._removeClickedCb)
- vbox.pack_start(button, False, False, 0)
+ self.connect("event", self._eventCb)
+ self.connect("notify::window", self._windowSetCb)
- self.get_widget().add(vbox)
- self.vbox = vbox
- self.vbox.hide()
- self.set_reactive(True)
+ def _windowSetCb(self, window, pspec):
+ self.props.window.set_cursor(CURSORS[self.edge])
- def show(self):
- GtkClutter.Actor.show(self)
- self.vbox.show_all()
+ def do_show_all(self):
+ self.info("DO not do anythin on .show_all")
- def hide(self):
- GtkClutter.Actor.hide(self)
- self.vbox.hide()
+ def _eventCb(self, element, event):
+ if event.type == Gdk.EventType.ENTER_NOTIFY:
+ self.clip.edit_mode = GES.EditMode.EDIT_TRIM
+ self.clip.dragging_edge = self.edge
+ elif event.type == Gdk.EventType.LEAVE_NOTIFY:
+ self.clip.dragging_edge = GES.Edge.EDGE_NONE
+ self.clip.edit_mode = None
- def _removeClickedCb(self, unused_button):
- self.keyframe.remove()
+ return False
+ def do_get_preferred_width(self):
+ return TrimHandle.DEFAULT_WIDTH, TrimHandle.DEFAULT_WIDTH
-class Keyframe(Clutter.Actor):
+ def do_draw(self, cr):
+ Gtk.EventBox.do_draw(self, cr)
+ Gdk.cairo_set_source_pixbuf(cr, GdkPixbuf.Pixbuf.new_from_file(os.path.join(
+ configure.get_pixmap_dir(), "trimbar-focused.png")), 10, 10)
- """
- @ivar has_changeable_time: if False, it means this is an edge keyframe.
- @type has_changeable_time: bool
- """
- def __init__(self, timelineElement, value, has_changeable_time):
- Clutter.Actor.__init__(self)
+class Clip(Gtk.EventBox, timelineUtils.Zoomable, Loggable):
- self.value = value
- self.timelineElement = weakref.proxy(timelineElement)
- self.has_changeable_time = has_changeable_time
- self.lastClick = datetime.now()
+ __gtype_name__ = "PitiviClip"
- self.set_size(KEYFRAME_SIZE, KEYFRAME_SIZE)
- self.set_background_color(KEYFRAME_NORMAL_COLOR)
+ def __init__(self, layer, bClip):
+ super(Clip, self).__init__()
+ timelineUtils.Zoomable.__init__(self)
+ Loggable.__init__(self)
- self.dragAction = Clutter.DragAction()
- self.add_action(self.dragAction)
+ self.handles = []
+ self.z_order = -1
+ self.layer = layer
+ self.timeline = layer.timeline
+ self.app = layer.app
- 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.connect("button-press-event", self._clickedCb)
+ self.bClip = bClip
+ self.bClip.ui = self
+ self.bClip.selected = timelineUtils.Selected()
- self.createMenu()
- self.dragProgressed = False
- self.set_reactive(True)
+ self._audioSource = None
+ self._videoSource = None
- def createMenu(self):
- self.menu = KeyframeMenu(self)
- self.timelineElement.timeline._container.stage.connect(
- "button-press-event", self._stageClickedCb)
- self.timelineElement.timeline.add_child(self.menu)
+ self._setupWidget()
- def _unselect(self):
- self.timelineElement.set_reactive(True)
- self.set_background_color(KEYFRAME_NORMAL_COLOR)
- self.timelineElement.timeline._container.embed.get_window().set_cursor(
- NORMAL_CURSOR)
+ for child in self.bClip.get_children(False):
+ self._childAdded(self.bClip, child)
- def remove(self):
- # Can't remove edge keyframes !
- if not self.has_changeable_time:
- return
+ self._savePositionState()
+ self._connectWidgetSignals()
- self.timelineElement.timeline.remove_child(self.menu)
- self._unselect()
- self.timelineElement.removeKeyframe(self)
-
- def _stageClickedCb(self, stage, event):
- actor = stage.get_actor_at_pos(
- Clutter.PickMode.REACTIVE, event.x, event.y)
- if actor != self.menu:
- self.menu.hide()
-
- def _clickedCb(self, unused_actor, event):
- if (event.modifier_state & Clutter.ModifierType.CONTROL_MASK):
- self.remove()
- elif (datetime.now() - self.lastClick).total_seconds() < 0.5:
- self.remove()
-
- self.lastClick = datetime.now()
-
- def _enterEventCb(self, unused_actor, unused_event):
- self.timelineElement.set_reactive(False)
- self.set_background_color(KEYFRAME_SELECTED_COLOR)
- self.timelineElement.timeline._container.embed.get_window().set_cursor(
- DRAG_CURSOR)
-
- def _leaveEventCb(self, unused_actor, unused_event):
- self._unselect()
-
- def startDrag(self, event_x, event_y, line=None):
- self.dragBeginStartX = event_x
- self.dragBeginStartY = event_y
- self.lastTs = self.value.timestamp
- self.valueStart = self.value.value
- self.tsStart = self.value.timestamp
- self.duration = self.timelineElement.bElement.props.duration
- self.inpoint = self.timelineElement.bElement.props.in_point
- self.start = self.timelineElement.bElement.props.start
- self.line = line
-
- def endDrag(self):
- if not self.dragProgressed and not self.line:
- timeline = self.timelineElement.timeline
- self.menu.set_position(
- self.timelineElement.props.x + self.props.x + 10, self.timelineElement.props.y +
self.props.y + 10)
- self.menu.show()
-
- self.line = None
-
- def updateValue(self, delta_x, delta_y):
- newTs = self.tsStart + Zoomable.pixelToNs(delta_x)
- newValue = self.valueStart - (delta_y / EXPANDED_SIZE)
-
- # Don't overlap first and last keyframes.
- newTs = min(max(newTs, self.inpoint + 1),
- self.duration + self.inpoint - 1)
-
- newValue = min(max(newValue, 0.0), 1.0)
-
- if not self.has_changeable_time:
- newTs = self.lastTs
-
- updating = self.timelineElement.updating_keyframes
- self.timelineElement.updating_keyframes = True
- self.timelineElement.source.unset(self.lastTs)
- if (self.timelineElement.source.set(newTs, newValue)):
- self.value = Gst.TimedValue()
- self.value.timestamp = newTs
- self.value.value = newValue
- self.lastTs = newTs
-
- self.timelineElement.setKeyframePosition(self, self.value)
- # Resort the keyframes list each time. Should be cheap as there should never be too much
keyframes,
- # if optimization is needed, check if resorting is needed, should
- # not be in 99 % of the cases.
- self.timelineElement.keyframes = sorted(
- self.timelineElement.keyframes, key=lambda keyframe: keyframe.value.timestamp)
- self.timelineElement.drawLines(self.line)
- # This will update the viewer. nifty.
- if not self.line:
- self.timelineElement.timeline._container.seekInPosition(
- newTs + self.start)
-
- self.timelineElement.updating_keyframes = updating
-
- def _dragBeginCb(self, unused_action, unused_actor, event_x, event_y, unused_modifiers):
- self.timelineElement.timeline._container.app.action_log.begin(
- "Dragging keyframe")
- self.dragProgressed = False
- self.startDrag(event_x, event_y)
-
- def _dragProgressCb(self, unused_action, unused_actor, delta_x, delta_y):
- self.dragProgressed = True
- coords = self.dragAction.get_motion_coords()
- delta_x = coords[0] - self.dragBeginStartX
- delta_y = coords[1] - self.dragBeginStartY
- self.updateValue(delta_x, delta_y)
- return False
+ self.edit_mode = None
+ self.dragging_edge = GES.Edge.EDGE_NONE
- def _dragEndCb(self, unused_action, unused_actor, unused_event_x, unused_event_y, unused_modifiers):
- self.endDrag()
- if self.timelineElement.timeline.getActorUnderPointer() != self:
- self._unselect()
- self.timelineElement.timeline._container.app.action_log.commit()
+ self._connectGES()
+ self.get_accessible().set_name(self.bClip.get_name())
+ def do_get_preferred_width(self):
+ return self.nsToPixel(self.bClip.props.duration), self.nsToPixel(self.bClip.props.duration)
-class URISourceElement(TimelineElement):
+ def do_get_preferred_height(self):
+ parent = self.get_parent()
+ return parent.get_allocated_height(), parent.get_allocated_height()
- def __init__(self, bElement, timeline):
- TimelineElement.__init__(self, bElement, timeline)
- self.gotDragged = False
+ def _savePositionState(self):
+ self._current_x = self.nsToPixel(self.bClip.props.start)
+ self._curent_width = self.nsToPixel(self.bClip.props.duration)
+ parent = self.get_parent()
+ if parent:
+ self._current_parent_height = self.get_parent(
+ ).get_allocated_height()
+ else:
+ self._current_parent_height = 0
+ self._current_parent = parent
- # public API
+ def updatePosition(self):
+ parent = self.get_parent()
+ x = self.nsToPixel(self.bClip.props.start)
+ width = self.nsToPixel(self.bClip.props.duration)
+ parent_height = parent.get_allocated_height()
- def hideHandles(self):
- self.rightHandle.hide()
- self.leftHandle.hide()
+ if x != self._current_x or \
+ width != self._curent_width \
+ or parent_height != self._current_parent_height or \
+ parent != self._current_parent:
- # private API
+ self.layer.move(self, x, 0)
+ self.set_size_request(width, parent_height)
- def _createGhostclip(self):
- self.ghostclip = Ghostclip(self.track_type, self.bElement)
- self.timeline.add_child(self.ghostclip)
+ elements = self._elements_container.get_children()
+ for child in elements:
+ child.setSize(width, parent_height / len(elements))
- def _createHandles(self):
- self.leftHandle = TrimHandle(self, True)
- self.rightHandle = TrimHandle(self, False)
+ self._savePositionState()
- self.leftHandle.set_position(0, 0)
+ def _setupWidget(self):
+ pass
- self.add_child(self.leftHandle)
- self.add_child(self.rightHandle)
+ def sendFakeEvent(self, event, event_widget):
+ if event.type == Gdk.EventType.BUTTON_RELEASE:
+ self._clickedCb(event_widget, event)
- def _createBackground(self):
- if self.track_type == GES.TrackType.AUDIO:
- # Audio clips go from dark green to light green
- # (27, 46, 14, 255) to (73, 108, 33, 255)
- background = Gradient(27, 46, 14, 73, 108, 33)
- else:
- # Video clips go from almost black to gray
- # (15, 15, 15, 255) to (45, 45, 45, 255)
- background = Gradient(15, 15, 15, 45, 45, 45)
- background.bElement = self.bElement
- return background
+ self.timeline.sendFakeEvent(event, event_widget)
+
+ def do_draw(self, cr):
+ self.updatePosition()
+ Gtk.EventBox.do_draw(self, cr)
- # Callbacks
def _clickedCb(self, unused_action, unused_actor):
+ if self.timeline.got_dragged:
+ # If the timeline just got dragged and @self
+ # is the element initiating the mode,
+ # do not do anything when the button is
+ # released
+ self.timeline.got_dragged = False
+
+ return False
+
# TODO : Let's be more specific, masks etc ..
- mode = SELECT
- if self.timeline._container._controlMask:
- if not self.bElement.selected:
- mode = SELECT_ADD
+ mode = timelineUtils.SELECT
+ if self.timeline.parent._controlMask:
+ if not self.get_state_flags() & Gtk.StateFlags.SELECTED:
+ mode = timelineUtils.SELECT_ADD
self.timeline.current_group.add(
- self.bElement.get_toplevel_parent())
+ self.bClip.get_toplevel_parent())
else:
self.timeline.current_group.remove(
- self.bElement.get_toplevel_parent())
- mode = UNSELECT
- elif not self.bElement.selected:
+ self.bClip.get_toplevel_parent())
+ mode = timelineUtils.UNSELECT
+ elif not self.get_state_flags() & Gtk.StateFlags.SELECTED:
GES.Container.ungroup(self.timeline.current_group, False)
self.timeline.createSelectionGroup()
self.timeline.current_group.add(
- self.bElement.get_toplevel_parent())
- self.timeline._container.gui.switchContextTab(self.bElement)
+ self.bClip.get_toplevel_parent())
+ self.timeline.parent.gui.switchContextTab(self.bClip)
- children = self.bElement.get_toplevel_parent().get_children(True)
- selection = [elem for elem in children if isinstance(elem, GES.Source)]
+ parent = self.bClip.get_parent()
+ if parent == self.timeline.current_group or parent is None:
+ selection = [self.bClip]
+ else:
+ while parent:
+ if parent.get_parent() == self.timeline.current_group:
+ break
+ parent = parent.get_parent()
+
+ children = parent.get_children(True)
+ selection = [elem for elem in children if isinstance(elem, GES.SourceClip) or
+ isinstance(elem, GES.TransitionClip)]
self.timeline.selection.setSelection(selection, mode)
- if self.keyframedElement:
- self.showKeyframes(self.keyframedElement, self.prop)
+ # if self.keyframedElement:
+ # self.showKeyframes(self.keyframedElement, self.prop)
return False
- def _dragBeginCb(self, action, actor, event_x, event_y, modifiers):
- self.gotDragged = False
- mode = self.timeline._container.getEditionMode()
-
- # This can't change during a drag, so we can safely compute it now for
- # drag events.
- nbrLayers = len(self.timeline.bTimeline.get_layers())
- self.brother = self.timeline.findBrother(self.bElement)
- self._dragBeginStart = self.bElement.get_start()
- self.dragBeginStartX = event_x
- self.dragBeginStartY = event_y
-
- self.nbrLayers = nbrLayers
- self.ghostclip.setNbrLayers(nbrLayers)
- self.ghostclip.setWidth(self.props.width)
- if self.brother:
- self.brother.ghostclip.setWidth(self.props.width)
- self.brother.ghostclip.setNbrLayers(nbrLayers)
-
- # We can also safely find if the object has a brother element
- self.setDragged(True)
-
- def _dragProgressCb(self, action, actor, delta_x, delta_y):
- # We can't use delta_x here because it fluctuates weirdly.
- if not self.gotDragged:
- self.gotDragged = True
- self._context = EditingContext(self.bElement,
- self.timeline.bTimeline,
- None,
- GES.Edge.EDGE_NONE,
- None,
- self.timeline._container.app.action_log)
-
- mode = self.timeline._container.getEditionMode()
- self._context.setMode(mode)
-
- 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)
- new_start = self._dragBeginStart + self.pixelToNs(delta_x)
-
- self.ghostclip.props.x = max(
- 0, self.nsToPixel(self._dragBeginStart) + delta_x)
- self.ghostclip.update(priority, y, False)
- if self.brother:
- self.brother.ghostclip.props.x = max(
- 0, self.nsToPixel(self._dragBeginStart) + delta_x)
- self.brother.ghostclip.update(priority, y, True)
-
- 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())
+ def _connectWidgetSignals(self):
+ self.connect("button-release-event", self._clickedCb)
+ self.connect("event", self._eventCb)
+
+ def _eventCb(self, element, event):
+ if event.type == Gdk.EventType.ENTER_NOTIFY:
+ ui.set_children_state_recurse(self, Gtk.StateFlags.PRELIGHT)
+ for handle in self.handles:
+ handle.show()
+ elif event.type == Gdk.EventType.LEAVE_NOTIFY:
+ ui.unset_children_state_recurse(self, Gtk.StateFlags.PRELIGHT)
+ for handle in self.handles:
+ handle.hide()
- self.timeline._updateSize(self.ghostclip)
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()))
- priority = max(0, priority)
-
- self.timeline._snapEndedCb()
- self.setDragged(False)
-
- self.ghostclip.props.visible = False
- if self.brother:
- self.brother.ghostclip.props.visible = False
-
- if self.ghostclip.shouldCreateLayer:
- self.timeline.createLayerForGhostClip(self.ghostclip)
-
- if self.gotDragged:
- self._context.editTo(new_start, priority)
- self._context.finish()
-
- def cleanup(self):
- if self.preview and not type(self.preview) is Clutter.Actor:
- self.preview.cleanup()
- self.leftHandle.cleanup()
- self.leftHandle = None
- self.rightHandle.cleanup()
- self.rightHandle = None
- TimelineElement.cleanup(self)
-
-
-class TransitionElement(TimelineElement):
-
- def __init__(self, bElement, timeline):
- TimelineElement.__init__(self, bElement, timeline)
- self.isDragged = True
- self.set_reactive(True)
-
- def _createBackground(self):
- background = Clutter.Actor()
- background.set_background_color(TRANSITION_COLOR)
- return background
-
- def _createBorder(self):
- border = Clutter.Actor()
- border.set_background_color(Clutter.Color.new(0, 0, 0, 0))
- return border
-
- def _selectedChangedCb(self, selected, isSelected):
- TimelineElement._selectedChangedCb(self, selected, isSelected)
-
- if isSelected:
- self.timeline._container.app.gui.trans_list.activate(self.bElement)
+ def _startChangedCb(self, unused_clip, unused_pspec):
+ if self.get_parent() is None:
+ # FIXME Check why that happens at all (looks like a GTK bug)
+ return
+
+ self.layer.move(self, self.nsToPixel(self.bClip.props.start), 0)
+
+ def _durationChangedCb(self, unused_clip, unused_pspec):
+ parent = self.get_parent()
+ if parent:
+ duration = self.nsToPixel(self.bClip.props.duration)
+ parent_height = parent.get_allocated_height()
+ self.set_size_request(duration, parent_height)
+
+ def _layerChangedCb(self, bClip, unused_pspec):
+ bLayer = bClip.props.layer
+ if bLayer:
+ self.layer = bLayer.ui
+
+ def _childAdded(self, clip, child):
+ child.selected = timelineUtils.Selected()
+
+ def _childAddedCb(self, clip, child):
+ self._childAdded(clip, child)
+
+ def _childRemoved(self, clip, child):
+ pass
+
+ def _childRemovedCb(self, clip, child):
+ self._childRemoved(clip, child)
+
+ def _connectGES(self):
+ self.bClip.connect("notify::start", self._startChangedCb)
+ self.bClip.connect("notify::inpoint", self._startChangedCb)
+ self.bClip.connect("notify::duration", self._durationChangedCb)
+ self.bClip.connect("notify::layer", self._layerChangedCb)
+
+ self.bClip.connect_after("child-added", self._childAddedCb)
+ self.bClip.connect_after("child-removed", self._childRemovedCb)
+
+
+class SourceClip(Clip):
+ __gtype_name__ = "PitiviSourceClip"
+
+ def __init__(self, layer, bClip):
+ super(SourceClip, self).__init__(layer, bClip)
+
+ def _setupWidget(self):
+ self._vbox = Gtk.Box()
+ self._vbox.set_orientation(Gtk.Orientation.HORIZONTAL)
+ self.add(self._vbox)
+
+ self.leftHandle = TrimHandle(self, GES.Edge.EDGE_START)
+ self._vbox.pack_start(self.leftHandle, False, False, 0)
+
+ self._elements_container = Gtk.Paned.new(Gtk.Orientation.VERTICAL)
+ self._vbox.pack_start(self._elements_container, True, True, 0)
+
+ self.rightHandle = TrimHandle(self, GES.Edge.EDGE_END)
+ self._vbox.pack_end(self.rightHandle, False, False, 0)\
+
+ self.handles.append(self.leftHandle)
+ self.handles.append(self.rightHandle)
+
+ self.get_style_context().add_class("Clip")
+
+ def _childRemoved(self, clip, child):
+ if child.ui is not None:
+ self._elements_container.remove(child.ui)
+ child.ui = None
+
+
+class UriClip(SourceClip):
+ __gtype_name__ = "PitiviuriClip"
+
+ def __init__(self, layer, bClip):
+ super(UriClip, self).__init__(layer, bClip)
+
+ self.set_tooltip_markup(misc.filename_from_uri(bClip.get_uri()))
+
+ def _childAdded(self, clip, child):
+ if isinstance(child, GES.Source):
+ if child.get_track_type() == GES.TrackType.AUDIO:
+ self._audioSource = AudioUriSource(child, self.timeline)
+ child.ui = self._audioSource
+ self._elements_container.pack2(self._audioSource, True, False)
+ self._audioSource.set_visible(True)
+ elif child.get_track_type() == GES.TrackType.VIDEO:
+ self._videoSource = VideoUriSource(child, self.timeline)
+ child.ui = self._videoSource
+ self._elements_container.pack1(self._videoSource, True, False)
+ self._videoSource.set_visible(True)
else:
- self.timeline._container.app.gui.trans_list.deactivate()
+ child.ui = None
- def _clickedCb(self, action, actor):
- selection = {self.bElement}
- self.timeline.selection.setSelection(selection, SELECT)
- return False
+
+class TitleClip(SourceClip):
+ __gtype_name__ = "PitiviTitleClip"
+
+ def _childAdded(self, clip, child):
+ if isinstance(child, GES.Source):
+ if child.get_track_type() == GES.TrackType.VIDEO:
+ self._videoSource = VideoSource(child, self.timeline)
+ child.ui = self._videoSource
+ self._elements_container.pack1(self._videoSource, True, False)
+ self._videoSource.set_visible(True)
+ else:
+ child.ui = None
+
+
+class TransitionClip(Clip):
+
+ __gtype_name__ = "PitiviTransitionClip"
+
+ def __init__(self, layer, bClip):
+ super(TransitionClip, self).__init__(layer, bClip)
+ self.get_style_context().add_class("TransitionClip")
+ self.z_order = 0
+
+ for child in bClip.get_children(True):
+ child.selected = timelineUtils.Selected()
+ self.bClip.connect("child-added", self._childAddedCb)
+ self.selected = False
+ self.connect("state-flags-changed", self._selectedChangedCb)
+ self.connect("button-press-event", self._pressEventCb)
+
+ # In the case of TransitionClips, we are the only container
+ self._elements_container = self
+ self.set_tooltip_markup("<span foreground='blue'>%s</span>" %
+ str(bClip.props.vtype.value_nick))
+
+ def _childAdded(self, clip, child):
+ child.selected = timelineUtils.Selected()
+
+ if isinstance(child, GES.VideoTransition):
+ self.z_order += 1
+
+ def do_draw(self, cr):
+ Clip.do_draw(self, cr)
+
+ def _selectedChangedCb(self, unused_widget, flags):
+ if not [c for c in self.bClip.get_children(True) if isinstance(c, GES.VideoTransition)]:
+ return
+
+ if flags & Gtk.StateFlags.SELECTED:
+ self.timeline.parent.app.gui.trans_list.activate(self.bClip)
+ self.selected = True
+ elif self.selected:
+ self.selected = False
+ self.timeline.parent.app.gui.trans_list.deactivate()
+
+ def _pressEventCb(self, unused_action, unused_widget):
+ selection = {self.bClip}
+ self.timeline.selection.setSelection(selection, timelineUtils.SELECT)
+ return True
+
+GES_TYPE_UI_TYPE = {
+ GES.UriClip.__gtype__: UriClip,
+ GES.TitleClip.__gtype__: TitleClip,
+ GES.TransitionClip.__gtype__: TransitionClip
+}
diff --git a/pitivi/timeline/layer.py b/pitivi/timeline/layer.py
index d9ba35f..4628f8a 100644
--- a/pitivi/timeline/layer.py
+++ b/pitivi/timeline/layer.py
@@ -21,18 +21,19 @@
# Boston, MA 02110-1301, USA.
from gi.repository import Gtk
-from gi.repository import Gdk
from gi.repository import GES
from gi.repository import GObject
from gettext import gettext as _
+from pitivi.timeline import elements
from pitivi.utils.loggable import Loggable
-from pitivi.utils.ui import LAYER_CONTROL_TARGET_ENTRY
+from pitivi.utils import ui
+from pitivi.utils import timeline as timelineUtils
-# TODO GTK3 port to GtkGrid
class BaseLayerControl(Gtk.Box, Loggable):
+
"""
Base Layer control classes
"""
@@ -70,9 +71,6 @@ class BaseLayerControl(Gtk.Box, Loggable):
self.eventbox.connect("button_press_event", self._buttonPressCb)
self.pack_start(self.eventbox, True, True, 0)
- self.sep = SpacedSeparator()
- self.pack_start(self.sep, True, True, 0)
-
icon_mapping = {GES.TrackType.AUDIO: "audio-x-generic",
GES.TrackType.VIDEO: "video-x-generic"}
@@ -157,9 +155,6 @@ class BaseLayerControl(Gtk.Box, Loggable):
self.popup.show_all()
# Drag and drop
-# self.drag_source_set(Gdk.ModifierType.BUTTON1_MASK,
-# [LAYER_CONTROL_TARGET_ENTRY],
-# Gdk.DragAction.MOVE)
def getSelected(self):
return self._selected
@@ -198,7 +193,7 @@ class BaseLayerControl(Gtk.Box, Loggable):
"""
Look if user selected layer or wants popup menu
"""
- self._control_container.selectLayerControl(self)
+ # FIXME!! self._control_container.selectLayerControl(self)
if event.button == 3:
self.popup.popup(None, None, None, None, event.button, event.time)
@@ -222,22 +217,24 @@ class BaseLayerControl(Gtk.Box, Loggable):
def _deleteLayerCb(self, unused_widget):
self._app.action_log.begin("delete layer")
- self._control_container.timeline.bTimeline.remove_layer(self.layer)
- self._control_container.timeline.bTimeline.get_asset().pipeline.commit_timeline()
+ bLayer = self.layer.bLayer
+ bTimeline = bLayer.get_timeline()
+ bTimeline.remove_layer(bLayer)
+ bTimeline.get_asset().pipeline.commit_timeline()
self._app.action_log.commit()
def _moveLayerCb(self, unused_widget, step):
- index = self.layer.get_priority()
+ index = self.layer.bLayer.get_priority()
if abs(step) == 1:
index += step
elif step == -2:
index = 0
else:
- index = len(self.layer.get_timeline().get_layers()) - 1
+ index = len(self.layer.bLayer.get_timeline().get_layers()) - 1
# if audio, set last position
self._app.moveLayer(self, index)
-# self._app.timeline._container.app.gui.timeline_ui.controls.moveControlWidget(self, index)
+ # self._app.timeline.parent.app.gui.timeline_ui.controls.moveControlWidget(self, index)
def getHeight(self):
return self.get_allocation().height
@@ -390,7 +387,227 @@ class SpacedSeparator(Gtk.EventBox):
self.box = Gtk.Box()
self.box.set_orientation(Gtk.Orientation.VERTICAL)
- self.box.add(Gtk.HSeparator())
- self.box.set_border_width(6)
-
self.add(self.box)
+
+ self.get_style_context().add_class("SpacedSeparator")
+ self.box.get_style_context().add_class("SpacedSeparator")
+
+
+class LayerControls(Gtk.Bin, Loggable):
+
+ __gtype_name__ = 'PitiviLayerControls'
+
+ def __init__(self, bLayer, app):
+ super(LayerControls, self).__init__()
+ Loggable.__init__(self)
+
+ ebox = Gtk.EventBox()
+ self.add(ebox)
+ self._hbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
+ ebox.add(self._hbox)
+ self.bLayer = bLayer
+ self.app = app
+
+ sep = SpacedSeparator()
+ self._hbox.pack_start(sep, False, False, 5)
+
+ self.video_control = VideoLayerControl(None, self, self.app)
+ self.video_control.set_visible(True)
+ self.video_control.props.width_request = ui.CONTROL_WIDTH
+ self.video_control.props.height_request = ui.LAYER_HEIGHT / 2
+ self._hbox.add(self.video_control)
+
+ self.audio_control = AudioLayerControl(None, self, self.app)
+ self.audio_control.set_visible(True)
+ self.audio_control.props.height_request = ui.LAYER_HEIGHT / 2
+ self.audio_control.props.width_request = ui.CONTROL_WIDTH
+ self._hbox.add(self.audio_control)
+
+ self._hbox.props.vexpand = False
+ self._hbox.props.width_request = ui.CONTROL_WIDTH
+ self.props.width_request = ui.CONTROL_WIDTH
+
+ sep = SpacedSeparator()
+ self._hbox.pack_start(sep, False, False, 5)
+
+
+class LayerLayout(Gtk.Layout, Loggable):
+ """
+ A GtkLayout that exclusivly container Clips.
+ This allows us to properly handle the z order of
+ """
+ __gtype_name__ = "PitiviLayerLayout"
+
+ def __init__(self, timeline):
+ super(LayerLayout, self).__init__()
+ Loggable.__init__(self)
+
+ self._children = []
+ self._changed = False
+ self.timeline = timeline
+
+ self.props.hexpand = True
+ self.get_style_context().add_class("LayerLayout")
+
+ def do_add(self, widget):
+ self._children.append(widget)
+ self._children.sort(key=lambda clip: clip.z_order)
+ Gtk.Layout.do_add(self, widget)
+ self._changed = True
+
+ for child in self._children:
+ if isinstance(child, elements.TransitionClip):
+ window = child.get_window()
+ if window is not None:
+ window.raise_()
+
+ def do_remove(self, widget):
+ self._children.remove(widget)
+ self._changed = True
+ Gtk.Layout.do_remove(self, widget)
+
+ def put(self, child, x, y):
+ self._children.append(child)
+ self._children.sort(key=lambda clip: clip.z_order)
+ Gtk.Layout.put(self, child, x, y)
+ self._changed = True
+
+ def do_draw(self, cr):
+ if self._changed:
+ self._children.sort(key=lambda clip: clip.z_order)
+ for child in self._children:
+
+ if isinstance(child, elements.TransitionClip):
+ window = child.get_window()
+ window.raise_()
+ self._changed = False
+
+ self.props.width = timelineUtils.Zoomable.nsToPixel(self.timeline.bTimeline.props.duration) + 500
+ self.props.width_request = timelineUtils.Zoomable.nsToPixel(self.timeline.bTimeline.props.duration)
+ 500
+
+ for child in self._children:
+ self.propagate_draw(child, cr)
+
+
+class Layer(Gtk.EventBox, timelineUtils.Zoomable, Loggable):
+
+ __gtype_name__ = "PitiviLayer"
+
+ __gsignals__ = {
+ "remove-me": (GObject.SignalFlags.RUN_LAST, None, (),)
+ }
+
+ def __init__(self, bLayer, timeline):
+ super(Layer, self).__init__()
+ Loggable.__init__(self)
+
+ self.bLayer = bLayer
+ self.bLayer.ui = self
+ self.timeline = timeline
+ self.app = timeline.app
+
+ self.bLayer.connect("clip-added", self._clipAddedCb)
+ self.bLayer.connect("clip-removed", self._clipRemovedCb)
+
+ # FIXME Make the layer height user setable with 'Paned'
+ self.props.height_request = ui.LAYER_HEIGHT
+ self.props.valign = Gtk.Align.START
+
+ self._layout = LayerLayout(self.timeline)
+ self.add(self._layout)
+
+ self.media_types = GES.TrackType(0)
+ for clip in bLayer.get_clips():
+ self._addClip(clip)
+
+ self.before_sep = None
+ self.after_sep = None
+
+ def _checkMediaTypes(self, bClip=None):
+ self.media_types = GES.TrackType(0)
+ bClips = self.bLayer.get_clips()
+
+ """
+ FIXME: That produces segfault in GES/GSequence
+ if not bClips:
+ self.emit("remove-me")
+ return
+ """
+
+ for bClip in bClips:
+ for child in bClip.get_children(False):
+ self.media_types |= child.get_track().props.track_type
+ if self.media_types == (GES.TrackType.AUDIO | GES.TrackType.VIDEO):
+ break
+
+ if not (self.media_types & GES.TrackType.AUDIO) and not (self.media_types & GES.TrackType.VIDEO):
+ self.media_types = GES.TrackType.AUDIO | GES.TrackType.VIDEO
+
+ height = 0
+ if self.media_types & GES.TrackType.AUDIO:
+ height += ui.LAYER_HEIGHT / 2
+ self.bLayer.control_ui.audio_control.show()
+ else:
+ self.bLayer.control_ui.audio_control.hide()
+
+ if self.media_types & GES.TrackType.VIDEO:
+ self.bLayer.control_ui.video_control.show()
+ height += ui.LAYER_HEIGHT / 2
+ else:
+ self.bLayer.control_ui.video_control.hide()
+
+ self.props.height_request = height
+ self.bLayer.control_ui.props.height_request = height
+
+ def move(self, child, x, y):
+ self._layout.move(child, x, y)
+
+ def _childAddedCb(self, bClip, child):
+ self._checkMediaTypes()
+
+ def _childRemovedCb(self, bClip, child):
+ self._checkMediaTypes()
+
+ def _clipAddedCb(self, layer, bClip):
+ self._addClip(bClip)
+
+ def _addClip(self, bClip):
+ ui_type = elements.GES_TYPE_UI_TYPE.get(bClip.__gtype__, None)
+ if ui_type is None:
+ self.error("Implement UI for type %s?" % bClip.__gtype__)
+ return
+
+ if not hasattr(bClip, "ui") or bClip.ui is None:
+ clip = ui_type(self, bClip)
+ else:
+ clip = bClip.ui
+
+ self._layout.put(clip, self.nsToPixel(bClip.props.start), 0)
+ self.show_all()
+ bClip.connect_after("child-added", self._childAddedCb)
+ bClip.connect_after("child-removed", self._childRemovedCb)
+ self._checkMediaTypes()
+
+ def _clipRemovedCb(self, bLayer, bClip):
+ self._removeClip(bClip)
+
+ def _removeClip(self, bClip):
+ ui_type = elements.GES_TYPE_UI_TYPE.get(bClip.__gtype__, None)
+ if ui_type is None:
+ self.error("Implement UI for type %s?" % bClip.__gtype__)
+ return
+
+ self._layout.remove(bClip.ui)
+ if self.timeline.draggingElement is None:
+ bClip.ui = None
+
+ bClip.disconnect_by_func(self._childAddedCb)
+ bClip.disconnect_by_func(self._childRemovedCb)
+ self._checkMediaTypes(bClip)
+
+ def updatePosition(self):
+ for bClip in self.bLayer.get_clips():
+ bClip.ui.updatePosition()
+
+ def do_draw(self, cr):
+ Gtk.Box.do_draw(self, cr)
diff --git a/pitivi/timeline/previewers.py b/pitivi/timeline/previewers.py
index 40ccd0e..dddd71b 100644
--- a/pitivi/timeline/previewers.py
+++ b/pitivi/timeline/previewers.py
@@ -20,7 +20,6 @@
# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
# Boston, MA 02110-1301, USA.
-from datetime import datetime, timedelta
from random import randrange
import cairo
import numpy
@@ -28,13 +27,13 @@ import os
import pickle
import sqlite3
-from gi.repository import Clutter
-from gi.repository import Cogl
from gi.repository import GES
from gi.repository import GObject
from gi.repository import GLib
from gi.repository import GdkPixbuf
from gi.repository import Gst
+from gi.repository import Gdk
+from gi.repository import Gtk
# Our C module optimizing waveforms rendering
try:
@@ -45,10 +44,9 @@ except ImportError:
from pitivi.settings import get_dir, xdg_cache_home
from pitivi.utils.loggable import Loggable
-from pitivi.utils.misc import binary_search, filename_from_uri, quantize, quote_uri, hash_file, format_ns
+from pitivi.utils.misc import binary_search, filename_from_uri, quantize, quote_uri, hash_file
from pitivi.utils.system import CPUUsageTracker
from pitivi.utils.timeline import Zoomable
-from pitivi.utils.ui import CONTROL_WIDTH
from pitivi.utils.ui import EXPANDED_SIZE
@@ -58,7 +56,6 @@ WAVEFORMS_CPU_USAGE = 30
THUMBNAILS_CPU_USAGE = 20
THUMB_MARGIN_PX = 3
-WAVEFORM_UPDATE_INTERVAL = timedelta(microseconds=500000)
# For the waveforms, ensures we always have a little extra surface when
# scrolling while playing.
MARGIN = 500
@@ -151,25 +148,24 @@ class PreviewGenerator(object):
PreviewGenerator.__manager.addPipeline(self)
-class VideoPreviewer(Clutter.ScrollActor, PreviewGenerator, Zoomable, Loggable):
+class VideoPreviewer(Gtk.Layout, PreviewGenerator, Zoomable, Loggable):
# We could define them in PreviewGenerator, but then for some reason they
# are ignored.
__gsignals__ = PREVIEW_GENERATOR_SIGNALS
- def __init__(self, bElement, timeline):
+ def __init__(self, bElement):
"""
@param bElement : the backend GES.TrackElement
@param track : the track to which the bElement belongs
- @param timeline : the containing graphic timeline.
"""
- Clutter.ScrollActor.__init__(self)
+ super(VideoPreviewer, self).__init__()
PreviewGenerator.__init__(self, GES.TrackType.VIDEO)
Zoomable.__init__(self)
Loggable.__init__(self)
# Variables related to the timeline objects
- self.timeline = timeline
+ self.timeline = bElement.get_parent().get_timeline().ui
self.bElement = bElement
# Guard against malformed URIs
self.uri = quote_uri(bElement.props.uri)
@@ -178,8 +174,8 @@ class VideoPreviewer(Clutter.ScrollActor, PreviewGenerator, Zoomable, Loggable):
# Variables related to thumbnailing
self.wishlist = []
self._thumb_cb_id = None
- self._allAnimated = False
self._running = False
+
# We should have one thumbnail per thumb_period.
# TODO: get this from the user settings
self.thumb_period = int(0.5 * Gst.SECOND)
@@ -194,20 +190,24 @@ class VideoPreviewer(Clutter.ScrollActor, PreviewGenerator, Zoomable, Loggable):
self.interval = 500 # Every 0.5 second, reevaluate the situation
# Connect signals and fire things up
- self.timeline.connect("scrolled", self._scrollCb)
+ self.timeline.hadj.connect("value-changed", self._scrollCb)
self.bElement.connect("notify::duration", self._durationChangedCb)
self.bElement.connect("notify::in-point", self._inpointChangedCb)
self.bElement.connect("notify::start", self._startChangedCb)
self.pipeline = None
+ self._needs_redraw = True
self.becomeControlled()
+ self.connect("notify::height-request", self._heightChangedCb)
+
# Internal API
- def _update(self, unused_msg_source=None):
- if self.thumb_width:
- self._addVisibleThumbnails()
- if self.wishlist:
- self.becomeControlled()
+ def _force_redraw(self, unused_msg_source=None):
+ self._needs_redraw = True
+ if self.wishlist:
+ self.becomeControlled()
+
+ return
def _setupPipeline(self):
"""
@@ -289,6 +289,10 @@ class VideoPreviewer(Clutter.ScrollActor, PreviewGenerator, Zoomable, Loggable):
# removed from the timeline after the PreviewGeneratorManager
# started this job.
return
+
+ # self.props.width_request =
self.nsToPixel(self.bElement.get_asset().get_filesource_asset().props.duration)
+ # self.props.width = self.nsToPixel(self.bElement.get_asset().get_filesource_asset().props.duration)
+
self.debug(
'Now generating thumbnails for: %s', filename_from_uri(self.uri))
query_success, duration = self.pipeline.query_duration(Gst.Format.TIME)
@@ -303,10 +307,11 @@ class VideoPreviewer(Clutter.ScrollActor, PreviewGenerator, Zoomable, Loggable):
self._checkCPU()
if self.bElement.props.in_point != 0:
- position = Clutter.Point()
- position.x = Zoomable.nsToPixel(self.bElement.props.in_point)
- self.scroll_to_point(position)
- self._addVisibleThumbnails()
+ adj = self.get_hadjustment()
+ adj.props.page_size = 1.0
+ adj.props.value = Zoomable.nsToPixel(self.bElement.props.in_point)
+
+ # self._addVisibleThumbnails()
# Save periodically to avoid the common situation where the user exits
# the app before a long clip has been fully thumbnailed.
# Spread timeouts between 30-80 secs to avoid concurrent disk writes.
@@ -353,8 +358,7 @@ class VideoPreviewer(Clutter.ScrollActor, PreviewGenerator, Zoomable, Loggable):
return False # Stop the timer
def _get_thumb_duration(self):
- thumb_duration_tmp = Zoomable.pixelToNs(
- self.thumb_width + THUMB_MARGIN_PX)
+ thumb_duration_tmp = Zoomable.pixelToNs(self.thumb_width + THUMB_MARGIN_PX)
# quantize thumb length to thumb_period
thumb_duration = quantize(thumb_duration_tmp, self.thumb_period)
# make sure that the thumb duration after the quantization isn't
@@ -364,35 +368,40 @@ class VideoPreviewer(Clutter.ScrollActor, PreviewGenerator, Zoomable, Loggable):
# make sure that we don't show thumbnails more often than thumb_period
return max(thumb_duration, self.thumb_period)
- def _addVisibleThumbnails(self):
+ def _remove_all_children(self):
+ for child in self.get_children():
+ self.remove(child)
+
+ def _addVisibleThumbnails(self, rect):
"""
Get the thumbnails to be displayed in the currently visible clip portion
"""
- self.remove_all_children()
- old_thumbs = self.thumbs
+ if self.thumb_width is None:
+ return False
+
self.thumbs = {}
self.wishlist = []
thumb_duration = self._get_thumb_duration()
- element_left, element_right = self._get_visible_range()
+
+ element_left = self.pixelToNs(rect.x) + self.bElement.props.in_point
+ element_right = element_left + self.pixelToNs(rect.width)
element_left = quantize(element_left, thumb_duration)
for current_time in range(element_left, element_right, thumb_duration):
thumb = Thumbnail(self.thumb_width, self.thumb_height)
- thumb.set_position(
- Zoomable.nsToPixel(current_time), THUMB_MARGIN_PX)
- self.add_child(thumb)
+ self.put(thumb, Zoomable.nsToPixel(current_time) - self.nsToPixel(self.bElement.props.in_point),
+ (self.props.height_request - self.thumb_height) / 2)
+
self.thumbs[current_time] = thumb
if current_time in self.thumb_cache:
gdkpixbuf = self.thumb_cache[current_time]
- if self._allAnimated or current_time not in old_thumbs:
- self.thumbs[
- current_time].set_from_gdkpixbuf_animated(gdkpixbuf)
- else:
- self.thumbs[current_time].set_from_gdkpixbuf(gdkpixbuf)
+ self.thumbs[current_time].set_from_pixbuf(gdkpixbuf)
+ self.thumbs[current_time].set_visible(True)
else:
self.wishlist.append(current_time)
- self._allAnimated = False
+
+ return True
def _get_wish(self):
"""
@@ -413,7 +422,6 @@ class VideoPreviewer(Clutter.ScrollActor, PreviewGenerator, Zoomable, Loggable):
# => Daniel: It is *not* nanosecond precise when we remove the videorate
# element from the pipeline
# => thiblahute: not the case with mpegts
- original_time = time
if time in self.thumbs:
thumb = self.thumbs[time]
else:
@@ -421,81 +429,19 @@ class VideoPreviewer(Clutter.ScrollActor, PreviewGenerator, Zoomable, Loggable):
index = binary_search(sorted_times, time)
time = sorted_times[index]
thumb = self.thumbs[time]
- if thumb.has_pixel_data:
- # If this happens, it means the precision of the thumbnail
- # generator is not good enough for the current thumbnail
- # interval.
- # We could consider shifting the thumbnails, but seems like
- # too much trouble for something which does not happen in
- # practice. My last words..
- self.fixme("Thumbnail is already set for time: %s, %s",
- format_ns(time), format_ns(original_time))
- return
- thumb.set_from_gdkpixbuf_animated(pixbuf)
+
+ thumb.set_from_pixbuf(pixbuf)
if time in self.queue:
self.queue.remove(time)
self.thumb_cache[time] = pixbuf
+ self._needs_redraw = True
+ self.queue_draw()
# Interface (Zoomable)
def zoomChanged(self):
- self.remove_all_children()
- self._allAnimated = True
- self._update()
-
- def _get_visible_range(self):
- # Shortcut/convenience variables:
- start = self.bElement.props.start
- in_point = self.bElement.props.in_point
- duration = self.bElement.props.duration
- timeline_left, timeline_right = self._get_visible_timeline_range()
-
- element_left = timeline_left - start + in_point
- element_left = max(element_left, in_point)
- element_right = timeline_right - start + in_point
- element_right = min(element_right, in_point + 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)
+ self._remove_all_children()
+ self._force_redraw()
# Callbacks
@@ -519,23 +465,25 @@ class VideoPreviewer(Clutter.ScrollActor, PreviewGenerator, Zoomable, Loggable):
return True
return False
+ def _heightChangedCb(self, unused_widget, unused_value):
+ self._remove_all_children()
+ self._force_redraw()
+
def _scrollCb(self, unused):
- self._update()
+ self._force_redraw()
def _startChangedCb(self, unused_bElement, unused_value):
- self._update()
+ self._force_redraw()
def _inpointChangedCb(self, unused_bElement, unused_value):
- position = Clutter.Point()
- position.x = Zoomable.nsToPixel(self.bElement.props.in_point)
- self.scroll_to_point(position)
- self._update()
+ self.get_hadjustment().set_value(Zoomable.nsToPixel(self.bElement.props.in_point))
+ self._force_redraw()
def _durationChangedCb(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()
+ self._force_redraw()
def startGeneration(self):
self._setupPipeline()
@@ -556,38 +504,23 @@ class VideoPreviewer(Clutter.ScrollActor, PreviewGenerator, Zoomable, Loggable):
self.stopGeneration()
Zoomable.__del__(self)
+ def do_draw(self, context):
+ clipped_rect = Gdk.cairo_get_clip_rectangle(context)[1]
+ if self._needs_redraw:
+ if self._addVisibleThumbnails(clipped_rect):
+ self._needs_redraw = False
-class Thumbnail(Clutter.Actor):
+ Gtk.Layout.do_draw(self, context)
+
+
+class Thumbnail(Gtk.Image):
def __init__(self, width, height):
- Clutter.Actor.__init__(self)
- image = Clutter.Image.new()
- self.props.content = image
+ super(Thumbnail, self).__init__()
self.width = width
self.height = height
- self.set_opacity(0)
- self.set_size(self.width, self.height)
- self.has_pixel_data = False
-
- def set_from_gdkpixbuf(self, gdkpixbuf):
- row_stride = gdkpixbuf.get_rowstride()
- pixel_data = gdkpixbuf.get_pixels()
- alpha = gdkpixbuf.get_has_alpha()
- self.has_pixel_data = True
- 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)
- self.set_opacity(255)
-
- def set_from_gdkpixbuf_animated(self, gdkpixbuf):
- self.save_easing_state()
- self.set_easing_duration(750)
- self.set_from_gdkpixbuf(gdkpixbuf)
- self.restore_easing_state()
-
+ self.props.width_request = self.width
+ self.props.height_request = self.height
caches = {}
@@ -756,7 +689,7 @@ class PipelineCpuAdapter(Loggable):
self.ready = False
-class AudioPreviewer(Clutter.Actor, PreviewGenerator, Zoomable, Loggable):
+class AudioPreviewer(Gtk.Layout, PreviewGenerator, Zoomable, Loggable):
"""
Audio previewer based on the results from the "level" gstreamer element.
@@ -764,8 +697,8 @@ class AudioPreviewer(Clutter.Actor, PreviewGenerator, Zoomable, Loggable):
__gsignals__ = PREVIEW_GENERATOR_SIGNALS
- def __init__(self, bElement, timeline):
- Clutter.Actor.__init__(self)
+ def __init__(self, bElement):
+ super(AudioPreviewer, self).__init__()
PreviewGenerator.__init__(self, GES.TrackType.AUDIO)
Zoomable.__init__(self)
Loggable.__init__(self)
@@ -773,28 +706,27 @@ class AudioPreviewer(Clutter.Actor, PreviewGenerator, Zoomable, Loggable):
self.pipeline = None
self.discovered = False
self.bElement = bElement
+ self.timeline = bElement.get_parent().get_timeline().ui
+
+ self.nSamples = self.bElement.get_parent().get_asset().get_duration() / 10000000
+ self._start = 0
+ self._end = 0
+ self._surface_x = 0
+
# Guard against malformed URIs
self._uri = quote_uri(bElement.props.uri)
- self.timeline = timeline
- self.actors = []
-
- self.set_content_scaling_filters(
- Clutter.ScalingFilter.NEAREST, Clutter.ScalingFilter.NEAREST)
- self.canvas = Clutter.Canvas()
- self.set_content(self.canvas)
- self.width = 0
- self._num_failures = 0
- self.lastUpdate = None
- self.current_geometry = (-1, -1)
+ self._num_failures = 0
self.adapter = None
self.surface = None
- self.timeline.connect("scrolled", self._scrolledCb)
- self.canvas.connect("draw", self._drawContentCb)
- self.canvas.invalidate()
- self._callback_id = 0
+ self._force_redraw = True
+
+ self.bElement.connect("notify::in-point", self._inpointChangedCb)
+
+ def _inpointChangedCb(self, unused_bElement, unused_value):
+ self._force_redraw = True
def startLevelsDiscoveryWhenIdle(self):
self.debug('Waiting for UI to become idle for: %s',
@@ -834,67 +766,10 @@ class AudioPreviewer(Clutter.Actor, PreviewGenerator, Zoomable, Loggable):
self.becomeControlled()
def set_size(self, unused_width, unused_height):
- if self.discovered:
- self._maybeUpdate()
+ self._force_redraw = True
def zoomChanged(self):
- self._maybeUpdate()
-
- def _maybeUpdate(self):
- if self.discovered:
- self.log('Checking if the waveform for "%s" needs to be redrawn' %
- self._uri)
- if self.lastUpdate is None or datetime.now() - self.lastUpdate > WAVEFORM_UPDATE_INTERVAL:
- # Last update was long ago or never.
- self._compute_geometry()
- else:
- if self._callback_id:
- GLib.source_remove(self._callback_id)
- self._callback_id = GLib.timeout_add(
- 500, self._compute_geometry)
-
- def _compute_geometry(self):
- self._callback_id = 0
- self.log("Computing the clip's geometry for waveforms")
- self.lastUpdate = datetime.now()
- width_px = self.nsToPixel(self.bElement.props.duration)
- if width_px <= 0:
- return
- start = self.timeline.get_scroll_point().x - self.nsToPixel(
- self.bElement.props.start)
- start = max(0, start)
- # Take into account the timeline width, to avoid building
- # huge clips when the timeline is zoomed in a lot.
- timeline_width = self.timeline._container.get_allocation(
- ).width - CONTROL_WIDTH
- end = min(width_px,
- self.timeline.get_scroll_point().x + timeline_width + MARGIN)
- self.width = int(end - start)
- # We've been called at a moment where size was updated but not
- # scroll_point.
- if self.width < 0:
- return
-
- # We need to take duration and inpoint into account.
- asset_duration = self.bElement.get_parent().get_asset().get_duration()
- if self.bElement.props.duration:
- nbSamples = self.nbSamples / \
- (float(asset_duration) / float(self.bElement.props.duration))
- else:
- nbSamples = self.nbSamples
- if self.bElement.props.in_point:
- startOffsetSamples = self.nbSamples / \
- (float(asset_duration) / float(self.bElement.props.in_point))
- else:
- startOffsetSamples = 0
-
- self.start = int(start / width_px * nbSamples + startOffsetSamples)
- self.end = int(end / width_px * nbSamples + startOffsetSamples)
-
- self.canvas.set_size(self.width, EXPANDED_SIZE)
- Clutter.Actor.set_size(self, self.width, EXPANDED_SIZE)
- self.set_position(start, self.props.y)
- self.canvas.invalidate()
+ self._force_redraw = True
def _prepareSamples(self):
# Let's go mono.
@@ -913,7 +788,6 @@ class AudioPreviewer(Clutter.Actor, PreviewGenerator, Zoomable, Loggable):
self.discovered = True
self.start = 0
self.end = self.nbSamples
- self._compute_geometry()
if self.adapter:
self.adapter.stop()
@@ -992,25 +866,38 @@ class AudioPreviewer(Clutter.Actor, PreviewGenerator, Zoomable, Loggable):
return True
return False
- def _drawContentCb(self, unused_canvas, context, unused_surf_w, unused_surf_h):
- context.set_operator(cairo.OPERATOR_CLEAR)
- context.paint()
+ def _get_num_inpoint_samples(self):
+ if self.bElement.props.in_point:
+ asset_duration = self.bElement.get_asset().get_filesource_asset().get_duration()
+ return int(self.nbSamples / (float(asset_duration) / float(self.bElement.props.in_point)))
+
+ return 0
+
+ def do_draw(self, context):
if not self.discovered:
return
- if self.surface:
- self.surface.finish()
+ clipped_rect = Gdk.cairo_get_clip_rectangle(context)[1]
- self.surface = renderer.fill_surface(
- self.samples[self.start:self.end], int(self.width), int(EXPANDED_SIZE))
+ num_inpoint_samples = self._get_num_inpoint_samples()
+ start = int(self.pixelToNs(clipped_rect.x) / 10000000) + num_inpoint_samples
+ end = int((self.pixelToNs(clipped_rect.x) + self.pixelToNs(clipped_rect.width)) / 10000000) +
num_inpoint_samples
+
+ if self._force_redraw or self._surface_x > clipped_rect.x or self._end < end:
+ self._start = start
+ end = int(min(self.nSamples, end + (self.pixelToNs(MARGIN) / 10000000)))
+ self._end = end
+ self._surface_x = clipped_rect.x
+ self.surface = renderer.fill_surface(self.samples[start:end],
+ min(self.props.width_request - clipped_rect.x,
clipped_rect.width + MARGIN),
+ int(self.get_parent().get_allocation().height))
+
+ self._force_redraw = False
context.set_operator(cairo.OPERATOR_OVER)
- context.set_source_surface(self.surface, 0, 0)
+ context.set_source_surface(self.surface, self._surface_x, 0)
context.paint()
- def _scrolledCb(self, unused):
- self._maybeUpdate()
-
def startGeneration(self):
self.pipeline.set_state(Gst.State.PLAYING)
if self.adapter is not None:
@@ -1029,6 +916,5 @@ class AudioPreviewer(Clutter.Actor, PreviewGenerator, Zoomable, Loggable):
def cleanup(self):
self.stopGeneration()
- self.canvas.disconnect_by_func(self._drawContentCb)
self.timeline.disconnect_by_func(self._scrolledCb)
Zoomable.__del__(self)
diff --git a/pitivi/timeline/ruler.py b/pitivi/timeline/ruler.py
index 9b8ca17..24df0b0 100644
--- a/pitivi/timeline/ruler.py
+++ b/pitivi/timeline/ruler.py
@@ -34,7 +34,7 @@ from gettext import gettext as _
from pitivi.utils.pipeline import Seeker
from pitivi.utils.timeline import Zoomable
from pitivi.utils.loggable import Loggable
-from pitivi.utils.ui import NORMAL_FONT, PLAYHEAD_COLOR, PLAYHEAD_WIDTH, set_cairo_color, time_to_string,
beautify_length
+from pitivi.utils.ui import NORMAL_FONT, PLAYHEAD_WIDTH, set_cairo_color, time_to_string, beautify_length
HEIGHT = 25
@@ -113,7 +113,6 @@ class ScaleRuler(Gtk.DrawingArea, Zoomable, Loggable):
self.connect('draw', self.drawCb)
self.connect('configure-event', self.configureEventCb)
self.callback_id = None
- self.callback_id_scroll = None
self.set_size_request(0, HEIGHT)
style = self.get_style_context()
@@ -138,22 +137,12 @@ class ScaleRuler(Gtk.DrawingArea, Zoomable, Loggable):
def _hadjValueChangedCb(self, unused_arg):
self.pixbuf_offset = self.hadj.get_value()
- if self.callback_id_scroll is not None:
- GLib.source_remove(self.callback_id_scroll)
- self.callback_id_scroll = GLib.timeout_add(100, self._maybeUpdate)
+ self.queue_draw()
# Zoomable interface override
- def _maybeUpdate(self):
- self.queue_draw()
- self.callback_id = None
- self.callback_id_scroll = None
- return False
-
def zoomChanged(self):
- if self.callback_id is not None:
- GLib.source_remove(self.callback_id)
- self.callback_id = GLib.timeout_add(100, self._maybeUpdate)
+ self.queue_draw()
# Timeline position changed method
@@ -398,7 +387,7 @@ class ScaleRuler(Gtk.DrawingArea, Zoomable, Loggable):
# without this the line appears blurry.
xpos = self.nsToPixel(self.position) - self.pixbuf_offset + 0.5
context.set_line_width(PLAYHEAD_WIDTH + 2)
- set_cairo_color(context, PLAYHEAD_COLOR)
+ set_cairo_color(context, (255, 0, 0))
context.move_to(xpos, 0)
context.line_to(xpos, context.get_target().get_height())
context.stroke()
diff --git a/pitivi/timeline/timeline.py b/pitivi/timeline/timeline.py
index d8ea45f..508eac0 100644
--- a/pitivi/timeline/timeline.py
+++ b/pitivi/timeline/timeline.py
@@ -24,28 +24,26 @@ import os
from gettext import gettext as _
-from gi.repository import Clutter
from gi.repository import GES
from gi.repository import GLib
-from gi.repository import GObject
from gi.repository import Gdk
from gi.repository import Gst
from gi.repository import Gtk
-from gi.repository import GtkClutter
+from pitivi.utils import ui
from pitivi.autoaligner import AlignmentProgressDialog, AutoAligner
from pitivi.configure import get_ui_dir
from pitivi.dialogs.prefs import PreferencesDialog
from pitivi.settings import GlobalSettings
-from pitivi.timeline.controls import ControlContainer
-from pitivi.timeline.elements import URISourceElement, TransitionElement, Ghostclip
from pitivi.timeline.ruler import ScaleRuler
from pitivi.utils.loggable import Loggable
-from pitivi.utils.pipeline import PipelineError
-from pitivi.utils.timeline import Zoomable, Selection, SELECT, TimelineError
-from pitivi.utils.ui import alter_style_class, EFFECT_TARGET_ENTRY, EXPANDED_SIZE, SPACING, PLAYHEAD_COLOR,
PLAYHEAD_WIDTH, CONTROL_WIDTH
+from pitivi.utils.timeline import Zoomable, TimelineError
+from pitivi.utils.ui import alter_style_class, EXPANDED_SIZE, SPACING, CONTROL_WIDTH
from pitivi.utils.widgets import ZoomBox
+from pitivi.timeline.elements import Clip
+from pitivi.utils import timeline as timelineUtils
+from pitivi.timeline.layer import SpacedSeparator, Layer, LayerControls
GlobalSettings.addConfigOption('edgeSnapDeadband',
section="user-interface",
@@ -73,12 +71,6 @@ PreferencesDialog.addNumericPreference('imageClipLength',
"Default clip length (in miliseconds) of images when inserting on
the timeline."),
lower=1)
-# Colors
-TIMELINE_BACKGROUND_COLOR = Clutter.Color.new(31, 30, 33, 255)
-SELECTION_MARQUEE_COLOR = Clutter.Color.new(100, 100, 100, 200)
-SNAPPING_INDICATOR_COLOR = Clutter.Color.new(50, 150, 200, 200)
-
-
"""
Convention throughout this file:
Every GES element which name could be mistaken with a UI element
@@ -86,61 +78,246 @@ is prefixed with a little b, example : bTimeline
"""
-class TimelineStage(Clutter.ScrollActor, Zoomable, Loggable):
+class VerticalBar(Gtk.DrawingArea, Loggable):
+ """
+ A simple vertical bar to be drawn on top of the timeline
+ """
+ __gtype_name__ = "PitiviVerticalBar"
+
+ def __init__(self, css_class):
+ super(VerticalBar, self).__init__()
+ Loggable.__init__(self)
+ self.get_style_context().add_class(css_class)
+ def do_get_preferred_width(self):
+ self.debug("Getting prefered height")
+ return ui.PLAYHEAD_WIDTH, ui.PLAYHEAD_WIDTH
+
+ def do_get_preferred_height(self):
+ self.debug("Getting prefered height")
+ return self.get_parent().get_allocated_height(), self.get_parent().get_allocated_height()
+
+
+class Marquee(Gtk.Box, Loggable):
"""
- The timeline view showing the clips.
+ Marquee widget representing a selection area inside the timeline
+ it should be drawn on top of the timeline layout.
+
+ It provides an API that makes it easy to update its value directly
+ from Gdk.Event
"""
- __gsignals__ = {
- 'scrolled': (GObject.SIGNAL_RUN_FIRST, None, ())
- }
+ __gtype_name__ = "PitiviMarquee"
- def __init__(self, container, settings):
- Clutter.ScrollActor.__init__(self)
- Zoomable.__init__(self)
+ def __init__(self, timeline):
+ """
+ @timeline: The #Timeline on which the marquee will
+ be used
+ """
+ super(Marquee, self).__init__()
+ Loggable.__init__(self)
+
+ self._timeline = timeline
+ self.start_x = None
+ self.start_y = None
+ self.set_visible(False)
+
+ self.get_style_context().add_class("Marquee")
+
+ def hide(self):
+ self.start_x = None
+ self.start_y = None
+ self.props.height_request = -1
+ self.props.width_request = -1
+ self.set_visible(False)
+
+ def setStartPosition(self, event):
+ event_widget = self._timeline.get_event_widget(event)
+ x, y = event_widget.translate_coordinates(self._timeline, event.x, event.y)
+
+ self.start_x, self.start_y = self._timeline.adjustCoords(x=x, y=y)
+
+ def move(self, event):
+ event_widget = self._timeline.get_event_widget(event)
+
+ x, y = self._timeline.adjustCoords(coords=event_widget.translate_coordinates(self._timeline,
event.x, event.y))
+
+ start_x = min(x, self.start_x)
+ start_y = min(y, self.start_y)
+
+ self.get_parent().move(self, start_x, start_y)
+ self.props.width_request = abs(self.start_x - x)
+ self.props.height_request = abs(self.start_y - y)
+ self.set_visible(True)
+
+ def findSelected(self):
+ x, y = self._timeline.layout.child_get(self, "x", "y")
+ res = []
+
+ w = self.props.width_request
+ for layer in self._timeline.bTimeline.get_layers():
+ intersects, unused_rect = Gdk.rectangle_intersect(layer.ui.get_allocation(),
self.get_allocation())
+
+ if not intersects:
+ continue
+
+ for clip in layer.get_clips():
+ if self.contains(clip, x, w):
+ toplevel = clip.get_toplevel_parent()
+ if isinstance(toplevel, GES.Group) and toplevel != self._timeline.current_group:
+ res.extend([c for c in clip.get_toplevel_parent().get_children(True)
+ if isinstance(c, GES.Clip)])
+ else:
+ res.append(clip)
+
+ self.debug("Selected clips: %s" % res)
+
+ return tuple(set(res))
+
+ def contains(self, clip, marquee_start, marquee_width):
+ if clip.ui is None:
+ return False
+
+ child_start = clip.ui.get_parent().child_get(clip.ui, "x")[0]
+ child_end = child_start + clip.ui.get_allocation().width
+
+ marquee_end = marquee_start + marquee_width
+
+ if child_start <= marquee_start <= child_end:
+ return True
+
+ if child_start <= marquee_end <= child_end:
+ return True
+
+ if marquee_start <= child_start and marquee_end >= child_end:
+ return True
+
+ return False
+
+
+class Timeline(Gtk.EventBox, timelineUtils.Zoomable, Loggable):
+ """
+ The main timeline Widget, it contains the representation of the GESTimeline
+ without any extra widgets.
+ """
+
+ __gtype_name__ = "PitiviTimeline"
+
+ def __init__(self, container, app):
+ super(Timeline, self).__init__()
+
+ timelineUtils.Zoomable.__init__(self)
Loggable.__init__(self)
+
+ self._main_hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
+ self.add(self._main_hbox)
+
+ self.layout = Gtk.Layout()
+ self.hadj = self.layout.get_hadjustment()
+ self.vadj = self.layout.get_vadjustment()
+
+ self.__layers_controls_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
+
+ # Stuff the layers controls in a viewport so it can be scrolled.
+ viewport = Gtk.Viewport(vadjustment=self.vadj)
+ viewport.add(self.__layers_controls_vbox)
+
+ # Make sure the viewport has no border or other decorations.
+ viewport_style = viewport.get_style_context()
+ for css_class in viewport_style.list_classes():
+ viewport_style.remove_class(css_class)
+ self._main_hbox.pack_start(viewport, False, False, 0)
+
+ self._main_hbox.pack_start(self.layout, False, True, 0)
+ self.get_style_context().add_class("Timeline")
+
+ self.__layers_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
+ self.__layers_vbox.props.width_request = self.get_allocated_width()
+ self.__layers_vbox.props.height_request = self.get_allocated_height()
+ self.layout.put(self.__layers_vbox, 0, 0)
+
self.bTimeline = None
+ self.__last_position = 0
+ self.selection = timelineUtils.Selection()
+
+ self._layers = []
+ self.parent = container
+ self.app = app
+ self.__snap_position = 0
self._project = None
+
+ self.current_group = None
self.createSelectionGroup()
- self._container = container
- self.allowSeek = True
- self._settings = settings
- self.elements = []
- self.ghostClips = []
- self.selection = Selection()
- self._scroll_point = Clutter.Point()
- self.lastPosition = 0 # Saved for redrawing when paused
- self.mouse = self._peekMouse()
-
- # The markers are used for placing clips at the right depth.
- # The first marker added as a child is the furthest and
- # the latest added marker is the closest to the viewer.
-
- # All the audio, video, image, title clips are placed above this
- # marker.
- self._clips_marker = Clutter.Actor()
- self.add_child(self._clips_marker)
- # All the transition clips are placed above this marker.
- self._transitions_marker = Clutter.Actor()
- self.add_child(self._transitions_marker)
-
- # Add the playhead later so it appears on top of all the clips.
- self.playhead = self._createPlayhead()
- self.add_child(self.playhead)
-
- self._snap_indicator = self._createSnapIndicator()
- self.add_child(self._snap_indicator)
-
- # Add the drag and drop marquee so it appears on top of the playhead.
- self.marquee = self._setUpDragAndDrop()
- self.add_child(self.marquee)
- self.drawMarquee = False
+ self.__playhead = VerticalBar("PlayHead")
+ self.__playhead.show()
+ self.layout.put(self.__playhead, self.nsToPixel(self.__last_position), 0)
- # Public API
+ self.__snap_bar = VerticalBar("SnapBar")
+ self.layout.put(self.__snap_bar, 0, 0)
+
+ self.__allow_seek = True
+
+ self.__setupTimelineEdition()
+ self.__setUpDragAndDrop()
+ self.__setupSelectionMarquee()
+
+ self.__button_pressed = False
+
+ # Setup our Gtk.Widget properties
+ self.add_events(Gdk.EventType.BUTTON_PRESS | Gdk.EventType.BUTTON_RELEASE)
+ self.connect("scroll-event", self.__scrollEventCb)
+ self.connect("button-press-event", self.__buttonPressEventCb)
+ self.connect("button-release-event", self.__buttonReleaseEventCb)
+ self.connect("motion-notify-event", self.__motionNotifyEventCb)
+ self.connect("drag-motion", self.__dragMotionCb)
+ self.connect("drag-leave", self.__dragLeaveCb)
+ self.connect("drag-drop", self.__dragDropCb)
+ self.connect("drag-data-received", self.__dragDataReceivedCb)
+
+ self.props.expand = True
+ self.get_accessible().set_name("timeline canvas")
+ self.__fake_event_widget = None
+
+ def sendFakeEvent(self, event, event_widget):
+ # Member usefull for testsing
+ self.__fake_event_widget = event_widget
+
+ self.info("Faking %s" % event)
+ if event.type == Gdk.EventType.BUTTON_PRESS:
+ self.__buttonPressEventCb(self, event)
+ elif event.type == Gdk.EventType.BUTTON_RELEASE:
+ self.__buttonReleaseEventCb(self, event)
+ elif event.type == Gdk.EventType.MOTION_NOTIFY:
+ self.__motionNotifyEventCb(self, event)
+
+ self.__fake_event_widget = None
+
+ def get_event_widget(self, event):
+ if self.__fake_event_widget:
+ return self.__fake_event_widget
+
+ return Gtk.get_event_widget(event)
+
+ def __get_event_widget(self, event):
+ if self.__fake_event_widget:
+ return self.__fake_event_widget
+
+ return Gtk.get_event_widget(event)
+
+ @property
+ def allowSeek(self):
+ return self.__allow_seek
+
+ @allowSeek.setter
+ def allowSeek(self, value):
+ self.debug("Setting AllowSeek to %s" % value)
+ self.__allow_seek = value
def createSelectionGroup(self):
+ if self.current_group:
+ GES.Container.ungroup(self.current_group, False)
+
self.current_group = GES.Group()
self.current_group.props.serialize = False
@@ -156,506 +333,606 @@ class TimelineStage(Clutter.ScrollActor, Zoomable, Loggable):
bTimeline = None
if self.bTimeline is not None:
- self.bTimeline.disconnect_by_func(self._trackAddedCb)
- self.bTimeline.disconnect_by_func(self._trackRemovedCb)
+ self.bTimeline.disconnect_by_func(self._durationChangedCb)
self.bTimeline.disconnect_by_func(self._layerAddedCb)
self.bTimeline.disconnect_by_func(self._layerRemovedCb)
self.bTimeline.disconnect_by_func(self._snapCb)
self.bTimeline.disconnect_by_func(self._snapEndedCb)
- for track in self.bTimeline.get_tracks():
- self._trackRemovedCb(self.bTimeline, track)
for layer in self.bTimeline.get_layers():
self._layerRemovedCb(self.bTimeline, layer)
+ self.bTimeline.ui = None
+
self.bTimeline = bTimeline
if bTimeline is None:
return
- for track in bTimeline.get_tracks():
- self._connectTrack(track)
for layer in bTimeline.get_layers():
- self._add_layer(layer)
+ self._addLayer(layer)
- self.bTimeline.connect("track-added", self._trackAddedCb)
- self.bTimeline.connect("track-removed", self._trackRemovedCb)
+ self.bTimeline.connect("notify::duration", self._durationChangedCb)
self.bTimeline.connect("layer-added", self._layerAddedCb)
self.bTimeline.connect("layer-removed", self._layerRemovedCb)
self.bTimeline.connect("snapping-started", self._snapCb)
self.bTimeline.connect("snapping-ended", self._snapEndedCb)
+ self.bTimeline.ui = self
- self.zoomChanged()
+ self.queue_draw()
- def findBrother(self, element):
- """
- Iterate over ui_elements to get the URI source with the same parent clip
- @param element: the ui_element for which we want to find the sibling.
- """
- father = element.get_parent()
- for elem in self.elements:
- if elem.bElement.get_parent() == father and elem.bElement != element:
- return elem
- return None
+ def _durationChangedCb(self, bTimeline, pspec):
+ self.queue_draw()
- def createLayerForGhostClip(self, ghostclip):
- """
- Creates a layer and moves subsequent layers down, if any.
+ def scrollToPlayhead(self,):
+ if self.__button_pressed or self.parent.ruler.pressed:
+ self.__button_pressed = False
+ return
- @param ghostclip: the ghostclip that was dropped, needing a new layer.
- @type ghostclip: L{Ghostclip}
- @rtype: L{GES.Layer}
- """
- layers = self.bTimeline.get_layers()
- if ghostclip.priority < len(layers):
- for layer in layers:
- if layer.get_priority() >= ghostclip.priority:
- layer.props.priority += 1
+ self.hadj.set_value(self.nsToPixel(self.__last_position) -
+ (self.layout.get_allocation().width / 2))
- layer = self.bTimeline.append_layer()
- layer.props.priority = ghostclip.priority
- self._project.pipeline.commit_timeline()
- self._container.controls._reorderLayerActors()
- return layer
-
- # Drag and drop from the medialibrary, handled by "ghost" (temporary) clips.
- # We create those when drag data is received, reset them after conversion.
- # This avoids bugs when dragging in and out of the timeline
-
- def resetGhostClips(self):
- self.ghostClips = []
-
- def addGhostClip(self, asset, unused_x, unused_y):
- ghostVideo = None
- if asset.get_supported_formats() & GES.TrackType.VIDEO:
- ghostVideo = self._createGhostclip(GES.TrackType.VIDEO, asset)
- ghostAudio = None
- if asset.get_supported_formats() & GES.TrackType.AUDIO:
- ghostAudio = self._createGhostclip(GES.TrackType.AUDIO, asset)
- self.ghostClips.append([ghostVideo, ghostAudio])
-
- def updateGhostClips(self, x, y):
- """
- This is called for each drag-motion.
- """
- priority = int(y / (EXPANDED_SIZE + SPACING))
- for ghostCouple in self.ghostClips:
- for ghostclip in ghostCouple:
- if ghostclip:
- ghostclip.update(priority, y, False)
- if x >= 0:
- ghostclip.props.x = x
- self._updateSize(ghostclip)
-
- def convertGhostClips(self):
+ def _positionCb(self, unused_pipeline, position):
+ if self.__last_position == position:
+ return
+
+ self.__last_position = position
+ self.scrollToPlayhead()
+ self.layout.move(self.__playhead, max(0, self.nsToPixel(self.__last_position)), 0)
+
+ # snapping indicator
+ def _snapCb(self, unused_timeline, unused_obj1, unused_obj2, position):
"""
- This is called at drag-drop
+ Display or hide a snapping indicator line
"""
- placement = 0
- layer = None
- for ghostVideo, ghostAudio in self.ghostClips:
- ghostclip = ghostVideo or ghostAudio
+ self.layout.move(self.__snap_bar, self.nsToPixel(position), 0)
+ self.__snap_bar.show()
+ self.__snap_position = position
+ self.debug("-> Snap START!")
- if layer is None:
- layer = self._getLayerForGhostClip(ghostclip)
+ def hideSnapBar(self):
+ self.debug("-> Force hiding snap bar")
+ self.__snap_position = 0
+ self.__snap_bar.hide()
- if ghostclip.asset.is_image():
- clip_duration = self._settings.imageClipLength * \
- Gst.SECOND / 1000.0
- else:
- clip_duration = ghostclip.asset.get_duration()
-
- if not placement:
- placement = Zoomable.pixelToNs(ghostclip.props.x)
- self._container.app.action_log.begin("add clip")
- layer.add_asset(ghostclip.asset,
- placement,
- 0,
- clip_duration,
- ghostclip.asset.get_supported_formats())
- self._container.app.action_log.commit()
- placement += clip_duration
- self._project.pipeline.commit_timeline()
+ def _snapEndedCb(self, *unused_args):
+ self.hideSnapBar()
+
+ # Gtk.Widget virtual methods implementation
+ def do_get_preferred_height(self):
+ natural_height = max(1, len(self._layers)) * (ui.LAYER_HEIGHT + 20)
+
+ return ui.LAYER_HEIGHT, natural_height
+
+ def do_draw(self, cr):
+ if self.bTimeline:
+ width = self._computeTheoricalWidth()
+ if self.draggingElement:
+ width = max(width, self.layout.props.width)
+
+ self.layout.set_size(width, len(self.bTimeline.get_layers()) * 200)
+
+ Gtk.EventBox.do_draw(self, cr)
+
+ self.__drawSnapIndicator(cr)
+ self.__drawPlayHead(cr)
- def _getLayerForGhostClip(self, ghostclip):
+ self.layout.propagate_draw(self.__marquee, cr)
+
+ def __drawSnapIndicator(self, cr):
+ if self.__snap_position > 0:
+ self.__snap_bar.props.height_request = self.layout.props.height
+ self.__snap_bar.props.width_request = ui.SNAPBAR_WIDTH
+
+ self.layout.propagate_draw(self.__snap_bar, cr)
+ else:
+ self.__snap_bar.hide()
+
+ def __drawPlayHead(self, cr):
+ self.__playhead.props.height_request = self.layout.props.height
+ self.__playhead.props.width_request = ui.PLAYHEAD_WIDTH
+
+ self.layout.propagate_draw(self.__playhead, cr)
+
+ # ------------- #
+ # util methods #
+ # ------------- #
+ def _computeTheoricalWidth(self):
+ if self.bTimeline is None:
+ return 100
+
+ return self.nsToPixel(self.bTimeline.props.duration)
+
+ def _getParentOfType(self, widget, _type):
"""
- Return the layer on which the specified ghostclip should be added.
+ Get a clip from a child widget, if the widget is a child of the clip
"""
- if ghostclip.shouldCreateLayer:
- return self.createLayerForGhostClip(ghostclip)
- for layer in self.bTimeline.get_layers():
- if layer.get_priority() == ghostclip.priority:
- return layer
- raise TimelineError()
-
- def removeGhostClips(self):
+ if isinstance(widget, _type):
+ return widget
+
+ parent = widget.get_parent()
+ while parent is not None and parent != self:
+ parent = parent.get_parent()
+
+ if isinstance(parent, _type):
+ return parent
+ return None
+
+ def adjustCoords(self, coords=None, x=None, y=None):
"""
- This is called at drag-leave. We don't empty the list on purpose.
+ Adjust coordinates passed as parametter that are raw
+ coordinates from the whole timeline into sensible
+ coordinates inside the visible area of the timeline.
"""
- for ghostCouple in self.ghostClips:
- for ghostclip in ghostCouple:
- if ghostclip and ghostclip.get_parent():
- self.remove_child(ghostclip)
- self._project.pipeline.commit_timeline()
+ if coords:
+ x = coords[0]
+ y = coords[1]
- def getActorUnderPointer(self):
- return self.mouse.get_pointer_actor()
+ if x is not None:
+ x += self.hadj.props.value
+ x -= ui.CONTROL_WIDTH
- # Internal API
+ if y is not None:
+ y += self.vadj.props.value
+
+ if x is None:
+ return y
+ else:
+ return x
+
+ return x, y
- def _elementIsInLasso(self, element, x1, y1, x2, y2):
- xE1 = element.props.x
- xE2 = element.props.x + element.props.width
- yE1 = element.props.y
- yE2 = element.props.y + element.props.height
+ # Gtk events management
+ def __scrollEventCb(self, unused_widget, event):
+ res, delta_x, delta_y = event.get_scroll_deltas()
+ if not res:
+ return False
- return self._segmentsOverlap((x1, x2), (xE1, xE2)) and self._segmentsOverlap((y1, y2), (yE1, yE2))
+ event_widget = self.get_event_widget(event)
+ x, y = event_widget.translate_coordinates(self, event.x, event.y)
+ if event.get_state() & Gdk.ModifierType.SHIFT_MASK:
+ if delta_y > 0:
+ self.parent.scroll_down()
+ elif delta_y < 0:
+ self.parent.scroll_up()
+ elif event.get_state() & Gdk.ModifierType.CONTROL_MASK:
+ if delta_y > 0:
+ timelineUtils.Zoomable.zoomOut()
+ self.queue_draw()
+ elif delta_y < 0:
+ timelineUtils.Zoomable.zoomIn()
+ self.queue_draw()
+ return False
- def _segmentsOverlap(self, a, b):
- x = max(a[0], b[0])
- y = min(a[1], b[1])
- return x < y
+ def __buttonPressEventCb(self, unused_widget, event):
+ event_widget = self.get_event_widget(event)
- def _translateToTimelineContext(self, event):
- event.x -= CONTROL_WIDTH
- event.x += self._scroll_point.x
- event.y += self._scroll_point.y
+ self.debug("PRESSED %s" % event)
+ self.__button_pressed = True
- delta_x = event.x - self.dragBeginStartX
- delta_y = event.y - self.dragBeginStartY
+ res, button = event.get_button()
+ if res and button == 1:
+ self.draggingElement = self._getParentOfType(event_widget, Clip)
+ self.debug("Dragging element is %s" % self.draggingElement)
+ if self.draggingElement is not None:
+ self.__drag_start_x = event.x
- newX = self.dragBeginStartX
- newY = self.dragBeginStartY
+ else:
+ self.__marquee.setStartPosition(event)
- # This is needed when you start to click and go left or up.
+ return False
- if delta_x < 0:
- newX = event.x
- delta_x = abs(delta_x)
+ def __buttonReleaseEventCb(self, unused_widget, event):
+ if self.draggingElement:
+ self.dragEnd()
+ else:
+ self._selectUnderMarquee()
- if delta_y < 0:
- newY = event.y
- delta_y = abs(delta_y)
+ if self.allowSeek:
+ event_widget = self.get_event_widget(event)
+ x, unused_y = event_widget.translate_coordinates(self, event.x, event.y)
+ x -= CONTROL_WIDTH
+ x += self.hadj.get_value()
- return newX, newY, delta_x, delta_y
+ position = self.pixelToNs(x)
+ self._project.seeker.seek(position)
- def _setUpDragAndDrop(self):
- self.set_reactive(True)
+ self.allowSeek = True
+ self._snapEndedCb()
- self._container.stage.connect("button-press-event", self._dragBeginCb)
- self._container.stage.connect("motion-event", self._dragProgressCb)
- self._container.stage.connect("button-release-event", self._dragEndCb)
- self._container.gui.connect("button-release-event", self._dragEndCb)
+ return False
- marquee = Clutter.Actor()
- marquee.set_background_color(SELECTION_MARQUEE_COLOR)
- marquee.hide()
- return marquee
+ def __motionNotifyEventCb(self, unused_widget, event):
+ if self.draggingElement:
+ state = event.get_state()
- @staticmethod
- def _peekMouse():
- manager = Clutter.DeviceManager.get_default()
- for device in manager.peek_devices():
- if device.props.device_type == Clutter.InputDeviceType.POINTER_DEVICE and device.props.enabled
is True:
- return device
+ if isinstance(state, tuple):
+ state = state[1]
- def _createGhostclip(self, trackType, asset):
- ghostclip = Ghostclip(trackType)
- ghostclip.asset = asset
- ghostclip.setNbrLayers(len(self.bTimeline.get_layers()))
+ if not state & Gdk.ModifierType.BUTTON1_MASK:
+ self.dragEnd()
+ return False
- if asset.is_image():
- clip_duration = self._settings.imageClipLength * \
- Gst.SECOND / 1000.0
- else:
- clip_duration = asset.get_duration()
+ self.__dragUpdate(self.get_event_widget(event), event.x, event.y)
+ self.got_dragged = True
+ elif self.__marquee.start_x:
+ self.__marquee.move(event)
- ghostclip.setWidth(Zoomable.nsToPixel(clip_duration))
- self.add_child(ghostclip)
- return ghostclip
+ return False
- def _connectTrack(self, track):
- for trackelement in track.get_elements():
- self._trackElementAddedCb(track, trackelement)
- track.connect("track-element-added", self._trackElementAddedCb)
- track.connect("track-element-removed", self._trackElementRemovedCb)
+ def _selectUnderMarquee(self):
+ if self.__marquee.props.width_request > 0:
+ clips = self.__marquee.findSelected()
- def _disconnectTrack(self, track):
- track.disconnect_by_func(self._trackElementAddedCb)
- track.disconnect_by_func(self._trackElementRemovedCb)
+ if clips:
+ self.createSelectionGroup()
- def _positionCb(self, unused_pipeline, position):
- self._movePlayhead(position)
- self._container._scrollToPlayhead()
- self.lastPosition = position
-
- def _updatePlayHead(self):
- height = len(self.bTimeline.get_layers()) * \
- (EXPANDED_SIZE + SPACING) * 2
- self.playhead.set_size(PLAYHEAD_WIDTH, height)
- self._movePlayhead(self.lastPosition)
-
- def _movePlayhead(self, position):
- self.playhead.props.x = self.nsToPixel(position)
-
- @staticmethod
- def _createPlayhead():
- playhead = Clutter.Actor()
- playhead.set_background_color(PLAYHEAD_COLOR)
- playhead.set_size(0, 0)
- playhead.set_position(0, 0)
- playhead.set_easing_duration(0)
- return playhead
-
- @staticmethod
- def _createSnapIndicator():
- indicator = Clutter.Actor()
- indicator.set_background_color(SNAPPING_INDICATOR_COLOR)
- indicator.props.visible = False
- indicator.props.width = 3
- indicator.props.y = 0
- return indicator
-
- def _addTimelineElement(self, track, bElement):
- if isinstance(bElement, GES.Effect):
- return
+ for clip in clips:
+ self.current_group.add(clip.get_toplevel_parent())
- if isinstance(bElement, GES.Transition):
- element = TransitionElement(bElement, self)
- marker = self._transitions_marker
- elif isinstance(bElement, GES.Source):
- element = URISourceElement(bElement, self)
- marker = self._clips_marker
+ self.selection.setSelection(clips, timelineUtils.SELECT)
+ else:
+ self.selection.setSelection([], timelineUtils.SELECT)
else:
- self.warning("Unknown element: %s", bElement)
- return
+ only_transitions = not bool([selected for selected in self.selection.selected
+ if not isinstance(selected, GES.TransitionClip)])
+ if not only_transitions:
+ self.selection.setSelection([], timelineUtils.SELECT)
- bElement.connect("notify::start", self._elementStartChangedCb, element)
- bElement.connect(
- "notify::duration", self._elementDurationChangedCb, element)
- bElement.connect(
- "notify::in-point", self._elementInPointChangedCb, element)
- bElement.connect(
- "notify::priority", self._elementPriorityChangedCb, element)
+ self.__marquee.hide()
- self.elements.append(element)
+ def updatePosition(self):
+ for layer in self._layers:
+ layer.updatePosition()
- self._setElementX(element, ease=True)
- self._setElementY(element)
+ self.queue_draw()
- self.insert_child_above(element, marker)
+ def __setupSelectionMarquee(self):
+ self.__marquee = Marquee(self)
+ self.layout.put(self.__marquee, 0, 0)
- def _removeTimelineElement(self, unused_track, bElement):
- if isinstance(bElement, GES.Effect):
- return
- bElement.disconnect_by_func(self._elementStartChangedCb)
- bElement.disconnect_by_func(self._elementDurationChangedCb)
- bElement.disconnect_by_func(self._elementInPointChangedCb)
- bElement.disconnect_by_func(self._elementPriorityChangedCb)
-
- element = self._getElement(bElement)
- if not element:
- raise TimelineError("Missing element for: " + bElement)
- element.cleanup()
- self.elements.remove(element)
- self.remove_child(element)
- self.selection.setSelection(set([]), SELECT)
-
- def _getElement(self, bElement):
- for element in self.elements:
- if element.bElement == bElement:
- return element
- return None
+ # drag and drop
+ def __setUpDragAndDrop(self):
+ self.got_dragged = False
+ self.dropHighlight = False
+ self.dropOccured = False
+ self.dropDataReady = False
+ self.dropData = None
+ self._createdClips = False
+ self.isDraggedClip = False
+ self._lastClipOnLeave = None
- def _setElementX(self, element, ease=False):
- if ease:
- element.save_easing_state()
- element.set_easing_duration(600)
- element.props.x = self.nsToPixel(element.bElement.get_start())
- if ease:
- element.restore_easing_state()
-
- # FIXME, change that when we have retractable layers
- def _setElementY(self, element):
- bElement = element.bElement
- track_type = bElement.get_track_type()
-
- y = 0
- if track_type == GES.TrackType.AUDIO:
- y = len(self.bTimeline.get_layers()) * (EXPANDED_SIZE + SPACING)
- y += bElement.get_parent().get_layer().get_priority() * \
- (EXPANDED_SIZE + SPACING) + SPACING
-
- element.save_easing_state()
- element.props.y = y
- element.restore_easing_state()
-
- def _updateSize(self, ghostclip=None):
- self.save_easing_state()
- self.set_easing_duration(0)
- self.props.width = self.nsToPixel(
- self.bTimeline.get_duration()) + CONTROL_WIDTH
- if ghostclip is not None:
- ghostEnd = ghostclip.props.x + \
- ghostclip.props.width + CONTROL_WIDTH
- self.props.width = max(ghostEnd, self.props.width)
- self.props.height = (len(self.bTimeline.get_layers()) + 1) * \
- (EXPANDED_SIZE + SPACING) * 2 + SPACING
- self.restore_easing_state()
- self._container.vadj.props.upper = self.props.height
- self._container.updateHScrollAdjustments()
-
- def _redraw(self):
- self._updateSize()
-
- self.save_easing_state()
- for element in self.elements:
- self._setElementX(element)
- self._setElementY(element)
- self.restore_easing_state()
-
- self._updatePlayHead()
-
- def _remove_layer(self, layer):
- self._container.controls.removeLayerControl(layer)
- self._redraw()
-
- def _add_layer(self, layer):
- self._redraw()
- self._container.controls.addLayerControl(layer)
-
- # Interface overrides
-
- # Zoomable Override
+ # To be able to receive effects dragged on clips.
+ self.drag_dest_set(0, [ui.EFFECT_TARGET_ENTRY], Gdk.DragAction.COPY)
+ # To be able to receive assets dragged from the media library.
+ self.drag_dest_add_uri_targets()
- def zoomChanged(self):
- self._redraw()
+ def createClip(self, x, y):
+ if self.isDraggedClip and self._createdClips is False:
- # Clutter Override
+ # From the media library
+ placement = 0
+ for uri in self.dropData:
+ asset = self.app.gui.medialibrary.getAssetForUri(uri)
+ if asset is None:
+ break
- # TODO: remove self._scroll_point and get_scroll_point as soon as the Clutter API
- # offers a way to query a ScrollActor for its current scroll point
- def scroll_to_point(self, point):
- Clutter.ScrollActor.scroll_to_point(self, point)
- self._scroll_point = point.copy()
- self.emit("scrolled")
+ if asset.is_image():
+ clip_duration = self._settings.imageClipLength * \
+ Gst.SECOND / 1000.0
+ else:
+ clip_duration = asset.get_duration()
- def get_scroll_point(self):
- return self._scroll_point
+ layer, on_sep = self.__getLayerAt(y)
+ if not placement:
+ placement = self.pixelToNs(x)
- # Callbacks
+ self.app.action_log.begin("add clip")
+ bClip = layer.add_asset(asset,
+ placement,
+ 0,
+ clip_duration,
+ asset.get_supported_formats())
+ self.app.action_log.commit()
- def _dragBeginCb(self, unused_actor, event):
- self.drawMarquee = self.getActorUnderPointer() == self
- if not self.drawMarquee:
- return
+ self.draggingElement = bClip.ui
+ self._createdClips = True
- if self.current_group:
- GES.Container.ungroup(self.current_group, False)
- self.createSelectionGroup()
+ return True
- self.dragBeginStartX = event.x - CONTROL_WIDTH + self._scroll_point.x
- self.dragBeginStartY = event.y + self._scroll_point.y
- self.marquee.set_size(0, 0)
- self.marquee.set_position(event.x - CONTROL_WIDTH, event.y)
- self.marquee.show()
+ return False
- def _dragProgressCb(self, unused_actor, event):
- if not self.drawMarquee:
- return False
+ def __dragMotionCb(self, unused_widget, context, x, y, timestamp):
- x, y, width, height = self._translateToTimelineContext(event)
+ target = self.drag_dest_find_target(context, None)
+ if not self.dropDataReady:
+ # We don't know yet the details of what's being dragged.
+ # Ask for the details.
+ self.drag_get_data(context, target, timestamp)
+ Gdk.drag_status(context, 0, timestamp)
+ else:
+ if not self.createClip(x, y):
+ self.__dragUpdate(self, x, y)
- self.marquee.set_position(x, y)
- self.marquee.set_size(width, height)
+ Gdk.drag_status(context, Gdk.DragAction.COPY, timestamp)
+ if not self.dropHighlight:
+ self.drag_highlight()
+ self.dropHighlight = True
+ return True
- return False
+ def __dragLeaveCb(self, unused_widget, unused_context, unused_timestamp):
+ if self.draggingElement:
+ self._lastClipOnLeave = (self.draggingElement.bClip.get_layer(), self.draggingElement.bClip)
+ self.draggingElement.bClip.get_layer().remove_clip(self.draggingElement.bClip)
+ self._createdClips = False
- def _dragEndCb(self, unused_actor, event):
- if not self.drawMarquee:
- return
- self.drawMarquee = False
+ def __dragDropCb(self, unused_widget, context, x, y, timestamp):
+ # Same as in insertEnd: this value changes during insertion, snapshot
+ # it
+ zoom_was_fitted = self.parent.zoomed_fitted
- x, y, width, height = self._translateToTimelineContext(event)
- elements = self._getElementsInRegion(x, y, width, height)
- self.createSelectionGroup()
- for element in elements:
- self.current_group.add(element)
- selection = [child for child in self.current_group.get_children(True)
- if isinstance(child, GES.Source)]
- self.selection.setSelection(selection, SELECT)
- self.marquee.hide()
-
- def _getElementsInRegion(self, x, y, width, height):
- elements = set()
- for element in self.elements:
- if self._elementIsInLasso(element, x, y, x + width, y + height):
- elements.add(element.bElement.get_toplevel_parent())
- return elements
+ target = self.drag_dest_find_target(context, None)
+ if target.name() == "text/uri-list":
+ self.debug("Got list of URIs")
+ if self._lastClipOnLeave:
+ self.dropData = None
+ self.dropDataReady = False
- # snapping indicator
- def _snapCb(self, unused_timeline, unused_obj1, unused_obj2, position):
- """
- Display or hide a snapping indicator line
- """
- if position == 0:
- self._snapEndedCb()
+ layer, clip = self._lastClipOnLeave
+ layer.add_clip(clip)
+
+ if zoom_was_fitted:
+ self.parent._setBestZoomRatio()
+
+ self.dragEnd()
+ elif target.name() == "pitivi/effect":
+ self.fixme("TODO Implement effect support")
+
+ return True
+
+ def __dragDataReceivedCb(self, unused_widget,
+ drag_context, unused_x,
+ unused_y, selection_data, unused_info, timestamp):
+ dragging_effect = selection_data.get_data_type().name() == "pitivi/effect"
+ if not self.dropDataReady:
+ self._lastClipOnLeave = None
+ if dragging_effect:
+ # Dragging an effect from the Effect Library.
+ factory_name = str(selection_data.get_data(), "UTF-8")
+ self.dropData = factory_name
+ self.dropDataReady = True
+ elif selection_data.get_length() > 0:
+ # Dragging assets from the Media Library.
+ # if not self.dropOccured:
+ # self.timeline.resetGhostClips()
+ self.dropData = selection_data.get_uris()
+ self.dropDataReady = True
+
+ if self.dropOccured:
+ # The data was requested by the drop handler.
+ self.dropOccured = False
+ drag_context.finish(True, False, timestamp)
else:
- height = len(self.bTimeline.get_layers()) * \
- (EXPANDED_SIZE + SPACING) * 2
- self._snap_indicator.props.height = height
- self._snap_indicator.props.x = Zoomable.nsToPixel(position)
- self._snap_indicator.props.visible = True
+ # The data was requested by the move handler.
+ self.isDraggedClip = not dragging_effect
+ self._createdClips = False
+ self.debug("Data received")
- def _snapEndedCb(self, *unused_args):
- self._snap_indicator.props.visible = False
+ # Handle layers
+ def _layerAddedCb(self, timeline, bLayer):
+ self._addLayer(bLayer)
+
+ def _addLayer(self, bLayer):
+ layer_widget = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
+
+ bLayer.control_ui = LayerControls(bLayer, self.app)
+ bLayer.ui = Layer(bLayer, self)
- def _layerAddedCb(self, unused_timeline, layer):
- self._add_layer(layer)
+ bLayer.ui.before_sep = SpacedSeparator()
+ layer_widget.pack_start(bLayer.ui.before_sep, False, False, 5)
+
+ self._layers.append(bLayer.ui)
+ layer_widget.pack_start(bLayer.ui, True, True, 0)
+
+ bLayer.ui.after_sep = SpacedSeparator()
+ layer_widget.pack_start(bLayer.ui.after_sep, False, False, 5)
+
+ self.__layers_vbox.pack_start(layer_widget, True, True, 0)
+ self.__layers_controls_vbox.pack_start(bLayer.control_ui, False, False, 0)
+
+ bLayer.ui.connect("remove-me", self._removeLayerCb)
+
+ self.show_all()
+
+ def _removeLayerCb(self, layer):
+ self.bTimeline.remove_layer(layer.bLayer)
+
+ def _removeLayer(self, bLayer):
+ self.info("Removing layer: %s" % bLayer.props.priority)
+ self.__layers_vbox.remove(bLayer.ui.get_parent())
+ self.__layers_controls_vbox.remove(bLayer.control_ui)
+
+ self._layers.remove(bLayer.ui)
+ bLayer.ui = None
+ bLayer.control_ui = None
+
+ self._layers.sort(key=lambda layer: layer.bLayer.props.priority)
+ i = 0
+ self.debug("Reseting layers priorities")
+ for layer in self._layers:
+ layer.bLayer.props.priority = i
+
+ self.__layers_vbox.child_set_property(layer.get_parent(),
+ "position",
+ layer.bLayer.props.priority)
+
+ self.__layers_controls_vbox.child_set_property(layer.bLayer.control_ui,
+ "position",
+ layer.bLayer.props.priority)
+
+ i += 1
def _layerRemovedCb(self, unused_timeline, layer):
- # FIXME : really remove layer ^^
- for lyr in self.bTimeline.get_layers():
- if lyr.props.priority > layer.props.priority:
- lyr.props.priority -= 1
- self._remove_layer(layer)
- self._updatePlayHead()
-
- def _trackAddedCb(self, unused_timeline, track):
- self._connectTrack(track)
- self._container.app.project_manager.current_project.update_restriction_caps(
- )
+ self._removeLayer(layer)
+
+ # Interface Zoomable
+ def zoomChanged(self):
+ self.debug("Zoom changed")
+ self.updatePosition()
+ self.layout.move(self.__playhead, self.nsToPixel(self.__last_position), 0)
+ self.queue_draw()
+
+ # Edition handling
+ def __setupTimelineEdition(self):
+ self.draggingElement = None
+ self.__editing_context = None
+ self.__got_dragged = False
+ self.__drag_start_x = 0
+ self.__on_separators = []
+ self._on_layer = None
+
+ def __getLayerAt(self, y, bLayer=None):
+ if y < 20 or not self.bTimeline.get_layers():
+ try:
+ bLayer = self.bTimeline.get_layers()[0]
+ except IndexError:
+ bLayer = self.bTimeline.append_layer()
+
+ self.debug("Returning very first layer")
+ return bLayer, [bLayer.ui.before_sep]
+
+ layers = self.bTimeline.get_layers()
+ rect = Gdk.Rectangle()
+ rect.x = 0
+ rect.y = y
+ rect.height = 1
+ rect.width = 1
+ for i in range(len(layers)):
+ layer = layers[i]
+ layer_alloc = layer.ui.get_allocation()
+
+ if Gdk.rectangle_intersect(rect, layer_alloc)[0] is True:
+ return layer, []
+
+ separators = [layer.ui.after_sep]
+ sep_rectangle = Gdk.Rectangle()
+ sep_rectangle.x = 0
+ sep_rectangle.y = layer_alloc.y + layer_alloc.height
+ try:
+ sep_rectangle.height = layers[i + 1].ui.get_allocation().y - \
+ layer_alloc.y - layer_alloc.height
+ separators.append(layers[i + 1].ui.before_sep)
+ except IndexError:
+ sep_rectangle.height += ui.LAYER_HEIGHT
+
+ if sep_rectangle.y <= rect.y <= sep_rectangle.y + sep_rectangle.height:
+ self.debug("Returning layer %s, separators: %s" % (layer, separators))
+ return layer, separators
+
+ self.debug("Returning very last layer")
+
+ return layers[-1], [layers[-1].ui.after_sep]
+
+ def __setHoverSeparators(self):
+ for sep in self.__on_separators:
+ ui.set_children_state_recurse(sep, Gtk.StateFlags.PRELIGHT)
+
+ def __unsetHoverSeparators(self):
+ for sep in self.__on_separators:
+ ui.unset_children_state_recurse(sep, Gtk.StateFlags.PRELIGHT)
+
+ def __dragUpdate(self, event_widget, x, y):
+ if self.__got_dragged is False:
+ self.__got_dragged = True
+ self.allowSeek = False
+ self.__editing_context = timelineUtils.EditingContext(self.draggingElement.bClip,
+ self.bTimeline,
+ self.draggingElement.edit_mode,
+ self.draggingElement.dragging_edge,
+ None,
+ self.app.action_log)
+
+ x, y = event_widget.translate_coordinates(self, x, y)
+ x -= ui.CONTROL_WIDTH
+ x += self.hadj.get_value()
+ y += self.vadj.get_value()
+
+ mode = self.get_parent().getEditionMode(isAHandle=self.__editing_context.edge != GES.Edge.EDGE_NONE)
+ self.__editing_context.setMode(mode)
+
+ if self.__editing_context.edge is GES.Edge.EDGE_END:
+ position = self.pixelToNs(x)
+ else:
+ position = self.pixelToNs(x - self.__drag_start_x)
+
+ self.__unsetHoverSeparators()
+
+ self._on_layer, self.__on_separators = self.__getLayerAt(y,
+ self.draggingElement.bClip.get_layer())
+
+ priority = self._on_layer.props.priority
+ if self.__on_separators:
+ self.__setHoverSeparators()
+
+ self.__editing_context.editTo(position, priority)
+
+ def createLayer(self, priority):
+ self.info("Creating layer %s" % priority)
+ new_bLayer = GES.Layer.new()
+ new_bLayer.props.priority = priority
+ self.bTimeline.add_layer(new_bLayer)
- def _trackRemovedCb(self, unused_timeline, track):
- self._disconnectTrack(track)
- for element in track.get_elements():
- self._removeTimelineElement(track, element)
+ bLayers = self.bTimeline.get_layers()
+ if priority < len(bLayers):
+ for bLayer in bLayers:
+ if bLayer == new_bLayer:
+ continue
- def _trackElementAddedCb(self, track, bElement):
- self._updateSize()
- self._addTimelineElement(track, bElement)
+ if bLayer.get_priority() >= priority:
+ bLayer.props.priority += 1
+ self.__layers_vbox.child_set_property(bLayer.ui.get_parent(),
+ "position",
+ bLayer.props.priority)
- def _trackElementRemovedCb(self, track, bElement):
- self._removeTimelineElement(track, bElement)
+ self.__layers_controls_vbox.child_set_property(bLayer.control_ui,
+ "position",
+ bLayer.props.priority)
+
+ self.__layers_vbox.child_set_property(new_bLayer.ui.get_parent(),
+ "position",
+ new_bLayer.props.priority)
+
+ self.__layers_controls_vbox.child_set_property(new_bLayer.control_ui,
+ "position",
+ new_bLayer.props.priority)
+
+ return new_bLayer
+
+ def dragEnd(self):
+ if self.draggingElement is not None and self.__got_dragged:
+ self.debug("DONE dargging %s" % self.draggingElement)
+ self._snapEndedCb()
- def _elementPriorityChangedCb(self, unused_bElement, unused_priority, element):
- self._setElementY(element)
+ if self.__on_separators:
+ priority = self._on_layer.props.priority
+ if self.__on_separators[0] == self._on_layer.ui.after_sep:
+ priority = self._on_layer.props.priority + 1
- def _elementStartChangedCb(self, unused_bElement, unused_start, element):
- self._updateSize()
- self.allowSeek = False
- self._setElementX(element)
+ self.createLayer(max(0, priority))
+ self._onSeparatorStartTime = None
+ self.__editing_context.editTo(self.__editing_context.new_position, priority)
+ self.layout.props.width = self._computeTheoricalWidth()
- def _elementDurationChangedCb(self, unused_bElement, unused_duration, element):
- self._updateSize()
- self.allowSeek = False
- element.update(ease=False)
+ self.__editing_context.finish()
- def _elementInPointChangedCb(self, unused_bElement, unused_inpoint, element):
- self.allowSeek = False
- self._setElementX(element)
+ self.draggingElement = None
+ self.__got_dragged = False
+ self.__editing_context = None
+ self.hideSnapBar()
- def _layerPriorityChangedCb(self, unused_layer, unused_priority):
- self._redraw()
+ self.__unsetHoverSeparators()
+ self.__on_separators = []
+
+ self.queue_draw()
class TimelineContainer(Gtk.Grid, Zoomable, Loggable):
@@ -671,8 +948,6 @@ class TimelineContainer(Gtk.Grid, Zoomable, Loggable):
# Allows stealing focus from other GTK widgets, prevent accidents:
self.props.can_focus = True
- self.connect("focus-in-event", self._focusInCb)
- self.connect("focus-out-event", self._focusOutCb)
self.gui = gui
self.ui_manager = ui_manager
@@ -688,12 +963,6 @@ class TimelineContainer(Gtk.Grid, Zoomable, Loggable):
self._createActions()
self._createUi()
- self.dropHighlight = False
- self.dropOccured = False
- self.dropDataReady = False
- self.dropData = None
- self._setUpDragAndDrop()
-
self._settings.connect("edgeSnapDeadbandChanged",
self._snapDistanceChangedCb)
@@ -763,36 +1032,10 @@ class TimelineContainer(Gtk.Grid, Zoomable, Loggable):
projectmanager.connect(
"new-project-loaded", self._projectChangedCb)
- def updateHScrollAdjustments(self):
- """
- Recalculate the horizontal scrollbar depending on the timeline duration.
- """
- timeline_ui_width = self.embed.get_allocation().width
- if self.bTimeline is None:
- contents_size = 0
- else:
- contents_size = Zoomable.nsToPixel(self.bTimeline.props.duration)
-
- # Provide some space for clip insertion at the end
- end_padding = CONTROL_WIDTH * 2
-
- self.hadj.props.lower = 0
- self.hadj.props.upper = contents_size + end_padding
- self.hadj.props.page_size = timeline_ui_width
- self.hadj.props.page_increment = contents_size * 0.9
- self.hadj.props.step_increment = contents_size * 0.1
-
- if contents_size <= timeline_ui_width:
- # We're zoomed out completely, re-enable automatic zoom fitting
- # when adding new clips.
- self.log("Setting 'zoomed_fitted' to True")
- self.zoomed_fitted = True
- else:
- self.log("Setting 'zoomed_fitted' to False")
- self.zoomed_fitted = False
-
def zoomFit(self):
- self._hscrollbar.set_value(0)
+ # self._hscrollbar.set_value(0)
+ self.app.write_action("set-zoom-fit", {"not-mandatory-action-type": True})
+
self._setBestZoomRatio(allow_zoom_in=True)
def scrollToPixel(self, x):
@@ -802,10 +1045,6 @@ class TimelineContainer(Gtk.Grid, Zoomable, Loggable):
else:
self._scrollToPixel(x)
- def seekInPosition(self, position):
- self.pressed = True
- self._seeker.seek(position)
-
def setProject(self, project):
self._project = project
if self._project:
@@ -847,48 +1086,25 @@ class TimelineContainer(Gtk.Grid, Zoomable, Loggable):
# Internal API
def _createUi(self):
- self.embed = GtkClutter.Embed()
- self.embed.get_accessible().set_name("timeline canvas") # for dogtail
- self.stage = self.embed.get_stage()
-
- self.timeline = TimelineStage(self, self._settings)
- self.controls = ControlContainer(self.app, self.timeline)
self.zoomBox = ZoomBox(self)
self._shiftMask = False
self._controlMask = False
- self.stage.set_background_color(TIMELINE_BACKGROUND_COLOR)
- self.timeline.set_position(CONTROL_WIDTH, 0)
- self.controls.set_position(0, 0)
+ # self.stage.set_background_color(TIMELINE_BACKGROUND_COLOR)
+ # self.timeline.set_position(CONTROL_WIDTH, 0)
+ # self.controls.set_position(0, 0)
- self.stage.add_child(self.controls)
- self.stage.add_child(self.timeline)
-
- self.timeline.connect("button-press-event", self._timelineClickedCb)
- self.timeline.connect(
- "button-release-event", self._timelineClickReleasedCb)
- # FIXME: Connect to the stage of the embed instead, see
- # https://bugzilla.gnome.org/show_bug.cgi?id=697522
- self.embed.connect("scroll-event", self._scrollEventCb)
-
- self.connect("key-press-event", self._keyPressEventCb)
- self.connect("key-release-event", self._keyReleaseEventCb)
-
- self.point = Clutter.Point()
- self.point.x = 0
- self.point.y = 0
+ # self.stage.add_child(self.controls)
+ # self.stage.add_child(self.timeline)
self.scrolled = 0
self.zoomed_fitted = True
- self.pressed = False
-
- self.hadj = Gtk.Adjustment()
- self.vadj = Gtk.Adjustment()
- self.hadj.connect("value-changed", self._updateScrollPosition)
- self.vadj.connect("value-changed", self._updateScrollPosition)
- self.vadj.props.lower = 0
- self.vadj.props.page_size = 250
+
+ self.timeline = Timeline(self, self.app)
+ self.hadj = self.timeline.layout.get_hadjustment()
+ self.vadj = self.timeline.layout.get_vadjustment()
+
self._vscrollbar = Gtk.VScrollbar(adjustment=self.vadj)
self._hscrollbar = Gtk.HScrollbar(adjustment=self.hadj)
@@ -926,7 +1142,7 @@ class TimelineContainer(Gtk.Grid, Zoomable, Loggable):
self.attach(self.zoomBox, 0, 0, 1, 1)
self.attach(self.ruler, 1, 0, 1, 1)
- self.attach(self.embed, 0, 1, 2, 1)
+ self.attach(self.timeline, 0, 1, 2, 1)
self.attach(self._vscrollbar, 2, 1, 1, 1)
self.attach(self._hscrollbar, 1, 2, 1, 1)
self.attach(toolbar, 3, 1, 1, 1)
@@ -944,28 +1160,14 @@ class TimelineContainer(Gtk.Grid, Zoomable, Loggable):
def disableKeyboardAndMouseEvents(self):
"""
- A safety measure to prevent interacting with the Clutter timeline
- during render (no, setting GtkClutterEmbed as insensitive won't work,
- neither will using handler_block_by_func, nor connecting to the "event"
- signals because they won't block the children and other widgets).
+ A safety measure to prevent interacting with the timeline
"""
self.info("Blocking timeline mouse and keyboard signals")
- self.stage.connect("captured-event", self._ignoreAllEventsCb)
+ self.timeline.connect("event", self._ignoreAllEventsCb)
def _ignoreAllEventsCb(self, *unused_args):
return True
- def _setUpDragAndDrop(self):
- # To be able to receive effects dragged on clips.
- self.drag_dest_set(0, [EFFECT_TARGET_ENTRY], Gdk.DragAction.COPY)
- # To be able to receive assets dragged from the media library.
- self.drag_dest_add_uri_targets()
-
- self.connect('drag-motion', self._dragMotionCb)
- self.connect('drag-data-received', self._dragDataReceivedCb)
- self.connect('drag-drop', self._dragDropCb)
- self.connect('drag-leave', self._dragLeaveCb)
-
def _getLayers(self):
"""
Make sure we have at least one layer in our timeline.
@@ -1069,17 +1271,6 @@ class TimelineContainer(Gtk.Grid, Zoomable, Loggable):
self.playhead_actions.add_actions(playhead_actions)
self.ui_manager.insert_action_group(self.playhead_actions, -1)
- def _updateScrollPosition(self, unused_adjustment):
- self._scroll_pos_ns = Zoomable.pixelToNs(self.hadj.get_value())
- point = Clutter.Point()
- point.x = self.hadj.get_value()
- point.y = self.vadj.get_value()
- self.point = point
-
- self.timeline.scroll_to_point(point)
- point.x = 0
- self.controls.scroll_to_point(point)
-
def _setBestZoomRatio(self, allow_zoom_in=False):
"""
Set the zoom level so that the entire timeline is in view.
@@ -1145,28 +1336,18 @@ class TimelineContainer(Gtk.Grid, Zoomable, Loggable):
(x, self.hadj.props.lower))
if self._project and self._project.pipeline.getState() != Gst.State.PLAYING:
- self.timeline.save_easing_state()
- self.timeline.set_easing_duration(600)
+ self.error("FIXME What should be done here?")
self._hscrollbar.set_value(x)
if self._project and self._project.pipeline.getState() != Gst.State.PLAYING:
- self.timeline.restore_easing_state()
+ self.error("FIXME What should be done here?")
+
+ self.timeline.updatePosition()
+ self.timeline.queue_draw()
return False
- def _scrollToPlayhead(self):
- if self.ruler.pressed or self.pressed:
- self.pressed = False
- return
- canvas_width = self.embed.get_allocation().width - CONTROL_WIDTH
- try:
- new_pos = Zoomable.nsToPixel(self._project.pipeline.getPosition())
- except PipelineError as e:
- self.info("Pipeline error: %s", e)
- return
- except AttributeError: # Standalone, no pipeline.
- return
- playhead_pos_centered = new_pos - canvas_width / 2
- self.scrollToPixel(max(0, playhead_pos_centered))
+ def scrollToPlayhead(self):
+ self.timeline.scrollToPlayhead()
def _deleteSelected(self, unused_action):
if self.bTimeline:
@@ -1174,6 +1355,8 @@ class TimelineContainer(Gtk.Grid, Zoomable, Loggable):
for clip in self.timeline.selection:
layer = clip.get_layer()
+ if isinstance(clip, GES.TransitionClip):
+ continue
layer.remove_clip(clip)
self._project.pipeline.commit_timeline()
@@ -1195,7 +1378,32 @@ class TimelineContainer(Gtk.Grid, Zoomable, Loggable):
containers.add(toplevel)
for container in containers:
- GES.Container.ungroup(container, False)
+ was_clip = isinstance(container, GES.Clip)
+ clips = GES.Container.ungroup(container, False)
+ if not was_clip:
+ continue
+
+ new_layers = {}
+ for clip in clips:
+ if isinstance(clip, GES.Clip):
+ all_audio = True
+ for child in clip.get_children(True):
+ if child.get_track_type() != GES.TrackType.AUDIO:
+ all_audio = False
+ break
+
+ if not all_audio:
+ self.debug("Not all audio, not moving anything to a new layer")
+
+ continue
+
+ new_layer = new_layers.get(clip.get_layer().get_priority(), None)
+ if not new_layer:
+ new_layer = self.timeline.createLayer(clip.get_layer().get_priority() + 1)
+ new_layers[clip.get_layer().get_priority()] = new_layer
+ self.info("Moving audio audio clip %s to new layer %s" % (clip, new_layer))
+ clip.move_to_layer(new_layer)
+
self._project.pipeline.commit_timeline()
self.timeline.createSelectionGroup()
@@ -1219,7 +1427,8 @@ class TimelineContainer(Gtk.Grid, Zoomable, Loggable):
containers.add(toplevel)
if containers:
- group = GES.Container.group(list(containers))
+ GES.Container.group(list(containers))
+
self.timeline.createSelectionGroup()
self._project.pipeline.commit_timeline()
@@ -1261,6 +1470,7 @@ class TimelineContainer(Gtk.Grid, Zoomable, Loggable):
for track in self.bTimeline.get_tracks():
self._splitElements(track.get_elements())
+ self.timeline.hideSnapBar()
self._project.pipeline.commit_timeline()
def _splitElements(self, elements):
@@ -1288,7 +1498,7 @@ class TimelineContainer(Gtk.Grid, Zoomable, Loggable):
for obj in selected:
keyframe_exists = False
position = self._project.pipeline.getPosition()
- position_in_obj = (position - obj.start) + obj.in_point
+ position_in_obj = (position - obj.props.start) + obj.props.in_point
interpolators = obj.getInterpolators()
for value in interpolators:
interpolator = obj.getInterpolator(value)
@@ -1320,11 +1530,9 @@ class TimelineContainer(Gtk.Grid, Zoomable, Loggable):
self.bTimeline.set_snapping_distance(
Zoomable.pixelToNs(self._settings.edgeSnapDeadband))
- self.updateHScrollAdjustments()
-
- # Callbacks
+ # Gtk widget virtual methods
- def _keyPressEventCb(self, unused_widget, event):
+ def do_key_press_event(self, event):
# This is used both for changing the selection modes and for affecting
# the seek keyboard shortcuts further below
if event.keyval == Gdk.KEY_Shift_L:
@@ -1345,33 +1553,25 @@ class TimelineContainer(Gtk.Grid, Zoomable, Loggable):
else:
self._project.pipeline.stepFrame(self._framerate, 1)
- def _keyReleaseEventCb(self, unused_widget, event):
+ def do_key_release_event(self, event):
if event.keyval == Gdk.KEY_Shift_L:
self._shiftMask = False
elif event.keyval == Gdk.KEY_Control_L:
self._controlMask = False
- def _focusInCb(self, unused_widget, unused_arg):
+ def do_focus_in_event(self, unused_event):
self.log("Timeline has grabbed focus")
self.setActionsSensitivity(True)
- def _focusOutCb(self, unused_widget, unused_arg):
+ def do_focus_out_event(self, unused_event):
self.log("Timeline has lost focus")
self.setActionsSensitivity(False)
- def _timelineClickedCb(self, unused_timeline, unused_event):
+ def __buttonPressCb(self, unused_event):
self.pressed = True
self.grab_focus() # Prevent other widgets from being confused
- def _timelineClickReleasedCb(self, unused_timeline, event):
- if self.app and self.timeline.allowSeek is True:
- position = self.pixelToNs(
- event.x - CONTROL_WIDTH + self.timeline._scroll_point.x)
- self._seeker.seek(position)
-
- self.timeline.allowSeek = True
- self.timeline._snapEndedCb()
-
+ # Callbacks
def _renderingSettingsChangedCb(self, project, item, value):
"""
Called when any Project metadata changes, we filter out the one
@@ -1445,26 +1645,6 @@ class TimelineContainer(Gtk.Grid, Zoomable, Loggable):
def _zoomFitCb(self, unused_action):
self.zoomFit()
- def _scrollEventCb(self, unused_embed, event):
- unused_res, delta_x, delta_y = event.get_scroll_deltas()
- if event.state & Gdk.ModifierType.CONTROL_MASK:
- if delta_y < 0:
- Zoomable.zoomIn()
- elif delta_y > 0:
- Zoomable.zoomOut()
- self._scrollToPlayhead()
- elif event.state & Gdk.ModifierType.SHIFT_MASK:
- if delta_y > 0:
- self.scroll_down()
- elif delta_y < 0:
- self.scroll_up()
- else:
- if delta_y > 0:
- self.scroll_right()
- elif delta_y < 0:
- self.scroll_left()
- self.scrolled += 1
-
def _selectionChangedCb(self, selection):
"""
The selected clips on the timeline canvas have changed with the
@@ -1486,96 +1666,3 @@ class TimelineContainer(Gtk.Grid, Zoomable, Loggable):
self.info("Automatic ripple deactivated")
self._autoripple_active = False
self._settings.timelineAutoRipple = self._autoripple_active
-
- # drag and drop
-
- def _dragMotionCb(self, widget, context, x, y, timestamp):
- target = widget.drag_dest_find_target(context, None)
- if not self.dropDataReady:
- # We don't know yet the details of what's being dragged.
- # Ask for the details.
- widget.drag_get_data(context, target, timestamp)
- Gdk.drag_status(context, 0, timestamp)
- else:
- if self.isDraggedClip:
- # From the media library
- x, y = self.transposeXY(x, y)
- if not self.timeline.ghostClips:
- for uri in self.dropData:
- asset = self.app.gui.medialibrary.getAssetForUri(uri)
- if asset is None:
- self.isDraggedClip = False
- break
- self.timeline.addGhostClip(asset, x, y)
- self.timeline.updateGhostClips(x, y)
-
- Gdk.drag_status(context, Gdk.DragAction.COPY, timestamp)
- if not self.dropHighlight:
- widget.drag_highlight()
- self.dropHighlight = True
- return True
-
- def _dragLeaveCb(self, widget, unused_context, unused_timestamp):
- if self.dropHighlight:
- widget.drag_unhighlight()
- self.dropHighlight = False
- # Cleanup because the user might have moved the mouse cursor outside
- # and abandon this widget.
- self.dropDataReady = False
- self.timeline.removeGhostClips()
-
- def _dragDropCb(self, widget, context, x, y, timestamp):
- # Same as in insertEnd: this value changes during insertion, snapshot
- # it
- zoom_was_fitted = self.zoomed_fitted
-
- target = widget.drag_dest_find_target(context, None)
- y -= self.ruler.get_allocation().height
- if target.name() == "text/uri-list":
- self.dropOccured = True
- widget.drag_get_data(context, target, timestamp)
- if self.isDraggedClip:
- self.timeline.convertGhostClips()
- self.timeline.resetGhostClips()
- self.dropData = None
- self.dropDataReady = False
- if zoom_was_fitted:
- self._setBestZoomRatio()
- else:
- x, y = self.transposeXY(x, y)
- # Add a margin (up to 50px) on the left, this prevents
- # disorientation & clarifies to users where the clip starts
- margin = min(x, 50)
- self.scrollToPixel(x - margin)
- elif target.name() == "pitivi/effect":
- actor = self.stage.get_actor_at_pos(
- Clutter.PickMode.REACTIVE, x, y)
- bElement = actor.bElement
- clip = bElement.get_parent()
- factory_name = self.dropData
- self.app.gui.clipconfig.effect_expander.addEffectToClip(
- clip, factory_name)
- return True
-
- def _dragDataReceivedCb(self, widget, drag_context, unused_x, unused_y, selection_data, unused_info,
timestamp):
- dragging_effect = selection_data.get_data_type().name() == "pitivi/effect"
- if not self.dropDataReady:
- if dragging_effect:
- # Dragging an effect from the Effect Library.
- factory_name = str(selection_data.get_data(), "UTF-8")
- self.dropData = factory_name
- self.dropDataReady = True
- elif selection_data.get_length() > 0:
- # Dragging assets from the Media Library.
- if not self.dropOccured:
- self.timeline.resetGhostClips()
- self.dropData = selection_data.get_uris()
- self.dropDataReady = True
-
- if self.dropOccured:
- # The data was requested by the drop handler.
- self.dropOccured = False
- drag_context.finish(True, False, timestamp)
- else:
- # The data was requested by the move handler.
- self.isDraggedClip = not dragging_effect
diff --git a/pitivi/transitions.py b/pitivi/transitions.py
index 86e9bbc..ec4fe23 100644
--- a/pitivi/transitions.py
+++ b/pitivi/transitions.py
@@ -173,11 +173,10 @@ class TransitionsListWidget(Gtk.Box, Loggable):
else:
self.props_widgets.set_sensitive(True)
- clip_asset = self.element.get_parent()
- clip_asset.set_asset(transition_asset)
+ self.element.set_asset(transition_asset)
self.app.write_action("element-set-asset", {
"asset-id": transition_asset.get_id(),
- "element-name": clip_asset.get_name()})
+ "element-name": self.element.get_name()})
self.app.project_manager.current_project.seeker.flush(True)
return True
@@ -289,7 +288,7 @@ class TransitionsListWidget(Gtk.Box, Loggable):
self.element.connect("notify::border", self._borderChangedCb)
self.element.connect("notify::invert", self._invertChangedCb)
self.element.connect("notify::type", self._transitionTypeChangedCb)
- transition_asset = element.get_parent().get_asset()
+ transition_asset = element.get_asset()
if transition_asset.get_id() == "crossfade":
self.props_widgets.set_sensitive(False)
else:
diff --git a/pitivi/undo/timeline.py b/pitivi/undo/timeline.py
index 6efb029..465d06f 100644
--- a/pitivi/undo/timeline.py
+++ b/pitivi/undo/timeline.py
@@ -20,6 +20,7 @@
# Boston, MA 02110-1301, USA.
from gi.repository import Gst
+from gi.repository import GstController
from gi.repository import GES
from gi.repository import GObject
diff --git a/pitivi/utils/pipeline.py b/pitivi/utils/pipeline.py
index 300636a..0de95d6 100644
--- a/pitivi/utils/pipeline.py
+++ b/pitivi/utils/pipeline.py
@@ -183,7 +183,7 @@ class SimplePipeline(GObject.Object, Loggable):
self._bus.add_signal_watch()
self._bus.connect("message", self._busMessageCb)
self._listening = False # for the position handler
- self._listeningInterval = 300 # default 300ms
+ self._listeningInterval = 50 # default 300ms
self._listeningSigId = 0
self._duration = Gst.CLOCK_TIME_NONE
self._last_position = int(0 * Gst.SECOND)
@@ -343,7 +343,7 @@ class SimplePipeline(GObject.Object, Loggable):
self._duration = dur
return dur
- def activatePositionListener(self, interval=500):
+ def activatePositionListener(self, interval=50):
"""
Activate the position listener.
@@ -433,9 +433,9 @@ class SimplePipeline(GObject.Object, Loggable):
self.debug("position: %s", format_ns(position))
# clamp between [0, duration]
- position = max(0, min(position, self.getDuration()) - 1)
+ position = max(0, min(position, self.getDuration()))
- res = self._pipeline.seek(1.0, Gst.Format.TIME, Gst.SeekFlags.FLUSH,
+ res = self._pipeline.seek(1.0, Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.ACCURATE,
Gst.SeekType.SET, position,
Gst.SeekType.NONE, -1)
self._addWaitingForAsyncDoneTimeout()
@@ -516,6 +516,14 @@ class SimplePipeline(GObject.Object, Loggable):
else:
self.log("%s [%r]", message.type, message.src)
+ @property
+ def _waiting_for_async_done(self):
+ return self.__waiting_for_async_done
+
+ @_waiting_for_async_done.setter
+ def _waiting_for_async_done(self, value):
+ self.__waiting_for_async_done = value
+
def _recover(self):
if self._attempted_recoveries > MAX_RECOVERIES:
self.emit("died")
diff --git a/pitivi/utils/timeline.py b/pitivi/utils/timeline.py
index a4d3dea..854396c 100644
--- a/pitivi/utils/timeline.py
+++ b/pitivi/utils/timeline.py
@@ -23,6 +23,10 @@
from gi.repository import GES
from gi.repository import GObject
from gi.repository import Gst
+from gi.repository import Gtk
+
+from pitivi.utils.loggable import Loggable
+from pitivi.utils import ui
# Selection modes
@@ -78,7 +82,7 @@ class Selected(GObject.Object):
selected = property(getSelected, setSelected)
-class Selection(GObject.Object):
+class Selection(GObject.Object, Loggable):
"""
A collection of L{GES.Clip}.
@@ -96,6 +100,7 @@ class Selection(GObject.Object):
def __init__(self):
GObject.Object.__init__(self)
+ Loggable.__init__(self)
self.selected = set()
def setToObj(self, obj, mode):
@@ -142,9 +147,12 @@ class Selection(GObject.Object):
for obj in old_selection - self.selected:
for element in obj.get_children(False):
+ ui.unset_children_state_recurse(obj.ui, Gtk.StateFlags.SELECTED)
if not isinstance(element, GES.BaseEffect) and not isinstance(element, GES.TextOverlay):
element.selected.selected = False
+
for obj in self.selected - old_selection:
+ ui.set_children_state_recurse(obj.ui, Gtk.StateFlags.SELECTED)
for element in obj.get_children(False):
if not isinstance(element, GES.BaseEffect) and not isinstance(element, GES.TextOverlay):
element.selected.selected = True
@@ -193,7 +201,7 @@ class Selection(GObject.Object):
# -----------------------------------------------------------------------------#
# Timeline edition modes helper #
-class EditingContext(GObject.Object):
+class EditingContext(GObject.Object, Loggable):
"""
Encapsulates interactive editing.
@@ -201,11 +209,6 @@ class EditingContext(GObject.Object):
This is the main class for interactive edition.
"""
- __gsignals__ = {
- "clip-trim": (GObject.SIGNAL_RUN_LAST, None, (GES.Clip, int)),
- "clip-trim-finished": (GObject.SIGNAL_RUN_LAST, None, ()),
- }
-
def __init__(self, focus, timeline, mode, edge, unused_settings, action_log):
"""
@param focus: the Clip or TrackElement which is to be the
@@ -230,6 +233,7 @@ class EditingContext(GObject.Object):
@returns: An instance of L{pitivi.utils.timeline.EditingContext}
"""
GObject.Object.__init__(self)
+ Loggable.__init__(self)
if isinstance(focus, GES.TrackElement):
self.focus = focus.get_parent()
else:
@@ -253,7 +257,7 @@ class EditingContext(GObject.Object):
def finish(self):
self.action_log.commit()
self.timeline.get_asset().pipeline.commit_timeline()
- self.emit("clip-trim-finished")
+ self.timeline.ui.app.gui.viewer.clipTrimPreviewFinished()
def setMode(self, mode):
"""Set the current editing mode.
@@ -271,8 +275,7 @@ class EditingContext(GObject.Object):
self.new_position = position
self.new_priority = priority
- res = self.focus.edit(
- [], priority, self.mode, self.edge, int(position))
+ res = self.focus.edit([], priority, self.mode, self.edge, int(position))
self.action_log.app.write_action("edit-container", {
"container-name": self.focus.get_name(),
"position": float(position / Gst.SECOND),
@@ -282,9 +285,10 @@ class EditingContext(GObject.Object):
if res and self.mode == GES.EditMode.EDIT_TRIM:
if self.edge == GES.Edge.EDGE_START:
- self.emit("clip-trim", self.focus, self.focus.props.in_point)
+ self.timeline.ui.app.gui.viewer.clipTrimPreview(self.focus, self.focus.props.in_point)
elif self.edge == GES.Edge.EDGE_END:
- self.emit("clip-trim", self.focus, self.focus.props.duration)
+ self.timeline.ui.app.gui.viewer.clipTrimPreview(self.focus,
+ self.focus.props.duration +
self.focus.props.in_point)
# -------------------------- Interfaces ----------------------------------------#
@@ -406,6 +410,18 @@ class Zoomable(object):
return int((float(duration) / Gst.SECOND) * cls.zoomratio)
@classmethod
+ def nsToPixelAccurate(cls, duration):
+ """
+ Returns the pixel equivalent of the given duration, according to the
+ set zoom ratio
+ """
+ # Here, a long time ago (206f3a05), a pissed programmer said:
+ # DIE YOU CUNTMUNCH CLOCK_TIME_NONE UBER STUPIDITY OF CRACK BINDINGS !!
+ if duration == Gst.CLOCK_TIME_NONE:
+ return 0
+ return ((float(duration) / Gst.SECOND) * cls.zoomratio)
+
+ @classmethod
def _zoomChanged(cls):
for inst in cls._instances:
inst.zoomChanged()
diff --git a/pitivi/utils/ui.py b/pitivi/utils/ui.py
index 3dbb189..a03952c 100644
--- a/pitivi/utils/ui.py
+++ b/pitivi/utils/ui.py
@@ -35,7 +35,6 @@ import urllib.error
from gettext import ngettext, gettext as _
-from gi.repository import Clutter
from gi.repository import GLib
from gi.repository import GES
from gi.repository import Gdk
@@ -47,21 +46,22 @@ from gi.repository.GstPbutils import DiscovererVideoInfo, DiscovererAudioInfo,\
from pitivi.utils.loggable import doLog, ERROR
from pitivi.utils.misc import path_from_uri
+from pitivi.configure import get_pixmap_dir
# Dimensions in pixels
TRACK_SPACING = 8
EXPANDED_SIZE = 65
-CONTROL_WIDTH = 250
+CONTROL_WIDTH = 300
PADDING = 6
SPACING = 10
PLAYHEAD_WIDTH = 1
+SNAPBAR_WIDTH = 5
CANVAS_SPACING = 21
KEYFRAME_SIZE = 8
-
-PLAYHEAD_COLOR = Clutter.Color.new(200, 0, 0, 255)
+LAYER_HEIGHT = 130
# Layer creation blocking time in s
LAYER_CREATION_BLOCK_TIME = 0.2
@@ -97,9 +97,72 @@ NORMAL_FONT = _get_font("font-name", "Cantarell")
DOCUMENT_FONT = _get_font("document-font-name", "Sans")
MONOSPACE_FONT = _get_font("monospace-font-name", "Monospace")
+TIMELINE_CSS = """
+ .AudioBackground {
+ background-color: #496c21;
+ }
+
+ .VideoBackground {
+ background-color: #2d2d2d;
+ }
+
+ .AudioBackground:selected {
+ background-color: #1b2e0e;
+ }
+
+ .VideoBackground:selected {
+ background-color: #0f0f0f;
+ }
+
+ .Trimbar {
+ background-image: url('%(trimbar_normal)s');
+ }
+
+ .Trimbar:first-child {
+ border-radius: 5px 0 0 5px;
+ }
+
+ .Trimbar:last-child {
+ border-radius: 0 5px 5px 0;
+ }
+
+ .Trimbar:hover {
+ background-image: url('%(trimbar_focused)s');
+ }
+
+ .PlayHead {
+ background-color: red;
+ }
+
+ .Clip {
+ /* TODO */
+ }
+
+ .SnapBar {
+ background-color: rgb(127, 153, 204);
+ }
+
+ .TransitionClip {
+ background-color: rgba(127, 153, 204, 0.5);
+ }
+
+ .TransitionClip:selected {
+ background-color: rgba(127, 200, 204, 0.7);
+ }
+
+ .SpacedSeparator:hover {
+ background-color: rgba(127, 153, 204, 0.5);
+ }
+
+ .Marquee {
+ background-color: rgba(224, 224, 224, 0.7);
+ }
+""" % ({'trimbar_normal': os.path.join(get_pixmap_dir(), "trimbar-normal.png"),
+ 'trimbar_focused': os.path.join(get_pixmap_dir(), "trimbar-focused.png")})
# ---------------------- ARGB color helper-------------------------------------#
+
def argb_to_gdk_rgba(color_int):
return Gdk.RGBA(color_int / 256 ** 2 % 256 / 255.,
color_int / 256 ** 1 % 256 / 255.,
@@ -164,9 +227,6 @@ def hex_to_rgb(value):
def set_cairo_color(context, color):
- if type(color) is Clutter.Color:
- color = (color.red, color.green, color.blue)
-
if type(color) is Gdk.RGBA:
cairo_color = (float(color.red), float(color.green), float(color.blue))
elif type(color) is tuple:
@@ -401,6 +461,22 @@ def alter_style_class(style_class, target_widget, css_style):
css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
+def set_children_state_recurse(widget, state):
+ widget.set_state_flags(state, False)
+ for child in widget.get_children():
+ child.set_state_flags(state, False)
+ if isinstance(child, Gtk.Container):
+ set_children_state_recurse(child, state)
+
+
+def unset_children_state_recurse(widget, state):
+ widget.unset_state_flags(state)
+ for child in widget.get_children():
+ child.unset_state_flags(state)
+ if isinstance(child, Gtk.Container):
+ unset_children_state_recurse(child, state)
+
+
# ----------------------- encoding datas --------------------------------------- #
# FIXME This should into a special file
frame_rates = model((str, object), (
diff --git a/pitivi/utils/validate.py b/pitivi/utils/validate.py
index 7b8575e..3583082 100644
--- a/pitivi/utils/validate.py
+++ b/pitivi/utils/validate.py
@@ -19,17 +19,146 @@
# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
# Boston, MA 02110-1301, USA.
import sys
+
+from gi.repository import Gst
from gi.repository import GES
+from gi.repository import Gdk
+from gi.repository import GLib
+
+from pitivi.utils import ui
+from pitivi.utils import timeline as timelineUtils
+
+try:
+ from gi.repository import GstValidate
+except ImportError:
+ GstValidate = None
has_validate = False
def stop(scenario, action):
- sys.stdout.write("STOP action, not doing anything in pitivi")
- sys.stdout.flush()
+ if action.structure.get_boolean("force")[0]:
+ timeline = scenario.pipeline.props.timeline
+ project = timeline.get_asset()
+
+ if project:
+ project.setModificationState(False)
+ GstValidate.print_action(action, "Force quiting, ignoring any"
+ " changes in the project")
+ timeline.ui.app.shutdown()
+
+ return 1
+
+ GstValidate.print_action(action, "not doing anything in pitivi")
+
+ return 1
+
+
+def editContainer(scenario, action):
+ # edit-container, edge=(string)edge_end, position=(double)2.2340325289999998,
edit-mode=(string)edit_trim, container-name=(string)uriclip0, new-layer-priority=(int)-1;
+ timeline = scenario.pipeline.props.timeline
+ container = timeline.get_element(action.structure["container-name"])
+
+ try:
+ res, edge = GstValidate.utils_enum_from_str(GES.Edge, action.structure["edge"])
+ if not res:
+ edge = GES.Edge.EDGE_NONE
+ else:
+ edge = GES.Edge(edge)
+ except KeyError:
+ edge = GES.Edge.EDGE_NONE
+
+ res, position = GstValidate.action_get_clocktime(scenario, action, "position")
+ layer_prio = action.structure["new-layer-priority"]
+
+ if res is False:
+ return 0
+
+ container_ui = container.ui
+
+ y = 21
+ if container.get_layer().get_priority() != layer_prio:
+ try:
+ layer = timeline.get_layers()[layer_prio]
+ y = layer.ui.get_allocation().y - container_ui.translate_coordinates(timeline.ui, 0, 0)[1]
+ if y < 0:
+ y += 21
+ else:
+ y -= 21
+ except IndexError:
+ if layer_prio == -1:
+ y = -5
+ else:
+ layer = timeline.get_layers()[-1]
+ alloc = layer.ui.get_allocation()
+ y = alloc.y + alloc.height + 10 - container_ui.translate_coordinates(timeline.ui, 0, 0)[1]
+
+ if not hasattr(scenario, "dragging") or scenario.dragging is False:
+ if isinstance(container, GES.SourceClip):
+ if edge == GES.Edge.EDGE_START:
+ container.ui.leftHandle._eventCb(Gdk.Event.new(Gdk.Event.ENTER_NOTIFY))
+ elif edge == GES.Edge.EDGE_END:
+ container.ui.leftHandle._eventCb(Gdk.Event.new(Gdk.Event.ENTER_NOTIFY))
+
+ scenario.dragging = True
+ event = Gdk.EventButton.new(Gdk.EventType.BUTTON_PRESS)
+ event.button = 1
+ event.y = y
+ container.ui.sendFakeEvent(event, container.ui)
+
+ event = Gdk.EventMotion.new(Gdk.EventType.MOTION_NOTIFY)
+ event.button = 1
+ event.x = timelineUtils.Zoomable.nsToPixelAccurate(position) -
container_ui.translate_coordinates(timeline.ui, 0, 0)[0] + ui.CONTROL_WIDTH
+ event.y = y
+ event.state = Gdk.ModifierType.BUTTON1_MASK
+ container.ui.sendFakeEvent(event, container.ui)
+
+ GstValidate.print_action(action, "Editing %s to %s in %s mode, edge: %s "
+ "with new layer prio: %d\n" % (action.structure["container-name"],
+ Gst.TIME_ARGS(position),
+ timeline.ui.draggingElement.edit_mode,
+ timeline.ui.draggingElement.dragging_edge,
+ layer_prio))
+
+ next_action = scenario.get_next_action()
+ if next_action is None or next_action.type != "edit-container":
+ scenario.dragging = False
+ event = Gdk.EventButton.new(Gdk.EventType.BUTTON_RELEASE)
+ event.button = 1
+ event.x = timelineUtils.Zoomable.nsToPixelAccurate(position)
+ event.y = y
+ container.ui.sendFakeEvent(event, container.ui)
+
+ if isinstance(container, GES.SourceClip):
+ if edge == GES.Edge.EDGE_START:
+ container.ui.leftHandle._eventCb(Gdk.Event.new(Gdk.Event.LEAVE_NOTIFY))
+ if edge == GES.Edge.EDGE_END:
+ container.ui.leftHandle._eventCb(Gdk.Event.new(Gdk.Event.LEAVE_NOTIFY))
+
+ if container.get_layer().get_priority() != layer_prio:
+ scenario.report_simple(GLib.quark_from_string("scenario::execution-error"),
+ "Resulting clip priority: %s"
+ " is not the same as the wanted one: %s"
+ % (container.get_layer().get_priority(),
+ layer_prio))
+
return 1
+def splitClip(scenario, action):
+ timeline = scenario.pipeline.props.timeline.ui
+ timeline.parent._splitCb(None)
+
+ return True
+
+
+def setZoomFit(scenario, action):
+ timeline = scenario.pipeline.props.timeline.ui
+ timeline.parent.zoomFit()
+
+ return True
+
+
def init():
global has_validate
try:
@@ -40,5 +169,19 @@ def init():
stop, None,
"Pitivi override for the stop action",
GstValidate.ActionTypeFlags.NONE)
+
+ GstValidate.register_action_type("edit-container", "pitivi",
+ editContainer, None,
+ "Start dragging a clip in the timeline",
+ GstValidate.ActionTypeFlags.NONE)
+
+ GstValidate.register_action_type("split-clip", "pitivi",
+ splitClip, None,
+ "Split a clip",
+ GstValidate.ActionTypeFlags.NONE)
+ GstValidate.register_action_type("set-zoom-fit", "pitivi",
+ setZoomFit, None,
+ "Split a clip",
+ GstValidate.ActionTypeFlags.NO_EXECUTION_NOT_FATAL)
except ImportError:
has_validate = False
diff --git a/pitivi/utils/widgets.py b/pitivi/utils/widgets.py
index c4ea608..645ebf8 100644
--- a/pitivi/utils/widgets.py
+++ b/pitivi/utils/widgets.py
@@ -233,7 +233,6 @@ class NumericWidget(Gtk.Box, DynamicWidget):
upper = GObject.G_MAXDOUBLE
if lower is None:
lower = GObject.G_MINDOUBLE
- range = upper - lower
self.adjustment.props.lower = lower
self.adjustment.props.upper = upper
self.spinner = Gtk.SpinButton(adjustment=self.adjustment)
@@ -1028,6 +1027,7 @@ class ZoomBox(Gtk.Grid, Zoomable):
Gtk.Grid.__init__(self)
Zoomable.__init__(self)
+ self._manual_set = False
self.timeline = timeline
zoom_fit_btn = Gtk.Button()
@@ -1073,7 +1073,9 @@ class ZoomBox(Gtk.Grid, Zoomable):
def _zoomAdjustmentChangedCb(self, adjustment):
Zoomable.setZoomLevel(adjustment.get_value())
- self.timeline._scrollToPlayhead()
+
+ if self._manual_set is False:
+ self.timeline.scrollToPlayhead()
def _zoomFitCb(self, unused_button):
self.timeline.zoomFit()
@@ -1096,7 +1098,9 @@ class ZoomBox(Gtk.Grid, Zoomable):
def zoomChanged(self):
zoomLevel = self.getCurrentZoomLevel()
if int(self._zoomAdjustment.get_value()) != zoomLevel:
+ self._manual_set = True
self._zoomAdjustment.set_value(zoomLevel)
+ self._manual_set = False
def _sliderTooltipCb(self, unused_slider, unused_x, unused_y, unused_keyboard_mode, tooltip):
# We assume the width of the ruler is exactly the width of the
diff --git a/pitivi/viewer.py b/pitivi/viewer.py
index 2f0a427..5fa035d 100644
--- a/pitivi/viewer.py
+++ b/pitivi/viewer.py
@@ -19,9 +19,7 @@
# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
# Boston, MA 02110-1301, USA.
-from gi.repository import Clutter
from gi.repository import Gtk
-from gi.repository import GtkClutter
from gi.repository import Gdk
from gi.repository import Gst
from gi.repository import GObject
@@ -415,22 +413,22 @@ class ViewerContainer(Gtk.Box, Loggable):
"""
self.timecode_entry.setWidgetValue(position, False)
- def clipTrimPreview(self, tl_obj, position):
+ def clipTrimPreview(self, clip, position):
"""
While a clip is being trimmed, show a live preview of it.
"""
- if isinstance(tl_obj, GES.TitleClip) or tl_obj.props.is_image or not hasattr(tl_obj, "get_uri"):
+ if isinstance(clip, GES.TitleClip) or clip.props.is_image or not hasattr(clip, "get_uri"):
self.log(
- "%s is an image or has no URI, so not previewing trim" % tl_obj)
+ "%s is an image or has no URI, so not previewing trim" % clip)
return False
- clip_uri = tl_obj.props.uri
+ clip_uri = clip.props.uri
cur_time = time()
if self.pipeline == self.app.project_manager.current_project.pipeline:
self.debug("Creating temporary pipeline for clip %s, position %s",
clip_uri, format_ns(position))
self._oldTimelinePos = self.pipeline.getPosition()
- self.setPipeline(AssetPipeline(tl_obj))
+ self.setPipeline(AssetPipeline(clip))
self._lastClipTrimTime = cur_time
if (cur_time - self._lastClipTrimTime) > 0.2 and self.pipeline.getState() == Gst.State.PAUSED:
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 9ecfb16..b2b2029 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -70,11 +70,11 @@ class TestDependencyChecks(TestCase):
gi_dep.check()
self.assertFalse(gi_dep.satisfied)
- gi_dep = GtkOrClutterDependency("Gtk", "3.0.0")
+ gi_dep = GtkDependency("Gtk", "3.0.0")
gi_dep.check()
self.assertTrue(gi_dep.satisfied)
- gi_dep = GtkOrClutterDependency("Gtk", "9.9.9")
+ gi_dep = GtkDependency("Gtk", "9.9.9")
gi_dep.check()
self.assertFalse(gi_dep.satisfied)
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]