diff options
-rw-r--r-- | tagit/actions/__init__.py | 2 | ||||
-rw-r--r-- | tagit/apps/port-schema.nt | 20 | ||||
-rw-r--r-- | tagit/utils/namespaces.py | 13 | ||||
-rw-r--r-- | tagit/widgets/browser.kv | 39 | ||||
-rw-r--r-- | tagit/widgets/browser.py | 116 |
5 files changed, 121 insertions, 69 deletions
diff --git a/tagit/actions/__init__.py b/tagit/actions/__init__.py index c5b33dc..c138655 100644 --- a/tagit/actions/__init__.py +++ b/tagit/actions/__init__.py @@ -13,7 +13,7 @@ from tagit.utils.builder import BuilderBase # inner-module imports from . import browser from . import filter -from . import grouping +#from . import grouping from . import misc #from . import objects from . import planes diff --git a/tagit/apps/port-schema.nt b/tagit/apps/port-schema.nt index ec111d4..c3bf237 100644 --- a/tagit/apps/port-schema.nt +++ b/tagit/apps/port-schema.nt @@ -8,6 +8,7 @@ prefix schema: <http://schema.org/> prefix bsfs: <http://bsfs.ai/schema/> prefix bse: <http://bsfs.ai/schema/Entity#> prefix bst: <http://bsfs.ai/schema/Tag#> +prefix bsg: <http://bsfs.ai/schema/Group#> # essential nodes bsfs:Entity rdfs:subClassOf bsfs:Node . @@ -35,6 +36,16 @@ bse:filesize rdfs:subClassOf bsfs:Predicate ; schema:description "File size of entity in some filesystem."^^xsd:string ; bsfs:unique "false"^^xsd:boolean . +bse:mime rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:File ; + rdfs:range xsd:string ; + bsfs:unique "true"^^xsd:boolean . + +bse:orientation rdfs:subClassOf bsfs:Predicate ; # FIXME: shouldn't this be part of the preview? + rdfs:domain bsfs:File ; + rdfs:range xsd:integer ; + bsfs:unique "true"^^xsd:boolean . + bse:tag rdfs:subClassOf bsfs:Predicate ; rdfs:domain bsfs:File ; rdfs:range bsfs:Tag ; @@ -57,6 +68,11 @@ bse:comment rdfs:subClassOf bsfs:Predicate ; bse:group rdfs:subClassOf bsfs:Predicate ; rdfs:domain bsfs:Entity ; - rdfs:range bsfs:Group ; - bsfs:unique "false"^^xsd:boolean . + rdfs:range bsfs:Group . + +bsg:represented_by rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Group ; + rdfs:range bsfs:File ; + bsfs:unique "true"^^xsd:boolean . + diff --git a/tagit/utils/namespaces.py b/tagit/utils/namespaces.py index dd26eef..5446922 100644 --- a/tagit/utils/namespaces.py +++ b/tagit/utils/namespaces.py @@ -10,18 +10,25 @@ import typing # inner-module imports from . import bsfs as _bsfs -# constants -bse = _bsfs.Namespace('http://bsfs.ai/schema/Entity') +# generic namespaces +xsd = _bsfs.Namespace('http://www.w3.org/2001/XMLSchema') + +# core bsfs namespaces bsfs = _bsfs.Namespace('http://bsfs.ai/schema', fsep='/') bsm = _bsfs.Namespace('http://bsfs.ai/schema/Meta') + +# auxiliary bsfs namespaces +bse = _bsfs.Namespace('http://bsfs.ai/schema/Entity') bst = _bsfs.Namespace('http://bsfs.ai/schema/Tag') -xsd = _bsfs.Namespace('http://www.w3.org/2001/XMLSchema') +bsg = _bsfs.Namespace('http://bsfs.ai/schema/Group') # export __all__: typing.Sequence[str] = ( 'bse', 'bsfs', + 'bsg', 'bsm', + 'bst', 'xsd', ) diff --git a/tagit/widgets/browser.kv b/tagit/widgets/browser.kv index 758d08f..8b4c8c3 100644 --- a/tagit/widgets/browser.kv +++ b/tagit/widgets/browser.kv @@ -37,25 +37,26 @@ tr_x: self.center_x + self.texture.width / 2.0 if self.texture is not None else None tr_y: self.center_y + self.texture.height / 2.0 if self.texture is not None else None - OpenGroup: - root: root.browser.root - # positioning: - # (1) top right corner of the root (inside root) - #x: root.width - self.width - #y: root.height - self.height - # (2) top right corner of the root (inside root) - #pos_hint: {'top': 1.0, 'right': 1.0} - # (3) top right corner of the image (outside the image) - #x: image.tx is not None and image.tx or float('inf') - #y: image.ty is not None and image.ty or float('inf') - # (4) top right corner of the image (inside root, outside the image if possible) - tr_x: root.width - self.width - tr_y: root.height - self.height - x: min(self.tr_x, image.tr_x is not None and image.tr_x or float('inf')) - y: min(self.tr_y, image.tr_y is not None and image.tr_y or float('inf')) - - opacity: root.is_group and 1.0 or 0.0 - show: 'image', + # FIXME: mb/port + #OpenGroup: + # root: root.browser.root + # # positioning: + # # (1) top right corner of the root (inside root) + # #x: root.width - self.width + # #y: root.height - self.height + # # (2) top right corner of the root (inside root) + # #pos_hint: {'top': 1.0, 'right': 1.0} + # # (3) top right corner of the image (outside the image) + # #x: image.tx is not None and image.tx or float('inf') + # #y: image.ty is not None and image.ty or float('inf') + # # (4) top right corner of the image (inside root, outside the image if possible) + # tr_x: root.width - self.width + # tr_y: root.height - self.height + # x: min(self.tr_x, image.tr_x is not None and image.tr_x or float('inf')) + # y: min(self.tr_y, image.tr_y is not None and image.tr_y or float('inf')) + # + # opacity: root.is_group and 1.0 or 0.0 + # show: 'image', <BrowserDescription>: # This be a list item spacer: 20 diff --git a/tagit/widgets/browser.py b/tagit/widgets/browser.py index dace58b..1dfc528 100644 --- a/tagit/widgets/browser.py +++ b/tagit/widgets/browser.py @@ -5,9 +5,11 @@ A copy of the license is provided with the project. Author: Matthias Baumgartner, 2022 """ # standard imports +from collections import defaultdict from functools import reduce, partial import logging import math +import operator import os import typing @@ -24,9 +26,8 @@ import kivy.properties as kp # tagit imports from tagit import config from tagit.external.setproperty import SetProperty -#from tagit.storage import PredicateNotSet # FIXME: mb/port -#from tagit.storage.broker import Representative, Tags # FIXME: mb/port -from tagit.utils import Frame, Resolution, ttime, truncate_dir, clamp, magnitude_fmt +from tagit.utils import Frame, Resolution, Struct, clamp, ns, ttime +from tagit.utils.bsfs import ast # inner-module imports from .loader import Loader @@ -167,13 +168,10 @@ class Browser(GridLayout, StorageAwareMixin, ConfigAwareMixin): """Replace items in *items* if they are grouped. Return the new item list and the dict of representatives. """ - # get groups - stor = self.root.session.storage - groups, s_items = dict(), set(items) - # get groups[group_id] = {items which are also members of the group} - #stor.entities(items).grp() - for grp in Tags.From_Entities(stor, items, Tags.S_TREE): # FIXME! - groups[grp] = s_items & set(Representative.Representative(stor, grp).members()) + # get groups and their shadow (group's members in items) + groups = defaultdict(set) + for obj, grp in reduce(operator.add, items).group(node=True, view=list): + groups[grp].add(obj) # don't fold groups if few members fold_threshold = self.root.session.cfg('ui', 'standalone', 'browser', 'fold_threshold') @@ -190,7 +188,10 @@ class Browser(GridLayout, StorageAwareMixin, ConfigAwareMixin): # create folds folds = { - Representative.Representative(self.root.session.storage, grp): objs + grp.represented_by(): Struct( + group=grp, + shadow=objs, + ) for grp, objs in groups.items() if not superset_exists(grp) } @@ -198,25 +199,25 @@ class Browser(GridLayout, StorageAwareMixin, ConfigAwareMixin): # add representatives for rep in folds: # add representative in place of the first of its members - idx = min([items.index(obj) for obj in folds[rep]]) + idx = min([items.index(obj) for obj in folds[rep].shadow]) items.insert(idx, rep) # remove folded items - for obj in reduce(set.union, folds.values(), set()): + for obj in {obj for fold in folds.values() for obj in fold.shadow}: items.remove(obj) return items, folds def unfold(self, items): """Replace group representatives by their group members.""" + # fetch each item or their shadow if applicable unfolded = set() - for obj in items: - if obj in self.folds: - unfolded |= self.folds[obj] + for itm in items: + if itm in self.folds: + unfolded |= self.folds[itm].shadow else: - unfolded.add(obj) - - return unfolded + unfolded |= {itm} + return reduce(operator.add, unfolded) def neighboring_unselected(self): """Return the item closest to the cursor and not being selected. May return None.""" @@ -279,7 +280,8 @@ class Browser(GridLayout, StorageAwareMixin, ConfigAwareMixin): def on_cursor(self, sender, cursor): if cursor is not None: - self.root.status.dispatch('on_status', truncate_dir(cursor.path)) + #self.root.status.dispatch('on_status', os.path.basename(next(iter(cursor.guids)))) + self.root.status.dispatch('on_status', cursor.filename(default='')) def on_items(self, sender, items): self.change_view = True @@ -394,13 +396,15 @@ class Browser(GridLayout, StorageAwareMixin, ConfigAwareMixin): # load previews for items # FIXME: Only relevant items, not all of them - return # FIXME: mb/port - thumbs = items.preview(default=open(resource_find('no_preview.png'), 'rb')) - resolution = self._cell_resolution() - for ent, thumb, child in zip(reversed(items), reversed(thumbs), childs): + #thumbs = items.preview(default=open(resource_find('no_preview.png'), 'rb')) # FIXME: mb/port + #resolution = self._cell_resolution() # FIXME: mb/port + #for ent, thumb, child in zip(reversed(items), reversed(thumbs), childs): # FIXME: mb/port + for ent, child in zip(reversed(items), childs): # FIXME: default/no preview handling - thumb = best_resolution_match(thumb, resolution) - child.update(ent, thumb, f'{ent.guid}x{resolution}') + #thumb = best_resolution_match(thumb, resolution) # FIXME: mb/port + #child.update(ent, thumb, f'{ent.guid}x{resolution}') # FIXME: mb/port + thumb = open(resource_find('no_preview.png'), 'rb') + child.update(ent, thumb, f'{ent}x{1234}') # load previews for items #resolution = self._cell_resolution() @@ -513,7 +517,8 @@ class BrowserItem(RelativeLayout): class BrowserImage(BrowserItem): def update(self, obj, buffer, source): super(BrowserImage, self).update(obj) - self.preview.load_image(buffer, source, obj.p.get('orientation', 1)) + self.preview.load_image(buffer, source, obj.orientation(default=1)) + self.preview.set_size(self.size) def clear(self): @@ -529,7 +534,7 @@ class BrowserDescription(BrowserItem): def update(self, obj, buffer, source): super(BrowserDescription, self).update(obj) - self.preview.load_image(buffer, source, obj.p.get('orientation', 1)) + self.preview.load_image(buffer, source, obj.orientation(default=1)) self.preview.set_size((self.height, self.height)) def clear(self): @@ -542,25 +547,48 @@ class BrowserDescription(BrowserItem): def on_obj(self, wx, obj): super(BrowserDescription, self).on_obj(wx, obj) if self.is_group: - tags_all = set.intersection(*[set(m.tags) for m in obj.members()]) - tags_any = {t for m in obj.members() for t in m.tags} - self.text = '{name} [size=20sp]x{count}[/size], {mime}, {time}\n[color=8f8f8f][b]{tags_all}[/b], [size=14sp]{tags_any}[/size][/color]'.format(**dict( - name='group', #str(obj.group)[-6:].upper(), - count=len(list(obj.members())), - mime=self.obj.get('mime', ''), - time=ttime.from_timestamp_loc(self.obj.t_created).strftime("%Y-%m-%d %H:%M"), + # get group and its members + grp = self.browser.folds[self.obj].group + # FIXME: Here we could actually use a predicate reversal for Nodes.get + # members = grp.get(ast.fetch.Node(ast.fetch.Predicate(ns.bse.group, reverse=True))) + members = self.browser.root.session.storage.get(ns.bsfs.File, + ast.filter.Any(ns.bse.group, ast.filter.Is(grp))) + # get group member's tags + member_tags = members.tag.label(node=True) + tags_all = set.intersection(*member_tags.values()) + tags_any = {tag for tags in member_tags.values() for tag in tags} + # get remaining info from representative + preds = self.obj.get( + ns.bse.mime, + ns.bsm.t_created, + ) + self.text = '{name} [size=20sp]x{count}[/size], {mime}, {time}\n[color=8f8f8f][b]{tags_all}[/b], [size=14sp]{tags_any}[/size][/color]'.format( + name=os.path.basename(next(iter(grp.guids))), + count=len(members), + mime=preds.get(ns.bse.mime, ''), + time=ttime.from_timestamp_loc( + preds.get(ns.bsm.t_created, ttime.timestamp_min)).strftime('%Y-%m-%d %H:%M'), tags_all=', '.join(sorted(tags_all)), tags_any=', '.join(sorted(tags_any - tags_all)), - )) + ) + elif self.obj is not None: + preds = self.obj.get( + ns.bse.filename, + ns.bse.filesize, + ns.bse.mime, + ns.bsm.t_created, + (ns.bse.tag, ns.bst.label), + ) + self.text = '[size=20sp]{filename}[/size], [size=18sp][i]{mime}[/i][/size] -- {time} -- {filesize}\n[color=8f8f8f][size=14sp]{tags}[/size][/color]'.format( + filename=preds.get(ns.bse.filename, 'n/a'), + mime=preds.get(ns.bse.mime, ''), + time=ttime.from_timestamp_loc( + preds.get(ns.bsm.t_created, ttime.timestamp_min)).strftime('%Y-%m-%d %H:%M'), + filesize=preds.get(ns.bse.filesize, 0), + tags=', '.join(sorted(preds.get((ns.bse.tag, ns.bst.label), []))), + ) else: - self.text = '[size=20sp]{filename}[/size], [size=18sp][i]{mime}[/i][/size] -- {time} -- {filesize}\n[color=8f8f8f][size=14sp]{tags}[/size][/color]'.format(**dict( - filename=os.path.basename(self.obj.path), - hash=str(self.obj), - mime=self.obj.get('mime', ''), - time=ttime.from_timestamp_loc(self.obj.t_created).strftime("%Y-%m-%d %H:%M"), - filesize=magnitude_fmt(self.obj.get('filesize', 0)), - tags=', '.join(sorted(self.obj.tag)), - )) + self.text = '' class AsyncBufferImage(AsyncImage): |