From ad49aedaad3acece200ea92fd5d5a5b3e19c143b Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Fri, 6 Jan 2023 14:07:15 +0100 Subject: desktop dependent widgets early port --- tagit/actions/__init__.py | 131 +++++++++++++++++++ tagit/actions/filter.kv | 41 ++++++ tagit/actions/filter.py | 317 ++++++++++++++++++++++++++++++++++++++++++++++ tagit/actions/grouping.kv | 27 ++++ tagit/actions/grouping.py | 257 +++++++++++++++++++++++++++++++++++++ 5 files changed, 773 insertions(+) create mode 100644 tagit/actions/__init__.py create mode 100644 tagit/actions/filter.kv create mode 100644 tagit/actions/filter.py create mode 100644 tagit/actions/grouping.kv create mode 100644 tagit/actions/grouping.py (limited to 'tagit/actions') 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 + +: + source: resource_find('atlas://filter/address_once') + tooltip: 'Open the filters in address mode for a single edit' + +: + source: resource_find('atlas://filter/set_token') + tooltip: 'Set all filters from a text query' + +: + source: resource_find('atlas://filter/add') + tooltip: 'Add a tag filter' + +: + source: resource_find('atlas://filter/edit_token') + tooltip: 'Edit a filter token' + +: + source: resource_find('atlas://filter/remove_token') + tooltip: 'Remove a filter token' + +: + source: resource_find('atlas://filter/go_back') + tooltip: 'Previous view' + +: + source: resource_find('atlas://filter/go_forth') + tooltip: 'Next view' + +: + source: resource_find('atlas://filter/jump') + tooltip: 'Jump to filter token' + +: + 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 + +: + source: resource_find('atlas://grouping/create') + tooltip: 'Group items' + +: + source: resource_find('atlas://grouping/ungroup') + tooltip: 'Ungroup items' + +: + source: resource_find('atlas://grouping/add') + tooltip: 'Add items to group' + +: + source: resource_find('atlas://grouping/group') + tooltip: 'Open Group' + +: + source: resource_find('atlas://grouping/represent') + tooltip: 'Make group representative' + +: + 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 ## -- cgit v1.2.3 From ceaaef069d8ffda23fce320ce66c86e0226f1046 Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Fri, 6 Jan 2023 17:40:25 +0100 Subject: first startup, empty screen --- tagit/actions/__init__.py | 2 +- tagit/actions/filter.py | 22 +++++++++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) (limited to 'tagit/actions') diff --git a/tagit/actions/__init__.py b/tagit/actions/__init__.py index 24524b4..444bd73 100644 --- a/tagit/actions/__init__.py +++ b/tagit/actions/__init__.py @@ -18,7 +18,7 @@ from . import grouping #from . import misc #from . import objects #from . import planes -#from . import search +from . import search #from . import session #from . import tabs diff --git a/tagit/actions/filter.py b/tagit/actions/filter.py index 3702879..869844e 100644 --- a/tagit/actions/filter.py +++ b/tagit/actions/filter.py @@ -22,7 +22,7 @@ from tagit.widgets.bindings import Binding from tagit.widgets.filter import FilterAwareMixin # inner-module imports -from .action import Action +from . import action # exports __all__ = [] @@ -35,7 +35,7 @@ Builder.load_file(os.path.join(os.path.dirname(__file__), 'filter.kv')) # classes -class SearchByAddressOnce(Action): +class SearchByAddressOnce(action.Action): """Open the filters in address mode for a single edit""" text = kp.StringProperty('Inline edit') @@ -46,7 +46,7 @@ class SearchByAddressOnce(Action): self.root.filter.show_address_once() -class SetToken(Action): +class SetToken(action.Action): """Set all filters from a text query.""" text = kp.StringProperty('Set tokens') @@ -84,7 +84,7 @@ class SetToken(Action): dialogues.Error(text=f'syntax error: {e}').open() -class AddToken(Action): +class AddToken(action.Action): """Show a dialogue for adding a filter.""" text = kp.StringProperty('Add filter') @@ -123,7 +123,7 @@ class AddToken(Action): self.root.browser.frame = Frame() -class EditToken(Action): +class EditToken(action.Action): """Show a dialogue for editing a filter.""" text = kp.StringProperty('Edit token') @@ -182,7 +182,7 @@ class EditToken(Action): filter.f_tail.pop(idx) -class RemoveToken(Action): +class RemoveToken(action.Action): """Remove a filter.""" text = kp.StringProperty('Remove token') @@ -204,7 +204,7 @@ class RemoveToken(Action): filter.t_tail.remove(token) -class GoBack(Action): +class GoBack(action.Action): """Remove the rightmost filter from the search.""" text = kp.StringProperty('Previous search') @@ -222,7 +222,7 @@ class GoBack(Action): self.root.browser.frame = filter.f_head.pop(-1) -class GoForth(Action): +class GoForth(action.Action): """Add the rightmost filter to the search""" text = kp.StringProperty('Next search') @@ -240,7 +240,7 @@ class GoForth(Action): self.root.browser.frame = filter.f_tail.pop(0) -class JumpToToken(Action): +class JumpToToken(action.Action): """Jump to a filter token.""" text = kp.StringProperty('Jump to token') @@ -254,12 +254,12 @@ class JumpToToken(Action): self.root.trigger('GoForth', filter.t_tail.index(token) + 1) -class SearchmodeSwitch(Action, FilterAwareMixin): +class SearchmodeSwitch(action.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) + action.Action.on_root(self, wx, root) FilterAwareMixin.on_root(self, wx, root) def on_searchmode(self, filter, searchmode): -- cgit v1.2.3 From 1a8d8f8a37e78f48da88dd69e785234d822425ed Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Fri, 6 Jan 2023 22:55:36 +0100 Subject: load from config, switch to browsing --- tagit/actions/__init__.py | 16 ++--- tagit/actions/misc.kv | 35 ++++++++++ tagit/actions/misc.py | 167 ++++++++++++++++++++++++++++++++++++++++++++++ tagit/actions/planes.kv | 15 +++++ tagit/actions/planes.py | 57 ++++++++++++++++ 5 files changed, 282 insertions(+), 8 deletions(-) create mode 100644 tagit/actions/misc.kv create mode 100644 tagit/actions/misc.py create mode 100644 tagit/actions/planes.kv create mode 100644 tagit/actions/planes.py (limited to 'tagit/actions') diff --git a/tagit/actions/__init__.py b/tagit/actions/__init__.py index 444bd73..9fd6342 100644 --- a/tagit/actions/__init__.py +++ b/tagit/actions/__init__.py @@ -15,9 +15,9 @@ from tagit.utils.builder import BuilderBase from . import filter from . import grouping #from . import library -#from . import misc +from . import misc #from . import objects -#from . import planes +from . import planes from . import search #from . import session #from . import tabs @@ -88,9 +88,9 @@ class ActionBuilder(BuilderBase): #'ShellDrop': misc.ShellDrop, #'OpenExternal': misc.OpenExternal, #'Menu': misc.Menu, - #'ShowConsole': misc.ShowConsole, - #'ShowHelp': misc.ShowHelp, - #'ShowSettings': misc.ShowSettings, + 'ShowConsole': misc.ShowConsole, + 'ShowHelp': misc.ShowHelp, + 'ShowSettings': misc.ShowSettings, #'ClipboardCopy': misc.ClipboardCopy, #'ClipboardPaste': misc.ClipboardPaste, ## objects @@ -105,9 +105,9 @@ class ActionBuilder(BuilderBase): #'SetRank4': objects.SetRank4, #'SetRank5': objects.SetRank5, ## planes - #'ShowDashboard': planes.ShowDashboard, - #'ShowBrowsing': planes.ShowBrowsing, - #'ShowCodash': planes.ShowCodash, + 'ShowDashboard': planes.ShowDashboard, + 'ShowBrowsing': planes.ShowBrowsing, + 'ShowCodash': planes.ShowCodash, ## search #'Search': search.Search, #'ShowSelected': search.ShowSelected, diff --git a/tagit/actions/misc.kv b/tagit/actions/misc.kv new file mode 100644 index 0000000..f9d5157 --- /dev/null +++ b/tagit/actions/misc.kv @@ -0,0 +1,35 @@ +#:import resource_find kivy.resources.resource_find + +: + source: resource_find('atlas://misc/menu') + tooltip: 'Open the menu' + +: + source: resource_find('atlas://misc/shell') + tooltip: 'Open a terminal shell' + +: + source: resource_find('atlas://misc/open_external') + tooltip: 'Open selected items in an external application' + +: + source: resource_find('atlas://misc/console') + tooltip: 'Open the log console' + +: + source: resource_find('atlas://misc/help') + tooltip: 'Open the help' + +: + source: resource_find('atlas://misc/settings') + tooltip: 'Open the settings menu' + +: + source: resource_find('atlas://misc/clip_copy') + tooltip: 'Copy selected items to the clipboard' + +: + source: resource_find('atlas://misc/clip_paste') + tooltip: 'Import files from the clipboard' + +## EOF ## diff --git a/tagit/actions/misc.py b/tagit/actions/misc.py new file mode 100644 index 0000000..dc939ca --- /dev/null +++ b/tagit/actions/misc.py @@ -0,0 +1,167 @@ +""" + +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 + +# kivy imports +from kivy.core.clipboard import Clipboard +from kivy.lang import Builder +import kivy.properties as kp +import webbrowser + +# tagit imports +from tagit import config +#from tagit.io_.sync import export # FIXME: mb/port +#from tagit.utils import fileopen # FIXME: mb/port +from tagit.widgets import Binding + +# inner-module imports +from .action import Action + +# constants +HELP_URL = 'https://www.igsor.net/projects/tagit/' + +# exports +__all__ = [] + + +## code ## + +logger = logging.getLogger(__name__) + +# load kv +Builder.load_file(os.path.join(os.path.dirname(__file__), 'misc.kv')) + +# classes +class Menu(Action): + """Open the menu.""" + text = kp.StringProperty('Menu') + + def ktrigger(self, evt): + return Binding.check(evt, self.cfg('bindings', 'misc', 'menu')) + + def apply(self): + x = self.pos[0] + self.width + y = self.pos[1] + self.height + self.root.context.show(x, y) + + +class ShellDrop(Action): + """Open a terminal shell.""" + text = kp.StringProperty('Shell') + + def apply(self): + from tagit import debug + debug(locals(), globals()) + + +class OpenExternal(Action): + """Open the selected items in an external application.""" + text = kp.StringProperty('Open') + + def ktrigger(self, evt): + # FIXME: Triggered on Shift + Click (Interferes with selection!) + # Triggered on when tags are edited. + return Binding.check(evt, self.cfg('bindings', 'misc', 'open')) + + def apply(self): + with self.root.browser as browser: + if browser.cursor is None: + logger.error('No file selected') + elif os.path.exists(browser.cursor.path): + fileopen(browser.cursor.path) + else: + logger.error('File unavailable') + + +class ShowConsole(Action): + """Open the log console.""" + text = kp.StringProperty('Console') + + def apply(self): + self.root.status.console() + + +class ShowHelp(Action): + """Show some help.""" + text = kp.StringProperty('Help') + + def ktrigger(self, evt): + return Binding.check(evt, self.cfg('bindings', 'misc', 'help')) + + def apply(self): + webbrowser.open(HELP_URL) + + +class ShowSettings(Action): + """Open the settings menu.""" + text = kp.StringProperty('Settings') + + def ktrigger(self, evt): + return Binding.check(evt, self.cfg('bindings', 'misc', 'settings')) + + def apply(self): + from kivy.app import App + App.get_running_app().open_settings() + + +class ClipboardCopy(Action): + """Copy selected items into the clipboard.""" + text = kp.StringProperty('Copy to clipboard') + + def ktrigger(self, evt): + return Binding.check(evt, self.cfg('bindings', 'clipboard', 'copy')) + + def apply(self): + browser = self.root.browser + paths = [obj.path for obj in browser.selection] + Clipboard.copy('\n'.join(paths)) + + +class ClipboardPaste(Action): + """Import items from the clipboard.""" + text = kp.StringProperty('Paste from clipboard') + + def ktrigger(self, evt): + return Binding.check(evt, self.cfg('bindings', 'clipboard', 'paste')) + + def apply(self): + paths = Clipboard.paste() + paths = paths.split('\n') + self.root.trigger('ImportObjects', paths) + + +## config ## + +# keybindings + +config.declare(('bindings', 'misc', 'menu'), + config.Keybind(), Binding.simple(Binding.CMD, None, Binding.mALL), + __name__, Menu.text.defaultvalue, Menu.__doc__) + +config.declare(('bindings', 'misc', 'open'), + config.Keybind(), Binding.simple(Binding.ENTER, None, Binding.mALL), + __name__, OpenExternal.text.defaultvalue, OpenExternal.__doc__) + +config.declare(('bindings', 'misc', 'help'), + config.Keybind(), Binding.simple('/', Binding.mSHIFT), + __name__, ShowHelp.text.defaultvalue, ShowHelp.__doc__) + +config.declare(('bindings', 'misc', 'settings'), + config.Keybind(), Binding.simple(Binding.F1), # also the kivy default + __name__, ShowSettings.text.defaultvalue, ShowSettings.__doc__) + +config.declare(('bindings', 'clipboard', 'copy'), + config.Keybind(), Binding.simple('c', Binding.mCTRL), + __name__, ClipboardCopy.text.defaultvalue, ClipboardCopy.__doc__) + +config.declare(('bindings', 'clipboard', 'paste'), + config.Keybind(), Binding.simple('v', Binding.mCTRL), + __name__, ClipboardPaste.text.defaultvalue, ClipboardPaste.__doc__) + +## EOF ## diff --git a/tagit/actions/planes.kv b/tagit/actions/planes.kv new file mode 100644 index 0000000..184f949 --- /dev/null +++ b/tagit/actions/planes.kv @@ -0,0 +1,15 @@ +#:import resource_find kivy.resources.resource_find + +: + source: resource_find('atlas://planes/dashboard') + tooltip: 'Switch to the Dashboard' + +: + source: resource_find('atlas://planes/browsing') + tooltip: 'Switch to the browsing plane' + +: + source: resource_find('atlas://planes/codash') + tooltip: 'Switch to the contextual dashboard' + +## EOF ## diff --git a/tagit/actions/planes.py b/tagit/actions/planes.py new file mode 100644 index 0000000..89f93bb --- /dev/null +++ b/tagit/actions/planes.py @@ -0,0 +1,57 @@ +""" + +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 + +# inner-module imports +from .action import Action + +# exports +__all__ = [] + + +## code ## + +# load kv +Builder.load_file(os.path.join(os.path.dirname(__file__), 'planes.kv')) + +# classes + +class ShowDashboard(Action): + """Switch to the dashboard.""" + text = kp.StringProperty('Dashboard') + + def apply(self): + planes = self.root.planes + if planes.current_slide != planes.dashboard: + planes.load_slide(planes.dashboard) + + +class ShowBrowsing(Action): + """Switch to the browsing plane.""" + text = kp.StringProperty('Browsing') + + def apply(self): + planes = self.root.planes + if planes.current_slide != planes.browsing: + planes.load_slide(planes.browsing) + + +class ShowCodash(Action): + """Switch to the contextual dashboard.""" + text = kp.StringProperty('Context') + + def apply(self): + planes = self.root.planes + if planes.current_slide != planes.codash: + planes.load_slide(planes.codash) + +## EOF ## -- cgit v1.2.3 From 6b6495b8f5b3bfd8fbd4caf56a44424df070e813 Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Sat, 7 Jan 2023 15:53:20 +0100 Subject: removed tabs --- tagit/actions/__init__.py | 5 ----- 1 file changed, 5 deletions(-) (limited to 'tagit/actions') diff --git a/tagit/actions/__init__.py b/tagit/actions/__init__.py index 9fd6342..ecff701 100644 --- a/tagit/actions/__init__.py +++ b/tagit/actions/__init__.py @@ -20,7 +20,6 @@ from . import misc from . import planes from . import search #from . import session -#from . import tabs # exports __all__: typing.Sequence[str] = ( @@ -122,10 +121,6 @@ class ActionBuilder(BuilderBase): #'CreateTempSession': session.CreateTempSession, #'ReloadSession': session.ReloadSession, #'CloseSessionAndExit': session.CloseSessionAndExit, - ## tabs - #'AddTab': tabs.AddTab, - #'CloseTab': tabs.CloseTab, - #'SwitchTab': tabs.SwitchTab, } ## EOF ## -- cgit v1.2.3 From 37f1ac3f456b6677d9ce14274f3987ccda752f03 Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Sat, 7 Jan 2023 16:26:26 +0100 Subject: browser and misc actions --- tagit/actions/__init__.py | 60 ++--- tagit/actions/action.kv | 45 ++++ tagit/actions/action.py | 257 +++++++++++++++++++ tagit/actions/browser.kv | 99 ++++++++ tagit/actions/browser.py | 628 ++++++++++++++++++++++++++++++++++++++++++++++ tagit/actions/misc.py | 3 +- 6 files changed, 1060 insertions(+), 32 deletions(-) create mode 100644 tagit/actions/action.kv create mode 100644 tagit/actions/action.py create mode 100644 tagit/actions/browser.kv create mode 100644 tagit/actions/browser.py (limited to 'tagit/actions') diff --git a/tagit/actions/__init__.py b/tagit/actions/__init__.py index ecff701..876ca1f 100644 --- a/tagit/actions/__init__.py +++ b/tagit/actions/__init__.py @@ -11,7 +11,7 @@ import typing from tagit.utils.builder import BuilderBase # inner-module imports -#from . import browser +from . import browser from . import filter from . import grouping #from . import library @@ -32,30 +32,30 @@ __all__: typing.Sequence[str] = ( 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, + '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, @@ -84,14 +84,14 @@ class ActionBuilder(BuilderBase): #'SyncObjects': library.SyncObjects, #'ItemExport': library.ItemExport, ## misc - #'ShellDrop': misc.ShellDrop, - #'OpenExternal': misc.OpenExternal, - #'Menu': misc.Menu, + 'ShellDrop': misc.ShellDrop, + 'OpenExternal': misc.OpenExternal, + 'Menu': misc.Menu, 'ShowConsole': misc.ShowConsole, 'ShowHelp': misc.ShowHelp, 'ShowSettings': misc.ShowSettings, - #'ClipboardCopy': misc.ClipboardCopy, - #'ClipboardPaste': misc.ClipboardPaste, + 'ClipboardCopy': misc.ClipboardCopy, + 'ClipboardPaste': misc.ClipboardPaste, ## objects #'RotateLeft': objects.RotateLeft, #'RotateRight': objects.RotateRight, diff --git a/tagit/actions/action.kv b/tagit/actions/action.kv new file mode 100644 index 0000000..5352964 --- /dev/null +++ b/tagit/actions/action.kv @@ -0,0 +1,45 @@ + +: + # internas + orientation: 'horizontal' + + # responsiveness + # *touch_trigger* is enabled automatically if an image or text is shown. + # If that is undesired, *touch_trigger* has to be disabled **after** the + # declaration of *show*. + key_trigger: True + touch_trigger: False + + # size + # By default the width expands as necessary. To get a fixed width, + # set the width manually or via size_hint_x and set *autowidth* to False. + # If something is shown, *height* is automatically set to *default_height* + # unless specified otherwise in by the caller. + default_height: 30 + size: 0, 0 + size_hint: None, None + autowidth: True + + # behaviour + # The default is that no buttons are shown and touch triggers are disabled. + # NOTE: Callers need to declare show **last** to ensure that all other + # properties are set. The only exception is *touch_trigger* which has + # to be disabled **after** show. + show: [] + + # decoration + canvas.before: + Color: + rgba: 17 / 255, 32 / 255, 148 / 255, self.selected_alpha + Rectangle: + pos: self.x, self.y + 1 + size: self.size + + canvas.after: + Color: + rgba: 17 / 255, 32 / 255, 148 / 255, self.selected_alpha + Line: + width: 2 + rectangle: self.x, self.y, self.width, self.height + + ## EOF ## diff --git a/tagit/actions/action.py b/tagit/actions/action.py new file mode 100644 index 0000000..e8866ce --- /dev/null +++ b/tagit/actions/action.py @@ -0,0 +1,257 @@ +"""Button for proxy actions. + +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 + +# kivy imports +from kivy import metrics +from kivy.animation import Animation +from kivy.lang import Builder +from kivy.uix.behaviors import ButtonBehavior +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.image import Image +from kivy.uix.label import Label +import kivy.properties as kp + +# tagit imports +from tagit.external.tooltip import Tooltip + +# exports +__all__ = ('Action', ) + + +## code ## + +logger = logging.getLogger(__name__) + +# load kv +Builder.load_file(os.path.join(os.path.dirname(__file__), 'action.kv')) + +class Action(ButtonBehavior, BoxLayout, Tooltip): + """ + + An Action can be triggered in three ways: + * Touch event: Clicking or touching on the button + * Key event: Entering a keyboard shortcut + * Single-shot: Programmatically triggered once, without UI attachment + + For the last, use the *single_shot* classmethod. + + For the first two, declare the Action in a kv file or in code. + Note that the remarks below about kv do not apply if the object is + created in code. + + + When an Action is declared in kv, two restrictions apply (also see + examples below): + * To disable touch_trigger, it must be declared last + * show must be declared after all other properties + + Enable key triggers, but hide the Action in the UI: + Action: + show: [] + + Action: + # alias for the one above + + Show text, image, or both, with default height and the width + stretched as necessary: + Action: + show: 'text', + + Action: + show: 'image', + + Action: + show: 'text', 'image' + + Action: + width: 200 # has no effect unless autowidth is False + show: 'image', 'text' + + Make the Action larger: + Action: + # increased height. The image width scales accordingly + height: 80 + show: 'image', 'text' + + Action: + # scales to parent widget's width + autowidth: False + size_hint_x: 1 + show: 'image', 'text' + + Action: + # fixed width and height + width: 150 + height: 80 + autowidth: False + show: 'image', 'text' # must be declared **last** + + Show the button but disable touch events: + Action: + height: 80 + show: 'image', 'text' + touch_trigger: False # must be declared **after** show + + Do the same in code: + >>> Action( + ... size=(130, 80), + ... autowidth=False, + ... show=('image', 'text'), + ... text='foobar', + ... touch_trigger=False) + + """ + # content + tooltip = kp.StringProperty() + source = kp.StringProperty() + text = kp.StringProperty() + + # visibility flags + show = kp.ListProperty([]) + + # sizing + default_height = kp.NumericProperty(30) + autowidth = kp.BooleanProperty(True) + + # responsiveness + key_trigger = kp.BooleanProperty(True) + touch_trigger = kp.BooleanProperty(True) + + # required such that properties can be set via constructor + font_size = kp.StringProperty("15sp") + # FIXME: Check why I have to pass size instead of height/width + height = kp.NumericProperty(0) + width = kp.NumericProperty(0) + + # internal properties + root = kp.ObjectProperty(None) + selected_alpha = kp.NumericProperty(0) + _image = kp.ObjectProperty(None) + _label = kp.ObjectProperty(None) + + def __init__(self, **kwargs): + show = kwargs.pop('show', []) + touch_trigger = kwargs.pop('touch_trigger', None) + super(Action, self).__init__(**kwargs) + # delay such that on_show is executed once the other + # properties are set + self.show = show + if touch_trigger is not None: + self.touch_trigger = touch_trigger + + ## display + + def on_show(self, wx, show): + if self._image is not None: + self.remove_widget(self._image) + if self._label is not None: + self.remove_widget(self._label) + + if 'image' in show: + self.height = self.default_height if self.height == 0 else self.height + self.touch_trigger = True + self._image = Image( + source=self.source, + # size + height=self.height, + width=self.height, # square image + size_hint=(None, None), + ) + self.add_widget(self._image) + if self.autowidth: + self.width = self.height + if 'text' in show: + self.height = self.default_height if self.height == 0 else self.height + self.touch_trigger = True + self._label = Label( + # text + text=self.text, + halign='left', + valign='middle', + padding=(metrics.dp(10), 0), + # size + font_size=self.font_size, + height=self.height, + size_hint=(None, None), + ) + self._label.bind(texture_size=self.on_texture_size) + self.add_widget(self._label) + + def on_size(self, wx, size): + for child in self.children: + child.height = self.height + if isinstance(child, Image): + child.width = self.height + elif not self.autowidth: # must be the label + self.on_texture_size(child, None) + + def on_texture_size(self, label, texture_size): + if self.autowidth: + # adapt the width to the label's width + self.width = max(0, sum([child.width for child in self.children])) + else: + # adapt the label's width + others = sum([child.width for child in self.children if child != label]) + label.width = self.width - others + label.height = self.height + label.text_size = (self.width - others, self.height) + + def on_tooltip(self, callee, text): + self.set_tooltip(text) + + + ## properties + + @property + def cfg(self): + return self.root.session.cfg + + + ## action triggering + + @classmethod + def single_shot(cls, root, *args, **kwargs): + #logger.info(f'action: {cls.__name__}, {args}, {kwargs}') + root.action_log.append(str(cls.__name__)) + return cls(root=root).apply(*args, **kwargs) + + def on_root(self, wx, root): + root.keys.bind(on_press=self.on_keyboard) + + def on_press(self): + self.selected_alpha = 1 + + def on_release(self): + if self.touch_trigger: + Animation(selected_alpha=0, d=.25, t='out_quad').start(self) + #logger.info(f'action: {type(self).__name__}') + self.root.action_log.append(str(type(self).__name__)) + self.apply() + + def on_keyboard(self, wx, evt): + if self.key_trigger and self.ktrigger(evt): + #logger.info(f'action: {type(self).__name__}') + self.root.action_log.append(str(type(self).__name__)) + self.apply() + # stop the event from further processing + return True + + + ## interfaces for subtypes + + def ktrigger(self, evt): + """Return True if the action should be triggered by keyboard event *evt*.""" + return False + + def apply(self, *args, **kwargs): + """Execute the action.""" + pass + +## EOF ## diff --git a/tagit/actions/browser.kv b/tagit/actions/browser.kv new file mode 100644 index 0000000..adcfbd6 --- /dev/null +++ b/tagit/actions/browser.kv @@ -0,0 +1,99 @@ +#:import resource_find kivy.resources.resource_find + +: + source: resource_find('atlas://browser/next_page') + tooltip: 'One page down' + +: + source: resource_find('atlas://browser/previous_page') + tooltip: 'One page up' + +: + source: resource_find('atlas://browser/scroll_up') + tooltip: 'One row up' + +: + source: resource_find('atlas://browser/scroll_down') + tooltip: 'One row down' + +: + source: resource_find('atlas://browser/jump_to_page') + tooltip: 'Jump to a specified page' + +: + source: resource_find('atlas://browser/zoom_in') + tooltip: 'Zoom in' + +: + source: resource_find('atlas://browser/zoom_out') + tooltip: 'Zoom out' + +: + source: resource_find('atlas://browser/jump_to_cursor') + tooltip: 'Jump to cursor' + +: + source: resource_find('atlas://browser/set_cursor') + tooltip: 'Set the cursor' + +: + source: resource_find('atlas://browser/cursor_first') + tooltip: 'Go to first image' + +: + source: resource_find('atlas://browser/cursor_last') + tooltip: 'Go to last image' + +: + source: resource_find('atlas://browser/cursor_up') + tooltip: 'Cursor up' + +: + source: resource_find('atlas://browser/cursor_down') + tooltip: 'Cursor down' + +: + source: resource_find('atlas://browser/cursor_left') + tooltip: 'Cursor left' + +: + source: resource_find('atlas://browser/cursor_right') + tooltip: 'Cursor right' + +: + source: resource_find('atlas://browser/select_all') + tooltip: 'Select all' + +: + source: resource_find('atlas://browser/select_none') + tooltip: 'Clear selection' + +: + source: resource_find('atlas://browser/select_invert') + tooltip: 'Invert selection' + +: + source: resource_find('atlas://browser/select_single') + tooltip: 'Select one' + +: + source: resource_find('atlas://browser/select_multi') + tooltip: 'Select many' + +: + source: resource_find('atlas://browser/select_add') + tooltip: 'Add to selection' + +: + source: resource_find('atlas://browser/select_sub') + tooltip: 'Remove from selection' + +: + source: resource_find('atlas://browser/select_range') + tooltip: 'Select range' + +