[pitivi/render_experience: 2/2] Render perspective: restructuring of render settings. Fixes #2382




commit 5d12e09731df71d5b9a65e035fb926762e8966f0
Author: Ayush Mittal <ayush mittal9398 gmail com>
Date:   Sun May 17 15:13:43 2020 +0000

    Render perspective: restructuring of render settings.
    Fixes #2382

 data/pixmaps/presets/youtube.png                 | Bin 0 -> 42449 bytes
 data/ui/renderingdialog.ui                       | 133 ++++++---
 pitivi/preset.py                                 | 122 --------
 pitivi/render.py                                 | 354 +++++++++++++++++++++--
 pitivi/utils/misc.py                             |   4 +
 tests/__init__.py                                |   3 +-
 tests/test-encoding-targets/test/test-remove.gep |  10 +
 tests/test_render.py                             | 122 ++++----
 8 files changed, 487 insertions(+), 261 deletions(-)
---
diff --git a/data/pixmaps/presets/youtube.png b/data/pixmaps/presets/youtube.png
new file mode 100755
index 00000000..582a86b4
Binary files /dev/null and b/data/pixmaps/presets/youtube.png differ
diff --git a/data/ui/renderingdialog.ui b/data/ui/renderingdialog.ui
index 38d4f38b..985337cc 100644
--- a/data/ui/renderingdialog.ui
+++ b/data/ui/renderingdialog.ui
@@ -1,11 +1,10 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<!-- Generated with glade 3.22.1 -->
+<!-- Generated with glade 3.36.0 -->
 <interface>
-  <requires lib="gtk+" version="3.10"/>
+  <requires lib="gtk+" version="3.22"/>
   <object class="GtkAdjustment" id="adjustment1">
     <property name="lower">1</property>
     <property name="upper">100</property>
-    <property name="value">1</property>
     <property name="step_increment">1</property>
     <property name="page_increment">1</property>
   </object>
@@ -14,6 +13,15 @@
     <property name="can_focus">False</property>
     <property name="icon_name">question-round-symbolic</property>
   </object>
+  <object class="GtkPopover" id="preset_popover">
+    <property name="can_focus">False</property>
+    <property name="relative_to">preset_selection_menubutton</property>
+    <property name="position">right</property>
+    <property name="constrain_to">none</property>
+    <child>
+      <placeholder/>
+    </child>
+  </object>
   <object class="GtkDialog" id="render-dialog">
     <property name="can_focus">False</property>
     <property name="border_width">12</property>
@@ -22,9 +30,6 @@
     <property name="window_position">center-on-parent</property>
     <property name="default_height">400</property>
     <property name="type_hint">dialog</property>
-    <child>
-      <placeholder/>
-    </child>
     <child internal-child="vbox">
       <object class="GtkBox" id="dialog-vbox3">
         <property name="visible">True</property>
@@ -36,20 +41,6 @@
             <property name="visible">True</property>
             <property name="can_focus">False</property>
             <property name="layout_style">end</property>
-            <child>
-              <object class="GtkButton" id="closebutton">
-                <property name="label" translatable="yes">Close</property>
-                <property name="visible">True</property>
-                <property name="can_focus">True</property>
-                <property name="receives_default">True</property>
-                <signal name="clicked" handler="_close_button_clicked_cb" swapped="no"/>
-              </object>
-              <packing>
-                <property name="expand">False</property>
-                <property name="fill">False</property>
-                <property name="position">0</property>
-              </packing>
-            </child>
             <child>
               <object class="GtkButton" id="render_button">
                 <property name="label" translatable="yes">Render</property>
@@ -94,7 +85,6 @@
                     <property name="halign">start</property>
                     <property name="hexpand">True</property>
                     <property name="action">select-folder</property>
-                    <signal name="current-folder-changed" handler="_current_folder_changed_cb" swapped="no"/>
                   </object>
                   <packing>
                     <property name="left_attach">1</property>
@@ -136,13 +126,26 @@
                     <property name="visible">True</property>
                     <property name="can_focus">True</property>
                     <property name="activates_default">True</property>
-                    <signal name="changed" handler="_filename_changed_cb" swapped="no"/>
                   </object>
                   <packing>
                     <property name="left_attach">1</property>
                     <property name="top_attach">1</property>
                   </packing>
                 </child>
+                <child>
+                  <object class="GtkLabel" id="label6">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="halign">start</property>
+                    <property name="valign">start</property>
+                    <property name="xpad">0</property>
+                    <property name="label" translatable="yes">Preset:</property>
+                  </object>
+                  <packing>
+                    <property name="left_attach">0</property>
+                    <property name="top_attach">2</property>
+                  </packing>
+                </child>
                 <child>
                   <object class="GtkLabel" id="label2">
                     <property name="visible">True</property>
@@ -202,53 +205,82 @@
                   </packing>
                 </child>
                 <child>
-                  <object class="GtkGrid" id="preset_table">
+                  <object class="GtkBox" id="preset_box">
                     <property name="visible">True</property>
                     <property name="can_focus">False</property>
-                    <property name="halign">end</property>
+                    <property name="baseline_position">top</property>
                     <child>
-                      <object class="GtkMenuButton" id="preset_menubutton">
+                      <object class="GtkMenuButton" id="preset_selection_menubutton">
                         <property name="visible">True</property>
                         <property name="can_focus">True</property>
                         <property name="receives_default">True</property>
-                        <property name="relief">none</property>
+                        <property name="direction">right</property>
+                        <property name="popover">preset_popover</property>
                         <child>
-                          <object class="GtkImage" id="image1">
+                          <object class="GtkGrid" id="preset_button_grid">
                             <property name="visible">True</property>
                             <property name="can_focus">False</property>
