aboutsummaryrefslogtreecommitdiffstats
path: root/tagit/actions
diff options
context:
space:
mode:
authorMatthias Baumgartner <dev@igsor.net>2023-01-06 14:07:15 +0100
committerMatthias Baumgartner <dev@igsor.net>2023-01-06 14:07:15 +0100
commitad49aedaad3acece200ea92fd5d5a5b3e19c143b (patch)
tree3f6833aa6f7a81f456e992cb7ea453cdcdf6c22e /tagit/actions
parent079b4da93ea336b5bcc801cfd64c310aa7f8ddee (diff)
downloadtagit-ad49aedaad3acece200ea92fd5d5a5b3e19c143b.tar.gz
tagit-ad49aedaad3acece200ea92fd5d5a5b3e19c143b.tar.bz2
tagit-ad49aedaad3acece200ea92fd5d5a5b3e19c143b.zip
desktop dependent widgets early port
Diffstat (limited to 'tagit/actions')
-rw-r--r--tagit/actions/__init__.py131
-rw-r--r--tagit/actions/filter.kv41
-rw-r--r--tagit/actions/filter.py317
-rw-r--r--tagit/actions/grouping.kv27
-rw-r--r--tagit/actions/grouping.py257
5 files changed, 773 insertions, 0 deletions
diff --git a/tagit/actions/__init__.py b/tagit/actions/__init__.py
new file mode 100644
index 0000000..24524b4
--- /dev/null
+++ b/tagit/actions/__init__.py
@@ -0,0 +1,131 @@
+"""
+
+Part of the tagit module.
+A copy of the license is provided with the project.
+Author: Matthias Baumgartner, 2022
+"""
+# standard imports
+import typing
+
+# tagit imports
+from tagit.utils.builder import BuilderBase
+
+# inner-module imports
+#from . import browser
+from . import filter
+from . import grouping
+#from . import library
+#from . import misc
+#from . import objects
+#from . import planes
+#from . import search
+#from . import session
+#from . import tabs
+
+# exports
+__all__: typing.Sequence[str] = (
+ 'ActionBuilder',
+ )
+
+
+## code ##
+
+class ActionBuilder(BuilderBase):
+ _factories = {
+ ## browser
+ #'NextPage': browser.NextPage,
+ #'PreviousPage': browser.PreviousPage,
+ #'ScrollDown': browser.ScrollDown,
+ #'ScrollUp': browser.ScrollUp,
+ #'JumpToPage': browser.JumpToPage,
+ #'SetCursor': browser.SetCursor,
+ #'ZoomIn': browser.ZoomIn,
+ #'ZoomOut': browser.ZoomOut,
+ #'JumpToCursor': browser.JumpToCursor,
+ #'MoveCursorFirst': browser.MoveCursorFirst,
+ #'MoveCursorLast': browser.MoveCursorLast,
+ #'MoveCursorUp': browser.MoveCursorUp,
+ #'MoveCursorDown': browser.MoveCursorDown,
+ #'MoveCursorLeft': browser.MoveCursorLeft,
+ #'MoveCursorRight': browser.MoveCursorRight,
+ #'SelectRange': browser.SelectRange,
+ #'SelectAdditive': browser.SelectAdditive,
+ #'SelectSubtractive': browser.SelectSubtractive,
+ #'SelectMulti': browser.SelectMulti,
+ #'SelectSingle': browser.SelectSingle,
+ #'SelectAll': browser.SelectAll,
+ #'SelectNone': browser.SelectNone,
+ #'SelectInvert': browser.SelectInvert,
+ #'Select': browser.Select,
+ ## filter
+ #'AddToken': filter.AddToken,
+ #'RemoveToken': filter.RemoveToken,
+ #'SetToken': filter.SetToken,
+ #'EditToken': filter.EditToken,
+ #'GoBack': filter.GoBack,
+ #'GoForth': filter.GoForth,
+ #'JumpToToken': filter.JumpToToken,
+ #'SearchByAddressOnce': filter.SearchByAddressOnce,
+ #'SearchmodeSwitch': filter.SearchmodeSwitch,
+ ## grouping
+ #'CreateGroup': grouping.CreateGroup,
+ #'DissolveGroup': grouping.DissolveGroup,
+ #'AddToGroup': grouping.AddToGroup,
+ #'OpenGroup': grouping.OpenGroup,
+ #'RepresentGroup': grouping.RepresentGroup,
+ #'RemoveFromGroup': grouping.RemoveFromGroup,
+ ## library
+ #'AutoUpdate': library.AutoUpdate,
+ #'UpdateSelectedObjects': library.UpdateSelectedObjects,
+ #'UpdateObjects': library.UpdateObjects,
+ #'AutoImport': library.AutoImport,
+ #'ImportObjects': library.ImportObjects,
+ #'AutoSync': library.AutoSync,
+ #'SyncSelectedObjects': library.SyncSelectedObjects,
+ #'SyncObjects': library.SyncObjects,
+ #'ItemExport': library.ItemExport,
+ ## misc
+ #'ShellDrop': misc.ShellDrop,
+ #'OpenExternal': misc.OpenExternal,
+ #'Menu': misc.Menu,
+ #'ShowConsole': misc.ShowConsole,
+ #'ShowHelp': misc.ShowHelp,
+ #'ShowSettings': misc.ShowSettings,
+ #'ClipboardCopy': misc.ClipboardCopy,
+ #'ClipboardPaste': misc.ClipboardPaste,
+ ## objects
+ #'RotateLeft': objects.RotateLeft,
+ #'RotateRight': objects.RotateRight,
+ #'DeleteObject': objects.DeleteObject,
+ #'AddTag': objects.AddTag,
+ #'EditTag': objects.EditTag,
+ #'SetRank1': objects.SetRank1,
+ #'SetRank2': objects.SetRank2,
+ #'SetRank3': objects.SetRank3,
+ #'SetRank4': objects.SetRank4,
+ #'SetRank5': objects.SetRank5,
+ ## planes
+ #'ShowDashboard': planes.ShowDashboard,
+ #'ShowBrowsing': planes.ShowBrowsing,
+ #'ShowCodash': planes.ShowCodash,
+ ## search
+ #'Search': search.Search,
+ #'ShowSelected': search.ShowSelected,
+ #'RemoveSelected': search.RemoveSelected,
+ #'SortKey': search.SortKey,
+ #'SortOrder': search.SortOrder,
+ ## session
+ #'LoadSession': session.LoadSession,
+ #'SaveSession': session.SaveSession,
+ #'SaveSessionAs': session.SaveSessionAs,
+ #'CreateSession': session.CreateSession,
+ #'CreateTempSession': session.CreateTempSession,
+ #'ReloadSession': session.ReloadSession,
+ #'CloseSessionAndExit': session.CloseSessionAndExit,
+ ## tabs
+ #'AddTab': tabs.AddTab,
+ #'CloseTab': tabs.CloseTab,
+ #'SwitchTab': tabs.SwitchTab,
+ }
+
+## EOF ##
diff --git a/tagit/actions/filter.kv b/tagit/actions/filter.kv
new file mode 100644
index 0000000..2fce0e2
--- /dev/null
+++ b/tagit/actions/filter.kv
@@ -0,0 +1,41 @@
+#:import resource_find kivy.resources.resource_find
+
+<SearchByAddressOnce>:
+ source: resource_find('atlas://filter/address_once')
+ tooltip: 'Open the filters in address mode for a single edit'
+
+<SetToken>:
+ source: resource_find('atlas://filter/set_token')
+ tooltip: 'Set all filters from a text query'
+
+<AddToken>:
+ source: resource_find('atlas://filter/add')
+ tooltip: 'Add a tag filter'
+
+<EditToken>:
+ source: resource_find('atlas://filter/edit_token')
+ tooltip: 'Edit a filter token'
+
+<RemoveToken>:
+ source: resource_find('atlas://filter/remove_token')
+ tooltip: 'Remove a filter token'
+
+<GoBack>:
+ source: resource_find('atlas://filter/go_back')
+ tooltip: 'Previous view'
+
+<GoForth>:
+ source: resource_find('atlas://filter/go_forth')
+ tooltip: 'Next view'
+
+<JumpToToken>:
+ source: resource_find('atlas://filter/jump')
+ tooltip: 'Jump to filter token'
+
+<SearchmodeSwitch>:
+ source_address: resource_find('atlas://filter/address')
+ source_shingles: resource_find('atlas://filter/shingles')
+ source: self.source_shingles
+ tooltip: 'Switch between adress and shingles display'
+
+## EOF ##
diff --git a/tagit/actions/filter.py b/tagit/actions/filter.py
new file mode 100644
index 0000000..3702879
--- /dev/null
+++ b/tagit/actions/filter.py
@@ -0,0 +1,317 @@
+"""
+
+Part of the tagit module.
+A copy of the license is provided with the project.
+Author: Matthias Baumgartner, 2022
+"""
+# standard imports
+import os
+
+# kivy imports
+from kivy.lang import Builder
+import kivy.properties as kp
+
+# tagit imports
+from tagit import config
+from tagit import dialogues
+#from tagit.parsing import ParserError # FIXME: mb/port
+#from tagit.parsing.search import ast_from_string, ast_to_string, ast # FIXME: mb/port
+#from tagit.storage.base import ns # FIXME: mb/port
+from tagit.utils import Frame
+from tagit.widgets.bindings import Binding
+from tagit.widgets.filter import FilterAwareMixin
+
+# inner-module imports
+from .action import Action
+
+# exports
+__all__ = []
+
+
+## code ##
+
+# load kv
+Builder.load_file(os.path.join(os.path.dirname(__file__), 'filter.kv'))
+
+# classes
+
+class SearchByAddressOnce(Action):
+ """Open the filters in address mode for a single edit"""
+ text = kp.StringProperty('Inline edit')
+
+ def ktrigger(self, evt):
+ return Binding.check(evt, self.cfg('bindings', 'filter', 'edit_once'))
+
+ def apply(self):
+ self.root.filter.show_address_once()
+
+
+class SetToken(Action):
+ """Set all filters from a text query."""
+ text = kp.StringProperty('Set tokens')
+
+ def apply(self, text):
+ with self.root.filter as filter:
+ try:
+ # parse filter into tokens
+ tokens = list(ast_from_string(text))
+
+ # grab current frame
+ filter.f_head.append(self.root.browser.frame)
+
+ # keep frames for tokens that didn't change
+ # create a new frame for changed (new or modified) tokens
+ frames = [filter.f_head[0]]
+ for tok in tokens:
+ if tok in filter.t_head: # t_head frames have precedence
+ frame = filter.f_head[filter.t_head.index(tok) + 1]
+ elif tok in filter.t_tail:
+ frame = filter.f_tail[filter.t_tail.index(tok)]
+ else:
+ frame = Frame()
+ frames.append(frame)
+ # use the last frame as current one
+ self.root.browser.frame = frames.pop()
+
+ # set tokens
+ filter.t_head = tokens
+ filter.t_tail = []
+ # set frames
+ filter.f_head = frames
+ filter.f_tail = []
+
+ except ParserError as e:
+ dialogues.Error(text=f'syntax error: {e}').open()
+
+
+class AddToken(Action):
+ """Show a dialogue for adding a filter."""
+ text = kp.StringProperty('Add filter')
+
+ def ktrigger(self, evt):
+ return Binding.check(evt, self.cfg('bindings', 'filter', 'add_token'))
+
+ def apply(self, token=None):
+ if token is None:
+ sugg = {node.label for node in self.root.session.storage.all(ns.tagit.storage.base.Tag)}
+ dlg = dialogues.TokenEdit(suggestions=sugg)
+ dlg.bind(on_ok=lambda wx: self.add_from_string(wx.text))
+ dlg.open()
+ elif isinstance(token, str):
+ self.add_from_string(token)
+ elif isinstance(token, ast.ASTNode):
+ self.add_token([token])
+
+ def add_from_string(self, text):
+ try:
+ self.add_token(ast_from_string(text))
+ except ParserError as e:
+ dialogues.Error(text=f'syntax error: {e}').open()
+
+ def add_token(self, tokens):
+ with self.root.filter as filter:
+ tokens = [tok for tok in tokens if tok not in filter.t_head]
+ for tok in tokens:
+ # add token and frame
+ filter.t_head.append(tok)
+ filter.f_head.append(self.root.browser.frame)
+ if len(tokens):
+ # clear tails
+ filter.t_tail = []
+ filter.f_tail = []
+ # issue new frame
+ self.root.browser.frame = Frame()
+
+
+class EditToken(Action):
+ """Show a dialogue for editing a filter."""
+ text = kp.StringProperty('Edit token')
+
+ def apply(self, token):
+ sugg = {node.label for node in self.root.session.storage.all(ns.tagit.storage.base.Tag)}
+ text = ast_to_string(token)
+ dlg = dialogues.TokenEdit(text=text, suggestions=sugg)
+ dlg.bind(on_ok=lambda obj: self.on_ok(token, obj))
+ dlg.open()
+
+ def on_ok(self, token, obj):
+ with self.root.filter as filter:
+ try:
+ tokens_from_text = ast_from_string(obj.text)
+ except ParserError as e:
+ dialogues.Error(text=f'Invalid token: {e}').open()
+ return
+
+ # TODO: Check if this can be simplified
+ keep = False
+ for tok in tokens_from_text:
+ if tok == token: # don't remove if the token hasn't changed
+ keep = True
+
+ if token in filter.t_head and tok not in filter.t_head:
+ # insert after token into t_head
+ idx = filter.t_head.index(token)
+ frame = filter.f_head[idx]
+ filter.t_head.insert(idx + 1, tok)
+ filter.f_head.insert(idx + 1, frame.copy())
+ # remove from t_tail
+ if tok in filter.t_tail:
+ idx = filter.t_tail.index(tok)
+ filter.f_tail.pop(idx)
+ filter.t_tail.pop(idx)
+ elif token in filter.t_tail and tok not in filter.t_tail:
+ # insert after token into t_tail
+ idx = filter.t_tail.index(token)
+ frame = filter.f_tail[idx]
+ filter.t_tail.insert(idx + 1, tok)
+ filter.f_tail.insert(idx + 1, frame.copy())
+ # remove from t_head
+ if tok in filter.t_head:
+ idx = filter.t_head.index(tok)
+ filter.t_head.pop(idx)
+ filter.f_head.pop(idx)
+
+ # remove original token
+ if not keep and token in filter.t_head:
+ idx = filter.t_head.index(token)
+ filter.t_head.pop(idx)
+ filter.f_head.pop(idx)
+ if not keep and token in filter.t_tail:
+ idx = filter.t_tail.index(token)
+ filter.t_tail.pop(idx)
+ filter.f_tail.pop(idx)
+
+
+class RemoveToken(Action):
+ """Remove a filter."""
+ text = kp.StringProperty('Remove token')
+
+ def apply(self, token):
+ with self.root.filter as filter:
+ if token in filter.t_head:
+ idx = filter.t_head.index(token)
+ # remove frame
+ if idx < len(filter.t_head) - 1:
+ filter.f_head.pop(idx + 1)
+ self.root.browser.frame = Frame()
+ else:
+ self.root.browser.frame = filter.f_head.pop()
+ # remove token
+ filter.t_head.remove(token)
+
+ if token in filter.f_tail:
+ filter.f_tail.pop(filter.t_tail.index(token))
+ filter.t_tail.remove(token)
+
+
+class GoBack(Action):
+ """Remove the rightmost filter from the search."""
+ text = kp.StringProperty('Previous search')
+
+ def ktrigger(self, evt):
+ return Binding.check(evt, self.cfg('bindings', 'filter', 'go_back'))
+
+ def apply(self, n_steps=1):
+ with self.root.filter as filter:
+ for _ in range(n_steps):
+ if len(filter.t_head) > 0:
+ # move tokens
+ filter.t_tail.insert(0, filter.t_head.pop(-1))
+ # move frames
+ filter.f_tail.insert(0, self.root.browser.frame)
+ self.root.browser.frame = filter.f_head.pop(-1)
+
+
+class GoForth(Action):
+ """Add the rightmost filter to the search"""
+ text = kp.StringProperty('Next search')
+
+ def ktrigger(self, evt):
+ return Binding.check(evt, self.cfg('bindings', 'filter', 'go_forth'))
+
+ def apply(self, n_steps=1):
+ with self.root.filter as filter:
+ for _ in range(n_steps):
+ if len(filter.t_tail) > 0:
+ # move tokens
+ filter.t_head.append(filter.t_tail.pop(0))
+ # move frames
+ filter.f_head.append(self.root.browser.frame)
+ self.root.browser.frame = filter.f_tail.pop(0)
+
+
+class JumpToToken(Action):
+ """Jump to a filter token."""
+ text = kp.StringProperty('Jump to token')
+
+ def apply(self, token):
+ filter = self.root.filter
+ if token == filter.t_head[-1]:
+ pass
+ elif token in filter.t_head: # go
+ self.root.trigger('GoBack', len(filter.t_head) - filter.t_head.index(token) - 1)
+ elif token in filter.t_tail:
+ self.root.trigger('GoForth', filter.t_tail.index(token) + 1)
+
+
+class SearchmodeSwitch(Action, FilterAwareMixin):
+ """Switch between shingle and address search bar display."""
+ text = kp.StringProperty('Toggle searchbar mode')
+
+ def on_root(self, wx, root):
+ Action.on_root(self, wx, root)
+ FilterAwareMixin.on_root(self, wx, root)
+
+ def on_searchmode(self, filter, searchmode):
+ if self._image is not None:
+ if searchmode == filter.MODE_ADDRESS:
+ self._image.source = self.source_address
+ else:
+ self._image.source = self.source_shingles
+
+ def on_filter(self, wx, filter):
+ # remove old binding
+ if self.filter is not None:
+ self.filter.unbind(searchmode=self.on_searchmode)
+ # add new binding
+ self.filter = filter
+ if self.filter is not None:
+ self.filter.bind(searchmode=self.on_searchmode)
+ self.on_searchmode(self.filter, self.filter.searchmode)
+
+ def __del__(self):
+ if self.filter is not None:
+ self.filter.unbind(searchmode=self.on_searchmode)
+ self.filter = None
+
+ def apply(self):
+ with self.root.filter as filter:
+ if filter.searchmode == filter.MODE_SHINGLES:
+ filter.searchmode = filter.MODE_ADDRESS
+ else:
+ filter.searchmode = filter.MODE_SHINGLES
+
+
+## config ##
+
+# keybindings
+
+config.declare(('bindings', 'filter', 'add_token'),
+ config.Keybind(), Binding.simple('k', Binding.mCTRL, Binding.mREST), # Ctrl + k
+ __name__, AddToken.text.defaultvalue, AddToken.__doc__)
+
+config.declare(('bindings', 'filter', 'go_forth'),
+ config.Keybind(), Binding.simple(Binding.RIGHT, Binding.mALT, Binding.mREST), # Alt + right
+ __name__, GoForth.text.defaultvalue, GoForth.__doc__)
+
+config.declare(('bindings', 'filter', 'edit_once'), config.Keybind(),
+ Binding.multi(('l', Binding.mCTRL, Binding.mREST),
+ ('/', Binding.mREST, Binding.mALL)), # Ctrl + l, /
+ __name__, SearchByAddressOnce.text.defaultvalue, SearchByAddressOnce.__doc__)
+
+config.declare(('bindings', 'filter', 'go_back'), config.Keybind(),
+ Binding.multi((Binding.BACKSPACE, Binding.mCTRL, Binding.mREST),
+ (Binding.LEFT, Binding.mALT, Binding.mREST)), # Ctrl + backspace, Alt + left
+ __name__, GoBack.text.defaultvalue, GoBack.__doc__)
+
+## EOF ##
diff --git a/tagit/actions/grouping.kv b/tagit/actions/grouping.kv
new file mode 100644
index 0000000..9135c55
--- /dev/null
+++ b/tagit/actions/grouping.kv
@@ -0,0 +1,27 @@
+#:import resource_find kivy.resources.resource_find
+
+<CreateGroup>:
+ source: resource_find('atlas://grouping/create')
+ tooltip: 'Group items'
+
+<DissolveGroup>:
+ source: resource_find('atlas://grouping/ungroup')
+ tooltip: 'Ungroup items'
+
+<AddToGroup>:
+ source: resource_find('atlas://grouping/add')
+ tooltip: 'Add items to group'
+
+<OpenGroup>:
+ source: resource_find('atlas://grouping/group')
+ tooltip: 'Open Group'
+
+<RepresentGroup>:
+ source: resource_find('atlas://grouping/represent')
+ tooltip: 'Make group representative'
+
+<RemoveFromGroup>:
+ source: resource_find('atlas://grouping/remove')
+ tooltip: 'Remove from group'
+
+## EOF ##
diff --git a/tagit/actions/grouping.py b/tagit/actions/grouping.py
new file mode 100644
index 0000000..eddaeb6
--- /dev/null
+++ b/tagit/actions/grouping.py
@@ -0,0 +1,257 @@
+"""
+
+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.parsing.search import ast # FIXME: mb/port
+#from tagit.storage.broker import Representative # FIXME: mb/port
+from tagit.widgets import Binding
+from tagit.utils import Frame
+
+# inner-module imports
+from .action import Action
+
+# 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 = Group.Create()
+ if label is not None:
+ grp.label = label
+
+ # add items to group
+ ents = self.root.session.storage.entities(browser.unfold(browser.selection))
+ ents.group += grp
+
+ # select a random representative
+ grp.represented_by = random.choice(ents)
+
+ # set selection and cursor to representative
+ # the representative will become valid after the search was re-applied
+ browser.selection.clear()
+ browser.selection.add(grp.represented_by)
+ browser.cursor = rep
+
+ # notification
+ logger.info(f'Grouped {len(items)} items')
+
+ # change event
+ session.dispatch('on_predicate_modified', 'group', items, {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:
+ # remove tag from items
+ items = list(cursor.members())
+ #ents = ...
+ #grp = ...
+ #ents.group -= grp
+ for obj in items:
+ obj.group -= [cursor.represents()] # FIXME
+
+ # 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(items)} items')
+
+ # change event
+ session.dispatch('on_predicate_modified', 'group', items, {cursor.represents()})
+
+ 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:
+ items = browser.unfold(browser.selection)
+ for obj in items:
+ if obj == cursor:
+ # don't add group to itself
+ continue
+ obj.group += [cursor.represents()] # FIXME: Not quite sure how to handle this
+
+ # 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', 'group', items, {cursor.represents()})
+
+
+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
+ if cursor is not None and cursor in self.root.browser.folds:
+ token = ast.Token('group', ast.SetInclude(cursor.represents()))
+ self.root.trigger('AddToken', token)
+
+
+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):
+ 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):
+ 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 ##