""" Part of the tagit module. A copy of the license is provided with the project. Author: Matthias Baumgartner, 2022 """ # standard imports import logging import os import random # kivy imports from kivy.lang import Builder import kivy.properties as kp # tagit imports from tagit import config, dialogues from tagit.utils import Frame, ns from tagit.utils.bsfs import Namespace, ast, uuid from tagit.widgets import Binding # inner-module imports from .action import Action # constants GROUP_PREFIX = Namespace('http://example.com/me/group')() # exports __all__ = [] ## code ## logger = logging.getLogger(__name__) # load kv Builder.load_file(os.path.join(os.path.dirname(__file__), 'grouping.kv')) # classes class CreateGroup(Action): """Create a group from selected items.""" text = kp.StringProperty('Group items') def ktrigger(self, evt): return Binding.check(evt, self.cfg('bindings', 'grouping', 'create')) def apply(self): if self.cfg('ui', 'standalone', 'grouping', 'autoname'): self.create_group() else: dlg = dialogues.SimpleInput() dlg.bind(on_ok=lambda wx: self.create_group(wx.text)) dlg.open() def create_group(self, label=None): if len(self.root.browser.selection) > 1: with self.root.browser as browser, \ self.root.session as session: # create group grp = session.storage.node(ns.bsn.Group, getattr(GROUP_PREFIX, uuid.UUID()())) if label is not None: grp.set(ns.bsg.label, label) # add items to group ents = browser.unfold(browser.selection) ents.set(ns.bse.group, grp) # select a random representative rep = random.choice(list(ents)) grp.set(ns.bsg.represented_by, rep) # set selection and cursor to representative # the representative will become valid after the search was re-applied browser.selection.clear() browser.selection.add(rep) browser.cursor = rep # notification logger.info(f'Grouped {len(ents)} items') # change event session.dispatch('on_predicate_modified', ns.bse.group, ents, {grp}) # jump to cursor # needs to be done *after* the browser was updated self.root.trigger('JumpToCursor') class DissolveGroup(Action): """Dissolve the selected group.""" text = kp.StringProperty('Dissolve group') def ktrigger(self, evt): return Binding.check(evt, self.cfg('bindings', 'grouping', 'ungroup')) def apply(self): with self.root.browser as browser, \ self.root.session as session: cursor = browser.cursor if cursor is not None and cursor in browser.folds: grp = browser.folds[cursor].group ents = session.storage.get(ns.bsn.Entity, ast.filter.Any(ns.bse.group, ast.filter.Is(grp))) #ents.remove(ns.bse.group, grp) # FIXME: mb/port #grp.delete() # FIXME: mb/port # FIXME: fix cursor and selection # cursor: leave at item that was the representative # selection: leave as is, select all group members if the cursor was selected browser.frame = Frame() # notification logger.info(f'Ungrouped {len(ents)} items') # change event session.dispatch('on_predicate_modified', ns.bse.group, ents, {grp}) self.root.trigger('JumpToCursor') class AddToGroup(Action): """Add an item to a group.""" text = kp.StringProperty('Add to group') def ktrigger(self, evt): return Binding.check(evt, self.cfg('bindings', 'grouping', 'add')) def apply(self): with self.root.browser as browser, \ self.root.session as session: cursor = browser.cursor if cursor is not None and cursor in browser.folds: grp = browser.folds[cursor].group ents = browser.unfold(browser.selection) for obj in ents: if obj == cursor: # don't add group to itself continue obj.set(ns.bse.group, gr) # all selected items will be folded, hence it becomes empty if cursor in browser.selection: browser.selection = {cursor} else: browser.selection.clear() # change event session.dispatch('on_predicate_modified', ns.bse.group, ents, {grp}) class OpenGroup(Action): """Show the items of the selected group.""" text = kp.StringProperty('Open group') def ktrigger(self, evt): return Binding.check(evt, self.cfg('bindings', 'grouping', 'open')) def apply(self, cursor=None): if cursor is None: cursor = self.root.browser.cursor elif cursor in self.root.browser.folds: grp = self.root.browser.folds[cursor].group self.root.trigger('AddToken', ast.filter.Any( ns.bse.group, ast.filter.Is(grp))) class RepresentGroup(Action): """Make the currently selected item the representative of the current group.""" text = kp.StringProperty('Represent') def ktrigger(self, evt): return Binding.check(evt, self.cfg('bindings', 'grouping', 'represent')) def apply(self): return # FIXME: mb/port with self.root.browser as browser, \ self.root.filter as filter, \ self.root.session as session: if browser.cursor is not None \ and len(filter.t_head) > 0 \ and filter.t_head[-1].predicate() == 'group': # we know that the cursor is part of the group, since it matches the filter. guid = filter.t_head[-1].condition()[0] # FIXME! grp = session.storage.node(guid, ns.tagit.storage.Group) grp.represented_by = browser.cursor logger.info(f'{browser.cursor} now represents {grp}') class RemoveFromGroup(Action): """Remove the selected item from the group""" text = kp.StringProperty('Remove from group') def ktrigger(self, evt): return Binding.check(evt, self.cfg('bindings', 'grouping', 'remove')) def apply(self): return # FIXME: mb/port with self.root.browser as browser, \ self.root.filter as filter, \ self.root.session as session: if len(filter.t_head) > 0 and \ filter.t_head[-1].predicate() == 'group': guid = filter.t_head[-1].condition()[0] grp = session.storage.group(guid) ents = self.root.session.storage.entities(browser.unfold(browser.selection)) ents -= grp try: rep = grp.represented_by if grp not in rep.group: # representative was removed, pick a random one random.choice(list(rep.members())).represent(grp) # FIXME: member relation? # grp.represented_by = random.choice(list(rep.members())) except ValueError: # group is now empty pass # set cursor to a non-selected (hence non-removed) item # clear the selection browser.cursor = browser.neighboring_unselected() browser.selection = [browser.cursor] if browser.cursor is not None else [] # change event session.dispatch('on_predicate_modified', 'group', items, {grp}) self.root.trigger('JumpToCursor') ## config ## config.declare(('ui', 'standalone', 'grouping', 'autoname'), config.Bool(), True, __name__, 'Auto-name groups', 'If enabled, group names are auto-generated (resulting in somewhat cryptical names). If disabled, a name can be specified when creating new groups.') # keybindings config.declare(('bindings', 'grouping', 'create'), config.Keybind(), Binding.simple('g', Binding.mCTRL, Binding.mREST), __name__, CreateGroup.text.defaultvalue, CreateGroup.__doc__) config.declare(('bindings', 'grouping', 'ungroup'), config.Keybind(), Binding.simple('g', [Binding.mALT, Binding.mCTRL], Binding.mREST), __name__, DissolveGroup.text.defaultvalue, DissolveGroup.__doc__) config.declare(('bindings', 'grouping', 'add'), config.Keybind(), Binding.simple('h', [Binding.mCTRL], Binding.mREST), __name__, AddToGroup.text.defaultvalue, AddToGroup.__doc__) config.declare(('bindings', 'grouping', 'open'), config.Keybind(), Binding.simple('g', None, Binding.mREST), __name__, OpenGroup.text.defaultvalue, OpenGroup.__doc__) config.declare(('bindings', 'grouping', 'represent'), config.Keybind(), Binding.simple('g', [Binding.mCTRL, Binding.mSHIFT], Binding.mREST), __name__, RepresentGroup.text.defaultvalue, RepresentGroup.__doc__) config.declare(('bindings', 'grouping', 'remove'), config.Keybind(), Binding.simple('h', [Binding.mSHIFT, Binding.mCTRL], Binding.mREST), __name__, RemoveFromGroup.text.defaultvalue, RemoveFromGroup.__doc__) ## EOF ##