-                            <property name="icon_name">open-menu-symbolic</property>
+                            <property name="margin_start">3</property>
+                            <property name="margin_end">3</property>
+                            <property name="margin_top">3</property>
+                            <property name="margin_bottom">3</property>
+                            <property name="row_spacing">4</property>
+                            <child>
+                              <object class="GtkImage" id="preset_icon">
+                                <property name="visible">True</property>
+                                <property name="can_focus">False</property>
+                                <property name="stock">gtk-missing-image</property>
+                              </object>
+                              <packing>
+                                <property name="left_attach">0</property>
+                                <property name="top_attach">0</property>
+                              </packing>
+                            </child>
+                            <child>
+                              <object class="GtkLabel" id="preset_label">
+                                <property name="visible">True</property>
+                                <property name="can_focus">False</property>
+                                <property name="label" translatable="yes">Custom</property>
+                              </object>
+                              <packing>
+                                <property name="left_attach">0</property>
+                                <property name="top_attach">1</property>
+                              </packing>
+                            </child>
                           </object>
                         </child>
                       </object>
                       <packing>
-                        <property name="left_attach">2</property>
-                        <property name="top_attach">0</property>
+                        <property name="expand">False</property>
+                        <property name="fill">True</property>
+                        <property name="position">0</property>
                       </packing>
                     </child>
                     <child>
-                      <object class="GtkLabel" id="label6">
+                      <object class="GtkMenuButton" id="preset_menubutton">
                         <property name="visible">True</property>
-                        <property name="can_focus">False</property>
-                        <property name="halign">start</property>
-                        <property name="xpad">10</property>
-                        <property name="label" translatable="yes">Preset:</property>
-                        <attributes>
-                          <attribute name="weight" value="bold"/>
-                        </attributes>
+                        <property name="can_focus">True</property>
+                        <property name="receives_default">True</property>
+                        <property name="valign">start</property>
+                        <property name="relief">none</property>
+                        <child>
+                          <object class="GtkImage" id="image1">
+                            <property name="visible">True</property>
+                            <property name="can_focus">False</property>
+                            <property name="icon_name">open-menu-symbolic</property>
+                          </object>
+                        </child>
                       </object>
                       <packing>
-                        <property name="left_attach">0</property>
-                        <property name="top_attach">0</property>
+                        <property name="expand">False</property>
+                        <property name="fill">True</property>
+                        <property name="position">1</property>
                       </packing>
                     </child>
-                    <child>
-                      <placeholder/>
-                    </child>
                   </object>
                   <packing>
-                    <property name="left_attach">0</property>
+                    <property name="left_attach">1</property>
                     <property name="top_attach">2</property>
-                    <property name="width">2</property>
                   </packing>
                 </child>
               </object>
@@ -303,7 +335,7 @@
                                 <property name="can_focus">True</property>
                                 <property name="halign">start</property>
                                 <property name="invisible_char">•</property>
-                                <property name="text" translatable="no">1.0</property>
+                                <property name="text">1.0</property>
                                 <property name="primary_icon_activatable">False</property>
                                 <property name="secondary_icon_activatable">False</property>
                                 <property name="adjustment">adjustment1</property>
@@ -328,7 +360,7 @@
                                 <property name="visible">True</property>
                                 <property name="can_focus">False</property>
                                 <property name="halign">start</property>
-                                <property name="label" translatable="no">1000 x 1000</property>
+                                <property name="label">1000 x 1000</property>
                                 <property name="width_chars">12</property>
                                 <property name="xalign">0</property>
                                 <child internal-child="accessible">
@@ -665,6 +697,7 @@
 
 This option is a good trade-off between quality of the rendered video and stability.</property>
                     <property name="halign">start</property>
+                    <property name="active">True</property>
                     <property name="draw_indicator">True</property>
                   </object>
                   <packing>
@@ -681,6 +714,7 @@ This option is a good trade-off between quality of the rendered video and stabil
                     <property name="receives_default">False</property>
                     <property name="tooltip_markup" translatable="yes">Render all proxied clips from the 
proxy assets. There might be some quality loss during the rendering process.</property>
                     <property name="halign">start</property>
+                    <property name="active">True</property>
                     <property name="draw_indicator">True</property>
                   </object>
                   <packing>
@@ -698,6 +732,7 @@ This option is a good trade-off between quality of the rendered video and stabil
                     <property name="tooltip_markup" translatable="yes">Always use source assets for 
rendering. It is the best choice for the quality of the rendered video, but you might hit some bugs because 
of the use of not officially supported media formats.
 &lt;i&gt;Use at your own risk!&lt;/i&gt;</property>
                     <property name="halign">start</property>
+                    <property name="active">True</property>
                     <property name="draw_indicator">True</property>
                   </object>
                   <packing>
@@ -723,8 +758,10 @@ This option is a good trade-off between quality of the rendered video and stabil
       </object>
     </child>
     <action-widgets>
-      <action-widget response="0">closebutton</action-widget>
       <action-widget response="0">render_button</action-widget>
     </action-widgets>
+    <child type="titlebar">
+      <placeholder/>
+    </child>
   </object>
 </interface>
diff --git a/pitivi/preset.py b/pitivi/preset.py
index d0db955e..8196ab5a 100644
--- a/pitivi/preset.py
+++ b/pitivi/preset.py
@@ -21,7 +21,6 @@ from gettext import gettext as _
 from gi.repository import Gio
 from gi.repository import GObject
 from gi.repository import Gst
-from gi.repository import GstPbutils
 from gi.repository import Gtk
 
 from pitivi.configure import get_audiopresets_dir
@@ -483,124 +482,3 @@ class AudioPresetManager(PresetManager):
         return {
             "channels": project.audiochannels,
             "sample-rate": project.audiorate}
