[pitivi] Implement SMPTE video transitions



commit c4296040ea154d15b14dba84bc594c9f56796e9e
Author: Jean-FranÃois Fortin Tam <nekohayo gmail com>
Date:   Wed Feb 1 14:20:32 2012 -0500

    Implement SMPTE video transitions

 pitivi/mainwindow.py     |   24 +++
 pitivi/timeline/track.py |   32 ++++-
 pitivi/transitions.py    |  360 ++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 412 insertions(+), 4 deletions(-)
---
diff --git a/pitivi/mainwindow.py b/pitivi/mainwindow.py
index a654e06..106fe0c 100644
--- a/pitivi/mainwindow.py
+++ b/pitivi/mainwindow.py
@@ -40,6 +40,7 @@ from pitivi.utils.loggable import Loggable
 from pitivi.utils.misc import in_devel
 from pitivi.settings import GlobalSettings
 from pitivi.effects import EffectListWidget
+from pitivi.transitions import TransitionsListWidget
 from pitivi.medialibrary import MediaLibraryWidget, MediaLibraryError
 
 from pitivi.utils.misc import show_user_manual
@@ -415,8 +416,11 @@ class PitiviMainWindow(gtk.Window, Loggable):
         # Second set of tabs
         self.context_tabs = BaseTabs(instance)
         self.clipconfig = ClipProperties(instance, self.uimanager)
+        self.trans_list = TransitionsListWidget(instance, self.uimanager)
         self.context_tabs.append_page(self.clipconfig, gtk.Label(_("Clip configuration")))
+        self.context_tabs.append_page(self.trans_list, gtk.Label(_("Transitions")))
         self.clipconfig.show()
+        self.trans_list.show()
 
         self.secondhpaned.pack2(self.context_tabs, resize=True, shrink=False)
         self.context_tabs.show()
@@ -478,6 +482,26 @@ class PitiviMainWindow(gtk.Window, Loggable):
         os.environ["PULSE_PROP_media.role"] = "production"
         os.environ["PULSE_PROP_application.icon_name"] = "pitivi"
 
+    def switchContextTab(self, tab=None):
+        """
+        Switch the tab being displayed on the second set of tabs,
+        depending on the context.
+
+        @param the name of the tab to switch to, or None to reset
+        """
+        if not tab:
+            page = 0
+        else:
+            tab = tab.lower()
+            if tab == "clip configuration":
+                page = 0
+            elif tab == "transitions":
+                page = 1
+            else:
+                self.debug("Invalid context tab switch requested")
+                return False
+        self.context_tabs.set_current_page(page)
+
     def setFullScreen(self, fullscreen):
         """ Toggle the fullscreen mode of the application """
         if fullscreen:
diff --git a/pitivi/timeline/track.py b/pitivi/timeline/track.py
index 8718377..37a0e49 100644
--- a/pitivi/timeline/track.py
+++ b/pitivi/timeline/track.py
@@ -130,6 +130,7 @@ class Selected (Signallable):
 
     def __init__(self):
         self._selected = False
