[pitivi] Begins the integration of the new ClutterTimeline
- From: Jean-François Fortin Tam <jfft src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [pitivi] Begins the integration of the new ClutterTimeline
- Date: Wed, 24 Apr 2013 18:01:05 +0000 (UTC)
commit 2b8b71fb20c433105312afbdf8558fe921b76838
Author: Mathieu Duponchelle <mathieu duponchelle epitech eu>
Date: Mon Apr 15 04:48:28 2013 +0200
Begins the integration of the new ClutterTimeline
pitivi/mainwindow.py | 2 +-
pitivi/timeline/layer.py | 12 +-
pitivi/timeline/thumbnailer.py | 871 -------------
pitivi/timeline/timeline.py | 2751 ++++++++++++++++++++--------------------
pitivi/timeline/track.py | 858 -------------
pitivi/utils/timeline.py | 34 +
6 files changed, 1450 insertions(+), 3078 deletions(-)
---
diff --git a/pitivi/mainwindow.py b/pitivi/mainwindow.py
index febb05e..0e86690 100644
--- a/pitivi/mainwindow.py
+++ b/pitivi/mainwindow.py
@@ -967,7 +967,7 @@ class PitiviMainWindow(Gtk.Window, Loggable):
if project.pipeline is not None:
project.pipeline.deactivatePositionListener()
- self.timeline_ui.timeline = None
+ self.timeline_ui.bTimeline = None
self.clipconfig.timeline = None
return False
diff --git a/pitivi/timeline/layer.py b/pitivi/timeline/layer.py
index 965b4d1..b518998 100644
--- a/pitivi/timeline/layer.py
+++ b/pitivi/timeline/layer.py
@@ -86,7 +86,7 @@ class BaseLayerControl(Gtk.VBox, Loggable):
self.name_entry.connect("focus-in-event", self._focusChangeCb, False)
self.name_entry.connect("focus-out-event", self._focusChangeCb, True)
self.name_entry.connect("button_press_event", self._buttonPressCb)
- self.name_entry.drag_dest_unset()
+# self.name_entry.drag_dest_unset()
self.name_entry.set_sensitive(False)
# 'Solo' toggle button
@@ -153,9 +153,9 @@ class BaseLayerControl(Gtk.VBox, Loggable):
self.popup.show_all()
# Drag and drop
- self.drag_source_set(Gdk.ModifierType.BUTTON1_MASK,
- [LAYER_CONTROL_TARGET_ENTRY],
- Gdk.DragAction.MOVE)
+# self.drag_source_set(Gdk.ModifierType.BUTTON1_MASK,
+# [LAYER_CONTROL_TARGET_ENTRY],
+# Gdk.DragAction.MOVE)
def getSelected(self):
return self._selected
@@ -197,7 +197,7 @@ class BaseLayerControl(Gtk.VBox, Loggable):
"""
Look if user selected layer or wants popup menu
"""
- self._app.gui.timeline_ui.controls.selectLayerControl(self)
+ self._app.selectLayerControl(self)
if event.button == 3:
self.popup.popup(None, None, None, None, event.button, event.time)
@@ -357,7 +357,7 @@ class TwoStateButton(Gtk.Button):
def set_states(self, state1, state2):
self.states = {True: state1, False: state2}
- def _clickedCb(self, widget):
+ def _clickedCb(self, widget):
self._state = not self._state
self.set_label(self.states[self._state])
diff --git a/pitivi/timeline/timeline.py b/pitivi/timeline/timeline.py
index d64303a..c510979 100644
--- a/pitivi/timeline/timeline.py
+++ b/pitivi/timeline/timeline.py
@@ -1,69 +1,41 @@
-# PiTiVi , Non-linear video editor
-#
-# pitivi/timeline/timeline.py
-#
-# Copyright (c) 2005, Edward Hervey <bilboed bilboed com>
-# Copyright (c) 2009, Brandon Lewis <brandon_lewis berkeley edu>
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU Lesser General Public
-# License as published by the Free Software Foundation; either
-# version 2.1 of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-# Lesser General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public
-# License along with this program; if not, write to the
-# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
-# Boston, MA 02110-1301, USA.
-
-"""
- Main Timeline widgets
-"""
-
-import sys
-import time
-
-from gi.repository import Gtk
+from gi.repository import GtkClutter
+GtkClutter.init([])
from gi.repository import Gst
from gi.repository import GES
from gi.repository import GObject
-from gi.repository import GooCanvas
-from gi.repository import Gdk
+import collections
+import hashlib
+import os
+import sqlite3
+import sys
+import xdg.BaseDirectory as xdg_dirs
+
+from gi.repository import Clutter, GObject, Gtk, Cogl
+
from gi.repository import GLib
+from gi.repository import Gdk
+from gi.repository import GdkPixbuf
-from gettext import gettext as _
-from os.path import join
-
-from pitivi.timeline import ruler
-from pitivi.check import missing_soft_deps
-from pitivi.effects import AUDIO_EFFECT, VIDEO_EFFECT
-from pitivi.autoaligner import AlignmentProgressDialog
-from pitivi.utils.misc import quote_uri, print_ns
-from pitivi.utils.pipeline import PipelineError
-from pitivi.settings import GlobalSettings
+import pitivi.configure as configure
-from track import Track, TrackElement
-from layer import VideoLayerControl, AudioLayerControl
-from pitivi.utils.timeline import EditingContext, SELECT, Zoomable
+from pitivi.viewer import ViewerWidget
+
+from pitivi.utils.timeline import Zoomable, EditingContext, Selection, SELECT, UNSELECT, Selected
+
+from pitivi.settings import GlobalSettings
-from pitivi.dialogs.depsmanager import DepsManager
-from pitivi.dialogs.filelisterrordialog import FileListErrorDialog
from pitivi.dialogs.prefs import PreferencesDialog
-from pitivi.utils.receiver import receiver, handler
-from pitivi.utils.loggable import Loggable
-from pitivi.utils.ui import SPACING, CANVAS_SPACING, \
- TYPE_PITIVI_FILESOURCE, VIDEO_EFFECT_TARGET_ENTRY, Point, \
- AUDIO_EFFECT_TARGET_ENTRY, EFFECT_TARGET_ENTRY, FILESOURCE_TARGET_ENTRY, TYPE_PITIVI_EFFECT, \
- LAYER_CREATION_BLOCK_TIME, LAYER_CONTROL_TARGET_ENTRY
+from ruler import ScaleRuler
-# FIXME GES Port regression
-# from pitivi.utils.align import AutoAligner
+from datetime import datetime
+
+from gettext import gettext as _
+
+from pitivi.utils.pipeline import Pipeline
+
+from layer import VideoLayerControl, AudioLayerControl
GlobalSettings.addConfigOption('edgeSnapDeadband',
section="user-interface",
@@ -90,18 +62,13 @@ PreferencesDialog.addNumericPreference('imageClipLength',
description=_("Default clip length (in miliseconds) of images when inserting on the timeline."),
lower=1)
+# CONSTANTS
-# cursors to be used for resizing objects
-ARROW = Gdk.Cursor.new(Gdk.CursorType.ARROW)
-# TODO: replace this with custom cursor
-PLAYHEAD_CURSOR = Gdk.Cursor.new(Gdk.CursorType.SB_H_DOUBLE_ARROW)
+EXPANDED_SIZE = 65
+SPACING = 10
+CONTROL_WIDTH = 250
-# Drag and drop constants/tuples
-# FIXME, rethink the way we handle that as it is quite 'hacky'
-DND_EFFECT_LIST = [[VIDEO_EFFECT_TARGET_ENTRY.target, EFFECT_TARGET_ENTRY.target],
- [AUDIO_EFFECT_TARGET_ENTRY.target, EFFECT_TARGET_ENTRY.target]]
-VIDEO_EFFECT_LIST = [VIDEO_EFFECT_TARGET_ENTRY.target, EFFECT_TARGET_ENTRY.target],
-AUDIO_EFFECT_LIST = [AUDIO_EFFECT_TARGET_ENTRY.target, EFFECT_TARGET_ENTRY.target],
+BORDER_WIDTH = 3 # For the timeline elements
# tooltip text for toolbar
DELETE = _("Delete Selected")
@@ -166,890 +133,954 @@ ui = '''
'''
-class TimelineCanvas(GooCanvas.Canvas, Zoomable, Loggable):
+"""
+Convention throughout this file:
+Every GES element which name could be mistaken with a UI element
+is prefixed with a little b, example : bTimeline
+"""
+
+
+class RoundedRectangle(Clutter.Actor):
"""
- The GooCanvas widget representing the timeline
+ Custom actor used to draw a rectangle that can have rounded corners
"""
+ __gtype_name__ = 'RoundedRectangle'
- __gtype_name__ = 'TimelineCanvas'
+ def __init__(self, width, height, arc, step,
+ color=None, border_color=None, border_width=0):
+ """
+ Creates a new rounded rectangle
+ """
+ Clutter.Actor.__init__(self)
+ self.props.width = width
+ self.props.height = height
+ self._arc = arc
+ self._step = step
+ self._border_width = border_width
+ self._color = color
+ self._border_color = border_color
+
+ def do_paint(self):
+ # Set a rectangle for the clipping
+ Cogl.clip_push_rectangle(0, 0, self.props.width, self.props.height)
+
+ if self._border_color:
+ # draw the rectangle for the border which is the same size as the
+ # object
+ Cogl.path_round_rectangle(0, 0, self.props.width, self.props.height,
+ self._arc, self._step)
+ Cogl.path_round_rectangle(self._border_width, self._border_width,
+ self.props.width - self._border_width,
+ self.props.height - self._border_width,
+ self._arc, self._step)
+ Cogl.path_set_fill_rule(Cogl.PathFillRule.EVEN_ODD)
+ Cogl.path_close()
+
+ # set color to border color
+ Cogl.set_source_color(self._border_color)
+ Cogl.path_fill()
+
+ if self._color:
+ # draw the content with is the same size minus the width of the border
+ # finish the clip
+ Cogl.path_round_rectangle(self._border_width, self._border_width,
+ self.props.width - self._border_width,
+ self.props.height - self._border_width,
+ self._arc, self._step)
+ Cogl.path_close()
+
+ # set the color of the filled area
+ Cogl.set_source_color(self._color)
+ Cogl.path_fill()
+
+ Cogl.clip_pop()
+
+ def get_color(self):
+ return self._color
+
+ def set_color(self, color):
+ self._color = color
+ self.queue_redraw()
+
+ def get_border_width(self):
+ return self._border_width
+
+ def set_border_width(self, width):
+ self._border_width = width
+ self.queue_redraw()
+
+ def get_border_color(color):
+ return self._border_color
+
+ def set_border_color(self, color):
+ self._border_color = color
+ self.queue_redraw()
+
+
+class TrimHandle(Clutter.Texture):
+ def __init__(self, timelineElement, isLeft):
+ Clutter.Texture.__init__(self)
+ self.set_from_file(os.path.join(configure.get_pixmap_dir(), "trimbar-normal.png"))
+ self.isLeft = isLeft
+ self.set_size(-1, EXPANDED_SIZE)
+ self.hide()
- _tracks = None
+ self.isSelected = False
- def __init__(self, instance, timeline=None):
- GooCanvas.Canvas.__init__(self)
- Zoomable.__init__(self)
- Loggable.__init__(self)
- self.app = instance
- self._tracks = []
- self.height = CANVAS_SPACING
-
- self._block_size_request = False
- self.props.integer_layout = True
- self.props.automatic_bounds = False
- self.props.clear_background = False
- self.get_root_item().set_simple_transform(0, 2.0, 1.0, 0)
-
- self._createUI()
- self._timeline = timeline
- self.settings = instance.settings
- self.connect("draw", self.drawCb)
-
- def _createUI(self):
- self._cursor = ARROW
- root = self.get_root_item()
- self.tracks = GooCanvas.CanvasGroup()
- self.tracks.set_simple_transform(0, 0, 1.0, 0)
- root.add_child(self.tracks, -1)
- self._marquee = GooCanvas.CanvasRect(
- parent=root,
- stroke_color_rgba=0x33CCFF66,
- fill_color_rgba=0x33CCFF66,
- visibility=GooCanvas.CanvasItemVisibility.INVISIBLE)
- self._playhead = GooCanvas.CanvasRect(
- y=-10,
- parent=root,
- line_width=1,
- fill_color_rgba=0x000000FF,
- stroke_color_rgba=0xFFFFFFFF,
- width=3)
- self._snap_indicator = GooCanvas.CanvasRect(
- parent=root, x=0, y=0, width=3, line_width=0.5,
- fill_color_rgba=0x85c0e6FF,
- stroke_color_rgba=0x294f95FF)
- self.connect("size-allocate", self._size_allocate_cb)
- root.connect("motion-notify-event", self._selectionDrag)
- root.connect("button-press-event", self._selectionStart)
- root.connect("button-release-event", self._selectionEnd)
- self.connect("button-release-event", self._buttonReleasedCb)
- # add some padding for the horizontal scrollbar
- self.set_size_request(-1, self.height)
-
- def from_event(self, event):
- x, y = event.x, event.y
- x += self.app.gui.timeline_ui.hadj.get_value()
- return Point(*self.convert_from_pixels(x, y))
-
- def setExpanded(self, track_element, expanded):
- track_ui = None
- for track in self._tracks:
- if track.track == track_element:
- track_ui = track
- break
+ self.timelineElement = timelineElement
- track_ui.setExpanded(expanded)
+ self.set_reactive(True)
-## sets the cursor as appropriate
+ self.dragAction = Clutter.DragAction()
+ self.add_action(self.dragAction)
- def _mouseEnterCb(self, unused_item, unused_target, event):
- event.window.set_cursor(self._cursor)
- return True
+ self.dragAction.connect("drag-begin", self._dragBeginCb)
+ self.dragAction.connect("drag-end", self._dragEndCb)
+ self.dragAction.connect("drag-progress", self._dragProgressCb)
- def drawCb(self, widget, cr):
- allocation = self.get_allocation()
- width = allocation.width
- height = allocation.height
- # draw the canvas background
- # we must have props.clear_background set to False
-
- # FIXME GObject Introspection -> move to Gtk.StyleContext
- #self.style.apply_default_background(event.window,
- #True,
- #Gtk.StateType.ACTIVE,
- #event.area,
- #event.area.x, event.area.y,
- #event.area.width, event.area.height)
-
- #GooCanvas.Canvas.do_expose_event(self, event)
-
-## implements selection marquee
-
- _selecting = False
- _mousedown = None
- _marquee = None
- _got_motion_notify = False
-
- def getItemsInArea(self, x1, y1, x2, y2):
- '''
- Permits to get the Non UI L{Track}/L{TrackElement} in a list of set
- corresponding to the L{Track}/L{TrackElement} which are in the are
-
- @param x1: The horizontal coordinate of the up left corner of the area
- @type x1: An C{int}
- @param y1: The vertical coordinate of the up left corner of the area
- @type y1: An C{int}
- @param x2: The horizontal coordinate of the down right area corner
- @type x2: An C{int}
- @param x2: The vertical coordinate of the down right corner of the area
- @type x2: An C{int}
-
- @returns: A list of L{Track}, L{TrackElement} tuples
- '''
- bounds = GooCanvas.CanvasBounds()
- bounds.x1 = x1
- bounds.x2 = x2
- bounds.y1 = y1
- bounds.y2 = y2
- items = self.get_items_in_area(bounds, True, True, True)
- if not items:
- return [], []
-
- tracks = set()
- track_elements = set()
-
- for item in items:
- if isinstance(item, Track):
- tracks.add(item.track)
- elif isinstance(item, TrackElement):
- track_elements.add(item.element)
-
- return tracks, track_elements
-
- def _normalize(self, p1, p2):
- w, h = p1 - p2
- w = abs(w)
- h = abs(h)
- x = min(p1[0], p2[0])
- y = min(p1[1], p2[1])
- return (x, y), (w, h)
-
- def _get_adjustment(self, xadj=True, yadj=True):
- return Point(self.app.gui.timeline_ui.hadj.get_value() * xadj,
- self.app.gui.timeline_ui.vadj.get_value() * yadj)
-
- def _selectionDrag(self, item, target, event):
- if self._selecting:
- self._got_motion_notify = True
- cur = self.from_event(event) - self._get_adjustment(True, False)
- pos, size = self._normalize(self._mousedown, cur)
- self._marquee.props.x, self._marquee.props.y = pos
- self._marquee.props.width, self._marquee.props.height = size
- return True
- return False
+ self.connect("enter-event", self._enterEventCb)
+ self.connect("leave-event", self._leaveEventCb)
+ self.timelineElement.connect("enter-event", self._elementEnterEventCb)
+ self.timelineElement.connect("leave-event", self._elementLeaveEventCb)
+ self.timelineElement.bElement.selected.connect("selected-changed", self._selectedChangedCb)
- def _selectionStart(self, item, target, event):
- self._selecting = True
- self._marquee.props.visibility = GooCanvas.CanvasItemVisibility.VISIBLE
- self._mousedown = self.from_event(event) + self._get_adjustment(False, True)
- self._marquee.props.width = 0
- self._marquee.props.height = 0
- self.pointer_grab(self.get_root_item(), Gdk.EventMask.POINTER_MOTION_MASK |
- Gdk.EventMask.BUTTON_RELEASE_MASK, self._cursor, event.time)
- return True
+ #Callbacks
- def _selectionEnd(self, item, target, event):
- self.pointer_ungrab(self.get_root_item(), event.time)
- self._selecting = False
- self._marquee.props.visibility = GooCanvas.CanvasItemVisibility.INVISIBLE
- if not self._got_motion_notify:
- self._timeline.selection.setSelection([], 0)
- self.app.current.seeker.seek(Zoomable.pixelToNs(event.x))
- elif self._timeline is not None:
- self._got_motion_notify = False
- mode = 0
- if event.get_state()[1] & Gdk.ModifierType.SHIFT_MASK:
- mode = 1
- if event.get_state()[1] & Gdk.ModifierType.CONTROL_MASK:
- mode = 2
- selected = self._elementsUnderMarquee()
- self._timeline.selection.setSelection(self._elementsUnderMarquee(), mode)
- return True
+ def _enterEventCb(self, actor, event):
+ self.timelineElement.set_reactive(False)
+ for elem in self.timelineElement.get_children():
+ elem.set_reactive(False)
+ self.set_reactive(True)
+ self.set_from_file(os.path.join(configure.get_pixmap_dir(), "trimbar-focused.png"))
+ if self.isLeft:
+
self.timelineElement.timeline._container.embed.get_window().set_cursor(Gdk.Cursor.new(Gdk.CursorType.LEFT_SIDE))
+ else:
+
self.timelineElement.timeline._container.embed.get_window().set_cursor(Gdk.Cursor.new(Gdk.CursorType.RIGHT_SIDE))
- def _elementsUnderMarquee(self):
- items = self.get_items_in_area(self._marquee.get_bounds(), True, True, True)
- if items:
- return set((item.element for item in items if isinstance(item,
- TrackElement) and item.bg in items))
- return set()
+ def _leaveEventCb(self, actor, event):
+ self.timelineElement.set_reactive(True)
+ for elem in self.timelineElement.get_children():
+ elem.set_reactive(True)
+ self.set_from_file(os.path.join(configure.get_pixmap_dir(), "trimbar-normal.png"))
+
self.timelineElement.timeline._container.embed.get_window().set_cursor(Gdk.Cursor.new(Gdk.CursorType.ARROW))
-## playhead implementation
+ def _elementEnterEventCb(self, actor, event):
+ self.show()
- position = 0
+ def _elementLeaveEventCb(self, actor, event):
+ if not self.isSelected:
+ self.hide()
- def timelinePositionChanged(self, position):
- self.position = position
- self._playhead.props.x = self.nsToPixel(position)
+ def _selectedChangedCb(self, selected, isSelected):
+ self.isSelected = isSelected
+ self.props.visible = isSelected
- max_duration = 0
+ def _dragBeginCb(self, action, actor, event_x, event_y, modifiers):
+ self.timelineElement.setDragged(True)
+ elem = self.timelineElement.bElement.get_parent()
- def setMaxDuration(self, duration):
- self.max_duration = duration
- self._request_size()
+ 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()
- def _request_size(self):
- alloc = self.get_allocation()
- self.set_bounds(0, 0, alloc.width, alloc.height)
- self._playhead.props.height = max(0, self.height + SPACING)
+ self._context = EditingContext(elem,
+ self.timelineElement.timeline.bTimeline,
+ GES.EditMode.EDIT_TRIM,
+ edge,
+ set([]),
+ None)
- def _size_allocate_cb(self, widget, allocation):
- self._request_size()
+ self.dragBeginStartX = event_x
+ self.dragBeginStartY = event_y
- def zoomChanged(self):
- self.queue_draw()
+ def _dragProgressCb(self, action, actor, delta_x, delta_y):
+ # We can't use delta_x here because it fluctuates weirdly.
+ coords = self.dragAction.get_motion_coords()
+ delta_x = coords[0] - self.dragBeginStartX
+
+ new_start = self._dragBeginStart + Zoomable.pixelToNs(delta_x)
-## snapping indicator
- def _snapCb(self, unused_timeline, obj1, obj2, position):
+ self._context.editTo(new_start,
self.timelineElement.bElement.get_parent().get_layer().get_priority())
+ return False
+
+ def _dragEndCb(self, action, actor, event_x, event_y, modifiers):
+ self.timelineElement.setDragged(False)
+ self._context.finish()
+ self.timelineElement.set_reactive(True)
+ for elem in self.timelineElement.get_children():
+ elem.set_reactive(True)
+ self.set_from_file(os.path.join(configure.get_pixmap_dir(), "trimbar-normal.png"))
+
self.timelineElement.timeline._container.embed.get_window().set_cursor(Gdk.Cursor.new(Gdk.CursorType.ARROW))
+
+
+class TimelineElement(Clutter.Actor, Zoomable):
+ def __init__(self, bElement, track, timeline):
"""
- Display or hide a snapping indicator line
+ @param bElement : the backend GES.TrackElement
+ @param track : the track to which the bElement belongs
+ @param timeline : the containing graphic timeline.
"""
- if position == 0:
- self._snapEndedCb()
- else:
- self.debug("Snapping indicator at %d" % position)
- self._snap_indicator.props.x = Zoomable.nsToPixel(position)
- self._snap_indicator.props.height = self.height
- self._snap_indicator.props.visibility = GooCanvas.CanvasItemVisibility.VISIBLE
+ Zoomable.__init__(self)
+ Clutter.Actor.__init__(self)
- def _snapEndedCb(self, *args):
- self._snap_indicator.props.visibility = GooCanvas.CanvasItemVisibility.INVISIBLE
+ self.timeline = timeline
- def _buttonReleasedCb(self, canvas, event):
- # select clicked layer, if any
- x, y = self.from_event(event) + self._get_adjustment(True, True)
- self.app.gui.timeline_ui.controls.selectLayerControlForY(y)
+ self.bElement = bElement
- # also hide snap indicator
- self._snapEndedCb()
+ self.bElement.selected = Selected()
+ self.bElement.selected.connect("selected-changed", self._selectedChangedCb)
-## settings callbacks
- def _setSettings(self):
- self.zoomChanged()
+ self._createBackground(track)
- settings = receiver(_setSettings)
+ self._createPreview()
- @handler(settings, "edgeSnapDeadbandChanged")
- def _edgeSnapDeadbandChangedCb(self, settings):
- self.zoomChanged()
+ self._createBorder()
-## Timeline callbacks
+ self._createMarquee()
- def setTimeline(self, timeline):
- while self._tracks:
- self._trackRemovedCb(None, 0)
+ self._createHandles()
- if self._timeline is not None:
- self._timeline.disconnect_by_func(self._trackAddedCb)
- self._timeline.disconnect_by_func(self._trackRemovedCb)
- self._timeline.disconnect_by_func(self._snapCb)
- self._timeline.disconnect_by_func(self._snapEndedCb)
+ self._createGhostclip()
- self._timeline = timeline
- if self._timeline is not None:
- for track in self._timeline.get_tracks():
- self._trackAddedCb(None, track)
+ self.track_type = self.bElement.get_track_type() # This won't change
- self._timeline.connect("track-added", self._trackAddedCb)
- self._timeline.connect("track-removed", self._trackRemovedCb)
- self._timeline.connect("snapping-started", self._snapCb)
- self._timeline.connect("snapping-ended", self._snapEndedCb)
+ self.isDragged = False
- self.zoomChanged()
+ size = self.bElement.get_duration()
+ self.set_size(self.nsToPixel(size), EXPANDED_SIZE, False)
+ self.set_reactive(True)
+ self._connectToEvents()
- def getTimeline(self):
- return self._timeline
+ # Public API
- timeline = property(getTimeline, setTimeline, None, "The timeline property")
+ 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)
+ self.rightHandle.save_easing_state()
+ self.rightHandle.set_easing_duration(600)
- def _trackAddedCb(self, timeline, track):
- track = Track(self.app, track, self._timeline)
- self._tracks.append(track)
- track.set_canvas(self)
- self.tracks.add_child(track, -1)
- self.regroupTracks()
-
- def _trackRemovedCb(self, unused_timeline, position):
- track = self._tracks[position]
- del self._tracks[position]
- track.remove()
- self.regroupTracks()
-
- def regroupTracks(self):
- """
- Make it so we have a real differentiation between the Audio tracks
- and video tracks
- This method should be called each time a change happen in the timeline
- """
- height = 0
- for i, track in enumerate(self._tracks):
- track.set_simple_transform(0, height, 1, 0)
- height += track.height
- self.height = height
- self._request_size()
+ self.marquee.set_size(width, height)
+ self.background.props.width = width
+ self.background.props.height = height
+ self.border.props.width = width
+ self.border.props.height = height
+ self.props.width = width
+ self.props.height = height
+ self.preview.set_size(width, height)
+ self.rightHandle.set_position(width - self.rightHandle.props.width, 0)
- def updateTracks(self):
- self.debug("Updating all TrackElements")
- for track in self._tracks:
- track.updateTrackElements()
+ if ease:
+ self.background.restore_easing_state()
+ self.border.restore_easing_state()
+ self.preview.restore_easing_state()
+ self.rightHandle.restore_easing_state()
+ self.restore_easing_state()
+ def updateGhostclip(self, priority, y, isControlledByBrother):
+ # Only tricky part of the code, can be called by the linked track element.
+ if priority < 0:
+ return
-class TimelineControls(Gtk.VBox, Loggable):
- """
- Holds and manages the LayerControlWidgets
- """
+ # 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)
- __gsignals__ = {
- "selection-changed": (GObject.SignalFlags.RUN_LAST, None, (GObject.TYPE_PYOBJECT,),)
- }
+ # And here we take into account the fact that the pointer might actually be
+ # on the other track element, meaning we have to offset it.
+ if isControlledByBrother:
+ if self.track_type == GES.TrackType.AUDIO:
+ y += self.nbrLayers * (EXPANDED_SIZE + SPACING)
+ else:
+ y -= self.nbrLayers * (EXPANDED_SIZE + SPACING)
+
+ # Would that be a new layer ?
+ if priority == self.nbrLayers:
+ self.ghostclip.set_size(self.props.width, SPACING)
+ self.ghostclip.props.y = priority * (EXPANDED_SIZE + SPACING)
+ if self.track_type == GES.TrackType.AUDIO:
+ self.ghostclip.props.y += self.nbrLayers * (EXPANDED_SIZE + SPACING)
+ self.ghostclip.props.visible = True
+ else:
+ # No need to mockup on the same layer
+ if priority == self.bElement.get_parent().get_layer().get_priority():
+ self.ghostclip.props.visible = False
+ # We would be moving to an existing layer.
+ elif priority < self.nbrLayers:
+ self.ghostclip.set_size(self.props.width, EXPANDED_SIZE)
+ self.ghostclip.props.y = priority * (EXPANDED_SIZE + SPACING) + SPACING
+ if self.track_type == GES.TrackType.AUDIO:
+ self.ghostclip.props.y += self.nbrLayers * (EXPANDED_SIZE + SPACING)
+ self.ghostclip.props.visible = True
+
+ def update(self, ease):
+ size = self.bElement.get_duration()
+ self.set_size(self.nsToPixel(size), EXPANDED_SIZE, ease)
+
+ def setDragged(self, dragged):
+ brother = self.timeline.findBrother(self.bElement)
+ if brother:
+ brother.isDragged = dragged
+ self.isDragged = dragged
+
+ # Internal API
+
+ def _createGhostclip(self):
+ self.ghostclip = Clutter.Actor.new()
+ self.ghostclip.set_background_color(Clutter.Color.new(100, 100, 100, 50))
+ self.ghostclip.props.visible = False
+ self.timeline.add_child(self.ghostclip)
+
+ def _createBorder(self):
+ self.border = RoundedRectangle(0, 0, 5, 5)
+ color = Cogl.Color()
+ color.init_from_4ub(100, 100, 100, 255)
+ self.border.set_border_color(color)
+ self.border.set_border_width(3)
+
+ self.border.set_position(0, 0)
+ self.add_child(self.border)
+
+ def _createBackground(self, track):
+ self.background = RoundedRectangle(0, 0, 5, 5)
+ if track.type == GES.TrackType.AUDIO:
+ color = Cogl.Color()
+ color.init_from_4ub(70, 79, 118, 255)
+ else:
+ color = Cogl.Color()
+ color.init_from_4ub(225, 232, 238, 255)
+ self.background.set_color(color)
+ self.background.set_border_width(3)
+
+ self.background.set_position(0, 0)
+ self.add_child(self.background)
+
+ def _createHandles(self):
+ self.leftHandle = TrimHandle(self, True)
+ self.rightHandle = TrimHandle(self, False)
+ self.add_child(self.leftHandle)
+ self.add_child(self.rightHandle)
+ self.leftHandle.set_position(0, 0)
+
+ def _createPreview(self):
+ self.preview = get_preview_for_object(self.bElement)
+ self.add_child(self.preview)
+
+ def _createMarquee(self):
+ # TODO: difference between Actor.new() and Actor()?
+ self.marquee = Clutter.Actor()
+ self.marquee.set_background_color(Clutter.Color.new(60, 60, 60, 100))
+ self.add_child(self.marquee)
+ self.marquee.props.visible = False
+
+ def _connectToEvents(self):
+ # Click
+ # We gotta go low-level cause Clutter.ClickAction["clicked"]
+ # gets emitted after Clutter.DragAction["drag-begin"]
+ self.connect("button-press-event", self._clickedCb)
+
+ # Drag and drop.
+ action = Clutter.DragAction()
+ self.add_action(action)
+ action.connect("drag-progress", self._dragProgressCb)
+ action.connect("drag-begin", self._dragBeginCb)
+ action.connect("drag-end", self._dragEndCb)
+ self.dragAction = action
+
+ def _getLayerForY(self, y):
+ if self.bElement.get_track_type() == GES.TrackType.AUDIO:
+ y -= self.nbrLayers * (EXPANDED_SIZE + SPACING)
+ priority = int(y / (EXPANDED_SIZE + SPACING))
+ return priority
- def __init__(self, instance):
- Gtk.VBox.__init__(self)
- Loggable.__init__(self)
- self.app = instance
- self._layer_controls = {}
- self._selected_layer = None
- self._timeline = None
- self.set_spacing(0)
- self.separator_height = 0
- self.type_map = {GES.TrackType.AUDIO: AudioLayerControl,
- GES.TrackType.VIDEO: VideoLayerControl}
- self.connect("size-allocate", self._sizeAllocatedCb)
- self.priority_block = sys.maxint
- self.priority_block_time = time.time()
+ # Interface (Zoomable)
- # drag'n' drop
- self.connect("drag_drop", self._dragDropCb)
- self.connect("drag_motion", self._dragMotionCb)
- self.connect("drag_leave", self._dragLeaveCb)
- self.drag_dest_set(Gtk.DestDefaults.ALL,
- [LAYER_CONTROL_TARGET_ENTRY], Gdk.DragAction.MOVE)
+ def zoomChanged(self):
+ self.update(True)
- def _sizeAllocatedCb(self, widget, alloc):
- if self.get_children():
- self.separator_height = self.get_children()[0].getSeparatorHeight()
- self.app.gui.timeline_ui._canvas.updateTracks()
+ # Callbacks
-## Timeline callbacks
+ def _clickedCb(self, action, actor):
+ #TODO : Let's be more specific, masks etc ..
+ self.timeline.selection.setToObj(self.bElement, SELECT)
- def getTimeline(self):
- return self._timeline
+ def _dragBeginCb(self, action, actor, event_x, event_y, modifiers):
+ self._context = EditingContext(self.bElement, self.timeline.bTimeline, GES.EditMode.EDIT_NORMAL,
GES.Edge.EDGE_NONE, self.timeline.selection.getSelectedTrackElements(), None)
- def setTimeline(self, timeline):
- self.debug("Setting timeline %s", timeline)
+ # This can't change during a drag, so we can safely compute it now for drag events.
+ self.nbrLayers = len(self.timeline.bTimeline.get_layers())
+ # We can also safely find if the object has a brother element
+ self.setDragged(True)
+ self.brother = self.timeline.findBrother(self.bElement)
+ if self.brother:
+ self.brother.nbrLayers = self.nbrLayers
- # remove old layer controls
- for layer in self._layer_controls.copy():
- self._layerRemovedCb(None, layer)
+ self._dragBeginStart = self.bElement.get_start()
+ self.dragBeginStartX = event_x
+ self.dragBeginStartY = event_y
- if timeline:
- for layer in timeline.get_layers():
- self._layerAddedCb(None, layer)
+ def _dragProgressCb(self, action, actor, delta_x, delta_y):
+ # We can't use delta_x here because it fluctuates weirdly.
+ coords = self.dragAction.get_motion_coords()
+ delta_x = coords[0] - self.dragBeginStartX
+ delta_y = coords[1] - self.dragBeginStartY
- timeline.connect("layer-added", self._layerAddedCb)
- timeline.connect("layer-removed", self._layerRemovedCb)
- self.connect = True
+ y = coords[1] + self.timeline._container.point.y
- elif self._timeline:
- self._timeline.disconnect_by_func(self._layerAddedCb)
- self._timeline.disconnect_by_func(self._layerRemovedCb)
+ priority = self._getLayerForY(y)
- self._timeline = timeline
+ self.ghostclip.props.x = self.nsToPixel(self._dragBeginStart) + delta_x
+ self.updateGhostclip(priority, y, False)
+ if self.brother:
+ self.brother.ghostclip.props.x = self.nsToPixel(self._dragBeginStart) + delta_x
+ self.brother.updateGhostclip(priority, y, True)
- timeline = property(getTimeline, setTimeline, None, "The timeline property")
+ new_start = self._dragBeginStart + self.pixelToNs(delta_x)
- def _layerAddedCb(self, timeline, layer):
- self.debug("Layer %s added", layer)
- video_control = VideoLayerControl(self.app, layer)
- audio_control = AudioLayerControl(self.app, layer)
+ if not self.ghostclip.props.visible:
+ self._context.editTo(new_start, self.bElement.get_parent().get_layer().get_priority())
+ else:
+ self._context.editTo(self._dragBeginStart, self.bElement.get_parent().get_layer().get_priority())
+ return False
- map = {GES.TrackType.AUDIO: audio_control,
- GES.TrackType.VIDEO: video_control}
- self._layer_controls[layer] = map
+ 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)
- self.pack_start(video_control, False, False, 0)
- self.pack_start(audio_control, False, False, 0)
+ priority = self._getLayerForY(coords[1] + self.timeline._container.point.y)
+ priority = min(priority, len(self.timeline.bTimeline.get_layers()))
- audio_control.show()
- video_control.show()
+ self.setDragged(False)
- self._orderControls()
- self._hideLastSeparator()
- self._updatePopupMenus()
+ self.ghostclip.props.visible = False
+ if self.brother:
+ self.brother.ghostclip.props.visible = False
- def _layerRemovedCb(self, timeline, layer):
- audio_control = self._layer_controls[layer][GES.TrackType.AUDIO]
- video_control = self._layer_controls[layer][GES.TrackType.VIDEO]
+ priority = max(0, priority)
+ self._context.editTo(new_start, priority)
+ self._context.finish()
- self.remove(audio_control)
- self.remove(video_control)
+ def _selectedChangedCb(self, selected, isSelected):
+ self.marquee.props.visible = isSelected
- del self._layer_controls[layer]
- self._hideLastSeparator()
- self._updatePopupMenus()
- def _orderControls(self):
- # this works since every layer has audio and video
- middle = len(self.get_children()) / 2
- for child in self.get_children():
- if isinstance(child, VideoLayerControl):
- self.reorder_child(child, child.layer.get_priority())
- elif isinstance(child, AudioLayerControl):
- self.reorder_child(child, middle + child.layer.get_priority())
+class TimelineStage(Clutter.ScrollActor, Zoomable):
+ def __init__(self, container):
+ Clutter.ScrollActor.__init__(self)
+ Zoomable.__init__(self)
+ self.set_background_color(Clutter.Color.new(31, 30, 33, 255))
+ self.elements = []
+ self.selection = Selection()
+ self._createPlayhead()
+ self._container = container
+ self.lastPosition = 0
- def _hideLastSeparator(self):
- if self.get_children():
- for child in self.get_children():
- child.setSeparatorVisibility(True)
+ # Public API
- self.get_children()[-1].setSeparatorVisibility(False)
+ def setPipeline(self, pipeline):
+ pipeline.connect('position', self._positionCb)
- def _updatePopupMenus(self):
+ def setTimeline(self, bTimeline):
"""
- Update sensitivity of menus
-
- Should be called after _orderControls as it expects the controls
- in ordered state
+ @param bTimeline : the backend GES.Timeline which we interface.
+ Does all the necessary connections.
"""
- children = self.get_children()
+ self.bTimeline = bTimeline
- # handle no layer case
- if not children:
- return
+ self.bTimeline.connect("track-added", self._trackAddedCb)
- # handle one layer case
- if len(children) == 2:
- for child in children:
- child.updateMenuSensitivity(-2)
- return
+ for track in bTimeline.get_tracks():
+ self.connectTrack(track)
+ for layer in bTimeline.get_layers():
+ self._add_layer(layer)
+ self.bTimeline.connect("layer-added", self._layerAddedCb)
+ self.bTimeline.connect("layer-removed", self._layerRemovedCb)
+ self.zoomChanged()
- # all other cases
- last = None
- index = 0
- first = True
- for child in children:
- if type(child) == AudioLayerControl and first:
- index = 0
- last.updateMenuSensitivity(-1)
- first = False
-
- child.updateMenuSensitivity(index)
- index += 1
- last = child
-
- last.updateMenuSensitivity(-1)
-
- def getHeightOfLayer(self, track_type, layer):
- if track_type == GES.TrackType.VIDEO:
- return self._layer_controls[layer][GES.TrackType.VIDEO].getControlHeight()
- else:
- return self._layer_controls[layer][GES.TrackType.AUDIO].getControlHeight()
+ def connectTrack(self, track):
+ track.connect("track-element-added", self._trackElementAddedCb)
+ track.connect("track-element-removed", self._trackElementRemovedCb)
- def getYOfLayer(self, track_type, layer):
+ #Stage was clicked with nothing under the pointer
+ def emptySelection(self):
+ """
+ Empty the current selection.
+ """
+ self.selection.setSelection(self.selection.getSelectedTrackElements(), UNSELECT)
+
+ def findBrother(self, element):
+ father = element.get_parent()
+ for elem in self.elements:
+ if elem.bElement.get_parent() == father and elem.bElement != element:
+ return elem
+ return None
+
+ #Internal API
+
+ def _positionCb(self, pipeline, position):
+ self.playhead.props.x = self.nsToPixel(position)
+ self._container._scrollToPlayhead()
+ self.lastPosition = position
+
+ def _updatePlayHead(self):
+ height = len(self.bTimeline.get_layers()) * (EXPANDED_SIZE + SPACING) * 2
+ self.playhead.set_size(2, height)
+
+ def _createPlayhead(self):
+ self.playhead = Clutter.Actor()
+ self.playhead.set_background_color(Clutter.Color.new(200, 0, 0, 255))
+ self.playhead.set_size(0, 0)
+ self.playhead.set_position(0, 0)
+ self.add_child(self.playhead)
+ self.playhead.set_easing_duration(0)
+ self.playhead.set_z_position(1)
+
+ def _addTimelineElement(self, track, bElement):
+ element = TimelineElement(bElement, track, self)
+ element.set_z_position(-1)
+
+ 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.elements.append(element)
+
+ self._setElementY(element)
+
+ self.add_child(element)
+
+ self._setElementX(element)
+
+ def _removeTimelineElement(self, track, bElement):
+ bElement.disconnect_by_func("notify::start", self._elementStartChangedCb)
+ bElement.disconnect_by_func("notify::duration", self._elementDurationChangedCb)
+ bElement.disconnect_by_func("notify::in-point", self._elementInPointChangedCb)
+
+ def _setElementX(self, element, ease=True):
+ 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()
+
+ # Crack, change that when we have retractable layers
+ def _setElementY(self, element):
+ element.save_easing_state()
y = 0
- for child in self.get_children():
- if layer == child.layer and isinstance(child, self.type_map[track_type]):
- return y
+ bElement = element.bElement
+ track_type = bElement.get_track_type()
- y += child.getHeight()
- return 0
+ if (track_type == GES.TrackType.AUDIO):
+ y = len(self.bTimeline.get_layers()) * (EXPANDED_SIZE + SPACING)
- def getHeightOfTrack(self, track_type):
- y = 0
- for child in self.get_children():
- if isinstance(child, self.type_map[track_type]):
- y += child.getHeight()
+ y += bElement.get_parent().get_layer().get_priority() * (EXPANDED_SIZE + SPACING) + SPACING
- return y - self.separator_height
+ element.props.y = y
+ element.restore_easing_state()
- def getPriorityForY(self, y):
- priority = -1
- current = 0
+ def _redraw(self):
+ self.save_easing_state()
+ self.props.width = self.nsToPixel(self.bTimeline.get_duration()) + 250
+ for element in self.elements:
+ self._setElementX(element)
+ self.restore_easing_state()
+ self.playhead.props.x = self.nsToPixel(self.lastPosition)
- # increment priority for each control we pass
- for child in self.get_children():
- if y <= current:
- return self._limitPriority(priority)
+ # Interface overrides (Zoomable)
- current += child.getHeight()
- priority += 1
+ def zoomChanged(self):
+ self._redraw()
- # another check if priority has been incremented but not returned
- # because there were no more children
- if y <= current:
- return self._limitPriority(priority)
+ def _add_layer(self, layer):
+ for element in self.elements:
+ self._setElementY(element)
+ self.save_easing_state()
+ 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.controls.addLayerControl(layer)
+ self._updatePlayHead()
- return 0
+ # Callbacks
- def _limitPriority(self, calculated):
- priority = min(self._getLayerBlock(), calculated)
- self._setLayerBlock(priority)
- return priority
+ def _layerAddedCb(self, timeline, layer):
+ self._add_layer(layer)
+
+ def _layerRemovedCb(self, timeline, layer):
+ layer.disconnect_by_func(self._clipAddedCb)
+ layer.disconnect_by_func(self._clipRemovedCb)
+ self._updatePlayHead()
+
+ def _clipAddedCb(self, layer, clip):
+ clip.connect("child-added", self._elementAddedCb)
+ clip.connect("child-removed", self._elementRemovedCb)
+
+ def _clipRemovedCb(self, layer, clip):
+ clip.disconnect_by_func(self._elementAddedCb)
+ clip.disconnect_by_func(self._elementRemovedCb)
+
+ def _trackAddedCb(self, timeline, track):
+ self.connectTrack(track)
+
+ def _elementAddedCb(self, clip, bElement):
+ pass
+
+ def _elementRemovedCb(self):
+ pass
+
+ def _trackElementAddedCb(self, track, bElement):
+ print "trackelement added"
+ self._addTimelineElement(track, bElement)
- def _setLayerBlock(self, n):
- if self.priority_block != n:
- self.debug("Blocking UI layer creation")
- self.priority_block = n
- self.priority_block_time = time.time()
+ def _trackElementRemovedCb(self, track, bElement):
+ self._removeTimelineElement(track, bElement)
- def _getLayerBlock(self):
- if time.time() - self.priority_block_time >= LAYER_CREATION_BLOCK_TIME:
- return sys.maxint
+ def _elementPriorityChangedCb(self, bElement, priority, element):
+ self._setElementY(element)
+
+ def _elementStartChangedCb(self, bElement, start, element):
+ if element.isDragged:
+ self._setElementX(element, ease=False)
else:
- return self.priority_block
+ self._setElementX(element)
- def soloLayer(self, layer):
- """
- Enable this layer and disable all others
- """
- for key, controls in self._layer_controls.iteritems():
- controls[GES.TrackType.VIDEO].setSoloState(key == layer)
- controls[GES.TrackType.AUDIO].setSoloState(key == layer)
+ def _elementDurationChangedCb(self, bElement, duration, element):
+ element.update(False)
- def selectLayerControl(self, layer_control):
- """
- Select layer_control and unselect all other controls
- """
- layer = layer_control.layer
- # if selected layer changed
- if self._selected_layer != layer:
- self._selected_layer = layer
- self.emit("selection-changed", layer)
-
- for key, controls in self._layer_controls.iteritems():
- # selected widget not in this layer
- if key != layer:
- controls[GES.TrackType.VIDEO].selected = False
- controls[GES.TrackType.AUDIO].selected = False
- # selected widget in this layer
- else:
- if type(layer_control) is AudioLayerControl:
- controls[GES.TrackType.VIDEO].selected = False
- controls[GES.TrackType.AUDIO].selected = True
- else: # video
- controls[GES.TrackType.VIDEO].selected = True
- controls[GES.TrackType.AUDIO].selected = False
+ def _elementInPointChangedCb(self, bElement, inpoint, element):
+ self._setElementX(element, ease=False)
- def getSelectedLayer(self):
- return self._selected_layer
+ def _layerPriorityChangedCb(self, layer, priority):
+ self._redraw()
+
+
+def quit_(stage):
+ Gtk.main_quit()
+
+
+def quit2_(*args, **kwargs):
+ Gtk.main_quit()
- def selectLayerControlForY(self, y):
- """
- Check if y is in the bounds of a layer control
- """
- current_y = 0
- # count height
- for child in self.get_children():
- # calculate upper bound
- next_y = current_y + child.getControlHeight()
-
- # if y is in bounds, activate control and terminate
- if y >= current_y and y <= next_y:
- self.selectLayerControl(child)
- return
- # else check next control
- else:
- current_y += child.getHeight()
- def _dragDropCb(self, widget, context, x, y, time):
+class ZoomBox(Gtk.HBox, Zoomable):
+ def __init__(self, timeline):
"""
- Handles received drag data to reorder layers
+ This will hold the widgets responsible for zooming.
"""
- widget = Gtk.drag_get_source_widget(context)
- self._unhighlightSeparators()
+ Gtk.HBox.__init__(self)
+ Zoomable.__init__(self)
- current = self.getControlIndex(widget)
- index = self._getIndexForPosition(y, widget)
+ self.timeline = timeline
- # if current control is before desired index move one place less
- if current < index:
- index -= 1
+ zoom_fit_btn = Gtk.Button()
+ zoom_fit_btn.set_relief(Gtk.ReliefStyle.NONE)
+ zoom_fit_btn.set_tooltip_text(ZOOM_FIT)
+ zoom_fit_icon = Gtk.Image()
+ zoom_fit_icon.set_from_stock(Gtk.STOCK_ZOOM_FIT, Gtk.IconSize.BUTTON)
+ zoom_fit_btn_hbox = Gtk.HBox()
+ zoom_fit_btn_hbox.pack_start(zoom_fit_icon, False, True, 0)
+ zoom_fit_btn_hbox.pack_start(Gtk.Label(_("Zoom")), False, True, 0)
+ zoom_fit_btn.add(zoom_fit_btn_hbox)
+ zoom_fit_btn.connect("clicked", self._zoomFitCb)
+ self.pack_start(zoom_fit_btn, False, True, 0)
- self.moveControlWidget(widget, index)
+ # zooming slider
+ self._zoomAdjustment = Gtk.Adjustment()
+ self._zoomAdjustment.set_value(Zoomable.getCurrentZoomLevel())
+ self._zoomAdjustment.connect("value-changed", self._zoomAdjustmentChangedCb)
+ self._zoomAdjustment.props.lower = 0
+ self._zoomAdjustment.props.upper = Zoomable.zoom_steps
+ zoomslider = Gtk.Scale.new(Gtk.Orientation.HORIZONTAL, adjustment=self._zoomAdjustment)
+ zoomslider.props.draw_value = False
+ zoomslider.set_tooltip_text(_("Zoom Timeline"))
+ zoomslider.connect("scroll-event", self._zoomSliderScrollCb)
+ zoomslider.set_size_request(100, 0) # At least 100px wide for precision
+ self.pack_start(zoomslider, True, True, 0)
- def _dragLeaveCb(self, widget, context, timestamp):
- self._unhighlightSeparators()
+ self.show_all()
- def _dragMotionCb(self, widget, context, x, y, timestamp):
- """
- Highlight separator where control would go when dropping
- """
- index = self._getIndexForPosition(y, Gtk.drag_get_source_widget(context))
+ self._updateZoomSlider = True
- self._unhighlightSeparators()
+ def _zoomAdjustmentChangedCb(self, adjustment):
+ # GTK crack
+ self._updateZoomSlider = False
+ Zoomable.setZoomLevel(int(adjustment.get_value()))
+ self.zoomed_fitted = False
+ self._updateZoomSlider = True
- # control would go in first position
- if index == 0:
- pass
- else:
- self.get_children()[index - 1].setSeparatorHighlight(True)
+ def _zoomFitCb(self, button):
+ self.timeline.zoomFit()
- def _unhighlightSeparators(self):
- for child in self.get_children():
- child.setSeparatorHighlight(False)
+ def _zoomSliderScrollCb(self, unused, event):
+ value = self._zoomAdjustment.get_value()
+ if event.direction in [Gdk.ScrollDirection.UP, Gdk.ScrollDirection.RIGHT]:
+ self._zoomAdjustment.set_value(value + 1)
+ elif event.direction in [Gdk.ScrollDirection.DOWN, Gdk.ScrollDirection.LEFT]:
+ self._zoomAdjustment.set_value(value - 1)
- def _getIndexForPosition(self, y, widget):
- """
- Calculates the new index for a dragged layer
- """
- counter = 0
- index = 0
- last = None
+ def zoomChanged(self):
+ if self._updateZoomSlider:
+ self._zoomAdjustment.set_value(self.getCurrentZoomLevel())
- # find new index
- for child in self.get_children():
- next = counter + child.getControlHeight()
- # add height of last separator
- if last:
- next += last.getSeparatorHeight()
+class ControlActor(GtkClutter.Actor):
+ def __init__(self, container, widget, layer):
+ GtkClutter.Actor.__init__(self)
+ self.get_widget().add(widget)
+ self.set_reactive(True)
+ self.layer = layer
+ self._setUpDragAndDrop()
+ self._container = container
+ self.widget = widget
+
+ def _getLayerForY(self, y):
+ if self.isAudio:
+ y -= self.nbrLayers * (EXPANDED_SIZE + SPACING)
+ priority = int(y / (EXPANDED_SIZE + SPACING))
+ return priority
- # check if current interval matches y
- if y >= counter and y < next:
- return self._limitPositionIndex(index, widget)
+ def _setUpDragAndDrop(self):
+ self.dragAction = Clutter.DragAction()
+ self.add_action(self.dragAction)
+ self.dragAction.connect("drag-begin", self._dragBeginCb)
+ self.dragAction.connect("drag-progress", self._dragProgressCb)
+ self.dragAction.connect("drag-end", self._dragEndCb)
+
+ def _dragBeginCb(self, action, actor, event_x, event_y, modifiers):
+ self.brother = self._container.getBrotherControl(self)
+ self.brother.raise_top()
+ self.raise_top()
+ self.nbrLayers = len(self._container.timeline.bTimeline.get_layers())
+ self._dragBeginStartX = event_x
+
+ def _dragProgressCb(self, action, actor, delta_x, delta_y):
+ y = self.dragAction.get_motion_coords()[1]
+ priority = self._getLayerForY(y)
+ lowerLimit = 0
+ if self.isAudio:
+ lowerLimit = self.nbrLayers * (EXPANDED_SIZE + SPACING)
+
+ if actor.props.y + delta_y > lowerLimit and priority < self.nbrLayers:
+ actor.move_by(0, delta_y)
+ self.brother.move_by(0, delta_y)
+
+ if self.layer.get_priority() != priority and priority >= 0 and priority < self.nbrLayers:
+ self._container.moveLayer(self, priority)
+ return False
- # setup next iteration
- counter = next
- index += 1
- last = child
+ def _dragEndCb(self, action, actor, event_x, event_y, modifiers):
+ priority = self._getLayerForY(event_y)
+ if self.layer.get_priority() != priority and priority >= 0 and priority < self.nbrLayers:
+ self._container.moveLayer(self, priority)
+ self._container._reorderLayerActors()
- # return a limited index
- return self._limitPositionIndex(index, widget)
- def _limitPositionIndex(self, index, widget):
- """
- Limit the index depending on the type of widget
- """
- limit = len(self.get_children()) / 2
- if type(widget) == AudioLayerControl:
- return max(index, limit)
+class ControlContainer(Clutter.ScrollActor):
+ __gsignals__ = {
+ "selection-changed": (GObject.SignalFlags.RUN_LAST, None, (GObject.TYPE_PYOBJECT,),)
+ }
+
+ def __init__(self, timeline):
+ Clutter.ScrollActor.__init__(self)
+ self.controlActors = []
+ self.trackControls = []
+ self.timeline = timeline
+
+ def _setTrackControlPosition(self, control):
+ y = control.layer.get_priority() * (EXPANDED_SIZE + SPACING) + SPACING
+ if control.isAudio:
+ y += len(self.timeline.bTimeline.get_layers()) * (EXPANDED_SIZE + SPACING)
+ control.set_position(0, y)
+
+ def _reorderLayerActors(self):
+ for control in self.controlActors:
+ control.save_easing_state()
+ control.set_easing_mode(Clutter.AnimationMode.EASE_OUT_BACK)
+ self._setTrackControlPosition(control)
+ control.restore_easing_state()
+
+ def getBrotherControl(self, control):
+ for cont in self.controlActors:
+ if cont != control and cont.layer == control.layer:
+ return cont
+
+ def moveLayer(self, control, target):
+ movedLayer = control.layer
+ priority = movedLayer.get_priority()
+ self.timeline.bTimeline.enable_update(False)
+ movedLayer.props.priority = 999
+
+ if priority > target:
+ for layer in self.timeline.bTimeline.get_layers():
+ prio = layer.get_priority()
+ if target <= prio < priority:
+ layer.props.priority = prio + 1
+ elif priority < target:
+ for layer in self.timeline.bTimeline.get_layers():
+ prio = layer.get_priority()
+ if priority < prio <= target:
+ layer.props.priority = prio - 1
+ movedLayer.props.priority = target
+
+ self._reorderLayerActors()
+ self.timeline.bTimeline.enable_update(True)
+
+ def addTrackControl(self, layer, isAudio):
+ if isAudio:
+ control = AudioLayerControl(self, layer)
else:
- return min(index, limit)
+ control = VideoLayerControl(self, layer)
- def moveControlWidget(self, control, index):
- """
- Moves control to the given index and cares for moving the linked layer
- as well as updating separators
- """
- self.reorder_child(control, index)
-
- # reorder linked audio/video layer
- widget_type = type(control)
- index = 0
- for child in self.get_children():
- # only set layer priority once
- if type(child) == widget_type:
- child.layer.set_priority(index)
- index += 1
-
- # order controls and update separators
- self._orderControls()
- self._hideLastSeparator()
- self._updatePopupMenus()
-
- def getControlIndex(self, control):
- """
- Returns an unique ID of a control
+ controlActor = ControlActor(self, control, layer)
+ controlActor.isAudio = isAudio
+ controlActor.layer = layer
+ controlActor.set_size(CONTROL_WIDTH, EXPANDED_SIZE + SPACING)
- Used for drag and drop
- """
- counter = 0
- for child in self.get_children():
- if child == control:
- return counter
+ self.add_child(controlActor)
+ self.trackControls.append(control)
+ self.controlActors.append(controlActor)
- counter += 1
+ def selectLayerControl(self, layer_control):
+ for control in self.trackControls:
+ control.selected = False
+ layer_control.selected = True
+ self.props.height += (EXPANDED_SIZE + SPACING) * 2 + SPACING
- def getControlFromId(self, id):
- """
- Returns the control for an ID
+ def addLayerControl(self, layer):
+ self.addTrackControl(layer, False)
+ self.addTrackControl(layer, True)
+ self._reorderLayerActors()
- Used for drag and drop
- """
- counter = 0
- for child in self.get_children():
- if counter == id:
- return child
- counter += 1
+class Timeline(Gtk.VBox, Zoomable):
+ def __init__(self, instance, ui_manager):
+ gtksettings = Gtk.Settings.get_default()
+ gtksettings.set_property("gtk-application-prefer-dark-theme", True)
+ Zoomable.__init__(self)
+ Gtk.VBox.__init__(self)
+ GObject.threads_init()
+ self.ui_manager = ui_manager
+ self.app = instance
+ self._settings = self.app.settings
-class InfoStub(Gtk.HBox, Loggable):
- """
- Box used to display information on the current state of the timeline
- """
+ self.embed = GtkClutter.Embed()
+ self.embed.show()
- def __init__(self):
- Gtk.HBox.__init__(self)
- Loggable.__init__(self)
- self.errors = []
- self._scroll_pos_ns = 0
- self._errorsmessage = _("One or more GStreamer errors occured!")
- self._makeUI()
+ self.point = Clutter.Point()
+ self.point.x = 0
+ self.point.y = 0
- def _makeUI(self):
- self.set_spacing(SPACING)
- self.erroricon = Gtk.Image.new_from_stock(Gtk.STOCK_DIALOG_WARNING,
- Gtk.IconSize.SMALL_TOOLBAR)
+ self.zoomBox = ZoomBox(self)
+# self.pack_end(self.zoomBox, False, False, False)
- self.pack_start(self.erroricon, False, True, 0)
+ self._packScrollbars(self)
- self.infolabel = Gtk.Label(label=self._errorsmessage)
- self.infolabel.set_alignment(0, 0.5)
+# self.viewer = ViewerWidget()
- self.questionbutton = Gtk.Button()
- self.infoicon = Gtk.Image()
- self.infoicon.set_from_stock(Gtk.STOCK_INFO, Gtk.IconSize.SMALL_TOOLBAR)
- self.questionbutton.add(self.infoicon)
- self.questionbutton.connect("clicked", self._questionButtonClickedCb)
+# self.pack_end(self.viewer, False, False, False)
- self.pack_start(self.infolabel, True, True, 0)
- self.pack_start(self.questionbutton, False, True, 0)
+# self.viewer.set_size_request(200, 200)
- def addErrors(self, *args):
- self.errors.append(args)
- self.show()
+ stage = self.embed.get_stage()
+ stage.set_background_color(Clutter.Color.new(31, 30, 33, 255))
- def _errorDialogBoxCloseCb(self, dialog):
- dialog.destroy()
-
- def _errorDialogBoxResponseCb(self, dialog, unused_response):
- dialog.destroy()
-
- def _questionButtonClickedCb(self, unused_button):
- msgs = (_("Error List"),
- _("The following errors have been reported:"))
- # show error dialog
- dbox = FileListErrorDialog(*msgs)
- dbox.connect("close", self._errorDialogBoxCloseCb)
- dbox.connect("response", self._errorDialogBoxResponseCb)
- for reason, extra in self.errors:
- dbox.addFailedFile(None, reason, extra)
- dbox.show()
- # reset error list
- self.errors = []
- self.hide()
+ self.stage = stage
- def show(self):
- self.log("showing")
- self.show_all()
+ self.embed.connect("scroll-event", self._scrollEventCb)
+ self.stage.set_throttle_motion_events(True)
-class Timeline(Gtk.Table, Loggable, Zoomable):
- """
- Initiate and manage the timeline's user interface components.
+ stage.show()
- This class is not to be confused with project.py's
- "timeline" instance of GESTimeline.
- """
+ widget = TimelineStage(self)
- def __init__(self, instance, ui_manager):
- Gtk.Table.__init__(self, rows=2, columns=1, homogeneous=False)
- Loggable.__init__(self)
- Zoomable.__init__(self)
- self.log("Creating Timeline")
+ self.controls = ControlContainer(widget)
+ stage.add_child(self.controls)
+ self.controls.set_position(0, 0)
+ self.controls.set_z_position(2)
- self._updateZoomSlider = True
- self.ui_manager = ui_manager
- self.app = instance
- self._temp_elements = []
- self._drag_started = False
- self._factories = None
- self._finish_drag = False
- self._createUI()
- self._framerate = Gst.Fraction(1, 1)
- self._timeline = None
+ stage.add_child(widget)
+ widget.set_position(CONTROL_WIDTH, 0)
+ stage.connect("destroy", quit_)
+ stage.connect("button-press-event", self._clickedCb)
+ self.timeline = widget
- self.zoomed_fitted = True
+ print self.timeline
- # Timeline edition related fields
- self._move_context = None
+ self.scrolled = 0
+
+ self._createActions()
- self._project = None
self._projectmanager = None
+ self._project = None
- # The IDs of the various gobject signals we connect to
- self._signal_ids = []
+ self.show_all()
- self._settings = self.app.settings
- self._settings.connect("edgeSnapDeadbandChanged",
- self._snapDistanceChangedCb)
+# self.ruler.hide()
- def _createUI(self):
- self.leftSizeGroup = Gtk.SizeGroup(Gtk.SizeGroupMode.HORIZONTAL)
- self.props.row_spacing = 2
- self.props.column_spacing = 2
- self.hadj = Gtk.Adjustment()
- self.vadj = Gtk.Adjustment()
+ def insertEnd(self, assets):
+ """
+ Add source at the end of the timeline
+ @type sources: An L{GES.TimelineSource}
+ @param x2: A list of sources to add to the timeline
+ """
+ self.app.action_log.begin("add clip")
+ # FIXME we should find the longets layer instead of adding it to the
+ # first one
+ # Handle the case of a blank project
+ layer = self._ensureLayer()[0]
+ self.bTimeline.enable_update(False)
+ for asset in assets:
+ if asset.is_image():
+ clip_duration = long(long(self._settings.imageClipLength) * Gst.SECOND / 1000)
+ else:
+ clip_duration = asset.get_duration()
- # zooming slider's "zoom fit" button
- zoom_controls_hbox = Gtk.HBox()
- zoom_fit_btn = Gtk.Button()
- zoom_fit_btn.set_relief(Gtk.ReliefStyle.NONE)
- zoom_fit_btn.set_tooltip_text(ZOOM_FIT)
- zoom_fit_icon = Gtk.Image()
- zoom_fit_icon.set_from_stock(Gtk.STOCK_ZOOM_FIT, Gtk.IconSize.BUTTON)
- zoom_fit_btn_hbox = Gtk.HBox()
- zoom_fit_btn_hbox.pack_start(zoom_fit_icon, False, True, 0)
- zoom_fit_btn_hbox.pack_start(Gtk.Label(_("Zoom")), False, True, 0)
- zoom_fit_btn.add(zoom_fit_btn_hbox)
- zoom_fit_btn.connect("clicked", self._zoomFitCb)
- zoom_controls_hbox.pack_start(zoom_fit_btn, False, True, 0)
- # zooming slider
- self._zoomAdjustment = Gtk.Adjustment()
- self._zoomAdjustment.set_value(Zoomable.getCurrentZoomLevel())
- self._zoomAdjustment.connect("value-changed", self._zoomAdjustmentChangedCb)
- self._zoomAdjustment.props.lower = 0
- self._zoomAdjustment.props.upper = Zoomable.zoom_steps
- zoomslider = Gtk.Scale.new(Gtk.Orientation.HORIZONTAL, adjustment=self._zoomAdjustment)
- zoomslider.props.draw_value = False
- zoomslider.set_tooltip_text(_("Zoom Timeline"))
- zoomslider.connect("scroll-event", self._zoomSliderScrollCb)
- zoomslider.set_size_request(100, 0) # At least 100px wide for precision
- zoom_controls_hbox.pack_start(zoomslider, True, True, 0)
- self.attach(zoom_controls_hbox, 0, 1, 0, 1, yoptions=0, xoptions=Gtk.AttachOptions.FILL)
-
- # controls for tracks and layers
- self.controls = TimelineControls(self.app)
- controlwindow = Gtk.Viewport(None, None)
- controlwindow.add(self.controls)
- controlwindow.set_size_request(-1, 1)
- controlwindow.set_shadow_type(Gtk.ShadowType.OUT)
- scrolledwindow = Gtk.ScrolledWindow()
- scrolledwindow.add(controlwindow)
- scrolledwindow.props.hscrollbar_policy = Gtk.PolicyType.NEVER
- scrolledwindow.props.vscrollbar_policy = Gtk.PolicyType.ALWAYS
- scrolledwindow.props.vadjustment = self.vadj
- # We need ALWAYS policy for correct sizing, but we don't want the
- # scrollbar to be visible. Yay gtk3!
- scrollbar = scrolledwindow.get_vscrollbar()
+ print "added asset"
+ layer.add_asset(asset, self.bTimeline.props.duration,
+ 0, clip_duration, 1.0, asset.get_supported_formats())
+ self.bTimeline.enable_update(True)
- def scrollbar_show_cb(scrollbar):
- scrollbar.hide()
+ def setProjectManager(self, projectmanager):
+ if self._projectmanager is not None:
+ self._projectmanager.disconnect_by_func(self._projectChangedCb)
- scrollbar.connect("show", scrollbar_show_cb)
- scrollbar.hide()
- self.attach(scrolledwindow, 0, 1, 1, 2, xoptions=Gtk.AttachOptions.FILL)
+ self._projectmanager = projectmanager
+ if projectmanager is not None:
+ projectmanager.connect("new-project-created", self._projectCreatedCb)
+ projectmanager.connect("new-project-loaded", self._projectChangedCb)
- # timeline ruler
- self.ruler = ruler.ScaleRuler(self.app, self.hadj)
- self.ruler.get_accessible().set_name("timeline ruler") # used for dogtail
- self.ruler.set_size_request(0, 25)
- self.ruler.connect("key-press-event", self._keyPressEventCb)
- rulerframe = Gtk.Frame()
- rulerframe.set_shadow_type(Gtk.ShadowType.OUT)
- rulerframe.add(self.ruler)
- self.attach(rulerframe, 1, 2, 0, 1, yoptions=0)
-
- # proportional timeline
- self._canvas = TimelineCanvas(self.app)
- self._canvas.get_accessible().set_name("timeline canvas") # used for dogtail
- self._root_item = self._canvas.get_root_item()
- self.attach(self._canvas, 1, 2, 1, 2)
-
- # scrollbar
- self._hscrollbar = Gtk.HScrollbar(self.hadj)
- self._vscrollbar = Gtk.VScrollbar(self.vadj)
- self.attach(self._hscrollbar, 1, 2, 2, 3, yoptions=0)
- self.attach(self._vscrollbar, 2, 3, 1, 2, xoptions=0)
- self.hadj.connect("value-changed", self._updateScrollPosition)
- self.vadj.connect("value-changed", self._updateScrollPosition)
+ def _ensureLayer(self):
+ """
+ Make sure we have a layer in our timeline
- # error infostub
- self.infostub = InfoStub()
- self.attach(self.infostub, 1, 2, 4, 5, yoptions=0)
+ Returns: The number of layer present in self.timeline
+ """
+ layers = self.bTimeline.get_layers()
- self.show_all()
- self.infostub.hide()
+ if (len(layers) == 0):
+ layer = GES.Layer()
+ layer.props.auto_transition = True
+ self.bTimeline.add_layer(layer)
+ layers = [layer]
+
+ return layers
- # toolbar actions
+ def _createActions(self):
actions = (
("ZoomIn", Gtk.STOCK_ZOOM_IN, None,
"<Control>plus", ZOOM_IN, self._zoomInCb),
@@ -1121,435 +1152,234 @@ class Timeline(Gtk.Table, Loggable, Zoomable):
self.ui_manager.add_ui_from_string(ui)
- # drag and drop
-
- self._canvas.drag_dest_set(Gtk.DestDefaults.MOTION | Gtk.DestDefaults.HIGHLIGHT,
- [FILESOURCE_TARGET_ENTRY, EFFECT_TARGET_ENTRY],
- Gdk.DragAction.COPY)
-
- self._canvas.drag_dest_add_text_targets()
-
- self._canvas.connect("drag-leave", self._dragLeaveCb)
- self._canvas.connect("drag-drop", self._dragDropCb)
- self._canvas.connect("drag-motion", self._dragMotionCb)
- self._canvas.connect("key-press-event", self._keyPressEventCb)
- self._canvas.connect("scroll-event", self._scrollEventCb)
-
-## Event callbacks
- def _keyPressEventCb(self, unused_widget, event):
- kv = event.keyval
- self.debug("kv:%r", kv)
- if kv not in [Gdk.KEY_Left, Gdk.KEY_Right]:
- return False
- mod = event.get_state()
- try:
- if mod & Gdk.ModifierType.CONTROL_MASK:
- now = self._project.pipeline.getPosition()
- ltime, rtime = self._project.timeline.edges.closest(now)
-
- if kv == Gdk.KEY_Left:
- if mod & Gdk.ModifierType.SHIFT_MASK:
- self._seeker.seekRelative(0 - Gst.SECOND)
- elif mod & Gdk.ModifierType.CONTROL_MASK:
- self._seeker.seek(ltime + 1)
- else:
- self.app.current.pipeline.stepFrame(self._framerate, -1)
- elif kv == Gdk.KEY_Right:
- if mod & Gdk.ModifierType.SHIFT_MASK:
- self._seeker.seekRelative(Gst.SECOND)
- elif mod & Gdk.ModifierType.CONTROL_MASK:
- self._seeker.seek(rtime + 1)
- else:
- self.app.current.pipeline.stepFrame(self._framerate, 1)
- finally:
- return True
+ def _packScrollbars(self, vbox):
+ self.hadj = Gtk.Adjustment()
+ self.vadj = Gtk.Adjustment()
+ self.hadj.connect("value-changed", self._updateScrollPosition)
+ self.vadj.connect("value-changed", self._updateScrollPosition)
-## Drag and Drop callbacks
-
- def _dragMotionCb(self, unused, context, x, y, timestamp):
- # Set up the initial data when we first initiate the drag operation
- if not self._drag_started:
- self._drag_started = True
- elif context.list_targets() not in DND_EFFECT_LIST and self.app.gui.medialibrary.dragged:
- if not self._temp_elements:
- self._create_temp_source(x, y)
-
- # Let some time for TrackElement-s to be created
- if self._temp_elements:
- focus = self._temp_elements[0]
- if self._move_context is None:
- self._move_context = EditingContext(focus,
- self.timeline, GES.EditMode.EDIT_NORMAL, GES.Edge.EDGE_NONE,
- set(self._temp_elements[1:]), self.app.settings)
-
- self._move_temp_source(x, y)
- return True
+ self._vscrollbar = Gtk.VScrollbar(self.vadj)
- def _dragLeaveCb(self, unused_layout, context, unused_tstamp):
- """
- This occurs when the user leaves the canvas area during a drag,
- or when the item being dragged has been dropped.
+ def scrollbar_show_cb(scrollbar):
+ scrollbar.hide()
- Since we always get a "drag-dropped" signal right after "drag-leave",
- we wait 75 ms to see if a drop happens and if we need to cleanup or not.
- """
- self.debug("Drag leave")
- self._canvas.handler_block_by_func(self._dragMotionCb)
- GLib.timeout_add(75, self._dragCleanUp, context)
+# self._vscrollbar.connect("show", scrollbar_show_cb)
- def _dragCleanUp(self, context):
- """
- If the user drags outside the timeline,
- remove the temporary objects we had created during the drap operation.
- """
- # Clean up only if clip was not dropped already
- if self._drag_started:
- self.debug("Drag cleanup")
- self._drag_started = False
- self._factories = []
- if context.list_targets() not in DND_EFFECT_LIST:
- self.timeline.enable_update(True)
- self.debug("Need to cleanup %d elements" % len(self._temp_elements))
- for obj in self._temp_elements:
- layer = obj.get_layer()
- layer.remove_clip(obj)
- self._temp_elements = []
- self._move_context = None
-
- self.debug("Drag cleanup ended")
- self._canvas.handler_unblock_by_func(self._dragMotionCb)
- return False
+ self._hscrollBar = Gtk.HScrollbar(self.hadj)
+ vbox.pack_end(self._hscrollBar, False, True, False)
- def _dragDropCb(self, widget, context, x, y, timestamp):
- # Resetting _drag_started will tell _dragCleanUp to not do anything
- self._drag_started = False
- self.debug("Drag drop")
-
- if self.app.gui.medialibrary.dragged:
- self._canvas.drag_unhighlight()
- self.app.action_log.begin("add clip")
- if self._move_context is not None:
- self._move_context.finish()
- self._move_context = None
- self.app.action_log.commit()
- # The temporary objects and factories that we had created
- # in _dragMotionCb are now kept for good.
- # Clear the temporary references to objects, as they are real now.
- self._temp_elements = []
- self._factories = []
- #context.drop_finish(True, timestamp)
- else:
- if self.app.current.timeline.props.duration == 0:
- return False
- clips = self._getClipsUnderMouse(x, y)
- if clips:
- # FIXME make a util function to add effects
- # instead of copy/pasting it from cliproperties
- bin_desc = self.app.gui.effectlist.getSelectedItems()
- media_type = self.app.effects.getFactoryFromName(bin_desc).media_type
-
- # Trying to apply effect only on the first object of the selection
- clip = clips[0]
-
- # Checking that this effect can be applied on this track object
- # Which means, it has the corresponding media_type
- for track_element in clip.get_children():
- track_type = track_element.get_track_type()
- if track_type == GES.TrackType.AUDIO and media_type == AUDIO_EFFECT or \
- track_type == GES.TrackType.VIDEO and media_type == VIDEO_EFFECT:
- #Actually add the effect
- self.app.action_log.begin("add effect")
- effect = GES.Effect.new(bin_description=bin_desc)
- clip.add(effect)
- self.app.gui.clipconfig.effect_expander.updateAll()
- self.app.action_log.commit()
- self._factories = None
- self._seeker.flush()
+ self.ruler = ScaleRuler(self, self.hadj)
+ self.ruler.setProjectFrameRate(24.)
- self.timeline.selection.setSelection(clips, SELECT)
- break
- Gtk.drag_finish(context, True, False, timestamp)
- return True
+ self.ruler.set_size_request(0, 25)
+ self.ruler.hide()
- def _getClipsUnderMouse(self, x, y):
- clips = []
- items_in_area = self._canvas.getItemsInArea(x, y, x + 1, y + 1)
+ self.vadj.props.lower = 0
+ self.vadj.props.upper = 500
+ self.vadj.props.page_size = 250
- track_elements = [obj for obj in items_in_area[1]]
- for track_element in track_elements:
- clips.append(track_element.get_parent())
+ hbox = Gtk.HBox()
+ hbox.set_size_request(-1, 500)
+ hbox.pack_start(self.embed, True, True, True)
+ hbox.pack_start(self._vscrollbar, False, True, False)
- return clips
+ vbox.pack_end(hbox, True, True, True)
- def _showSaveScreenshotDialog(self):
- """
- Show a filechooser dialog asking the user where to save the snapshot
- and what file type to use.
+ hbox = Gtk.HBox()
+ self.zoomBox.set_size_request(CONTROL_WIDTH, -1)
+ hbox.pack_start(self.zoomBox, False, True, False)
+ hbox.pack_start(self.ruler, True, True, True)
- Returns a list containing the full path and the mimetype if successful,
- returns none otherwise.
- """
- chooser = Gtk.FileChooserDialog(_("Save As..."), self.app.gui,
- action=Gtk.FileChooserAction.SAVE,
- buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
- Gtk.STOCK_SAVE, Gtk.ResponseType.OK))
- chooser.set_icon_name("pitivi")
- chooser.set_select_multiple(False)
- chooser.set_current_name(_("Untitled"))
- chooser.props.do_overwrite_confirmation = True
- formats = {_("PNG image"): ["image/png", ("png",)],
- _("JPEG image"): ["image/jpeg", ("jpg", "jpeg")]}
- for format in formats:
- filt = Gtk.FileFilter()
- filt.set_name(format)
- filt.add_mime_type(formats.get(format)[0])
- chooser.add_filter(filt)
- response = chooser.run()
- if response == Gtk.ResponseType.OK:
- chosen_format = formats.get(chooser.get_filter().get_name())
- chosen_ext = chosen_format[1][0]
- chosen_mime = chosen_format[0]
- uri = join(chooser.get_current_folder(), chooser.get_filename())
- ret = [uri + "." + chosen_ext, chosen_mime]
- else:
- ret = None
- chooser.destroy()
- return ret
+ vbox.pack_end(hbox, False, True, False)
- def _ensureLayer(self):
- """
- Make sure we have a layer in our timeline
+ def _updateScrollPosition(self, 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)
- Returns: The number of layer present in self.timeline
+ def zoomChanged(self):
+ self.updateHScrollAdjustments()
+
+ def updateHScrollAdjustments(self):
+ """
+ Recalculate the horizontal scrollbar depending on the timeline duration.
"""
- layers = self.timeline.get_layers()
+ timeline_ui_width = self.embed.get_allocation().width
+# controls_width = self.controls.get_allocation().width
+# scrollbar_width = self._vscrollbar.get_allocation().width
+ controls_width = 0
+ scrollbar_width = 0
+ contents_size = Zoomable.nsToPixel(self.bTimeline.props.duration)
- if (len(layers) == 0):
- layer = GES.Layer()
- layer.props.auto_transition = True
- self.timeline.add_layer(layer)
- layers = [layer]
+ widgets_width = controls_width + scrollbar_width
+ end_padding = 250 # Provide some space for clip insertion at the end
- return layers
+ self.hadj.props.lower = 0
+ self.hadj.props.upper = contents_size + widgets_width + 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
- def purgeObject(self, uri):
- """Remove all instances of a clip from the timeline."""
- quoted_uri = quote_uri(uri)
- layers = self.timeline.get_layers()
- for layer in layers:
- for clip in layer.get_clips():
- if hasattr(clip, "get_uri"):
- if quote_uri(clip.get_uri()) == quoted_uri:
- layer.remove_clip(clip)
- else:
- # TimelineStandardTransition and the like don't have URIs
- # GES will remove those transitions automatically.
- self.debug("Not removing %s from timeline as it has no URI" % clip)
-
- def _create_temp_source(self, x, y):
- """
- Create temporary clips to be displayed on the timeline during a
- drag-and-drop operation.
- """
- layer = self._ensureLayer()[0]
- start = 0
+ if contents_size + widgets_width <= 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
- for asset in self.app.gui.medialibrary.getSelectedAssets():
- if asset.is_image():
- clip_duration = long(long(self._settings.imageClipLength) * Gst.SECOND / 1000)
- else:
- clip_duration = asset.get_duration()
+ def run(self):
+ self.testTimeline(self.timeline)
+ GLib.io_add_watch(sys.stdin, GLib.IO_IN, quit2_)
+ Gtk.main()
- source = layer.add_asset(asset, start, 0,
- clip_duration, asset.get_supported_formats())
+ def _setBestZoomRatio(self):
+ """
+ Set the zoom level so that the entire timeline is in view.
+ """
+ ruler_width = self.ruler.get_allocation().width
+ # Add Gst.SECOND - 1 to the timeline duration to make sure the
+ # last second of the timeline will be in view.
+ duration = self.timeline.bTimeline.get_duration()
+ if duration == 0:
+# self.debug("The timeline duration is 0, impossible to calculate zoom")
+ return
- self._temp_elements.insert(0, source)
- start += asset.get_duration()
+ timeline_duration = duration + Gst.SECOND - 1
+ timeline_duration_s = int(timeline_duration / Gst.SECOND)
- def _move_temp_source(self, x, y):
- x = self.hadj.props.value + x
- y = self.vadj.props.value + y
+ #self.debug("duration: %s, timeline duration: %s" % (print_ns(duration),
+ # print_ns(timeline_duration)))
- priority = self.controls.getPriorityForY(y)
+ ideal_zoom_ratio = float(ruler_width) / timeline_duration_s
+ nearest_zoom_level = Zoomable.computeZoomLevel(ideal_zoom_ratio)
+ #self.debug("Ideal zoom: %s, nearest_zoom_level %s", ideal_zoom_ratio, nearest_zoom_level)
+ Zoomable.setZoomLevel(nearest_zoom_level)
+ #self.timeline.props.snapping_distance = \
+ # Zoomable.pixelToNs(self.app.settings.edgeSnapDeadband)
- delta = Zoomable.pixelToNs(x)
- self._move_context.editTo(delta, priority)
+ # Only do this at the very end, after updating the other widgets.
+ #self.log("Setting 'zoomed_fitted' to True")
+ self.zoomed_fitted = True
-## Zooming and Scrolling
+ def zoomFit(self):
+ self._hscrollBar.set_value(0)
+ self._setBestZoomRatio()
- def _scrollEventCb(self, canvas, event):
- if event.get_state() & Gdk.ModifierType.SHIFT_MASK:
- # shift + scroll => vertical (up/down) scroll
- if event.direction == Gdk.ScrollDirection.UP:
- self.scroll_up()
- elif event.direction == Gdk.ScrollDirection.DOWN:
- self.scroll_down()
- event.props.state &= ~Gdk.SHIFT_MASK
- elif event.get_state() & Gdk.ModifierType.CONTROL_MASK:
- # zoom + scroll => zooming (up: zoom in)
- if event.direction == Gdk.ScrollDirection.UP:
- Zoomable.zoomIn()
- self.log("Setting 'zoomed_fitted' to False")
- self.zoomed_fitted = False
- return True
- elif event.direction == Gdk.ScrollDirection.DOWN:
- Zoomable.zoomOut()
- self.log("Setting 'zoomed_fitted' to False")
- self.zoomed_fitted = False
- return True
- return False
+ def scrollToPosition(self, position):
+ if position > self.hadj.props.upper:
+ # we can't perform the scroll because the canvas needs to be
+ # updated
+ GLib.idle_add(self._scrollToPosition, position)
else:
- if event.direction == Gdk.ScrollDirection.UP:
- self.scroll_left()
- elif event.direction == Gdk.ScrollDirection.DOWN:
- self.scroll_right()
- return True
+ self._scrollToPosition(position)
- def scroll_left(self):
- self._hscrollbar.set_value(self._hscrollbar.get_value() -
+ def _scrollLeft(self):
+ self._hscrollBar.set_value(self._hscrollBar.get_value() -
self.hadj.props.page_size ** (2.0 / 3.0))
- def scroll_right(self):
- self._hscrollbar.set_value(self._hscrollbar.get_value() +
+ def _scrollRight(self):
+ self._hscrollBar.set_value(self._hscrollBar.get_value() +
self.hadj.props.page_size ** (2.0 / 3.0))
- def scroll_up(self):
+ def _scrollUp(self):
self._vscrollbar.set_value(self._vscrollbar.get_value() -
self.vadj.props.page_size ** (2.0 / 3.0))
- def scroll_down(self):
+ def _scrollDown(self):
self._vscrollbar.set_value(self._vscrollbar.get_value() +
self.vadj.props.page_size ** (2.0 / 3.0))
- def unsureVadjHeight(self):
- self._scroll_pos_ns = Zoomable.pixelToNs(self.hadj.get_value())
- self._root_item.set_simple_transform(0 - self.hadj.get_value(),
- 0 - self.vadj.get_value(), 1.0, 0)
-
- def _updateScrollPosition(self, adjustment):
- self.unsureVadjHeight()
-
- def _zoomAdjustmentChangedCb(self, adjustment):
- # GTK crack
- self._updateZoomSlider = False
- Zoomable.setZoomLevel(int(adjustment.get_value()))
- self.log("Setting 'zoomed_fitted' to False")
- self.zoomed_fitted = False
- self._updateZoomSlider = True
-
- def _zoomSliderScrollCb(self, unused_widget, event):
- value = self._zoomAdjustment.get_value()
- if event.direction in [Gdk.ScrollDirection.UP, Gdk.ScrollDirection.RIGHT]:
- self._zoomAdjustment.set_value(value + 1)
- elif event.direction in [Gdk.ScrollDirection.DOWN, Gdk.ScrollDirection.LEFT]:
- self._zoomAdjustment.set_value(value - 1)
-
- def zoomChanged(self):
- if self._updateZoomSlider:
- self._zoomAdjustment.set_value(self.getCurrentZoomLevel())
-
- if self._settings and self._timeline:
- # zoomChanged might be called various times before the UI is ready
- self._timeline.props.snapping_distance = \
- Zoomable.pixelToNs(self._settings.edgeSnapDeadband)
-
- # the new scroll position should preserve the current horizontal
- # position of the playhead in the window
- cur_playhead_offset = self._canvas._playhead.props.x - self.hadj.props.value
- try:
- position = self.app.current.pipeline.getPosition()
- except PipelineError:
- position = 0
- new_pos = Zoomable.nsToPixel(position) - cur_playhead_offset
-
- # Update the position of the playhead's line on the canvas
- # This does not actually change the timeline position
- self._canvas._playhead.props.x = Zoomable.nsToPixel(position)
-
- self.updateHScrollAdjustments()
- self.scrollToPosition(new_pos)
- self.ruler.queue_resize()
- self.ruler.queue_draw()
-
- def positionChangedCb(self, seeker, position):
- self.ruler.timelinePositionChanged(position)
- self._canvas.timelinePositionChanged(position)
- if self.app.current.pipeline.getState() == Gst.State.PLAYING:
- self.scrollToPlayhead()
+ def _scrollToPosition(self, position):
+ self._hscrollBar.set_value(position)
+ return False
- def scrollToPlayhead(self):
- """
- If the current position is out of the view bounds, then scroll
- as close to the center of the view as possible or as close as the
- timeline canvas allows.
- """
- canvas_size = self._canvas.get_allocation().width
- new_pos = Zoomable.nsToPixel(self.app.current.pipeline.getPosition())
+ def _scrollToPlayhead(self):
+ canvas_size = self.embed.get_allocation().width - CONTROL_WIDTH
+ new_pos = self.timeline.playhead.props.x
scroll_pos = self.hadj.get_value()
- if (new_pos > scroll_pos + canvas_size) or (new_pos < scroll_pos):
- self.scrollToPosition(min(new_pos - canvas_size / 6,
- self.hadj.props.upper - canvas_size - 1))
+ self.scrollToPosition(min(new_pos - canvas_size / 2,
+ self.hadj.props.upper - canvas_size - 1))
+
+ def goToPoint(self, timeline):
+ point = Clutter.Point()
+ point.x = 1000
+ point.y = 0
+ timeline.scroll_to_point(point)
return False
- def scrollToPosition(self, position):
- if position > self.hadj.props.upper:
- # we can't perform the scroll because the canvas needs to be
- # updated
- GLib.idle_add(self._scrollToPosition, position)
- else:
- self._scrollToPosition(position)
+ def addClipToLayer(self, layer, asset, start, duration, inpoint):
+ layer.add_asset(asset, start * Gst.SECOND, 0, duration * Gst.SECOND, 1.0,
asset.get_supported_formats())
+
+ def handle_message(self, bus, message):
+ if message.type == Gst.MessageType.ELEMENT:
+ if message.has_name('prepare-window-handle'):
+ Gdk.threads_enter()
+ self.sink = message.src
+ self.sink.set_window_handle(self.viewer.window_xid)
+ self.sink.expose()
+ Gdk.threads_leave()
+ elif message.type == Gst.MessageType.STATE_CHANGED:
+ prev, new, pending = message.parse_state_changed()
+ return True
- def _scrollToPosition(self, position):
- self._hscrollbar.set_value(position)
+ def _clickedCb(self, stage, event):
+ actor = self.stage.get_actor_at_pos(Clutter.PickMode.REACTIVE, event.x, event.y)
+ if actor == stage:
+ self.timeline.emptySelection()
+
+ def doSeek(self):
+ #self.pipeline.simple_seek(3000000000)
return False
- def _snapDistanceChangedCb(self, settings):
- if self._timeline:
- self._timeline.props.snapping_distance = \
- Zoomable.pixelToNs(settings.edgeSnapDeadband)
+ def togglePlayback(self, button):
+ self.pipeline.togglePlayback()
- def _setBestZoomRatio(self):
- """
- Set the zoom level so that the entire timeline is in view.
+ def _renderingSettingsChangedCb(self, project, item, value):
"""
- ruler_width = self.ruler.get_allocation().width
- # Add Gst.SECOND - 1 to the timeline duration to make sure the
- # last second of the timeline will be in view.
- duration = self.timeline.get_duration()
- if duration == 0:
- self.debug("The timeline duration is 0, impossible to calculate zoom")
- return
-
- timeline_duration = duration + Gst.SECOND - 1
- timeline_duration_s = int(timeline_duration / Gst.SECOND)
+ Called when any Project metadata changes, we filter out the one
+ we are interested in.
- self.debug("duration: %s, timeline duration: %s" % (print_ns(duration),
- print_ns(timeline_duration)))
+ if @item is None, it mean we called it ourself, and want to force
+ getting the project videorate value
+ """
+ if item == "videorate" or item is None:
+ if value is None:
+ value = project.videorate
+ self._framerate = value
+ self.ruler.setProjectFrameRate(self._framerate)
- ideal_zoom_ratio = float(ruler_width) / timeline_duration_s
- nearest_zoom_level = Zoomable.computeZoomLevel(ideal_zoom_ratio)
- self.debug("Ideal zoom: %s, nearest_zoom_level %s", ideal_zoom_ratio, nearest_zoom_level)
- Zoomable.setZoomLevel(nearest_zoom_level)
- self.timeline.props.snapping_distance = \
- Zoomable.pixelToNs(self.app.settings.edgeSnapDeadband)
+ def _doAssetAddedCb(self, project, asset, layer):
+ self.addClipToLayer(layer, asset, 2, 10, 5)
+ self.addClipToLayer(layer, asset, 15, 10, 5)
- # Only do this at the very end, after updating the other widgets.
- self.log("Setting 'zoomed_fitted' to True")
- self.zoomed_fitted = True
+ self.pipeline = Pipeline()
+ self.pipeline.add_timeline(layer.get_timeline())
-## Project callbacks
+ self.bus = self.pipeline.get_bus()
+ self.bus.add_signal_watch()
+ self.bus.connect("message", self.handle_message)
+ self.playButton.connect("clicked", self.togglePlayback)
+ #self.pipeline.togglePlayback()
+ self.pipeline.activatePositionListener(interval=30)
+ self.timeline.setPipeline(self.pipeline)
+ GObject.timeout_add(1000, self.doSeek)
+ Zoomable.setZoomLevel(50)
def _projectChangedCb(self, app, project, unused_fully_loaded):
"""
When a project is loaded, we connect to its pipeline
"""
- self.debug("Project changed")
+# self.debug("Project changed")
if project:
- self.debug("Project is not None, connecting to its pipeline")
+# self.debug("Project is not None, connecting to its pipeline")
self._seeker = self._project.seeker
self._pipeline = self._project.pipeline
- self._pipeline.connect("position", self.positionChangedCb)
+# self._pipeline.connect("position", self.positionChangedCb)
self.ruler.setProjectFrameRate(self._project.videorate)
self.ruler.zoomChanged()
self._renderingSettingsChangedCb(self._project, None, None)
@@ -1560,13 +1390,13 @@ class Timeline(Gtk.Table, Loggable, Zoomable):
"""
When a project is created, we connect to it timeline
"""
- self.debug("Setting project %s", project)
+# self.debug("Setting project %s", project)
if self._project:
self._project.disconnect_by_func(self._renderingSettingsChangedCb)
- try:
- self._pipeline.disconnect_by_func(self.positionChangedCb)
- except TypeError:
- pass # We were not connected no problem
+ #try:
+ # self._pipeline.disconnect_by_func(self.positionChangedCb)
+ #except TypeError:
+ # pass # We were not connected no problem
self._pipeline = None
self._seeker = None
@@ -1577,139 +1407,6 @@ class Timeline(Gtk.Table, Loggable, Zoomable):
self._renderingSettingsChangedCb)
self.setTimeline(project.timeline)
- def setProjectManager(self, projectmanager):
- if self._projectmanager is not None:
- self._projectmanager.disconnect_by_func(self._projectChangedCb)
-
- self._projectmanager = projectmanager
- if projectmanager is not None:
- projectmanager.connect("new-project-created", self._projectCreatedCb)
- projectmanager.connect("new-project-loaded", self._projectChangedCb)
-
- def _renderingSettingsChangedCb(self, project, item, value):
- """
- Called when any Project metadata changes, we filter out the one
- we are interested in.
-
- if @item is None, it mean we called it ourself, and want to force
- getting the project videorate value
- """
- if item == "videorate" or item is None:
- if value is None:
- value = project.videorate
- self._framerate = value
- self.ruler.setProjectFrameRate(self._framerate)
-
-## Timeline callbacks
-
- def setTimeline(self, timeline):
- self.debug("Setting timeline %s", timeline)
-
- self.delTimeline()
- self.controls.timeline = timeline
- self._timeline = timeline
-
- if timeline:
- # Connecting to timeline gobject signals
- self._signal_ids.append(timeline.connect("layer-added",
- self._layerAddedCb))
- self._signal_ids.append(timeline.connect("layer-removed",
- self._layerRemovedCb))
- self._signal_ids.append(timeline.connect("notify::update",
- self._timelineUpdateChangedCb))
- self._signal_ids.append(timeline.connect("notify::duration",
- self._timelineDurationChangedCb))
-
- # The Selection object of _timeline inherits signallable
- # We will be able to disconnect it with disconnect_by_func
- self._timeline.selection.connect("selection-changed", self._selectionChangedCb)
-
- # Make sure to set the current layer in use
- self._layerAddedCb(None, None)
- self._timeline.props.snapping_distance = \
- Zoomable.pixelToNs(self._settings.edgeSnapDeadband)
-
- self._canvas.setTimeline(timeline)
- self._canvas.zoomChanged()
-
- def getTimeline(self):
- return self._timeline
-
- def delTimeline(self):
- # Disconnect signals
- for sigid in self._signal_ids:
- self._timeline.disconnect(sigid)
- self._signal_ids = []
- if hasattr(self._timeline, "selection"):
- self._timeline.selection.disconnect_by_func(self._selectionChangedCb)
-
- #Remove references to the ges timeline
- self._timeline = None
- self.controls.timeline = None
-
- timeline = property(getTimeline, setTimeline, delTimeline, "The GESTimeline")
-
- def _timelineDurationChangedCb(self, timeline, unused_duration):
- self.updateHScrollAdjustments()
-
- def _timelineUpdateChangedCb(self, unused, unsued2=None):
- if self.zoomed_fitted is True:
- self._setBestZoomRatio()
-
- def _layerAddedCb(self, unused_layer, unused_user_data):
- self.updateVScrollAdjustments()
-
- def _layerRemovedCb(self, unused_layer, unused_user_data):
- self.updateVScrollAdjustments()
-
- def updateVScrollAdjustments(self):
- """
- Recalculate the vertical scrollbar depending on the number of layer in
- the timeline.
- """
- self.vadj.props.upper = self.controls.get_allocation().height + 50
-
- def updateHScrollAdjustments(self):
- """
- Recalculate the horizontal scrollbar depending on the timeline duration.
- """
- timeline_ui_width = self.get_allocation().width
- controls_width = self.controls.get_allocation().width
- scrollbar_width = self._vscrollbar.get_allocation().width
- contents_size = Zoomable.nsToPixel(self.app.current.timeline.props.duration)
-
- widgets_width = controls_width + scrollbar_width
- end_padding = 250 # Provide some space for clip insertion at the end
-
- self.hadj.props.lower = 0
- self.hadj.props.upper = contents_size + widgets_width + 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 + widgets_width <= 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
-
- def _selectionChangedCb(self, selection):
- """
- The selected clips on the timeline canvas have changed with the
- "selection-changed" signal.
-
- This is where you apply global UI changes, unlike individual
- track elements' "selected-changed" signal from the Selected class.
- """
- if selection:
- self.selection_actions.set_sensitive(True)
- else:
- self.selection_actions.set_sensitive(False)
-
-## ToolBar callbacks
- def _zoomFitCb(self, unused, unsued2=None):
- self._setBestZoomRatio()
-
def _zoomInCb(self, unused_action):
# This only handles the button callbacks (from the menus),
# not keyboard shortcuts or the zoom slider!
@@ -1724,6 +1421,18 @@ class Timeline(Gtk.Table, Loggable, Zoomable):
self.log("Setting 'zoomed_fitted' to False")
self.zoomed_fitted = False
+ def _zoomFitCb(self, unused, unsued2=None):
+ self._setBestZoomRatio()
+
+ def _screenshotCb(self, unused_action):
+ """
+ Export a snapshot of the current frame as an image file.
+ """
+ foo = self._showSaveScreenshotDialog()
+ if foo:
+ path, mime = foo[0], foo[1]
+ self._project.pipeline.save_thumbnail(-1, -1, mime, path)
+
def deleteSelected(self, unused_action):
if self.timeline:
self.app.action_log.begin("delete clip")
@@ -1774,16 +1483,16 @@ class Timeline(Gtk.Table, Loggable, Zoomable):
"""
Split clips at the current playhead position, regardless of selections.
"""
- self.timeline.enable_update(False)
+ self.bTimeline.enable_update(False)
position = self.app.current.pipeline.getPosition()
- for track in self.timeline.get_tracks():
+ for track in self.bTimeline.get_tracks():
for element in track.get_elements():
start = element.get_start()
end = start + element.get_duration()
if start < position and end > position:
clip = element.get_parent()
clip.split(position)
- self.timeline.enable_update(True)
+ self.bTimeline.enable_update(True)
def keyframe(self, action):
"""
@@ -1811,9 +1520,6 @@ class Timeline(Gtk.Table, Loggable, Zoomable):
interpolator.newKeyframe(position_in_obj)
self.app.action_log.commit()
- def playPause(self, unused_action):
- self.app.current.pipeline.togglePlayback()
-
def _previousKeyframeCb(self, action):
position = self.app.current.pipeline.getPosition()
prev_kf = self.timeline.getPrevKeyframe(position)
@@ -1828,33 +1534,394 @@ class Timeline(Gtk.Table, Loggable, Zoomable):
self._seeker.seek(next_kf)
self.scrollToPlayhead()
- def _screenshotCb(self, unused_action):
+ def playPause(self, unused_action):
+ self.app.current.pipeline.togglePlayback()
+
+ def setTimeline(self, bTimeline):
+ self.bTimeline = bTimeline
+ self.timeline.setTimeline(bTimeline)
+
+ def _scrollEventCb(self, embed, event):
+ # FIXME : see https://bugzilla.gnome.org/show_bug.cgi?id=697522
+ deltas = event.get_scroll_deltas()
+ if event.state & Gdk.ModifierType.CONTROL_MASK:
+ if deltas[2] < 0:
+ Zoomable.zoomIn()
+ elif deltas[2] > 0:
+ Zoomable.zoomOut()
+ self._scrollToPlayhead()
+ elif event.state & Gdk.ModifierType.SHIFT_MASK:
+ if deltas[2] > 0:
+ self._scrollDown()
+ elif deltas[2] < 0:
+ self._scrollUp()
+ else:
+ if deltas[2] > 0:
+ self._scrollRight()
+ elif deltas[2] < 0:
+ self._scrollLeft()
+ self.scrolled += 1
+
+ def testTimeline(self, timeline):
+ timeline.set_easing_duration(600)
+
+ Gst.init([])
+ GES.init()
+
+ self.project = GES.Project(uri=None, extractable_type=GES.Timeline)
+
+ bTimeline = GES.Timeline()
+ bTimeline.add_track(GES.Track.audio_raw_new())
+ bTimeline.add_track(GES.Track.video_raw_new())
+
+ timeline.setTimeline(bTimeline)
+
+ layer = GES.Layer()
+ bTimeline.add_layer(layer)
+
+ self.bTimeline = bTimeline
+
+ self.project.connect("asset-added", self._doAssetAddedCb, layer)
+ self.project.create_asset("file://" + sys.argv[1], GES.UriClip)
+
+
+def get_preview_for_object(bElement):
+ track_type = bElement.get_track_type()
+ if track_type == GES.TrackType.AUDIO:
+ # FIXME: RandomAccessAudioPreviewer doesn't work yet
+ # previewers[key] = RandomAccessAudioPreviewer(instance, uri)
+ # TODO: return waveform previewer
+ return Clutter.Actor()
+ elif track_type == GES.TrackType.VIDEO:
+ if bElement.get_parent().is_image():
+ # TODO: return still image previewer
+ return Clutter.Actor()
+ else:
+ return VideoPreviewer(bElement)
+ else:
+ return Clutter.Actor()
+
+
+class VideoPreviewer(Clutter.Actor, Zoomable):
+ def __init__(self, bElement):
"""
- Export a snapshot of the current frame as an image file.
+ @param bElement : the backend GES.TrackElement
+ @param track : the track to which the bElement belongs
+ @param timeline : the containing graphic timeline.
"""
- foo = self._showSaveScreenshotDialog()
- if foo:
- path, mime = foo[0], foo[1]
- self._project.pipeline.save_thumbnail(-1, -1, mime, path)
+ Zoomable.__init__(self)
+ Clutter.Actor.__init__(self)
- def insertEnd(self, assets):
+ self.uri = bElement.props.uri
+
+ self.layoutManager = Clutter.BinLayout()
+ self.set_layout_manager(self.layoutManager)
+
+ self.bElement = bElement
+
+# self.bElement.connect("notify::duration", self.element_changed)
+# self.bElement.connect("notify::in-point", self.element_changed)
+
+ self.duration = self.bElement.get_duration()
+ self.in_point = self.bElement.get_inpoint()
+
+ self.thumb_margin = BORDER_WIDTH
+ self.thumb_height = EXPANDED_SIZE - 2 * self.thumb_margin
+ # self.thumb_width will be set by self._setupPipeline()
+
+ # TODO: read this property from the settings
+ self.thumb_period = long(0.1 * Gst.SECOND)
+
+ # maps (quantized) times to Thumbnail objects
+ self.thumbs = {}
+
+ self.thumb_cache = ThumbnailCache(uri=self.uri)
+
+ self.queue = []
+
+ self.waiting_timestamp = None
+
+ self._setupPipeline()
+
+ self.callback_id = None
+
+ # Internal API
+
+ def _setupPipeline(self):
"""
- Add source at the end of the timeline
- @type sources: An L{GES.TimelineSource}
- @param x2: A list of sources to add to the timeline
+ Create the pipeline.
+
+ It has the form "playbin ! thumbnailsink" where thumbnailsink
+ is a Bin made out of "capsfilter ! gdkpixbufsink"
"""
- self.app.action_log.begin("add clip")
- # FIXME we should find the longets layer instead of adding it to the
- # first one
- # Handle the case of a blank project
- layer = self._ensureLayer()[0]
- self.timeline.enable_update(False)
- for asset in assets:
- if asset.is_image():
- clip_duration = long(long(self._settings.imageClipLength) * Gst.SECOND / 1000)
+ self.pipeline = Gst.ElementFactory.make("playbin", None)
+ self.pipeline.props.uri = self.uri
+ self.pipeline.props.flags = 1 # Only render video
+
+ # Set up the thumbnailsink
+ thumbnailsink = Gst.parse_bin_from_description("capsfilter
caps=video/x-raw,format=(string)RGB,pixel-aspect-ratio=(fraction)1/1 ! gdkpixbufsink name=gdkpixbufsink",
True)
+
+ # get the gdkpixbufsink and the automatically created ghostpad
+ self.gdkpixbufsink = thumbnailsink.get_by_name("gdkpixbufsink")
+ sinkpad = thumbnailsink.get_static_pad("sink")
+
+ # Connect the playbin and the thumbnailsink
+ self.pipeline.props.video_sink = thumbnailsink
+
+ # add a message handler that listens for the created pixbufs
+ self.pipeline.get_bus().add_signal_watch()
+ self.pipeline.get_bus().connect("message", self.bus_message_handler)
+
+ self.pipeline.set_state(Gst.State.PAUSED)
+ # Wait for the pipeline to be prerolled so we can check the width
+ # that the thumbnails will have and set the aspect ratio accordingly
+ # as well as getting the framerate of the video:
+ change_return = self.pipeline.get_state(Gst.CLOCK_TIME_NONE)
+ if Gst.StateChangeReturn.SUCCESS == change_return[0]:
+ neg_caps = sinkpad.get_current_caps()[0]
+ video_width = neg_caps["width"]
+ video_height = neg_caps["height"]
+ self.thumb_width = video_width * self.thumb_height / video_height
+ else:
+ # the pipeline couldn't be prerolled so we can't determine the
+ # correct values. Set sane defaults (this should never happen)
+ self.warning("Couldn't preroll the pipeline")
+ self.thumb_width = 16 * self.thumb_height / 9 # assume 16:9 aspect ratio
+
+ def _addThumbnails(self):
+ """
+ Adds thumbnails for the whole clip.
+
+ Takes the zoom setting into account and removes potentially
+ existing thumbnails prior to adding the new ones.
+ """
+ # TODO: check if duration or zoomratio really changed?
+ self.remove_all_children()
+ self.thumbs = {}
+
+ # calculate unquantized length of a thumb in nano seconds
+ thumb_duration_tmp = Zoomable.pixelToNs(self.thumb_width + self.thumb_margin)
+
+ # quantize thumb length to thumb_period
+ # TODO: replace with a call to utils.misc.quantize:
+ thumb_duration = (thumb_duration_tmp // self.thumb_period) * self.thumb_period
+ # make sure that the thumb duration after the quantization isn't smaller than before
+ if thumb_duration < thumb_duration_tmp:
+ thumb_duration += self.thumb_period
+
+ # make sure that we don't show thumbnails more often than thumb_period
+ thumb_duration = max(thumb_duration, self.thumb_period)
+
+ number_of_thumbs = self.duration / thumb_duration
+
+ current_time = 0
+ # +1 because wa want to draw the rightmost thumbnail even if it will be clipped
+ for i in range(0, number_of_thumbs + 1):
+ thumb = Thumbnail(self.thumb_width, self.thumb_height)
+ thumb.set_position(Zoomable.nsToPixel(current_time), self.thumb_margin)
+ self.add_child(thumb)
+ self.thumbs[current_time] = thumb
+ self._thumbForTime(current_time)
+ current_time += thumb_duration
+
+ def _thumbForTime(self, time):
+ if time in self.thumb_cache:
+ gdkpixbuf = self.thumb_cache[time]
+ self.thumbs[time].set_from_gdkpixbuf(gdkpixbuf)
+ else:
+ self._requestThumbnail(time)
+
+ def _requestThumbnail(self, time):
+ """Queue a thumbnail request for the given time"""
+ if time not in self.queue: # and len(self._queue) <= self.max_requests:
+ if self.queue:
+ self.queue.append(time)
else:
- clip_duration = asset.get_duration()
+ self.queue.append(time)
+ self._nextThumbnail()
+
+ def _nextThumbnail(self):
+ """Notifies the preview object that the pipeline is ready to process
+ the next thumbnail in the queue. This should always be called from the
+ main application thread."""
+ if self.queue:
+ if not self._startThumbnail(self.queue[0]):
+ self.queue.pop(0)
+ self._nextThumbnail()
+ return False
+
+ def _startThumbnail(self, time):
+ self.waiting_timestamp = time
+ return self.pipeline.seek(1.0,
+ Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.ACCURATE,
+ Gst.SeekType.SET, time,
+ Gst.SeekType.NONE, -1)
+
+ def _finishThumbnail(self, gdkpixbuf, time):
+ """Notifies the preview object that the a new thumbnail is ready to be
+ cached. This should be called by subclasses when they have finished
+ processing the thumbnail for the current segment. This function should
+ always be called from the main thread of the application."""
+ waiting = self.waiting_timestamp
+ self.waiting_timestamp = None
+
+ if time != waiting:
+ time = waiting
+
+ thumbnail = gdkpixbuf.scale_simple(self.thumb_width, self.thumb_height, 3)
+
+ self.thumb_cache[time] = thumbnail
+
+ if time in self.thumbs:
+ self.thumbs[time].set_from_gdkpixbuf(thumbnail)
+ #self.emit("update", time)
+
+ if time in self.queue:
+ self.queue.remove(time)
+ self._nextThumbnail()
+ return False
+
+ # Interface (Zoomable)
+
+ def _maybeUpdate(self):
+ self._addThumbnails()
+ self.callback_id = None
+ return False
+
+ def zoomChanged(self):
+ if self.callback_id is not None:
+ GObject.source_remove(self.callback_id)
+ self.callback_id = GObject.timeout_add(100, self._maybeUpdate)
+
+ # Callbacks
+
+ def bus_message_handler(self, unused_bus, message):
+ if message.type == Gst.MessageType.ELEMENT and \
+ message.src == self.gdkpixbufsink:
+ struct = message.get_structure()
+
+ # TODO: does struct.get_name() work?
+ #if struct.get_name() == "pixbuf":
+
+ # TODO: there exists no value named "timestamp"
+ #self._finishThumbnail(struct.get_value("pixbuf"), struct.get_value("timestamp"))
+ GLib.idle_add(self._finishThumbnail, struct.get_value("pixbuf"),
+ struct.get_value("timestamp"))
+ return Gst.BusSyncReply.PASS
+
+ #bElement = receiver()
+
+ # handler(bElement, "notify::duration")
+ # handler(bElement, "notify::in-point")
+ def element_changed(self, unused_bElement, unused_start_duration):
+ self.duration = self.bElement.get_duration()
+ self.in_point = self.bElement.get_inpoint()
+ GLib.idle_add(self._addThumbnails)
+
+
+class Thumbnail(Clutter.Actor):
+
+ def __init__(self, width, height):
+ Clutter.Actor.__init__(self)
+ image = Clutter.Image.new()
+ self.props.content = image
+ self.width = width
+ self.height = height
+ self.set_background_color(Clutter.Color.new(0, 100, 150, 100))
+ self.set_size(self.width, self.height)
+
+ def set_from_gdkpixbuf(self, gdkpixbuf):
+ row_stride = gdkpixbuf.get_rowstride()
+ pixel_data = gdkpixbuf.get_pixels()
+ # Cogl.PixelFormat.RGB_888 := 2
+ self.props.content.set_data(pixel_data, Cogl.PixelFormat.RGB_888, self.width, self.height,
row_stride)
+
+
+# TODO: replace with utils.misc.hash_file
+def hash_file(uri):
+ """Hashes the first 256KB of the specified file"""
+ sha256 = hashlib.sha256()
+ with open(uri, "rb") as file:
+ for _ in range(1024):
+ chunk = file.read(256)
+ if not chunk:
+ break
+ sha256.update(chunk)
+ return sha256.hexdigest()
+
+# TODO: remove eventually
+autocreate = True
+
+
+# TODO: replace with pitivi.settings.get_dir
+def get_dir(path, autocreate=True):
+ if autocreate and not os.path.exists(path):
+ os.makedirs(path)
+ return path
+
+
+class ThumbnailCache(object):
+
+ """Caches thumbnails by key using LRU policy, implemented with heapq.
+
+ Uses a two stage caching mechanism. A limited number of elements are
+ held in memory, the rest is being cached on disk using an sqlite db."""
+
+ def __init__(self, uri):
+ object.__init__(self)
+ # TODO: replace with utils.misc.hash_file
+ self.hash = hash_file(Gst.uri_get_location(uri))
+ # TODO: replace with pitivi.settings.xdg_cache_home()
+ cache_dir = get_dir(os.path.join(xdg_dirs.xdg_cache_home, "pitivi"), autocreate)
+ dbfile = os.path.join(get_dir(os.path.join(cache_dir, "thumbs")), self.hash)
+ self.conn = sqlite3.connect(dbfile)
+ self.cur = self.conn.cursor()
+ self.cur.execute("CREATE TABLE IF NOT EXISTS Thumbs (Time INTEGER NOT NULL PRIMARY KEY,\
+ Data BLOB NOT NULL, Width INTEGER NOT NULL, Height INTEGER NOT NULL, Stride INTEGER NOT NULL)")
+
+ def __contains__(self, key):
+ # check if item is present in on disk cache
+ self.cur.execute("SELECT Time FROM Thumbs WHERE Time = ?", (key,))
+ if self.cur.fetchone():
+ return True
+ return False
- layer.add_asset(asset, self.timeline.props.duration,
- 0, clip_duration, asset.get_supported_formats())
- self.timeline.enable_update(True)
+ def __getitem__(self, key):
+ self.cur.execute("SELECT * FROM Thumbs WHERE Time = ?", (key,))
+ row = self.cur.fetchone()
+ if row:
+ pixbuf = GdkPixbuf.Pixbuf.new_from_data(row[1],
+ GdkPixbuf.Colorspace.RGB,
+ False,
+ 8,
+ row[2],
+ row[3],
+ row[4],
+ None,
+ None)
+ return pixbuf
+ raise KeyError(key)
+
+ def __setitem__(self, key, value):
+ blob = sqlite3.Binary(bytearray(value.get_pixels()))
+ #Replace if the key already existed
+ self.cur.execute("DELETE FROM Thumbs WHERE time=?", (key,))
+ self.cur.execute("INSERT INTO Thumbs VALUES (?,?,?,?,?)", (key, blob, value.get_width(),
value.get_height(), value.get_rowstride()))
+ self.conn.commit()
+
+if __name__ == "__main__":
+ # Basic argument handling, no need for getopt here
+ if len(sys.argv) < 2:
+ print "Supply a uri as argument"
+ sys.exit()
+
+ print "Starting stupid demo, using uri as a new clip, with start = 2, duration = 25 and inpoint = 5."
+ print "Use ipython if you want to interact with the timeline in a more interesting way"
+ print "ipython ; %gui gtk3 ; %run timeline.py ; help yourself"
+
+ window = Gtk.Window()
+ widget = Timeline()
+ window.add(widget)
+ window.maximize()
+ window.show_all()
+ widget.run()
diff --git a/pitivi/utils/timeline.py b/pitivi/utils/timeline.py
index 2b5b5db..1c4d9e0 100644
--- a/pitivi/utils/timeline.py
+++ b/pitivi/utils/timeline.py
@@ -49,6 +49,40 @@ class TimelineError(Exception):
pass
+class Selected(Signallable):
+ """
+ A simple class that let us emit a selected-changed signal
+ when needed, and that can be check directly to know if the
+ object is selected or not.
+
+ This is meant only for individual elements, do not confuse this with
+ utils.timeline's "Selection" class.
+ """
+
+ __signals__ = {
+ "selected-changed": []}
+
+ def __init__(self):
+ self._selected = False
+ self.movable = True
+
+ def __nonzero__(self):
+ """
+ checking a Selected object is the same as checking its _selected
+ property
+ """
+ return self._selected
+
+ def getSelected(self):
+ return self._selected
+
+ def setSelected(self, selected):
+ self._selected = selected
+ self.emit("selected-changed", selected)
+
+ selected = property(getSelected, setSelected)
+
+
class Selection(Signallable):
"""
A collection of L{GES.Clip}.
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]