aboutsummaryrefslogtreecommitdiffstats
path: root/tagit/widgets
diff options
context:
space:
mode:
authorMatthias Baumgartner <dev@igsor.net>2023-03-05 19:16:41 +0100
committerMatthias Baumgartner <dev@igsor.net>2023-03-05 19:16:41 +0100
commit98e567933723c59d1d97b3a85e649cfdce514676 (patch)
treee6e0b475c7ab5c6a7ff4f0ea7ad1b08cecf05e68 /tagit/widgets
parentbf98c062ece242a5fc56de0f1adbc12f0588809a (diff)
parent5e88d395dee651175a277092c712249e3898a7d8 (diff)
downloadtagit-98e567933723c59d1d97b3a85e649cfdce514676.tar.gz
tagit-98e567933723c59d1d97b3a85e649cfdce514676.tar.bz2
tagit-98e567933723c59d1d97b3a85e649cfdce514676.zip
Merge branch 'mb/newdesign' into develop
Diffstat (limited to 'tagit/widgets')
-rw-r--r--tagit/widgets/browser.kv23
-rw-r--r--tagit/widgets/browser.py92
-rw-r--r--tagit/widgets/filter.kv105
-rw-r--r--tagit/widgets/filter.py34
-rw-r--r--tagit/widgets/session.py12
-rw-r--r--tagit/widgets/status.kv16
6 files changed, 155 insertions, 127 deletions
diff --git a/tagit/widgets/browser.kv b/tagit/widgets/browser.kv
index 8b4c8c3..63495be 100644
--- a/tagit/widgets/browser.kv
+++ b/tagit/widgets/browser.kv
@@ -11,19 +11,6 @@
is_cursor: False
is_selected: False
- canvas.after:
- Color:
- rgba: 1,1,1, 1 if self.is_cursor else 0
- Line:
- width: 2
- rectangle: self.x, self.y, self.width, self.height
-
- Color:
- rgba: self.scolor + [0.5 if self.is_selected else 0]
- Rectangle:
- pos: self.x, self.center_y - int(self.height) / 2
- size: self.width, self.height
-
<BrowserImage>: # This be an image
preview: image
@@ -58,6 +45,11 @@
# opacity: root.is_group and 1.0 or 0.0
# show: 'image',
+<BrowserDescriptionLabel@Label>:
+ halign: 'left'
+ valign: 'center'
+ text_size: self.size
+
<BrowserDescription>: # This be a list item
spacer: 20
preview: image
@@ -68,12 +60,9 @@
# actual size is set in code
pos: 0, 0
- Label:
+ BrowserDescriptionLabel:
text: root.text
markup: True
- halign: 'left'
- valign: 'center'
- text_size: self.size
size_hint: None, 1
width: root.width - image.width - root.spacer - 35
pos: root.height + root.spacer, 0
diff --git a/tagit/widgets/browser.py b/tagit/widgets/browser.py
index 1e42c9c..17d99ed 100644
--- a/tagit/widgets/browser.py
+++ b/tagit/widgets/browser.py
@@ -171,7 +171,8 @@ class Browser(GridLayout, StorageAwareMixin, ConfigAwareMixin):
"""
# 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):
+ all_items = reduce(operator.add, items, self.root.session.storage.empty(ns.bsn.Entity))
+ for obj, grp in all_items.group(node=True, view=list):
groups[grp].add(obj)
# don't fold groups if few members
@@ -218,7 +219,7 @@ class Browser(GridLayout, StorageAwareMixin, ConfigAwareMixin):
unfolded |= self.folds[itm].shadow
else:
unfolded |= {itm}
- return reduce(operator.add, unfolded) # FIXME: What if items is empty?
+ return reduce(operator.add, unfolded, self.root.session.storage.empty(ns.bsn.Entity))
def neighboring_unselected(self):
"""Return the item closest to the cursor and not being selected. May return None."""
@@ -281,7 +282,6 @@ class Browser(GridLayout, StorageAwareMixin, ConfigAwareMixin):
def on_cursor(self, sender, cursor):
if cursor is not None:
- #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):
@@ -347,10 +347,7 @@ class Browser(GridLayout, StorageAwareMixin, ConfigAwareMixin):
self.clear_widgets()
for itm in range(self.page_size):
- wx = factory(
- browser=self,
- scolor=self.root.session.cfg('ui', 'standalone', 'browser', 'select_color'),
- )
+ wx = factory(browser=self)
self.bind(selection=wx.on_selection)
self.bind(cursor=wx.on_cursor)
self.add_widget(wx)
@@ -398,18 +395,30 @@ class Browser(GridLayout, StorageAwareMixin, ConfigAwareMixin):
if len(items) == 0: # FIXME: mb/port
return
+ resolution = self._cell_resolution()
+ previews = self._fetch_previews(items, resolution)
+ default = resource_find('no_preview.png')
+ for ent, child in zip(reversed(items), childs):
+ if ent in previews:
+ buf = previews[ent]
+ else:
+ buf = open(default, 'rb')
+ child.update(ent, buf, f'{ent}x{resolution}')
+
+ def _fetch_previews(self, items, resolution):
+ """Fetch previews matching *resolution* for *items*.
+ Return a dict with items as key and a BytesIO as value.
+ Items without valid asset are omitted from the dict.
+ """
# fetch previews
node_preview = reduce(operator.add, items).get(ns.bse.preview, node=True)
previews = {p for previews in node_preview.values() for p in previews}
- previews = reduce(operator.add, previews)
+ previews = reduce(operator.add, previews) # FIXME: empty previews
# fetch preview resolutions
res_preview = previews.get(ns.bsp.width, ns.bsp.height, node=True)
- # get target resolution
- resolution = self._cell_resolution()
- # get default preview
- default = resource_find('no_preview.png')
# select a preview for each item
- for ent, child in zip(reversed(items), childs):
+ chosen = {}
+ for ent in items:
try:
# get previews and their resolution for this ent
options = []
@@ -420,19 +429,21 @@ class Browser(GridLayout, StorageAwareMixin, ConfigAwareMixin):
height = res.get(ns.bsp.height, 0)
options.append((preview, Resolution(width, height)))
# select the best fitting preview
- chosen = rmatcher.by_area_min(resolution, options)
- # open the preview file, default if no asset is available
- thumb_data = chosen.asset(default=None) # FIXME: get all assets in one call
- if thumb_data is None:
- raise KeyError()
- thumb = io.BytesIO(thumb_data)
-
+ chosen[ent] = rmatcher.by_area_min(resolution, options)
except (KeyError, IndexError):
- # KeyError:
- # no viable resolution found
- thumb = open(default, 'rb')
- # update the image in the child widget
- child.update(ent, thumb, f'{ent}x{resolution}')
+ # skip objects w/o preview (KeyError in node_preview)
+ # skip objects w/o valid preview (IndexError in rmatcher)
+ pass
+
+ # fetch assets
+ assets = reduce(operator.add, chosen.values()).asset(node=True) # FIXME: empty chosen
+ # build ent -> asset mapping and convert raw data to io buffer
+ return {
+ ent: io.BytesIO(assets[thumb])
+ for ent, thumb
+ in chosen.items()
+ if thumb in assets
+ }
#def _preload_all(self):
# # prefer loading from start to end
@@ -440,26 +451,24 @@ class Browser(GridLayout, StorageAwareMixin, ConfigAwareMixin):
def _preload_items(self, items, resolution=None):
"""Load an item into the kivy *Cache* without displaying the image anywhere."""
- resolution = resolution if resolution is not None else self._cell_resolution()
-
def _buf_loader(buffer, fname):
# helper method to load the image from a raw buffer
with buffer as buf:
return ImageLoaderTagit(filename=fname, inline=True, rawdata=buf)
- for obj in items:
- try:
- buffer = obj.get('preview').best_match(resolution)
- source = f'{obj.guid}x{resolution}'
-
- Loader.image(source,
- nocache=False, mipmap=False,
- anim_delay=0,
- load_callback=partial(_buf_loader, buffer) # mb: pass load_callback
- )
-
- except PredicateNotSet:
- pass
+ resolution = resolution if resolution is not None else self._cell_resolution()
+ try:
+ foo = self._fetch_previews(items, resolution) # FIXME: _fetch_previews fails on empty previews/chosen
+ except TypeError:
+ return
+ for obj, buffer in foo.items():
+ guid = ','.join(obj.guids)
+ source = f'{guid}x{resolution}'
+ Loader.image(source,
+ nocache=False, mipmap=False,
+ anim_delay=0,
+ load_callback=partial(_buf_loader, buffer) # mb: pass load_callback
+ )
class BrowserAwareMixin(object):
@@ -484,7 +493,6 @@ class BrowserItem(RelativeLayout):
is_cursor = kp.BooleanProperty(False)
is_selected = kp.BooleanProperty(False)
is_group = kp.BooleanProperty(False)
- scolor = kp.ListProperty([1, 0, 0]) # FIXME: set from config
def update(self, obj):
self.obj = obj
@@ -572,7 +580,7 @@ class BrowserDescription(BrowserItem):
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,
+ members = self.browser.root.session.storage.get(ns.bsn.Entity,
ast.filter.Any(ns.bse.group, ast.filter.Is(grp)))
# get group member's tags
member_tags = members.tag.label(node=True)
diff --git a/tagit/widgets/filter.kv b/tagit/widgets/filter.kv
index b638570..5407610 100644
--- a/tagit/widgets/filter.kv
+++ b/tagit/widgets/filter.kv
@@ -1,4 +1,5 @@
#:import SearchmodeSwitch tagit.actions.filter
+#:import AddToken tagit.actions.filter
#-- #:import SortKey tagit.actions.search
<Filter>:
@@ -7,20 +8,36 @@
spacing: 5
tokens: tokens
- BoxLayout:
- orientation: 'horizontal'
- spacing: 10
- id: tokens
-
- # Tokens will be inserted here
-
- SearchmodeSwitch:
- show: 'image',
+ Widget:
+ size_hint_x: None
+ width: 5
+
+ ScrollView:
+ do_scroll_x: True
+ do_scroll_y: False
+ size_hint: 1, 1
+
+ BoxLayout:
+ orientation: 'horizontal'
+ spacing: 10
+ id: tokens
+ size_hint: None, None
+ height: 35
+ width: self.minimum_width
+ # Tokens will be inserted here
+
+ AddToken:
+ show: 'image',
root: root.root
- SortKey:
- show: 'image',
- root: root.root
+ # FIXME: Temporarily disabled
+ #SearchmodeSwitch:
+ # show: 'image',
+ # root: root.root
+
+ #SortKey:
+ # show: 'image',
+ # root: root.root
SortOrder:
show: 'image',
@@ -30,55 +47,43 @@
root: root.root
name: 'filter'
orientation: 'lr-tb'
- # space for 2 buttons
- width: 3*30 + 2*5
- size_hint: None, 1.0
+ # space for two buttons
+ width: 2*30 + 5
spacing: 5
+ size_hint: None, None
+ height: 35
button_height: 30
button_show: 'image',
+<Avatar@Label>:
+ active: False
+
+<ShingleText@Label>:
+ active: False
+
<Shingle>:
orientation: 'horizontal'
label: tlabel
-
- canvas.before:
- Color:
- rgba: 0,0,1, 0.25 if root.active else 0
- Rectangle:
- pos: root.pos
- size: root.size
-
- canvas.after:
- Color:
- rgba: 1,1,1,1
- Line:
- rectangle: self.x+1, self.y+1, self.width-1, self.height-1
-
- Label:
+ size_hint: None, None
+ width: self.minimum_width
+ height: 30
+
+ Avatar:
+ id: avatar
+ size_hint: None, None
+ text: root.avatar
+ width: self.parent.height
+ height: self.parent.height
+ active: root.active
+
+ ShingleText:
id: tlabel
text: root.text
-
- canvas.after:
- Color:
- rgba: 0,0,0,0.5 if not root.active else 0
- Rectangle:
- pos: self.pos
- size: self.size
-
-
- Button:
- text: 'x'
- bold: True
- opacity: 0.5
- width: 20
- size_hint: None, 1.0
- background_color: [0,0,0,0]
- background_normal: ''
- on_press: root.remove()
+ active: root.active
+ width: (self.texture_size[0] + dp(20)) if self.text != '' else 0
+ size_hint_x: None
<Addressbar>:
multiline: False
- background_color: (0.2,0.2,0.2,1) if self.focus else (0.15,0.15,0.15,1)
- foreground_color: (1,1,1,1)
## EOF ##
diff --git a/tagit/widgets/filter.py b/tagit/widgets/filter.py
index 15aefd6..1382c43 100644
--- a/tagit/widgets/filter.py
+++ b/tagit/widgets/filter.py
@@ -120,26 +120,46 @@ class Filter(BoxLayout, ConfigAwareMixin):
return query, sort
def abbreviate(self, token):
+ # FIXME: Return image
matches = matcher.Filter()
if matches(token, ast.filter.Any(ns.bse.tag, ast.filter.Any(ns.bst.label, matcher.Any()))):
# tag token
- return self.root.session.filter_to_string(token)
+ return 'T'
if matches(token, matcher.Partial(ast.filter.Is)) or \
matches(token, ast.filter.Or(matcher.Rest(matcher.Partial(ast.filter.Is)))):
# exclusive token
- return 'E'
+ return '='
if matches(token, ast.filter.Not(matcher.Partial(ast.filter.Is))) or \
matches(token, ast.filter.Not(ast.filter.Or(matcher.Rest(matcher.Partial(ast.filter.Is))))):
# reduce token
- return 'R'
+ return '—'
if matches(token, ast.filter.Any(ns.bse.group, matcher.Any())):
# group token
return 'G'
if matches(token, ast.filter.Any(matcher.Partial(ast.filter.Predicate), matcher.Any())):
# generic token
- return token.predicate.predicate.get('fragment', '?').title()
+ #return token.predicate.predicate.get('fragment', '?').title()[0]
+ return 'P'
return '?'
+ def tok_label(self, token):
+ matches = matcher.Filter()
+ if matches(token, ast.filter.Any(ns.bse.tag, ast.filter.Any(ns.bst.label, matcher.Any()))):
+ # tag token
+ return self.root.session.filter_to_string(token)
+ if matches(token, matcher.Partial(ast.filter.Is)) or \
+ matches(token, ast.filter.Not(matcher.Partial(ast.filter.Is))):
+ return '1'
+ if matches(token, ast.filter.Or(matcher.Rest(matcher.Partial(ast.filter.Is)))):
+ return str(len(token))
+ if matches(token, ast.filter.Not(ast.filter.Or(matcher.Rest(matcher.Partial(ast.filter.Is))))):
+ return str(len(token.expr))
+ if matches(token, ast.filter.Any(matcher.Partial(ast.filter.Predicate), matcher.Any())):
+ # generic token
+ #return self.root.session.filter_to_string(token)
+ return token.predicate.predicate.get('fragment', '')
+ return ''
+
def show_address_once(self):
"""Single-shot address mode without changing the search mode."""
self.tokens.clear_widgets()
@@ -189,7 +209,8 @@ class Filter(BoxLayout, ConfigAwareMixin):
Shingle(
tok,
active=(tok in self.t_head),
- text=self.abbreviate(tok),
+ avatar=self.abbreviate(tok),
+ text=self.tok_label(tok),
root=self.root
))
@@ -235,6 +256,7 @@ class Shingle(BoxLayout):
# content
active = kp.BooleanProperty(False)
text = kp.StringProperty('')
+ avatar = kp.StringProperty('')
# touch behaviour
_single_tap_action = None
@@ -249,7 +271,7 @@ class Shingle(BoxLayout):
def on_touch_down(self, touch):
"""Edit shingle when touched."""
- if self.label.collide_point(*touch.pos):
+ if self.collide_point(*touch.pos):
if touch.is_double_tap: # edit filter
# ignore touch, such that the dialogue
# doesn't loose the focus immediately after open
diff --git a/tagit/widgets/session.py b/tagit/widgets/session.py
index c233a15..30dfe51 100644
--- a/tagit/widgets/session.py
+++ b/tagit/widgets/session.py
@@ -68,13 +68,13 @@ class Session(Widget):
# open BSFS storage
store = bsfs.Open(cfg('session', 'bsfs'))
# check storage schema
- # FIXME: how to properly load the required schema?
- with open(os.path.join(os.path.dirname(__file__), '..', 'apps', 'port-schema.nt'), 'rt') as ifile:
+ with open(resource_find('required_schema.nt'), 'rt') as ifile:
required_schema = bsfs.schema.from_string(ifile.read())
- # FIXME: Since the store isn't persistent, we migrate to the required one here.
- #if not required_schema <= store.schema:
- # raise Exception('')
- store.migrate(required_schema)
+ if not required_schema.consistent_with(store.schema):
+ raise Exception("The storage's schema is incompatible with tagit's requirements")
+ if not required_schema <= store.schema:
+ store.migrate(required_schema | store.schema)
+
# replace current with new storage
self.storage = store
diff --git a/tagit/widgets/status.kv b/tagit/widgets/status.kv
index 2d49b15..0a680ab 100644
--- a/tagit/widgets/status.kv
+++ b/tagit/widgets/status.kv
@@ -1,5 +1,13 @@
#-- #:import ButtonDock tagit.widgets.dock.ButtonDock # FIXME: mb/port
+<NavigationLabel@Label>:
+ markup: True
+
+<StatusLabel@Label>:
+ markup: True
+ valign: 'middle'
+ halign: 'center'
+
<Status>:
orientation: 'horizontal'
status: ''
@@ -18,11 +26,10 @@
button_height: 30
button_show: 'image',
- Label:
+ NavigationLabel:
id: navigation_label
size_hint: None, 1
width: 180
- markup: True
text: root.navigation
ButtonDock:
@@ -36,13 +43,10 @@
button_height: 30
button_show: 'image',
- Label:
+ StatusLabel:
# gets remaining size
id: status_label
text_size: self.size
- markup: True
- valign: 'middle'
- halign: 'left'
text: root.status
ButtonDock: