[pitivi] viewer: Add overlay stack and MoveScaleOverlay.



commit 82533c1eece15dfbbbaf0b0cc30cd079675e47c4
Author: Lubosz Sarnecki <lubosz sarnecki collabora co uk>
Date:   Fri Jan 29 15:41:52 2016 +0100

    viewer: Add overlay stack and MoveScaleOverlay.
    
    Differential Revision: https://phabricator.freedesktop.org/D724
    
    Review changes:
    
    viewer: reorder imports
    clipproperties: Update overlays and selection
    
    movescaleoverlay:
    
    * make python < 3.5 compatible by not joining dicts
    * remove redundancy
    * replace handle index strings by enums.
    * refactor handle index usage
    * do not unpack partial function values for python < 3.5
    * do not use enum class for python < 3.4
    * rename Handle.name to Handle.placement.
    
    overlaystack:
    
    * reorder imports
    * change remaining np imports to numpy
    * make class members uppercase and called by Class. instead of self.
    * Add better comments
    * Fix bug where cursor did not reset when releasing the mouse button outside of the viewer.
    * Do not shadow python buildin "type" in set_cursor()

 pitivi/clipproperties.py            |    2 +
 pitivi/viewer/move_scale_overlay.py |  527 +++++++++++++++++++++++++++++++++++
 pitivi/viewer/overlay.py            |   74 +++++
 pitivi/viewer/overlay_stack.py      |  154 ++++++++++
 pitivi/viewer/viewer.py             |  144 +---------
 5 files changed, 765 insertions(+), 136 deletions(-)
---
diff --git a/pitivi/clipproperties.py b/pitivi/clipproperties.py
index 815fde0..abe0fe3 100644
--- a/pitivi/clipproperties.py
+++ b/pitivi/clipproperties.py
@@ -629,6 +629,7 @@ class TransformationProperties(Gtk.Expander, Loggable):
             self.source.set_child_property(prop, value)
             self.app.action_log.commit()
             self._project.pipeline.commit_timeline()
+            self.app.gui.viewer.target.overlay_stack.update(self.source)
 
     def __setSource(self, source):
         if self.source:
@@ -648,6 +649,7 @@ class TransformationProperties(Gtk.Expander, Loggable):
             if source:
                 self._selected_clip = clip
                 self.__setSource(source)
+                self.app.gui.viewer.target.overlay_stack.select(source)
                 self.show()
                 return
 
