aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMatthias Baumgartner <dev@igsor.net>2023-01-25 10:38:00 +0100
committerMatthias Baumgartner <dev@igsor.net>2023-01-25 10:38:00 +0100
commit20d31b0c4a61b5f026fc8b0ff98c43b8a00bee48 (patch)
tree31ee34801284168276745950b28be3089533d257
parent5a818f9e90c7758f2f77de822390b7f4bf4f9ef5 (diff)
downloadtagit-20d31b0c4a61b5f026fc8b0ff98c43b8a00bee48.tar.gz
tagit-20d31b0c4a61b5f026fc8b0ff98c43b8a00bee48.tar.bz2
tagit-20d31b0c4a61b5f026fc8b0ff98c43b8a00bee48.zip
browser folding
-rw-r--r--tagit/actions/__init__.py2
-rw-r--r--tagit/apps/port-schema.nt20
-rw-r--r--tagit/utils/namespaces.py13
-rw-r--r--tagit/widgets/browser.kv39
-rw-r--r--tagit/widgets/browser.py116
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):