-
-
-class EncodingTargetManager(PresetManager):
-    """Manager of EncodingTargets used as render presets.
-
-    Uses the GstEncodingTarget API to discover and access the EncodingProfiles.
-
-    Attributes:
-        _project (Project): The project.
-    """
-
-    __gsignals__ = {
-        "profile-selected": (GObject.SignalFlags.RUN_LAST, None, (GstPbutils.EncodingProfile,)),
-    }
-
-    def __init__(self, project):
-        PresetManager.__init__(self)
-        self._project = project
-        self._removed_file_list = os.path.join(xdg_data_home(),
-                                               'hidden_encoding_profiles.json')
-        try:
-            with open(self._removed_file_list) as removed:
-                self._removed_profiles = json.loads(removed.read())
-        except FileNotFoundError:
-            self._removed_profiles = []
-
-    def _add_target(self, target):
-        profiles = target.get_profiles()
-        for profile in profiles:
-            name = target.get_name().split(';')[0]
-            if len(profiles) != 1 and profile.get_name().lower() != 'default':
-                name += '_' + profile.get_name()
-
-            if name in self._removed_profiles:
-                continue
-
-            self.presets[name] = profile
-            self._add_preset(name, profile)
-
-    def load_all(self):
-        """Loads profiles from GstEncodingTarget and add them to self.combo.
-
-        Override from PresetManager
-        """
-        for target in GstPbutils.encoding_list_all_targets():
-            if target.get_category() != GstPbutils.ENCODING_CATEGORY_FILE_EXTENSION:
-                self._add_target(target)
-
-    def create_preset(self, name, values=None):
-        self._save_current_preset_as_target(name)
-
-    def get_new_preset_name(self):
-        """Gets a unique name for a new preset."""
-        # Translators: This must contain exclusively low case alphanum and '-'
-        name = _("new-profile")
-        i = 1
-        while self.has_preset(name):
-            # Translators: This must contain exclusively low case alphanum and '-'
-            name = _("new-profile-%d") % i
-            i += 1
-        return name
-
-    def save_current_preset(self, new_name=None):
-        """Saves currently selected profile on disk.
-
-        Override from PresetManager
-
-        Args:
-            new_name (str): The name to save current Gst.EncodingProfile as.
-        """
-        if not self.combo.get_parent().valid:
-            self.error("Current encoding target name is not valid")
-            return
-
-        self._save_current_preset_as_target(new_name)
-
-    def _save_current_preset_as_target(self, new_name):
-        if new_name in self._removed_profiles:
-            self._removed_profiles.remove(new_name)
-            self._save_removed_profiles()
-
-        target = GstPbutils.EncodingTarget.new(new_name, "user-defined",
-                                               new_name,
-                                               [self._project.container_profile])
-        target.save()
-
-        self._add_target(target)
-
-    def select_preset(self, combo):
-        """Selects preset from currently active row in @combo.
-
-        Override from PresetManager
-
-        Args:
-            combo (Gtk.ComboBox): The combo to retrieve selected GstEncodingProfile from.
-        """
-        active_iter = combo.get_active_iter()
-        name = None
-        if active_iter:
-            # The user selected a preset.
-            name = combo.props.model.get_value(active_iter, 0)
-            profile = combo.props.model.get_value(active_iter, 1)
-            self.emit("profile-selected", profile)
-        self.cur_preset = name
-
-    def _save_removed_profiles(self):
-        with open(self._removed_file_list, 'w') as removed:
-            json.dump(self._removed_profiles, removed)
-
-    def remove_current_preset(self):
-        self._removed_profiles.append(self.cur_preset)
-        self._save_removed_profiles()
-        self._forget_preset(self.cur_preset)
-        self.cur_preset = None
-
-    def restore_preset(self, values):
-        """Raises NotImplemented as it does not make sense for that class.
-
-        Override from PresetManager
-        """
-        raise NotImplementedError
diff --git a/pitivi/render.py b/pitivi/render.py
index 3de26887..8cf20874 100644
--- a/pitivi/render.py
+++ b/pitivi/render.py
@@ -20,17 +20,19 @@ import posixpath
 import time
 from gettext import gettext as _
 
+from gi.repository import GdkPixbuf
 from gi.repository import GES
 from gi.repository import Gio
 from gi.repository import GLib
 from gi.repository import GObject
 from gi.repository import Gst
+from gi.repository import GstPbutils
 from gi.repository import Gtk
 
 from pitivi import configure
 from pitivi.check import MISSING_SOFT_DEPS
-from pitivi.preset import EncodingTargetManager
 from pitivi.utils.loggable import Loggable
+from pitivi.utils.misc import cmp
 from pitivi.utils.misc import path_from_uri
 from pitivi.utils.misc import show_user_manual
 from pitivi.utils.ripple_update_group import RippleUpdateGroup
@@ -41,7 +43,275 @@ from pitivi.utils.ui import create_frame_rates_model
 from pitivi.utils.ui import get_combo_value
 from pitivi.utils.ui import set_combo_value
 from pitivi.utils.widgets import GstElementSettingsDialog