diff --git a/pitivi/viewer/move_scale_overlay.py b/pitivi/viewer/move_scale_overlay.py
new file mode 100644
index 0000000..54ea1b4
--- /dev/null
+++ b/pitivi/viewer/move_scale_overlay.py
@@ -0,0 +1,527 @@
+# -*- coding: utf-8 -*-
+# Pitivi video editor
+#
+#       pitivi/viewer/move_scale_overlay.py
+#
+# Copyright (c) 2016, Lubosz Sarnecki <lubosz sarnecki collabora co uk>
+#
+# 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 cairo
+import numpy
+from collections import OrderedDict
+from math import pi
+
+from gi.repository import Gdk
+
+from pitivi.viewer.overlay import Overlay
+
+
+class Edge:
+    top = 1
+    bottom = 2
+    left = 3
+    right = 4
+
+
+class Handle:
+    GLOW = 0.9
+    INITIAL_RADIUS = 15
+    MINIMAL_RADIUS = 5
+    CURSORS = {
+        (Edge.top, Edge.left): Gdk.CursorType.TOP_LEFT_CORNER,
+        (Edge.bottom, Edge.left): Gdk.CursorType.BOTTOM_LEFT_CORNER,
+        (Edge.bottom, Edge.right): Gdk.CursorType.BOTTOM_RIGHT_CORNER,
+        (Edge.top, Edge.right): Gdk.CursorType.TOP_RIGHT_CORNER,
+        (Edge.top,): Gdk.CursorType.TOP_SIDE,
+        (Edge.bottom,): Gdk.CursorType.BOTTOM_SIDE,
+        (Edge.left,): Gdk.CursorType.LEFT_SIDE,
+        (Edge.right,): Gdk.CursorType.RIGHT_SIDE
+    }
+
+    def __init__(self, overlay):
+        self.__radius = Handle.INITIAL_RADIUS
+        self.__clicked = False
+        self.__window_position = numpy.array([0, 0])
+        self.__translation = numpy.array([0, 0])
+        self.__click_position_compare = numpy.array([0, 0])
+        self.__click_position = numpy.array([0, 0])
+        self._opposite_position = numpy.array([0, 0])
+        self._opposite_to_handle = numpy.array([0, 0])
+        self._overlay = overlay
+        self.placement = ()
+        self.position = numpy.array([0, 0])
+        self.hovered = False
+        self.neighbours = []
+
+    def _get_minimal_box_size(self):
+        pass
+
+    def _needs_size_restriction(self, handle_position_compare, cursor_position_compare):
+        pass
+
+    def _update_neighbours(self):
+        pass
+
+    def _restrict(self, handle_to_cursor):
+        pass
+
+    def __update_window_position(self):
+        self.__window_position = (self.position + self.__translation) * self._overlay.stack.window_size
+
+    def __update_opposite(self):
+        self._opposite_to_handle = 2 * (self.position - self._overlay.get_center())
+        self._opposite_position = self.position - self._opposite_to_handle
+
+    def __init_neighbours(self):
+        for corner in self._overlay.corner_handles:
+            for edge in self.placement:
+                if edge in corner and corner != self.placement:
+                    self.neighbours.append(self._overlay.corner_handles[corner])
+
+    def _restrict_to_minimal_size(self, cursor_position):
+        minimal_size = self._get_minimal_box_size()
+        handle_to_opposite_sign = numpy.sign(self._opposite_to_handle)
+        minimal_size_handle_position = self._opposite_position + minimal_size * handle_to_opposite_sign
+        cursor_position_compare = cursor_position >= minimal_size_handle_position
+        handle_position_compare = handle_to_opposite_sign >= numpy.array([0, 0])
+
+        if self._needs_size_restriction(handle_position_compare, cursor_position_compare):
+            cursor_position = minimal_size_handle_position
+        return cursor_position
+
+    def _get_normalized_minimal_size(self):
+        return 4 * Handle.MINIMAL_RADIUS / self._overlay.stack.window_size
+
+    def get_window_position(self):
+        return self.__window_position.tolist()
+
+    def get_source_position(self):
+        """
+        Returns a source translation when handles at TOP or LEFT are dragged.
+        The user is not translating here, but scaling.
+        This is needed to move the pivot point of the scale operation
+        from the TOP LEFT corner to the CENTER.
+        Returns None for handles where this is not needed
+        """
+        source_position = None
+        if Edge.top in self.placement or Edge.left in self.placement:
+            position_stream_size = self.position * self._overlay.project_size
+            # only x source translation changes
+            if self.placement in [(Edge.bottom, Edge.left), (Edge.left,)]:
+                position_stream_size[1] = 0
+            # only y source translation changes
+            elif self.placement in [(Edge.top, Edge.right), (Edge.top,)]:
+                position_stream_size[0] = 0
+            source_position = position_stream_size + self._overlay.click_source_position
+
+        return source_position
+
+    def set_placement(self, placement):
+        self.placement = placement
+        self.__init_neighbours()
+
+    def set_position(self, position):
+        self.position = position
+        self.__update_window_position()
+
+    def set_translation(self, translation):
+        self.__translation = translation
+        self.__update_window_position()
+
+    def set_x(self, x):
+        self.position = numpy.array([x, self.position[1]])
+        self.__update_window_position()
+
+    def set_y(self, y):
+        self.position = numpy.array([self.position[0], y])
+        self.__update_window_position()
+
+    def on_hover(self, cursor_pos):
+        distance = numpy.linalg.norm(self.__window_position - cursor_pos)
+
+        if distance < self.__radius:
+            self.hovered = True
+            self._overlay.stack.set_cursor(Handle.CURSORS[self.placement])
+        else:
+            self.hovered = False
+
+    def on_click(self):
+        self.__click_position = self.position
+        self.__update_opposite()
+
+    def on_drag(self, click_to_cursor):
+        handle_to_cursor = click_to_cursor + self.__click_position
+        restricted_handle_to_cursor = self._restrict(handle_to_cursor)
+
+        # Update box from motion event coordinates
+        self.set_position(restricted_handle_to_cursor)
+        self._update_neighbours()
+
+    def on_release(self):
+        self._opposite_position = None
+        self._opposite_to_handle = None
+
+    def restrict_radius_to_size(self, size):
+        if size < Handle.INITIAL_RADIUS * 5:
+            radius = size / 5
+            if radius < Handle.MINIMAL_RADIUS:
+                radius = Handle.MINIMAL_RADIUS
+            self.__radius = radius
+        else:
+            self.__radius = Handle.INITIAL_RADIUS
+
+    def reset_size(self):
+        self.__radius = Handle.INITIAL_RADIUS
+
+    def draw(self, cr):
+        if self.__clicked:
+            outer_color = .2
+            glow_radius = 1.08
+        elif self.hovered:
+            outer_color = .8
+            glow_radius = 1.08
+        else:
+            outer_color = .5
+            glow_radius = 1.01
+
+        cr.set_source_rgba(Handle.GLOW, Handle.GLOW, Handle.GLOW, 0.9)
+        x, y = self.get_window_position()
+        cr.arc(x, y, self.__radius * glow_radius, 0, 2 * pi)
+        cr.fill()
+
+        from_point = (x, y - self.__radius)
+        to_point = (x, y + self.__radius)
+        linear = cairo.LinearGradient(*(from_point + to_point))
+        linear.add_color_stop_rgba(0.00, outer_color, outer_color, outer_color, 1)
+        linear.add_color_stop_rgba(0.55, .1, .1, .1, 1)
+        linear.add_color_stop_rgba(0.65, .1, .1, .1, 1)
+        linear.add_color_stop_rgba(1.00, outer_color, outer_color, outer_color, 1)
+
+        cr.set_source(linear)
+
+        cr.arc(x, y, self.__radius * .9, 0, 2 * pi)
+        cr.fill()
+
+
+class CornerHandle(Handle):
+    def __init__(self, overlay):
+        Handle.__init__(self, overlay)
+
+    def __restrict_to_aspect_ratio(self, cursor_position):
+        opposite_to_cursor = cursor_position - self._opposite_position
+        opposite_to_cursor_ratio = opposite_to_cursor[0] / opposite_to_cursor[1]
+        opposite_to_handle_ratio = self._opposite_to_handle[0] / self._opposite_to_handle[1]
+        restricted_cursor_position = cursor_position
+
+        if abs(opposite_to_cursor_ratio) > abs(opposite_to_handle_ratio):
+            # adjust width
+            restricted_cursor_position[0] =\
+                self._opposite_position[0] + opposite_to_cursor[1] * opposite_to_handle_ratio
+        else:
+            # adjust height
+            restricted_cursor_position[1] =\
+                self._opposite_position[1] + opposite_to_cursor[0] / opposite_to_handle_ratio
+        return restricted_cursor_position
+
+    def _get_minimal_box_size(self):
+        # keep aspect when making a minimal box when corner is dragged
+        minimal_size = self._get_normalized_minimal_size()
+        if self._overlay.get_aspect_ratio() < 1.0:
+            minimal_size[1] = minimal_size[0] / self._overlay.get_aspect_ratio()
+        else:
+            minimal_size[0] = minimal_size[1] * self._overlay.get_aspect_ratio()
+        return minimal_size
+
+    def _needs_size_restriction(self, handle_position_compare, cursor_position_compare):
+        if (handle_position_compare != cursor_position_compare).any():
+            return True
+
+    def _update_neighbours(self):
+        for neighbour in self.neighbours:
+            if neighbour.placement[0] == self.placement[0]:
+                neighbour.set_y(self.position[1])
+            elif neighbour.placement[1] == self.placement[1]:
+                neighbour.set_x(self.position[0])
+
+    def _restrict(self, handle_to_cursor):
+        return self._restrict_to_minimal_size(
+            self.__restrict_to_aspect_ratio(handle_to_cursor))
+
+
+class EdgeHandle(Handle):
+    def __init__(self, overlay):
+        Handle.__init__(self, overlay)
+
+    def _get_minimal_box_size(self):
+        # nullify x / y in minimal box for edge handles
+        # required in minimal handle position calculation
+        minimal_size = self._get_normalized_minimal_size()
+        if self._opposite_to_handle[0] == 0:
+            # top bottom
+            minimal_size[0] = 0
+        else:
+            # left right
+            minimal_size[1] = 0
+        return minimal_size
+
+    def _needs_size_restriction(self, handle_position_compare, cursor_position_compare):
+        if self._opposite_to_handle[0] == 0:
+            # top bottom
+            if handle_position_compare[1] != cursor_position_compare[1]:
+                return True
+        else:
+            # left right
+            if handle_position_compare[0] != cursor_position_compare[0]:
+                return True
+
+    def _update_neighbours(self):
+        if self.placement[0] in (Edge.left, Edge.right):
+            for neighbour in self.neighbours:
+                neighbour.set_x(self.position[0])
+        elif self.placement[0] in (Edge.top, Edge.bottom):
+            for neighbour in self.neighbours:
+                neighbour.set_y(self.position[1])
+
+    def _restrict(self, handle_to_cursor):
+        return self._restrict_to_minimal_size(handle_to_cursor)
+
+
+class MoveScaleOverlay(Overlay):
+    """
+    Viewer overlays class for GESVideoSource transformations
+    """
+    def __init__(self, stack, source):
+        Overlay.__init__(self, stack, source)
+
+        self.__clicked_handle = None
+        self.__click_diagonal_sign = None
+        self.__box_hovered = False
+
+        self.hovered_handle = None
+
+        # Corner handles need to be ordered for drawing.
+        self.corner_handles = OrderedDict([
+            ((Edge.top, Edge.left), CornerHandle(self)),
+            ((Edge.bottom, Edge.left), CornerHandle(self)),
+            ((Edge.bottom, Edge.right), CornerHandle(self)),
+            ((Edge.top, Edge.right), CornerHandle(self))])
+
+        self.handles = self.corner_handles.copy()
+        for edge in range(1, 5):
+            self.handles[(edge,)] = EdgeHandle(self)
+
+        for key in self.handles:
+            self.handles[key].set_placement(key)
+
+        self.update_from_source()
+
+    def __get_source_position(self):
+        res_x, x = self._source.get_child_property("posx")
+        res_y, y = self._source.get_child_property("posy")
+        assert res_x and res_y
+        return numpy.array([x, y])
+
+    def __get_source_size(self):
+        res_x, x = self._source.get_child_property("width")
+        res_y, y = self._source.get_child_property("height")
+        assert res_x and res_y
+        return numpy.array([x, y])
+
+    def __get_normalized_source_position(self):
+        return self.__get_source_position() / self.project_size
+
+    def __set_source_position(self, position):
+        self._source.set_child_property("posx", int(position[0]))
+        self._source.set_child_property("posy", int(position[1]))
+
+    def __set_source_size(self, size):
+        self._source.set_child_property("width", int(size[0]))
+        self._source.set_child_property("height", int(size[1]))
+
+    def __get_size(self):
+        return numpy.array([self.__get_width(), self.__get_height()])
+
+    def __get_size_stream(self):
+        return self.__get_size() * self.project_size
+
+    def __get_height(self):
+        return self.handles[(Edge.bottom, Edge.left)].position[1] - self.handles[(Edge.top, 
Edge.left)].position[1]
+
+    def __get_width(self):
+        return self.handles[(Edge.bottom, Edge.right)].position[0] - self.handles[(Edge.bottom, 
Edge.left)].position[0]
+
+    def __set_size(self, size):
+        self.handles[(Edge.top, Edge.left)].position = numpy.array([0, 0])
+        self.handles[(Edge.bottom, Edge.left)].position = numpy.array([0, size[1]])
+        self.handles[(Edge.bottom, Edge.right)].position = numpy.array([size[0], size[1]])
+        self.handles[(Edge.top, Edge.right)].position = numpy.array([size[0], 0])
+        self.__update_egdes_from_corners()
+
+    def __set_position(self, position):
+        for handle in self.handles.values():
+            handle.set_translation(position)
+
+    def __reset_handle_sizes(self):
+        for handle in self.handles.values():
+            handle.reset_size()
+        self.__update_handle_sizes()
+
+    def __update_handle_sizes(self):
+        size = self.__get_size() * self.stack.window_size
+        smaller_size = numpy.amin(size)
+
+        for handle in self.handles.values():
+            handle.restrict_radius_to_size(smaller_size)
+
+    def __update_egdes_from_corners(self):
+        half_w = numpy.array([self.__get_width() * 0.5, 0])
+        half_h = numpy.array([0, self.__get_height() * 0.5])
+
+        self.handles[(Edge.left,)].set_position(self.handles[(Edge.top, Edge.left)].position + half_h)
+        self.handles[(Edge.right,)].set_position(self.handles[(Edge.top, Edge.right)].position + half_h)
+        self.handles[(Edge.bottom,)].set_position(self.handles[(Edge.bottom, Edge.left)].position + half_w)
+        self.handles[(Edge.top,)].set_position(self.handles[(Edge.top, Edge.right)].position - half_w)
+
+    def __draw_rectangle(self, cr):
+        for handle in self.corner_handles.values():
+            cr.line_to(*handle.get_window_position())
+        cr.line_to(*self.handles[(Edge.top, Edge.left)].get_window_position())
+
+    def get_center(self):
+        diagonal = self.handles[(Edge.bottom, Edge.right)].position - self.handles[(Edge.top, 
Edge.left)].position
+        return self.handles[(Edge.top, Edge.left)].position + (diagonal / 2)
+
+    def get_aspect_ratio(self):
+        size = self.__get_size()
+        return size[0] / size[1]
+
+    def on_button_press(self):
+        self.click_source_position = self.__get_source_position()
+        self.__clicked_handle = None
+
+        if self.hovered_handle:
+            self.hovered_handle.on_click()
+            self.__clicked_handle = self.hovered_handle
+        elif self.__box_hovered:
+            self._select()
+            self.stack.set_cursor("grabbing")
+            self.stack.selected_overlay = self
+        elif self._is_selected():
+            self._deselect()
+            self.hovered_handle = None
+
+    def on_button_release(self, cursor_position):
+        self.click_source_position = None
+        self.update_from_source()
+        self.on_hover(cursor_position)
+
+        if self.__clicked_handle:
+            if not self.__clicked_handle.hovered:
+                self.stack.reset_cursor()
+            self.__clicked_handle.on_release()
+            self.__clicked_handle = None
+        elif self._is_hovered():
+            self.stack.set_cursor("grab")
+
+        self.queue_draw()
+
+    def on_motion_notify(self, cursor_pos):
+        click_to_cursor = self.stack.get_normalized_drag_distance(cursor_pos)
+        if self.__clicked_handle:
+            # Resize Box / Use Handle
+            self.__clicked_handle.on_drag(click_to_cursor)
+            self.__update_egdes_from_corners()
+
+            # We only need to change translation coordinates in the source for resizing
+            # when handle does not return NULL for get_source_position
+            source_position = self.__clicked_handle.get_source_position()
+            if isinstance(source_position, numpy.ndarray):
+                self.__set_source_position(source_position)
+
+            self.__set_source_size(self.__get_size_stream())
+            self.__update_handle_sizes()
+        else:
+            # Move Box
+            stream_position = self.click_source_position + click_to_cursor * self.project_size
+            self.__set_position(stream_position / self.project_size)
+            self.__set_source_position(stream_position)
+        self.queue_draw()
+        self._commit()
+
+    def on_hover(self, cursor_pos):
+        # handles hover check
+        self.hovered_handle = None
+        if self._is_selected():
+            for handle in self.handles.values():
+                handle.on_hover(cursor_pos)
+                if handle.hovered:
+                    self.hovered_handle = handle
+            if self.hovered_handle:
+                self._hover()
+                self.queue_draw()
+                return True
+
+        # box hover check
+        source = self.__get_normalized_source_position()
+        cursor = self.stack.get_normalized_cursor_position(cursor_pos)
+
+        self.__box_hovered = False
+        if (source < cursor).all() and (cursor < source + self.__get_size()).all():
+            self.__box_hovered = True
+            self.stack.set_cursor("grab")
+            self._hover()
+        else:
+            self.__box_hovered = False
+            self.unhover()
+
+        self.queue_draw()
+        return self.__box_hovered
+
+    def update_from_source(self):
+        self.__set_size(self.__get_source_size() / self.project_size)
+        self.__set_position(self.__get_source_position() / self.project_size)
+        self.__reset_handle_sizes()
+        self.queue_draw()
+
+    def do_draw(self, cr):
+        if not self._is_selected() and not self._is_hovered():
+            return
+
+        cr.save()
+        # clear background
+        cr.set_operator(cairo.OPERATOR_OVER)
+        cr.set_source_rgba(0.0, 0.0, 0.0, 0.0)
+        cr.paint()
+
+        if self.__box_hovered:
+            brightness = 0.65
+        else:
+            brightness = 0.3
+
+        # clip away outer mask
+        self.__draw_rectangle(cr)
+        cr.clip()
+        cr.set_source_rgba(brightness, brightness, brightness, 0.6)
+        self.__draw_rectangle(cr)
+
+        cr.set_line_width(16)
+        cr.stroke()
+        cr.restore()
+
+        if self._is_selected():
+            for handle in self.handles.values():
+                handle.draw(cr)
diff --git a/pitivi/viewer/overlay.py b/pitivi/viewer/overlay.py
new file mode 100644
index 0000000..4873ed2
--- /dev/null
+++ b/pitivi/viewer/overlay.py
@@ -0,0 +1,74 @@
+# -*- coding: utf-8 -*-
+# Pitivi video editor
+#
+#       pitivi/viewer/overlay.py
+#
+# Copyright (c) 2016, Lubosz Sarnecki <lubosz sarnecki collabora co uk>
+#
+# 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 numpy
+
+from gi.repository import GES
+from gi.repository import Gtk
+
+from pitivi.utils.loggable import Loggable
+from pitivi.utils.timeline import SELECT
+
+
+class Overlay(Gtk.DrawingArea, Loggable):
+    """
+    Abstract class for viewer overlays.
+    """
+    def __init__(self, stack, source):
+        Gtk.DrawingArea.__init__(self)
+        Loggable.__init__(self)
+        self._source = source
+        self.click_source_position = None
+        self.stack = stack
+        project = stack.app.project_manager.current_project
+        self.project_size = numpy.array([project.videowidth, project.videoheight])
+
+    def _is_hovered(self):
+        return self.stack.hovered_overlay == self
+
+    def _is_selected(self):
+        return self.stack.selected_overlay == self
+
+    def _select(self):
+        self.stack.selected_overlay = self
+        self.stack.app.gui.timeline_ui.timeline.selection.setSelection([self._source], SELECT)
+        if isinstance(self._source, GES.TitleSource):
+            page = 2
+        elif isinstance(self._source, GES.VideoUriSource):
+            page = 0
+        else:
+            self.warning("Unknown clip type: %s", self._source)
+            return
+        self.stack.app.gui.context_tabs.set_current_page(page)
+
+    def _deselect(self):
+        self.stack.selected_overlay = None
+
+    def _hover(self):
+        self.stack.hovered_overlay = self
+
+    def unhover(self):
+        self.stack.hovered_overlay = None
+        self.queue_draw()
+
+    def _commit(self):
+        self.stack.app.project_manager.current_project.pipeline.commit_timeline()
diff --git a/pitivi/viewer/overlay_stack.py b/pitivi/viewer/overlay_stack.py
new file mode 100644
index 0000000..9495bf0
--- /dev/null
+++ b/pitivi/viewer/overlay_stack.py
@@ -0,0 +1,154 @@
+# -*- coding: utf-8 -*-
+# Pitivi video editor
+#
+#       pitivi/viewer/overlay_stack.py
+#
+# Copyright (c) 2016, Lubosz Sarnecki <lubosz sarnecki collabora co uk>
+#
+# 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 numpy
+
+from gi.repository import GES
+from gi.repository import Gdk
+from gi.repository import Gtk
+
+from pitivi.viewer.move_scale_overlay import MoveScaleOverlay
+
+
+class OverlayStack(Gtk.Overlay):
+    def __init__(self, app, sink_widget):
+        Gtk.Overlay.__init__(self)
+        self.__overlays = {}
+        self.__visible_overlays = []
+        self.app = app
+        self.window_size = numpy.array([1, 1])
+        self.click_position = None
+        self.hovered_overlay = None
+        self.selected_overlay = None
+        self.add_events(Gdk.EventMask.BUTTON_PRESS_MASK |
+                        Gdk.EventMask.BUTTON_RELEASE_MASK |
+                        Gdk.EventMask.POINTER_MOTION_MASK |
+                        Gdk.EventMask.SCROLL_MASK |
+                        Gdk.EventMask.ENTER_NOTIFY_MASK |
+                        Gdk.EventMask.LEAVE_NOTIFY_MASK |
+                        Gdk.EventMask.ALL_EVENTS_MASK)
+        self.add(sink_widget)
+        self.connect("size-allocate", self.__on_size_allocate)
+
+    def __on_size_allocate(self, widget, rectangle):
+        self.window_size = numpy.array([rectangle.width,
+                                        rectangle.height])
+        for overlay in self.__overlays.values():
+            overlay.update_from_source()
+
+    def __create_overlay_for_source(self, source):
+        overlay = MoveScaleOverlay(self, source)
+
+        self.add_overlay(overlay)
+        self.__overlays[source] = overlay
+
+    def do_event(self, event):
+        if event.type == Gdk.EventType.BUTTON_RELEASE:
+            cursor_position = numpy.array([event.x, event.y])
+            # reset the cursor if we are outside of the viewer
+            self.click_position = None
+            if (cursor_position < numpy.zeros(2)).any() or (cursor_position > self.window_size).any():
+                self.reset_cursor()
+                return
+            if self.selected_overlay:
+                self.selected_overlay.on_button_release(cursor_position)
+        elif event.type == Gdk.EventType.LEAVE_NOTIFY and event.mode == Gdk.CrossingMode.NORMAL:
+            # If we have a click position, the user is dragging, so we don't want to lose focus and return
+            if isinstance(self.click_position, numpy.ndarray):
+                return
+            for overlay in self.__overlays.values():
+                overlay.unhover()
+            self.reset_cursor()
+        elif event.type == Gdk.EventType.BUTTON_PRESS:
+            self.click_position = numpy.array([event.x, event.y])
+            if self.hovered_overlay:
+                self.hovered_overlay.on_button_press()
+            elif self.selected_overlay:
+                self.selected_overlay.on_button_press()
+        elif event.type == Gdk.EventType.MOTION_NOTIFY:
+            cursor_position = numpy.array([event.x, event.y])
+
+            if isinstance(self.click_position, numpy.ndarray):
+                if self.selected_overlay:
+                    self.selected_overlay.on_motion_notify(cursor_position)
+            else:
+
+                # Prioritize Handles
+                if isinstance(self.selected_overlay, MoveScaleOverlay):
+                    if self.selected_overlay.on_hover(cursor_position):
+                        if self.selected_overlay.hovered_handle:
+                            self.hovered_overlay = self.selected_overlay
+                            return
+
+                for overlay in self.__visible_overlays:
+                    if overlay.on_hover(cursor_position):
+                        self.hovered_overlay = overlay
+                        break
+                if not self.hovered_overlay:
+                    self.reset_cursor()
+        elif event.type == Gdk.EventType.SCROLL:
+            # TODO: Viewer zoom
+            pass
+        return True
+
+    def set_current_sources(self, sources):
+        self.__visible_overlays = []
+        # check if source has instanced viewer
+        for source in sources:
+            if source not in self.__overlays.keys():
+                self.__create_overlay_for_source(source)
+            self.__visible_overlays.append(self.__overlays[source])
+        # check if viewer should be visible
+        for source in self.__overlays.keys():
+            if source in sources:
+                self.__overlays[source].show()
+            else:
+                self.__overlays[source].hide()
+
+    def update(self, source):
+        self.__overlays[source].update_from_source()
+
+    def select(self, source):
+        if source not in self.__overlays.keys():
+            self.__create_overlay_for_source(source)
+        self.selected_overlay = self.__overlays[source]
+        self.selected_overlay.queue_draw()
+
+    def set_cursor(self, name):
+        display = Gdk.Display.get_default()
+        if isinstance(name, Gdk.CursorType):
+            cursor = Gdk.Cursor.new_for_display(display, name)
+        else:
+            cursor = Gdk.Cursor.new_from_name(display, name)
+        self.app.gui.get_window().set_cursor(cursor)
+
+    def reset_cursor(self):
+        self.app.gui.get_window().set_cursor(None)
+
+    def get_drag_distance(self, cursor_position):
+        return cursor_position - self.click_position
+
+    def get_normalized_drag_distance(self, cursor_position):
+        return self.get_drag_distance(cursor_position) / self.window_size
+
+    def get_normalized_cursor_position(self, cursor_position):
+        return cursor_position / self.window_size
diff --git a/pitivi/viewer/viewer.py b/pitivi/viewer/viewer.py
index 831ec1b..dce04fa 100644
--- a/pitivi/viewer/viewer.py
+++ b/pitivi/viewer/viewer.py
@@ -34,6 +34,7 @@ from pitivi.utils.misc import format_ns
 from pitivi.utils.pipeline import AssetPipeline
 from pitivi.utils.ui import SPACING
 from pitivi.utils.widgets import TimeWidget
