From 2f25452a732427daa634883e25d7b992b8b5b9c7 Mon Sep 17 00:00:00 2001 From: Cordell Rhoads Date: Mon, 20 Apr 2020 15:45:05 -0500 Subject: [PATCH 1/7] Issue 2279 Updated user manual for issue 2279 We needed to update to include new feature Added new pages and references to existing code No issue. Linked to 2279 --- data/ui/alignmenteditor.ui | 175 +++++++++++ help/C/alignment.page | 54 ++++ help/C/effects.page | 2 +- help/C/figures/interactivealignment.png | Bin 0 -> 7214 bytes help/C/figures/manualalignment.png | Bin 0 -> 22531 bytes help/C/figures/vieweralignment.png | Bin 0 -> 4502 bytes help/C/index.page | 4 +- help/C/layers.page | 15 +- help/C/mainwindow.page | 4 +- help/C/transitions.page | 2 +- help/C/usingeffects.page | 2 +- help/meson.build | 2 + pitivi/clipalignment.py | 396 ++++++++++++++++++++++++ pitivi/debug.log | 1 + pitivi/editorperspective.py | 4 + pitivi/timeline/layer.py | 25 -- pitivi/timeline/timeline.py | 24 +- pitivi/undo/undo.py | 14 +- pitivi/utils/proxy.py | 95 +++--- tests/test_medialibrary.py | 23 +- tests/test_timeline_layer.py | 45 --- tests/test_timeline_timeline.py | 48 +-- tests/test_undo.py | 26 +- 23 files changed, 721 insertions(+), 240 deletions(-) create mode 100644 data/ui/alignmenteditor.ui create mode 100644 help/C/alignment.page create mode 100644 help/C/figures/interactivealignment.png create mode 100644 help/C/figures/manualalignment.png create mode 100644 help/C/figures/vieweralignment.png create mode 100644 pitivi/clipalignment.py create mode 100644 pitivi/debug.log diff --git a/data/ui/alignmenteditor.ui b/data/ui/alignmenteditor.ui new file mode 100644 index 000000000..d3ba8bc99 --- /dev/null +++ b/data/ui/alignmenteditor.ui @@ -0,0 +1,175 @@ + + + + + + -100 + 100 + 0.5 + 0.050000000000000003 + 0.10000000000000001 + + + -100 + 100 + 0.5 + 0.050000000000000003 + 0.10000000000000001 + + + True + False + vertical + + + True + False + etched-in + + + True + False + 12 + 12 + 12 + 12 + vertical + + + True + False + start + start + 10 + Alignment + 0 + 0 + 0 + + + False + True + 1 + + + + + True + False + start + start + 12 + 6 + 8 + 6 + + + True + False + + + + 1 + 0 + + + + + True + False + Horizontal: + 0 + + + 0 + 0 + + + + + True + False + 1 + + Bottom Left + Bottom + + + + + 1 + 1 + + + + + True + False + Vertical: + 0 + + + 0 + 1 + + + + + True + True + + position_y_adj + 2 + True + + + + 2 + 1 + + + + + True + True + + position_x_adj + 2 + True + + + + 2 + 0 + + + + + False + True + 2 + + + + + + + True + True + 1 + + + + + 1 + 0.5 + 0.050000000000000003 + 0.10000000000000001 + + + 1 + 0.5 + 0.050000000000000003 + 0.10000000000000001 + + diff --git a/help/C/alignment.page b/help/C/alignment.page new file mode 100644 index 000000000..6c07205f9 --- /dev/null +++ b/help/C/alignment.page @@ -0,0 +1,54 @@ + + + + + + + Cordell Rhoads + rhoadscordell7@gmail.com + + + Jackson Eickhoff + jacksoneick@gmail.com + + + Tanner Skelton + tskelton@huskers.unl.edu + + Alignment of clips in and out of frame + +

Creative Commons Share Alike 3.0

+
+
+ Clip Alignment +
+ Click and Drag Alignment +

Users have an ability to manually align their clips within the project frame. This can be done by clicking on the clip in the Viewer and dragging the clip to the desired location.

+
+ Viewer Alignment + Click and drag the clip to re-align in the viewer. + +
+
+
+ Manual Horizontal/Vertical Alignment +

You also have the ability to set the horizontal and vertical alignment within the Title tab located in the Contextual Tabs.

+

This feature offers the following options for horizontal alignment: Absolute, Left, Center, Right.

+

For vertical alignment, you have the following options: Absolute, Top, Center, Bottom, Baseline.

+
+ Manual Clip Alignment + This gives the ability for manual horizontal and vertical clip alignment. + +
+
+
+ Preset Alignment +

Alternatively, you can use preset alignments using the interactive alignment tool under the Alignment tab within the Context Box that is shown at the top of the Main Window.

+

The inner box contained within the interactive alignment tool represents in-frame placement. This tool allows you to align clips both in and out of frame by clicking on one of the preset choices.

