[pitivi] viewer: Add overlay stack and MoveScaleOverlay.
- From: Thibault Saunier <tsaunier src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [pitivi] viewer: Add overlay stack and MoveScaleOverlay.
- Date: Thu, 7 Apr 2016 12:39:30 +0000 (UTC)
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]