+from pitivi.viewer.overlay_stack import OverlayStack
 
 GlobalSettings.addConfigSection("viewer")
 GlobalSettings.addConfigOption("viewerDocked", section="viewer",
@@ -63,7 +64,6 @@ GlobalSettings.addConfigOption("pointColor", section="viewer",
 
 
 class ViewerContainer(Gtk.Box, Loggable):
-
     """
     A wiget holding a viewer and the controls.
 
@@ -484,135 +484,7 @@ class ViewerContainer(Gtk.Box, Loggable):
             self.system.uninhibitScreensaver(self.INHIBIT_REASON)
 
 
-class TransformationBox(Gtk.EventBox, Loggable):
-    def __init__(self, app):
-        Gtk.EventBox.__init__(self)
-        Loggable.__init__(self)
-
-        self.app = app
-        self.__editSource = None
-        self.__startDraggingPosition = None
-        self.__startEditSourcePosition = None
-
-        self.add_events(Gdk.EventMask.SCROLL_MASK)
-
-    def __setupEditSource(self):
-        if not self.app:
-            return
-
-        if self.__editSource:
-            return
-        elif self.app.project_manager.current_project.pipeline.getState() != Gst.State.PAUSED:
-            return
-
-        try:
-            position = self.app.project_manager.current_project.pipeline.getPosition()
-        except:
-            return False
-
-        self.__editSource = None
-        selection = self.app.project_manager.current_project.timeline.ui.selection
-        selected_videoelements = selection.getSelectedTrackElementsAtPosition(position,
-                                                                              GES.VideoSource,
-                                                                              GES.TrackType.VIDEO)
-        if len(selected_videoelements) == 1:
-            self.__editSource = selected_videoelements[0]
-
-    def do_event(self, event):
-        if event.type == Gdk.EventType.ENTER_NOTIFY and event.mode == Gdk.CrossingMode.NORMAL:
-            self.__setupEditSource()
-            if self.__editSource:
-                self.get_window().set_cursor(Gdk.Cursor.new(Gdk.CursorType.HAND1))
-        elif event.type == Gdk.EventType.BUTTON_RELEASE:
-            self.__startDraggingPosition = None
-        elif event.type == Gdk.EventType.LEAVE_NOTIFY and event.mode == Gdk.CrossingMode.NORMAL:
-            self.get_window().set_cursor(None)
-            self.__startDraggingPosition = None
-            self.__editSource = None
-            self.__startEditSourcePosition = None
-        elif event.type == Gdk.EventType.BUTTON_PRESS:
-            self.__setupEditSource()
-            if self.__editSource:
-                res_x, current_x = self.__editSource.get_child_property("posx")
-                res_y, current_y = self.__editSource.get_child_property("posy")
-
-                if res_x and res_y:
-                    event_widget = Gtk.get_event_widget(event)
-                    x, y = event_widget.translate_coordinates(self, event.x, event.y)
-                    self.__startEditSourcePosition = (current_x, current_y)
-                    self.__startDraggingPosition = (x, y)
-                    if type(self.__editSource) == GES.TitleSource:
-                        res_x, xpos = self.__editSource.get_child_property("xpos")
-                        res_y, ypos = self.__editSource.get_child_property("ypos")
-                        assert res_x
-                        assert res_y
-                        self.__startDraggingTitlePos = (xpos, ypos)
-        elif event.type == Gdk.EventType.MOTION_NOTIFY:
-            if self.__startDraggingPosition and self.__editSource:
-                event_widget = Gtk.get_event_widget(event)
-                x, y = event_widget.translate_coordinates(self, event.x, event.y)
-                delta_x = x - self.__startDraggingPosition[0]
-                delta_y = y - self.__startDraggingPosition[1]
-                if type(self.__editSource) == GES.TitleSource:
-                    self.__editSource.set_child_property("halignment", GES.TextHAlign.POSITION)
-                    self.__editSource.set_child_property("valignment", GES.TextVAlign.POSITION)
-                    alloc = self.get_allocation()
-                    delta_xpos = delta_x / alloc.width
-                    delta_ypos = delta_y / alloc.height
-                    xpos = max(0, min(self.__startDraggingTitlePos[0] + delta_xpos, 1))
-                    ypos = max(0, min(self.__startDraggingTitlePos[1] + delta_ypos, 1))
-                    self.__editSource.set_child_property("xpos", xpos)
-                    self.__editSource.set_child_property("ypos", ypos)
-                else:
-                    self.__editSource.set_child_property("posx",
-                                                         self.__startEditSourcePosition[0] +
-                                                         delta_x)
-
-                    self.__editSource.set_child_property("posy", self.__startEditSourcePosition[1] +
-                                                         delta_y)
-                self.app.project_manager.current_project.pipeline.commit_timeline()
-        elif event.type == Gdk.EventType.SCROLL:
-            if self.__editSource:
-                res, delta_x, delta_y = event.get_scroll_deltas()
-                if not res:
-                    res, direction = event.get_scroll_direction()
-                    if not res:
-                        self.error("Could not get scroll delta")
-                        return True
-
-                    if direction == Gdk.ScrollDirection.UP:
-                        delta_y = -1.0
-                    elif direction == Gdk.ScrollDirection.DOWN:
-                        delta_y = 1.0
-                    else:
-                        self.error("Could not handle %s scroll event" % direction)
-                        return True
-
-                delta_y = delta_y * -1.0
-                width = self.__editSource.get_child_property("width")[1]
-                height = self.__editSource.get_child_property("height")[1]
-                if event.get_state()[1] & Gdk.ModifierType.SHIFT_MASK:
-                    height += delta_y
-                elif event.get_state()[1] & Gdk.ModifierType.CONTROL_MASK:
-                    width += delta_y
-                else:
-                    width += delta_y
-                    if isinstance(self.__editSource, GES.VideoUriSource):
-                        wpercent = float(width) / 
float(self.__editSource.get_asset().get_stream_info().get_width())
-                        height = int(float(self.__editSource.get_asset().get_stream_info().get_height()) * 
float(wpercent))
-                    else:
-                        wpercent = float(width) / float(self.app.project_manager.current_project.videowidth)
-                        height = int(float(self.app.project_manager.current_project.videoheight) * 
float(wpercent))
-
-                self.__editSource.set_child_property("width", width)
-                self.__editSource.set_child_property("height", height)
-                self.app.project_manager.current_project.pipeline.commit_timeline()
-
-        return True
-
-
 class ViewerWidget(Gtk.AspectFrame, Loggable):
-
     """
     Widget for displaying a GStreamer video sink.
 
@@ -630,21 +502,21 @@ class ViewerWidget(Gtk.AspectFrame, Loggable):
 
         self._pipeline = pipeline
 
-        transformation_box = TransformationBox(app)
-        self.add(transformation_box)
-
         # We only work with a gtkglsink inside a glsinkbin
         sink = pipeline.video_sink
         try:
-            self.drawing_area = sink.props.sink.props.widget
+            sink_widget = sink.props.sink.props.widget
         except AttributeError:
-            self.drawing_area = sink.props.widget
-        self.drawing_area.show()
-        transformation_box.add(self.drawing_area)
+            sink_widget = sink.props.widget
+        sink_widget.show()
 
         # We keep the ViewerWidget hidden initially, or the desktop wallpaper
         # would show through the non-double-buffered widget!
 
+        # Assign Viewer Overlay via Gtk.Overlay
+        self.overlay_stack = OverlayStack(app, sink_widget)
+        self.add(self.overlay_stack)
+
     def setDisplayAspectRatio(self, ratio):
         self.set_property("ratio", float(ratio))
 



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