[pitivi] Add tagging functionality for assets in the media library.
- From: Alexandru Băluț <alexbalut src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [pitivi] Add tagging functionality for assets in the media library.
- Date: Mon, 1 Mar 2021 20:58:44 +0000 (UTC)
commit 5fb696ac801957fdd0ce2db8a46b0e98a89cf85f
Author: AsociTon <asociton outlook com>
Date: Mon Aug 10 00:56:51 2020 +0530
Add tagging functionality for assets in the media library.
Fixes #537
data/pixmaps/tag-symbolic.svg | 3 +
data/ui/medialibrary.ui | 19 ++++
pitivi/medialibrary.py | 201 ++++++++++++++++++++++++++++++++++++++++++
pitivi/undo/project.py | 4 +
tests/test_medialibrary.py | 153 ++++++++++++++++++++++++++++++++
5 files changed, 380 insertions(+)
---
diff --git a/data/pixmaps/tag-symbolic.svg b/data/pixmaps/tag-symbolic.svg
new file mode 100755
index 000000000..4ee4fc09a
--- /dev/null
+++ b/data/pixmaps/tag-symbolic.svg
@@ -0,0 +1,3 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M0 1V7L6.69141 13.6914C6.87947 13.8795 7.10448 13.8955 7.28906 13.7109L13.6717 7.32831C13.8934
7.10653 13.8787 6.87864 13.6918 6.69177L7 0H1.00015C0.535593 0 0 0.49129 0 1ZM3 1.96213C3.59045 1.96213
4.06911 2.4408 4.06911 3.03125C4.06911 3.6217 3.59045 4.10037 3 4.10037C2.40955 4.10037 1.93089 3.6217
1.93089 3.03125C1.93089 2.4408 2.40955 1.96213 3 1.96213Z" fill="#2E3436"/>
+</svg>
diff --git a/data/ui/medialibrary.ui b/data/ui/medialibrary.ui
index a1ec30bb6..b4b901aec 100644
--- a/data/ui/medialibrary.ui
+++ b/data/ui/medialibrary.ui
@@ -52,6 +52,25 @@
<property name="homogeneous">True</property>
</packing>
</child>
+ <child>
+ <object class="GtkToggleToolButton" id="tags_button">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="tooltip_markup" translatable="yes">Show tags associated with selected
clips</property>
+ <property name="use_underline">True</property>
+ <property name="icon_name">tag-symbolic</property>
+ <signal name="toggled" handler="_tags_button_clicked_cb" swapped="no"/>
+ <child internal-child="accessible">
+ <object class="AtkObject" id="tags_button-atkobject">
+ <property name="AtkObject::accessible-name">tags_button</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="homogeneous">True</property>
+ </packing>
+ </child>
<child>
<object class="GtkToolButton" id="media_insert_button">
<property name="visible">True</property>
diff --git a/pitivi/medialibrary.py b/pitivi/medialibrary.py
index 31989810e..12b50c7e2 100644
--- a/pitivi/medialibrary.py
+++ b/pitivi/medialibrary.py
@@ -103,6 +103,31 @@ class AssetStoreItem(GObject.GObject):
self.search_text = info_name(asset)
self.thumb_decorator = thumb_decorator
+ # Fetch or Register tags
+ tags = asset.get_meta("pitivi::tags")
+ if not tags:
+ self.tags = set()
+ self.asset.register_meta(GES.MetaFlag.READWRITE, "pitivi::tags", "")
+ else:
+ self.tags = set(tags.split(","))
+
+
+class TagState(IntEnum):
+ """How the tag is associated with assets under selection."""
+
+ PRESENT = 1
+ INCONSISTENT = 2
+ ABSENT = 3
+
+
+class TagStoreItem(GObject.GObject):
+ """Data for displaying a Tag in the list."""
+
+ def __init__(self, name: str, initial_state: TagState):
+ GObject.GObject.__init__(self)
+ self.name = name
+ self.initial_state = initial_state
+
class OptimizeOption(IntEnum):
UNSUPPORTED_ASSETS = 0
@@ -485,6 +510,10 @@ class MediaLibraryWidget(Gtk.Box, Loggable):
self._dragged_paths = []
self.dragged = False
self.rubberbanding = False
+ self.witnessed_tags = set()
+ self.tags_popover = None
+ self.new_tag_entry = None
+ self.__adding_tag = False
self.clip_view = ViewType.__members__.get(self.app.settings.last_clip_view, ViewType.ICON)
self.import_start_time = time.time()
self._last_imported_uris = set()
@@ -525,6 +554,7 @@ class MediaLibraryWidget(Gtk.Box, Loggable):
bg_color = bottom_toolbar_container.get_style_context().get_background_color(Gtk.StateFlags.NORMAL)
bottom_toolbar.override_background_color(Gtk.StateFlags.NORMAL, bg_color)
self._clipprops_button = builder.get_object("media_props_button")
+ self.tags_button = builder.get_object("tags_button")
self.scrollwin = Gtk.ScrolledWindow()
self.scrollwin.set_policy(
@@ -583,6 +613,8 @@ class MediaLibraryWidget(Gtk.Box, Loggable):
self.remove_assets_action,
_("Remove the selected assets"))
+ # self.update_assets_tag.
+
self.insert_at_end_action = Gio.SimpleAction.new("insert-assets-at-end", None)
self.insert_at_end_action.connect("activate", self._insert_end_cb)
actions_group.add_action(self.insert_at_end_action)
@@ -811,12 +843,29 @@ class MediaLibraryWidget(Gtk.Box, Loggable):
for asset in self._pending_assets:
thumb_decorator = AssetThumbnail(asset, self.app.proxy_manager)
item = AssetStoreItem(asset, thumb_decorator)
+
+ asset.connect("notify-meta", self.asset_meta_changed_cb)
+ self.witnessed_tags.update(item.tags)
self.store.append(item)
thumb_decorator.connect("thumb-updated", self.__thumb_updated_cb, asset)
del self._pending_assets[:]
+ def asset_meta_changed_cb(self, asset, meta_key, meta_value):
+ if meta_key != "pitivi::tags":
+ return
+
+ tags = set(meta_value.split(",")) if meta_value != "" else set()
+ for item in self.store:
+ if item.asset == asset:
+ item.tags = tags
+ # Rebuild witnessed_tags
+ self.witnessed_tags = set()
+ for item_ in self.store:
+ self.witnessed_tags.update(item_.tags)
+ break
+
def __thumb_updated_cb(self, asset_thumbnail, asset):
"""Handles the thumb-updated signal of the AssetThumbnails in the model."""
pos = -1
@@ -1113,6 +1162,157 @@ class MediaLibraryWidget(Gtk.Box, Loggable):
# In case the toggling is done when the items are filtered.
self.filter_store()
+ def _tags_button_clicked_cb(self, unused_widget):
+ self.tags_popover = Gtk.Popover()
+ popover_heading = Gtk.Label(label=_("Tag as:"))
+ popover_heading.set_halign(Gtk.Align.START)
+
+ paths = self.get_selected_paths()
+
+ # Find the common tags in the selected assets
+ common_tags = list(self.store[paths[0]].tags)
+ for path in paths:
+ for tag in common_tags:
+ if tag not in self.store[path].tags:
+ common_tags.remove(tag)
+ if not common_tags:
+ break
+
+ # Union of tags associated with assets under the current selection.
+ all_tags = []
+ for path in paths:
+ all_tags = list(set().union(all_tags, self.store[path].tags))
+ inconsistent_tags = list(set(all_tags) - set(common_tags))
+
+ tags = list(self.witnessed_tags)
+ tags.sort()
+ tagstore = Gio.ListStore()
+ for tag in tags:
+ if tag in common_tags:
+ state = TagState.PRESENT
+ elif tag in inconsistent_tags:
+ state = TagState.INCONSISTENT
+ else:
+ state = TagState.ABSENT
+ tagstore.append(TagStoreItem(tag, state))
+
+ tags_list = Gtk.ListBox()
+ tags_list.bind_model(tagstore, self.create_tagslist_widget_func)
+ tags_list.set_can_focus(False)
+
+ new_entry_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
+ self.new_tag_entry = Gtk.Entry()
+ self.new_tag_entry.props.placeholder_text = _("Enter tag")
+ self.new_tag_entry.set_width_chars(12)
+ self.new_tag_entry.connect("activate", self.new_tag_entry_activated_cb, tagstore)
+ add_tag_button = Gtk.Button.new_with_label(_("Add"))
+ add_tag_button.connect("clicked", self.add_tag_button_clicked_cb, tagstore)
+ new_entry_box.pack_start(self.new_tag_entry, False, False, 0)
+ new_entry_box.pack_start(add_tag_button, False, False, PADDING)
+
+ box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
+ box.props.margin = PADDING
+ box.pack_start(popover_heading, False, False, SPACING)
+ box.pack_start(tags_list, True, True, PADDING)
+ box.pack_start(new_entry_box, False, False, 0)
+
+ self.tags_popover.connect("closed", self._tags_popover_closed_cb, tags_list, tagstore)
+ self.tags_popover.add(box)
+ self.tags_popover.set_position(Gtk.PositionType.BOTTOM)
+ self.tags_popover.set_relative_to(self.tags_button)
+ self.tags_popover.show_all()
+ self.tags_popover.popup()
+ tags_list.unselect_all()
+
+ def create_tagslist_widget_func(self, tag_item: TagStoreItem):
+ """Converts a tag item to a widget."""
+ box = Gtk.ListBoxRow()
+ box.props.margin = PADDING
+
+ checkbutton = Gtk.CheckButton.new_with_label(tag_item.name)
+
+ if tag_item.initial_state == TagState.INCONSISTENT:
+ checkbutton.props.inconsistent = True
+ active = self.__adding_tag or tag_item.initial_state == TagState.PRESENT
+ checkbutton.set_active(active)
+
+ checkbutton.connect("toggled", self._tag_toggled_cb, tag_item)
+
+ box.add(checkbutton)
+ box.show_all()
+
+ return box
+
+ def _tags_popover_closed_cb(self, popover, tags_list, tagstore):
+ self.apply_changed_tags(tags_list, tagstore)
+ popover.hide()
+ self.tags_button.set_active(False)
+ self.tags_popover = None
+
+ def _tag_toggled_cb(self, toggle_button, tag_item):
+ """Handles the toggling of a tag in the list."""
+ if toggle_button.props.inconsistent:
+ toggle_button.props.active = True
+ toggle_button.props.inconsistent = False
+ else:
+ if toggle_button.props.active:
+ if tag_item.initial_state == TagState.INCONSISTENT:
+ toggle_button.props.inconsistent = True
+
+ def new_tag_entry_activated_cb(self, unused_widget, tagstore):
+ self._add_new_tag(tagstore)
+
+ def add_tag_button_clicked_cb(self, unused_widget, tagstore):
+ self._add_new_tag(tagstore)
+
+ def _add_new_tag(self, tagstore):
+ if not self.new_tag_entry.get_text():
+ return
+
+ tag_name = self.new_tag_entry.get_text()
+ # Don't accept duplicate entry
+ for tag_item in tagstore:
+ if tag_item.name == tag_name:
+ return
+
+ self.__adding_tag = True
+ try:
+ tagstore.append(TagStoreItem(tag_name, TagState.ABSENT))
+ finally:
+ self.__adding_tag = False
+
+ self.new_tag_entry.set_text("")
+
+ def apply_changed_tags(self, tags_list, tagstore):
+ paths = self.get_selected_paths()
+ with self.app.action_log.started("Alter tags", toplevel=True):
+ for i, row_widget in enumerate(tags_list):
+ checkbox = row_widget.get_child()
+ initial_state = tagstore[i].initial_state
+ tag_name = tagstore[i].name
+ if checkbox.props.inconsistent:
+ # Nothing shall be changed.
+ continue
+
+ if checkbox.props.active:
+ if initial_state != TagState.PRESENT:
+ for path in paths:
+ asset_store_item = self.store[path]
+ if tag_name not in asset_store_item.tags:
+ old_meta = asset_store_item.asset.get_meta("pitivi::tags")
+ new_meta = old_meta + "," + tag_name if old_meta else tag_name
+ asset_store_item.asset.set_meta("pitivi::tags", new_meta)
+ else:
+ if initial_state != TagState.ABSENT:
+ for path in paths:
+ asset_store_item = self.store[path]
+ if tag_name in asset_store_item.tags:
+ old_meta = asset_store_item.asset.get_meta("pitivi::tags")
+ new_meta = old_meta.split(",")
+ new_meta.remove(tag_name)
+ new_meta = ",".join(new_meta)
+ asset_store_item.asset.set_meta("pitivi::tags", new_meta)
+
def __stop_using_proxy_cb(self, unused_action, unused_parameter):
prefer_original = self.app.settings.proxying_strategy == ProxyingStrategy.NOTHING
self._project.disable_proxies_for_assets(self.get_selected_assets(),
@@ -1288,6 +1488,7 @@ class MediaLibraryWidget(Gtk.Box, Loggable):
selected_count = len(self.flowbox.get_selected_children())
self.remove_assets_action.set_enabled(selected_count)
self.insert_at_end_action.set_enabled(selected_count)
+ self.tags_button.set_sensitive(selected_count)
# Some actions can only be done on a single item at a time:
self._clipprops_button.set_sensitive(selected_count == 1)
diff --git a/pitivi/undo/project.py b/pitivi/undo/project.py
index 296cd5d5e..beba2240c 100644
--- a/pitivi/undo/project.py
+++ b/pitivi/undo/project.py
@@ -133,12 +133,16 @@ class ProjectObserver(MetaContainerObserver):
MetaContainerObserver.__init__(self, project, action_log)
project.connect("asset-added", self._asset_added_cb)
project.connect("asset-removed", self._asset_removed_cb)
+ assets = project.list_assets(GES.Extractable)
+ for asset in assets:
+ MetaContainerObserver.__init__(self, asset, action_log)
self.timeline_observer = TimelineObserver(project.ges_timeline,
action_log)
def _asset_added_cb(self, unused_project, asset):
if not isinstance(asset, GES.UriClipAsset):
return
+ MetaContainerObserver.__init__(self, asset, self.action_log)
action = AssetAddedAction(asset)
self.action_log.push(action)
diff --git a/tests/test_medialibrary.py b/tests/test_medialibrary.py
index f077126b3..8636f74fe 100644
--- a/tests/test_medialibrary.py
+++ b/tests/test_medialibrary.py
@@ -26,6 +26,7 @@ from gi.repository import Gst
from pitivi.medialibrary import AssetThumbnail
from pitivi.medialibrary import MediaLibraryWidget
+from pitivi.medialibrary import TagState
from pitivi.medialibrary import ViewType
from pitivi.project import ProjectManager
from pitivi.utils.misc import ASSET_DURATION_META
@@ -514,3 +515,155 @@ class TestMediaLibrary(BaseTestMediaLibrary):
# Release click
event = create_event(Gdk.EventType.BUTTON_RELEASE, button=3)
mlib._flowbox_button_release_event_cb(mlib.flowbox, event)
+
+
+class TestTaggingAssets(BaseTestMediaLibrary):
+
+ def import_assets_in_medialibrary(self):
+ samples = ["30fps_numeroted_frames_red.mkv",
+ "30fps_numeroted_frames_blue.webm", "1sec_simpsons_trailer.mp4"]
+ with common.cloned_sample(*samples):
+ self.check_import(samples, proxying_strategy=ProxyingStrategy.NOTHING)
+
+ self.assertTrue(self.medialibrary.tags_button.is_sensitive())
+
+ def add_new_tag(self, tag_name):
+ # Open the popover
+ self.medialibrary.tags_button.props.active = True
+ self.medialibrary.new_tag_entry.set_text(tag_name)
+ self.medialibrary.new_tag_entry.emit("activate")
+ self.medialibrary.tags_popover.hide()
+
+ def get_tags_list(self):
+ box = self.medialibrary.tags_popover.get_child()
+ popover_widgets = box.get_children()
+ return popover_widgets[1]
+
+ def assert_tags_popover(self, tags_state):
+ box = self.medialibrary.tags_popover.get_child()
+ popover_widgets = box.get_children()
+ tags_list = popover_widgets[1]
+
+ for row_widget in tags_list:
+ checkbox = row_widget.get_child()
+ tag_name = checkbox.get_label()
+ if tags_state[tag_name] == TagState.PRESENT:
+ self.assertTrue(checkbox.props.active)
+ elif tags_state[tag_name] == TagState.INCONSISTENT:
+ self.assertTrue(checkbox.props.inconsistent)
+ elif tags_state[tag_name] == TagState.ABSENT:
+ self.assertFalse(checkbox.props.active)
+ else:
+ raise Exception(tag_name)
+
+ def test_adding_tags(self):
+ self.mainloop = common.create_main_loop()
+ self.import_assets_in_medialibrary()
+ self.medialibrary.flowbox.unselect_all()
+
+ # Add a new tag "TAG" to asset1 via new tag entry field
+ asset1 = self.medialibrary.flowbox.get_child_at_index(0)
+ self.medialibrary.flowbox.select_child(asset1)
+ tag = "TAG"
+ self.add_new_tag(tag)
+ self.medialibrary.tags_button.props.active = True
+ self.assert_tags_popover({tag: TagState.PRESENT})
+ self.medialibrary.tags_popover.hide()
+ self.assertEqual(self.medialibrary.store[0].tags, {tag})
+ self.assertEqual(self.medialibrary.witnessed_tags, {tag})
+ self.medialibrary.flowbox.unselect_all()
+
+ # Add an existing tag "TAG" to asset2 via toggling the unchecked mark
+ asset2 = self.medialibrary.flowbox.get_child_at_index(1)
+ self.medialibrary.flowbox.select_child(asset2)
+ # Check if the tag is unchecked for now
+ self.medialibrary.tags_button.props.active = True
+ self.assert_tags_popover({tag: TagState.ABSENT})
+ tags_list = self.get_tags_list()
+ row_widget = tags_list.get_row_at_index(0)
+ checkbox = row_widget.get_child()
+ self.assertFalse(checkbox.props.active)
+ checkbox.props.active = True
+ self.medialibrary.tags_popover.hide()
+
+ # Check if "TAG" is applied to asset2
+ self.assertEqual(self.medialibrary.store[1].tags, {tag})
+ # Reopen popover to check if "TAG" is present and correctly checked in asset2
+ self.medialibrary.tags_button.props.active = True
+ self.assert_tags_popover({tag: TagState.PRESENT})
+ self.medialibrary.tags_popover.hide()
+
+ # We have "TAG" for both asset1 and asset2, but asset3 has no tags
+ # Add the existing inconsistent tag to every selected asset
+ self.medialibrary.flowbox.select_all()
+ self.medialibrary.tags_button.props.active = True
+ self.assert_tags_popover({tag: TagState.INCONSISTENT})
+ tags_list = self.get_tags_list()
+ row_widget = tags_list.get_row_at_index(0)
+ checkbox = row_widget.get_child()
+ # Make the tag present in all the tags
+ checkbox.props.active = True
+
+ # Reopen the tags popover to check if "TAG" is present in all assets
+ self.medialibrary.tags_popover.hide()
+ # Check if "TAG" is present in all the 3 assets
+ self.medialibrary.tags_button.props.active = True
+ self.assert_tags_popover({tag: TagState.PRESENT})
+ self.medialibrary.tags_popover.hide()
+
+ def test_removing_tags(self):
+ self.mainloop = common.create_main_loop()
+ self.import_assets_in_medialibrary()
+ tag = "TAG"
+ self.add_new_tag(tag)
+ self.assertEqual(self.medialibrary.witnessed_tags, {tag})
+
+ # Make sure "TAG" is present in all the assets
+ self.medialibrary.tags_button.props.active = True
+ self.assert_tags_popover({tag: TagState.PRESENT})
+ self.medialibrary.tags_popover.hide()
+ self.medialibrary.flowbox.unselect_all()
+
+ # Remove "TAG" from a single asset
+ child = self.medialibrary.flowbox.get_child_at_index(0)
+ self.medialibrary.flowbox.select_child(child)
+ self.medialibrary.tags_button.props.active = True
+ self.assertEqual(self.medialibrary.store[0].tags, {tag})
+ tags_list = self.get_tags_list()
+ row_widget = tags_list.get_row_at_index(0)
+ checkbox = row_widget.get_child()
+ self.assertTrue(checkbox.props.active)
+ checkbox.props.active = False
+ self.medialibrary.tags_popover.hide()
+
+ # Confirm "TAG" is removed from asset1
+ self.assertEqual(self.medialibrary.store[0].tags, set())
+ # Reopen the tags popover to check if "TAG" is removed from asset1
+ self.medialibrary.tags_button.props.active = True
+ self.assert_tags_popover({tag: TagState.ABSENT})
+ self.medialibrary.tags_popover.hide()
+
+ # Remove inconsistent "TAG" from the selected assets
+ self.medialibrary.flowbox.select_all()
+ self.medialibrary.tags_button.props.active = True
+
+ tags_list = self.get_tags_list()
+ row_widget = tags_list.get_row_at_index(0)
+ checkbox = row_widget.get_child()
+ self.assertTrue(checkbox.props.inconsistent)
+ # Toggle it to checked
+ checkbox.props.active = True
+ # Toggle it to unchecked
+ checkbox.props.active = False
+ self.medialibrary.tags_popover.hide()
+
+ # Check if popover is empty
+ self.medialibrary.tags_button.props.active = True
+ tags_list = self.get_tags_list()
+ row_widget = tags_list.get_row_at_index(0)
+ self.assertIsNone(row_widget)
+ self.medialibrary.tags_popover.hide()
+
+ self.assertEqual(self.medialibrary.witnessed_tags, set())
+ for item in self.medialibrary.store:
+ self.assertEqual(item.tags, set())
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]