[pitivi] viewer: Add composition guidelines
- From: Alexandru Băluț <alexbalut src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [pitivi] viewer: Add composition guidelines
- Date: Sun, 3 May 2020 05:04:53 +0000 (UTC)
commit c24e0c14fbf8533535a16892ef302f4a1c15398e
Author: Daniel Rudebusch, Jessie Guo, and Jaden Goter <>
Date: Fri Apr 17 14:30:32 2020 -0500
viewer: Add composition guidelines
Composition guidelines are simple lines that help with framing and
position in an aesthetically pleasing way.
Added a new overlay class (that is not attached to a source) to draw the
composition guidelines. Also, add a menu in the viewer to select the
guidelines.
Fixes #2298
pitivi/timeline/timeline.py | 3 +
pitivi/viewer/guidelines.py | 179 ++++++++++++++++++++++++++++++++++++++++
pitivi/viewer/overlay_stack.py | 5 +-
pitivi/viewer/viewer.py | 34 +++++++-
po/POTFILES.in | 1 +
tests/common.py | 7 ++
tests/test_viewer_guidelines.py | 77 +++++++++++++++++
7 files changed, 303 insertions(+), 3 deletions(-)
---
diff --git a/pitivi/timeline/timeline.py b/pitivi/timeline/timeline.py
index c4ed37bb..db0d2eca 100644
--- a/pitivi/timeline/timeline.py
+++ b/pitivi/timeline/timeline.py
@@ -1814,6 +1814,9 @@ class TimelineContainer(Gtk.Grid, Zoomable, Loggable):
self.forward_one_second_action,
_("Seek forward one second"))
+ # Viewer actions.
+ self.timeline.layout.insert_action_group("viewer", self.app.gui.editor.viewer.action_group)
+
self.update_actions()
def _scroll_to_pixel(self, x):
diff --git a/pitivi/viewer/guidelines.py b/pitivi/viewer/guidelines.py
new file mode 100644
index 00000000..d6fd6b7f
--- /dev/null
+++ b/pitivi/viewer/guidelines.py
@@ -0,0 +1,179 @@
+# -*- coding: utf-8 -*-
+# Pitivi video editor
+# Copyright (c) 2020, Jaden Goter <jadengoter huskers unl edu>
+# Copyright (c) 2020, Jessie Guo <jessie guo huskers unl edu>
+# Copyright (c) 2020, Daniel Rudebusch <daniel rudebusch huskers unl edu>
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this program; if not, see <http://www.gnu.org/licenses/>.
+from enum import Enum
+from gettext import gettext as _
+
+from gi.repository import Gtk
+
+from pitivi.utils.ui import SPACING
+
+
+class Guideline(Enum):
+ """Guideline types."""
+
+ @staticmethod
+ def __three_by_three_draw_func(cr, width, height):
+ for i in range(1, 3):
+ cr.move_to(i * width / 3, 0)
+ cr.line_to(i * width / 3, height)
+ cr.move_to(0, i * height / 3)
+ cr.line_to(width, i * height / 3)
+
+ @staticmethod
+ def __vertical_horizontal_center_draw_func(cr, width, height):
+ cr.move_to(width / 2, 0)
+ cr.line_to(width / 2, height)
+ cr.move_to(0, height / 2)
+ cr.line_to(width, height / 2)
+
+ @staticmethod
+ def __diagonals_draw_func(cr, width, height):
+ cr.move_to(0, 0)
+ cr.line_to(width, height)
+ cr.move_to(width, 0)
+ cr.line_to(0, height)
+
+ three_by_three = (_("3 by 3"), __three_by_three_draw_func)
+ vertical_horizontal_center = (_("Vertical/Horizontal"), __vertical_horizontal_center_draw_func)
+ diagonals = (_("Diagonals"), __diagonals_draw_func)
+
+ def __init__(self, label, func):
+ self.label = label
+ self.draw_func = func
+
+
+class GuidelinesPopover(Gtk.Popover):
+ """A popover for controlling the visible composition guidelines.
+
+ Attributes:
+ overlay (GuidelinesOverlay): The managed overlay showing the guidelines.
+ switches (dict): Maps the Guideline types to Gtk.Switch widgets.
+ """
+
+ def __init__(self):
+ Gtk.Popover.__init__(self)
+
+ self.switches = dict()
+
+ self.overlay = GuidelinesOverlay()
+ self._create_ui()
+
+ self._last_guidelines = {Guideline.three_by_three}
+
+ def _create_ui(self):
+ grid = Gtk.Grid()
+ grid.props.row_spacing = SPACING
+ grid.props.column_spacing = SPACING
+ grid.props.margin = SPACING * 2
+
+ label = Gtk.Label(_("Composition Guidelines"))
+ label.props.wrap = True
+ grid.attach(label, 0, 0, 2, 1)
+
+ grid.attach(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL), 0, 1, 2, 1)
+
+ row = 1
+ for guideline in Guideline:
+ row += 1
+
+ label = Gtk.Label(guideline.label)
+ label.props.halign = Gtk.Align.START
+ label.props.wrap = True
+ label.props.xalign = 0
+ grid.attach(label, 0, row, 1, 1)
+
+ switch = Gtk.Switch()
+ switch.connect("state-set", self.__guideline_switch_state_set_cb, guideline)
+ grid.attach(switch, 1, row, 1, 1)
+
+ self.switches[guideline] = switch
+
+ grid.show_all()
+ self.add(grid)
+
+ def toggle(self):
+ """Toggle the visible guidelines on the managed overlay."""
+ # Keep a copy and restore it since the last active guidelines
+ # can be changed when the switches are toggled.
+ last_guidelines_copy = self._last_guidelines.copy()
+ try:
+ for guideline in last_guidelines_copy:
+ switch = self.switches[guideline]
+ switch.set_active(not switch.get_active())
+ finally:
+ self._last_guidelines = last_guidelines_copy
+
+ def __guideline_switch_state_set_cb(self, switch_widget, unused_parameter, guideline):
+ last_guidelines = {guideline
+ for guideline in Guideline
+ if self.switches[guideline].get_active()}
+ if last_guidelines:
+ self._last_guidelines = last_guidelines
+ if switch_widget.get_active():
+ self.overlay.add_guideline(guideline)
+ else:
+ self.overlay.remove_guideline(guideline)
+
+
+class GuidelinesOverlay(Gtk.DrawingArea):
+ """Overlay which draws the composition guidelines.
+
+ Attributes:
+ active_guidelines (set[Guideline]): The guidelines to be drawn.
+ """
+
+ def __init__(self):
+ Gtk.DrawingArea.__init__(self)
+
+ self.active_guidelines = set()
+
+ self.hide()
+ self.props.no_show_all = True
+
+ def add_guideline(self, guideline):
+ if guideline not in self.active_guidelines:
+ if not self.active_guidelines:
+ self.set_visible(True)
+ self.active_guidelines.add(guideline)
+ self.queue_draw()
+
+ def remove_guideline(self, guideline):
+ if guideline in self.active_guidelines:
+ self.active_guidelines.remove(guideline)
+ if not self.active_guidelines:
+ self.set_visible(False)
+ self.queue_draw()
+
+ def do_draw(self, cr):
+ width = self.get_allocated_width()
+ height = self.get_allocated_height()
+
+ # Draw black border.
+ cr.set_source_rgb(0, 0, 0)
+ cr.set_line_width(2)
+ for guideline in self.active_guidelines:
+ guideline.draw_func(cr, width, height)
+ cr.stroke()
+
+ # Draw blue line in middle.
+ cr.set_source_rgb(0.75, 1.0, 1.0)
+ cr.set_line_width(1)
+ for guideline in self.active_guidelines:
+ guideline.draw_func(cr, width, height)
+ cr.stroke()
diff --git a/pitivi/viewer/overlay_stack.py b/pitivi/viewer/overlay_stack.py
index 27a21700..fc0b1d95 100644
--- a/pitivi/viewer/overlay_stack.py
+++ b/pitivi/viewer/overlay_stack.py
@@ -28,7 +28,7 @@ from pitivi.viewer.title_overlay import TitleOverlay
class OverlayStack(Gtk.Overlay, Loggable):
"""Manager for the viewer overlays."""
- def __init__(self, app, sink_widget):
+ def __init__(self, app, sink_widget, guidelines_overlay):
Gtk.Overlay.__init__(self)
Loggable.__init__(self)
self.__overlays = {}
@@ -61,6 +61,9 @@ class OverlayStack(Gtk.Overlay, Loggable):
self.resize_status = Gtk.Label(name="resize_status")
self.revealer.add(self.resize_status)
self.add_overlay(self.revealer)
+
+ self.add_overlay(guidelines_overlay)
+
sink_widget.connect("size-allocate", self.__sink_widget_size_allocate_cb)
def __size_allocate_cb(self, widget, rectangle):
diff --git a/pitivi/viewer/viewer.py b/pitivi/viewer/viewer.py
index df5a7820..783d2f72 100644
--- a/pitivi/viewer/viewer.py
+++ b/pitivi/viewer/viewer.py
@@ -19,6 +19,7 @@ from gettext import gettext as _
from gi.repository import Gdk
from gi.repository import GES
+from gi.repository import Gio
from gi.repository import GLib
from gi.repository import GObject
from gi.repository import Gst
@@ -29,8 +30,10 @@ from pitivi.utils.loggable import Loggable
from pitivi.utils.pipeline import AssetPipeline
from pitivi.utils.ui import SPACING
from pitivi.utils.widgets import TimeWidget
+from pitivi.viewer.guidelines import GuidelinesPopover
from pitivi.viewer.overlay_stack import OverlayStack
+
GlobalSettings.add_config_section("viewer")
GlobalSettings.add_config_option("viewerDocked", section="viewer",
key="docked",
@@ -88,7 +91,9 @@ class ViewerContainer(Gtk.Box, Loggable):
self.target = None
self.overlay_stack = None
+ self.guidelines_popover = None
self._create_ui()
+ self._create_actions()
if not self.settings.viewerDocked:
self.undock()
@@ -148,7 +153,11 @@ class ViewerContainer(Gtk.Box, Loggable):
def __create_new_viewer(self):
_, sink_widget = self.project.pipeline.create_sink()
- self.overlay_stack = OverlayStack(self.app, sink_widget)
+ self.guidelines_popover = GuidelinesPopover()
+ self.guidelines_button.set_popover(self.guidelines_popover)
+ self.overlay_stack = OverlayStack(self.app,
+ sink_widget,
+ self.guidelines_popover.overlay)
self.target = ViewerWidget(self.overlay_stack)
self._reset_viewer_aspect_ratio(self.project)
@@ -241,9 +250,14 @@ class ViewerContainer(Gtk.Box, Loggable):
bbox.set_margin_right(SPACING)
self.pack_end(bbox, False, False, 0)
+ self.guidelines_button = Gtk.MenuButton.new()
+ self.guidelines_button.props.image = Gtk.Image.new_from_icon_name("view-grid-symbolic",
Gtk.IconSize.BUTTON)
+ self.guidelines_button.set_relief(Gtk.ReliefStyle.NONE)
+ self.guidelines_button.set_tooltip_text(_("Select composition guidelines"))
+ bbox.pack_start(self.guidelines_button, False, False, 0)
+
self.start_button = Gtk.Button.new_from_icon_name("media-skip-backward-symbolic",
Gtk.IconSize.BUTTON)
-
self.start_button.connect("clicked", self._start_button_clicked_cb)
self.start_button.set_relief(Gtk.ReliefStyle.NONE)
self.start_button.set_tooltip_text(
@@ -320,6 +334,22 @@ class ViewerContainer(Gtk.Box, Loggable):
self.buttons_container = bbox
self.external_vbox.show_all()
+ def _create_actions(self):
+ self.action_group = Gio.SimpleActionGroup()
+ self.insert_action_group("viewer", self.action_group)
+ self.app.shortcuts.register_group("viewer", _("Viewer"), position=60)
+
+ self.toggle_guidelines_action = Gio.SimpleAction.new("toggle-composition-guidelines", None)
+ self.toggle_guidelines_action.connect("activate", self.__toggle_guidelines_cb)
+ self.action_group.add_action(self.toggle_guidelines_action)
+ self.app.shortcuts.add("viewer.toggle-composition-guidelines",
+ ["<Primary><Shift>c"],
+ self.toggle_guidelines_action,
+ _("Toggle the currently selected composition guidelines"))
+
+ def __toggle_guidelines_cb(self, unused_action, unused_parameter):
+ self.guidelines_popover.toggle()
+
def __corner_draw_cb(self, unused_widget, cr, lines, space, margin):
cr.set_line_width(1)
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 23183a0a..eb08bbcc 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -42,6 +42,7 @@ pitivi/render.py
pitivi/settings.py
pitivi/tabsmanager.py
pitivi/transitions.py
+pitivi/viewer/guidelines_popover.py
pitivi/viewer/viewer.py
pitivi/timeline/elements.py
diff --git a/tests/common.py b/tests/common.py
index c17511a9..761beb2c 100644
--- a/tests/common.py
+++ b/tests/common.py
@@ -30,6 +30,7 @@ from unittest import mock
from gi.repository import Gdk
from gi.repository import GES
+from gi.repository import Gio
from gi.repository import GLib
from gi.repository import Gst
from gi.repository import Gtk
@@ -91,6 +92,8 @@ def create_pitivi_mock(**settings):
app.settings = __create_settings(**settings)
app.proxy_manager = ProxyManager(app)
+ app.gui.editor.viewer.action_group = Gio.SimpleActionGroup()
+
# TODO: Get rid of Zoomable.app.
Zoomable.app = app
@@ -106,8 +109,12 @@ def create_project():
def create_pitivi(**settings):
app = Pitivi()
app._setup()
+
app.gui = mock.Mock()
+ app.gui.editor.viewer.action_group = Gio.SimpleActionGroup()
+
app.settings = __create_settings(**settings)
+
return app
diff --git a/tests/test_viewer_guidelines.py b/tests/test_viewer_guidelines.py
new file mode 100644
index 00000000..d3823455
--- /dev/null
+++ b/tests/test_viewer_guidelines.py
@@ -0,0 +1,77 @@
+# -*- coding: utf-8 -*-
+# Pitivi video editor
+# Copyright (c) 2020, Jaden Goter <jadengoter huskers unl edu>
+# Copyright (c) 2020, Jessie Guo <jessie guo huskers unl edu>
+# Copyright (c) 2020, Daniel Rudebusch <daniel rudebusch huskers unl edu>
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this program; if not, see <http://www.gnu.org/licenses/>.
+"""Tests for the pitivi.viewer.guidelines module."""
+# pylint: disable=protected-access,no-self-use,attribute-defined-outside-init
+from pitivi.viewer.guidelines import Guideline
+from pitivi.viewer.viewer import ViewerContainer
+from tests import common
+
+
+class GuidelinesPopoverTest(common.TestCase):
+ """Tests for the guidelines classes."""
+
+ def setup_viewer_widget(self):
+ """Sets a viewer widget up."""
+ self.app = common.create_pitivi()
+ self.viewer_container = ViewerContainer(self.app)
+ project = self.app.project_manager.new_blank_project()
+ self.viewer_container.set_project(project)
+
+ def _check_guidelines(self, guidelines, toggled_guidelines):
+ overlay = self.viewer_container.guidelines_popover.overlay
+
+ self.assertSetEqual(overlay.active_guidelines, set(guidelines))
+ self.assertEqual(overlay.get_visible(), bool(guidelines), guidelines)
+
+ self.viewer_container.toggle_guidelines_action.activate(None)
+ self.assertSetEqual(overlay.active_guidelines, set(toggled_guidelines))
+ self.assertEqual(overlay.get_visible(), bool(toggled_guidelines), guidelines)
+
+ self.viewer_container.toggle_guidelines_action.activate(None)
+ self.assertSetEqual(overlay.active_guidelines, set(guidelines))
+ self.assertEqual(overlay.get_visible(), bool(guidelines), guidelines)
+
+ def test_activate_deactivate_toggle(self):
+ self.setup_viewer_widget()
+ popover = self.viewer_container.guidelines_popover
+
+ self._check_guidelines([], [Guideline.three_by_three])
+
+ all_guidelines = set()
+ for guideline in Guideline:
+ popover.switches[guideline].set_active(True)
+ self._check_guidelines([guideline], [])
+
+ popover.switches[guideline].set_active(False)
+ self._check_guidelines([], [guideline])
+
+ all_guidelines.add(guideline)
+
+ for guideline in Guideline:
+ popover.switches[guideline].set_active(True)
+ self._check_guidelines(all_guidelines, [])
+
+ for guideline in Guideline:
+ popover.switches[guideline].set_active(False)
+ expected = set(all_guidelines)
+ expected.remove(guideline)
+ self._check_guidelines(expected, [])
+
+ popover.switches[guideline].set_active(True)
+ self._check_guidelines(all_guidelines, [])
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]