diff --git a/help/C/alignment.page b/help/C/alignment.page new file mode 100644 index 0000000000000000000000000000000000000000..6c07205f9263534674176d7be9b882da85c87320 --- /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 1b4af16e06a76cc0f68f9d722f0cc00e3221ea39..99091a11274b23fdcad541ca52ff94ef5c982941 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 Binary files /dev/null and b/help/C/figures/interactivealignment.png differ diff --git a/help/C/figures/manualalignment.png b/help/C/figures/manualalignment.png new file mode 100644 index 0000000000000000000000000000000000000000..792cf446bbff190db1215d7b40d94c3a7340158b Binary files /dev/null and b/help/C/figures/manualalignment.png differ diff --git a/help/C/figures/vieweralignment.png b/help/C/figures/vieweralignment.png new file mode 100644 index 0000000000000000000000000000000000000000..e9f872ab043f6550c3243848b728cd0de2b3d607 Binary files /dev/null and b/help/C/figures/vieweralignment.png differ diff --git a/help/C/index.page b/help/C/index.page index 0dff9ce6afb936a8644184c5c4050eab0146db83..d2b60b7aad7c49cfd6daa29e52b3a2e31154515b 100644 --- a/help/C/index.page +++ b/help/C/index.page @@ -30,8 +30,8 @@
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 c1167130b00838a0f0da5d997336524768e1dc05..0ac9ba395a81edf0074a8482c5c827dc172e970c 100644 --- a/help/C/layers.page +++ b/help/C/layers.page @@ -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 e4fdbe0704939d9f1744077abe8baa68a76c1405..cf012801f6ccde7b983e26a9b759582663317c25 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 3730ccf8aed6d81c38d404e4839621e16f8cb5c4..fc7a45b6eb0e8bd5bf9ac57f532541c6e1569767 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 2abee3b210b9c8959074c94bd0b37861d987eaff..22c4df60252b9fbdf060c3c82575a50c5f122f9d 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 ca7212c4f04c8a337d6a42a061ed515863582361..0d93eccbc6237b76089eba30899983b9fe5629f1 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/editorperspective.py b/pitivi/editorperspective.py deleted file mode 100644 index 8b921515887b217aa66ec80d069555c500a101ef..0000000000000000000000000000000000000000 --- a/pitivi/editorperspective.py +++ /dev/null @@ -1,874 +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.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.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"))) - # 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 diff --git a/pitivi/timeline/layer.py b/pitivi/timeline/layer.py index d8e2658eb24c0217620fbcfcebd8a197ba4ffb94..8e5a52f10826a7dacfb321ad8e32c34e5288739c 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 0cf2f7c70af9d80e41002c0d105ae2ac91472f12..44e7b72c75b99c51ca8c19fe31c062c0fcfc53c3 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 3803fcbb814d82289c85ab6d542a793fc9780136..13e7cde550e73ad38f39a631387a79d5e80035f1 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 bedf006c722fab9529de17905a07c86d828bdaa1..4bbe167baa69065044811e050bd96462adcf0ac4 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 ac628486a2151deac08f638f900ae42bc6977245..ebb496adcc0b9d3b3b4dd65ee41e7eb4fc7ef685 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 fa0bcf2494e6a4a20add49ec09f24f45553b7481..ba3935e42a0069e3f51cfe3bd6ab505bd1181a1d 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 d77a4fa6b3ec1d82d72165ac6cc046d61820163b..e094302ce0688b9d71a5335a3ae83985ca48d991 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 29a7976d1c75287c7ef5ddc01282069ed0c2a935..95fcde20be69ee6fba048315ebf8921253de7860 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)