-from pitivi.utils.widgets import TextWidget
+
+
+# The category of GstPbutils.EncodingTarget objects holding
+# a GstPbutils.EncodingProfile used as a Pitivi render preset.
+PITIVI_ENCODING_TARGET_CATEGORY = "user-defined"
+
+
+def set_icon_and_title(icon, title, preset_item, icon_size=Gtk.IconSize.DND):
+    """Adds icon for the respective preset.
+
+    Args:
+        icon (Gtk.Image): The image widget to be updated.
+        title (Gtk.Label): The label widget to be updated.
+        preset_item (PresetItem): Preset profile related information.
+        icon_size (Gtk.IconSize): Size of the icon.
+    """
+    icon_files = {
+        "youtube": "youtube.png"
+    }
+    icon_names = {
+        "dvd": "media-optical-dvd-symbolic"
+    }
+
+    if not preset_item:
+        display_name = _("Custom")
+        icon_name = "applications-multimedia-symbolic"
+    else:
+        display_name = preset_item.display_name
+        icon_name = preset_item.name
+
+    title.props.label = display_name
+    title.set_xalign(0)
+    title.set_yalign(0)
+
+    if icon_name in icon_files:
+        icon_filename = icon_files[icon_name]
+        icon_path = os.path.join(configure.get_pixmap_dir(), "presets", icon_filename)
+
+        res, width, height = Gtk.IconSize.lookup(icon_size)
+        assert res
+
+        pic = GdkPixbuf.Pixbuf.new_from_file_at_scale(icon_path, height, width, True)
+        icon.set_from_pixbuf(pic)
+        return
+
+    icon_name = icon_names.get(icon_name, "applications-multimedia-symbolic")
+    icon.set_from_icon_name(icon_name, icon_size)
+    icon.props.valign = Gtk.Align.START
+
+
+class PresetItem(GObject.Object):
+
+    def __init__(self, name, target, profile):
+        GObject.Object.__init__(self)
+
+        name_dict = {
+            "youtube": _("YouTube"),
+            "dvd": _("DVD")
+        }
+
+        self.name = name
+        self.target = target
+        self.profile = profile
+        self.display_name = name_dict.get(name, name)
+
+    @staticmethod
+    def compare_func(item1, item2, *unused_data):
+        user_defined1 = item1.target.get_category() == PITIVI_ENCODING_TARGET_CATEGORY
+        user_defined2 = item2.target.get_category() == PITIVI_ENCODING_TARGET_CATEGORY
+        if user_defined1 != user_defined2:
+            return cmp(user_defined1, user_defined2)
+
+        return cmp(item1.name, item2.name)
+
+
+class PresetBoxRow(Gtk.ListBoxRow):
+    """ListBoxRow displaying a render preset.
+
+    Attributes:
+        preset_item (PresetItem): Preset profile related information.
+    """
+
+    def __init__(self, preset_item):
+        Gtk.ListBoxRow.__init__(self)
+
+        self.preset_item = preset_item
+        grid = Gtk.Grid()
+
+        title = Gtk.Label()
+        icon = Gtk.Image()
+        set_icon_and_title(icon, title, preset_item)
+
+        description = Gtk.Label(preset_item.target.get_description())
+        description.set_xalign(0)
+        description.set_line_wrap(True)
+        description.props.max_width_chars = 30
+
+        grid.attach(title, 1, 0, 1, 1)
+        grid.attach(description, 1, 1, 1, 1)
+        grid.attach(icon, 0, 0, 1, 2)
+        grid.set_row_spacing(6)
+        grid.set_column_spacing(10)
+        grid.set_row_homogeneous(False)
+        grid.props.margin = 6
+        self.add(grid)
+
+
+class PresetsManager(GObject.Object, Loggable):
+    """Manager of EncodingProfiles used as render presets.
+
+    The EncodingProfiles are retrieved from the available EncodingTargets.
+    An EncodingTarget can contain multiple EncodingProfiles.
+
+    The render presets created by us are stored as EncodingProfiles,
+    each in its own EncodingTarget.
+
+    Attributes:
+        cur_preset_item (PresetItem): The currently selected PresetItem.
+        model (Gio.ListStore): The model to store PresetItems for all the preset-profiles.
+        project (Project): The project holding the container_profile to be saved or turned into a new preset.
+    """
+
+    __gsignals__ = {
+        "profile-selected": (GObject.SignalFlags.RUN_LAST, None, (PresetItem,))
+    }
+
+    def __init__(self, project):
+        GObject.Object.__init__(self)
+        Loggable.__init__(self)
+
+        self.project = project
+
+        # menu button actions
+        self.action_new = None
+        self.action_remove = None
+        self.action_save = None
+
+        self.cur_preset_item = None
+        self.model = Gio.ListStore.new(PresetItem)          # preset profiles model
+
+        self.load_all()
+
+    def load_all(self):
+        """Loads profiles from GstEncodingTarget and add them to self.model."""
+        for target in GstPbutils.encoding_list_all_targets():
+            if target.get_category() != GstPbutils.ENCODING_CATEGORY_FILE_EXTENSION:
+                self._add_target(target)
+
+    def preset_menubutton_setup(self, button):
+        action_group = Gio.SimpleActionGroup()
+        menu_model = Gio.Menu()
+
+        action = Gio.SimpleAction.new("new", None)
+        action.connect("activate", self._add_preset_cb)
+        action_group.add_action(action)
+        menu_model.append(_("New"), "preset.%s" % action.get_name())
+        self.action_new = action
+
+        action = Gio.SimpleAction.new("remove", None)
+        action.connect("activate", self._remove_preset_cb)
+        action_group.add_action(action)
+        menu_model.append(_("Remove"), "preset.%s" % action.get_name())
+        self.action_remove = action
+
+        action = Gio.SimpleAction.new("save", None)
+        action.connect("activate", self._save_preset_cb)
+        action_group.add_action(action)
+        menu_model.append(_("Save"), "preset.%s" % action.get_name())
+        self.action_save = action
+
+        self.action_remove.set_enabled(False)
+        self.action_save.set_enabled(False)
+        menu = Gtk.Menu.new_from_model(menu_model)
+        menu.insert_action_group("preset", action_group)
+        button.set_popup(menu)
+
+    def _add_preset_cb(self, unused_action, unused_param):
+        preset_name = self.get_new_preset_name()
+        self.select_preset(self.create_preset(preset_name))
+
+    def _remove_preset_cb(self, unused_action, unused_param):
+        self.action_remove.set_enabled(False)
+        self.action_save.set_enabled(False)
+
+        if not self.cur_preset_item:
+            return
+
+        # There is only one EncodingProfile in the EncodingTarget.
+        preset_path = self.cur_preset_item.target.get_path()
+        if preset_path:
+            os.remove(preset_path)
+
+        res, pos = self.model.find(self.cur_preset_item)
+        assert res, self.cur_preset_item.name
+        self.model.remove(pos)
+
+        self.cur_preset_item = None
+        self.emit("profile-selected", None)
+
+    def _save_preset_cb(self, unused_action, unused_param):
+        name = self.cur_preset_item.target.get_name()
+
+        res, pos = self.model.find(self.cur_preset_item)
+        assert res, self.cur_preset_item.name
+        self.model.remove(pos)
+
+        self.cur_preset_item = self.create_preset(name)
+        self.emit("profile-selected", self.cur_preset_item)
+
+    def _add_target(self, encoding_target):
+        """Adds the profiles of the specified encoding_target as render presets.
+
+        Args:
+            encoding_target (GstPbutils.EncodingTarget): An encoding target.
+        """
+        preset_items = []
+        for profile in encoding_target.get_profiles():
+            # The name can be for example "youtube;yt"
+            name = encoding_target.get_name().split(";")[0]
+            if len(encoding_target.get_profiles()) != 1 and profile.get_name().lower() != "default":
+                name += "_" + profile.get_name()
+
+            preset_item = PresetItem(name, encoding_target, profile)
+            self.model.insert_sorted(preset_item, PresetItem.compare_func)
+            preset_items.append(preset_item)
+
+        return preset_items
+
+    def has_preset(self, name):
+        name = name.lower()
+        preset_names = (item.name for item in self.model)
+        return any(name == preset.lower() for preset in preset_names)
+
+    def create_preset(self, preset_name):
+        """Creates a preset, overwriting the preset with the same name if any.
+
+        Args:
+            preset_name (str): The name for the new preset created.
+            values (dict): The values of the new preset.
+        """
+        target = GstPbutils.EncodingTarget.new(preset_name, PITIVI_ENCODING_TARGET_CATEGORY,
+                                               "",
+                                               [self.project.container_profile])
+        target.save()
+        return self._add_target(target)[0]
+
+    def get_new_preset_name(self):
+        """Gets a unique name for a new preset."""
+        # Translators: This must contain exclusively low case alphanum and '-'
+        name = _("new-profile")
+        i = 1
+        while self.has_preset(name):
+            # Translators: This must contain exclusively low case alphanum and '-'
+            name = _("new-profile-%d") % i
+            i += 1
+        return name
+
+    def select_preset(self, preset_item):
+        """Selects preset from currently active row in preset listbox.
+
+        Args:
+            preset_item (PresetItem): The row representing the preset to be applied.
+        """
+        self.cur_preset_item = preset_item
+        writable = len(preset_item.target.get_profiles()) == 1 and os.access(preset_item.target.get_path(), 
os.W_OK)
+
+        self.action_remove.set_enabled(writable)
+        self.action_save.set_enabled(writable)
+        self.emit("profile-selected", preset_item)
 
 
 class Encoders(Loggable):
