[pitivi] Implemented easy clip alignment



commit f40805b9e82a7b72f0cbf042bb348db802a0661f
Author: Jackson Eickhoff <jacksoneick gmail com>
Date:   Mon Apr 27 22:50:57 2020 -0500

    Implemented easy clip alignment
    
    Previously, there was no way to easily align clips inside
    or outside of the view.
    
    There existed alignment settings for title clips, but no easy way
    to align video clips.
    
    To address this issue, a new AlignmentEditor widget was made and placed
    in the transformation properties expander.
    
    Fixes #2279

 data/ui/cliptransformation.ui       |  60 +++++++++----
 pitivi/clip_properties/alignment.py | 165 ++++++++++++++++++++++++++++++++++++
 pitivi/clipproperties.py            |  41 +++++----
 3 files changed, 232 insertions(+), 34 deletions(-)
---
diff --git a/data/ui/cliptransformation.ui b/data/ui/cliptransformation.ui
index fe91f56c..337d3bed 100644
--- a/data/ui/cliptransformation.ui
+++ b/data/ui/cliptransformation.ui
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<!-- Generated with glade 3.20.0 -->
+<!-- Generated with glade 3.22.2 -->
 <interface>
   <requires lib="gtk+" version="3.10"/>
   <object class="GtkActionGroup" id="actiongroup1"/>
@@ -42,11 +42,12 @@
     <property name="margin_bottom">6</property>
     <property name="row_spacing">6</property>
     <property name="column_spacing">6</property>
-    <property name="column_homogeneous">True</property>
+    <property name="row_homogeneous">True</property>
     <child>
       <object class="GtkLabel" id="label11">
         <property name="visible">True</property>
         <property name="can_focus">False</property>
+        <property name="halign">end</property>
         <property name="label" translatable="yes">Y:</property>
         <property name="xalign">0</property>
       </object>
@@ -87,22 +88,11 @@
         <property name="width">3</property>
       </packing>
     </child>
-    <child>
-      <object class="GtkLabel" id="label4">
-        <property name="visible">True</property>
-        <property name="can_focus">False</property>
-        <property name="label" translatable="yes">Width:</property>
-        <property name="xalign">0</property>
-      </object>
-      <packing>
-        <property name="left_attach">0</property>
-        <property name="top_attach">2</property>
-      </packing>
-    </child>
     <child>
       <object class="GtkLabel" id="label5">
         <property name="visible">True</property>
         <property name="can_focus">False</property>
+        <property name="halign">end</property>
         <property name="label" translatable="yes">Height:</property>
         <property name="xalign">0</property>
       </object>
@@ -146,6 +136,7 @@
       <object class="GtkLabel" id="label10">
         <property name="visible">True</property>
         <property name="can_focus">False</property>
+        <property name="halign">end</property>
         <property name="label" translatable="yes">X:</property>
         <property name="xalign">0</property>
       </object>
@@ -158,10 +149,11 @@
       <object class="GtkButtonBox">
         <property name="visible">True</property>
         <property name="can_focus">False</property>
+        <property name="halign">center</property>
         <property name="layout_style">expand</property>
         <child>
           <object class="GtkButton" id="prev_keyframe_button">
-            <property name="label" translatable="no">&lt;</property>
+            <property name="label">&lt;</property>
             <property name="visible">True</property>
             <property name="can_focus">True</property>
             <property name="focus_on_click">False</property>
@@ -177,7 +169,7 @@
         </child>
         <child>
           <object class="GtkButton" id="next_keyframe_button">
-            <property name="label" translatable="no">&gt;</property>
+            <property name="label">&gt;</property>
             <property name="visible">True</property>
             <property name="can_focus">True</property>
             <property name="focus_on_click">False</property>
@@ -193,7 +185,7 @@
         </child>
         <child>
           <object class="GtkToggleButton" id="activate_keyframes_button">
-            <property name="label" translatable="no">◇</property>
+            <property name="label">◇</property>
             <property name="visible">True</property>
             <property name="can_focus">True</property>
             <property name="focus_on_click">False</property>
@@ -226,7 +218,39 @@
       <packing>
         <property name="left_attach">0</property>
         <property name="top_attach">4</property>
-        <property name="width">4</property>
+        <property name="width">5</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkLabel" id="label4">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="halign">end</property>
+        <property name="label" translatable="yes">Width:</property>
+        <property name="xalign">0</property>
+      </object>
+      <packing>
+        <property name="left_attach">0</property>
+        <property name="top_attach">2</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkBox" id="clip_alignment">
+        <property name="width_request">175</property>
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="margin_left">25</property>
+        <property name="margin_right">25</property>
+        <property name="margin_top">25</property>
+        <property name="margin_bottom">25</property>
+        <child>
+          <placeholder/>
+        </child>
+      </object>
+      <packing>
+        <property name="left_attach">4</property>
+        <property name="top_attach">0</property>
+        <property name="height">4</property>
       </packing>
     </child>
   </object>
