diff options
| author | Matthias Baumgartner <dev@igsor.net> | 2023-03-05 19:16:41 +0100 |
|---|---|---|
| committer | Matthias Baumgartner <dev@igsor.net> | 2023-03-05 19:16:41 +0100 |
| commit | 98e567933723c59d1d97b3a85e649cfdce514676 (patch) | |
| tree | e6e0b475c7ab5c6a7ff4f0ea7ad1b08cecf05e68 /tagit/widgets | |
| parent | bf98c062ece242a5fc56de0f1adbc12f0588809a (diff) | |
| parent | 5e88d395dee651175a277092c712249e3898a7d8 (diff) | |
| download | tagit-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.kv | 23 | ||||
| -rw-r--r-- | tagit/widgets/browser.py | 92 | ||||
| -rw-r--r-- | tagit/widgets/filter.kv | 105 | ||||
| -rw-r--r-- | tagit/widgets/filter.py | 34 | ||||
| -rw-r--r-- | tagit/widgets/session.py | 12 | ||||
| -rw-r--r-- | tagit/widgets/status.kv | 16 |
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: |