@@ -132,7 +402,7 @@ class Encoders(Loggable):
 
         for fact in Gst.ElementFactory.list_get_elements(
                 Gst.ELEMENT_FACTORY_TYPE_ENCODER, Gst.Rank.SECONDARY):
-            klist = fact.get_klass().split('/')
+            klist = fact.get_klass().split("/")
             if "Video" in klist or "Image" in klist:
                 self.vencoders.append(fact)
             elif "Audio" in klist:
@@ -421,14 +691,15 @@ class RenderDialog(Loggable):
         # {object: sigId}
         self._gst_signal_handlers_ids = {}
 
-        self.render_presets = EncodingTargetManager(project)
-        self.render_presets.connect('profile-selected', self._encoding_profile_selected_cb)
+        self.presets_manager = PresetsManager(project)
+        self.presets_manager.connect("profile-selected", self._presets_manager_profile_selected_cb)
 
         # Whether encoders changing are a result of changing the muxer.
         self.muxer_combo_changing = False
-        self._create_ui()
         self.progress = None
         self.dialog = None
+        self.preset_listbox = None
+        self._create_ui()
 
         # Directory and Filename
         self.filebutton.set_current_folder(self.app.settings.lastExportFolder)
@@ -470,8 +741,7 @@ class RenderDialog(Loggable):
         self.widgets_group.add_vertex(self.muxer_combo, signal="changed")
         self.widgets_group.add_vertex(self.audio_encoder_combo, signal="changed")
         self.widgets_group.add_vertex(self.video_encoder_combo, signal="changed")
-        self.widgets_group.add_vertex(self.preset_menubutton,
-                                      update_func=self._update_preset_menu_button)
+        self.widgets_group.add_vertex(self.preset_menubutton, signal="clicked")
 
         self.widgets_group.add_edge(self.frame_rate_combo, self.preset_menubutton)
         self.widgets_group.add_edge(self.audio_encoder_combo, self.preset_menubutton)
@@ -480,8 +750,12 @@ class RenderDialog(Loggable):
         self.widgets_group.add_edge(self.channels_combo, self.preset_menubutton)
         self.widgets_group.add_edge(self.sample_rate_combo, self.preset_menubutton)
 
-    def _encoding_profile_selected_cb(self, unused_target, encoding_profile):
-        self._set_encoding_profile(encoding_profile)
+    def _presets_manager_profile_selected_cb(self, unused_target, preset_item):
+        """Handles the selection of a render preset."""
+        set_icon_and_title(self.preset_icon, self.preset_label, preset_item)
+
+        if preset_item:
+            self._set_encoding_profile(preset_item.profile)
 
     def _set_encoding_profile(self, encoding_profile, recursing=False):
         old_profile = self.project.container_profile
@@ -498,7 +772,7 @@ class RenderDialog(Loggable):
         self.project.set_container_profile(encoding_profile)
         self._setting_encoding_profile = True
 
-        if not set_combo_value(self.muxer_combo, factory('muxer')):
+        if not set_combo_value(self.muxer_combo, factory("muxer")):
             rollback()
             return
 
@@ -528,9 +802,6 @@ class RenderDialog(Loggable):
         self._update_file_extension()
         self._setting_encoding_profile = False
 