+
+ Using interactive alignment tool + The clip will be positioned at the top left corner in frame by left-clicking in the highlighted area. + +
+
+
diff --git a/help/C/effects.page b/help/C/effects.page index 1b4af16e0..99091a112 100644 --- a/help/C/effects.page +++ b/help/C/effects.page @@ -1,7 +1,7 @@ - + diff --git a/help/C/figures/interactivealignment.png b/help/C/figures/interactivealignment.png new file mode 100644 index 0000000000000000000000000000000000000000..fea67f08cd4bffca977b5f9f09e59fb6c1d41375 GIT binary patch literal 7214 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 J`UV6?{11t$<-q^| literal 0 HcmV?d00001 diff --git a/help/C/figures/vieweralignment.png b/help/C/figures/vieweralignment.png new file mode 100644 index 0000000000000000000000000000000000000000..e9f872ab043f6550c3243848b728cd0de2b3d607 GIT binary patch literal 4502 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 Basic Editing with the Timeline -
- Effects and Transitions +
+ Effects, Transitions and Alignment
Exporting Your Finished Movie diff --git a/help/C/layers.page b/help/C/layers.page index c1167130b..3f8eced06 100644 --- a/help/C/layers.page +++ b/help/C/layers.page @@ -27,7 +27,7 @@

It is easier to think of layers in terms of images painted on glass. With several pieces of glass stacked on top of each other, each of these pieces of glass is a layer. If the top piece of glass is completely painted over, none of the pieces of glass underneath will be visible. If, on the other hand, you only paint over a portion of a piece of glass, you will be able to see what is underneath the non-painted parts.

Opacity (how solid “opaque” things are) -

Each layer (and each clip) has its own transparency. To continue the paint on glass metaphor, if the paint is thin enough, it can be seen through. Visually, an opacity of 100% means you cannot see the clips below that layer or clip, and an opacity of 50% means you can partly see them.

+

Each layer (and each clip) has its own transparency. To continue the paint on glass metaphor, if the paint is thin enough, it can be seen through. Visually, an opacity of 100% means you cannot see the clips below that layer or clip, and an opacity of of 50% means you can partly see them.

How this translates in terms of user interface @@ -40,24 +40,11 @@

Clips located on a layer above will block the clips below from view, unless they have an opacity value lower than 100% (as shown with the two topmost layers in the previous illustration).

-
- Renaming a layer -

To rename a layer, click the layer's name, which is an editable text field.

-
-
- Adjusting layer positioning -

To move a layer to the top of the layer order, click Layer iconMove layer to top. To move a layer to the one position higher in the layer order, click Layer iconMove layer up.

-

To move a layer to the one position lower in the layer order, click Layer iconMove layer down. To move a layer to the bottom of the layer order, click Layer iconMove layer to bottom.

-
Adding and removing layers

To create a layer, drag a clip to the middle space between two existing layers or just above the top layer or just below the bottom layer. Once the thin space between the layers is highlighted, release the clip.

To remove a layer, click Layer iconDelete layer. The Layer icon can be found at the right of the layer name field.

-
- Muting a layer -

To mute a layer, click Volume icon. The icon will be toggled to show the layer has been muted. To unmute a layer, click Volume icon again.

-
What about audio layers?

Unlike in vision, multiple sounds do not “block” each other. If you have multiple audio clips on separate layers, their sound will be mixed together. Controlling the volume of those audio clips simply changes their relative loudness.

diff --git a/help/C/mainwindow.page b/help/C/mainwindow.page index e4fdbe070..cf012801f 100644 --- a/help/C/mainwindow.page +++ b/help/C/mainwindow.page @@ -35,7 +35,7 @@

Primary tabs: media library and effects library

-

Contextual tabs: clip properties, transitions, titles

+

Contextual tabs: clip properties, transitions, titles, alignment (not pictured)

Viewer

@@ -162,6 +162,6 @@
Middle pane -

The middle pane contains Clip configuration, Transitions and Title editor. You can switch between them by selecting the appropriate tab. The Clip configuration allows you to activate, deactivate or configure settings of effects applied to a selected clip. Transitions let you pick and configure the transition type when two clips overlap in the timeline. With Title editor you can create new clips with titles or add titles to existing ones.

+

The middle pane contains Clip configuration, Transitions, Title editor and Alignment editor. You can switch between them by selecting the appropriate tab. The Clip configuration allows you to activate, deactivate or configure settings of effects applied to a selected clip. Transitions let you pick and configure the transition type when two clips overlap in the timeline. With Title editor you can create new clips with titles or add titles to existing ones. The Alignment editor allows you to edit the alignment of your clips both inside and outside of the frame.

diff --git a/help/C/transitions.page b/help/C/transitions.page index 3730ccf8a..fc7a45b6e 100644 --- a/help/C/transitions.page +++ b/help/C/transitions.page @@ -1,7 +1,7 @@ - + Tomáš Karger diff --git a/help/C/usingeffects.page b/help/C/usingeffects.page index 2abee3b21..22c4df602 100644 --- a/help/C/usingeffects.page +++ b/help/C/usingeffects.page @@ -1,7 +1,7 @@ - + Tomáš Karger diff --git a/help/meson.build b/help/meson.build index ca7212c4f..0d93eccbc 100644 --- a/help/meson.build +++ b/help/meson.build @@ -1,5 +1,6 @@ sources = [ 'about.page', + 'alignment.page', 'cheatsheet.page', 'codecscontainers.page', 'effects.page', @@ -31,6 +32,7 @@ media = [ 'figures/fadestep1.png', 'figures/fadestep2.png', 'figures/fadestep3.png', + 'figures/interactivealignment.png', 'figures/keyframecurves.png', 'figures/layers.png', 'figures/logo.png', diff --git a/pitivi/clipalignment.py b/pitivi/clipalignment.py new file mode 100644 index 000000000..b9250ba96 --- /dev/null +++ b/pitivi/clipalignment.py @@ -0,0 +1,396 @@ +# -*- coding: utf-8 -*- +# Pitivi video editor +# Copyright (c) 2012, Matas Brazdeikis +# +# 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 . +from gi.repository import Gdk +from gi.repository import GES +from gi.repository import Gtk + +from pitivi.utils.loggable import Loggable +# from pitivi.configure import get_ui_dir + + +class Position: + """Object for configuring the values for alignment. Each Variable should contain a tuple after being set: x, y.""" + + TOP_LEFT_CORNER_OUT = None # Top Left corner, out of the viewer + LEFT_TOP_OUT = None # Left, Top out of viewer + LEFT_CENTER_OUT = None # Left, Center out of viewer + LEFT_BOTTOM_OUT = None # Left, Bottom out of viewer + BOTTOM_LEFT_CORNER_OUT = None # Bottom, Left out of viewer + + TOP_RIGHT_CORNER_OUT = None # Top Right corner, out of the viewer + RIGHT_TOP_OUT = None # Right, Top out of viewer + RIGHT_CENTER_OUT = None # Right, Center out of viewer + RIGHT_BOTTOM_OUT = None # Right, Bottom out of viwer + BOTTOM_RIGHT_CORNER_OUT = None # Bottom Right corner + + TOP_LEFT_OUT = None # Top Center Left, out of viewer + TOP_CENTER_OUT = None # Top Center, out of viewer + TOP_RIGHT_OUT = None # Top Center Right, out of viewer + + BOTTOM_LEFT_OUT = None # Bottom Center Left, out of viewer + BOTTOM_CENTER_OUT = None # Bottom Center, out of viewer + BOTTOM_RIGHT_OUT = None # Bottom Center Right, out of viewer + + TOP_LEFT = None # Top Left, inside the viewer + TOP_CENTER = None # Top Center, inside the viewer + TOP_RIGHT = None # Top right, inside the viewer + LEFT_CENTER = None # Left Center, inside the viewer + CENTER = None # Center, inside the viewer + RIGHT_CENTER = None # Right Center, inside the viewer + BOTTOM_LEFT = None # Bottom Left, inside the viewer + BOTTOM_CENTER = None # Bottom Center, inside the viewer + BOTTOM_RIGHT = None # Bottom Right, inside the viewer + + +class AlignmentEditor(Gtk.EventBox, Loggable): + """Widget for configuring a title. + + Attributes: + app (Pitivi): The app. + _project (Project): The project. + """ + + __gtype_name__ = "AlignmentEditor" + + PIXBUF = None + + def __init__(self, app): + Gtk.EventBox.__init__(self) + Loggable.__init__(self) + self.app = app + self.source = None + self._project = None + self._selection = None + self._selected_clip = None + self._cr = None + self._mouse_x = 0 + self._mouse_y = 0 + + self._setting_props = False + self._children_props_handler = None + + self.builder = Gtk.Builder() + + self.connect('button-release-event', self._button_release_event_cb) + self.connect('motion-notify-event', self._motion_notify_event_cb) + + self.app.project_manager.connect_after( + "new-project-loaded", self._new_project_loaded_cb) + + def _new_project_loaded_cb(self, unused_project_manager, project): + if self._selection is not None: + self._selection.disconnect_by_func(self._selection_changed_cb) + self._selection = None + if project: + self._selection = project.ges_timeline.ui.selection + self._selection.connect('selection-changed', self._selection_changed_cb) + self._project = project + + def _get_clip_position(self): + x = self.source.get_child_property("posx").value + y = self.source.get_child_property("posy").value + return x, y + + def _get_clip_width_height(self): + width = self.source.get_child_property("width").value + height = self.source.get_child_property("height").value + return width, height + + def _set_clip_position(self, new_x, new_y): + # Set the posx of the clip + self.source.set_child_property("posx", new_x) + # Set the posy of the clip + self.source.set_child_property("posy", new_y) + self.source.connect("deep-notify", self.__source_property_changed_cb) + + def _set_clip_width_height(self, new_width, new_height): + # Set the new width of the clip + self.source.set_child_property("width", new_width) + # Set the new height of the clip + self.source.set_child_property("height", new_height) + self.source.connect("deep-notify", self.__source_property_changed_cb) + + def _placeholder_cb(self, junk1, junk2, junk3): + pass + + def __draw_rectangle(self, cr, x, y, w, h): + cr.rectangle(x, y, w, h) + + def _button_release_event_cb(self, widget, event): + self._mouse_x = event.x + self._mouse_y = event.y + print(event.x, event.y) + + def _motion_notify_event_cb(self, widget, event): + self._mouse_x = event.x + self._mouse_y = event.y + self.queue_draw() + + def do_draw(self, cr): + self._cr = cr + self.add_events(Gdk.EventMask.POINTER_MOTION_MASK) + x = 100 + y = 100 + w = 160 + h = 120 + cr.set_source_rgb(1, 1, 1) + cr.move_to(180, 90) + cr.line_to(180, 110) + + cr.move_to(180, 210) + cr.line_to(180, 230) + + cr.move_to(90, 160) + cr.line_to(110, 160) + + cr.move_to(250, 160) + cr.line_to(270, 160) + + cr.move_to(100, 100) + cr.line_to(100, 60) + + cr.move_to(100, 100) + cr.line_to(60, 100) + + cr.move_to(100, 220) + cr.line_to(100, 260) + + cr.move_to(100, 220) + cr.line_to(60, 220) + + cr.move_to(260, 100) + cr.line_to(260, 60) + + cr.move_to(260, 100) + cr.line_to(300, 100) + + cr.move_to(260, 220) + cr.line_to(300, 220) + + cr.move_to(260, 220) + cr.line_to(260, 260) + + self.__draw_rectangle(cr, x, y, w, h) + cr.stroke() + + # highlight selected area + # if 100 < self._mouse_x <= 153 and 60 < self._mouse_y <= 99: + # self.__draw_rectangle(cr, 100, 40, 80, 60) + # if 100 < self._mouse_x <= 153 and 141 < self._mouse_y <= 181: + # self.__draw_rectangle(cr, 100, 130, 80, 60) + # if 100 < self._mouse_x <= 153 and 182 < self._mouse_y <= 221: + # self.__draw_rectangle(cr, 100, 160, 80, 60) + # if 100 < self._mouse_x <= 153 and 100 < self._mouse_y <= 140: + # self.__draw_rectangle(cr, 100, 100, 80, 60) + # if 100 < self._mouse_x <= 153 and 141 < self._mouse_y <= 181: + # self.__draw_rectangle(cr, 100, 130, 80, 60) + # if 100 < self._mouse_x <= 153 and 182 < self._mouse_y <= 221: + # self.__draw_rectangle(cr, 100, 160, 80, 60) + # if 100 < self._mouse_x <= 153 and 221 < self._mouse_y <= 280: + # self.__draw_rectangle(cr, 100, 220, 80, 60) + + # if 20 < self._mouse_x <= 100 and 60 < self._mouse_y <= 99: + # self.__draw_rectangle(cr, 20, 40, 80, 60) + # if 20 < self._mouse_x <= 100 and 141 < self._mouse_y <= 181: + # self.__draw_rectangle(cr, 20, 130, 80, 60) + # if 20 < self._mouse_x <= 100 and 182 < self._mouse_y <= 221: + # self.__draw_rectangle(cr, 20, 160, 80, 60) + # if 20 < self._mouse_x <= 100 < self._mouse_y <= 140: + # self.__draw_rectangle(cr, 20, 100, 80, 60) + # if 20 < self._mouse_x <= 100 and 141 < self._mouse_y <= 181: + # self.__draw_rectangle(cr, 20, 130, 80, 60) + # if 20 < self._mouse_x <= 100 and 182 < self._mouse_y <= 221: + # self.__draw_rectangle(cr, 20, 160, 80, 60) + # if 20 < self._mouse_x <= 100 and 221 < self._mouse_y <= 280: + # self.__draw_rectangle(cr, 20, 220, 80, 60) + + # if 154 < self._mouse_x <= 204 and 60 < self._mouse_y <= 99: + # self.__draw_rectangle(cr, 140, 40, 80, 60) + # if 154 < self._mouse_x <= 204 and 141 < self._mouse_y <= 181: + # self.__draw_rectangle(cr, 140, 130, 80, 60) + # if 154 < self._mouse_x <= 204 and 182 < self._mouse_y <= 221: + # self.__draw_rectangle(cr, 140, 160, 80, 60) + # if 154 < self._mouse_x <= 204 and 100 < self._mouse_y <= 140: + # self.__draw_rectangle(cr, 140, 100, 80, 60) + # if 154 < self._mouse_x <= 204 and 141 < self._mouse_y <= 181: + # self.__draw_rectangle(cr, 140, 130, 80, 60) + # if 154 < self._mouse_x <= 204 and 182 < self._mouse_y <= 221: + # self.__draw_rectangle(cr, 140, 160, 80, 60) + # if 154 < self._mouse_x <= 204 and 221 < self._mouse_y <= 280: + # self.__draw_rectangle(cr, 140, 220, 80, 60) + + # if 205 < self._mouse_x <= 260 and 60 < self._mouse_y <= 99: + # self.__draw_rectangle(cr, 180, 40, 80, 60) + # if 205 < self._mouse_x <= 260 and 141 < self._mouse_y <= 181: + # self.__draw_rectangle(cr, 180, 130, 80, 60) + # if 205 < self._mouse_x <= 260 and 182 < self._mouse_y <= 221: + # self.__draw_rectangle(cr, 180, 160, 80, 60) + # if 205 < self._mouse_x <= 260 and 100 < self._mouse_y <= 140: + # self.__draw_rectangle(cr, 180, 100, 80, 60) + # if 205 < self._mouse_x <= 260 and 141 < self._mouse_y <= 181: + # self.__draw_rectangle(cr, 180, 130, 80, 60) + # if 205 < self._mouse_x <= 260 and 182 < self._mouse_y <= 221: + # self.__draw_rectangle(cr, 180, 160, 80, 60) + # if 205 < self._mouse_x <= 260 and 221 < self._mouse_y <= 280: + # self.__draw_rectangle(cr, 180, 220, 80, 60) + + # if 261 < self._mouse_x <= 320 and 60 < self._mouse_y <= 99: + # self.__draw_rectangle(cr, 261, 40, 80, 60) + # if 261 < self._mouse_x <= 320 and 141 < self._mouse_y <= 181: + # self.__draw_rectangle(cr, 261, 130, 80, 60) + # if 261 < self._mouse_x <= 320 and 182 < self._mouse_y <= 221: + # self.__draw_rectangle(cr, 261, 160, 80, 60) + # if 261 < self._mouse_x <= 320 and 100 < self._mouse_y <= 140: + # self.__draw_rectangle(cr, 261, 100, 80, 60) + # if 261 < self._mouse_x <= 320 and 141 < self._mouse_y <= 181: + # self.__draw_rectangle(cr, 261, 130, 80, 60) + # if 261 < self._mouse_x <= 320 and 182 < self._mouse_y <= 221: + # self.__draw_rectangle(cr, 261, 160, 80, 60) + # if 261 < self._mouse_x <= 320 and 221 < self._mouse_y <= 280: + # self.__draw_rectangle(cr, 261, 220, 80, 60) + + current_box, x, y = self.get_cursor_positons() + if current_box is not None: + self.__draw_rectangle(cr, x, y, 80, 60) + + cr.set_source_rgb(1, 0.1, 0) + cr.fill() + cr.stroke() + + def get_cursor_positons(self): + """Returns position of mouse and which box it is located in. + + Format: + 1) [x,y] - which box it cursor is in in x,y format + 2) x - x axis point for top left of rectangle to be drawn. + 3) y - y axis point for top left of rectangle to be drawn. + """ + if 20 < self._mouse_x <= 100 and 60 < self._mouse_y <= 99: + return [1, 1], 20, 40 + if 20 < self._mouse_x <= 100 < self._mouse_y <= 140: + return [1, 2], 20, 80 + if 20 < self._mouse_x <= 100 and 141 < self._mouse_y <= 181: + return [1, 3], 20, 130 + if 20 < self._mouse_x <= 100 and 182 < self._mouse_y <= 221: + return [1, 4], 20, 160 + if 20 < self._mouse_x <= 100 and 221 < self._mouse_y <= 280: + return [1, 5], 20, 220 + + if 100 < self._mouse_x <= 153 and 60 < self._mouse_y <= 99: + return [2, 1], 100, 40 + if 100 < self._mouse_x <= 153 and 100 < self._mouse_y <= 140: + return [2, 2], 100, 100 + if 100 < self._mouse_x <= 153 and 141 < self._mouse_y <= 181: + return [2, 3], 100, 130 + if 100 < self._mouse_x <= 153 and 182 < self._mouse_y <= 221: + return [2, 4], 100, 160 + if 100 < self._mouse_x <= 153 and 221 < self._mouse_y <= 280: + return [2, 5], 100, 220 + + if 154 < self._mouse_x <= 204 and 60 < self._mouse_y <= 99: + return [3, 1], 140, 40 + if 154 < self._mouse_x <= 204 and 141 < self._mouse_y <= 181: + return [3, 2], 140, 130 + if 154 < self._mouse_x <= 204 and 182 < self._mouse_y <= 221: + return [3, 3], 140, 160 + if 154 < self._mouse_x <= 204 and 100 < self._mouse_y <= 140: + return [3, 4], 140, 100 + if 154 < self._mouse_x <= 204 and 221 < self._mouse_y <= 280: + return [3, 5], 140, 220 + + if 205 < self._mouse_x <= 260 and 60 < self._mouse_y <= 99: + return [4, 1], 180, 40 + if 205 < self._mouse_x <= 260 and 100 < self._mouse_y <= 140: + return [4, 2], 180, 100 + if 205 < self._mouse_x <= 260 and 141 < self._mouse_y <= 181: + return [4, 3], 180, 130 + if 205 < self._mouse_x <= 260 and 182 < self._mouse_y <= 221: + return [4, 3], 180, 160 + if 205 < self._mouse_x <= 260 and 221 < self._mouse_y <= 280: + return [4, 5], 180, 220 + + if 261 < self._mouse_x <= 320 and 60 < self._mouse_y <= 99: + return [5, 1], 261, 40 + if 261 < self._mouse_x <= 320 and 100 < self._mouse_y <= 140: + return [5, 2], 261, 100 + if 261 < self._mouse_x <= 320 and 141 < self._mouse_y <= 181: + return [5, 3], 261, 130 + if 261 < self._mouse_x <= 320 and 182 < self._mouse_y <= 221: + return [5, 4], 261, 160 + if 261 < self._mouse_x <= 320 and 221 < self._mouse_y <= 280: + return [5, 5], 261, 220 + + return None, 0, 0 + + def __set_source(self, source): + self.source = source + + def _selection_changed_cb(self, unused_timeline): + if len(self._selection) == 1: + clip = list(self._selection)[0] + source = clip.find_track_element(None, GES.VideoSource) + if source: + self._selected_clip = clip + self.__set_source(source) + self.app.gui.editor.viewer.overlay_stack.select(source) + self._set_object_values() + return + + # Deselect + if self._selected_clip: + self._selected_clip = None + self._project.pipeline.commit_timeline() + self.__set_source(None) + + def _set_object_values(self): + # Get all the necessary information + project_height = self._project.videoheight + project_width = self._project.videowidth + video_height = self._get_clip_width_height()[1] * .8 + video_width = self._get_clip_width_height()[0] * .8 + middle_x = video_width / 2 + middle_y = video_height / 2 + # Set all the values for the left, outside the viewer + Position.TOP_LEFT_CORNER_OUT = (-video_width, -video_height) + Position.LEFT_TOP_OUT = (-video_width, 0) + Position.LEFT_CENTER_OUT = (-video_width, project_height / 2 - middle_y) + Position.LEFT_BOTTOM_OUT = (-video_width, project_height - video_height) + Position.BOTTOM_LEFT_CORNER_OUT = (-video_width, video_height) + # Set all the values for the right, outside the viewer + Position.TOP_RIGHT_CORNER_OUT = (project_width, -video_height) + Position.RIGHT_TOP_OUT = (project_width, -video_height) + Position.RIGHT_CENTER_OUT = (project_width, project_height / 2 - middle_y) + Position.RIGHT_BOTTOM_OUT = (project_width, project_height - video_height) + Position.BOTTOM_RIGHT_CORNER_OUT = (project_width, project_height) + # Set all the values for the top section, outside of the viewer + Position.TOP_LEFT_OUT = (0, -video_height) + Position.TOP_CENTER_OUT = (project_width / 2 - middle_x, -video_height) + Position.TOP_RIGHT_OUT = (project_width - video_width, -video_height) + # Set all the values for the bottom section, outside of the viewer + Position.BOTTOM_LEFT_OUT = (0, project_height) + Position.BOTTOM_CENTER_OUT = (project_width / 2 - middle_x, project_height) + Position.BOTTOM_RIGHT_OUT = (project_width - video_width, project_height) + # Set all the values for inside the viewer + Position.TOP_LEFT = (0, 0) + Position.TOP_CENTER = (project_width / 2 - middle_x, 0) + Position.TOP_RIGHT = (project_width - video_width, 0) + Position.LEFT_CENTER = (0, project_height / 2 - middle_y) + Position.CENTER = (project_width / 2 - middle_x, project_height / 2 - middle_y) + Position.RIGHT_CENTER = (project_width - video_width, project_height / 2 - middle_y) + Position.BOTTOM_LEFT = (0, project_height - video_height) + Position.BOTTOM_CENTER = (middle_x, project_height - middle_y * 2) + Position.BOTTOM_RIGHT = (project_width - video_width, project_height - video_height) diff --git a/pitivi/debug.log b/pitivi/debug.log new file mode 100644 index 000000000..a0e78f071 --- /dev/null +++ b/pitivi/debug.log @@ -0,0 +1 @@ +bash: bin/pitivi: No such file or directory diff --git a/pitivi/editorperspective.py b/pitivi/editorperspective.py index 8b9215158..f3e3cb4f1 100644 --- a/pitivi/editorperspective.py +++ b/pitivi/editorperspective.py @@ -24,6 +24,7 @@ from gi.repository import GES from gi.repository import Gio from gi.repository import Gtk +from pitivi.clipalignment import AlignmentEditor from pitivi.clipproperties import ClipProperties from pitivi.configure import APPNAME from pitivi.configure import get_ui_dir @@ -209,12 +210,15 @@ class EditorPerspective(Perspective, Loggable): self.clipconfig = ClipProperties(self.app) self.trans_list = TransitionsListWidget(self.app) self.title_editor = TitleEditor(self.app) + self.alignment_editor = AlignmentEditor(self.app) self.context_tabs.append_page("Clip", self.clipconfig, Gtk.Label(label=_("Clip"))) self.context_tabs.append_page("Transition", self.trans_list, Gtk.Label(label=_("Transition"))) self.context_tabs.append_page("Title", self.title_editor.widget, Gtk.Label(label=_("Title"))) + self.context_tabs.append_page("Alignment", + self.alignment_editor, Gtk.Label(label=_("Alignment"))) # Show by default the Title tab, as the Clip and Transition tabs # are useful only when a clip or transition is selected, but # the Title tab allows adding titles. diff --git a/pitivi/timeline/layer.py b/pitivi/timeline/layer.py index d8e2658eb..8e5a52f10 100644 --- a/pitivi/timeline/layer.py +++ b/pitivi/timeline/layer.py @@ -58,9 +58,6 @@ class LayerControls(Gtk.EventBox, Loggable): self.app = app self.__icon = None - tracks = self.ges_timeline.get_tracks() - self.timeline_audio_tracks = [track for track in tracks if track.props.track_type == GES.TrackType.AUDIO] - hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) self.add(hbox) @@ -86,13 +83,6 @@ class LayerControls(Gtk.EventBox, Loggable): self.__update_name() name_row.pack_start(self.name_entry, True, True, 0) - self.mute_toggle_button = Gtk.ToggleButton.new() - self.mute_toggle_button.props.valign = Gtk.Align.CENTER - self.mute_toggle_button.props.relief = Gtk.ReliefStyle.NONE - self.mute_toggle_button.connect("toggled", self.__mute_button_toggled_cb) - self.__update_mute_button() - name_row.pack_start(self.mute_toggle_button, False, False, 0) - self.menubutton = Gtk.MenuButton.new() self.menubutton.props.valign = Gtk.Align.CENTER self.menubutton.props.relief = Gtk.ReliefStyle.NONE @@ -108,7 +98,6 @@ class LayerControls(Gtk.EventBox, Loggable): vbox.pack_start(space, False, False, 0) self.ges_layer.connect("notify::priority", self.__layer_priority_changed_cb) - self.ges_layer.connect("active-changed", self.__layer_active_changed_cb) self.ges_timeline.connect("layer-added", self.__timeline_layer_added_cb) self.ges_timeline.connect("layer-removed", self.__timeline_layer_removed_cb) self.__update_actions() @@ -224,20 +213,6 @@ class LayerControls(Gtk.EventBox, Loggable): self.ges_timeline.ui.move_layer(self.ges_layer, index) self.app.project_manager.current_project.pipeline.commit_timeline() - def __mute_button_toggled_cb(self, button): - self.ges_layer.set_active_for_tracks(not button.get_active(), self.timeline_audio_tracks) - self.app.project_manager.current_project.pipeline.commit_timeline() - - def __update_mute_button(self): - muted = all([not self.ges_layer.get_active_for_track(t) for t in self.timeline_audio_tracks]) - self.mute_toggle_button.set_active(muted) - icon_name = "audio-volume-muted-symbolic" if muted else "audio-volume-high-symbolic" - image = Gtk.Image.new_from_icon_name(icon_name, Gtk.IconSize.BUTTON) - self.mute_toggle_button.set_image(image) - - def __layer_active_changed_cb(self, ges_layer, active, tracks): - self.__update_mute_button() - def update(self, media_types): self.props.height_request = self.ges_layer.ui.props.height_request diff --git a/pitivi/timeline/timeline.py b/pitivi/timeline/timeline.py index 0cf2f7c70..44e7b72c7 100644 --- a/pitivi/timeline/timeline.py +++ b/pitivi/timeline/timeline.py @@ -949,31 +949,21 @@ class Timeline(Gtk.EventBox, Zoomable, Loggable): assets = self._project.assets_for_uris(self.drop_data) if not assets: self._project.add_uris(self.drop_data) - return + return False ges_clips = [] - self.app.action_log.begin("Add clips") for asset in assets: + ges_layer, unused_on_sep = self.get_layer_at(y) if not placement: placement = self.pixel_to_ns(x) placement = max(0, placement) - self.debug("Adding %s at %s on layer %s", asset.props.id, Gst.TIME_ARGS(placement), ges_layer) - self.app.action_log.begin("Add one clip") + self.debug("Creating %s at %s", asset.props.id, Gst.TIME_ARGS(placement)) + ges_clip = self.add_clip_to_layer(ges_layer, asset, placement) if not ges_clip: - # The clip cannot be placed. - - # Rollback the current "Add one asset" transaction without - # doing anything, since nothing really changed but GES still - # emitted signals as if it added AND removed the clip. - self.app.action_log.rollback(undo=False) - # Rollback the rest of the "Add assets" transaction. - self.app.action_log.rollback() - return - - self.app.action_log.commit("Add one clip") + return False placement += ges_clip.props.duration ges_clip.first_placement = True @@ -981,8 +971,6 @@ class Timeline(Gtk.EventBox, Zoomable, Loggable): ges_clips.append(ges_clip) - self.app.action_log.commit("Add clips") - if ges_clips: ges_clip = ges_clips[0] self.dragging_element = ges_clip.ui @@ -993,6 +981,8 @@ class Timeline(Gtk.EventBox, Zoomable, Loggable): self.dragging_group = self.selection.group() + return True + def _drag_motion_cb(self, widget, context, x, y, timestamp): target = self.drag_dest_find_target(context, None) if not target: diff --git a/pitivi/undo/undo.py b/pitivi/undo/undo.py index 3803fcbb8..13e7cde55 100644 --- a/pitivi/undo/undo.py +++ b/pitivi/undo/undo.py @@ -254,26 +254,20 @@ class UndoableActionLog(GObject.Object, Loggable): action, stack.action_group_name) self.emit("push", stack, action) - def rollback(self, undo=True): - """Forgets about the last started operation. - - Args: - undo (bool): Whether to undo the last started operation. - If False, it's disregarded without any action. - """ + def rollback(self): + """Forgets about the last started operation.""" if self.running: self.debug("Ignore rollback because running") return - self.debug("Rolling back, undo=%s", undo) self.rolling_back = True try: + self.debug("Rolling back") stack = self._get_last_stack(pop=True) self.debug("rollback action group %s, nested %s", stack.action_group_name, len(self.stacks)) self.emit("rollback", stack) - if undo: - stack.undo() + stack.undo() finally: self.rolling_back = False diff --git a/pitivi/utils/proxy.py b/pitivi/utils/proxy.py index bedf006c7..4bbe167ba 100644 --- a/pitivi/utils/proxy.py +++ b/pitivi/utils/proxy.py @@ -466,6 +466,7 @@ class ProxyManager(GObject.Object, Loggable): proxy_duration = asset_get_duration(proxy) if asset_duration != proxy_duration: duration = min(asset_duration, proxy_duration) + self.info("Resetting %s duration from %s to %s as" " new proxy has a different duration", asset.props.id, Gst.TIME_ARGS(asset_duration), Gst.TIME_ARGS(duration)) @@ -633,33 +634,14 @@ class ProxyManager(GObject.Object, Loggable): return is_queued - def __create_transcoder(self, asset, scaled=False, shadow=False): + def __create_transcoder(self, asset, width=None, height=None, shadow=False): self._total_time_to_transcode += asset.get_duration() / Gst.SECOND asset_uri = asset.get_id() - proxy_uri = self.get_proxy_uri(asset, scaled=scaled) - - if Gio.File.new_for_uri(proxy_uri).query_exists(None): - self.debug("Using proxy already generated: %s", proxy_uri) - GES.Asset.request_async(GES.UriClip, - proxy_uri, None, - self.__asset_loaded_cb, asset, - None) - return - - self.debug("Creating a proxy for %s (strategy: %s, force: %s, scaled: %s)", - asset.get_id(), self.app.settings.proxying_strategy, - asset.force_proxying, scaled) - width = None - height = None - if scaled: - project = self.app.project_manager.current_project - w = project.scaled_proxy_width - h = project.scaled_proxy_height - if not project.has_scaled_proxy_size(): - project.scaled_proxy_width = w - project.scaled_proxy_height = h - width, height = self._scale_asset_resolution(asset, w, h) + if width and height: + proxy_uri = self.get_proxy_uri(asset, scaled=True) + else: + proxy_uri = self.get_proxy_uri(asset) dispatcher = GstTranscoder.TranscoderGMainContextSignalDispatcher.new() @@ -737,27 +719,28 @@ class ProxyManager(GObject.Object, Loggable): to shadow a scaled proxy. """ force_proxying = asset.force_proxying - video_streams = asset.get_info().get_video_streams() - if video_streams: - # Handle Automatic scaling - if self.app.settings.auto_scaling_enabled and not force_proxying \ - and not shadow and not self.asset_matches_target_res(asset): - scaled = True - - # Create shadow proxies for unsupported assets - if not self.is_asset_format_well_supported(asset) and not \ - self.app.settings.proxying_strategy == ProxyingStrategy.NOTHING \ - and not shadow and scaled: - hq_uri = self.app.proxy_manager.get_proxy_uri(asset) - if not Gio.File.new_for_uri(hq_uri).query_exists(None): - self.add_job(asset, shadow=True) - else: - # Scaled proxy is not for audio assets - scaled = False + # Handle Automatic scaling + if self.app.settings.auto_scaling_enabled and not force_proxying \ + and not shadow and not self.asset_matches_target_res(asset): + scaled = True + + # Create shadow proxies for unsupported assets + if not self.is_asset_format_well_supported(asset) and not \ + self.app.settings.proxying_strategy == ProxyingStrategy.NOTHING \ + and not shadow and scaled: + hq_uri = self.app.proxy_manager.get_proxy_uri(asset) + if not Gio.File.new_for_uri(hq_uri).query_exists(None): + self.add_job(asset, shadow=True) - if self.is_asset_queued(asset, scaling=scaled, optimisation=not scaled): - self.log("Asset %s already queued for %s", asset, "scaling" if scaled else "optimization") - return + if scaled: + if self.is_asset_queued(asset, optimisation=False): + self.log("Asset already queued for scaling: %s", asset) + return + + else: + if self.is_asset_queued(asset, scaling=False): + self.log("Asset already queued for optimization: %s", asset) + return if not force_proxying: if not self.__asset_needs_transcoding(asset, scaled): @@ -767,7 +750,29 @@ class ProxyManager(GObject.Object, Loggable): self.emit("proxy-ready", asset, None) return - self.__create_transcoder(asset, scaled=scaled, shadow=shadow) + proxy_uri = self.get_proxy_uri(asset, scaled) + if Gio.File.new_for_uri(proxy_uri).query_exists(None): + self.debug("Using proxy already generated: %s", proxy_uri) + GES.Asset.request_async(GES.UriClip, + proxy_uri, None, + self.__asset_loaded_cb, asset, + None) + return + + self.debug("Creating a proxy for %s (strategy: %s, force: %s, scaled: %s)", + asset.get_id(), self.app.settings.proxying_strategy, + force_proxying, scaled) + if scaled: + project = self.app.project_manager.current_project + w = project.scaled_proxy_width + h = project.scaled_proxy_height + if not project.has_scaled_proxy_size(): + project.scaled_proxy_width = w + project.scaled_proxy_height = h + t_width, t_height = self._scale_asset_resolution(asset, w, h) + self.__create_transcoder(asset, width=t_width, height=t_height, shadow=shadow) + else: + self.__create_transcoder(asset, shadow=shadow) def get_proxy_target(obj): diff --git a/tests/test_medialibrary.py b/tests/test_medialibrary.py index ac628486a..ebb496adc 100644 --- a/tests/test_medialibrary.py +++ b/tests/test_medialibrary.py @@ -22,6 +22,7 @@ from unittest import mock from gi.repository import Gdk from gi.repository import GES +from gi.repository import GObject # pylint: disable=unused-import from gi.repository import Gst from pitivi import medialibrary @@ -95,12 +96,10 @@ class BaseTestMediaLibrary(common.TestCase): common.get_sample_uri(sample_name), GES.UriClip) def check_import(self, samples, proxying_strategy=ProxyingStrategy.ALL, - check_no_transcoding=False, auto_scaling_enabled=False): - """Simulates the user importing an asset.""" + check_no_transcoding=False): self._custom_set_up(proxying_strategy=proxying_strategy, num_transcoding_jobs=4, - last_clip_view=medialibrary.SHOW_TREEVIEW, - auto_scaling_enabled=auto_scaling_enabled) + last_clip_view=medialibrary.SHOW_TREEVIEW) self.check_no_transcoding = check_no_transcoding self.medialibrary._progressbar.connect( @@ -112,7 +111,6 @@ class BaseTestMediaLibrary(common.TestCase): def check_add_proxy(self, asset, scaled=False, w=160, h=120, check_progress=True): - """Simulates the user requesting an asset to be proxied.""" self.assertFalse(self.app.proxy_manager.is_proxy_asset(asset)) # Check the initial state of the asset, nothing should be going on. @@ -164,13 +162,11 @@ class BaseTestMediaLibrary(common.TestCase): medialibrary.AssetThumbnail.PROXIED) proxy = self.medialibrary.storemodel[0][medialibrary.COL_ASSET] + stream = proxy.get_info().get_video_streams()[0] + resolution = [stream.get_width(), stream.get_height()] self.assertEqual(proxy.props.proxy_target.props.id, asset.props.id) - # Check if the asset is video or not - if w: - stream = proxy.get_info().get_video_streams()[0] - resolution = [stream.get_width(), stream.get_height()] - if scaled: - self.assertEqual(resolution, [w, h]) + if scaled: + self.assertEqual(resolution, [w, h]) return proxy @@ -474,11 +470,6 @@ class TestMediaLibrary(BaseTestMediaLibrary): self.check_import([sample], check_no_transcoding=True, proxying_strategy=ProxyingStrategy.AUTOMATIC) - def test_import_supported_forced_scaled_audio(self): - sample = "mp3_sample.mp3" - with common.cloned_sample(sample): - self.check_import([sample], auto_scaling_enabled=True) - def test_missing_uri_displayed(self): asset_uri = common.get_sample_uri("image-which-does-not-exist.png") with common.created_project_file(asset_uri) as uri: diff --git a/tests/test_timeline_layer.py b/tests/test_timeline_layer.py index fa0bcf249..ba3935e42 100644 --- a/tests/test_timeline_layer.py +++ b/tests/test_timeline_layer.py @@ -46,51 +46,6 @@ class TestLayerControl(common.TestCase): layer.set_name("Layer 0x") self.assertEqual(layer.get_name(), "Layer 0x") - def test_mute_and_unmute_layer(self): - timeline_container = common.create_timeline_container() - timeline = timeline_container.timeline - ges_layer = timeline.ges_timeline.append_layer() - layer_controls = ges_layer.control_ui - mute_toggle_button = layer_controls.mute_toggle_button - - for audio_track in layer_controls.timeline_audio_tracks: - self.assertTrue(ges_layer.get_active_for_track(audio_track)) - self.assertFalse(mute_toggle_button.get_active()) - - ges_layer.set_active_for_tracks(False, [audio_track]) - common.create_main_loop().run(until_empty=True) - self.assertFalse(ges_layer.get_active_for_track(audio_track)) - self.assertTrue(mute_toggle_button.get_active()) - - ges_layer.set_active_for_tracks(True, [audio_track]) - common.create_main_loop().run(until_empty=True) - self.assertTrue(ges_layer.get_active_for_track(audio_track)) - self.assertFalse(mute_toggle_button.get_active()) - - def test_mute_and_unmute_layer_button(self): - timeline_container = common.create_timeline_container() - timeline = timeline_container.timeline - ges_layer = timeline.ges_timeline.append_layer() - layer_controls = ges_layer.control_ui - mute_toggle_button = layer_controls.mute_toggle_button - - for audio_track in layer_controls.timeline_audio_tracks: - self.assertTrue(ges_layer.get_active_for_track(audio_track)) - common.create_main_loop().run(until_empty=True) - self.assertFalse(mute_toggle_button.get_active()) - - mute_toggle_button.clicked() - for audio_track in layer_controls.timeline_audio_tracks: - self.assertFalse(ges_layer.get_active_for_track(audio_track)) - common.create_main_loop().run(until_empty=True) - self.assertTrue(mute_toggle_button.get_active()) - - mute_toggle_button.clicked() - for audio_track in layer_controls.timeline_audio_tracks: - self.assertTrue(ges_layer.get_active_for_track(audio_track)) - common.create_main_loop().run(until_empty=True) - self.assertFalse(mute_toggle_button.get_active()) - class TestLayer(common.TestCase): diff --git a/tests/test_timeline_timeline.py b/tests/test_timeline_timeline.py index d77a4fa6b..e094302ce 100644 --- a/tests/test_timeline_timeline.py +++ b/tests/test_timeline_timeline.py @@ -23,8 +23,6 @@ from gi.repository import GES from gi.repository import Gst from gi.repository import Gtk -from pitivi.undo.timeline import TimelineObserver -from pitivi.undo.undo import UndoableActionLog from pitivi.utils.timeline import UNSELECT from pitivi.utils.ui import LAYER_HEIGHT from pitivi.utils.ui import SEPARATOR_HEIGHT @@ -828,17 +826,17 @@ class TestClipsEdges(BaseTestTimeline): class TestDragFromOutside(BaseTestTimeline): - def setUp(self): - super().setUp() - + def test_adding_overlap_clip(self): + """Checks asset drag&drop on top of an existing clip.""" timeline_container = common.create_timeline_container() - self.ges_timeline = timeline_container.timeline.ges_timeline + timeline_ui = timeline_container.timeline - timeline_container.app.action_log = UndoableActionLog() - self.timeline_observer = TimelineObserver(timeline_container.ges_timeline, timeline_container.app.action_log) + asset = GES.UriClipAsset.request_sync( + common.get_sample_uri("tears_of_steel.webm")) + layer, = timeline_ui.ges_timeline.get_layers() + layer.add_asset(asset, 0, 0, 10, GES.TrackType.UNKNOWN) - def check_drag_assets_to_timeline(self, timeline_ui, assets): - # Events emitted while dragging assets over a clip in the timeline: + # Events emitted while dragging an asset over a clip in the timeline: # motion, receive, motion. with mock.patch.object(Gdk, "drag_status") as _drag_status_mock: with mock.patch.object(Gtk, "drag_finish") as _drag_finish_mock: @@ -852,11 +850,11 @@ class TestDragFromOutside(BaseTestTimeline): self.assertFalse(timeline_ui.drop_data_ready) selection_data = mock.Mock() selection_data.get_data_type = mock.Mock(return_value=target) - selection_data.get_uris.return_value = [asset.props.id for asset in assets] + selection_data.get_uris.return_value = [asset.props.id] self.assertIsNone(timeline_ui.drop_data) self.assertFalse(timeline_ui.drop_data_ready) timeline_ui._drag_data_received_cb(None, None, 0, 0, selection_data, None, 0) - self.assertEqual(timeline_ui.drop_data, [asset.props.id for asset in assets]) + self.assertEqual(timeline_ui.drop_data, [asset.props.id]) self.assertTrue(timeline_ui.drop_data_ready) timeline_ui.drag_get_data.reset_mock() @@ -871,29 +869,3 @@ class TestDragFromOutside(BaseTestTimeline): self.assertFalse(timeline_ui.drag_get_data.called) self.assertIsNone(timeline_ui.dragging_element) self.assertFalse(timeline_ui.dropping_clips) - - def test_adding_overlap_clip(self): - """Checks asset drag&drop on top of an existing clip.""" - asset = GES.UriClipAsset.request_sync( - common.get_sample_uri("tears_of_steel.webm")) - - layer, = self.ges_timeline.get_layers() - layer.add_asset(asset, 0, 0, 10, GES.TrackType.UNKNOWN) - clips = layer.get_clips() - - self.check_drag_assets_to_timeline(self.ges_timeline.ui, [asset]) - self.assertEqual(layer.get_clips(), clips) - - def test_dragging_multiple_clips_over_timeline(self): - """Checks drag&drop two assets when only the first one can be placed.""" - asset = GES.UriClipAsset.request_sync( - common.get_sample_uri("tears_of_steel.webm")) - - layer, = self.ges_timeline.get_layers() - start = asset.get_duration() - layer.add_asset(asset, start, 0, 10, GES.TrackType.UNKNOWN) - clips = layer.get_clips() - - # Use same asset to mimic dragging multiple assets - self.check_drag_assets_to_timeline(self.ges_timeline.ui, [asset, asset]) - self.assertEqual(layer.get_clips(), clips) diff --git a/tests/test_undo.py b/tests/test_undo.py index 29a7976d1..95fcde20b 100644 --- a/tests/test_undo.py +++ b/tests/test_undo.py @@ -89,10 +89,6 @@ class TestUndoableActionLog(common.TestCase): def tearDown(self): self._disconnect_from_undoable_action_log() - def check_signals(self, *expected_signals): - signals = [item[0] for item in self.signals] - self.assertListEqual(signals, list(expected_signals)) - def _undo_action_log_signal_cb(self, log, *args): args = list(args) signal_name = args.pop(-1) @@ -223,35 +219,19 @@ class TestUndoableActionLog(common.TestCase): self.assertEqual(len(self.log.undo_stacks), 0) self.assertEqual(len(self.log.redo_stacks), 0) self.log.begin("meh") - self.check_signals("begin") + self.assertEqual(len(self.signals), 1) name, (_stack,) = self.signals[0] self.assertEqual(name, "begin") self.assertTrue(self.log.is_in_transaction()) - action = mock.Mock(spec=UndoableAction) - self.log.push(action) - self.log.rollback() - - action.undo.assert_called_once_with() - - self.check_signals("begin", "push", "rollback") - name, (_stack,) = self.signals[2] + self.assertEqual(len(self.signals), 2) + name, (_stack,) = self.signals[1] self.assertEqual(name, "rollback") self.assertFalse(self.log.is_in_transaction()) self.assertEqual(len(self.log.undo_stacks), 0) self.assertEqual(len(self.log.redo_stacks), 0) - def test_rollback_noop(self): - """Checks a rollback which does not act.""" - self.log.begin("meh") - - action = mock.Mock(spec=UndoableAction) - self.log.push(action) - - self.log.rollback(undo=False) - action.undo.assert_not_called() - def test_nested_rollback(self): """Checks two nested rollbacks.""" self.assertEqual(len(self.log.undo_stacks), 0) -- GitLab From 602c4de13878ae0f0181cf44c05946bdbf467b72 Mon Sep 17 00:00:00 2001 From: Cordell Rhoads Date: Mon, 20 Apr 2020 20:54:03 +0000 Subject: [PATCH 2/7] Update layers.page --- help/C/layers.page | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/help/C/layers.page b/help/C/layers.page index 3f8eced06..0ac9ba395 100644 --- a/help/C/layers.page +++ b/help/C/layers.page @@ -27,7 +27,7 @@

It is easier to think of layers in terms of images painted on glass. With several pieces of glass stacked on top of each other, each of these pieces of glass is a layer. If the top piece of glass is completely painted over, none of the pieces of glass underneath will be visible. If, on the other hand, you only paint over a portion of a piece of glass, you will be able to see what is underneath the non-painted parts.

Opacity (how solid “opaque” things are) -

Each layer (and each clip) has its own transparency. To continue the paint on glass metaphor, if the paint is thin enough, it can be seen through. Visually, an opacity of 100% means you cannot see the clips below that layer or clip, and an opacity of of 50% means you can partly see them.

+

Each layer (and each clip) has its own transparency. To continue the paint on glass metaphor, if the paint is thin enough, it can be seen through. Visually, an opacity of 100% means you cannot see the clips below that layer or clip, and an opacity of 50% means you can partly see them.

How this translates in terms of user interface -- GitLab From 24c5971f81fbd38ac05af37f83e7a61b058125c5 Mon Sep 17 00:00:00 2001 From: Cordell Rhoads Date: Mon, 20 Apr 2020 20:55:13 +0000 Subject: [PATCH 3/7] Delete alignmenteditor.ui --- data/ui/alignmenteditor.ui | 175 ------------------------------------- 1 file changed, 175 deletions(-) diff --git a/data/ui/alignmenteditor.ui b/data/ui/alignmenteditor.ui index d3ba8bc99..e69de29bb 100644 --- a/data/ui/alignmenteditor.ui +++ b/data/ui/alignmenteditor.ui @@ -1,175 +0,0 @@ - - - - - - -100 - 100 - 0.5 - 0.050000000000000003 - 0.10000000000000001 - - - -100 - 100 - 0.5 - 0.050000000000000003 - 0.10000000000000001 - - - True - False - vertical - - - True - False - etched-in - - - True - False - 12 - 12 - 12 - 12 - vertical - - - True - False - start - start - 10 - Alignment - 0 - 0 - 0 - - - False - True - 1 - - - - - True - False - start - start - 12 - 6 - 8 - 6 - - - True - False - - - - 1 - 0 - - - - - True - False - Horizontal: - 0 - - - 0 - 0 - - - - - True - False - 1 - - Bottom Left - Bottom - - - - - 1 - 1 - - - - - True - False - Vertical: - 0 - - - 0 - 1 - - - - - True - True - - position_y_adj - 2 - True - - - - 2 - 1 - - - - - True - True - - position_x_adj - 2 - True - - - - 2 - 0 - - - - - False - True - 2 - - - - - - - True - True - 1 - - - - - 1 - 0.5 - 0.050000000000000003 - 0.10000000000000001 - - - 1 - 0.5 - 0.050000000000000003 - 0.10000000000000001 - - -- GitLab From 02660ef7922b62de63656718c9c4d2fd8875468e Mon Sep 17 00:00:00 2001 From: Cordell Rhoads Date: Mon, 20 Apr 2020 20:55:23 +0000 Subject: [PATCH 4/7] Delete alignmenteditor.ui --- data/ui/alignmenteditor.ui | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 data/ui/alignmenteditor.ui diff --git a/data/ui/alignmenteditor.ui b/data/ui/alignmenteditor.ui deleted file mode 100644 index e69de29bb..000000000 -- GitLab From 433e49bf631d29376c4e8a4c7b672092048c6ce3 Mon Sep 17 00:00:00 2001 From: Cordell Rhoads Date: Mon, 20 Apr 2020 20:57:24 +0000 Subject: [PATCH 5/7] Delete clipalignment.py --- pitivi/clipalignment.py | 396 ---------------------------------------- 1 file changed, 396 deletions(-) delete mode 100644 pitivi/clipalignment.py diff --git a/pitivi/clipalignment.py b/pitivi/clipalignment.py deleted file mode 100644 index b9250ba96..000000000 --- a/pitivi/clipalignment.py +++ /dev/null @@ -1,396 +0,0 @@ -# -*- coding: utf-8 -*- -# Pitivi video editor -# Copyright (c) 2012, Matas Brazdeikis -# -# 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 . -from gi.repository import Gdk -from gi.repository import GES -from gi.repository import Gtk - -from pitivi.utils.loggable import Loggable -# from pitivi.configure import get_ui_dir - - -class Position: - """Object for configuring the values for alignment. Each Variable should contain a tuple after being set: x, y.""" - - TOP_LEFT_CORNER_OUT = None # Top Left corner, out of the viewer - LEFT_TOP_OUT = None # Left, Top out of viewer - LEFT_CENTER_OUT = None # Left, Center out of viewer - LEFT_BOTTOM_OUT = None # Left, Bottom out of viewer - BOTTOM_LEFT_CORNER_OUT = None # Bottom, Left out of viewer - - TOP_RIGHT_CORNER_OUT = None # Top Right corner, out of the viewer - RIGHT_TOP_OUT = None # Right, Top out of viewer - RIGHT_CENTER_OUT = None # Right, Center out of viewer - RIGHT_BOTTOM_OUT = None # Right, Bottom out of viwer - BOTTOM_RIGHT_CORNER_OUT = None # Bottom Right corner - - TOP_LEFT_OUT = None # Top Center Left, out of viewer - TOP_CENTER_OUT = None # Top Center, out of viewer - TOP_RIGHT_OUT = None # Top Center Right, out of viewer - - BOTTOM_LEFT_OUT = None # Bottom Center Left, out of viewer - BOTTOM_CENTER_OUT = None # Bottom Center, out of viewer - BOTTOM_RIGHT_OUT = None # Bottom Center Right, out of viewer - - TOP_LEFT = None # Top Left, inside the viewer - TOP_CENTER = None # Top Center, inside the viewer - TOP_RIGHT = None # Top right, inside the viewer - LEFT_CENTER = None # Left Center, inside the viewer - CENTER = None # Center, inside the viewer - RIGHT_CENTER = None # Right Center, inside the viewer - BOTTOM_LEFT = None # Bottom Left, inside the viewer - BOTTOM_CENTER = None # Bottom Center, inside the viewer - BOTTOM_RIGHT = None # Bottom Right, inside the viewer - - -class AlignmentEditor(Gtk.EventBox, Loggable): - """Widget for configuring a title. - - Attributes: - app (Pitivi): The app. - _project (Project): The project. - """ - - __gtype_name__ = "AlignmentEditor" - - PIXBUF = None - - def __init__(self, app): - Gtk.EventBox.__init__(self) - Loggable.__init__(self) - self.app = app - self.source = None - self._project = None - self._selection = None - self._selected_clip = None - self._cr = None - self._mouse_x = 0 - self._mouse_y = 0 - - self._setting_props = False - self._children_props_handler = None - - self.builder = Gtk.Builder() - - self.connect('button-release-event', self._button_release_event_cb) - self.connect('motion-notify-event', self._motion_notify_event_cb) - - self.app.project_manager.connect_after( - "new-project-loaded", self._new_project_loaded_cb) - - def _new_project_loaded_cb(self, unused_project_manager, project): - if self._selection is not None: - self._selection.disconnect_by_func(self._selection_changed_cb) - self._selection = None - if project: - self._selection = project.ges_timeline.ui.selection - self._selection.connect('selection-changed', self._selection_changed_cb) - self._project = project - - def _get_clip_position(self): - x = self.source.get_child_property("posx").value - y = self.source.get_child_property("posy").value - return x, y - - def _get_clip_width_height(self): - width = self.source.get_child_property("width").value - height = self.source.get_child_property("height").value - return width, height - - def _set_clip_position(self, new_x, new_y): - # Set the posx of the clip - self.source.set_child_property("posx", new_x) - # Set the posy of the clip - self.source.set_child_property("posy", new_y) - self.source.connect("deep-notify", self.__source_property_changed_cb) - - def _set_clip_width_height(self, new_width, new_height): - # Set the new width of the clip - self.source.set_child_property("width", new_width) - # Set the new height of the clip - self.source.set_child_property("height", new_height) - self.source.connect("deep-notify", self.__source_property_changed_cb) - - def _placeholder_cb(self, junk1, junk2, junk3): - pass - - def __draw_rectangle(self, cr, x, y, w, h): - cr.rectangle(x, y, w, h) - - def _button_release_event_cb(self, widget, event): - self._mouse_x = event.x - self._mouse_y = event.y - print(event.x, event.y) - - def _motion_notify_event_cb(self, widget, event): - self._mouse_x = event.x - self._mouse_y = event.y - self.queue_draw() - - def do_draw(self, cr): - self._cr = cr - self.add_events(Gdk.EventMask.POINTER_MOTION_MASK) - x = 100 - y = 100 - w = 160 - h = 120 - cr.set_source_rgb(1, 1, 1) - cr.move_to(180, 90) - cr.line_to(180, 110) - - cr.move_to(180, 210) - cr.line_to(180, 230) - - cr.move_to(90, 160) - cr.line_to(110, 160) - - cr.move_to(250, 160) - cr.line_to(270, 160) - - cr.move_to(100, 100) - cr.line_to(100, 60) - - cr.move_to(100, 100) - cr.line_to(60, 100) - - cr.move_to(100, 220) - cr.line_to(100, 260) - - cr.move_to(100, 220) - cr.line_to(60, 220) - - cr.move_to(260, 100) - cr.line_to(260, 60) - - cr.move_to(260, 100) - cr.line_to(300, 100) - - cr.move_to(260, 220) - cr.line_to(300, 220) - - cr.move_to(260, 220) - cr.line_to(260, 260) - - self.__draw_rectangle(cr, x, y, w, h) - cr.stroke() - - # highlight selected area - # if 100 < self._mouse_x <= 153 and 60 < self._mouse_y <= 99: - # self.__draw_rectangle(cr, 100, 40, 80, 60) - # if 100 < self._mouse_x <= 153 and 141 < self._mouse_y <= 181: - # self.__draw_rectangle(cr, 100, 130, 80, 60) - # if 100 < self._mouse_x <= 153 and 182 < self._mouse_y <= 221: - # self.__draw_rectangle(cr, 100, 160, 80, 60) - # if 100 < self._mouse_x <= 153 and 100 < self._mouse_y <= 140: - # self.__draw_rectangle(cr, 100, 100, 80, 60) - # if 100 < self._mouse_x <= 153 and 141 < self._mouse_y <= 181: - # self.__draw_rectangle(cr, 100, 130, 80, 60) - # if 100 < self._mouse_x <= 153 and 182 < self._mouse_y <= 221: - # self.__draw_rectangle(cr, 100, 160, 80, 60) - # if 100 < self._mouse_x <= 153 and 221 < self._mouse_y <= 280: - # self.__draw_rectangle(cr, 100, 220, 80, 60) - - # if 20 < self._mouse_x <= 100 and 60 < self._mouse_y <= 99: - # self.__draw_rectangle(cr, 20, 40, 80, 60) - # if 20 < self._mouse_x <= 100 and 141 < self._mouse_y <= 181: - # self.__draw_rectangle(cr, 20, 130, 80, 60) - # if 20 < self._mouse_x <= 100 and 182 < self._mouse_y <= 221: - # self.__draw_rectangle(cr, 20, 160, 80, 60) - # if 20 < self._mouse_x <= 100 < self._mouse_y <= 140: - # self.__draw_rectangle(cr, 20, 100, 80, 60) - # if 20 < self._mouse_x <= 100 and 141 < self._mouse_y <= 181: - # self.__draw_rectangle(cr, 20, 130, 80, 60) - # if 20 < self._mouse_x <= 100 and 182 < self._mouse_y <= 221: - # self.__draw_rectangle(cr, 20, 160, 80, 60) - # if 20 < self._mouse_x <= 100 and 221 < self._mouse_y <= 280: - # self.__draw_rectangle(cr, 20, 220, 80, 60) - - # if 154 < self._mouse_x <= 204 and 60 < self._mouse_y <= 99: - # self.__draw_rectangle(cr, 140, 40, 80, 60) - # if 154 < self._mouse_x <= 204 and 141 < self._mouse_y <= 181: - # self.__draw_rectangle(cr, 140, 130, 80, 60) - # if 154 < self._mouse_x <= 204 and 182 < self._mouse_y <= 221: - # self.__draw_rectangle(cr, 140, 160, 80, 60) - # if 154 < self._mouse_x <= 204 and 100 < self._mouse_y <= 140: - # self.__draw_rectangle(cr, 140, 100, 80, 60) - # if 154 < self._mouse_x <= 204 and 141 < self._mouse_y <= 181: - # self.__draw_rectangle(cr, 140, 130, 80, 60) - # if 154 < self._mouse_x <= 204 and 182 < self._mouse_y <= 221: - # self.__draw_rectangle(cr, 140, 160, 80, 60) - # if 154 < self._mouse_x <= 204 and 221 < self._mouse_y <= 280: - # self.__draw_rectangle(cr, 140, 220, 80, 60) - - # if 205 < self._mouse_x <= 260 and 60 < self._mouse_y <= 99: - # self.__draw_rectangle(cr, 180, 40, 80, 60) - # if 205 < self._mouse_x <= 260 and 141 < self._mouse_y <= 181: - # self.__draw_rectangle(cr, 180, 130, 80, 60) - # if 205 < self._mouse_x <= 260 and 182 < self._mouse_y <= 221: - # self.__draw_rectangle(cr, 180, 160, 80, 60) - # if 205 < self._mouse_x <= 260 and 100 < self._mouse_y <= 140: - # self.__draw_rectangle(cr, 180, 100, 80, 60) - # if 205 < self._mouse_x <= 260 and 141 < self._mouse_y <= 181: - # self.__draw_rectangle(cr, 180, 130, 80, 60) - # if 205 < self._mouse_x <= 260 and 182 < self._mouse_y <= 221: - # self.__draw_rectangle(cr, 180, 160, 80, 60) - # if 205 < self._mouse_x <= 260 and 221 < self._mouse_y <= 280: - # self.__draw_rectangle(cr, 180, 220, 80, 60) - - # if 261 < self._mouse_x <= 320 and 60 < self._mouse_y <= 99: - # self.__draw_rectangle(cr, 261, 40, 80, 60) - # if 261 < self._mouse_x <= 320 and 141 < self._mouse_y <= 181: - # self.__draw_rectangle(cr, 261, 130, 80, 60) - # if 261 < self._mouse_x <= 320 and 182 < self._mouse_y <= 221: - # self.__draw_rectangle(cr, 261, 160, 80, 60) - # if 261 < self._mouse_x <= 320 and 100 < self._mouse_y <= 140: - # self.__draw_rectangle(cr, 261, 100, 80, 60) - # if 261 < self._mouse_x <= 320 and 141 < self._mouse_y <= 181: - # self.__draw_rectangle(cr, 261, 130, 80, 60) - # if 261 < self._mouse_x <= 320 and 182 < self._mouse_y <= 221: - # self.__draw_rectangle(cr, 261, 160, 80, 60) - # if 261 < self._mouse_x <= 320 and 221 < self._mouse_y <= 280: - # self.__draw_rectangle(cr, 261, 220, 80, 60) - - current_box, x, y = self.get_cursor_positons() - if current_box is not None: - self.__draw_rectangle(cr, x, y, 80, 60) - - cr.set_source_rgb(1, 0.1, 0) - cr.fill() - cr.stroke() - - def get_cursor_positons(self): - """Returns position of mouse and which box it is located in. - - Format: - 1) [x,y] - which box it cursor is in in x,y format - 2) x - x axis point for top left of rectangle to be drawn. - 3) y - y axis point for top left of rectangle to be drawn. - """ - if 20 < self._mouse_x <= 100 and 60 < self._mouse_y <= 99: - return [1, 1], 20, 40 - if 20 < self._mouse_x <= 100 < self._mouse_y <= 140: - return [1, 2], 20, 80 - if 20 < self._mouse_x <= 100 and 141 < self._mouse_y <= 181: - return [1, 3], 20, 130 - if 20 < self._mouse_x <= 100 and 182 < self._mouse_y <= 221: - return [1, 4], 20, 160 - if 20 < self._mouse_x <= 100 and 221 < self._mouse_y <= 280: - return [1, 5], 20, 220 - - if 100 < self._mouse_x <= 153 and 60 < self._mouse_y <= 99: - return [2, 1], 100, 40 - if 100 < self._mouse_x <= 153 and 100 < self._mouse_y <= 140: - return [2, 2], 100, 100 - if 100 < self._mouse_x <= 153 and 141 < self._mouse_y <= 181: - return [2, 3], 100, 130 - if 100 < self._mouse_x <= 153 and 182 < self._mouse_y <= 221: - return [2, 4], 100, 160 - if 100 < self._mouse_x <= 153 and 221 < self._mouse_y <= 280: - return [2, 5], 100, 220 - - if 154 < self._mouse_x <= 204 and 60 < self._mouse_y <= 99: - return [3, 1], 140, 40 - if 154 < self._mouse_x <= 204 and 141 < self._mouse_y <= 181: - return [3, 2], 140, 130 - if 154 < self._mouse_x <= 204 and 182 < self._mouse_y <= 221: - return [3, 3], 140, 160 - if 154 < self._mouse_x <= 204 and 100 < self._mouse_y <= 140: - return [3, 4], 140, 100 - if 154 < self._mouse_x <= 204 and 221 < self._mouse_y <= 280: - return [3, 5], 140, 220 - - if 205 < self._mouse_x <= 260 and 60 < self._mouse_y <= 99: - return [4, 1], 180, 40 - if 205 < self._mouse_x <= 260 and 100 < self._mouse_y <= 140: - return [4, 2], 180, 100 - if 205 < self._mouse_x <= 260 and 141 < self._mouse_y <= 181: - return [4, 3], 180, 130 - if 205 < self._mouse_x <= 260 and 182 < self._mouse_y <= 221: - return [4, 3], 180, 160 - if 205 < self._mouse_x <= 260 and 221 < self._mouse_y <= 280: - return [4, 5], 180, 220 - - if 261 < self._mouse_x <= 320 and 60 < self._mouse_y <= 99: - return [5, 1], 261, 40 - if 261 < self._mouse_x <= 320 and 100 < self._mouse_y <= 140: - return [5, 2], 261, 100 - if 261 < self._mouse_x <= 320 and 141 < self._mouse_y <= 181: - return [5, 3], 261, 130 - if 261 < self._mouse_x <= 320 and 182 < self._mouse_y <= 221: - return [5, 4], 261, 160 - if 261 < self._mouse_x <= 320 and 221 < self._mouse_y <= 280: - return [5, 5], 261, 220 - - return None, 0, 0 - - def __set_source(self, source): - self.source = source - - def _selection_changed_cb(self, unused_timeline): - if len(self._selection) == 1: - clip = list(self._selection)[0] - source = clip.find_track_element(None, GES.VideoSource) - if source: - self._selected_clip = clip - self.__set_source(source) - self.app.gui.editor.viewer.overlay_stack.select(source) - self._set_object_values() - return - - # Deselect - if self._selected_clip: - self._selected_clip = None - self._project.pipeline.commit_timeline() - self.__set_source(None) - - def _set_object_values(self): - # Get all the necessary information - project_height = self._project.videoheight - project_width = self._project.videowidth - video_height = self._get_clip_width_height()[1] * .8 - video_width = self._get_clip_width_height()[0] * .8 - middle_x = video_width / 2 - middle_y = video_height / 2 - # Set all the values for the left, outside the viewer - Position.TOP_LEFT_CORNER_OUT = (-video_width, -video_height) - Position.LEFT_TOP_OUT = (-video_width, 0) - Position.LEFT_CENTER_OUT = (-video_width, project_height / 2 - middle_y) - Position.LEFT_BOTTOM_OUT = (-video_width, project_height - video_height) - Position.BOTTOM_LEFT_CORNER_OUT = (-video_width, video_height) - # Set all the values for the right, outside the viewer - Position.TOP_RIGHT_CORNER_OUT = (project_width, -video_height) - Position.RIGHT_TOP_OUT = (project_width, -video_height) - Position.RIGHT_CENTER_OUT = (project_width, project_height / 2 - middle_y) - Position.RIGHT_BOTTOM_OUT = (project_width, project_height - video_height) - Position.BOTTOM_RIGHT_CORNER_OUT = (project_width, project_height) - # Set all the values for the top section, outside of the viewer - Position.TOP_LEFT_OUT = (0, -video_height) - Position.TOP_CENTER_OUT = (project_width / 2 - middle_x, -video_height) - Position.TOP_RIGHT_OUT = (project_width - video_width, -video_height) - # Set all the values for the bottom section, outside of the viewer - Position.BOTTOM_LEFT_OUT = (0, project_height) - Position.BOTTOM_CENTER_OUT = (project_width / 2 - middle_x, project_height) - Position.BOTTOM_RIGHT_OUT = (project_width - video_width, project_height) - # Set all the values for inside the viewer - Position.TOP_LEFT = (0, 0) - Position.TOP_CENTER = (project_width / 2 - middle_x, 0) - Position.TOP_RIGHT = (project_width - video_width, 0) - Position.LEFT_CENTER = (0, project_height / 2 - middle_y) - Position.CENTER = (project_width / 2 - middle_x, project_height / 2 - middle_y) - Position.RIGHT_CENTER = (project_width - video_width, project_height / 2 - middle_y) - Position.BOTTOM_LEFT = (0, project_height - video_height) - Position.BOTTOM_CENTER = (middle_x, project_height - middle_y * 2) - Position.BOTTOM_RIGHT = (project_width - video_width, project_height - video_height) -- GitLab From 2d509b33fdb3aac17f6b20e8b6ea57169a9a4de9 Mon Sep 17 00:00:00 2001 From: Cordell Rhoads Date: Mon, 20 Apr 2020 20:57:59 +0000 Subject: [PATCH 6/7] Delete editorperspective.py --- pitivi/editorperspective.py | 878 ------------------------------------ 1 file changed, 878 deletions(-) delete mode 100644 pitivi/editorperspective.py diff --git a/pitivi/editorperspective.py b/pitivi/editorperspective.py deleted file mode 100644 index f3e3cb4f1..000000000 --- a/pitivi/editorperspective.py +++ /dev/null @@ -1,878 +0,0 @@ -# -*- coding: utf-8 -*- -# Pitivi video editor -# Copyright (c) 2005, Edward Hervey -# -# 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 . -import os -from gettext import gettext as _ -from time import time -from urllib.parse import unquote - -from gi.repository import Gdk -from gi.repository import GES -from gi.repository import Gio -from gi.repository import Gtk - -from pitivi.clipalignment import AlignmentEditor -from pitivi.clipproperties import ClipProperties -from pitivi.configure import APPNAME -from pitivi.configure import get_ui_dir -from pitivi.dialogs.missingasset import MissingAssetDialog -from pitivi.effects import EffectListWidget -from pitivi.interactiveintro import InteractiveIntro -from pitivi.mediafilespreviewer import PreviewWidget -from pitivi.medialibrary import MediaLibraryWidget -from pitivi.perspective import Perspective -from pitivi.project import ProjectSettingsDialog -from pitivi.settings import GlobalSettings -from pitivi.tabsmanager import BaseTabs -from pitivi.timeline.previewers import ThumbnailCache -from pitivi.timeline.timeline import TimelineContainer -from pitivi.titleeditor import TitleEditor -from pitivi.transitions import TransitionsListWidget -from pitivi.utils.loggable import Loggable -from pitivi.utils.misc import path_from_uri -from pitivi.utils.ui import beautify_time_delta -from pitivi.utils.ui import EDITOR_PERSPECTIVE_CSS -from pitivi.utils.ui import info_name -from pitivi.viewer.viewer import ViewerContainer - - -GlobalSettings.add_config_section("main-window") -GlobalSettings.add_config_option('mainWindowHPanePosition', - section="main-window", - key="hpane-position", - type_=int) -GlobalSettings.add_config_option('mainWindowMainHPanePosition', - section="main-window", - key="main-hpane-position", - type_=int) -GlobalSettings.add_config_option('mainWindowVPanePosition', - section="main-window", - key="vpane-position", - type_=int) -GlobalSettings.add_config_option('lastProjectFolder', - section="main-window", - key="last-folder", - environment="PITIVI_PROJECT_FOLDER", - default=os.path.expanduser("~")) - - -class EditorPerspective(Perspective, Loggable): - """Pitivi's Editor perspective. - - Attributes: - app (Pitivi): The app. - """ - - def __init__(self, app): - Perspective.__init__(self) - Loggable.__init__(self) - - self.app = app - self.settings = app.settings - - self.builder = Gtk.Builder() - - pm = self.app.project_manager - pm.connect("new-project-loaded", - self._project_manager_new_project_loaded_cb) - pm.connect("save-project-failed", - self._project_manager_save_project_failed_cb) - pm.connect("project-saved", self._project_manager_project_saved_cb) - pm.connect("closing-project", self._project_manager_closing_project_cb) - pm.connect("reverting-to-saved", - self._project_manager_reverting_to_saved_cb) - pm.connect("project-closed", self._project_manager_project_closed_cb) - pm.connect("missing-uri", self._project_manager_missing_uri_cb) - - def setup_ui(self): - """Sets up the UI.""" - self.__setup_css() - self._create_ui() - self.app.gui.connect("focus-in-event", self.__focus_in_event_cb) - self.app.gui.connect("destroy", self._destroyed_cb) - - def activate_compact_mode(self): - """Shrinks widgets to suit better a small screen.""" - self.medialibrary.activate_compact_mode() - self.viewer.activate_compact_mode() - - def refresh(self): - """Refreshes the perspective.""" - self.focus_timeline() - - def __setup_css(self): - css_provider = Gtk.CssProvider() - css_provider.load_from_data(EDITOR_PERSPECTIVE_CSS.encode("UTF-8")) - screen = Gdk.Screen.get_default() - style_context = self.app.gui.get_style_context() - style_context.add_provider_for_screen(screen, css_provider, - Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) - - def __focus_in_event_cb(self, unused_widget, unused_event): - ges_timeline = self.timeline_ui.timeline.ges_timeline - if not ges_timeline: - # Nothing to work with, Pitivi is starting up. - return - - # Commit the timeline so its nested timelines assets are refreshed. - ges_timeline.commit() - - # We need to track the changed assets ourselves. - changed_files_uris = ThumbnailCache.update_caches() - if changed_files_uris: - self.medialibrary.update_asset_thumbs(changed_files_uris) - - for ges_layer in ges_timeline.get_layers(): - for ges_clip in ges_layer.get_clips(): - if ges_clip.get_asset().props.id in changed_files_uris: - if ges_clip.ui.audio_widget: - ges_clip.ui.audio_widget.update_previewer() - if ges_clip.ui.video_widget: - ges_clip.ui.video_widget.update_previewer() - - def _destroyed_cb(self, unused_main_window): - """Cleanup before destroying this window.""" - pm = self.app.project_manager - pm.disconnect_by_func(self._project_manager_new_project_loaded_cb) - pm.disconnect_by_func(self._project_manager_save_project_failed_cb) - pm.disconnect_by_func(self._project_manager_project_saved_cb) - pm.disconnect_by_func(self._project_manager_closing_project_cb) - pm.disconnect_by_func(self._project_manager_reverting_to_saved_cb) - pm.disconnect_by_func(self._project_manager_project_closed_cb) - pm.disconnect_by_func(self._project_manager_missing_uri_cb) - self.toplevel_widget.remove(self.timeline_ui) - self.timeline_ui.destroy() - - def _render_cb(self, unused_button): - """Shows the RenderDialog for the current project.""" - from pitivi.render import RenderDialog - - project = self.app.project_manager.current_project - dialog = RenderDialog(self.app, project) - dialog.window.show() - - def _create_ui(self): - """Creates the graphical interface. - - The rough hierarchy is: - vpaned: - - mainhpaned(secondhpaned(main_tabs, context_tabs), viewer) - - timeline_ui - - The full hierarchy can be admired by starting the GTK+ Inspector - with Ctrl+Shift+I. - """ - # pylint: disable=attribute-defined-outside-init - # Main "toolbar" (using client-side window decorations with HeaderBar) - self.headerbar = self.__create_headerbar() - - # Set up our main containers, in the order documented above - - # Separates the tabs+viewer from the timeline - self.toplevel_widget = Gtk.Paned(orientation=Gtk.Orientation.VERTICAL) - # Separates the tabs from the viewer - self.mainhpaned = Gtk.Paned(orientation=Gtk.Orientation.HORIZONTAL) - # Separates the two sets of tabs - self.secondhpaned = Gtk.Paned(orientation=Gtk.Orientation.HORIZONTAL) - self.toplevel_widget.pack1(self.mainhpaned, resize=False, shrink=False) - self.mainhpaned.pack1(self.secondhpaned, resize=True, shrink=False) - self.toplevel_widget.show() - self.secondhpaned.show() - self.mainhpaned.show() - - # First set of tabs - self.main_tabs = BaseTabs(self.app) - self.medialibrary = MediaLibraryWidget(self.app) - self.effectlist = EffectListWidget(self.app) - self.main_tabs.append_page("Media Library", - self.medialibrary, Gtk.Label(label=_("Media Library"))) - self.main_tabs.append_page("Effect Library", - self.effectlist, Gtk.Label(label=_("Effect Library"))) - self.medialibrary.connect('play', self._media_library_play_cb) - self.medialibrary.show() - self.effectlist.show() - - # Second set of tabs - self.context_tabs = BaseTabs(self.app) - self.clipconfig = ClipProperties(self.app) - self.trans_list = TransitionsListWidget(self.app) - self.title_editor = TitleEditor(self.app) - self.alignment_editor = AlignmentEditor(self.app) - self.context_tabs.append_page("Clip", - self.clipconfig, Gtk.Label(label=_("Clip"))) - self.context_tabs.append_page("Transition", - self.trans_list, Gtk.Label(label=_("Transition"))) - self.context_tabs.append_page("Title", - self.title_editor.widget, Gtk.Label(label=_("Title"))) - self.context_tabs.append_page("Alignment", - self.alignment_editor, Gtk.Label(label=_("Alignment"))) - # Show by default the Title tab, as the Clip and Transition tabs - # are useful only when a clip or transition is selected, but - # the Title tab allows adding titles. - self.context_tabs.set_current_page(2) - - self.secondhpaned.pack1(self.main_tabs, resize=False, shrink=False) - self.secondhpaned.pack2(self.context_tabs, resize=False, shrink=False) - self.main_tabs.show() - self.context_tabs.show() - - # Viewer - self.viewer = ViewerContainer(self.app) - self.mainhpaned.pack2(self.viewer, resize=True, shrink=False) - - # Now, the lower part: the timeline - self.timeline_ui = TimelineContainer(self.app) - self.toplevel_widget.pack2(self.timeline_ui, resize=True, shrink=False) - - self.intro = InteractiveIntro(self.app) - self.headerbar.pack_end(self.intro.intro_button) - - # Setup shortcuts for HeaderBar buttons and menu items. - self._create_actions() - - # Identify widgets for AT-SPI, making our test suite easier to develop - # These will show up in sniff, accerciser, etc. - self.headerbar.get_accessible().set_name("editor_headerbar") - self.menu_button.get_accessible().set_name("main menu button") - self.toplevel_widget.get_accessible().set_name("contents") - self.mainhpaned.get_accessible().set_name("upper half") - self.secondhpaned.get_accessible().set_name("tabs") - self.main_tabs.get_accessible().set_name("primary tabs") - self.context_tabs.get_accessible().set_name("secondary tabs") - self.viewer.get_accessible().set_name("viewer") - self.timeline_ui.get_accessible().set_name("timeline area") - - # Restore settings for position and visibility. - if self.settings.mainWindowHPanePosition is None: - self._set_default_positions() - self.secondhpaned.set_position(self.settings.mainWindowHPanePosition) - self.mainhpaned.set_position(self.settings.mainWindowMainHPanePosition) - self.toplevel_widget.set_position(self.settings.mainWindowVPanePosition) - - def _set_default_positions(self): - window_width = self.app.gui.get_size()[0] - if self.settings.mainWindowHPanePosition is None: - self.settings.mainWindowHPanePosition = window_width / 3 - if self.settings.mainWindowMainHPanePosition is None: - self.settings.mainWindowMainHPanePosition = 2 * window_width / 3 - if self.settings.mainWindowVPanePosition is None: - screen_width = float(self.app.gui.get_screen().get_width()) - screen_height = float(self.app.gui.get_screen().get_height()) - req = self.toplevel_widget.get_preferred_size()[0] - if screen_width / screen_height < 0.75: - # Tall screen, give some more vertical space the the tabs. - value = req.height / 3 - else: - value = req.height / 2 - self.settings.mainWindowVPanePosition = value - - def switch_context_tab(self, ges_clip): - """Activates the appropriate tab on the second set of tabs. - - Args: - ges_clip (GES.SourceClip): The clip which has been focused. - """ - if isinstance(ges_clip, GES.TitleClip): - page = 2 - elif isinstance(ges_clip, GES.SourceClip): - page = 0 - elif isinstance(ges_clip, GES.TransitionClip): - page = 1 - else: - self.warning("Unknown clip type: %s", ges_clip) - return - self.context_tabs.set_current_page(page) - - def focus_timeline(self): - layers_representation = self.timeline_ui.timeline.layout - # Check whether it has focus already, grab_focus always emits an event. - if not layers_representation.props.is_focus: - layers_representation.grab_focus() - - def __create_headerbar(self): - headerbar = Gtk.HeaderBar() - headerbar.set_show_close_button(True) - - undo_button = Gtk.Button.new_from_icon_name( - "edit-undo-symbolic", Gtk.IconSize.SMALL_TOOLBAR) - undo_button.set_always_show_image(True) - undo_button.set_label(_("Undo")) - undo_button.set_action_name("app.undo") - undo_button.set_use_underline(True) - - redo_button = Gtk.Button.new_from_icon_name( - "edit-redo-symbolic", Gtk.IconSize.SMALL_TOOLBAR) - redo_button.set_always_show_image(True) - redo_button.set_action_name("app.redo") - redo_button.set_use_underline(True) - - # pylint: disable=attribute-defined-outside-init - self.save_button = Gtk.Button.new_with_label(_("Save")) - self.save_button.set_focus_on_click(False) - - self.render_button = Gtk.Button.new_from_icon_name( - "system-run-symbolic", Gtk.IconSize.SMALL_TOOLBAR) - self.render_button.set_always_show_image(True) - self.render_button.set_label(_("Render")) - self.render_button.set_tooltip_text( - _("Export your project as a finished movie")) - self.render_button.set_sensitive(False) # The only one we have to set. - self.render_button.connect("clicked", self._render_cb) - - undo_redo_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0) - undo_redo_box.get_style_context().add_class("linked") - undo_redo_box.pack_start(undo_button, expand=False, fill=False, padding=0) - undo_redo_box.pack_start(redo_button, expand=False, fill=False, padding=0) - headerbar.pack_start(undo_redo_box) - - self.builder.add_from_file( - os.path.join(get_ui_dir(), "mainmenubutton.ui")) - - self.menu_button = self.builder.get_object("menubutton") - self.keyboard_shortcuts_button = self.builder.get_object("menu_shortcuts") - - headerbar.pack_end(self.menu_button) - headerbar.pack_end(self.save_button) - headerbar.pack_end(self.render_button) - headerbar.show_all() - - return headerbar - - def _create_actions(self): - group = Gio.SimpleActionGroup() - self.toplevel_widget.insert_action_group("editor", group) - self.headerbar.insert_action_group("editor", group) - - # pylint: disable=attribute-defined-outside-init - self.save_action = Gio.SimpleAction.new("save", None) - self.save_action.connect("activate", self.__save_project_cb) - group.add_action(self.save_action) - self.app.shortcuts.add("editor.save", ["s"], self.save_action, - _("Save the current project"), group="win") - self.save_button.set_action_name("editor.save") - - self.save_as_action = Gio.SimpleAction.new("save-as", None) - self.save_as_action.connect("activate", self.__save_project_as_cb) - group.add_action(self.save_as_action) - self.app.shortcuts.add("editor.save-as", ["s"], - self.save_as_action, - _("Save the current project as"), group="win") - - self.revert_to_saved_action = Gio.SimpleAction.new("revert-to-saved", None) - self.revert_to_saved_action.connect("activate", self.__revert_to_saved_cb) - group.add_action(self.revert_to_saved_action) - - self.export_project_action = Gio.SimpleAction.new("export-project", None) - self.export_project_action.connect("activate", self.__export_project_cb) - group.add_action(self.export_project_action) - - self.save_frame_action = Gio.SimpleAction.new("save-frame", None) - self.save_frame_action.connect("activate", self.__save_frame_cb) - group.add_action(self.save_frame_action) - - self.project_settings_action = Gio.SimpleAction.new("project-settings", None) - self.project_settings_action.connect("activate", self.__project_settings_cb) - group.add_action(self.project_settings_action) - - group.add_action(self.intro.intro_action) - self.app.shortcuts.add("editor.interactive-intro", [], self.intro.intro_action, - _("Quick intros to Pitivi"), group="win") - - self.import_asset_action = Gio.SimpleAction.new("import-asset", None) - self.import_asset_action.connect("activate", self.__import_asset_cb) - group.add_action(self.import_asset_action) - self.app.shortcuts.add("editor.import-asset", ["i"], - self.import_asset_action, - _("Add media files to your project"), group="win") - - def __import_asset_cb(self, unused_action, unused_param): - self.medialibrary.show_import_assets_dialog() - - def show_project_status(self): - project = self.app.project_manager.current_project - dirty = project.has_unsaved_modifications() - self.save_action.set_enabled(dirty) - self.revert_to_saved_action.set_enabled(bool(project.uri) and dirty) - self.update_title() - -# UI Callbacks - - def _media_library_play_cb(self, unused_medialibrary, asset): - """Previews the specified asset. - - If the media library item to preview is an image, show it in the user's - favorite image viewer. Else, preview the video/sound in Pitivi. - """ - # Technically, our preview widget can show images, but it's never going - # to do a better job (sizing, zooming, metadata, editing, etc.) - # than the user's favorite image viewer. - if asset.is_image(): - Gio.AppInfo.launch_default_for_uri(asset.get_id(), None) - else: - preview_window = PreviewAssetWindow(asset, self.app) - preview_window.preview() - - def _project_changed_cb(self, unused_project): - self.save_action.set_enabled(True) - self.update_title() - -# Toolbar/Menu actions callback - - def __save_project_cb(self, unused_action, unused_param): - self.save_project() - - def __save_project_as_cb(self, unused_action, unused_param): - self.save_project_as() - - def save_project(self): - if not self.app.project_manager.current_project.uri or self.app.project_manager.disable_save: - self.save_project_as() - else: - self.app.project_manager.save_project() - - def __revert_to_saved_cb(self, unused_action, unused_param): - self.app.project_manager.revert_to_saved_project() - - def __export_project_cb(self, unused_action, unused_param): - uri = self._show_export_dialog(self.app.project_manager.current_project) - result = None - if uri: - result = self.app.project_manager.export_project( - self.app.project_manager.current_project, uri) - - if not result: - self.log("Project couldn't be exported") - return result - - def __project_settings_cb(self, unused_action, unused_param): - self.show_project_settings_dialog() - - def show_project_settings_dialog(self): - project = self.app.project_manager.current_project - dialog = ProjectSettingsDialog(self.app.gui, project, self.app) - dialog.window.run() - self.update_title() - -# Project management callbacks - - def _project_manager_new_project_loaded_cb(self, project_manager, project): - """Connects the UI to the specified project. - - Args: - project_manager (ProjectManager): The project manager. - project (Project): The project which has been loaded. - """ - self.log("A new project has been loaded") - - self._connect_to_project(project) - project.pipeline.activate_position_listener() - - self.clipconfig.project = project - - self.timeline_ui.set_project(project) - - # When creating a blank project there's no project URI yet. - if project.uri: - folder_path = os.path.dirname(path_from_uri(project.uri)) - self.settings.lastProjectFolder = folder_path - - self.update_title() - - if project_manager.disable_save is True: - # Special case: we enforce "Save as", but the normal "Save" button - # redirects to it if needed, so we still want it to be enabled: - self.save_action.set_enabled(True) - - if project.ges_timeline.props.duration != 0: - self.render_button.set_sensitive(True) - - def _project_manager_save_project_failed_cb(self, unused_project_manager, uri, exception=None): - project_filename = unquote(uri.split("/")[-1]) - dialog = Gtk.MessageDialog(transient_for=self.app.gui, - modal=True, - message_type=Gtk.MessageType.ERROR, - buttons=Gtk.ButtonsType.OK, - text=_('Unable to save project "%s"') % project_filename) - if exception: - dialog.set_property("secondary-use-markup", True) - dialog.set_property("secondary-text", unquote(str(exception))) - dialog.set_transient_for(self.app.gui) - dialog.run() - dialog.destroy() - self.error("failed to save project") - - def _project_manager_project_saved_cb(self, unused_project_manager, unused_project, unused_uri): - self.update_title() - self.save_action.set_enabled(False) - - def _project_manager_closing_project_cb(self, project_manager, project): - """Investigates whether it's possible to close the specified project. - - Args: - project_manager (ProjectManager): The project manager. - project (Project): The project which has been closed. - - Returns: - bool: True when it's OK to close it, False when the user chooses - to cancel the closing operation. - """ - if not project.has_unsaved_modifications(): - return True - - if project.uri and not project_manager.disable_save: - save = _("Save") - else: - save = _("Save as...") - - dialog = Gtk.MessageDialog(transient_for=self.app.gui, modal=True) - reject_btn = dialog.add_button(_("Close without saving"), - Gtk.ResponseType.REJECT) - - dialog.add_buttons(_("Cancel"), Gtk.ResponseType.CANCEL, - save, Gtk.ResponseType.YES) - - dialog.set_default_response(Gtk.ResponseType.CANCEL) - dialog.get_accessible().set_name("unsaved changes dialog") - reject_btn.get_style_context().add_class("destructive-action") - - primary = _("Save changes to the current project before closing?") - dialog.props.use_markup = True - dialog.props.text = "" + primary + "" - - if project.uri: - path = unquote(project.uri).split("file://")[1] - last_saved = max( - os.path.getmtime(path), project_manager.time_loaded) - time_delta = time() - last_saved - message = _("If you don't save, " - "the changes from the last %s will be lost.") % \ - beautify_time_delta(time_delta) - else: - message = _("If you don't save, your changes will be lost.") - - dialog.props.secondary_text = message - - response = dialog.run() - dialog.destroy() - - if response == Gtk.ResponseType.YES: - if project.uri is not None and project_manager.disable_save is False: - res = self.app.project_manager.save_project() - else: - res = self.save_project_as() - elif response == Gtk.ResponseType.REJECT: - res = True - else: - res = False - - return res - - def _project_manager_project_closed_cb(self, project_manager, project): - """Starts disconnecting the UI from the specified project. - - This happens when the user closes the app or asks to load another - project, immediately after the user confirmed that unsaved changes, - if any, can be discarded but before the filechooser to pick the next - project to load appears. - """ - # We must disconnect from the project pipeline before it is released: - if project.pipeline is not None: - project.pipeline.deactivate_position_listener() - - self.info("Project closed") - if project.loaded: - self._disconnect_from_project(project) - self.timeline_ui.set_project(None) - self.render_button.set_sensitive(False) - return False - - def _project_manager_reverting_to_saved_cb(self, unused_project_manager, unused_project): - if self.app.project_manager.current_project.has_unsaved_modifications(): - dialog = Gtk.MessageDialog(transient_for=self.app.gui, - modal=True, - message_type=Gtk.MessageType.WARNING, - buttons=Gtk.ButtonsType.NONE, - text=_("Revert to saved project version?")) - dialog.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.NO, - Gtk.STOCK_REVERT_TO_SAVED, Gtk.ResponseType.YES) - dialog.set_resizable(False) - dialog.set_property("secondary-text", - _("This will reload the current project. All unsaved changes will be lost.")) - dialog.set_default_response(Gtk.ResponseType.NO) - dialog.set_transient_for(self.app.gui) - response = dialog.run() - dialog.destroy() - if response != Gtk.ResponseType.YES: - return False - return True - - def _project_manager_missing_uri_cb(self, project_manager, project, unused_error, asset): - if project.at_least_one_asset_missing: - # One asset is already missing so no point in spamming the user - # with more file-missing dialogs, as we need all of them. - return None - - if self.app.proxy_manager.is_proxy_asset(asset): - uri = self.app.proxy_manager.get_target_uri(asset) - else: - uri = asset.get_id() - - dialog = MissingAssetDialog(self.app, asset, uri) - new_uri = dialog.get_new_uri() - - if not new_uri: - dialog.hide() - if not self.app.proxy_manager.check_proxy_loading_succeeded(asset): - # Reset the project manager and disconnect all the signals. - project_manager.close_running_project() - # Signal the project loading failure. - # You have to do this *after* successfully creating a blank project, - # or the startupwizard will still be connected to that signal too. - reason = _("No replacement file was provided for \"%s\".\n\n" - "Pitivi does not currently support partial projects.") % \ - info_name(asset) - project_manager.emit("new-project-failed", project.uri, reason) - - dialog.destroy() - return new_uri - - def _connect_to_project(self, project): - project.connect("project-changed", self._project_changed_cb) - project.ges_timeline.connect("notify::duration", - self._timeline_duration_changed_cb) - - def _disconnect_from_project(self, project): - project.disconnect_by_func(self._project_changed_cb) - project.ges_timeline.disconnect_by_func(self._timeline_duration_changed_cb) - - def _timeline_duration_changed_cb(self, timeline, unused_duration): - """Updates the render button. - - This covers the case when a clip is inserted into a blank timeline. - This callback is not triggered by loading a project. - """ - duration = timeline.get_duration() - self.debug("Timeline duration changed to %s", duration) - self.render_button.set_sensitive(duration > 0) - - def _show_export_dialog(self, project): - self.log("Export requested") - chooser = Gtk.FileChooserDialog(title=_("Export To..."), - transient_for=self.app.gui, - action=Gtk.FileChooserAction.SAVE) - chooser.add_buttons(_("Cancel"), Gtk.ResponseType.CANCEL, - _("Save"), Gtk.ResponseType.OK) - chooser.set_default_response(Gtk.ResponseType.OK) - - chooser.set_select_multiple(False) - chooser.props.do_overwrite_confirmation = True - - asset = GES.Formatter.get_default() - asset_extension = asset.get_meta(GES.META_FORMATTER_EXTENSION) - - chooser.set_current_name( - project.name + "." + asset_extension + "_tar") - - filt = Gtk.FileFilter() - filt.set_name(_("Tar archive")) - filt.add_pattern("*.%s_tar" % asset_extension) - chooser.add_filter(filt) - default = Gtk.FileFilter() - default.set_name(_("Detect automatically")) - default.add_pattern("*") - chooser.add_filter(default) - - response = chooser.run() - if response == Gtk.ResponseType.OK: - self.log("User chose a URI to export project to") - # need to do this to work around bug in Gst.uri_construct - # which escapes all /'s in path! - uri = "file://" + chooser.get_filename() - self.log("uri: %s", uri) - ret = uri - else: - self.log("User didn't choose a URI to export project to") - ret = None - - chooser.destroy() - return ret - - def save_project_as(self): - uri = self._show_save_as_dialog() - if uri is None: - return False - return self.app.project_manager.save_project(uri) - - def _show_save_as_dialog(self): - self.log("Save URI requested") - chooser = Gtk.FileChooserDialog(title=_("Save As..."), - transient_for=self.app.gui, - action=Gtk.FileChooserAction.SAVE) - chooser.add_buttons(_("Cancel"), Gtk.ResponseType.CANCEL, - _("Save"), Gtk.ResponseType.OK) - chooser.set_default_response(Gtk.ResponseType.OK) - asset = GES.Formatter.get_default() - filt = Gtk.FileFilter() - filt.set_name(asset.get_meta(GES.META_DESCRIPTION)) - filt.add_pattern("*.%s" % asset.get_meta(GES.META_FORMATTER_EXTENSION)) - chooser.add_filter(filt) - - chooser.set_select_multiple(False) - chooser.set_current_name(_("Untitled") + "." + - asset.get_meta(GES.META_FORMATTER_EXTENSION)) - chooser.set_current_folder(self.settings.lastProjectFolder) - chooser.props.do_overwrite_confirmation = True - - default = Gtk.FileFilter() - default.set_name(_("Detect automatically")) - default.add_pattern("*") - chooser.add_filter(default) - - response = chooser.run() - if response == Gtk.ResponseType.OK: - self.log("User chose a URI to save project to") - # need to do this to work around bug in Gst.uri_construct - # which escapes all /'s in path! - uri = "file://" + chooser.get_filename() - file_filter = chooser.get_filter().get_name() - self.log("uri:%s , filter:%s", uri, file_filter) - self.settings.lastProjectFolder = chooser.get_current_folder() - ret = uri - else: - self.log("User didn't choose a URI to save project to") - ret = None - - chooser.destroy() - return ret - - def __save_frame_cb(self, unused_action, unused_param): - """Exports a snapshot of the current frame as an image file.""" - res = self._show_save_screenshot_dialog() - if res: - path, mime = res[0], res[1] - self.app.project_manager.current_project.pipeline.save_thumbnail( - -1, -1, mime, path) - - def _show_save_screenshot_dialog(self): - """Asks the user where to save the current frame. - - Returns: - List[str]: The full path and the mimetype if successful, None otherwise. - """ - chooser = Gtk.FileChooserDialog(title=_("Save As..."), - transient_for=self.app.gui, action=Gtk.FileChooserAction.SAVE) - chooser.add_buttons(_("Cancel"), Gtk.ResponseType.CANCEL, - _("Save"), Gtk.ResponseType.OK) - chooser.set_default_response(Gtk.ResponseType.OK) - chooser.set_select_multiple(False) - chooser.set_current_name(_("Untitled")) - chooser.props.do_overwrite_confirmation = True - formats = {_("PNG image"): ["image/png", ("png",)], - _("JPEG image"): ["image/jpeg", ("jpg", "jpeg")]} - for image_format in formats: - filt = Gtk.FileFilter() - filt.set_name(image_format) - filt.add_mime_type(formats.get(image_format)[0]) - chooser.add_filter(filt) - response = chooser.run() - if response == Gtk.ResponseType.OK: - chosen_format = formats.get(chooser.get_filter().get_name()) - chosen_ext = chosen_format[1][0] - chosen_mime = chosen_format[0] - uri = os.path.join( - chooser.get_current_folder(), chooser.get_filename()) - ret = ["%s.%s" % (uri, chosen_ext), chosen_mime] - else: - ret = None - chooser.destroy() - return ret - - def update_title(self): - project = self.app.project_manager.current_project - unsaved_mark = "" - if project.has_unsaved_modifications(): - unsaved_mark = "*" - title = "%s%s — %s" % (unsaved_mark, project.name, APPNAME) - self.headerbar.set_title(title) - - -class PreviewAssetWindow(Gtk.Window): - """Window for previewing a video or audio asset. - - Args: - asset (GES.UriClipAsset): The asset to be previewed. - app (Pitivi): The app. - """ - - def __init__(self, asset, app): - Gtk.Window.__init__(self) - self._asset = asset - self.app = app - - self.set_title(_("Preview")) - self.set_type_hint(Gdk.WindowTypeHint.UTILITY) - self.set_transient_for(app.gui) - - self._previewer = PreviewWidget(app.settings, minimal=True) - self.add(self._previewer) - self._previewer.preview_uri(self._asset.get_id()) - self._previewer.show() - - self.connect("focus-out-event", self._leave_preview_cb) - - def preview(self): - """Shows the window and starts the playback.""" - width, height = self._calculate_preview_window_size() - self.resize(width, height) - # Setting the position of the window only works if it's currently hidden - # otherwise, after the resize the position will not be readjusted - self.set_position(Gtk.WindowPosition.CENTER_ON_PARENT) - self.show() - - self._previewer.play() - # Hack so that we really really force the "utility" window to be - # focused - self.present() - - def _calculate_preview_window_size(self): - info = self._asset.get_info() - video_streams = info.get_video_streams() - if not video_streams: - # There is no video/image stream. This is an audio file. - # Resize to the minimum and let the window manager deal with it. - return 1, 1 - # For videos and images, automatically resize the window - # Try to keep it 1:1 if it can fit within 85% of the parent window - video = video_streams[0] - img_width = video.get_natural_width() - img_height = video.get_natural_height() - mainwindow_width, mainwindow_height = self.app.gui.get_size() - max_width = 0.85 * mainwindow_width - max_height = 0.85 * mainwindow_height - - controls_height = self._previewer.bbox.get_preferred_size()[0].height - if img_width < max_width and (img_height + controls_height) < max_height: - # The video is small enough, keep it 1:1 - return img_width, img_height + controls_height - else: - # The video is too big, size it down - # TODO: be smarter, figure out which (width, height) is bigger - new_height = max_width * img_height / img_width - return int(max_width), int(new_height + controls_height) - - def _leave_preview_cb(self, window, unused): - self.destroy() - return True -- GitLab From edb20071118aabe966877a5b4855e1fc8dd60879 Mon Sep 17 00:00:00 2001 From: Cordell Rhoads Date: Mon, 20 Apr 2020 20:58:37 +0000 Subject: [PATCH 7/7] Delete debug.log --- pitivi/debug.log | 1 - 1 file changed, 1 deletion(-) delete mode 100644 pitivi/debug.log diff --git a/pitivi/debug.log b/pitivi/debug.log deleted file mode 100644 index a0e78f071..000000000 --- a/pitivi/debug.log +++ /dev/null @@ -1 +0,0 @@ -bash: bin/pitivi: No such file or directory -- GitLab