diff --git a/pitivi/clip_properties/alignment.py b/pitivi/clip_properties/alignment.py
new file mode 100644
index 00000000..b890c2ab
--- /dev/null
+++ b/pitivi/clip_properties/alignment.py
@@ -0,0 +1,165 @@
+# -*- coding: utf-8 -*-
+# Pitivi video editor
+# Copyright (c) 2020, Jackson Eickhoff <jacksoneick gmail com>
+# Copyright (c) 2020, Tanner Skelton <tskelton huskers unl edu>
+# Copyright (c) 2020, Cordell Rhoads <rhoadscordell7 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, see <http://www.gnu.org/licenses/>.
+from gi.repository import Gdk
+from gi.repository import GObject
+from gi.repository import Gtk
+
+from pitivi.utils.loggable import Loggable
+
+
+class AlignmentEditor(Gtk.EventBox, Loggable):
+    """Widget for aligning a video clip.
+
+    Attributes:
+        app (Pitivi): The app.
+        _project (Project): The project.
+    """
+
+    __gtype_name__ = "AlignmentEditor"
+
+    __gsignals__ = {
+        "align": (GObject.SignalFlags.RUN_LAST, None, ()),
+    }
+
+    def __init__(self):
+        Gtk.EventBox.__init__(self)
+        Loggable.__init__(self)
+        self._hovered_box = None
+
+        self.connect("button-release-event", self._button_release_event_cb)
+        self.connect("motion-notify-event", self._motion_notify_event_cb)
+        self.connect("leave-notify-event", self._leave_notify_event_cb)
+        self.add_events(Gdk.EventMask.POINTER_MOTION_MASK)
+
+    def _leave_notify_event_cb(self, unused_widget, _):
+        self._hovered_box = None
+        self.queue_draw()
+
+    def _button_release_event_cb(self, widget, event):
+        if not self._hovered_box:
+            return
+        self.emit("align")
+
+    def get_clip_position(self, project, source):
+        """Returns corresponding clip position in the viewer."""
+        clip_width = source.get_child_property("width").value
+        clip_height = source.get_child_property("height").value
+        x = self.__calculate_clip_position(self._hovered_box[0], clip_width, project.videowidth)
+        y = self.__calculate_clip_position(self._hovered_box[1], clip_height, project.videoheight)
+
+        return x, y
+
+    def __calculate_clip_position(self, index, clip_size, project_size):
+        if index == 0:
+            coordinate = -clip_size
+        elif index == 1:
+            coordinate = -clip_size / 2
+        elif index == 2:
+            coordinate = 0
+        elif index == 3:
+            coordinate = project_size / 2 - clip_size / 2
+        elif index == 4:
+            coordinate = project_size - clip_size
+        elif index == 5:
+            coordinate = project_size - clip_size / 2
+        elif index == 6:
+            coordinate = project_size
+        else:
+            coordinate = 0
+
+        return int(coordinate)
+
+    def _motion_notify_event_cb(self, widget, event):
+        hovered_box = self._get_box(event.x, event.y)
+        if hovered_box != self._hovered_box:
+            self._hovered_box = hovered_box
+            self.queue_draw()
+
+    def get_used_size(self):
+        """Returns the size used for drawing.
+
+        For drawing pixel perfect lines, we need to be able to divide
+        the width and height between four boxes and three 1px lines,
+        such that the box size is an int value:
+
+            box_size + 1 + box_size + 1 + box_size + 1 + box_size
+        """
+        box_width = ((self.get_allocated_width() - 3) // 4)
+        box_height = ((self.get_allocated_height() - 3) // 4)
+        width = box_width * 4 + 3
+        height = box_height * 4 + 3
+        return width, height, box_width, box_height
+
+    def do_draw(self, cr):
+        width, height, box_width, box_height = self.get_used_size()
+
+        self._draw_frame(cr, width, height)
+
+        # Highlight the box that the cursor is hovering over
+        if self._hovered_box:
+            x = width / 8 * self._hovered_box[0]
+            y = height / 8 * self._hovered_box[1]
+            cr.rectangle(x, y, box_width, box_height)
+
+            color = self.get_style_context().get_color(Gtk.StateFlags.LINK)
+            cr.set_source_rgba(color.red, color.green, color.blue, 0.7)
+            cr.set_line_width(1)
+            cr.fill()
+            cr.stroke()
+
+    def _draw_frame(self, cr, width, height):
+        # How far the line should be displaced from the edge of the widget
+        line_offset_x = (width - 3) / 4 + 0.5
+        line_offset_y = (height - 3) / 4 + 0.5
+
+        cr.move_to(0, line_offset_y)
+        cr.line_to(width, line_offset_y)
+
+        cr.move_to(0, height - line_offset_y)
+        cr.line_to(width, height - line_offset_y)
+
+        cr.move_to(line_offset_x, 0)
+        cr.line_to(line_offset_x, height)
+
+        cr.move_to(width - line_offset_x, 0)
+        cr.line_to(width - line_offset_x, height)
+
+        color = self.get_style_context().get_color(Gtk.StateFlags.ACTIVE)
+        cr.set_source_rgba(color.red, color.green, color.blue, 0.6)
+        cr.set_line_width(1)
+        cr.stroke()
+
+        # cr.rectangle(line_offset_x + 0.5, line_offset_y + 0.5, width - line_offset_x * 2 - 1, height - 
line_offset_y * 2 - 1)
+        cr.rectangle(line_offset_x, line_offset_y, width - line_offset_x * 2, height - line_offset_y * 2)
+
+        cr.set_source_rgba(color.red, color.green, color.blue, color.alpha)
+        cr.set_line_width(2)
+        cr.stroke()
+
+    def _get_box(self, x, y):
+        """Returns the box containing the specified pixel."""
+        width, height, _, _ = self.get_used_size()
+
+        box_x = int(x * 7 / width)
+        box_y = int(y * 7 / height)
+
+        if box_x < 0 or box_x > 6 or box_y < 0 or box_y > 6:
+            return None
+
+        return box_x, box_y
diff --git a/pitivi/clipproperties.py b/pitivi/clipproperties.py
index c3c6e181..f493b7f0 100644
--- a/pitivi/clipproperties.py
+++ b/pitivi/clipproperties.py
@@ -27,6 +27,7 @@ from gi.repository import GstController
 from gi.repository import Gtk
 from gi.repository import Pango
 
+from pitivi.clip_properties.alignment import AlignmentEditor
 from pitivi.clip_properties.color import ColorProperties
 from pitivi.clip_properties.title import TitleProperties
 from pitivi.configure import get_ui_dir
@@ -658,9 +659,6 @@ class EffectProperties(Gtk.Expander, Loggable):
 class TransformationProperties(Gtk.Expander, Loggable):
     """Widget for configuring the placement and size of the clip."""
 
-    __signals__ = {
-        'selection-changed': []}
-
     def __init__(self, app):
         Gtk.Expander.__init__(self)
         Loggable.__init__(self)
@@ -672,15 +670,23 @@ class TransformationProperties(Gtk.Expander, Loggable):
         self.spin_buttons = {}
         self.spin_buttons_handler_ids = {}
         self.set_label(_("Transformation"))
+        self.set_expanded(True)
 
         self.builder = Gtk.Builder()
         self.builder.add_from_file(os.path.join(get_ui_dir(),
                                                 "cliptransformation.ui"))
+
+        alignment_editor_container = self.builder.get_object("clip_alignment")
+        self.alignment_editor = AlignmentEditor()
+        self.alignment_editor.connect("align", self.__alignment_editor_align_cb)
+        alignment_editor_container.pack_start(self.alignment_editor, True, True, 0)
+
         self.__control_bindings = {}
         # Used to make sure self.__control_bindings_changed doesn't get called
         # when bindings are changed from this class
         self.__own_bindings_change = False
         self.add(self.builder.get_object("transform_box"))
+
         self._init_buttons()
         self.show_all()
         self.hide()
@@ -707,6 +713,15 @@ class TransformationProperties(Gtk.Expander, Loggable):
     def __project_closed_cb(self, unused_project_manager, unused_project):
         self._project = None
 
+    def __alignment_editor_align_cb(self, widget):
+        """Callback method to align a clip from the AlignmentEditor widget."""
+        x, y = self.alignment_editor.get_clip_position(self._project, self.source)
+        with self.app.action_log.started("Position change",
+                                         
finalizing_action=CommitTimelineFinalizingAction(self._project.pipeline),
+                                         toplevel=True):
+            self.__set_prop("posx", x)
+            self.__set_prop("posy", y)
+
     def _init_buttons(self):
         clear_button = self.builder.get_object("clear_button")
         clear_button.connect("clicked", self._default_values_cb)
@@ -936,22 +951,13 @@ class TransformationProperties(Gtk.Expander, Loggable):
                     return
                 source_position = position - start + in_point
 
-                with self.app.action_log.started(
-                        "Transformation property change",
-                        finalizing_action=CommitTimelineFinalizingAction(
-                            self._project.pipeline),
-                        toplevel=True):
-                    self.__control_bindings[prop].props.control_source.set(
-                        source_position, value)
+                self.__control_bindings[prop].props.control_source.set(
+                    source_position, value)
             except PipelineError:
                 self.warning("Could not get pipeline position")
                 return
         else:
-            with self.app.action_log.started("Transformation property change",
-                                             finalizing_action=CommitTimelineFinalizingAction(
-                                                 self._project.pipeline),
-                                             toplevel=True):
-                self.source.set_child_property(prop, value)
+            self.source.set_child_property(prop, value)
 
     def __setup_spin_button(self, widget_name, property_name):
         """Creates a SpinButton for editing a property value."""
@@ -973,7 +979,10 @@ class TransformationProperties(Gtk.Expander, Loggable):
             return
 
         if value != cvalue:
-            self.__set_prop(prop, value)
+            with self.app.action_log.started("Transformation property change",
+                                             
finalizing_action=CommitTimelineFinalizingAction(self._project.pipeline),
+                                             toplevel=True):
+                self.__set_prop(prop, value)
             self.app.gui.editor.viewer.overlay_stack.update(self.source)
 
     def __set_source(self, source):


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