From 41fdbe254cbfc74896080b9f5820f856067da537 Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Wed, 25 Jan 2023 16:19:03 +0100 Subject: tagging ported --- .gitignore | 1 + tagit/actions/__init__.py | 5 +- tagit/actions/tagging.kv | 11 + tagit/actions/tagging.py | 162 ++++++++++++ tagit/apps/port-config.yaml | 12 +- tagit/assets/icons/scalable/tagging/add_tag.svg | 296 ++++++++++++++++++++++ tagit/assets/icons/scalable/tagging/edit_tag.svg | 303 +++++++++++++++++++++++ 7 files changed, 783 insertions(+), 7 deletions(-) create mode 100644 tagit/actions/tagging.kv create mode 100644 tagit/actions/tagging.py create mode 100644 tagit/assets/icons/scalable/tagging/add_tag.svg create mode 100644 tagit/assets/icons/scalable/tagging/edit_tag.svg diff --git a/.gitignore b/.gitignore index 4b3bd39..5c8310a 100644 --- a/.gitignore +++ b/.gitignore @@ -37,5 +37,6 @@ tagit/assets/icons/kivy/filter* tagit/assets/icons/kivy/misc* tagit/assets/icons/kivy/planes* tagit/assets/icons/kivy/search* +tagit/assets/icons/kivy/tagging* ## EOF ## diff --git a/tagit/actions/__init__.py b/tagit/actions/__init__.py index bf99807..6416a4b 100644 --- a/tagit/actions/__init__.py +++ b/tagit/actions/__init__.py @@ -19,6 +19,7 @@ from . import misc from . import planes from . import search #from . import session +from . import tagging # exports __all__: typing.Sequence[str] = ( @@ -85,8 +86,8 @@ class ActionBuilder(BuilderBase): #'RotateLeft': objects.RotateLeft, #'RotateRight': objects.RotateRight, #'DeleteObject': objects.DeleteObject, - #'AddTag': objects.AddTag, - #'EditTag': objects.EditTag, + 'AddTag': tagging.AddTag, + 'EditTag': tagging.EditTag, #'SetRank1': objects.SetRank1, #'SetRank2': objects.SetRank2, #'SetRank3': objects.SetRank3, diff --git a/tagit/actions/tagging.kv b/tagit/actions/tagging.kv new file mode 100644 index 0000000..53ba926 --- /dev/null +++ b/tagit/actions/tagging.kv @@ -0,0 +1,11 @@ +#:import resource_find kivy.resources.resource_find + +: + source: resource_find('atlas://objects/add_tag') + tooltip: 'Add tags to items' + +: + source: resource_find('atlas://objects/edit_tag') + tooltip: 'Edit tags of items' + +## EOF ## diff --git a/tagit/actions/tagging.py b/tagit/actions/tagging.py new file mode 100644 index 0000000..c6d920d --- /dev/null +++ b/tagit/actions/tagging.py @@ -0,0 +1,162 @@ +""" + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# standard imports +from functools import reduce, partial +import operator +import os + +# kivy imports +from kivy.lang import Builder +import kivy.properties as kp + +# tagit imports +from tagit import config, dialogues +from tagit.utils import ns +from tagit.utils.bsfs import Namespace +from tagit.widgets import Binding + +# inner-module imports +from .action import Action + +# constants +TAGS_SEPERATOR = ',' +TAG_PREFIX = Namespace('http://example.com/me/tag') + +# exports +__all__ = [] + + +## code ## + +# load kv +Builder.load_file(os.path.join(os.path.dirname(__file__), 'tagging.kv')) + +# classes +class AddTag(Action): + """Add tags to the selected items.""" + text = kp.StringProperty('Add tag') + + def ktrigger(self, evt): + return Binding.check(evt, self.cfg('bindings', 'objects', 'add_tag')) + + def apply(self): + if len(self.root.browser.selection) > 0: + tags = self.root.session.storage.all(ns.bsfs.Tag).label(node=True) + dlg = dialogues.SimpleInput( + suggestions=set(tags.values()), + suggestion_sep=TAGS_SEPERATOR) + dlg.bind(on_ok=partial(self.add_tags, tags)) + dlg.open() + + else: + dialogues.Error(text='You must select some images first.').open() + + def add_tags(self, tags, wx): + # user-specified labels + labels = {t.strip() for t in wx.text.split(TAGS_SEPERATOR) if len(t.strip())} + # label to tag mapping + lut = {label: tag for tag, label in tags.items()} + # get previous tags + tags = {lut[lbl] for lbl in labels if lbl in lut} + # create new tag nodes and set their label + # FIXME: deny adding tags if tag vocabulary is fixed (ontology case) + # FIXME: replace with proper tag factory + for lbl in labels: + if lbl not in lut: + tag = self.root.session.storage.node(ns.bsfs.Tag, TAG_PREFIX[lbl]) + tag.set(ns.bst.label, lbl) + tags.add(tag) + with self.root.browser as browser, \ + self.root.session as session: + # get objects + ents = browser.unfold(browser.selection) + # collect tags + tags = reduce(operator.add, tags) # FIXME: mb/port: pass set once supported by Nodes.set + # set tags + ents.set(ns.bse.tag, tags) + session.dispatch('on_predicate_modified', 'tag', ents, tags) + # cursor and selection might become invalid. Will be fixed in Browser. + + +class EditTag(Action): + """Edit tags of the selected items""" + text = kp.StringProperty('Edit tags') + + def ktrigger(self, evt): + return Binding.check(evt, self.cfg('bindings', 'objects', 'edit_tag')) + + def apply(self): + with self.root.browser as browser: + if len(browser.selection) > 0: + # get all known tags + all_tags = self.root.session.storage.all(ns.bsfs.Tag).label(node=True) + # get selection tags + ent_tags = browser.unfold(browser.selection).tag.label(node=True) + if len(ent_tags) == 0: + text = '' + else: + sep = TAGS_SEPERATOR + ' ' + text = sep.join(sorted(set.intersection(*ent_tags.values()))) + + dlg = dialogues.SimpleInput( + text=text, + suggestions=set(all_tags.values()), + suggestion_sep=TAGS_SEPERATOR, + ) + dlg.bind(on_ok=partial(self.edit_tag, ent_tags, all_tags)) + dlg.open() + + else: + dialogues.Error(text='You must select some images first.').open() + + def edit_tag(self, original, tags, wx): + """Add or remove tags from images. + *original* and *modified* are strings, split at *TAGS_SEPERATOR*. + Tags are added and removed with respect to the difference between those two sets. + """ + # user-specified labels + labels = {t.strip() for t in wx.text.split(TAGS_SEPERATOR) if len(t.strip())} + # label differences + original_labels = {lbl for lbls in original.values() for lbl in lbls} + removed_labels = original_labels - labels + added_labels = labels - original_labels + # get tags of removed labels + removed = {tag for tag, lbl in tags.items() if lbl in removed_labels} + removed = reduce(operator.add, removed) + # get tags of added labels + # FIXME: deny adding tags if tag vocabulary is fixed (ontology case) + lut = {label: tag for tag, label in tags.items()} + added = {lut[lbl] for lbl in added_labels if lbl in lut} + # FIXME: replace with proper tag factory + for lbl in added_labels: + if lbl not in lut: + tag = self.root.session.storage.node(ns.bsfs.Tag, TAG_PREFIX[lbl]) + tag.set(ns.bst.label, lbl) + added.add(tag) + added = reduce(operator.add, added) + # apply differences + with self.root.browser as browser, \ + self.root.session as session: + ents = browser.unfold(browser.selection) + ents.set(ns.bse.tag, added) + #ents.remove(ns.bse.tag, removed) # FIXME: mb/port + session.dispatch('on_predicate_modified', 'tag', ents, added | removed) + # cursor and selection might become invalid. Will be fixed in Browser. + +## config ## + +# keybindings + +config.declare(('bindings', 'objects', 'add_tag'), + config.Keybind(), Binding.simple('t', Binding.mCTRL, Binding.mSHIFT), + __name__, AddTag.text.defaultvalue, AddTag.__doc__) + +config.declare(('bindings', 'objects', 'edit_tag'), + config.Keybind(), Binding.simple('e', Binding.mCTRL), + __name__, EditTag.text.defaultvalue, EditTag.__doc__) + +## EOF ## diff --git a/tagit/apps/port-config.yaml b/tagit/apps/port-config.yaml index 872ac29..33afbbd 100644 --- a/tagit/apps/port-config.yaml +++ b/tagit/apps/port-config.yaml @@ -37,6 +37,8 @@ ui: - SelectNone - SelectMulti - SelectRange + - AddTag + - EditTag - ShowHelp browser: maxcols: 8 @@ -45,9 +47,9 @@ ui: sidebar_left: - Menu - ShowDashboard - #- AddTag - #- EditTag - AddToken + - AddTag + - EditTag #- CreateGroup #- DissolveGroup - SelectAll @@ -94,9 +96,9 @@ ui: - SelectRange - SelectAdditive - SelectSubtractive - # tagging: - # - AddTag - # - EditTag + tagging: + - AddTag + - EditTag # - SetRank1 # - SetRank3 # - SetRank5 diff --git a/tagit/assets/icons/scalable/tagging/add_tag.svg b/tagit/assets/icons/scalable/tagging/add_tag.svg new file mode 100644 index 0000000..6e73d56 --- /dev/null +++ b/tagit/assets/icons/scalable/tagging/add_tag.svg @@ -0,0 +1,296 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + diff --git a/tagit/assets/icons/scalable/tagging/edit_tag.svg b/tagit/assets/icons/scalable/tagging/edit_tag.svg new file mode 100644 index 0000000..c7d64e1 --- /dev/null +++ b/tagit/assets/icons/scalable/tagging/edit_tag.svg @@ -0,0 +1,303 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + -- cgit v1.2.3