+        self.movable = True
 
     def __nonzero__(self):
         """
@@ -191,12 +192,16 @@ class TrackObjectController(Controller):
         self._mousedown = Point(self._mousedown[0], 0)
 
     def drag_end(self, item, target, event):
+        if not self._view.movable:
+            return
         self.debug("Drag end")
         self._context.finish()
         self._context = None
         self._view.app.action_log.commit()
 
     def set_pos(self, item, pos):
+        if not self._view.movable:
+            return
         x, y = pos
         x = x + self._hadj.get_value()
 
@@ -233,6 +238,7 @@ class TrimHandle(View, goocanvas.Image, Loggable, Zoomable):
         self.app = instance
         self.element = element
         self.timeline = timeline
+        self.movable = True
         goocanvas.Image.__init__(self,
             pixbuf=TRIMBAR_PIXBUF,
             line_width=0,
@@ -322,6 +328,8 @@ class TrackObject(View, goocanvas.Group, Zoomable, Loggable):
         _handle_enter_leave = True
 
         def drag_start(self, item, target, event):
+            if not self._view.movable:
+                return
             point = self.from_item_event(item, event)
             TrackObjectController.drag_start(self, item, target, event)
 
@@ -368,6 +376,7 @@ class TrackObject(View, goocanvas.Group, Zoomable, Loggable):
         self.nameheight = 0
         self._element = None
         self._settings = None
+        self.movable = True
 
         self.bg = goocanvas.Rect(height=self.height, line_width=1)
 
@@ -542,8 +551,17 @@ class TrackObject(View, goocanvas.Group, Zoomable, Loggable):
     def _updateCb(self, track_object, start):
         self._update()
 
-    def selectedChangedCb(self, element, selected):
+    def selectedChangedCb(self, element, unused_selection):
+        # unused_selection is True only when no clip was selected before
+        # Note that element is a track.Selected object,
+        # whereas self.element is a GES object (ex: TrackVideoTransition)
         if element.selected:
+            if isinstance(self.element, ges.TrackTransition):
+                if isinstance(self.element, ges.TrackVideoTransition):
+                    self.app.gui.trans_list.activate(self.element)
+            else:
+                self.app.gui.trans_list.deactivate()
+                self.app.gui.switchContextTab()
             self._selec_indic.props.visibility = goocanvas.ITEM_VISIBLE
         else:
             self._selec_indic.props.visibility = goocanvas.ITEM_INVISIBLE
@@ -596,17 +614,23 @@ class TrackTransition(TrackObject):
     """
     def __init__(self, instance, element, track, timeline, utrack):
         TrackObject.__init__(self, instance, element, track, timeline, utrack)
-        for thing in (self.bg, self.name):
+        for thing in (self.bg, self._selec_indic, self.namebg, self.name):
             self.add_child(thing)
+        if isinstance(element, ges.TrackVideoTransition):
+            element.connect("notify::transition-type", self._changeVideoTransitionCb)
+        self.movable = False
 
     def _setElement(self, element):
-        # FIXME: add the transition name as the label
-        pass
+        if isinstance(element, ges.TrackVideoTransition):
+            self.name.props.text = element.props.transition_type.value_nick
 
     def _getColor(self):
         # Transitions are bright blue, independent of the user color settings
         return 0x0089CFF0
 