-    def _update_preset_menu_button(self, unused_source, unused_target):
-        self.render_presets.update_menu_actions()
-
     def _create_ui(self):
         builder = Gtk.Builder()
         builder.add_from_file(
@@ -559,25 +830,25 @@ class RenderDialog(Loggable):
         self.filebutton = builder.get_object("filebutton")
         self.fileentry = builder.get_object("fileentry")
         self.resolution_label = builder.get_object("resolution_label")
+        self.preset_selection_menubutton = builder.get_object("preset_selection_menubutton")
+        self.preset_label = builder.get_object("preset_label")
+        self.preset_icon = builder.get_object("preset_icon")
         self.preset_menubutton = builder.get_object("preset_menubutton")
-
-        text_widget = TextWidget(matches=r'^[a-z][a-z-0-9-]+$', combobox=True)
-        self.presets_combo = text_widget.combo
-        preset_table = builder.get_object("preset_table")
-        preset_table.attach(text_widget, 1, 0, 1, 1)
-        text_widget.show()
+        self.preset_popover = builder.get_object("preset_popover")
 
         self.__automatically_use_proxies = builder.get_object(
             "automatically_use_proxies")
 
+        set_icon_and_title(self.preset_icon, self.preset_label, None)
+        self.preset_selection_menubutton.connect("clicked", self._preset_selection_menubutton_clicked_cb)
+
         self.__always_use_proxies = builder.get_object("always_use_proxies")
         self.__always_use_proxies.props.group = self.__automatically_use_proxies
 
         self.__never_use_proxies = builder.get_object("never_use_proxies")
         self.__never_use_proxies.props.group = self.__automatically_use_proxies
 
-        self.render_presets.setup_ui(self.presets_combo, self.preset_menubutton)
-        self.render_presets.load_all()
+        self.presets_manager.preset_menubutton_setup(self.preset_menubutton)
 
         self.window.set_icon_name("system-run-symbolic")
         self.window.set_transient_for(self.app.gui)
@@ -590,6 +861,27 @@ class RenderDialog(Loggable):
         self.video_output_checkbutton.props.active = media_types & GES.TrackType.VIDEO
         self._update_video_widgets_sensitivity()
 
+        self.listbox_setup()
+
+    def listbox_setup(self):
+        self.preset_listbox = Gtk.ListBox()
+        self.preset_listbox.set_selection_mode(Gtk.SelectionMode.NONE)
+
+        self.preset_listbox.bind_model(self.presets_manager.model, self._create_preset_row_func)
+
+        self.preset_listbox.connect("row-activated", self._preset_listbox_row_activated_cb)
+        self.preset_popover.add(self.preset_listbox)
+
+    def _create_preset_row_func(self, preset_item):
+        return PresetBoxRow(preset_item)
+
+    def _preset_listbox_row_activated_cb(self, listbox, row):
+        self.presets_manager.select_preset(row.preset_item)
+        self.preset_popover.hide()
+
+    def _preset_selection_menubutton_clicked_cb(self, button):
+        self.preset_popover.show_all()
+
     def _rendering_settings_changed_cb(self, unused_project, unused_item):
         """Handles Project metadata changes."""
         self.update_resolution()
@@ -857,19 +1149,19 @@ class RenderDialog(Loggable):
 
         Args:
             factory (Gst.ElementFactory): The factory for editing.
-            media_type (str): String describing the media type ('audio' or 'video')
+            media_type (str): String describing the media type ("audio" or "video")
         """
         # Reconstitute the property name from the media type (vcodecsettings or acodecsettings)
-        properties = getattr(self.project, media_type[0] + 'codecsettings')
+        properties = getattr(self.project, media_type[0] + "codecsettings")
 
         self.dialog = GstElementSettingsDialog(factory, properties=properties,
-                                               caps=getattr(self.project, media_type + 
'_profile').get_format(),
+                                               caps=getattr(self.project, media_type + 
"_profile").get_format(),
                                                parent_window=self.window)
         self.dialog.ok_btn.connect(
             "clicked", self._ok_button_clicked_cb, media_type)
 
     def __additional_debug_info(self):
-        if self.project.vencoder == 'x264enc':
+        if self.project.vencoder == "x264enc":
             if self.project.videowidth % 2 or self.project.videoheight % 2:
                 return "\n\n%s\n\n" % _("<b>Make sure your rendering size is even, "
                                         "x264enc might not be able to render otherwise.</b>\n\n")
@@ -1034,11 +1326,11 @@ class RenderDialog(Loggable):
     # -- UI callbacks
     def _ok_button_clicked_cb(self, unused_button, media_type):
         assert media_type in ("audio", "video")
-        setattr(self.project, media_type[0] + 'codecsettings', self.dialog.get_settings())
+        setattr(self.project, media_type[0] + "codecsettings", self.dialog.get_settings())
 
         caps = self.dialog.get_caps()
         if caps:
-            getattr(self.project, media_type + '_profile').set_format(caps)
+            getattr(self.project, media_type + "_profile").set_format(caps)
         self.dialog.window.destroy()
 
     def _render_button_clicked_cb(self, unused_button):
@@ -1061,7 +1353,7 @@ class RenderDialog(Loggable):
         self.progress.connect("pause", self._pause_render)
         bus = self._pipeline.get_bus()
         bus.add_signal_watch()
-        self._gst_signal_handlers_ids[bus] = bus.connect('message', self._bus_message_cb)
+        self._gst_signal_handlers_ids[bus] = bus.connect("message", self._bus_message_cb)
         self.project.pipeline.connect("position", self._update_position_cb)
         # Force writing the config now, or the path will be reset
         # if the user opens the rendering dialog again
@@ -1253,7 +1545,7 @@ class RenderDialog(Loggable):
         if self._setting_encoding_profile:
             return
         factory = get_combo_value(self.video_encoder_combo)
-        self._element_settings_dialog(factory, 'video')
+        self._element_settings_dialog(factory, "video")
 
     def _channels_combo_changed_cb(self, combo):
         if self._setting_encoding_profile:
@@ -1279,7 +1571,7 @@ class RenderDialog(Loggable):
 
     def _audio_settings_button_clicked_cb(self, unused_button):
         factory = get_combo_value(self.audio_encoder_combo)
-        self._element_settings_dialog(factory, 'audio')
+        self._element_settings_dialog(factory, "audio")
 
     def _update_file_extension(self):
         # Update the extension of the filename.
diff --git a/pitivi/utils/misc.py b/pitivi/utils/misc.py
index 27b875f6..d983eda2 100644
--- a/pitivi/utils/misc.py
+++ b/pitivi/utils/misc.py
@@ -476,3 +476,7 @@ def asset_get_duration(asset):
         return duration
 
     return asset.get_duration()
+
+
+def cmp(item1, item2):
+    return (item1 > item2) - (item1 < item2)
diff --git a/tests/__init__.py b/tests/__init__.py
index 74b0e1e9..7c88abc6 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -58,8 +58,7 @@ def setup():
 
     _prepend_env_paths(GST_PRESET_PATH=[os.path.join(pitivi_dir, "data", "videopresets"),
                                         os.path.join(pitivi_dir, "data", "audiopresets")],
-                       GST_ENCODING_TARGET_PATH=[os.path.join(pitivi_dir, "tests", "test-encoding-targets"),
-                                                 os.path.join(pitivi_dir, "data", "encoding-profiles")])
+                       GST_ENCODING_TARGET_PATH=[os.path.join(pitivi_dir, "data", "encoding-profiles")])
     os.environ.setdefault('PITIVI_TOP_LEVEL_DIR', pitivi_dir)
 
     # Make sure the modules are initialized correctly.
diff --git a/tests/test-encoding-targets/test/test-remove.gep 
b/tests/test-encoding-targets/test/test-remove.gep
new file mode 100644
index 00000000..c3029a2f
--- /dev/null
+++ b/tests/test-encoding-targets/test/test-remove.gep
@@ -0,0 +1,10 @@
+[GStreamer Encoding Target]
+name=test-remove
+category=test
+description=Just a test
+
+[profile-default]
+name=default
+description=Test ogg container
+type=container
+format=application/ogg
diff --git a/tests/test_render.py b/tests/test_render.py
index 04e312d1..7f864c6a 100644
--- a/tests/test_render.py
+++ b/tests/test_render.py
@@ -17,6 +17,7 @@
 """Tests for the render module."""
 # pylint: disable=protected-access,no-self-use
 import os
+import shutil
 import tempfile
 from unittest import mock
 from unittest import skipUnless
@@ -26,9 +27,10 @@ from gi.repository import Gst
 from gi.repository import GstPbutils
 from gi.repository import Gtk
 
-from pitivi.preset import EncodingTargetManager
 from pitivi.render import Encoders
 from pitivi.render import extension_for_muxer
+from pitivi.render import PresetBoxRow
+from pitivi.render import PresetsManager
 from pitivi.timeline.timeline import TimelineContainer
 from pitivi.utils.proxy import ProxyingStrategy
 from pitivi.utils.ui import get_combo_value
@@ -54,15 +56,36 @@ def encoding_target_exists(tname):
     return False, "EncodingTarget %s not present on the system" % tname
 
 
-def find_preset_row_index(combo, name):
-    """Finds @name in @combo."""
-    for i, row in enumerate(combo.get_model()):
-        if row[0] == name:
-            return i
+def get_preset_model_row(model, name):
+    """Finds @name in preset model."""
+    for item in model:
+        if item.name == name:
+            return PresetBoxRow(item)
 
     return None
 
 
+def setup_render_presets(*profiles):
+    """Temporary directory setup for testing render profiles."""
+    def setup_wrapper(func):
+
+        def wrapped(self):
+            with tempfile.TemporaryDirectory() as tmp_presets_dir:
+                os.mkdir(os.path.join(tmp_presets_dir, "test"))
+
+                for profile in profiles:
+                    path = os.path.join(os.environ["PITIVI_TOP_LEVEL_DIR"], 
"tests/test-encoding-targets/test", profile + ".gep")
+                    tmp_path = os.path.join(tmp_presets_dir, "test", profile + ".gep")
+                    shutil.copy(path, tmp_path)
+
+                os.environ["GST_ENCODING_TARGET_PATH"] = tmp_presets_dir
+                func(self)
+
+        return wrapped
+
+    return setup_wrapper
+
+
 class TestRender(BaseTestMediaLibrary):
     """Tests for functions."""
 
@@ -76,14 +99,13 @@ class TestRender(BaseTestMediaLibrary):
         project = self.create_simple_project()
         with mock.patch("pitivi.preset.xdg_data_home") as xdg_data_home:
             xdg_data_home.return_value = "/pitivi-dir-which-does-not-exist"
-            preset_manager = EncodingTargetManager(project.app)
+            preset_manager = PresetsManager(project.app)
             preset_manager.load_all()
-            self.assertTrue(preset_manager.presets)
-            for unused_name, container_profile in preset_manager.presets.items():
+            for preset_item in preset_manager.model:
                 # Preset name is only set when the project loads it
-                project.set_container_profile(container_profile)
-                muxer = container_profile.get_preset_name()
-                self.assertIsNotNone(extension_for_muxer(muxer), container_profile)
+                project.set_container_profile(preset_item.profile)
+                muxer = preset_item.profile.get_preset_name()
+                self.assertIsNotNone(extension_for_muxer(muxer), preset_item.profile)
 
     def create_simple_project(self):
         """Creates a Project with a layer a clip."""
@@ -160,19 +182,12 @@ class TestRender(BaseTestMediaLibrary):
 
     @skipUnless(*factory_exists("vorbisenc", "theoraenc", "oggmux",
                                 "opusenc", "vp8enc"))
+    @setup_render_presets("test")
     def test_loading_preset(self):
         """Checks preset values are properly exposed in the UI."""
-        def preset_changed_cb(combo, changed):
-            """Callback for the "combo::changed" signal."""
-            changed.append(1)
-
         project = self.create_simple_project()
         dialog = self.create_rendering_dialog(project)
 
-        preset_combo = dialog.render_presets.combo
-        changed = []
-        preset_combo.connect("changed", preset_changed_cb, changed)
-
         test_data = [
             ("test", {"aencoder": "vorbisenc",
                       "vencoder": "theoraenc",
@@ -208,13 +223,13 @@ class TestRender(BaseTestMediaLibrary):
             "muxer": dialog.muxer_combo,
         }
 
-        for preset_name, values in test_data:
-            i = find_preset_row_index(preset_combo, preset_name)
-            self.assertNotEqual(i, None)
+        dialog._preset_selection_menubutton_clicked_cb(None)
+        preset_listbox = dialog.preset_listbox
 
-            del changed[:]
-            preset_combo.set_active(i)
-            self.assertEqual(changed, [1], "Preset %s" % preset_name)
+        for preset_name, values in test_data:
+            row = get_preset_model_row(dialog.presets_manager.model, preset_name)
+            dialog._preset_listbox_row_activated_cb(preset_listbox, row)
+            self.assertEqual(dialog.presets_manager.cur_preset_item.name, preset_name)
 
             for attr, val in values.items():
                 val = val if isinstance(val, list) else [val]
@@ -229,41 +244,31 @@ class TestRender(BaseTestMediaLibrary):
 
     @skipUnless(*factory_exists("vorbisenc", "theoraenc", "oggmux",
                                 "opusenc", "vp8enc"))
+    @setup_render_presets("test-remove")
     def test_remove_profile(self):
         """Tests removing EncodingProfile and re-saving it."""
         project = self.create_simple_project()
         dialog = self.create_rendering_dialog(project)
-        preset_combo = dialog.render_presets.combo
-        i = find_preset_row_index(preset_combo, "test")
-        self.assertIsNotNone(i)
-        preset_combo.set_active(i)
-
-        # Check the 'test' profile is selected
-        active_iter = preset_combo.get_active_iter()
-        self.assertEqual(preset_combo.props.model.get_value(active_iter, 0), "test")
-
-        # Remove current profile and verify it has been removed
-        dialog.render_presets.action_remove.activate()
-        profile_names = [i[0] for i in preset_combo.props.model]
-        active_iter = preset_combo.get_active_iter()
-        self.assertEqual(active_iter, None)
-        self.assertEqual(preset_combo.get_child().props.text, "")
-
-        # Re save the current EncodingProfile calling it the same as before.
-        preset_combo.get_child().set_text("test")
-        self.assertTrue(dialog.render_presets.action_save.get_enabled())
-        dialog.render_presets.action_save.activate(None)
-        self.assertEqual([i[0] for i in preset_combo.props.model],
-                         sorted(profile_names + ["test"]))
-        active_iter = preset_combo.get_active_iter()
-        self.assertEqual(preset_combo.props.model.get_value(active_iter, 0), "test")
+        self.set_preset_listbox_profile_for_dialog(dialog, "test-remove")
+
+        # Check the "test" profile is selected
+        self.assertEqual(dialog.presets_manager.cur_preset_item.name, "test-remove")
+
+        # If EncodingTarget has single profile, PresetItem's name is same as that of the EncodingTarget.
+        profile_name = dialog.presets_manager.cur_preset_item.name
+
+        if self.assertEqual(profile_name, "test-remove"):
+            # Remove current profile and verify it has been removed
+            dialog.presets_manager.action_remove.activate()
+            self.assertIsNone(dialog.presets_manager.cur_preset_item)
+            self.assertEqual(dialog.preset_label.get_text(), "Custom")
 
     def setup_project_with_profile(self, profile_name):
         """Creates a simple project, open the render dialog and select @profile_name."""
         project = self.create_simple_project()
         dialog = self.create_rendering_dialog(project)
 
-        self.set_preset_combo_profile_for_dialog(dialog, profile_name)
+        self.set_preset_listbox_profile_for_dialog(dialog, profile_name)
 
         return project, dialog
 
@@ -429,19 +434,20 @@ class TestRender(BaseTestMediaLibrary):
         _, dialog = self.setup_project_with_profile("youtube")
         self.assertTrue(dialog.fileentry.get_text().endswith("mov"))
 
-        self.set_preset_combo_profile_for_dialog(dialog, "dvd")
+        self.set_preset_listbox_profile_for_dialog(dialog, "dvd")
         self.assertTrue(dialog.fileentry.get_text().endswith("mpeg"))
 
-        self.set_preset_combo_profile_for_dialog(dialog, "youtube")
+        self.set_preset_listbox_profile_for_dialog(dialog, "youtube")
         self.assertTrue(dialog.fileentry.get_text().endswith("mov"))
 
-    def set_preset_combo_profile_for_dialog(self, dialog, profile_name):
+    def set_preset_listbox_profile_for_dialog(self, dialog, profile_name):
         """Sets the preset value for an existing dialog."""
-        preset_combo = dialog.render_presets.combo
+        preset_model = dialog.presets_manager.model
+        preset_listbox = dialog.preset_listbox
         if profile_name:
-            i = find_preset_row_index(preset_combo, profile_name)
-            self.assertIsNotNone(i)
-            preset_combo.set_active(i)
+            row = get_preset_model_row(preset_model, profile_name)
+            self.assertIsNotNone(row)
+            dialog._preset_listbox_row_activated_cb(preset_listbox, row)
 
     @skipUnless(*encoding_target_exists("dvd"))
     def test_rendering_with_dvd_profile(self):


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