[pitivi] greeter: Add project thumbnails
- From: Alexandru Băluț <alexbalut src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [pitivi] greeter: Add project thumbnails
- Date: Thu, 19 Jul 2018 22:18:40 +0000 (UTC)
commit a6511c1ee14e4dd01034e82d7d8930462ab2046b
Author: HarishFulara07 <harish14143 iiitd ac in>
Date: Mon Jul 16 22:58:32 2018 +0530
greeter: Add project thumbnails
data/ui/project_info.ui | 37 +++++++++++++-
pitivi/application.py | 2 +-
pitivi/greeterperspective.py | 2 +
pitivi/medialibrary.py | 24 ++++++----
pitivi/project.py | 112 ++++++++++++++++++++++++++++++++++++++++++-
pitivi/undo/undo.py | 7 +++
pitivi/utils/misc.py | 17 +++++++
pitivi/utils/ui.py | 4 ++
tests/test_misc.py | 32 +++++++++++++
tests/test_undo_project.py | 3 ++
10 files changed, 228 insertions(+), 12 deletions(-)
---
diff --git a/data/ui/project_info.ui b/data/ui/project_info.ui
index 3aa810b9..b880c605 100644
--- a/data/ui/project_info.ui
+++ b/data/ui/project_info.ui
@@ -21,7 +21,38 @@
</packing>
</child>
<child>
- <object class="GtkBox" id="project_info_vbox">
+ <object class="GtkBox">
+ <property name="name">project_thumbnail_box</property>
+ <property name="width_request">96</property>
+ <property name="height_request">54</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="valign">center</property>
+ <property name="margin_right">12</property>
+ <property name="vexpand">True</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkImage" id="project_thumbnail">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="valign">center</property>
+ <property name="vexpand">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
@@ -70,7 +101,9 @@
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
+ <property name="valign">end</property>
<property name="margin_right">200</property>
+ <property name="vexpand">True</property>
</object>
<packing>
<property name="expand">False</property>
@@ -82,7 +115,7 @@
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
- <property name="position">1</property>
+ <property name="position">2</property>
</packing>
</child>
</object>
diff --git a/pitivi/application.py b/pitivi/application.py
index ee47959a..1713c9f4 100644
--- a/pitivi/application.py
+++ b/pitivi/application.py
@@ -151,7 +151,7 @@ class Pitivi(Gtk.Application, Loggable):
"new-project-loading", self._newProjectLoadingCb)
self.project_manager.connect(
"new-project-loaded", self._newProjectLoaded)
- self.project_manager.connect("project-closed", self._projectClosed)
+ self.project_manager.connect_after("project-closed", self._projectClosed)
self.project_manager.connect("project-saved", self.__project_saved_cb)
self._createActions()
diff --git a/pitivi/greeterperspective.py b/pitivi/greeterperspective.py
index dbae89b8..77d260fb 100644
--- a/pitivi/greeterperspective.py
+++ b/pitivi/greeterperspective.py
@@ -29,6 +29,7 @@ from gi.repository import Gtk
from pitivi.configure import get_ui_dir
from pitivi.dialogs.browseprojects import BrowseProjectsDialog
from pitivi.perspective import Perspective
+from pitivi.project import Project
from pitivi.utils.ui import beautify_last_updated_timestamp
from pitivi.utils.ui import beautify_project_path
from pitivi.utils.ui import fix_infobar
@@ -57,6 +58,7 @@ class ProjectInfoRow(Gtk.ListBoxRow):
# show it during projects removal screen.
self.select_button.hide()
+ builder.get_object("project_thumbnail").set_from_pixbuf(Project.get_thumb(self.uri))
builder.get_object("project_name_label").set_text(self.name)
builder.get_object("project_uri_label").set_text(
beautify_project_path(recent_project_item.get_uri_display()))
diff --git a/pitivi/medialibrary.py b/pitivi/medialibrary.py
index e5843e2a..e3ebc00f 100644
--- a/pitivi/medialibrary.py
+++ b/pitivi/medialibrary.py
@@ -252,7 +252,20 @@ class AssetThumbnail(Loggable):
return small_thumb, large_thumb
@staticmethod
- def get_thumbnails_from_xdg_cache(real_uri):
+ def get_asset_thumbnails_path(real_uri):
+ """Gets normal & large thumbnail path for the asset in the XDG cache.
+
+ Returns:
+ List[str]: The path of normal thumbnail and large thumbnail.
+ """
+ quoted_uri = quote_uri(real_uri)
+ thumbnail_hash = md5(quoted_uri.encode()).hexdigest()
+ thumb_dir = os.path.join(GLib.get_user_cache_dir(), "thumbnails")
+ return os.path.join(thumb_dir, "normal", thumbnail_hash + ".png"),\
+ os.path.join(thumb_dir, "large", thumbnail_hash + ".png")
+
+ @classmethod
+ def get_thumbnails_from_xdg_cache(cls, real_uri):
"""Gets pixbufs for the specified thumbnail from the user's cache dir.
Looks for thumbnails according to the [Thumbnail Managing
Standard](https://specifications.freedesktop.org/thumbnail-spec/thumbnail-spec-latest.html#DIRECTORY).
@@ -264,10 +277,7 @@ class AssetThumbnail(Loggable):
List[GdkPixbuf.Pixbuf]: The small thumbnail and the large thumbnail,
if available in the user's cache directory, otherwise (None, None).
"""
- quoted_uri = quote_uri(real_uri)
- thumbnail_hash = md5(quoted_uri.encode()).hexdigest()
- thumb_dir = os.path.join(GLib.get_user_cache_dir(), "thumbnails")
- path_128 = os.path.join(thumb_dir, "normal", thumbnail_hash + ".png")
+ path_128, path_256 = cls.get_asset_thumbnails_path(real_uri)
interpolation = GdkPixbuf.InterpType.BILINEAR
# The cache dirs might have resolutions of 256 and/or 128,
@@ -280,7 +290,6 @@ class AssetThumbnail(Loggable):
return small_thumb, large_thumb
except GLib.GError:
# path_128 doesn't exist, try the 256 version.
- path_256 = os.path.join(thumb_dir, "large", thumbnail_hash + ".png")
try:
thumb_256 = GdkPixbuf.Pixbuf.new_from_file(path_256)
w, h = thumb_256.get_width(), thumb_256.get_height()
@@ -649,8 +658,7 @@ class MediaLibraryWidget(Gtk.Box, Loggable):
rows = [Gtk.TreeRowReference.new(model, path)
for path in paths]
- with self.app.action_log.started("remove asset from media library",
- toplevel=True):
+ with self.app.action_log.started("assets-removal", toplevel=True):
for row in rows:
asset = model[row.get_path()][COL_ASSET]
target = asset.get_proxy_target()
diff --git a/pitivi/project.py b/pitivi/project.py
index 7383147a..3fa29024 100644
--- a/pitivi/project.py
+++ b/pitivi/project.py
@@ -21,11 +21,14 @@
import datetime
import os
import pwd
+import shutil
import tarfile
import time
import uuid
from gettext import gettext as _
+from hashlib import md5
+from gi.repository import GdkPixbuf
from gi.repository import GES
from gi.repository import GLib
from gi.repository import GObject
@@ -35,9 +38,12 @@ from gi.repository import GstVideo
from gi.repository import Gtk
from pitivi.configure import get_ui_dir
+from pitivi.medialibrary import AssetThumbnail
from pitivi.preset import AudioPresetManager
from pitivi.preset import VideoPresetManager
from pitivi.render import Encoders
+from pitivi.settings import get_dir
+from pitivi.settings import xdg_cache_home
from pitivi.undo.project import AssetAddedIntention
from pitivi.undo.project import AssetProxiedIntention
from pitivi.utils.loggable import Loggable
@@ -45,6 +51,7 @@ from pitivi.utils.misc import fixate_caps_with_default_values
from pitivi.utils.misc import isWritable
from pitivi.utils.misc import path_from_uri
from pitivi.utils.misc import quote_uri
+from pitivi.utils.misc import scale_pixbuf
from pitivi.utils.misc import unicode_error_dialog
from pitivi.utils.pipeline import Pipeline
from pitivi.utils.ripple_update_group import RippleUpdateGroup
@@ -80,6 +87,11 @@ for i in range(2, GLib.MAXINT):
# a project.
IGNORED_PROPS = ["name", "parent"]
+SCALED_THUMB_WIDTH = 96
+SCALED_THUMB_HEIGHT = 54
+SCALED_THUMB_DIR = "96x54"
+ORIGINAL_THUMB_DIR = "original"
+
class ProjectManager(GObject.Object, Loggable):
"""The project manager.
@@ -477,6 +489,7 @@ class ProjectManager(GObject.Object, Loggable):
project = self.current_project
self.current_project = None
+ project.create_thumb()
self.emit("project-closed", project)
# We should never choke on silly stuff like disconnecting signals
# that were already disconnected. It blocks the UI for nothing.
@@ -783,6 +796,103 @@ class Project(Loggable, GES.Project):
return
self.set_meta("author", author)
+ @staticmethod
+ def get_thumb_path(uri, resolution):
+ """Returns path of thumbnail of specified resolution in the cache."""
+ thumb_hash = md5(quote_uri(uri).encode()).hexdigest()
+ thumbs_cache_dir = get_dir(os.path.join(xdg_cache_home(),
+ "project_thumbs", resolution))
+ return os.path.join(thumbs_cache_dir, thumb_hash) + ".png"
+
+ @classmethod
+ def get_thumb(cls, uri):
+ """Gets the project thumb, if exists, else the default thumb or None."""
+ try:
+ thumb = GdkPixbuf.Pixbuf.new_from_file(cls.get_thumb_path(uri, SCALED_THUMB_DIR))
+ except GLib.Error:
+ # Try to get the default thumb.
+ try:
+ thumb = Gtk.IconTheme.get_default().load_icon("video-x-generic", 128, 0)
+ except GLib.Error:
+ return None
+ thumb = scale_pixbuf(thumb, SCALED_THUMB_WIDTH, SCALED_THUMB_HEIGHT)
+
+ return thumb
+
+ def __create_scaled_thumb(self):
+ """Creates scaled thumbnail from the original thumbnail."""
+ try:
+ thumb = GdkPixbuf.Pixbuf.new_from_file(self.get_thumb_path(self.uri, ORIGINAL_THUMB_DIR))
+ thumb = scale_pixbuf(thumb, SCALED_THUMB_WIDTH, SCALED_THUMB_HEIGHT)
+ thumb.savev(self.get_thumb_path(self.uri, SCALED_THUMB_DIR), "png", [], [])
+ except GLib.Error as e:
+ self.warning("Failed to create scaled project thumbnail: %s", e)
+
+ def __remove_thumbs(self):
+ """Removes existing project thumbnails."""
+ for thumb_dir in (ORIGINAL_THUMB_DIR, SCALED_THUMB_DIR):
+ try:
+ os.remove(self.get_thumb_path(self.uri, thumb_dir))
+ except FileNotFoundError:
+ pass
+
+ def create_thumb(self):
+ """Creates project thumbnails."""
+ thumb_path = self.get_thumb_path(self.uri, ORIGINAL_THUMB_DIR)
+
+ if os.path.exists(thumb_path) and not self.app.action_log.has_assets_operations():
+ # The project thumbnail already exists and the assets are the same.
+ return
+
+ # Project Thumbnail Generation Approach: Out of thumbnails of all
+ # the assets in the current project, the one with maximum file size
+ # will be our project thumbnail - http://bit.ly/thumbnail-generation
+
+ assets_uri = [asset.props.id for asset in self.listSources()]
+ normal_thumb_path = None
+ large_thumb_path = None
+ normal_thumb_size = 0
+ large_thumb_size = 0
+ n_normal_thumbs = 0
+ n_large_thumbs = 0
+
+ for uri in assets_uri:
+ path_128, path_256 = AssetThumbnail.get_asset_thumbnails_path(uri)
+
+ try:
+ thumb_size = os.stat(path_128).st_size
+ if thumb_size > normal_thumb_size:
+ normal_thumb_path = path_128
+ normal_thumb_size = thumb_size
+ n_normal_thumbs += 1
+ except FileNotFoundError:
+ # The asset is missing the normal thumbnail.
+ pass
+
+ try:
+ thumb_size = os.stat(path_256).st_size
+ if thumb_size > large_thumb_size:
+ large_thumb_path = path_256
+ large_thumb_size = thumb_size
+ n_large_thumbs += 1
+ except FileNotFoundError:
+ # The asset is missing the large thumbnail.
+ pass
+
+ if normal_thumb_path or large_thumb_path:
+ # Use the category for which we found the max number of
+ # thumbnails to find the most complex thumbnail, because
+ # we can't compare the small with the large.
+ if n_normal_thumbs > n_large_thumbs:
+ shutil.copyfile(normal_thumb_path, thumb_path)
+ else:
+ shutil.copyfile(large_thumb_path, thumb_path)
+ self.__create_scaled_thumb()
+ else:
+ # No asset thumbs available, so remove the existing
+ # project thumbnails, if any.
+ self.__remove_thumbs()
+
def set_rendering(self, rendering):
"""Sets the a/v restrictions for rendering or for editing."""
self._ensureAudioRestrictions()
@@ -1470,7 +1580,7 @@ class Project(Loggable, GES.Project):
Args:
uris (List[str]): The URIs of the assets.
"""
- with self.app.action_log.started("Adding assets"):
+ with self.app.action_log.started("assets-addition"):
for uri in uris:
if self.create_asset(quote_uri(uri), GES.UriClip):
# The asset was not already part of the project.
diff --git a/pitivi/undo/undo.py b/pitivi/undo/undo.py
index 9c826a9e..692b09d3 100644
--- a/pitivi/undo/undo.py
+++ b/pitivi/undo/undo.py
@@ -358,6 +358,13 @@ class UndoableActionLog(GObject.Object, Loggable):
"""Gets whether currently recording an operation."""
return bool(self.stacks)
+ def has_assets_operations(self):
+ """Checks whether user added/removed assets while working on the project."""
+ for stack in self.undo_stacks:
+ if stack.action_group_name in ["assets-addition", "assets-removal"]:
+ return True
+ return False
+
class MetaChangedAction(UndoableAction):
diff --git a/pitivi/utils/misc.py b/pitivi/utils/misc.py
index 88068ce4..dc59bdc7 100644
--- a/pitivi/utils/misc.py
+++ b/pitivi/utils/misc.py
@@ -28,6 +28,7 @@ from urllib.parse import unquote
from urllib.parse import urlparse
from urllib.parse import urlsplit
+from gi.repository import GdkPixbuf
from gi.repository import GES
from gi.repository import GLib
from gi.repository import Gst
@@ -40,6 +41,22 @@ from pitivi.configure import APPNAME
from pitivi.utils.threads import Thread
+def scale_pixbuf(pixbuf, width, height):
+ """Scales the given pixbuf preserving the original aspect ratio."""
+ pixbuf_width = pixbuf.props.width
+ pixbuf_height = pixbuf.props.height
+
+ if pixbuf_width > width:
+ pixbuf_height = width * pixbuf_height / pixbuf_width
+ pixbuf_width = width
+
+ if pixbuf_height > height:
+ pixbuf_width = height * pixbuf_width / pixbuf_height
+ pixbuf_height = height
+
+ return pixbuf.scale_simple(pixbuf_width, pixbuf_height, GdkPixbuf.InterpType.BILINEAR)
+
+
# Work around https://bugzilla.gnome.org/show_bug.cgi?id=759249
def disconnectAllByFunc(obj, func):
i = 0
diff --git a/pitivi/utils/ui.py b/pitivi/utils/ui.py
index 424b6de6..cbfa56da 100644
--- a/pitivi/utils/ui.py
+++ b/pitivi/utils/ui.py
@@ -149,6 +149,10 @@ GREETER_PERSPECTIVE_CSS = """
#recent_projects_label {
font-weight: bold;
}
+
+ #project_thumbnail_box {
+ background-color: #181818;
+ }
"""
TIMELINE_CSS = """
diff --git a/tests/test_misc.py b/tests/test_misc.py
index bd4dc04f..b80ad6dc 100644
--- a/tests/test_misc.py
+++ b/tests/test_misc.py
@@ -19,13 +19,45 @@
"""Tests for the utils.misc module."""
# pylint: disable=protected-access,no-self-use
import os
+from unittest import mock
+from gi.repository import GdkPixbuf
from gi.repository import Gst
from pitivi.utils.misc import PathWalker
+from pitivi.utils.misc import scale_pixbuf
from tests import common
+class MiscMethodsTest(common.TestCase):
+ """Tests methods in utils.misc module."""
+
+ # pylint: disable=too-many-arguments
+ def check_pixbuf_scaling(self, pixbuf_width, pixbuf_height,
+ width, height,
+ expected_width, expected_height):
+ """Checks pixbuf scaling."""
+ pixbuf = mock.Mock()
+ pixbuf.props.width = pixbuf_width
+ pixbuf.props.height = pixbuf_height
+ _ = scale_pixbuf(pixbuf, width, height)
+ pixbuf.scale_simple.assert_called_once_with(expected_width, expected_height,
GdkPixbuf.InterpType.BILINEAR)
+
+ def test_scale_pixbuf(self):
+ """Tests pixbuf scaling."""
+ # Larger, same aspect ratio.
+ self.check_pixbuf_scaling(200, 100, 20, 10, 20, 10)
+ # Larger, wider aspect ratio.
+ self.check_pixbuf_scaling(200, 50, 20, 10, 20, 5)
+ # Larger, taller aspect ratio.
+ self.check_pixbuf_scaling(100, 200, 20, 10, 5, 10)
+
+ # Smaller.
+ self.check_pixbuf_scaling(1, 1, 20, 10, 1, 1)
+ self.check_pixbuf_scaling(20, 1, 20, 10, 20, 1)
+ self.check_pixbuf_scaling(1, 10, 20, 10, 1, 10)
+
+
class PathWalkerTest(common.TestCase):
"""Tests for the `PathWalker` class."""
diff --git a/tests/test_undo_project.py b/tests/test_undo_project.py
index 3a2688a4..3f8630f4 100644
--- a/tests/test_undo_project.py
+++ b/tests/test_undo_project.py
@@ -63,10 +63,13 @@ class TestProjectUndo(common.TestCase):
mainloop.run()
+ self.assertTrue(self.action_log.has_assets_operations())
self.assertEqual(len(self.project.list_assets(GES.Extractable)), 1)
self.action_log.undo()
+ self.assertFalse(self.action_log.has_assets_operations())
self.assertEqual(len(self.project.list_assets(GES.Extractable)), 0)
self.action_log.redo()
+ self.assertTrue(self.action_log.has_assets_operations())
self.assertEqual(len(self.project.list_assets(GES.Extractable)), 1)
def test_use_proxy(self):
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]