+    def _changeVideoTransitionCb(self, transition, unused_transition_type):
+        self.name.props.text = transition.props.transition_type.value_nick
+
 
 class TrackFileSource(TrackObject):
     """
diff --git a/pitivi/transitions.py b/pitivi/transitions.py
new file mode 100644
index 0000000..65fe38a
--- /dev/null
+++ b/pitivi/transitions.py
@@ -0,0 +1,360 @@
+# -*- coding: utf-8 -*-
+# PiTiVi , Non-linear video editor
+#
+#       transitions.py
+#
+# Copyright (c) 2012, Jean-FranÃois Fortin Tam <nekohayo gmail com>
+#
+# 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.
+
+import ges
+import gtk
+import os
+import gobject
+
+from gettext import gettext as _
+from xml.sax.saxutils import escape
+
+from pitivi.configure import get_pixmap_dir
+from pitivi.utils.loggable import Loggable
+from pitivi.utils.signal import Signallable
+from pitivi.utils.ui import SPACING, PADDING
+
+(COL_TRANSITION_ID,
+ COL_NAME_TEXT,
+ COL_DESC_TEXT,
+ COL_ICON) = range(4)
+
+
+class TransitionsListWidget(Signallable, gtk.VBox, Loggable):
+
+    def __init__(self, instance, uiman):
+        gtk.VBox.__init__(self)
+        Loggable.__init__(self)
+        Signallable.__init__(self)
+
+        self.app = instance
+        self.element = None
+        self._pixdir = os.path.join(get_pixmap_dir(), "transitions")
+        icon_theme = gtk.icon_theme_get_default()
+        self._question_icon = icon_theme.load_icon("dialog-question", 48, 0)
+
+        #Tooltip handling
+        self._current_transition_name = None
+        self._current_tooltip_icon = None
+
+        #Searchbox
+        self.searchbar = gtk.HBox()
+        self.searchbar.set_spacing(SPACING)
+        self.searchbar.set_border_width(3)  # Prevents being flush against the notebook
+        searchStr = gtk.Label(_("Search:"))
+        self.searchEntry = gtk.Entry()
+        self.searchEntry.set_icon_from_stock(gtk.ENTRY_ICON_SECONDARY, "gtk-clear")
+        self.searchbar.pack_start(searchStr, expand=False)
+        self.searchbar.pack_end(self.searchEntry, expand=True)
+
+        self.props_widgets = gtk.VBox()
+        borderTable = gtk.Table(rows=2, columns=3)
+
+        self.border_mode_normal = gtk.RadioButton(group=None, label=_("Normal"))
+        self.border_mode_loop = gtk.RadioButton(group=self.border_mode_normal, label=_("Loop"))
+        self.border_mode_normal.set_active(True)
+        self.borderScale = gtk.HScale()
+        self.borderScale.set_draw_value(False)
+
+        borderTable.attach(self.border_mode_normal, 0, 1, 0, 1, xoptions=gtk.FILL, yoptions=gtk.FILL)
+        borderTable.attach(self.border_mode_loop, 1, 2, 0, 1, xoptions=gtk.FILL, yoptions=gtk.FILL)
+        # The ypadding is a hack to make the slider widget align with the radiobuttons.
+        borderTable.attach(self.borderScale, 2, 3, 0, 2, ypadding=SPACING * 2)
+
+        self.invert_checkbox = gtk.CheckButton(_("Reverse direction"))
+        self.invert_checkbox.set_border_width(SPACING)
+
+        self.props_widgets.add(borderTable)
+        self.props_widgets.add(self.invert_checkbox)
+
+        # Set the default values
+        self._borderTypeChangedCb()
+
+        self.infobar = gtk.InfoBar()
+        txtlabel = gtk.Label()
+        txtlabel.set_padding(PADDING, PADDING)
+        txtlabel.set_line_wrap(True)
+        txtlabel.set_text(
+            _("Create a transition by overlapping two adjacent clips on the "
+                "same layer. Click the transition on the timeline to change "
+                "the transition type."))
+        self.infobar.add(txtlabel)
+        self.infobar.show_all()
+
+        self.storemodel = gtk.ListStore(str, str, str, gtk.gdk.Pixbuf)
+
+        self.iconview_scrollwin = gtk.ScrolledWindow()
+        self.iconview_scrollwin.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
+        self.iconview_scrollwin.set_shadow_type(gtk.SHADOW_ETCHED_IN)
+
+        self.iconview = gtk.IconView(self.storemodel)
+        self.iconview.set_pixbuf_column(COL_ICON)
+        # We don't show text because we have a searchbar and the names are ugly
+        #self.iconview.set_text_column(COL_NAME_TEXT)
+        self.iconview.set_item_width(48 + 10)
+        self.iconview_scrollwin.add(self.iconview)
+        self.iconview.set_property("has_tooltip", True)
+
+        self.searchEntry.connect("changed", self._searchEntryChangedCb)
+        self.searchEntry.connect("focus-in-event", self._searchEntryActivateCb)
+        self.searchEntry.connect("focus-out-event", self._searchEntryDeactivateCb)
+        self.searchEntry.connect("icon-press", self._searchEntryIconClickedCb)
+        self.iconview.connect("button-release-event", self._transitionSelectedCb)
+        self.iconview.connect("query-tooltip", self._queryTooltipCb)
+        self.borderScale.connect("value-changed", self._borderScaleCb)
+        self.invert_checkbox.connect("toggled", self._invertCheckboxCb)
+        self.border_mode_normal.connect("released", self._borderTypeChangedCb)
+        self.border_mode_loop.connect("released", self._borderTypeChangedCb)
+
+        # Speed-up startup by only checking available transitions on idle
+        gobject.idle_add(self._loadAvailableTransitionsCb)
+
+        self.pack_start(self.infobar, expand=False)
+        self.pack_start(self.searchbar, expand=False)
+        self.pack_start(self.iconview_scrollwin, expand=True)
+        self.pack_start(self.props_widgets, expand=False)
+
+        # Create the filterModel for searching
+        self.modelFilter = self.storemodel.filter_new()
+        self.iconview.set_model(self.modelFilter)
+
+        self.infobar.show()
+        self.iconview_scrollwin.show_all()
+        self.iconview.set_sensitive(False)
+        self.props_widgets.set_sensitive(False)
+        self.props_widgets.show_all()
+        self.searchbar.hide_all()
+
+# UI callbacks
+
+    def _transitionSelectedCb(self, unused_view, event):
+        if event.button == 1:
+            transition_id = int(self.getSelectedItem())
+            transition = self.available_transitions.get(transition_id)
+            self.debug("New transition type selected: %s" % transition)
+            if transition.value_nick == "crossfade":
+                self.props_widgets.set_sensitive(False)
+            else:
+                self.props_widgets.set_sensitive(True)
+            self.element.set_transition_type(transition)
+            self.app.current.seeker.flush()
+        return True
+
+    def _borderScaleCb(self, range_changed):
+        value = range_changed.get_value()
+        self.debug("User changed the border property to %s" % value)
+        self.element.set_border(int(value))
+        self.app.current.seeker.flush()
+
+    def _invertCheckboxCb(self, widget):
+        value = widget.get_active()
+        self.debug("User changed the invert property to %s" % value)
+        self.element.set_inverted(value)
+        self.app.current.seeker.flush()
+
+    def _borderTypeChangedCb(self, widget=None):
+        """
+        The "border" property in gstreamer is unlimited, but if you go over
+        25 thousand it "loops" the transition instead of smoothing it.
+        """
+        if widget == self.border_mode_loop:
+            self.borderScale.set_range(50000, 500000)
+            self.borderScale.clear_marks()
+            self.borderScale.add_mark(50000, gtk.POS_BOTTOM, _("Slow"))
+            self.borderScale.add_mark(200000, gtk.POS_BOTTOM, _("Fast"))
+            self.borderScale.add_mark(500000, gtk.POS_BOTTOM, _("Epileptic"))
+        else:
+            self.borderScale.set_range(0, 25000)
+            self.borderScale.clear_marks()
+            self.borderScale.add_mark(0, gtk.POS_BOTTOM, _("Sharp"))
+            self.borderScale.add_mark(25000, gtk.POS_BOTTOM, _("Smooth"))
+
+    def _searchEntryChangedCb(self, entry):
+        self.modelFilter.refilter()
+
+    def _searchEntryIconClickedCb(self, entry, unused, unsed1):
+        entry.set_text("")
+
+    def _searchEntryDeactivateCb(self, entry, event):
+        self.app.gui.setActionsSensitive("default", True)
+        self.app.gui.setActionsSensitive(['DeleteObj'], True)
+
+    def _searchEntryActivateCb(self, entry, event):
+        self.app.gui.setActionsSensitive("default", False)
+        self.app.gui.setActionsSensitive(['DeleteObj'], False)
+
+# GES callbacks
+
+    def _transitionTypeChangedCb(self, element, unused_prop):
+        transition = element.get_transition_type()
+        try:
+            self.iconview.disconnect_by_func(self._transitionSelectedCb)
+        except TypeError:
+            pass
+        finally:
+            self.selectTransition(transition)
+            self.iconview.connect("button-release-event", self._transitionSelectedCb)
+
+    def _borderChangedCb(self, element, unused_prop):
+        """
+        The "border" transition property changed in the backend. Update the UI.
+        """
+        value = element.get_border()
+        try:
+            self.borderScale.disconnect_by_func(self._borderScaleCb)
+        except TypeError:
+            pass
+        finally:
+            self.borderScale.set_value(float(value))
+            self.borderScale.connect("value-changed", self._borderScaleCb)
+
+    def _invertChangedCb(self, element, unused_prop):
+        """
+        The "invert" transition property changed in the backend. Update the UI.
+        """
+        value = element.is_inverted()
+        try:
+            self.invert_checkbox.disconnect_by_func(self._invertCheckboxCb)
+        except TypeError:
+            pass
+        finally:
+            self.invert_checkbox.set_active(value)
+            self.invert_checkbox.connect("toggled", self._invertCheckboxCb)
+
+# UI methods
+
+    def _loadAvailableTransitionsCb(self):
+        """
+        Get the list of transitions from GES and load the associated thumbnails.
+        """
+        # TODO: rewrite this method when GESRegistry exists
+        self.available_transitions = {}
+        # GES currently has transitions IDs up to 512
+        # Number 0 means "no transition", so we might just as well skip it.
+        for i in range(1, 513):
+            try:
+                transition = ges.VideoStandardTransitionType(i)
+            except ValueError:
+                # We hit a gap in the enum
+                pass
+            else:
+                self.available_transitions[transition.numerator] = transition
+                self.storemodel.append(\
+                    [transition.numerator,
+                    transition.value_nick,
+                    transition.value_name,
+                    self._getIcon(transition.value_nick)])
+
+        # Now that the UI is fully ready, enable searching
+        self.modelFilter.set_visible_func(self._setRowVisible, data=None)
+        # Alphabetical/name sorting instead of based on the ID number
+        #self.storemodel.set_sort_column_id(COL_NAME_TEXT, gtk.SORT_ASCENDING)
+
+    def activate(self, element):
+        """
+        Hide the infobar and make the transitions UI sensitive.
+        """
+        self.element = element
+        self.element.connect("notify::border", self._borderChangedCb)
+        self.element.connect("notify::invert", self._invertChangedCb)
+        self.element.connect("notify::type", self._transitionTypeChangedCb)
+        transition = element.get_transition_type()
+        if transition.value_nick == "crossfade":
+            self.props_widgets.set_sensitive(False)
+        else:
+            self.props_widgets.set_sensitive(True)
+        self.iconview.set_sensitive(True)
+        self.infobar.hide()
+        self.searchbar.show_all()
+        self.selectTransition(transition)
+        self.app.gui.switchContextTab("transitions")
+
+    def selectTransition(self, transition):
+        """
+        For a given transition type, select it in the iconview if available.
+        """
+        model = self.iconview.get_model()
+        for row in model:
+            if int(transition.numerator) == int(row[COL_TRANSITION_ID]):
+                path = model.get_path(row.iter)
+                self.iconview.select_path(path)
+                self.iconview.scroll_to_path(path, False, 0, 0)
+
+    def deactivate(self):
+        """
+        Show the infobar and make the transitions UI insensitive.
+        """
+        try:
+            self.element.disconnect_by_func(self._borderChangedCb)
+            self.element.disconnect_by_func(self._invertChangedCb)
+            self.element.disconnect_by_func(self._transitionTypeChangedCb)
+        except TypeError:
+            pass
+        self.iconview.unselect_all()
+        self.iconview.set_sensitive(False)
+        self.props_widgets.set_sensitive(False)
+        self.infobar.show()
+        self.searchbar.hide_all()
+
+    def _getIcon(self, transition_nick):
+        """
+        If available, return an icon pixbuf for a given transition nickname.
+        """
+        name = transition_nick + ".png"
+        icon = None
+        try:
+            icon = gtk.gdk.pixbuf_new_from_file(os.path.join(self._pixdir, name))
+        except:
+            icon = self._question_icon
+        return icon
+
+    def _queryTooltipCb(self, view, x, y, keyboard_mode, tooltip):
+        context = view.get_tooltip_context(x, y, keyboard_mode)
+        if context is None:
+            return False
+
+        view.set_tooltip_item(tooltip, context[1][0])
+
+        name = self.modelFilter.get_value(context[2], COL_TRANSITION_ID)
+        if self._current_transition_name != name:
+            self._current_transition_name = name
+            icon = self.modelFilter.get_value(context[2], COL_ICON)
+            self._current_tooltip_icon = icon
+
+        longname = escape(self.modelFilter.get_value(context[2], COL_NAME_TEXT).strip())
+        description = escape(self.modelFilter.get_value(context[2], COL_DESC_TEXT))
+        txt = "<b>%s:</b>\n%s" % (longname, description)
+        tooltip.set_markup(txt)
+        return True
+
+    def getSelectedItem(self):
+        path = self.iconview.get_selected_items()
+        return self.storemodel[path[0]][COL_TRANSITION_ID]
+
+    def _setRowVisible(self, model, iter, data):
+        """
+        Filters the icon view depending on the search results
+        """
+        text = self.searchEntry.get_text().lower()
+        return text in model.get_value(iter, COL_DESC_TEXT).lower() or\
+               text in model.get_value(iter, COL_NAME_TEXT).lower()
+        return False



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]