diff options
Diffstat (limited to 'tagit')
182 files changed, 24161 insertions, 2 deletions
diff --git a/tagit/__init__.py b/tagit/__init__.py index 7197091..8c4285a 100644 --- a/tagit/__init__.py +++ b/tagit/__init__.py @@ -1,13 +1,32 @@ -""" +"""Tagit standalone user interfaces. + +Tagit contains several submodules: +* apps: Launchable standalone applications. +* windows: Compositions of various UI functionality. +* widgets: Individual UI components and core functions. +* dialogues: Pop-up dialogues. +* actions: Single actions that can be performed on widgets. +* tiles: Display a digestible amount of information. + +Additional core functionality is provided by some generic +modules (utils, parsing, config, etc.). Part of the tagit module. A copy of the license is provided with the project. Author: Matthias Baumgartner, 2022 """ -# imports +# standard imports import collections +import os import typing +import kivy.config +kivy.config.Config.set('input', 'mouse', 'mouse,disable_multitouch') + +# kivy imports +from kivy.resources import resource_add_path +import kivy + # constants T_VERSION_INFO = collections.namedtuple('T_VERSION_INFO', ('major', 'minor', 'micro')) version_info = T_VERSION_INFO(0, 0, 1) @@ -15,4 +34,47 @@ version_info = T_VERSION_INFO(0, 0, 1) # exports __all__: typing.Sequence[str] = [] + +## code ## + +# check kivy version +kivy.require('1.9.1') + +# add resources +resource_add_path(os.path.join(os.path.dirname(__file__), 'assets', 'icons', 'kivy')) +resource_add_path(os.path.join(os.path.dirname(__file__), 'assets', 'fonts', 'kivy')) +resource_add_path(os.path.join(os.path.dirname(__file__), 'assets', 'themes')) +resource_add_path(os.path.join(os.path.dirname(__file__), 'assets')) + +# load font +from kivy.core.text import LabelBase +LabelBase.register(name='Unifont', fn_regular='Unifont.ttf') + +# logging +# the default logger is quite verbose. Should be restricted by the app. +import logging +from . import logger +loghandler = logger.logger_config( + handler=logging.StreamHandler(), + colors=logger.ColorsTerminal, + config=dict( + level='DEBUG', + fmt='[{levelname}] [{module:<12}] {title}{message}', + title='[{title}] ', + filter=['tagit'], + ) + ) + +termlogger = logging.getLogger(__name__) +termlogger.addHandler(loghandler) +termlogger.setLevel(logging.DEBUG) + +# console logging fix: +# kivy adds an extra whitespace in front of tagit log entries. +# Adding a carriage return in front of the log fixes this bug. +# This is only needed for the console log handler, not others. +# Note that this mechanism is repeated in apps/gui to +# achieve the same for user-defined log handlers. +loghandler.formatter.prefix = '\r' + ## EOF ## diff --git a/tagit/actions/__init__.py b/tagit/actions/__init__.py new file mode 100644 index 0000000..b8b65b8 --- /dev/null +++ b/tagit/actions/__init__.py @@ -0,0 +1,105 @@ +""" + +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 misc +#from . import objects +from . import search +from . import session +from . import tagging + +# 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, + ## 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': tagging.AddTag, + 'EditTag': tagging.EditTag, + #'SetRank1': objects.SetRank1, + #'SetRank2': objects.SetRank2, + #'SetRank3': objects.SetRank3, + #'SetRank4': objects.SetRank4, + #'SetRank5': objects.SetRank5, + ## search + 'Search': search.Search, + 'ShowSelected': search.ShowSelected, + 'RemoveSelected': search.RemoveSelected, + #'SortKey': search.SortKey, + 'SortOrder': search.SortOrder, + ## session + 'LoadSession': session.LoadSession, + } + +## EOF ## diff --git a/tagit/actions/action.kv b/tagit/actions/action.kv new file mode 100644 index 0000000..807dd18 --- /dev/null +++ b/tagit/actions/action.kv @@ -0,0 +1,30 @@ + +<Action>: + # 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: [] + + ## 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 + +<NextPage>: + source: resource_find('atlas://browser/next_page') + tooltip: 'One page down' + +<PreviousPage>: + source: resource_find('atlas://browser/previous_page') + tooltip: 'One page up' + +<ScrollUp>: + source: resource_find('atlas://browser/scroll_up') + tooltip: 'One row up' + +<ScrollDown>: + source: resource_find('atlas://browser/scroll_down') + tooltip: 'One row down' + +<JumpToPage>: + source: resource_find('atlas://browser/jump_to_page') + tooltip: 'Jump to a specified page' + +<ZoomIn>: + source: resource_find('atlas://browser/zoom_in') + tooltip: 'Zoom in' + +<ZoomOut>: + source: resource_find('atlas://browser/zoom_out') + tooltip: 'Zoom out' + +<JumpToCursor>: + source: resource_find('atlas://browser/jump_to_cursor') + tooltip: 'Jump to cursor' + +<SetCursor>: + source: resource_find('atlas://browser/set_cursor') + tooltip: 'Set the cursor' + +<MoveCursorFirst>: + source: resource_find('atlas://browser/cursor_first') + tooltip: 'Go to first image' + +<MoveCursorLast>: + source: resource_find('atlas://browser/cursor_last') + tooltip: 'Go to last image' + +<MoveCursorUp>: + source: resource_find('atlas://browser/cursor_up') + tooltip: 'Cursor up' + +<MoveCursorDown>: + source: resource_find('atlas://browser/cursor_down') + tooltip: 'Cursor down' + +<MoveCursorLeft>: + source: resource_find('atlas://browser/cursor_left') + tooltip: 'Cursor left' + +<MoveCursorRight>: + source: resource_find('atlas://browser/cursor_right') + tooltip: 'Cursor right' + +<SelectAll>: + source: resource_find('atlas://browser/select_all') + tooltip: 'Select all' + +<SelectNone>: + source: resource_find('atlas://browser/select_none') + tooltip: 'Clear selection' + +<SelectInvert>: + source: resource_find('atlas://browser/select_invert') + tooltip: 'Invert selection' + +<SelectSingle>: + source: resource_find('atlas://browser/select_single') + tooltip: 'Select one' + +<SelectMulti>: + source: resource_find('atlas://browser/select_multi') + tooltip: 'Select many' + +<SelectAdditive>: + source: resource_find('atlas://browser/select_add') + tooltip: 'Add to selection' + +<SelectSubtractive>: + source: resource_find('atlas://browser/select_sub') + tooltip: 'Remove from selection' + +<SelectRange>: + source: resource_find('atlas://browser/select_range') + tooltip: 'Select range' + +<Select>: + source: resource_find('atlas://browser/select') + tooltip: 'Select/deselect an item' + +## EOF ## diff --git a/tagit/actions/browser.py b/tagit/actions/browser.py new file mode 100644 index 0000000..2eaead8 --- /dev/null +++ b/tagit/actions/browser.py @@ -0,0 +1,628 @@ +""" + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# standard imports +import math +import os + +# kivy imports +from kivy.lang import Builder +import kivy.properties as kp + +# tagit imports +from tagit import config, dialogues +from tagit.utils import clamp +from tagit.widgets import Binding + +# inner-module imports +from .action import Action + +# exports +__all__ = [] + + +## code ## + +# load kv +Builder.load_file(os.path.join(os.path.dirname(__file__), 'browser.kv')) + +# classes + +class NextPage(Action): + """Scroll one page downwards without moving the cursor.""" + text = kp.StringProperty('Next page') + + def ktrigger(self, evt): + return Binding.check(evt, self.cfg('bindings', 'browser', 'page_next')) + + def apply(self): + with self.root.browser as browser: + browser.offset = clamp(browser.offset + browser.page_size, browser.max_offset) + + +class PreviousPage(Action): + """Scroll one page upwards without moving the cursor.""" + text = kp.StringProperty('Previous page') + def ktrigger(self, evt): + return Binding.check(evt, self.cfg('bindings', 'browser', 'page_prev')) + + def apply(self): + with self.root.browser as browser: + browser.offset = max(browser.offset - browser.page_size, 0) + + +class ScrollUp(Action): + """Scroll one row up without moving the cursor.""" + text = kp.StringProperty('Scroll up') + + def ktrigger(self, evt): + return Binding.check(evt, self.cfg('bindings', 'browser', 'scroll_up')) + + def on_touch_down(self, touch): + scrollcfg = self.cfg('ui', 'standalone', 'browser', 'scroll') + scrolldir = 'scrolldown' if scrollcfg == 'mouse' else 'scrollup' + if self.root.browser.collide_point(*touch.pos) \ + and not self.root.keys.ctrl_pressed: + if touch.button == scrolldir: + self.apply() + return super(ScrollUp, self).on_touch_down(touch) + + def apply(self): + with self.root.browser as browser: + browser.offset = clamp(browser.offset - browser.cols, browser.max_offset) + + +class ScrollDown(Action): + """Scroll one row down without moving the cursor.""" + text = kp.StringProperty('Scroll down') + + def ktrigger(self, evt): + return Binding.check(evt, self.cfg('bindings', 'browser', 'scroll_down')) + + def on_touch_down(self, touch): + scrollcfg = self.cfg('ui', 'standalone', 'browser', 'scroll') + scrolldir = 'scrollup' if scrollcfg == 'mouse' else 'scrolldown' + if self.root.browser.collide_point(*touch.pos) \ + and not self.root.keys.ctrl_pressed: + if touch.button == scrolldir: + self.apply() + return super(ScrollDown, self).on_touch_down(touch) + + def apply(self): + with self.root.browser as browser: + browser.offset = clamp(browser.offset + browser.cols, browser.max_offset) + + +class JumpToPage(Action): + """Jump to a specified offset.""" + text = kp.StringProperty('Go to page') + + def apply(self, offset=None): + if offset is None: + browser = self.root.browser + dlg = dialogues.NumericInput(lo=0, hi=browser.max_offset, init_value=browser.offset) + dlg.bind(on_ok=lambda wx: self.set_offset(wx.value)) + dlg.open() + else: + self.set_offset(offset) + + def set_offset(self, offset): + with self.root.browser as browser: + browser.offset = clamp(offset, browser.max_offset) + +class ZoomIn(Action): + """Decrease the grid size.""" + text = kp.StringProperty('Zoom in') + + def ktrigger(self, evt): + return Binding.check(evt, self.cfg('bindings', 'browser', 'zoom_in')) + + # TODO: zoom by gesture + + def on_touch_down(self, touch): # not triggered (but ScrollDown is!) + if self.root.browser.collide_point(*touch.pos) \ + and self.root.keys.ctrl_pressed: + if touch.button == 'scrolldown': + self.apply() + return super(ZoomIn, self).on_touch_down(touch) + + def apply(self): + with self.root.browser as browser: + step = self.cfg('ui', 'standalone', 'browser', 'zoom_step') + if browser.gridmode == browser.GRIDMODE_LIST: + cols = browser.cols + else: + cols = max(1, browser.cols - step) + rows = max(1, browser.rows - step) + # TODO: Zoom to center? (adjust offset) + if cols != browser.cols or rows != browser.rows: + # clear widgets first, otherwise GridLayout will + # complain about too many childrens. + browser.clear_widgets() + # adjust the grid size + browser.cols = cols + browser.rows = rows + + +class ZoomOut(Action): + """Increase the grid size.""" + text = kp.StringProperty('Zoom out') + + def ktrigger(self, evt): + return Binding.check(evt, self.cfg('bindings', 'browser', 'zoom_out')) + + # TODO: zoom by gesture + + def on_touch_down(self, touch): + if self.root.browser.collide_point(*touch.pos) \ + and self.root.keys.ctrl_pressed: + if touch.button == 'scrollup': + self.apply() + return super(ZoomOut, self).on_touch_down(touch) + + def apply(self): + with self.root.browser as browser: + # TODO: Zoom from center? (adjust offset) + step = self.cfg('ui', 'standalone', 'browser', 'zoom_step') + # get maxcols + maxcols = self.cfg('ui', 'standalone', 'browser', 'maxcols') + maxcols = float('inf') if maxcols <= 0 else maxcols + # get maxrows + maxrows = self.cfg('ui', 'standalone', 'browser', 'maxrows') + maxrows = float('inf') if maxrows <= 0 else maxrows + # set cols/rows + if browser.gridmode != browser.GRIDMODE_LIST: + browser.cols = min(browser.cols + step, maxcols) + browser.rows = min(browser.rows + step, maxrows) + # adjust offset to ensure that one full page is visible + browser.offset = clamp(browser.offset, browser.max_offset) + + +class JumpToCursor(Action): + """Focus the field of view at the cursor.""" + text = kp.StringProperty('Find cursor') + + def apply(self): + with self.root.browser as browser: + if browser.cursor is None: + # cursor not set, nothing to do + pass + else: + idx = browser.items.index(browser.cursor) + if idx < browser.offset: + # cursor is above view, scroll up such that the cursor + # is in the first row. + offset = math.floor(idx / browser.cols) * browser.cols + browser.offset = clamp(offset, browser.max_offset) + elif browser.offset + browser.page_size <= idx: + # cursor is below view, scroll down such that the cursor + # is in the last row. + offset = math.floor(idx / browser.cols) * browser.cols + offset -= (browser.page_size - browser.cols) + browser.offset = clamp(offset, browser.max_offset) + else: + # cursor is visible, nothing to do + pass + + +class SetCursor(Action): + """Set the cursor to a specific item.""" + text = kp.StringProperty('Set cursor') + + def apply(self, obj): + with self.root.browser as browser: + browser.cursor = obj + self.root.trigger('JumpToCursor') + # is invoked via mouse click only, hence + # the item selection should always toggle + self.root.trigger('Select', browser.cursor) + + +class MoveCursorFirst(Action): + """Set the cursor to the first item.""" + text = kp.StringProperty('First') + + def ktrigger(self, evt): + return Binding.check(evt, self.cfg('bindings', 'browser', 'go_first')) + + def apply(self): + with self.root.browser as browser: + if browser.n_items == 0: + # browser is empty, nothing to do + pass + else: + # set cursor to first item + old = browser.cursor + browser.cursor = browser.items[0] + # scroll to first page if need be + self.root.trigger('JumpToCursor') + # fix selection + if browser.select_mode != browser.SELECT_MULTI and \ + (browser.select_mode != browser.SELECT_SINGLE or old != browser.cursor): + self.root.trigger('Select', browser.cursor) + + +class MoveCursorLast(Action): + """Set the cursor to the last item.""" + text = kp.StringProperty('Last') + + def ktrigger(self, evt): + return Binding.check(evt, self.cfg('bindings', 'browser', 'go_last')) + + def apply(self): + with self.root.browser as browser: + if browser.n_items == 0: + # browser is empty, nothing to do + pass + else: + # set cursor to last item + old = browser.cursor + browser.cursor = browser.items[-1] + # scroll to last page if need be + self.root.trigger('JumpToCursor') + # fix selection + if browser.select_mode != browser.SELECT_MULTI and \ + (browser.select_mode != browser.SELECT_SINGLE or old != browser.cursor): + self.root.trigger('Select', browser.cursor) + + +class MoveCursorUp(Action): + """Move the cursor one item upwards. Scroll if needbe.""" + text = kp.StringProperty('Cursor up') + + def ktrigger(self, evt): + return Binding.check(evt, self.cfg('bindings', 'browser', 'cursor_up')) + + def apply(self): + with self.root.browser as browser: + if browser.n_items == 0: + # browser is empty, nothing to do + pass + elif browser.cursor is None: + # cursor wasn't set before. Set to last item + self.root.trigger('MoveCursorLast') + else: + # move cursor one row up + old = browser.items.index(browser.cursor) + # check if the cursor is in the first row already + if old < browser.cols: return # first row already + # move cursor up + new = clamp(old - browser.cols, browser.n_items - 1) + browser.cursor = browser.items[new] + # fix field of view + self.root.trigger('JumpToCursor') + # fix selection + if browser.select_mode != browser.SELECT_MULTI and \ + (browser.select_mode != browser.SELECT_SINGLE or old != new): + self.root.trigger('Select', browser.cursor) + + +class MoveCursorDown(Action): + """Move the cursor one item downwards. Scroll if needbe.""" + text = kp.StringProperty('Cursor down') + + def ktrigger(self, evt): + return Binding.check(evt, self.cfg('bindings', 'browser', 'cursor_down')) + + def apply(self): + with self.root.browser as browser: + if browser.n_items == 0: + # browser is empty, nothing to do + pass + elif browser.cursor is None: + # cursor wasn't set before. Set to first item + self.root.trigger('MoveCursorFirst') + else: + # move cursor one row down + old = browser.items.index(browser.cursor) + # check if the cursor is in the last row already + last_row = browser.n_items % browser.cols + last_row = last_row if last_row > 0 else browser.cols + if old >= browser.n_items - last_row: return # last row already + # move cursor down + new = clamp(old + browser.cols, browser.n_items - 1) + browser.cursor = browser.items[new] + # fix field of view + self.root.trigger('JumpToCursor') + # fix selection + if browser.select_mode != browser.SELECT_MULTI and \ + (browser.select_mode != browser.SELECT_SINGLE or old != new): + self.root.trigger('Select', browser.cursor) + + +class MoveCursorLeft(Action): + """Move the cursor to the previous item.""" + text = kp.StringProperty('Cursor left') + + def ktrigger(self, evt): + return Binding.check(evt, self.cfg('bindings', 'browser', 'cursor_left')) + + def apply(self): + with self.root.browser as browser: + if browser.n_items == 0: + # browser is empty, nothing to do + pass + elif browser.cursor is None: + # cursor wasn't set before. Set to the last item + self.root.trigger('MoveCursorLast') + else: + # move cursor one position to the left + old = browser.items.index(browser.cursor) + new = clamp(old - 1, browser.n_items - 1) + browser.cursor = browser.items[new] + self.root.trigger('JumpToCursor') + # fix selection + if browser.select_mode != browser.SELECT_MULTI and \ + (browser.select_mode != browser.SELECT_SINGLE or old != new): + self.root.trigger('Select', browser.cursor) + + +class MoveCursorRight(Action): + """Move the cursor to the next item.""" + text = kp.StringProperty('Cursor right') + + def ktrigger(self, evt): + return Binding.check(evt, self.cfg('bindings', 'browser', 'cursor_right')) + + def apply(self): + with self.root.browser as browser: + if browser.n_items == 0: + # browser is empty, nothing to do + pass + elif browser.cursor is None: + # cursor wasn't set before. Set to the last item + self.root.trigger('MoveCursorFirst') + else: + # move cursor one position to the right + old = browser.items.index(browser.cursor) + new = clamp(old + 1, browser.n_items - 1) + browser.cursor = browser.items[new] + self.root.trigger('JumpToCursor') + # fix selection + if browser.select_mode != browser.SELECT_MULTI and \ + (browser.select_mode != browser.SELECT_SINGLE or old != new): + self.root.trigger('Select', browser.cursor) + + +class SelectAll(Action): + """Select all items.""" + text = kp.StringProperty('Select all') + + def ktrigger(self, evt): + return Binding.check(evt, self.cfg('bindings', 'browser', 'select_all')) + + def apply(self): + with self.root.browser as browser: + browser.selection = browser.items.as_set().copy() + + +class SelectNone(Action): + """Clear the selection.""" + text = kp.StringProperty('Clear selection') + + def ktrigger(self, evt): + return Binding.check(evt, self.cfg('bindings', 'browser', 'select_none')) + + def apply(self): + with self.root.browser as browser: + browser.selection = set() + + +class SelectInvert(Action): + """Invert the selection.""" + text = kp.StringProperty('Invert selection') + + def apply(self): + with self.root.browser as browser: + browser.selection = browser.items.as_set() - browser.selection + + +class SelectSingle(Action): + """Select only the cursor.""" + text = kp.StringProperty('Select one') + + def apply(self): + with self.root.browser as browser: + browser.select_mode = browser.SELECT_SINGLE + + +class SelectAdditive(Action): + """Set the selection mode to additive select.""" + text = kp.StringProperty('Always select') + + def apply(self): + with self.root.browser as browser: + browser.select_mode = browser.SELECT_ADDITIVE + + +class SelectSubtractive(Action): + """Set the selection mode to subtractive select.""" + text = kp.StringProperty('Always deselect') + + def apply(self): + with self.root.browser as browser: + browser.select_mode = browser.SELECT_SUBTRACTIVE + + +class SelectMulti(Action): + """Set the selection mode to random access.""" + text = kp.StringProperty('Select many') + browser = kp.ObjectProperty(None, allownone=True) + + def ktrigger(self, evt): + key, _, _ = evt + if key in self.root.keys.modemap[self.root.keys.MODIFIERS_CTRL]: + self.browser = self.root.browser + self.apply() + + def on_root(self, wx, root): + super(SelectMulti, self).on_root(wx, root) + root.keys.bind(on_release=self.on_key_up) + + def on_key_up(self, wx, key): + if key in self.root.keys.modemap[self.root.keys.MODIFIERS_CTRL]: + if self.browser is not None: + with self.browser as browser: + if browser.select_mode & browser.SELECT_MULTI: + browser.select_mode -= browser.SELECT_MULTI + + def apply(self): + with self.root.browser as browser: + browser.select_mode |= browser.SELECT_MULTI + + +class SelectRange(Action): + """Set the selection mode to range select.""" + text = kp.StringProperty('Select range') + browser = kp.ObjectProperty(None, allownone=True) + + def ktrigger(self, evt): + key, _, _ = evt + if key in self.root.keys.modemap[self.root.keys.MODIFIERS_SHIFT]: + self.browser = self.root.browser + self.apply() + + def on_root(self, wx, root): + super(SelectRange, self).on_root(wx, root) + root.keys.bind(on_release=self.on_key_up) + + def on_key_up(self, wx, key): + if key in self.root.keys.modemap[self.root.keys.MODIFIERS_SHIFT]: + if self.browser is not None: + with self.browser as browser: + if browser.select_mode & browser.SELECT_RANGE: + browser.select_mode -= browser.SELECT_RANGE + browser.range_base = set() + browser.range_origin = None + + def apply(self): + with self.root.browser as browser: + browser.select_mode |= browser.SELECT_RANGE + browser.range_base = browser.selection.copy() + idx = None if browser.cursor is None else browser.items.index(browser.cursor) + browser.range_origin = idx + + +class Select(Action): + """Select or deselect an item. How the selection changes depends on the selection mode.""" + text = kp.StringProperty('Select') + + def ktrigger(self, evt): + return Binding.check(evt, self.cfg('bindings', 'browser', 'select')) + + def apply(self, obj=None): + with self.root.browser as browser: + obj = obj if obj is not None else browser.cursor + + if obj is None: + # nothing to do + pass + + elif browser.select_mode & browser.SELECT_ADDITIVE: + browser.selection.add(obj) + + elif browser.select_mode & browser.SELECT_SUBTRACTIVE: + if obj in browser.selection: + browser.selection.remove(obj) + + elif browser.select_mode & browser.SELECT_RANGE: + idx = browser.items.index(obj) + lo = min(idx, browser.range_origin) + hi = max(idx, browser.range_origin) + browser.selection = browser.range_base | set(browser.items[lo:hi+1]) + + elif browser.select_mode & browser.SELECT_MULTI: + # Toggle + if obj in browser.selection: + browser.selection.remove(obj) + else: + browser.selection.add(obj) + + elif browser.select_mode == 0: #elif browser.select_mode & browser.SELECT_SINGLE: + # Toggle + if obj in browser.selection: + browser.selection = set() + else: + browser.selection = {obj} + + +## config ## + +config.declare(('ui', 'standalone', 'browser', 'maxcols'), config.Unsigned(), 0, + __name__, 'Column maximum', 'Maximal number of columns. This guards against aggressive zooming, because the application may become unresponsive if too many preview images are shown on the same page. A value of Infinity means that no limit is enforced.') + +config.declare(('ui', 'standalone', 'browser', 'maxrows'), config.Unsigned(), 0, + __name__, 'Row maximum', 'Maximal number of rows. This guards against aggressive zooming, because the application may become unresponsive if too many preview images are shown on the same page. A value of Infinity means that no limit is enforced.') + +config.declare(('ui', 'standalone', 'browser', 'zoom_step'), config.Unsigned(), 1, + __name__, 'Zoom step', 'Controls by how much the gridsize is increased or decreased when zoomed in or out. Affects both dimensions (cols/rows) in grid mode.') + +config.declare(('ui', 'standalone', 'browser', 'scroll'), config.Enum('mouse', 'touch'), 'mouse', + __name__, 'Inverted scroll', 'To scroll downwards, one can either move the fingers in an upward direction (touch) or use the scroll wheel in a downward direction (mouse).') + +# keybindings + +config.declare(('bindings', 'browser', 'page_next'), + config.Keybind(), Binding.simple(Binding.PGDN), + __name__, NextPage.text.defaultvalue, NextPage.__doc__) + +config.declare(('bindings', 'browser', 'page_prev'), + config.Keybind(), Binding.simple(Binding.PGUP), + __name__, PreviousPage.text.defaultvalue, PreviousPage.__doc__) + +config.declare(('bindings', 'browser', 'scroll_up'), + config.Keybind(), Binding.simple('k', None, Binding.mALL), + __name__, ScrollUp.text.defaultvalue, ScrollUp.__doc__) + +config.declare(('bindings', 'browser', 'scroll_down'), + config.Keybind(), Binding.simple('j', None, Binding.mALL), + __name__, ScrollDown.text.defaultvalue, ScrollDown.__doc__) + +config.declare(('bindings', 'browser', 'zoom_in'), + config.Keybind(), Binding.simple('+'), + __name__, ZoomIn.text.defaultvalue, ZoomIn.__doc__) + +config.declare(('bindings', 'browser', 'zoom_out'), + config.Keybind(), Binding.simple('-'), + __name__, ZoomOut.text.defaultvalue, ZoomOut.__doc__) + +config.declare(('bindings', 'browser', 'go_first'), + config.Keybind(), Binding.simple(Binding.HOME), + __name__, MoveCursorFirst.text.defaultvalue, MoveCursorFirst.__doc__) + +config.declare(('bindings', 'browser', 'go_last'), + config.Keybind(), Binding.simple(Binding.END), + __name__, MoveCursorLast.text.defaultvalue, MoveCursorLast.__doc__) + +config.declare(('bindings', 'browser', 'cursor_up'), + config.Keybind(), Binding.simple(Binding.UP), + __name__, MoveCursorUp.text.defaultvalue, MoveCursorUp.__doc__) + +config.declare(('bindings', 'browser', 'cursor_down'), + config.Keybind(), Binding.simple(Binding.DOWN), + __name__, MoveCursorDown.text.defaultvalue, MoveCursorDown.__doc__) + +config.declare(('bindings', 'browser', 'cursor_left'), + config.Keybind(), Binding.simple(Binding.LEFT), + __name__, MoveCursorLeft.text.defaultvalue, MoveCursorLeft.__doc__) + +config.declare(('bindings', 'browser', 'cursor_right'), + config.Keybind(), Binding.simple(Binding.RIGHT), + __name__, MoveCursorRight.text.defaultvalue, MoveCursorRight.__doc__) + +config.declare(('bindings', 'browser', 'select_all'), + config.Keybind(), Binding.simple('a', Binding.mCTRL, Binding.mREST), + __name__, SelectAll.text.defaultvalue, SelectAll.__doc__) + +config.declare(('bindings', 'browser', 'select_none'), + config.Keybind(), Binding.simple('a', (Binding.mCTRL, Binding.mSHIFT), Binding.mREST), + __name__, SelectNone.text.defaultvalue, SelectNone.__doc__) + +config.declare(('bindings', 'browser', 'select'), + config.Keybind(), Binding.simple(Binding.SPACEBAR), + __name__, Select.text.defaultvalue, Select.__doc__) + +## 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..52f5817 --- /dev/null +++ b/tagit/actions/filter.py @@ -0,0 +1,314 @@ +""" + +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, dialogues +from tagit.utils import errors, ns, Frame +from tagit.utils.bsfs import ast +from tagit.widgets 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(self.root.session.filter_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 = self.root.session.storage.all(ns.bsn.Tag).label(node=False) + 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.filter.FilterExpression): + self.add_token([token]) + + def add_from_string(self, text): + try: + self.add_token(self.root.session.filter_from_string(text)) + except errors.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 = self.root.session.storage.all(ns.bsn.Tag).label(node=False) + text = self.root.session.filter_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 = self.root.session.filter_from_string(obj.text) + except errors.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..301af20 --- /dev/null +++ b/tagit/actions/grouping.py @@ -0,0 +1,263 @@ +""" + +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 ## 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 + +<Menu>: + source: resource_find('atlas://misc/menu') + tooltip: 'Open the menu' + +<ShellDrop>: + source: resource_find('atlas://misc/shell') + tooltip: 'Open a terminal shell' + +<OpenExternal>: + source: resource_find('atlas://misc/open_external') + tooltip: 'Open selected items in an external application' + +<ShowConsole>: + source: resource_find('atlas://misc/console') + tooltip: 'Open the log console' + +<ShowHelp>: + source: resource_find('atlas://misc/help') + tooltip: 'Open the help' + +<ShowSettings>: + source: resource_find('atlas://misc/settings') + tooltip: 'Open the settings menu' + +<ClipboardCopy>: + source: resource_find('atlas://misc/clip_copy') + tooltip: 'Copy selected items to the clipboard' + +<ClipboardPaste>: + 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..f74e0f9 --- /dev/null +++ b/tagit/actions/misc.py @@ -0,0 +1,178 @@ +""" + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# standard imports +import code +import logging +import os +import sys + +# external imports +import webbrowser + +# kivy imports +from kivy.core.clipboard import Clipboard +from kivy.lang import Builder +import kivy.properties as kp + +# tagit imports +from tagit import config +from tagit.utils import fileopen +from tagit.widgets import Binding + +# inner-module imports +from .action import Action + +# constants +HELP_URL = 'https://www.bsfs.io/' + +# 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 pprint import pprint as pp + loc = globals() + loc.update(locals()) + code.interact(banner='tagit shell', local=loc) + if loc.get('abort', False): + sys.exit(1) + + + +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 <Enter> when tags are edited. + return Binding.check(evt, self.cfg('bindings', 'misc', 'open')) + + def apply(self): + return # FIXME: mb/port + 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): + return # FIXME: mb/port + 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): + return # FIXME: mb/port + 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/search.kv b/tagit/actions/search.kv new file mode 100644 index 0000000..00f6d6d --- /dev/null +++ b/tagit/actions/search.kv @@ -0,0 +1,25 @@ +#:import resource_find kivy.resources.resource_find + +<Search>: + source: resource_find('atlas://search/search') + tooltip: 'Apply the current search filter' + +<ShowSelected>: + source: resource_find('atlas://search/exclusive_filter') + tooltip: 'Show only selected items' + +<RemoveSelected>: + source: resource_find('atlas://search/exclude_filter') + tooltip: 'Exclude selected items' + +<SortKey>: + source: resource_find('atlas://search/sort_key') + tooltip: 'Specify the sort key' + +<SortOrder>: + source_up: resource_find('atlas://search/sort_order_up') + source_down: resource_find('atlas://search/sort_order_down') + source: self.source_up + tooltip: 'Sort order' + +## EOF ## diff --git a/tagit/actions/search.py b/tagit/actions/search.py new file mode 100644 index 0000000..dd6c50c --- /dev/null +++ b/tagit/actions/search.py @@ -0,0 +1,335 @@ +""" + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# standard imports +from functools import partial +import os + +# kivy imports +from kivy.cache import Cache +from kivy.lang import Builder +import kivy.properties as kp + +# tagit imports +from tagit import config, dialogues +from tagit.external.kivy_garden.contextmenu import ContextMenu +from tagit.utils import clamp, errors +from tagit.utils import ns +from tagit.utils.bsfs import ast +from tagit.widgets import Binding +from tagit.widgets.filter import FilterAwareMixin +from tagit.widgets.session import StorageAwareMixin, ConfigAwareMixin +#from tagit.ai.features.content import ContentFeature, FeatureBuilder # FIXME: mb/port +#from tagit.parsing.search import sortkeys # FIXME: mb/port + + +# inner-module imports +from .action import Action + +# exports +__all__ = [] + + +## code ## + +# load kv +Builder.load_file(os.path.join(os.path.dirname(__file__), 'search.kv')) + +# classes +class Search(Action, StorageAwareMixin, ConfigAwareMixin): + """Apply the current search filter and update the browser.""" + text = kp.StringProperty('Search') + + # internal category for the cache + _CACHE_CATEGORY = 'tagit.search' + + def ktrigger(self, evt): + return Binding.check(evt, self.cfg('bindings', 'search', 'search')) + + def on_root(self, wx, root): + Action.on_root(self, wx, root) + ConfigAwareMixin.on_root(self, wx, root) + StorageAwareMixin.on_root(self, wx, root) + + def on_config_changed(self, session, key, value): + """Update cache settings.""" + if self._CACHE_CATEGORY not in Cache._categories: + pass + elif key == ('ui', 'standalone', 'search', 'cache_items'): + value = None if value <= 0 else value + Cache._categories[self._CACHE_CATEGORY]['limit'] = value + elif key == ('ui', 'standalone', 'search', 'cache_timeout'): + value = None if value <= 0 else value + Cache._categories[self._CACHE_CATEGORY]['timeout'] = value + + def on_cfg(self, wx, cfg): + """Initialize the cache.""" + if self._CACHE_CATEGORY not in Cache._categories: + n_items = self.cfg('ui', 'standalone', 'search', 'cache_items') + n_items = None if n_items <= 0 else n_items + timeout = self.cfg('ui', 'standalone', 'search', 'cache_timeout') + timeout = None if timeout <= 0 else timeout + Cache.register(self._CACHE_CATEGORY, n_items, timeout) + + def on_storage_modified(self, sender): + # clear the whole cache + Cache.remove(self._CACHE_CATEGORY, None) + + def on_predicate_modified(self, sender, predicate, objects, diff): + Cache.remove(self._CACHE_CATEGORY, None) # clears the whole cache + self.apply() + return # FIXME: mb/port + tbd = set() + # walk through cache + for ast, sort in Cache._objects[self._CACHE_CATEGORY]: + # check ast + if ast is not None: + for token in ast: + if token.predicate() == predicate: + if predicate in ('tag', 'group') and \ + len(set(diff) & set(token.condition())) == 0: + # tag predicate but the tag in question was not changed; skip + continue + tbd.add((ast, sort)) + break # no need to search further + + # check sort + if sort is not None: + if sort.predicate() == predicate: + tbd.add((ast, sort)) + + for key in tbd: + Cache.remove(self._CACHE_CATEGORY, key) + + # re-apply searches + self.apply() + + def apply(self): + browser = self.root.browser + filter = self.root.filter + session = self.root.session + + with browser: + # get query + query, sort = filter.get_query() + # log search + # FIXME: mb/port/log + #session.log.log_search( + # 'filter', + # session.storage.lib.meta, + # (filter.t_head, filter.t_tail), + # (filter.f_head + [browser.frame], filter.f_tail), + # ) + + # apply search or fetch it from the cache + items = Cache.get(self._CACHE_CATEGORY, (query, sort), None) + if items is None: + # FIXME: mb/port: consider sort + items = list(session.storage.sorted(ns.bsn.Entity, query)) + Cache.append(self._CACHE_CATEGORY, (query, sort), items) + + # apply search order because it's cheaper to do it here rather + # than in the backend (also see uix.kivy.filter.get_query). + items = list(reversed(items[:])) if filter.sortdir else items[:] + # update browser + browser.set_items(items) + + +class ShowSelected(Action): + """Show only selected items.""" + text = kp.StringProperty('Selected only') + + def ktrigger(self, evt): + return Binding.check(evt, self.cfg('bindings', 'search', 'exclusive')) + + def apply(self): + with self.root.browser as browser: + if len(browser.selection) == 0: + # silently ignore if no images selected + pass + elif len(browser.selection) == 1 and list(browser.selection)[0] in browser.folds: + # selection is a group + self.root.trigger('OpenGroup', list(browser.selection)[0]) + else: + token = ast.filter.IsIn(browser.unfold(browser.selection)) + self.root.trigger('AddToken', token) + + +class RemoveSelected(Action): + """Exclude selected items.""" + text = kp.StringProperty('Exclude selection') + + def ktrigger(self, evt): + return Binding.check(evt, self.cfg('bindings', 'search', 'remove')) + + def apply(self): + with self.root.browser as browser: + if len(browser.selection) == 0: + # silently ignore if no images selected + pass + else: + new_cursor = browser.neighboring_unselected() + token = ast.filter.IsNotIn(browser.unfold(browser.selection)) + self.root.trigger('AddToken', token) + # fix frame + browser.cursor = new_cursor + browser.selection = {browser.cursor} if browser.cursor is not None else set() + self.root.trigger('JumpToCursor') + + +class SortKey(Action): + """Select by which property items are ordered.""" + text = kp.StringProperty('Sort by') + + def apply(self, predicate=None): + if predicate is None: + x = self.pos[0] + self.width + y = self.pos[1] + self.height + self.menu.show(x, y) + else: + self.set_sortkey(predicate) + + def on_root(self, wx, root): + super(SortKey, self).on_root(wx, root) + + # Order is essential here: + # 1. the menu has to be created + # 2. the menu has to be attached to a parent + # 3. the menu has to be populated + # The visibility has to be triggered after (2) or (3) + self.menu = ContextMenu( + bounding_box_widget = self.root, + cancel_handler_widget = self.root) + self.root.add_widget(self.menu) + self.menu._on_visible(False) + + # TODO: The whole sortkeys setup is rather brittle + # e.g. what happens if new features become available at runtime? + + # default sortkeys + return # FIXME: mb/port + + options = sortkeys.scope.library | sortkeys.typedef.anchored # FIXME: mb/port + # apply whitelist and blacklist config + options -= set(self.cfg('ui', 'standalone', 'search', 'sort_blacklist')) + whitelist = set(self.cfg('ui', 'standalone', 'search', 'sort_whitelist')) + whitelist = whitelist if len(whitelist) else options + options &= whitelist + # TODO: If there are several versions of the same feature class, keep only the most frequent + # * get feature predicates and their feature class (i.e. name) + # * if needed, get their frequencies via Features.Entities(ctrl.stor.num, fid) + # For now, all known features are used. + + # populate menu + for sortkey in sorted(options): + text = sortkey + if ContentFeature.is_feature_id(sortkey): + text = FeatureBuilder.class_from_guid(sortkey).friendly_guid(sortkey) + + self.menu.add_text_item( + text=text, + on_release=partial(self.release_wrapper, sortkey) + ) + + def release_wrapper(self, sortkey, *args): + # hide + self.menu.hide() + # trigger event + self.set_sortkey(sortkey) + + def set_sortkey(self, predicate): + return # FIXME: mb/port + with self.root.filter as filter: + try: + # TODO: What if a predicate accepts several types (e.g. num and anchored) + if predicate in sortkeys.typedef.anchored: + cursor = self.root.browser.cursor + if cursor is None: + raise errors.UserError('an image needs to be selected for similarity sort.') + # TODO: We normally want the anchored search to be sorted most similar + # to least similar (sortdir=False). We could adjust the sortdir automatically. + # Note that VFilterAction_SortOrder would *not* get notified automatically. + filter.sortkey = partial(ast.AnchoredSort, predicate, cursor.guid) + elif predicate in sortkeys.typedef.numerical: + filter.sortkey = partial(ast.NumericalSort, predicate) + elif predicate in sortkeys.typedef.alphabetical: + filter.sortkey = partial(ast.AlphabeticalSort, predicate) + else: + raise errors.UserError('invalid sort key selected') + + except Exception as e: + dialogues.Error(text=str(e)).open() + + # stick to cursor + self.root.trigger('JumpToCursor') + + +class SortOrder(Action, FilterAwareMixin): + """Switch between ascending and descending order.""" + text = kp.StringProperty('Toggle sort order') + + def on_root(self, wx, root): + Action.on_root(self, wx, root) + FilterAwareMixin.on_root(self, wx, root) + + def on_sortdir(self, wx, sortdir): + if self._image is not None: + self._image.source = self.source_down if sortdir else self.source_up + + def on_filter(self, wx, filter): + # remove old binding + if self.filter is not None: + self.filter.unbind(sortdir=self.on_sortdir) + # add new binding + self.filter = filter + if self.filter is not None: + self.filter.bind(sortdir=self.on_sortdir) + self.on_sortdir(self.filter, self.filter.sortdir) + + def __del__(self): + # remove old binding + if self.filter is not None: + self.filter.unbind(sortdir=self.on_sortdir) + self.filter = None + + def apply(self): + with self.root.filter as filter, \ + self.root.browser as browser: + filter.sortdir = not filter.sortdir + # keep the same field of view as before + browser.offset = clamp(browser.n_items - (browser.offset + browser.page_size), + browser.max_offset) + + +## config ## + +config.declare(('ui', 'standalone', 'search', 'sort_blacklist'), config.List(config.String()), [], + __name__, 'Blacklisted sortkeys', 'Sort keys that will not be shown in the sort selection. This does not affect whitelisted keys.') + +config.declare(('ui', 'standalone', 'search', 'sort_whitelist'), config.List(config.String()), [], + __name__, 'Whitelisted sortkeys', 'Sort keys that will always be shown in the sort selection. Overrules blacklisted keys.') + +config.declare(('ui', 'standalone', 'search', 'cache_items'), config.Unsigned(), 0, + __name__, 'Search cache size', 'Number of searches that are held in cache. Zero means no limit.') + +config.declare(('ui', 'standalone', 'search', 'cache_timeout'), config.Unsigned(), 0, + __name__, 'Search cache timeout', 'Number of seconds until searches are discarded from the search cache. Zero means no limit.') + +# keybindings + +config.declare(('bindings', 'search', 'search'), + config.Keybind(), Binding.simple(Binding.F5), + __name__, Search.text.defaultvalue, Search.__doc__) + +config.declare(('bindings', 'search', 'exclusive'), + config.Keybind(), Binding.simple(Binding.ENTER, Binding.mCTRL, Binding.mREST), + __name__, ShowSelected.text.defaultvalue, ShowSelected.__doc__) + +config.declare(('bindings', 'search', 'remove'), + config.Keybind(), Binding.simple(Binding.DEL, None, Binding.mALL), + __name__, RemoveSelected.text.defaultvalue, RemoveSelected.__doc__) + +## EOF ## diff --git a/tagit/actions/session.kv b/tagit/actions/session.kv new file mode 100644 index 0000000..21807b2 --- /dev/null +++ b/tagit/actions/session.kv @@ -0,0 +1,7 @@ +#:import resource_find kivy.resources.resource_find + +<LoadSession>: + source: resource_find('atlas://session/open') + tooltip: 'Load a session' + +## EOF ## diff --git a/tagit/actions/session.py b/tagit/actions/session.py new file mode 100644 index 0000000..3c5ad39 --- /dev/null +++ b/tagit/actions/session.py @@ -0,0 +1,56 @@ +""" + +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, dialogues +from tagit.config.loader import load_settings + +# inner-module imports +from .action import Action + +# exports +__all__ = [] + + +## code ## + +# load kv +Builder.load_file(os.path.join(os.path.dirname(__file__), 'session.kv')) + +# classes +class LoadSession(Action): + """Load a session from a project file.""" + text = kp.StringProperty('Load') + + def apply(self): + """Open a file load dialogue to select a session file.""" + dlg = dialogues.FilePicker(title='Select a session file to load') + dlg.bind(on_ok=self.load_from_path) + dlg.open() + + def load_from_path(self, wx): + """Load a session from *path*.""" + with self.root.session as session: + try: + if not os.path.exists(wx.path) or not os.path.isfile(wx.path): + raise FileNotFoundError(wx.path) + + # load config from path + cfg = load_settings(wx.path, verbose=self.cfg('session', 'verbose')) + session.load(cfg) + + except Exception as e: + dialogues.Error(text=f'The file cannot be loaded ({e})').open() + + +## EOF ## diff --git a/tagit/actions/tagging.kv b/tagit/actions/tagging.kv new file mode 100644 index 0000000..53ba926 --- /dev/null +++ b/tagit/actions/tagging.kv @@ -0,0 +1,11 @@ +#:import resource_find kivy.resources.resource_find + +<AddTag>: + source: resource_find('atlas://objects/add_tag') + tooltip: 'Add tags to items' + +<EditTag>: + source: resource_find('atlas://objects/edit_tag') + tooltip: 'Edit tags of items' + +## EOF ## diff --git a/tagit/actions/tagging.py b/tagit/actions/tagging.py new file mode 100644 index 0000000..ab25cb8 --- /dev/null +++ b/tagit/actions/tagging.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 +from functools import reduce, partial +import operator +import os + +# external imports +import urllib.parse + +# kivy imports +from kivy.lang import Builder +import kivy.properties as kp + +# tagit imports +from tagit import config, dialogues +from tagit.utils import ns +from tagit.utils.bsfs import Namespace +from tagit.widgets import Binding + +# inner-module imports +from .action import Action + +# constants +TAGS_SEPERATOR = ',' +TAG_PREFIX = Namespace('http://example.com/me/tag')() + +# exports +__all__ = [] + + +## code ## + +# load kv +Builder.load_file(os.path.join(os.path.dirname(__file__), 'tagging.kv')) + +# classes +class AddTag(Action): + """Add tags to the selected items.""" + text = kp.StringProperty('Add tag') + + def ktrigger(self, evt): + return Binding.check(evt, self.cfg('bindings', 'objects', 'add_tag')) + + def apply(self): + if len(self.root.browser.selection) > 0: + tags = self.root.session.storage.all(ns.bsn.Tag).label(node=True) + dlg = dialogues.SimpleInput( + suggestions=set(tags.values()), + suggestion_sep=TAGS_SEPERATOR) + dlg.bind(on_ok=partial(self.add_tags, tags)) + dlg.open() + + else: + dialogues.Error(text='You must select some images first.').open() + + def add_tags(self, tags, wx): + # user-specified labels + labels = {t.strip() for t in wx.text.split(TAGS_SEPERATOR) if len(t.strip())} + # label to tag mapping + lut = {label: tag for tag, label in tags.items()} + # get previous tags + tags = {lut[lbl] for lbl in labels if lbl in lut} + # create new tag nodes and set their label + # FIXME: deny adding tags if tag vocabulary is fixed (ontology case) + # FIXME: replace with proper tag factory + for lbl in labels: + if lbl not in lut: + tag = self.root.session.storage.node(ns.bsn.Tag, + getattr(TAG_PREFIX, urllib.parse.quote(lbl))) + tag.set(ns.bst.label, lbl) + tags.add(tag) + with self.root.browser as browser, \ + self.root.session as session: + # get objects + ents = browser.unfold(browser.selection) + # collect tags + tags = reduce(operator.add, tags, self.root.session.storage.empty(ns.bsn.Tag)) # FIXME: mb/port: pass set once supported by Nodes.set + # set tags + ents.set(ns.bse.tag, tags) + session.dispatch('on_predicate_modified', ns.bse.tag, ents, tags) + # cursor and selection might become invalid. Will be fixed in Browser. + + +class EditTag(Action): + """Edit tags of the selected items""" + text = kp.StringProperty('Edit tags') + + def ktrigger(self, evt): + return Binding.check(evt, self.cfg('bindings', 'objects', 'edit_tag')) + + def apply(self): + with self.root.browser as browser: + if len(browser.selection) > 0: + # get all known tags + all_tags = self.root.session.storage.all(ns.bsn.Tag).label(node=True) + # get selection tags + ent_tags = browser.unfold(browser.selection).tag.label(node=True) + if len(ent_tags) == 0: + text = '' + else: + sep = TAGS_SEPERATOR + ' ' + text = sep.join(sorted(set.intersection(*ent_tags.values()))) + + dlg = dialogues.SimpleInput( + text=text, + suggestions=set(all_tags.values()), + suggestion_sep=TAGS_SEPERATOR, + ) + dlg.bind(on_ok=partial(self.edit_tag, ent_tags, all_tags)) + dlg.open() + + else: + dialogues.Error(text='You must select some images first.').open() + + def edit_tag(self, original, tags, wx): + """Add or remove tags from images. + *original* and *modified* are strings, split at *TAGS_SEPERATOR*. + Tags are added and removed with respect to the difference between those two sets. + """ + # user-specified labels + labels = {t.strip() for t in wx.text.split(TAGS_SEPERATOR) if len(t.strip())} + # label differences + original_labels = {lbl for lbls in original.values() for lbl in lbls} + removed_labels = original_labels - labels + added_labels = labels - original_labels + # get tags of removed labels + removed = {tag for tag, lbl in tags.items() if lbl in removed_labels} + removed = reduce(operator.add, removed, self.root.session.storage.empty(ns.bsn.Tag)) + # get tags of added labels + # FIXME: deny adding tags if tag vocabulary is fixed (ontology case) + lut = {label: tag for tag, label in tags.items()} + added = {lut[lbl] for lbl in added_labels if lbl in lut} + # FIXME: replace with proper tag factory + for lbl in added_labels: + if lbl not in lut: + tag = self.root.session.storage.node(ns.bsn.Tag, + getattr(TAG_PREFIX, urllib.parse.quote(lbl))) + tag.set(ns.bst.label, lbl) + added.add(tag) + added = reduce(operator.add, added, self.root.session.storage.empty(ns.bsn.Tag)) + # apply differences + with self.root.browser as browser, \ + self.root.session as session: + ents = browser.unfold(browser.selection) + ents.set(ns.bse.tag, added) + #ents.remove(ns.bse.tag, removed) # FIXME: mb/port + session.dispatch('on_predicate_modified', ns.bse.tag, ents, added | removed) + # cursor and selection might become invalid. Will be fixed in Browser. + +## config ## + +# keybindings + +config.declare(('bindings', 'objects', 'add_tag'), + config.Keybind(), Binding.simple('t', Binding.mCTRL, Binding.mSHIFT), + __name__, AddTag.text.defaultvalue, AddTag.__doc__) + +config.declare(('bindings', 'objects', 'edit_tag'), + config.Keybind(), Binding.simple('e', Binding.mCTRL), + __name__, EditTag.text.defaultvalue, EditTag.__doc__) + +## EOF ## diff --git a/tagit/apps/__init__.py b/tagit/apps/__init__.py new file mode 100644 index 0000000..84d0bf1 --- /dev/null +++ b/tagit/apps/__init__.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +"""The tagit applications. + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# standard imports +import argparse +import typing + +# tagit imports +import tagit + +# inner-module imports +from .desktop import main as desktop + +# exports +__all__: typing.Sequence[str] = ( + 'desktop', + 'main', + ) + +# config +apps = { + 'desktop' : desktop, + } + + +## code ## + +def main(argv=None): + """The BSFS browser, focused on image tagging.""" + parser = argparse.ArgumentParser(description=main.__doc__, prog='tagit') + # version + parser.add_argument('--version', action='version', + version='%(prog)s {}.{}.{}'.format(*tuple(tagit.version_info))) # pylint: disable=C0209 + # application selection + parser.add_argument('app', choices=apps.keys(), nargs='?', default='desktop', + help='Select the application to run.') + # dangling args + parser.add_argument('rest', nargs=argparse.REMAINDER) + # parse + args = parser.parse_args() + # run application + apps[args.app](args.rest) + +if __name__ == '__main__': + import sys + main(sys.argv[1:]) + +## EOF ## diff --git a/tagit/apps/desktop.py b/tagit/apps/desktop.py new file mode 100644 index 0000000..149bf30 --- /dev/null +++ b/tagit/apps/desktop.py @@ -0,0 +1,98 @@ +""" + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# standard imports +import os +import typing + +# kivy imports +from kivy.app import App +from kivy.resources import resource_find +from kivy.uix.settings import SettingsWithSidebar + +# tagit imports +from tagit import config +from tagit.utils import bsfs, ns +from tagit.utils.bsfs import URI +from tagit.windows import desktop + +# exports +__all__: typing.Sequence[str] = ( + 'main', + ) + + +## code ## + +def load_data_hook(cfg, store): + """Data loading hook to circumvent non-persistent storage.""" + import pickle + import os + # fetch data_hook config flags + schema_path = os.path.expanduser(cfg('session', 'data_hook', 'schema')) + triples_path = os.path.expanduser(cfg('session', 'data_hook', 'triples')) + # load data if present + if os.path.exists(schema_path) and os.path.exists(triples_path): + with open(schema_path, 'rb') as ifile: + store._backend._schema = pickle.load(ifile) + with open(triples_path, 'rb') as ifile: + for triple in pickle.load(ifile): + store._backend._graph.add(triple) + return store + +config.declare(('session', 'data_hook', 'schema'), config.String(), '') +config.declare(('session', 'data_hook', 'triples'), config.String(), '') + + +class TagitApp(App): + """The tagit main application.""" + + def build(self): + # set settings panel style + self.settings_cls = SettingsWithSidebar + + # set title + self.title = 'tagit v0.23.03' + + # load config + from tagit.config.loader import load_settings, TAGITRC + cfg = load_settings(TAGITRC, 0) + + # open BSFS storage + store = load_data_hook(cfg, bsfs.Open(cfg('session', 'bsfs'))) # FIXME: mb/port: data hook + # check storage schema + with open(resource_find('required_schema.nt'), 'rt') as ifile: + required_schema = bsfs.schema.from_string(ifile.read()) + 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) + + # create widget + return desktop.MainWindow(cfg, store, None) # FIXME: mb/port: expects log arguments + + def on_start(self): + # trigger startup operations + self.root.on_startup() + + +def main(argv): + """Start the tagit GUI. Opens a window to browse images.""" + # Run the GUI + app = TagitApp() + app.run() + +## config + +config.declare(('session', 'bsfs'), config.Any(), {}, + __name__, 'bsfs config', 'Configuration to connect to a BSFS storage.') + +## main ## + +if __name__ == '__main__': + main() + +## EOF ## diff --git a/tagit/assets/fonts/kivy/Unifont.ttf b/tagit/assets/fonts/kivy/Unifont.ttf Binary files differnew file mode 100644 index 0000000..ec875c5 --- /dev/null +++ b/tagit/assets/fonts/kivy/Unifont.ttf diff --git a/tagit/assets/icons/kivy/make.sh b/tagit/assets/icons/kivy/make.sh new file mode 100755 index 0000000..41a2741 --- /dev/null +++ b/tagit/assets/icons/kivy/make.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +# convert svg to png +for prefix in `find ../scalable/* -type d`; do + # make directory + dirname=$(basename "${prefix}") + mkdir -p "${dirname}" + + # convert svg to png + for path in `find "${prefix}" -type f -iname "*.svg"`; do + # keep the file name but change the extension to png + filename=$(basename "${path}") + filename="${filename/\.svg/.png}" + inkscape -z -w 30 -h 30 "${path}" -o "${dirname}/${filename}" + done +done + +# create atlas through kivy +for folder in `ls -d */`; do + # get the atlas width from the file count + count=$(ls "${folder}"/*.png | wc -l) + width=$(expr 32 \* "${count}") + name=$(basename "${folder}") + # create the atlas + python -m kivy.atlas "${name}" "${width}x32" "${folder}"*.png + +done diff --git a/tagit/assets/icons/kivy/no_preview.png b/tagit/assets/icons/kivy/no_preview.png Binary files differnew file mode 100644 index 0000000..3049768 --- /dev/null +++ b/tagit/assets/icons/kivy/no_preview.png diff --git a/tagit/assets/icons/scalable/README b/tagit/assets/icons/scalable/README new file mode 100644 index 0000000..7c17504 --- /dev/null +++ b/tagit/assets/icons/scalable/README @@ -0,0 +1,5 @@ + +internal.svg is derived work from: + https://commons.wikimedia.org/wiki/File:Jolly-Roger.svg + by RootOfAllLight + diff --git a/tagit/assets/icons/scalable/browser/cursor_down.svg b/tagit/assets/icons/scalable/browser/cursor_down.svg new file mode 100644 index 0000000..b75b288 --- /dev/null +++ b/tagit/assets/icons/scalable/browser/cursor_down.svg @@ -0,0 +1,164 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="100mm" + height="100mm" + id="svg2" + version="1.1" + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="cursor_down.svg" + inkscape:export-filename="../../kivy/browser/cursor_down.png" + inkscape:export-xdpi="7.6199999" + inkscape:export-ydpi="7.6199999"> + <defs + id="defs4" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="1.979899" + inkscape:cx="183.64532" + inkscape:cy="182.16579" + inkscape:document-units="mm" + inkscape:current-layer="layer1" + showgrid="false" + showguides="true" + inkscape:guide-bbox="true" + inkscape:snap-global="true" + inkscape:snap-bbox="true" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:window-width="1920" + inkscape:window-height="1031" + inkscape:window-x="0" + inkscape:window-y="25" + inkscape:window-maximized="1" + inkscape:pagecheckerboard="true" + units="mm" + inkscape:bbox-paths="true" + inkscape:bbox-nodes="true" + inkscape:snap-bbox-edge-midpoints="true" + inkscape:snap-bbox-midpoints="true" + inkscape:object-paths="true" + inkscape:snap-intersection-paths="true" + inkscape:snap-smooth-nodes="true" + inkscape:snap-midpoints="true" + inkscape:snap-object-midpoints="true" + inkscape:snap-center="true" + inkscape:snap-text-baseline="true" + inkscape:snap-page="true" + showborder="true"> + <sodipodi:guide + orientation="0,1" + position="13.637059,643.40404" + id="guide3788" + inkscape:locked="false" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="0,1" + id="guide1099" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="1,0" + id="guide1101" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="233.588,370" + orientation="1,0" + id="guide1107" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="144.36496,311.42857" + orientation="1,0" + id="guide1109" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="-77.142857,144.36496" + orientation="0,1" + id="guide1111" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="5.000315,233.58779" + orientation="0,1" + id="guide1113" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="-305.71429,497.85714" + orientation="0,1" + id="guide822" + inkscape:locked="false" /> + </sodipodi:namedview> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-167.28122,-322.85977)"> + <g + id="g845" + transform="matrix(0,1,1,0,-155.57855,155.57855)"> + <path + inkscape:transform-center-y="3.8269577e-06" + transform="matrix(0.77742888,0,0,0.88132688,181.59692,74.405789)" + inkscape:transform-center-x="-71.079211" + d="m 467.74318,496.33157 -150.23098,86.73589 -150.23097,86.7359 0,-173.47179 0,-173.47179 150.23097,86.73591 z" + inkscape:randomized="0" + inkscape:rounded="0" + inkscape:flatsided="false" + sodipodi:arg2="1.0471976" + sodipodi:arg1="0" + sodipodi:r2="100.15398" + sodipodi:r1="200.30797" + sodipodi:cy="496.33157" + sodipodi:cx="267.43521" + sodipodi:sides="3" + id="path2995" + style="fill:#c8c8c8;fill-opacity:1;stroke:none;stroke-width:2.19086838;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + sodipodi:type="star" /> + <rect + y="496.479" + x="167.28122" + height="30.714285" + width="144.36497" + id="rect820" + style="opacity:1;fill:#c8c8c8;fill-opacity:1;stroke:none;stroke-width:39.62062454;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" /> + </g> + </g> +</svg> diff --git a/tagit/assets/icons/scalable/browser/cursor_first.svg b/tagit/assets/icons/scalable/browser/cursor_first.svg new file mode 100644 index 0000000..57d878e --- /dev/null +++ b/tagit/assets/icons/scalable/browser/cursor_first.svg @@ -0,0 +1,155 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="100mm" + height="100mm" + id="svg2" + version="1.1" + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="cursor_first.svg" + inkscape:export-filename="../../kivy/browser/cursor_first.png" + inkscape:export-xdpi="7.6199999" + inkscape:export-ydpi="7.6199999"> + <defs + id="defs4" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="1.4" + inkscape:cx="104.09835" + inkscape:cy="254.99343" + inkscape:document-units="mm" + inkscape:current-layer="layer1" + showgrid="false" + showguides="true" + inkscape:guide-bbox="true" + inkscape:snap-global="true" + inkscape:snap-bbox="true" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:window-width="1920" + inkscape:window-height="1031" + inkscape:window-x="0" + inkscape:window-y="25" + inkscape:window-maximized="1" + inkscape:pagecheckerboard="true" + units="mm" + inkscape:bbox-paths="true" + inkscape:bbox-nodes="true" + inkscape:snap-bbox-edge-midpoints="true" + inkscape:snap-bbox-midpoints="true" + inkscape:object-paths="true" + inkscape:snap-intersection-paths="true" + inkscape:snap-smooth-nodes="true" + inkscape:snap-midpoints="true" + inkscape:snap-object-midpoints="true" + inkscape:snap-center="true" + inkscape:snap-text-baseline="true" + inkscape:snap-page="true"> + <sodipodi:guide + orientation="0,1" + position="13.637059,643.40404" + id="guide3788" + inkscape:locked="false" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="0,1" + id="guide1099" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="1,0" + id="guide1101" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="233.588,370" + orientation="1,0" + id="guide1107" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="144.36496,311.42857" + orientation="1,0" + id="guide1109" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="-77.142857,144.36496" + orientation="0,1" + id="guide1111" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="5.000315,233.58779" + orientation="0,1" + id="guide1113" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + </sodipodi:namedview> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title /> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-167.28122,-322.85977)"> + <path + sodipodi:type="star" + style="fill:#c8c8c8;fill-opacity:1;stroke:none;stroke-width:2.19086838;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="path2995" + sodipodi:sides="3" + sodipodi:cx="267.43521" + sodipodi:cy="496.33157" + sodipodi:r1="200.30797" + sodipodi:r2="100.15398" + sodipodi:arg1="0" + sodipodi:arg2="1.0471976" + inkscape:flatsided="false" + inkscape:rounded="0" + inkscape:randomized="0" + d="m 467.74318,496.33157 -150.23098,86.73589 -150.23097,86.7359 0,-173.47179 0,-173.47179 150.23097,86.73591 z" + inkscape:transform-center-x="-87.858587" + transform="matrix(-0.96095331,0,0,-1.0893781,705.98338,1052.5288)" + inkscape:transform-center-y="1.8482077e-06" /> + <rect + style="fill:#c8c8c8;fill-opacity:1;stroke:none;stroke-width:2.62828088;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect3798" + width="57.780041" + height="377.95276" + x="-225.06128" + y="-700.8125" + transform="scale(-1)" /> + </g> +</svg> diff --git a/tagit/assets/icons/scalable/browser/cursor_last.svg b/tagit/assets/icons/scalable/browser/cursor_last.svg new file mode 100644 index 0000000..ffe7449 --- /dev/null +++ b/tagit/assets/icons/scalable/browser/cursor_last.svg @@ -0,0 +1,154 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="100mm" + height="100mm" + id="svg2" + version="1.1" + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="cursor_last.svg" + inkscape:export-filename="../../kivy/browser/cursor_last.png" + inkscape:export-xdpi="7.6199999" + inkscape:export-ydpi="7.6199999"> + <defs + id="defs4" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="1.4" + inkscape:cx="104.09835" + inkscape:cy="254.99343" + inkscape:document-units="mm" + inkscape:current-layer="layer1" + showgrid="false" + showguides="true" + inkscape:guide-bbox="true" + inkscape:snap-global="true" + inkscape:snap-bbox="true" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:window-width="1920" + inkscape:window-height="1031" + inkscape:window-x="0" + inkscape:window-y="25" + inkscape:window-maximized="1" + inkscape:pagecheckerboard="true" + units="mm" + inkscape:bbox-paths="true" + inkscape:bbox-nodes="true" + inkscape:snap-bbox-edge-midpoints="true" + inkscape:snap-bbox-midpoints="true" + inkscape:object-paths="true" + inkscape:snap-intersection-paths="true" + inkscape:snap-smooth-nodes="true" + inkscape:snap-midpoints="true" + inkscape:snap-object-midpoints="true" + inkscape:snap-center="true" + inkscape:snap-text-baseline="true" + inkscape:snap-page="true"> + <sodipodi:guide + orientation="0,1" + position="13.637059,643.40404" + id="guide3788" + inkscape:locked="false" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="0,1" + id="guide1099" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="1,0" + id="guide1101" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="233.588,370" + orientation="1,0" + id="guide1107" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="144.36496,311.42857" + orientation="1,0" + id="guide1109" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="-77.142857,144.36496" + orientation="0,1" + id="guide1111" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="5.000315,233.58779" + orientation="0,1" + id="guide1113" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + </sodipodi:namedview> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title /> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-167.28122,-322.85977)"> + <path + sodipodi:type="star" + style="fill:#c8c8c8;fill-opacity:1;stroke:none;stroke-width:2.19086838;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="path2995" + sodipodi:sides="3" + sodipodi:cx="267.43521" + sodipodi:cy="496.33157" + sodipodi:r1="200.30797" + sodipodi:r2="100.15398" + sodipodi:arg1="0" + sodipodi:arg2="1.0471976" + inkscape:flatsided="false" + inkscape:rounded="0" + inkscape:randomized="0" + d="m 467.74318,496.33157 -150.23098,86.73589 -150.23097,86.7359 0,-173.47179 0,-173.47179 150.23097,86.73591 z" + inkscape:transform-center-x="-87.858587" + transform="matrix(0.96095331,0,0,1.0893781,6.5317797,-28.856589)" + inkscape:transform-center-y="1.8482077e-06" /> + <rect + style="fill:#c8c8c8;fill-opacity:1;stroke:none;stroke-width:2.62828088;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect3798" + width="57.780041" + height="377.95276" + x="487.45389" + y="322.85974" /> + </g> +</svg> diff --git a/tagit/assets/icons/scalable/browser/cursor_left.svg b/tagit/assets/icons/scalable/browser/cursor_left.svg new file mode 100644 index 0000000..d1877f8 --- /dev/null +++ b/tagit/assets/icons/scalable/browser/cursor_left.svg @@ -0,0 +1,159 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="100mm" + height="100mm" + id="svg2" + version="1.1" + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="cursor_left.svg" + inkscape:export-filename="../../kivy/browser/scroll_down.png" + inkscape:export-xdpi="7.6199999" + inkscape:export-ydpi="7.6199999"> + <defs + id="defs4" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="1.979899" + inkscape:cx="183.64532" + inkscape:cy="262.978" + inkscape:document-units="mm" + inkscape:current-layer="layer1" + showgrid="false" + showguides="true" + inkscape:guide-bbox="true" + inkscape:snap-global="true" + inkscape:snap-bbox="true" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:window-width="1920" + inkscape:window-height="1031" + inkscape:window-x="0" + inkscape:window-y="25" + inkscape:window-maximized="1" + inkscape:pagecheckerboard="true" + units="mm" + inkscape:bbox-paths="true" + inkscape:bbox-nodes="true" + inkscape:snap-bbox-edge-midpoints="true" + inkscape:snap-bbox-midpoints="true" + inkscape:object-paths="true" + inkscape:snap-intersection-paths="true" + inkscape:snap-smooth-nodes="true" + inkscape:snap-midpoints="true" + inkscape:snap-object-midpoints="true" + inkscape:snap-center="true" + inkscape:snap-text-baseline="true" + inkscape:snap-page="true"> + <sodipodi:guide + orientation="0,1" + position="13.637059,643.40404" + id="guide3788" + inkscape:locked="false" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="0,1" + id="guide1099" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="1,0" + id="guide1101" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="233.588,370" + orientation="1,0" + id="guide1107" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="144.36496,311.42857" + orientation="1,0" + id="guide1109" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="-77.142857,144.36496" + orientation="0,1" + id="guide1111" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="5.000315,233.58779" + orientation="0,1" + id="guide1113" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="-305.71429,497.85714" + orientation="0,1" + id="guide822" + inkscape:locked="false" /> + </sodipodi:namedview> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-167.28122,-322.85977)"> + <path + sodipodi:type="star" + style="fill:#c8c8c8;fill-opacity:1;stroke:none;stroke-width:2.19086838;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="path2995" + sodipodi:sides="3" + sodipodi:cx="267.43521" + sodipodi:cy="496.33157" + sodipodi:r1="200.30797" + sodipodi:r2="100.15398" + sodipodi:arg1="0" + sodipodi:arg2="1.0471976" + inkscape:flatsided="false" + inkscape:rounded="0" + inkscape:randomized="0" + d="m 467.74318,496.33157 -150.23098,86.73589 -150.23097,86.7359 0,-173.47179 0,-173.47179 150.23097,86.73591 z" + inkscape:transform-center-x="-71.079211" + transform="matrix(0.77742888,0,0,0.88132688,181.59692,74.405789)" + inkscape:transform-center-y="3.8269577e-06" /> + <rect + style="opacity:1;fill:#c8c8c8;fill-opacity:1;stroke:none;stroke-width:39.62062454;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" + id="rect820" + width="144.36497" + height="30.714285" + x="167.28122" + y="496.479" /> + </g> +</svg> diff --git a/tagit/assets/icons/scalable/browser/cursor_right.svg b/tagit/assets/icons/scalable/browser/cursor_right.svg new file mode 100644 index 0000000..bfe235d --- /dev/null +++ b/tagit/assets/icons/scalable/browser/cursor_right.svg @@ -0,0 +1,163 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="100mm" + height="100mm" + id="svg2" + version="1.1" + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="cursor_right.svg" + inkscape:export-filename="../../kivy/browser/cursor_right.png" + inkscape:export-xdpi="7.6199999" + inkscape:export-ydpi="7.6199999"> + <defs + id="defs4" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="1.979899" + inkscape:cx="183.64532" + inkscape:cy="262.978" + inkscape:document-units="mm" + inkscape:current-layer="layer1" + showgrid="false" + showguides="true" + inkscape:guide-bbox="true" + inkscape:snap-global="true" + inkscape:snap-bbox="true" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:window-width="1920" + inkscape:window-height="1031" + inkscape:window-x="0" + inkscape:window-y="25" + inkscape:window-maximized="1" + inkscape:pagecheckerboard="true" + units="mm" + inkscape:bbox-paths="true" + inkscape:bbox-nodes="true" + inkscape:snap-bbox-edge-midpoints="true" + inkscape:snap-bbox-midpoints="true" + inkscape:object-paths="true" + inkscape:snap-intersection-paths="true" + inkscape:snap-smooth-nodes="true" + inkscape:snap-midpoints="true" + inkscape:snap-object-midpoints="true" + inkscape:snap-center="true" + inkscape:snap-text-baseline="true" + inkscape:snap-page="true"> + <sodipodi:guide + orientation="0,1" + position="13.637059,643.40404" + id="guide3788" + inkscape:locked="false" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="0,1" + id="guide1099" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="1,0" + id="guide1101" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="233.588,370" + orientation="1,0" + id="guide1107" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="144.36496,311.42857" + orientation="1,0" + id="guide1109" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="-77.142857,144.36496" + orientation="0,1" + id="guide1111" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="5.000315,233.58779" + orientation="0,1" + id="guide1113" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="-305.71429,497.85714" + orientation="0,1" + id="guide822" + inkscape:locked="false" /> + </sodipodi:namedview> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-167.28122,-322.85977)"> + <g + id="g845" + transform="rotate(180,356.2576,511.83615)"> + <path + inkscape:transform-center-y="3.8269577e-06" + transform="matrix(0.77742888,0,0,0.88132688,181.59692,74.405789)" + inkscape:transform-center-x="-71.079211" + d="m 467.74318,496.33157 -150.23098,86.73589 -150.23097,86.7359 0,-173.47179 0,-173.47179 150.23097,86.73591 z" + inkscape:randomized="0" + inkscape:rounded="0" + inkscape:flatsided="false" + sodipodi:arg2="1.0471976" + sodipodi:arg1="0" + sodipodi:r2="100.15398" + sodipodi:r1="200.30797" + sodipodi:cy="496.33157" + sodipodi:cx="267.43521" + sodipodi:sides="3" + id="path2995" + style="fill:#c8c8c8;fill-opacity:1;stroke:none;stroke-width:2.19086838;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + sodipodi:type="star" /> + <rect + y="496.479" + x="167.28122" + height="30.714285" + width="144.36497" + id="rect820" + style="opacity:1;fill:#c8c8c8;fill-opacity:1;stroke:none;stroke-width:39.62062454;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" /> + </g> + </g> +</svg> diff --git a/tagit/assets/icons/scalable/browser/cursor_up.svg b/tagit/assets/icons/scalable/browser/cursor_up.svg new file mode 100644 index 0000000..907c16c --- /dev/null +++ b/tagit/assets/icons/scalable/browser/cursor_up.svg @@ -0,0 +1,163 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="100mm" + height="100mm" + id="svg2" + version="1.1" + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="cursor_up.svg" + inkscape:export-filename="../../kivy/browser/cursor_up.png" + inkscape:export-xdpi="7.6199999" + inkscape:export-ydpi="7.6199999"> + <defs + id="defs4" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="1.979899" + inkscape:cx="183.64532" + inkscape:cy="222.57189" + inkscape:document-units="mm" + inkscape:current-layer="layer1" + showgrid="false" + showguides="true" + inkscape:guide-bbox="true" + inkscape:snap-global="true" + inkscape:snap-bbox="true" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:window-width="1920" + inkscape:window-height="1031" + inkscape:window-x="0" + inkscape:window-y="25" + inkscape:window-maximized="1" + inkscape:pagecheckerboard="true" + units="mm" + inkscape:bbox-paths="true" + inkscape:bbox-nodes="true" + inkscape:snap-bbox-edge-midpoints="true" + inkscape:snap-bbox-midpoints="true" + inkscape:object-paths="true" + inkscape:snap-intersection-paths="true" + inkscape:snap-smooth-nodes="true" + inkscape:snap-midpoints="true" + inkscape:snap-object-midpoints="true" + inkscape:snap-center="true" + inkscape:snap-text-baseline="true" + inkscape:snap-page="true"> + <sodipodi:guide + orientation="0,1" + position="13.637059,643.40404" + id="guide3788" + inkscape:locked="false" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="0,1" + id="guide1099" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="1,0" + id="guide1101" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="233.588,370" + orientation="1,0" + id="guide1107" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="144.36496,311.42857" + orientation="1,0" + id="guide1109" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="-77.142857,144.36496" + orientation="0,1" + id="guide1111" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="5.000315,233.58779" + orientation="0,1" + id="guide1113" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="-305.71429,497.85714" + orientation="0,1" + id="guide822" + inkscape:locked="false" /> + </sodipodi:namedview> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-167.28122,-322.85977)"> + <g + id="g845" + transform="rotate(-90,356.2576,511.83615)"> + <path + inkscape:transform-center-y="3.8269577e-06" + transform="matrix(0.77742888,0,0,0.88132688,181.59692,74.405789)" + inkscape:transform-center-x="-71.079211" + d="m 467.74318,496.33157 -150.23098,86.73589 -150.23097,86.7359 0,-173.47179 0,-173.47179 150.23097,86.73591 z" + inkscape:randomized="0" + inkscape:rounded="0" + inkscape:flatsided="false" + sodipodi:arg2="1.0471976" + sodipodi:arg1="0" + sodipodi:r2="100.15398" + sodipodi:r1="200.30797" + sodipodi:cy="496.33157" + sodipodi:cx="267.43521" + sodipodi:sides="3" + id="path2995" + style="fill:#c8c8c8;fill-opacity:1;stroke:none;stroke-width:2.19086838;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + sodipodi:type="star" /> + <rect + y="496.479" + x="167.28122" + height="30.714285" + width="144.36497" + id="rect820" + style="opacity:1;fill:#c8c8c8;fill-opacity:1;stroke:none;stroke-width:39.62062454;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" /> + </g> + </g> +</svg> diff --git a/tagit/assets/icons/scalable/browser/next_page.svg b/tagit/assets/icons/scalable/browser/next_page.svg new file mode 100644 index 0000000..77f4488 --- /dev/null +++ b/tagit/assets/icons/scalable/browser/next_page.svg @@ -0,0 +1,164 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="100mm" + height="100mm" + id="svg2" + version="1.1" + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="next_page.svg" + inkscape:export-filename="../../kivy/browser/template.png" + inkscape:export-xdpi="7.6199999" + inkscape:export-ydpi="7.6199999"> + <defs + id="defs4" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="0.98994949" + inkscape:cx="-105.2055" + inkscape:cy="381.61002" + inkscape:document-units="mm" + inkscape:current-layer="layer1" + showgrid="false" + showguides="true" + inkscape:guide-bbox="true" + inkscape:snap-global="true" + inkscape:snap-bbox="true" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:window-width="1920" + inkscape:window-height="1031" + inkscape:window-x="0" + inkscape:window-y="25" + inkscape:window-maximized="1" + inkscape:pagecheckerboard="true" + units="mm" + inkscape:bbox-paths="true" + inkscape:bbox-nodes="true" + inkscape:snap-bbox-edge-midpoints="true" + inkscape:snap-bbox-midpoints="true" + inkscape:object-paths="true" + inkscape:snap-intersection-paths="true" + inkscape:snap-smooth-nodes="true" + inkscape:snap-midpoints="true" + inkscape:snap-object-midpoints="true" + inkscape:snap-center="true" + inkscape:snap-text-baseline="true" + inkscape:snap-page="true" + inkscape:lockguides="false"> + <sodipodi:guide + orientation="0,1" + position="13.637059,643.40404" + id="guide3788" + inkscape:locked="false" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="0,1" + id="guide1099" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="1,0" + id="guide1101" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="233.588,370" + orientation="1,0" + id="guide1107" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="144.36496,311.42857" + orientation="1,0" + id="guide1109" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="-77.142857,144.36496" + orientation="0,1" + id="guide1111" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="5.000315,233.58779" + orientation="0,1" + id="guide1113" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + </sodipodi:namedview> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-167.28122,-322.85977)"> + <path + sodipodi:type="star" + style="fill:#c8c8c8;fill-opacity:1;stroke:none;stroke-width:4;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="path2995" + sodipodi:sides="3" + sodipodi:cx="-334.38327" + sodipodi:cy="361.73682" + sodipodi:r1="365.71429" + sodipodi:r2="182.85715" + sodipodi:arg1="0" + sodipodi:arg2="1.0471976" + inkscape:flatsided="false" + inkscape:rounded="0" + inkscape:randomized="0" + d="m 31.331024,361.73682 -274.285724,158.35894 -274.28572,158.35893 0,-316.71788 0,-316.717863 274.28573,158.358943 z" + inkscape:transform-center-x="-91.42857" + transform="matrix(0.34523871,0,0,0.5917254,345.85263,296.22092)" /> + <path + sodipodi:type="star" + style="fill:#c8c8c8;fill-opacity:1;stroke:none;stroke-width:4;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="path2995-8" + sodipodi:sides="3" + sodipodi:cx="211.80305" + sodipodi:cy="367.03107" + sodipodi:r1="365.71429" + sodipodi:r2="182.85715" + sodipodi:arg1="0" + sodipodi:arg2="1.0471976" + inkscape:flatsided="false" + inkscape:rounded="0" + inkscape:randomized="0" + d="M 577.51735,367.03107 303.23162,525.39001 28.945908,683.74894 l 0,-316.71788 0,-316.717863 274.285732,158.358943 z" + inkscape:transform-center-x="-91.42857" + transform="matrix(0.34523871,0,0,0.5917254,345.85263,296.22092)" /> + </g> +</svg> diff --git a/tagit/assets/icons/scalable/browser/previous_page.svg b/tagit/assets/icons/scalable/browser/previous_page.svg new file mode 100644 index 0000000..82eaa21 --- /dev/null +++ b/tagit/assets/icons/scalable/browser/previous_page.svg @@ -0,0 +1,164 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="100mm" + height="100mm" + id="svg2" + version="1.1" + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="previous_page.svg" + inkscape:export-filename="../../kivy/browser/previous_page.png" + inkscape:export-xdpi="7.6199999" + inkscape:export-ydpi="7.6199999"> + <defs + id="defs4" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="0.98994949" + inkscape:cx="-105.2055" + inkscape:cy="381.61002" + inkscape:document-units="mm" + inkscape:current-layer="layer1" + showgrid="false" + showguides="true" + inkscape:guide-bbox="true" + inkscape:snap-global="true" + inkscape:snap-bbox="true" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:window-width="1920" + inkscape:window-height="1031" + inkscape:window-x="0" + inkscape:window-y="25" + inkscape:window-maximized="1" + inkscape:pagecheckerboard="true" + units="mm" + inkscape:bbox-paths="true" + inkscape:bbox-nodes="true" + inkscape:snap-bbox-edge-midpoints="true" + inkscape:snap-bbox-midpoints="true" + inkscape:object-paths="true" + inkscape:snap-intersection-paths="true" + inkscape:snap-smooth-nodes="true" + inkscape:snap-midpoints="true" + inkscape:snap-object-midpoints="true" + inkscape:snap-center="true" + inkscape:snap-text-baseline="true" + inkscape:snap-page="true" + inkscape:lockguides="false"> + <sodipodi:guide + orientation="0,1" + position="13.637059,643.40404" + id="guide3788" + inkscape:locked="false" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="0,1" + id="guide1099" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="1,0" + id="guide1101" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="233.588,370" + orientation="1,0" + id="guide1107" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="144.36496,311.42857" + orientation="1,0" + id="guide1109" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="-77.142857,144.36496" + orientation="0,1" + id="guide1111" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="5.000315,233.58779" + orientation="0,1" + id="guide1113" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + </sodipodi:namedview> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-167.28122,-322.85977)"> + <path + sodipodi:type="star" + style="fill:#c8c8c8;fill-opacity:1;stroke:none;stroke-width:4;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="path2995" + sodipodi:sides="3" + sodipodi:cx="-334.38327" + sodipodi:cy="361.73682" + sodipodi:r1="365.71429" + sodipodi:r2="182.85715" + sodipodi:arg1="0" + sodipodi:arg2="1.0471976" + inkscape:flatsided="false" + inkscape:rounded="0" + inkscape:randomized="0" + d="m 31.331024,361.73682 -274.285724,158.35894 -274.28572,158.35893 0,-316.71788 0,-316.717863 274.28573,158.358943 z" + inkscape:transform-center-x="91.42857" + transform="matrix(-0.34523871,0,0,0.5917254,366.66256,296.22092)" /> + <path + sodipodi:type="star" + style="fill:#c8c8c8;fill-opacity:1;stroke:none;stroke-width:4;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="path2995-8" + sodipodi:sides="3" + sodipodi:cx="211.80305" + sodipodi:cy="367.03107" + sodipodi:r1="365.71429" + sodipodi:r2="182.85715" + sodipodi:arg1="0" + sodipodi:arg2="1.0471976" + inkscape:flatsided="false" + inkscape:rounded="0" + inkscape:randomized="0" + d="M 577.51735,367.03107 303.23162,525.39001 28.945908,683.74894 l 0,-316.71788 0,-316.717863 274.285732,158.358943 z" + inkscape:transform-center-x="91.42857" + transform="matrix(-0.34523871,0,0,0.5917254,366.66256,296.22092)" /> + </g> +</svg> diff --git a/tagit/assets/icons/scalable/browser/scroll_down.svg b/tagit/assets/icons/scalable/browser/scroll_down.svg new file mode 100644 index 0000000..443eb0a --- /dev/null +++ b/tagit/assets/icons/scalable/browser/scroll_down.svg @@ -0,0 +1,147 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="100mm" + height="100mm" + id="svg2" + version="1.1" + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="scroll_down.svg" + inkscape:export-filename="../../kivy/browser/scroll_down.png" + inkscape:export-xdpi="7.6199999" + inkscape:export-ydpi="7.6199999"> + <defs + id="defs4" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="1.4" + inkscape:cx="-5.5445071" + inkscape:cy="252.13629" + inkscape:document-units="mm" + inkscape:current-layer="layer1" + showgrid="false" + showguides="true" + inkscape:guide-bbox="true" + inkscape:snap-global="true" + inkscape:snap-bbox="true" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:window-width="1920" + inkscape:window-height="1031" + inkscape:window-x="0" + inkscape:window-y="25" + inkscape:window-maximized="1" + inkscape:pagecheckerboard="true" + units="mm" + inkscape:bbox-paths="true" + inkscape:bbox-nodes="true" + inkscape:snap-bbox-edge-midpoints="true" + inkscape:snap-bbox-midpoints="true" + inkscape:object-paths="true" + inkscape:snap-intersection-paths="true" + inkscape:snap-smooth-nodes="true" + inkscape:snap-midpoints="true" + inkscape:snap-object-midpoints="true" + inkscape:snap-center="true" + inkscape:snap-text-baseline="true" + inkscape:snap-page="true"> + <sodipodi:guide + orientation="0,1" + position="13.637059,643.40404" + id="guide3788" + inkscape:locked="false" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="0,1" + id="guide1099" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="1,0" + id="guide1101" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="233.588,370" + orientation="1,0" + id="guide1107" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="144.36496,311.42857" + orientation="1,0" + id="guide1109" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="-77.142857,144.36496" + orientation="0,1" + id="guide1111" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="5.000315,233.58779" + orientation="0,1" + id="guide1113" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + </sodipodi:namedview> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-167.28122,-322.85977)"> + <path + sodipodi:type="star" + style="fill:#c8c8c8;fill-opacity:1;stroke:none;stroke-width:2.19086838;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="path2995" + sodipodi:sides="3" + sodipodi:cx="267.43521" + sodipodi:cy="496.33157" + sodipodi:r1="200.30797" + sodipodi:r2="100.15398" + sodipodi:arg1="0" + sodipodi:arg2="1.0471976" + inkscape:flatsided="false" + inkscape:rounded="0" + inkscape:randomized="0" + d="m 467.74318,496.33157 -150.23098,86.73589 -150.23097,86.7359 0,-173.47179 0,-173.47179 150.23097,86.73591 z" + inkscape:transform-center-x="-87.858587" + transform="matrix(0.96095331,0,0,1.0893781,51.143197,-28.856601)" + inkscape:transform-center-y="1.8482077e-06" /> + </g> +</svg> diff --git a/tagit/assets/icons/scalable/browser/scroll_up.svg b/tagit/assets/icons/scalable/browser/scroll_up.svg new file mode 100644 index 0000000..3f965f7 --- /dev/null +++ b/tagit/assets/icons/scalable/browser/scroll_up.svg @@ -0,0 +1,147 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="100mm" + height="100mm" + id="svg2" + version="1.1" + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="scroll_up.svg" + inkscape:export-filename="../../kivy/browser/scroll_up.png" + inkscape:export-xdpi="7.6199999" + inkscape:export-ydpi="7.6199999"> + <defs + id="defs4" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="1.4" + inkscape:cx="-5.5445071" + inkscape:cy="252.13629" + inkscape:document-units="mm" + inkscape:current-layer="layer1" + showgrid="false" + showguides="true" + inkscape:guide-bbox="true" + inkscape:snap-global="true" + inkscape:snap-bbox="true" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:window-width="1920" + inkscape:window-height="1031" + inkscape:window-x="0" + inkscape:window-y="25" + inkscape:window-maximized="1" + inkscape:pagecheckerboard="true" + units="mm" + inkscape:bbox-paths="true" + inkscape:bbox-nodes="true" + inkscape:snap-bbox-edge-midpoints="true" + inkscape:snap-bbox-midpoints="true" + inkscape:object-paths="true" + inkscape:snap-intersection-paths="true" + inkscape:snap-smooth-nodes="true" + inkscape:snap-midpoints="true" + inkscape:snap-object-midpoints="true" + inkscape:snap-center="true" + inkscape:snap-text-baseline="true" + inkscape:snap-page="true"> + <sodipodi:guide + orientation="0,1" + position="13.637059,643.40404" + id="guide3788" + inkscape:locked="false" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="0,1" + id="guide1099" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="1,0" + id="guide1101" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="233.588,370" + orientation="1,0" + id="guide1107" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="144.36496,311.42857" + orientation="1,0" + id="guide1109" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="-77.142857,144.36496" + orientation="0,1" + id="guide1111" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="5.000315,233.58779" + orientation="0,1" + id="guide1113" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + </sodipodi:namedview> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-167.28122,-322.85977)"> + <path + sodipodi:type="star" + style="fill:#c8c8c8;fill-opacity:1;stroke:none;stroke-width:2.19086838;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="path2995" + sodipodi:sides="3" + sodipodi:cx="267.43521" + sodipodi:cy="496.33157" + sodipodi:r1="200.30797" + sodipodi:r2="100.15398" + sodipodi:arg1="0" + sodipodi:arg2="1.0471976" + inkscape:flatsided="false" + inkscape:rounded="0" + inkscape:randomized="0" + d="m 467.74318,496.33157 -150.23098,86.73589 -150.23097,86.7359 0,-173.47179 0,-173.47179 150.23097,86.73591 z" + inkscape:transform-center-x="87.858589" + transform="matrix(-0.96095331,0,0,1.0893781,661.372,-28.856601)" + inkscape:transform-center-y="1.8482077e-06" /> + </g> +</svg> diff --git a/tagit/assets/icons/scalable/browser/select.svg b/tagit/assets/icons/scalable/browser/select.svg new file mode 100644 index 0000000..2f1ded2 --- /dev/null +++ b/tagit/assets/icons/scalable/browser/select.svg @@ -0,0 +1,291 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="100mm" + height="100mm" + id="svg2" + version="1.1" + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="select.svg" + inkscape:export-filename="../../kivy/browser/select.png" + inkscape:export-xdpi="7.6199999" + inkscape:export-ydpi="7.6199999"> + <defs + id="defs4"> + <marker + inkscape:stockid="Arrow2Send" + orient="auto" + refY="0.0" + refX="0.0" + id="Arrow2Send" + style="overflow:visible;" + inkscape:isstock="true"> + <path + id="path1058" + style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#c8c8c8;stroke-opacity:1;fill:#c8c8c8;fill-opacity:1" + d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z " + transform="scale(0.3) rotate(180) translate(-2.3,0)" /> + </marker> + <marker + inkscape:stockid="Arrow1Send" + orient="auto" + refY="0.0" + refX="0.0" + id="marker1609" + style="overflow:visible;" + inkscape:isstock="true"> + <path + id="path1607" + d="M 0.0,0.0 L 5.0,-5.0 L -12.5,0.0 L 5.0,5.0 L 0.0,0.0 z " + style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt;stroke-opacity:1;fill:#000000;fill-opacity:1" + transform="scale(0.2) rotate(180) translate(6,0)" /> + </marker> + <marker + inkscape:stockid="Arrow1Send" + orient="auto" + refY="0.0" + refX="0.0" + id="marker1527" + style="overflow:visible;" + inkscape:isstock="true"> + <path + id="path1525" + d="M 0.0,0.0 L 5.0,-5.0 L -12.5,0.0 L 5.0,5.0 L 0.0,0.0 z " + style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt;stroke-opacity:1;fill:#000000;fill-opacity:1" + transform="scale(0.2) rotate(180) translate(6,0)" /> + </marker> + <marker + inkscape:stockid="Arrow1Send" + orient="auto" + refY="0.0" + refX="0.0" + id="marker1475" + style="overflow:visible;" + inkscape:isstock="true"> + <path + id="path1473" + d="M 0.0,0.0 L 5.0,-5.0 L -12.5,0.0 L 5.0,5.0 L 0.0,0.0 z " + style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt;stroke-opacity:1;fill:#000000;fill-opacity:1" + transform="scale(0.2) rotate(180) translate(6,0)" /> + </marker> + <marker + inkscape:stockid="Arrow2Mend" + orient="auto" + refY="0.0" + refX="0.0" + id="Arrow2Mend" + style="overflow:visible;" + inkscape:isstock="true"> + <path + id="path1052" + style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1" + d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z " + transform="scale(0.6) rotate(180) translate(0,0)" /> + </marker> + <marker + inkscape:stockid="Arrow2Sstart" + orient="auto" + refY="0.0" + refX="0.0" + id="Arrow2Sstart" + style="overflow:visible" + inkscape:isstock="true"> + <path + id="path1055" + style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1" + d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z " + transform="scale(0.3) translate(-2.3,0)" /> + </marker> + <marker + inkscape:stockid="Arrow1Send" + orient="auto" + refY="0.0" + refX="0.0" + id="marker1335" + style="overflow:visible;" + inkscape:isstock="true"> + <path + id="path1333" + d="M 0.0,0.0 L 5.0,-5.0 L -12.5,0.0 L 5.0,5.0 L 0.0,0.0 z " + style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt;stroke-opacity:1;fill:#000000;fill-opacity:1" + transform="scale(0.2) rotate(180) translate(6,0)" /> + </marker> + <marker + inkscape:stockid="Arrow1Send" + orient="auto" + refY="0.0" + refX="0.0" + id="Arrow1Send" + style="overflow:visible;" + inkscape:isstock="true"> + <path + id="path1040" + d="M 0.0,0.0 L 5.0,-5.0 L -12.5,0.0 L 5.0,5.0 L 0.0,0.0 z " + style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt;stroke-opacity:1;fill:#000000;fill-opacity:1" + transform="scale(0.2) rotate(180) translate(6,0)" /> + </marker> + <marker + inkscape:stockid="Arrow1Mend" + orient="auto" + refY="0.0" + refX="0.0" + id="Arrow1Mend" + style="overflow:visible;" + inkscape:isstock="true"> + <path + id="path1034" + d="M 0.0,0.0 L 5.0,-5.0 L -12.5,0.0 L 5.0,5.0 L 0.0,0.0 z " + style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt;stroke-opacity:1;fill:#000000;fill-opacity:1" + transform="scale(0.4) rotate(180) translate(10,0)" /> + </marker> + <inkscape:path-effect + effect="bspline" + id="path-effect1023" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + </defs> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="1.979899" + inkscape:cx="66.103038" + inkscape:cy="189.53698" + inkscape:document-units="mm" + inkscape:current-layer="layer1" + showgrid="false" + showguides="true" + inkscape:guide-bbox="true" + inkscape:snap-global="true" + inkscape:snap-bbox="true" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:window-width="1920" + inkscape:window-height="1031" + inkscape:window-x="0" + inkscape:window-y="25" + inkscape:window-maximized="1" + inkscape:pagecheckerboard="true" + units="mm" + inkscape:bbox-paths="true" + inkscape:bbox-nodes="true" + inkscape:snap-bbox-edge-midpoints="true" + inkscape:snap-bbox-midpoints="true" + inkscape:object-paths="true" + inkscape:snap-intersection-paths="true" + inkscape:snap-smooth-nodes="true" + inkscape:snap-midpoints="true" + inkscape:snap-object-midpoints="true" + inkscape:snap-center="true" + inkscape:snap-text-baseline="true" + inkscape:snap-page="true" + inkscape:lockguides="false"> + <sodipodi:guide + orientation="0,1" + position="13.637059,643.40404" + id="guide3788" + inkscape:locked="false" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="0,1" + id="guide1099" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="1,0" + id="guide1101" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="-25.75889,207.87402" + orientation="0,1" + id="guide837" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="-43.941635,170.07874" + orientation="0,1" + id="guide854" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="170.07874,284.018" + orientation="1,0" + id="guide877" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="207.87402,258.09397" + orientation="1,0" + id="guide879" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="102.53048,247.56521" + orientation="0,1" + id="guide914" + inkscape:locked="false" /> + <sodipodi:guide + position="85.03952,130.38754" + orientation="0,1" + id="guide916" + inkscape:locked="false" /> + </sodipodi:namedview> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-167.28122,-322.85977)"> + <rect + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#c8c8c8;fill-opacity:1;fill-rule:nonzero;stroke:#c8c8c8;stroke-width:13.7840004;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:23.10000038;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + id="rect4136-3-5" + width="156.29443" + height="103.39367" + x="174.17352" + y="460.13931" /> + <rect + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#c8c8c8;stroke-width:13.7840004;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:23.10000038;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + id="rect4136-3-5-3" + width="156.29443" + height="103.39367" + x="382.04724" + y="460.13931" /> + </g> +</svg> diff --git a/tagit/assets/icons/scalable/browser/select_add.svg b/tagit/assets/icons/scalable/browser/select_add.svg new file mode 100644 index 0000000..a7d771c --- /dev/null +++ b/tagit/assets/icons/scalable/browser/select_add.svg @@ -0,0 +1,169 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="100mm" + height="100mm" + id="svg2" + version="1.1" + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="select_add.svg" + inkscape:export-filename="../../kivy/browser/select_add.png" + inkscape:export-xdpi="7.6199999" + inkscape:export-ydpi="7.6199999"> + <defs + id="defs4" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="1.979899" + inkscape:cx="62.016423" + inkscape:cy="199.39131" + inkscape:document-units="mm" + inkscape:current-layer="layer1" + showgrid="false" + showguides="true" + inkscape:guide-bbox="true" + inkscape:snap-global="true" + inkscape:snap-bbox="true" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:window-width="1920" + inkscape:window-height="1031" + inkscape:window-x="0" + inkscape:window-y="25" + inkscape:window-maximized="1" + inkscape:pagecheckerboard="true" + units="mm" + inkscape:bbox-paths="true" + inkscape:bbox-nodes="true" + inkscape:snap-bbox-edge-midpoints="true" + inkscape:snap-bbox-midpoints="true" + inkscape:object-paths="true" + inkscape:snap-intersection-paths="true" + inkscape:snap-smooth-nodes="true" + inkscape:snap-midpoints="true" + inkscape:snap-object-midpoints="true" + inkscape:snap-center="true" + inkscape:snap-text-baseline="true" + inkscape:snap-page="true" + inkscape:lockguides="false"> + <sodipodi:guide + orientation="0,1" + position="13.637059,643.40404" + id="guide3788" + inkscape:locked="false" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="0,1" + id="guide1099" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="1,0" + id="guide1101" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="-25.75889,207.87402" + orientation="0,1" + id="guide837" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="-43.941635,170.07874" + orientation="0,1" + id="guide854" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="170.07874,284.018" + orientation="1,0" + id="guide877" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="207.87402,258.09397" + orientation="1,0" + id="guide879" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="102.53048,247.56521" + orientation="0,1" + id="guide914" + inkscape:locked="false" /> + <sodipodi:guide + position="85.03952,130.38754" + orientation="0,1" + id="guide916" + inkscape:locked="false" /> + <sodipodi:guide + position="292.91339,281.32748" + orientation="1,0" + id="guide918" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + </sodipodi:namedview> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-167.28122,-322.85977)"> + <rect + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#c8c8c8;fill-opacity:1;fill-rule:nonzero;stroke:#c8c8c8;stroke-width:13.7840004;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:23.10000038;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + id="rect4136-3-5" + width="156.29443" + height="103.39367" + x="174.17352" + y="460.13931" /> + <rect + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#c8c8c8;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:17.84657669;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:23.10000038;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + id="rect4136-7-5" + width="29.217978" + height="117.93987" + x="446.58844" + y="452.86621" /> + <rect + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#c8c8c8;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:17.84657669;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:23.10000038;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + id="rect4136-7-5-2" + width="29.217978" + height="117.93987" + x="-526.44513" + y="402.22751" + transform="rotate(-90)" /> + </g> +</svg> diff --git a/tagit/assets/icons/scalable/browser/select_all.svg b/tagit/assets/icons/scalable/browser/select_all.svg new file mode 100644 index 0000000..21f9fe5 --- /dev/null +++ b/tagit/assets/icons/scalable/browser/select_all.svg @@ -0,0 +1,158 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="100mm" + height="100mm" + id="svg2" + version="1.1" + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="select_all.svg" + inkscape:export-filename="../../kivy/browser/select_all.png" + inkscape:export-xdpi="7.6199999" + inkscape:export-ydpi="7.6199999"> + <defs + id="defs4" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="1.979899" + inkscape:cx="139.54563" + inkscape:cy="201.41162" + inkscape:document-units="mm" + inkscape:current-layer="layer1" + showgrid="false" + showguides="true" + inkscape:guide-bbox="true" + inkscape:snap-global="true" + inkscape:snap-bbox="true" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:window-width="1920" + inkscape:window-height="1031" + inkscape:window-x="0" + inkscape:window-y="25" + inkscape:window-maximized="1" + inkscape:pagecheckerboard="true" + units="mm" + inkscape:bbox-paths="true" + inkscape:bbox-nodes="true" + inkscape:snap-bbox-edge-midpoints="true" + inkscape:snap-bbox-midpoints="true" + inkscape:object-paths="true" + inkscape:snap-intersection-paths="true" + inkscape:snap-smooth-nodes="true" + inkscape:snap-midpoints="true" + inkscape:snap-object-midpoints="true" + inkscape:snap-center="true" + inkscape:snap-text-baseline="true" + inkscape:snap-page="true" + inkscape:lockguides="false"> + <sodipodi:guide + orientation="0,1" + position="13.637059,643.40404" + id="guide3788" + inkscape:locked="false" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="0,1" + id="guide1099" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="1,0" + id="guide1101" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="-25.75889,207.87402" + orientation="0,1" + id="guide837" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="-43.941635,170.07874" + orientation="0,1" + id="guide854" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="170.07874,284.018" + orientation="1,0" + id="guide877" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="207.87402,258.09397" + orientation="1,0" + id="guide879" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + </sodipodi:namedview> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-167.28122,-322.85977)"> + <rect + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#c8c8c8;fill-opacity:1;fill-rule:nonzero;stroke:#c8c8c8;stroke-width:13.78430176;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:23.10000038;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + id="rect4136-3-5" + width="156.29443" + height="103.39367" + x="174.17337" + y="382.65268" /> + <rect + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#c8c8c8;fill-opacity:1;fill-rule:nonzero;stroke:#c8c8c8;stroke-width:13.78430176;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:23.10000038;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + id="rect4136-3-5-2" + width="156.29443" + height="103.39367" + x="382.04739" + y="382.65268" /> + <rect + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#c8c8c8;fill-opacity:1;fill-rule:nonzero;stroke:#c8c8c8;stroke-width:13.78430176;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:23.10000038;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + id="rect4136-3-5-9" + width="156.29443" + height="103.39367" + x="174.17337" + y="537.62592" /> + <rect + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#c8c8c8;fill-opacity:1;fill-rule:nonzero;stroke:#c8c8c8;stroke-width:13.78430176;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:23.10000038;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + id="rect4136-3-5-1" + width="156.29443" + height="103.39367" + x="382.04739" + y="537.62592" /> + </g> +</svg> diff --git a/tagit/assets/icons/scalable/browser/select_invert.svg b/tagit/assets/icons/scalable/browser/select_invert.svg new file mode 100644 index 0000000..9f7ba65 --- /dev/null +++ b/tagit/assets/icons/scalable/browser/select_invert.svg @@ -0,0 +1,255 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="100mm" + height="100mm" + id="svg2" + version="1.1" + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="select_invert.svg" + inkscape:export-filename="../../kivy/browser/select_invert.png" + inkscape:export-xdpi="7.6199999" + inkscape:export-ydpi="7.6199999"> + <defs + id="defs4"> + <marker + inkscape:stockid="Arrow2Mend" + orient="auto" + refY="0.0" + refX="0.0" + id="Arrow2Mend" + style="overflow:visible;" + inkscape:isstock="true"> + <path + id="path935" + style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#c8c8c8;stroke-opacity:1;fill:#c8c8c8;fill-opacity:1" + d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z " + transform="scale(0.6) rotate(180) translate(0,0)" /> + </marker> + <marker + inkscape:stockid="Arrow2Lend" + orient="auto" + refY="0.0" + refX="0.0" + id="Arrow2Lend" + style="overflow:visible;" + inkscape:isstock="true"> + <path + id="path929" + style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1" + d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z " + transform="scale(1.1) rotate(180) translate(1,0)" /> + </marker> + <marker + inkscape:stockid="Arrow1Send" + orient="auto" + refY="0.0" + refX="0.0" + id="Arrow1Send" + style="overflow:visible;" + inkscape:isstock="true"> + <path + id="path923" + d="M 0.0,0.0 L 5.0,-5.0 L -12.5,0.0 L 5.0,5.0 L 0.0,0.0 z " + style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt;stroke-opacity:1;fill:#000000;fill-opacity:1" + transform="scale(0.2) rotate(180) translate(6,0)" /> + </marker> + <marker + inkscape:stockid="Arrow1Mend" + orient="auto" + refY="0.0" + refX="0.0" + id="Arrow1Mend" + style="overflow:visible;" + inkscape:isstock="true"> + <path + id="path917" + d="M 0.0,0.0 L 5.0,-5.0 L -12.5,0.0 L 5.0,5.0 L 0.0,0.0 z " + style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt;stroke-opacity:1;fill:#000000;fill-opacity:1" + transform="scale(0.4) rotate(180) translate(10,0)" /> + </marker> + <inkscape:path-effect + effect="bspline" + id="path-effect906" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + <marker + inkscape:stockid="Arrow2Mend" + orient="auto" + refY="0" + refX="0" + id="Arrow2Mend-3" + style="overflow:visible" + inkscape:isstock="true"> + <path + inkscape:connector-curvature="0" + id="path935-6" + style="fill:#c8c8c8;fill-opacity:1;fill-rule:evenodd;stroke:#c8c8c8;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1" + d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z" + transform="scale(-0.6)" /> + </marker> + <inkscape:path-effect + effect="bspline" + id="path-effect906-7" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath1751-3"> + <path + style="fill:#0cc8c8;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="M 167.28122,700.81253 545.23398,322.85977 H 167.28122 Z" + id="path1753-5" + inkscape:connector-curvature="0" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath1780"> + <rect + style="opacity:1;fill:#c8c8c8;fill-opacity:1;stroke:none;stroke-width:30;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" + id="rect1782" + width="377.95276" + height="377.95276" + x="167.28122" + y="322.85977" + clip-path="url(#clipPath1751-3)" + transform="rotate(180,356.2576,511.83615)" /> + </clipPath> + </defs> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="1.979899" + inkscape:cx="183.64532" + inkscape:cy="182.16579" + inkscape:document-units="mm" + inkscape:current-layer="layer1" + showgrid="false" + showguides="true" + inkscape:guide-bbox="true" + inkscape:snap-global="true" + inkscape:snap-bbox="true" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:window-width="1920" + inkscape:window-height="1031" + inkscape:window-x="0" + inkscape:window-y="25" + inkscape:window-maximized="1" + inkscape:pagecheckerboard="true" + units="mm" + inkscape:bbox-paths="true" + inkscape:bbox-nodes="true" + inkscape:snap-bbox-edge-midpoints="true" + inkscape:snap-bbox-midpoints="true" + inkscape:object-paths="true" + inkscape:snap-intersection-paths="true" + inkscape:snap-smooth-nodes="true" + inkscape:snap-midpoints="true" + inkscape:snap-object-midpoints="true" + inkscape:snap-center="true" + inkscape:snap-text-baseline="true" + inkscape:snap-page="true" + showborder="true"> + <sodipodi:guide + orientation="0,1" + position="13.637059,643.40404" + id="guide3788" + inkscape:locked="false" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="0,1" + id="guide1099" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="1,0" + id="guide1101" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="233.588,370" + orientation="1,0" + id="guide1107" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="144.36496,311.42857" + orientation="1,0" + id="guide1109" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="5.000315,233.58779" + orientation="0,1" + id="guide1113" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="-305.71429,497.85714" + orientation="0,1" + id="guide822" + inkscape:locked="false" /> + </sodipodi:namedview> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-167.28122,-322.85977)"> + <circle + style="opacity:1;fill:#c8c8c8;fill-opacity:1;stroke:none;stroke-width:30;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" + id="path1743" + cx="347.08838" + cy="516.96478" + r="103.03556" + clip-path="url(#clipPath1780)" + transform="translate(9.1692211,-5.1286368)" /> + <path + style="fill:#c8c8c8;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="M 0 0 L 0 377.95312 L 116.16406 261.78906 A 103.03556 103.03556 0 0 1 85.941406 188.97656 A 103.03556 103.03556 0 0 1 188.97656 85.941406 A 103.03556 103.03556 0 0 1 261.78711 116.16602 L 377.95312 0 L 0 0 z " + transform="translate(167.28122,322.85977)" + id="path1895" /> + </g> +</svg> diff --git a/tagit/assets/icons/scalable/browser/select_multi.svg b/tagit/assets/icons/scalable/browser/select_multi.svg new file mode 100644 index 0000000..5912429 --- /dev/null +++ b/tagit/assets/icons/scalable/browser/select_multi.svg @@ -0,0 +1,158 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="100mm" + height="100mm" + id="svg2" + version="1.1" + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="select_multi.svg" + inkscape:export-filename="../../kivy/browser/select_multi.png" + inkscape:export-xdpi="7.6199999" + inkscape:export-ydpi="7.6199999"> + <defs + id="defs4" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="1.979899" + inkscape:cx="139.54563" + inkscape:cy="201.41162" + inkscape:document-units="mm" + inkscape:current-layer="layer1" + showgrid="false" + showguides="true" + inkscape:guide-bbox="true" + inkscape:snap-global="true" + inkscape:snap-bbox="true" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:window-width="1920" + inkscape:window-height="1031" + inkscape:window-x="0" + inkscape:window-y="25" + inkscape:window-maximized="1" + inkscape:pagecheckerboard="true" + units="mm" + inkscape:bbox-paths="true" + inkscape:bbox-nodes="true" + inkscape:snap-bbox-edge-midpoints="true" + inkscape:snap-bbox-midpoints="true" + inkscape:object-paths="true" + inkscape:snap-intersection-paths="true" + inkscape:snap-smooth-nodes="true" + inkscape:snap-midpoints="true" + inkscape:snap-object-midpoints="true" + inkscape:snap-center="true" + inkscape:snap-text-baseline="true" + inkscape:snap-page="true" + inkscape:lockguides="false"> + <sodipodi:guide + orientation="0,1" + position="13.637059,643.40404" + id="guide3788" + inkscape:locked="false" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="0,1" + id="guide1099" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="1,0" + id="guide1101" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="-25.75889,207.87402" + orientation="0,1" + id="guide837" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="-43.941635,170.07874" + orientation="0,1" + id="guide854" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="170.07874,284.018" + orientation="1,0" + id="guide877" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="207.87402,258.09397" + orientation="1,0" + id="guide879" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + </sodipodi:namedview> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-167.28122,-322.85977)"> + <rect + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#c8c8c8;fill-opacity:1;fill-rule:nonzero;stroke:#c8c8c8;stroke-width:13.7840004;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:23.10000038;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + id="rect4136-3-5" + width="156.29443" + height="103.39367" + x="174.17337" + y="382.65268" /> + <rect + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#c8c8c8;fill-opacity:1;fill-rule:nonzero;stroke:#c8c8c8;stroke-width:13.7840004;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:23.10000038;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + id="rect4136-3-5-2" + width="156.29443" + height="103.39367" + x="382.04739" + y="382.65268" /> + <rect + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#c8c8c8;stroke-width:13.7840004;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:23.10000038;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + id="rect4136-3-5-9" + width="156.29443" + height="103.39367" + x="174.17337" + y="537.62592" /> + <rect + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#c8c8c8;fill-opacity:1;fill-rule:nonzero;stroke:#c8c8c8;stroke-width:13.7840004;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:23.10000038;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + id="rect4136-3-5-1" + width="156.29443" + height="103.39367" + x="382.04739" + y="537.62592" /> + </g> +</svg> diff --git a/tagit/assets/icons/scalable/browser/select_none.svg b/tagit/assets/icons/scalable/browser/select_none.svg new file mode 100644 index 0000000..e0d807b --- /dev/null +++ b/tagit/assets/icons/scalable/browser/select_none.svg @@ -0,0 +1,158 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="100mm" + height="100mm" + id="svg2" + version="1.1" + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="select_none.svg" + inkscape:export-filename="../../kivy/browser/select_none.png" + inkscape:export-xdpi="7.6199999" + inkscape:export-ydpi="7.6199999"> + <defs + id="defs4" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="1.979899" + inkscape:cx="62.016423" + inkscape:cy="199.39131" + inkscape:document-units="mm" + inkscape:current-layer="layer1" + showgrid="false" + showguides="true" + inkscape:guide-bbox="true" + inkscape:snap-global="true" + inkscape:snap-bbox="true" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:window-width="1920" + inkscape:window-height="1031" + inkscape:window-x="0" + inkscape:window-y="25" + inkscape:window-maximized="1" + inkscape:pagecheckerboard="true" + units="mm" + inkscape:bbox-paths="true" + inkscape:bbox-nodes="true" + inkscape:snap-bbox-edge-midpoints="true" + inkscape:snap-bbox-midpoints="true" + inkscape:object-paths="true" + inkscape:snap-intersection-paths="true" + inkscape:snap-smooth-nodes="true" + inkscape:snap-midpoints="true" + inkscape:snap-object-midpoints="true" + inkscape:snap-center="true" + inkscape:snap-text-baseline="true" + inkscape:snap-page="true" + inkscape:lockguides="false"> + <sodipodi:guide + orientation="0,1" + position="13.637059,643.40404" + id="guide3788" + inkscape:locked="false" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="0,1" + id="guide1099" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="1,0" + id="guide1101" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="-25.75889,207.87402" + orientation="0,1" + id="guide837" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="-43.941635,170.07874" + orientation="0,1" + id="guide854" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="170.07874,284.018" + orientation="1,0" + id="guide877" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="207.87402,258.09397" + orientation="1,0" + id="guide879" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + </sodipodi:namedview> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title /> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-167.28122,-322.85977)"> + <rect + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#c8c8c8;stroke-width:13.7840004;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:23.10000038;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + id="rect4136-3-5" + width="156.29443" + height="103.39367" + x="174.17337" + y="382.65268" /> + <rect + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#c8c8c8;stroke-width:13.7840004;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:23.10000038;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + id="rect4136-3-5-2" + width="156.29443" + height="103.39367" + x="382.04739" + y="382.65268" /> + <rect + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#c8c8c8;stroke-width:13.7840004;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:23.10000038;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + id="rect4136-3-5-9" + width="156.29443" + height="103.39367" + x="174.17337" + y="537.62592" /> + <rect + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#c8c8c8;stroke-width:13.7840004;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:23.10000038;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + id="rect4136-3-5-1" + width="156.29443" + height="103.39367" + x="382.04739" + y="537.62592" /> + </g> +</svg> diff --git a/tagit/assets/icons/scalable/browser/select_range.svg b/tagit/assets/icons/scalable/browser/select_range.svg new file mode 100644 index 0000000..e3aa805 --- /dev/null +++ b/tagit/assets/icons/scalable/browser/select_range.svg @@ -0,0 +1,312 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="100mm" + height="100mm" + id="svg2" + version="1.1" + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="select_range.svg" + inkscape:export-filename="../../kivy/browser/select_range.png" + inkscape:export-xdpi="7.6199999" + inkscape:export-ydpi="7.6199999"> + <defs + id="defs4"> + <marker + inkscape:stockid="Arrow2Send" + orient="auto" + refY="0.0" + refX="0.0" + id="Arrow2Send" + style="overflow:visible;" + inkscape:isstock="true"> + <path + id="path1058" + style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#c8c8c8;stroke-opacity:1;fill:#c8c8c8;fill-opacity:1" + d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z " + transform="scale(0.3) rotate(180) translate(-2.3,0)" /> + </marker> + <marker + inkscape:stockid="Arrow1Send" + orient="auto" + refY="0.0" + refX="0.0" + id="marker1609" + style="overflow:visible;" + inkscape:isstock="true"> + <path + id="path1607" + d="M 0.0,0.0 L 5.0,-5.0 L -12.5,0.0 L 5.0,5.0 L 0.0,0.0 z " + style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt;stroke-opacity:1;fill:#000000;fill-opacity:1" + transform="scale(0.2) rotate(180) translate(6,0)" /> + </marker> + <marker + inkscape:stockid="Arrow1Send" + orient="auto" + refY="0.0" + refX="0.0" + id="marker1527" + style="overflow:visible;" + inkscape:isstock="true"> + <path + id="path1525" + d="M 0.0,0.0 L 5.0,-5.0 L -12.5,0.0 L 5.0,5.0 L 0.0,0.0 z " + style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt;stroke-opacity:1;fill:#000000;fill-opacity:1" + transform="scale(0.2) rotate(180) translate(6,0)" /> + </marker> + <marker + inkscape:stockid="Arrow1Send" + orient="auto" + refY="0.0" + refX="0.0" + id="marker1475" + style="overflow:visible;" + inkscape:isstock="true"> + <path + id="path1473" + d="M 0.0,0.0 L 5.0,-5.0 L -12.5,0.0 L 5.0,5.0 L 0.0,0.0 z " + style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt;stroke-opacity:1;fill:#000000;fill-opacity:1" + transform="scale(0.2) rotate(180) translate(6,0)" /> + </marker> + <marker + inkscape:stockid="Arrow2Mend" + orient="auto" + refY="0.0" + refX="0.0" + id="Arrow2Mend" + style="overflow:visible;" + inkscape:isstock="true"> + <path + id="path1052" + style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1" + d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z " + transform="scale(0.6) rotate(180) translate(0,0)" /> + </marker> + <marker + inkscape:stockid="Arrow2Sstart" + orient="auto" + refY="0.0" + refX="0.0" + id="Arrow2Sstart" + style="overflow:visible" + inkscape:isstock="true"> + <path + id="path1055" + style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1" + d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z " + transform="scale(0.3) translate(-2.3,0)" /> + </marker> + <marker + inkscape:stockid="Arrow1Send" + orient="auto" + refY="0.0" + refX="0.0" + id="marker1335" + style="overflow:visible;" + inkscape:isstock="true"> + <path + id="path1333" + d="M 0.0,0.0 L 5.0,-5.0 L -12.5,0.0 L 5.0,5.0 L 0.0,0.0 z " + style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt;stroke-opacity:1;fill:#000000;fill-opacity:1" + transform="scale(0.2) rotate(180) translate(6,0)" /> + </marker> + <marker + inkscape:stockid="Arrow1Send" + orient="auto" + refY="0.0" + refX="0.0" + id="Arrow1Send" + style="overflow:visible;" + inkscape:isstock="true"> + <path + id="path1040" + d="M 0.0,0.0 L 5.0,-5.0 L -12.5,0.0 L 5.0,5.0 L 0.0,0.0 z " + style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt;stroke-opacity:1;fill:#000000;fill-opacity:1" + transform="scale(0.2) rotate(180) translate(6,0)" /> + </marker> + <marker + inkscape:stockid="Arrow1Mend" + orient="auto" + refY="0.0" + refX="0.0" + id="Arrow1Mend" + style="overflow:visible;" + inkscape:isstock="true"> + <path + id="path1034" + d="M 0.0,0.0 L 5.0,-5.0 L -12.5,0.0 L 5.0,5.0 L 0.0,0.0 z " + style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt;stroke-opacity:1;fill:#000000;fill-opacity:1" + transform="scale(0.4) rotate(180) translate(10,0)" /> + </marker> + <inkscape:path-effect + effect="bspline" + id="path-effect1023" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + </defs> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="2.8" + inkscape:cx="89.108921" + inkscape:cy="176.88932" + inkscape:document-units="mm" + inkscape:current-layer="layer1" + showgrid="false" + showguides="true" + inkscape:guide-bbox="true" + inkscape:snap-global="true" + inkscape:snap-bbox="true" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:window-width="1920" + inkscape:window-height="1031" + inkscape:window-x="0" + inkscape:window-y="25" + inkscape:window-maximized="1" + inkscape:pagecheckerboard="true" + units="mm" + inkscape:bbox-paths="true" + inkscape:bbox-nodes="true" + inkscape:snap-bbox-edge-midpoints="true" + inkscape:snap-bbox-midpoints="true" + inkscape:object-paths="true" + inkscape:snap-intersection-paths="true" + inkscape:snap-smooth-nodes="true" + inkscape:snap-midpoints="true" + inkscape:snap-object-midpoints="true" + inkscape:snap-center="true" + inkscape:snap-text-baseline="true" + inkscape:snap-page="true" + inkscape:lockguides="false"> + <sodipodi:guide + orientation="0,1" + position="13.637059,643.40404" + id="guide3788" + inkscape:locked="false" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="0,1" + id="guide1099" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="1,0" + id="guide1101" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="-25.75889,207.87402" + orientation="0,1" + id="guide837" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="-43.941635,170.07874" + orientation="0,1" + id="guide854" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="170.07874,284.018" + orientation="1,0" + id="guide877" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="207.87402,258.09397" + orientation="1,0" + id="guide879" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="102.53048,247.56521" + orientation="0,1" + id="guide914" + inkscape:locked="false" /> + <sodipodi:guide + position="85.03952,130.38754" + orientation="0,1" + id="guide916" + inkscape:locked="false" /> + <sodipodi:guide + position="292.91339,281.32748" + orientation="1,0" + id="guide918" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="329.83559,230.81985" + orientation="1,0" + inkscape:locked="false" + id="guide1811" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + </sodipodi:namedview> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title /> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-167.28122,-322.85977)"> + <rect + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#c8c8c8;fill-opacity:1;fill-rule:nonzero;stroke:#c8c8c8;stroke-width:13.7840004;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:23.10000038;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + id="rect4136-3-5" + width="156.29443" + height="103.39367" + x="174.17352" + y="460.13931" /> + <rect + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#c8c8c8;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:17.84657669;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:23.10000038;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + id="rect4136-7-5-2" + width="29.217978" + height="117.93987" + x="516.01599" + y="452.86621" /> + <path + style="fill:none;stroke:#c8c8c8;stroke-width:18.90862083;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow2Send)" + d="m 356.2576,511.83615 c 37.38613,0 74.77137,0 112.15571,0" + id="path1021" + inkscape:connector-curvature="0" + inkscape:path-effect="#path-effect1023" + inkscape:original-d="m 356.2576,511.83615 c 37.38613,-10e-4 74.77137,-10e-4 112.15571,0" /> + </g> +</svg> diff --git a/tagit/assets/icons/scalable/browser/select_single.svg b/tagit/assets/icons/scalable/browser/select_single.svg new file mode 100644 index 0000000..6b9ab45 --- /dev/null +++ b/tagit/assets/icons/scalable/browser/select_single.svg @@ -0,0 +1,158 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="100mm" + height="100mm" + id="svg2" + version="1.1" + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="select_single.svg" + inkscape:export-filename="../../kivy/browser/select_single.png" + inkscape:export-xdpi="7.6199999" + inkscape:export-ydpi="7.6199999"> + <defs + id="defs4" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="1.979899" + inkscape:cx="62.016423" + inkscape:cy="199.39131" + inkscape:document-units="mm" + inkscape:current-layer="layer1" + showgrid="false" + showguides="true" + inkscape:guide-bbox="true" + inkscape:snap-global="true" + inkscape:snap-bbox="true" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:window-width="1920" + inkscape:window-height="1031" + inkscape:window-x="0" + inkscape:window-y="25" + inkscape:window-maximized="1" + inkscape:pagecheckerboard="true" + units="mm" + inkscape:bbox-paths="true" + inkscape:bbox-nodes="true" + inkscape:snap-bbox-edge-midpoints="true" + inkscape:snap-bbox-midpoints="true" + inkscape:object-paths="true" + inkscape:snap-intersection-paths="true" + inkscape:snap-smooth-nodes="true" + inkscape:snap-midpoints="true" + inkscape:snap-object-midpoints="true" + inkscape:snap-center="true" + inkscape:snap-text-baseline="true" + inkscape:snap-page="true" + inkscape:lockguides="false"> + <sodipodi:guide + orientation="0,1" + position="13.637059,643.40404" + id="guide3788" + inkscape:locked="false" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="0,1" + id="guide1099" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="1,0" + id="guide1101" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="-25.75889,207.87402" + orientation="0,1" + id="guide837" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="-43.941635,170.07874" + orientation="0,1" + id="guide854" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="170.07874,284.018" + orientation="1,0" + id="guide877" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="207.87402,258.09397" + orientation="1,0" + id="guide879" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + </sodipodi:namedview> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-167.28122,-322.85977)"> + <rect + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#c8c8c8;fill-opacity:1;fill-rule:nonzero;stroke:#c8c8c8;stroke-width:13.7840004;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:23.10000038;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + id="rect4136-3-5" + width="156.29443" + height="103.39367" + x="174.17337" + y="382.65268" /> + <rect + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#c8c8c8;stroke-width:13.7840004;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:23.10000038;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + id="rect4136-3-5-2" + width="156.29443" + height="103.39367" + x="382.04739" + y="382.65268" /> + <rect + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#c8c8c8;stroke-width:13.7840004;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:23.10000038;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + id="rect4136-3-5-9" + width="156.29443" + height="103.39367" + x="174.17337" + y="537.62592" /> + <rect + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#c8c8c8;stroke-width:13.7840004;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:23.10000038;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + id="rect4136-3-5-1" + width="156.29443" + height="103.39367" + x="382.04739" + y="537.62592" /> + </g> +</svg> diff --git a/tagit/assets/icons/scalable/browser/select_sub.svg b/tagit/assets/icons/scalable/browser/select_sub.svg new file mode 100644 index 0000000..724e4d0 --- /dev/null +++ b/tagit/assets/icons/scalable/browser/select_sub.svg @@ -0,0 +1,162 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="100mm" + height="100mm" + id="svg2" + version="1.1" + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="select_sub.svg" + inkscape:export-filename="../../kivy/browser/select_sub.png" + inkscape:export-xdpi="7.6199999" + inkscape:export-ydpi="7.6199999"> + <defs + id="defs4" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="2.8" + inkscape:cx="89.108921" + inkscape:cy="176.88932" + inkscape:document-units="mm" + inkscape:current-layer="layer1" + showgrid="false" + showguides="true" + inkscape:guide-bbox="true" + inkscape:snap-global="true" + inkscape:snap-bbox="true" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:window-width="1920" + inkscape:window-height="1031" + inkscape:window-x="0" + inkscape:window-y="25" + inkscape:window-maximized="1" + inkscape:pagecheckerboard="true" + units="mm" + inkscape:bbox-paths="true" + inkscape:bbox-nodes="true" + inkscape:snap-bbox-edge-midpoints="true" + inkscape:snap-bbox-midpoints="true" + inkscape:object-paths="true" + inkscape:snap-intersection-paths="true" + inkscape:snap-smooth-nodes="true" + inkscape:snap-midpoints="true" + inkscape:snap-object-midpoints="true" + inkscape:snap-center="true" + inkscape:snap-text-baseline="true" + inkscape:snap-page="true" + inkscape:lockguides="false"> + <sodipodi:guide + orientation="0,1" + position="13.637059,643.40404" + id="guide3788" + inkscape:locked="false" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="0,1" + id="guide1099" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="1,0" + id="guide1101" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="-25.75889,207.87402" + orientation="0,1" + id="guide837" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="-43.941635,170.07874" + orientation="0,1" + id="guide854" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="170.07874,284.018" + orientation="1,0" + id="guide877" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="207.87402,258.09397" + orientation="1,0" + id="guide879" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="102.53048,247.56521" + orientation="0,1" + id="guide914" + inkscape:locked="false" /> + <sodipodi:guide + position="85.03952,130.38754" + orientation="0,1" + id="guide916" + inkscape:locked="false" /> + <sodipodi:guide + position="292.91339,281.32748" + orientation="1,0" + id="guide918" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + </sodipodi:namedview> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title /> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-167.28122,-322.85977)"> + <rect + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#c8c8c8;fill-opacity:1;fill-rule:nonzero;stroke:#c8c8c8;stroke-width:13.7840004;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:23.10000038;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + id="rect4136-3-5" + width="156.29443" + height="103.39367" + x="174.17352" + y="460.13931" /> + <rect + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#c8c8c8;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:17.84657669;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:23.10000038;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + id="rect4136-7-5-2" + width="29.217978" + height="117.93987" + x="-526.44513" + y="402.22751" + transform="rotate(-90)" /> + </g> +</svg> diff --git a/tagit/assets/icons/scalable/browser/zoom_in.svg b/tagit/assets/icons/scalable/browser/zoom_in.svg new file mode 100644 index 0000000..f61660c --- /dev/null +++ b/tagit/assets/icons/scalable/browser/zoom_in.svg @@ -0,0 +1,241 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="100mm" + height="100mm" + id="svg2" + version="1.1" + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="zoom_in.svg" + inkscape:export-filename="../../kivy/browser/zoom_in.png" + inkscape:export-xdpi="7.6199999" + inkscape:export-ydpi="7.6199999"> + <defs + id="defs4"> + <marker + inkscape:stockid="Arrow2Mend" + orient="auto" + refY="0.0" + refX="0.0" + id="Arrow2Mend" + style="overflow:visible;" + inkscape:isstock="true"> + <path + id="path935" + style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#c8c8c8;stroke-opacity:1;fill:#c8c8c8;fill-opacity:1" + d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z " + transform="scale(0.6) rotate(180) translate(0,0)" /> + </marker> + <marker + inkscape:stockid="Arrow2Lend" + orient="auto" + refY="0.0" + refX="0.0" + id="Arrow2Lend" + style="overflow:visible;" + inkscape:isstock="true"> + <path + id="path929" + style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1" + d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z " + transform="scale(1.1) rotate(180) translate(1,0)" /> + </marker> + <marker + inkscape:stockid="Arrow1Send" + orient="auto" + refY="0.0" + refX="0.0" + id="Arrow1Send" + style="overflow:visible;" + inkscape:isstock="true"> + <path + id="path923" + d="M 0.0,0.0 L 5.0,-5.0 L -12.5,0.0 L 5.0,5.0 L 0.0,0.0 z " + style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt;stroke-opacity:1;fill:#000000;fill-opacity:1" + transform="scale(0.2) rotate(180) translate(6,0)" /> + </marker> + <marker + inkscape:stockid="Arrow1Mend" + orient="auto" + refY="0.0" + refX="0.0" + id="Arrow1Mend" + style="overflow:visible;" + inkscape:isstock="true"> + <path + id="path917" + d="M 0.0,0.0 L 5.0,-5.0 L -12.5,0.0 L 5.0,5.0 L 0.0,0.0 z " + style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt;stroke-opacity:1;fill:#000000;fill-opacity:1" + transform="scale(0.4) rotate(180) translate(10,0)" /> + </marker> + <inkscape:path-effect + effect="bspline" + id="path-effect906" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + <marker + inkscape:stockid="Arrow2Mend" + orient="auto" + refY="0" + refX="0" + id="Arrow2Mend-3" + style="overflow:visible" + inkscape:isstock="true"> + <path + inkscape:connector-curvature="0" + id="path935-6" + style="fill:#c8c8c8;fill-opacity:1;fill-rule:evenodd;stroke:#c8c8c8;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1" + d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z" + transform="scale(-0.6)" /> + </marker> + <inkscape:path-effect + effect="bspline" + id="path-effect906-7" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + </defs> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="1.979899" + inkscape:cx="183.64532" + inkscape:cy="182.16579" + inkscape:document-units="mm" + inkscape:current-layer="layer1" + showgrid="false" + showguides="true" + inkscape:guide-bbox="true" + inkscape:snap-global="true" + inkscape:snap-bbox="true" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:window-width="1920" + inkscape:window-height="1031" + inkscape:window-x="0" + inkscape:window-y="25" + inkscape:window-maximized="1" + inkscape:pagecheckerboard="true" + units="mm" + inkscape:bbox-paths="true" + inkscape:bbox-nodes="true" + inkscape:snap-bbox-edge-midpoints="true" + inkscape:snap-bbox-midpoints="true" + inkscape:object-paths="true" + inkscape:snap-intersection-paths="true" + inkscape:snap-smooth-nodes="true" + inkscape:snap-midpoints="true" + inkscape:snap-object-midpoints="true" + inkscape:snap-center="true" + inkscape:snap-text-baseline="true" + inkscape:snap-page="true" + showborder="true"> + <sodipodi:guide + orientation="0,1" + position="13.637059,643.40404" + id="guide3788" + inkscape:locked="false" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="0,1" + id="guide1099" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="1,0" + id="guide1101" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="233.588,370" + orientation="1,0" + id="guide1107" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="144.36496,311.42857" + orientation="1,0" + id="guide1109" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="-77.142857,144.36496" + orientation="0,1" + id="guide1111" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="5.000315,233.58779" + orientation="0,1" + id="guide1113" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="-305.71429,497.85714" + orientation="0,1" + id="guide822" + inkscape:locked="false" /> + </sodipodi:namedview> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-167.28122,-322.85977)"> + <path + style="fill:none;stroke:#c8c8c8;stroke-width:23.83593559;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow2Mend)" + d="M 179.3116,688.76623 C 229.41562,638.77017 279.51884,588.77492 329.62126,538.78045" + id="path904" + inkscape:connector-curvature="0" + inkscape:path-effect="#path-effect906" + inkscape:original-d="M 179.3116,688.76623 C 229.41562,638.77017 279.51883,588.77491 329.62126,538.78045" /> + <path + style="fill:none;stroke:#c8c8c8;stroke-width:23.83593559;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow2Mend-3)" + d="M 533.22005,334.92184 C 483.11603,384.9179 433.01281,434.91315 382.91039,484.90762" + id="path904-5" + inkscape:connector-curvature="0" + inkscape:path-effect="#path-effect906-7" + inkscape:original-d="M 533.22005,334.92184 C 483.11603,384.9179 433.01282,434.91316 382.91039,484.90762" /> + </g> +</svg> diff --git a/tagit/assets/icons/scalable/browser/zoom_out.svg b/tagit/assets/icons/scalable/browser/zoom_out.svg new file mode 100644 index 0000000..3d45b40 --- /dev/null +++ b/tagit/assets/icons/scalable/browser/zoom_out.svg @@ -0,0 +1,241 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="100mm" + height="100mm" + id="svg2" + version="1.1" + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="zoom_out.svg" + inkscape:export-filename="../../kivy/browser/zoom_out.png" + inkscape:export-xdpi="7.6199999" + inkscape:export-ydpi="7.6199999"> + <defs + id="defs4"> + <marker + inkscape:stockid="Arrow2Mend" + orient="auto" + refY="0.0" + refX="0.0" + id="Arrow2Mend" + style="overflow:visible;" + inkscape:isstock="true"> + <path + id="path935" + style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#c8c8c8;stroke-opacity:1;fill:#c8c8c8;fill-opacity:1" + d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z " + transform="scale(0.6) rotate(180) translate(0,0)" /> + </marker> + <marker + inkscape:stockid="Arrow2Lend" + orient="auto" + refY="0.0" + refX="0.0" + id="Arrow2Lend" + style="overflow:visible;" + inkscape:isstock="true"> + <path + id="path929" + style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1" + d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z " + transform="scale(1.1) rotate(180) translate(1,0)" /> + </marker> + <marker + inkscape:stockid="Arrow1Send" + orient="auto" + refY="0.0" + refX="0.0" + id="Arrow1Send" + style="overflow:visible;" + inkscape:isstock="true"> + <path + id="path923" + d="M 0.0,0.0 L 5.0,-5.0 L -12.5,0.0 L 5.0,5.0 L 0.0,0.0 z " + style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt;stroke-opacity:1;fill:#000000;fill-opacity:1" + transform="scale(0.2) rotate(180) translate(6,0)" /> + </marker> + <marker + inkscape:stockid="Arrow1Mend" + orient="auto" + refY="0.0" + refX="0.0" + id="Arrow1Mend" + style="overflow:visible;" + inkscape:isstock="true"> + <path + id="path917" + d="M 0.0,0.0 L 5.0,-5.0 L -12.5,0.0 L 5.0,5.0 L 0.0,0.0 z " + style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt;stroke-opacity:1;fill:#000000;fill-opacity:1" + transform="scale(0.4) rotate(180) translate(10,0)" /> + </marker> + <inkscape:path-effect + effect="bspline" + id="path-effect906" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + <marker + inkscape:stockid="Arrow2Mend" + orient="auto" + refY="0" + refX="0" + id="Arrow2Mend-3" + style="overflow:visible" + inkscape:isstock="true"> + <path + inkscape:connector-curvature="0" + id="path935-6" + style="fill:#c8c8c8;fill-opacity:1;fill-rule:evenodd;stroke:#c8c8c8;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1" + d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z" + transform="scale(-0.6)" /> + </marker> + <inkscape:path-effect + effect="bspline" + id="path-effect906-7" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + </defs> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="1.979899" + inkscape:cx="183.64532" + inkscape:cy="182.16579" + inkscape:document-units="mm" + inkscape:current-layer="layer1" + showgrid="false" + showguides="true" + inkscape:guide-bbox="true" + inkscape:snap-global="true" + inkscape:snap-bbox="true" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:window-width="1920" + inkscape:window-height="1031" + inkscape:window-x="0" + inkscape:window-y="25" + inkscape:window-maximized="1" + inkscape:pagecheckerboard="true" + units="mm" + inkscape:bbox-paths="true" + inkscape:bbox-nodes="true" + inkscape:snap-bbox-edge-midpoints="true" + inkscape:snap-bbox-midpoints="true" + inkscape:object-paths="true" + inkscape:snap-intersection-paths="true" + inkscape:snap-smooth-nodes="true" + inkscape:snap-midpoints="true" + inkscape:snap-object-midpoints="true" + inkscape:snap-center="true" + inkscape:snap-text-baseline="true" + inkscape:snap-page="true" + showborder="true"> + <sodipodi:guide + orientation="0,1" + position="13.637059,643.40404" + id="guide3788" + inkscape:locked="false" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="0,1" + id="guide1099" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="1,0" + id="guide1101" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="233.588,370" + orientation="1,0" + id="guide1107" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="144.36496,311.42857" + orientation="1,0" + id="guide1109" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="-77.142857,144.36496" + orientation="0,1" + id="guide1111" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="5.000315,233.58779" + orientation="0,1" + id="guide1113" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="-305.71429,497.85714" + orientation="0,1" + id="guide822" + inkscape:locked="false" /> + </sodipodi:namedview> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-167.28122,-322.85977)"> + <path + style="fill:none;stroke:#c8c8c8;stroke-width:23.83593559;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow2Mend)" + d="M 368.29607,499.79793 C 418.40009,449.80187 468.50331,399.80662 518.60573,349.81215" + id="path904" + inkscape:connector-curvature="0" + inkscape:path-effect="#path-effect906" + inkscape:original-d="M 368.29607,499.79793 C 418.40009,449.80187 468.5033,399.80661 518.60573,349.81215" /> + <path + style="fill:none;stroke:#c8c8c8;stroke-width:23.83593559;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow2Mend-3)" + d="M 344.24368,523.89823 C 294.13966,573.89429 244.03644,623.88955 193.93401,673.88401" + id="path904-5" + inkscape:connector-curvature="0" + inkscape:path-effect="#path-effect906-7" + inkscape:original-d="M 344.24368,523.89823 C 294.13966,573.89429 244.03644,623.88955 193.93401,673.88401" /> + </g> +</svg> diff --git a/tagit/assets/icons/scalable/filter/add.svg b/tagit/assets/icons/scalable/filter/add.svg new file mode 100644 index 0000000..a544053 --- /dev/null +++ b/tagit/assets/icons/scalable/filter/add.svg @@ -0,0 +1,102 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="100mm" + height="100mm" + id="svg2" + version="1.1" + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="add.svg" + inkscape:export-filename="../../kivy/filter/add.png" + inkscape:export-xdpi="7.6199999" + inkscape:export-ydpi="7.6199999"> + <defs + id="defs4" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="1.979899" + inkscape:cx="106.95662" + inkscape:cy="169.42756" + inkscape:document-units="mm" + inkscape:current-layer="layer1" + showgrid="false" + showguides="true" + inkscape:guide-bbox="true" + inkscape:snap-global="true" + inkscape:snap-bbox="true" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:window-width="1920" + inkscape:window-height="1151" + inkscape:window-x="0" + inkscape:window-y="25" + inkscape:window-maximized="1" + inkscape:pagecheckerboard="true" + units="mm" + inkscape:bbox-paths="true" + inkscape:bbox-nodes="true" + inkscape:snap-bbox-edge-midpoints="true" + inkscape:snap-bbox-midpoints="true" + inkscape:object-paths="true" + inkscape:snap-intersection-paths="true" + inkscape:snap-smooth-nodes="true" + inkscape:snap-midpoints="true" + inkscape:snap-object-midpoints="true" + inkscape:snap-center="true" + inkscape:snap-text-baseline="true" + inkscape:snap-page="true" + inkscape:lockguides="false"> + <sodipodi:guide + position="188.97638,188.97638" + orientation="0,1" + id="guide1099" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="1,0" + id="guide1101" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + </sodipodi:namedview> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-167.28122,-322.85977)"> + <path + style="fill:none;stroke:#c8c8c8;stroke-width:29.81036186;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="m 182.82699,337.92736 123.49426,144.97182 v 203.0089 L 406.37348,630.06527 V 482.89684 L 529.86773,337.92736 H 182.82699" + id="path851" + inkscape:connector-curvature="0" /> + </g> +</svg> diff --git a/tagit/assets/icons/scalable/filter/address.svg b/tagit/assets/icons/scalable/filter/address.svg new file mode 100644 index 0000000..de30faa --- /dev/null +++ b/tagit/assets/icons/scalable/filter/address.svg @@ -0,0 +1,124 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="100mm" + height="100mm" + id="svg2" + version="1.1" + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="address.svg" + inkscape:export-filename="../../kivy/filter/address.png" + inkscape:export-xdpi="7.6199999" + inkscape:export-ydpi="7.6199999"> + <defs + id="defs4" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="0.9899495" + inkscape:cx="8.0179354" + inkscape:cy="311.11216" + inkscape:document-units="mm" + inkscape:current-layer="layer1" + showgrid="false" + showguides="true" + inkscape:guide-bbox="true" + inkscape:snap-global="true" + inkscape:snap-bbox="true" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:window-width="1920" + inkscape:window-height="1151" + inkscape:window-x="0" + inkscape:window-y="25" + inkscape:window-maximized="1" + inkscape:pagecheckerboard="true" + units="mm" + inkscape:bbox-paths="true" + inkscape:bbox-nodes="true" + inkscape:snap-bbox-edge-midpoints="true" + inkscape:snap-bbox-midpoints="true" + inkscape:object-paths="true" + inkscape:snap-intersection-paths="true" + inkscape:snap-smooth-nodes="true" + inkscape:snap-midpoints="true" + inkscape:snap-object-midpoints="true" + inkscape:snap-center="true" + inkscape:snap-text-baseline="true" + inkscape:snap-page="true" + inkscape:lockguides="false"> + <sodipodi:guide + position="188.97638,188.97638" + orientation="0,1" + id="guide1099" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="1,0" + id="guide1101" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + </sodipodi:namedview> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title /> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-167.28122,-322.85977)"> + <rect + style="opacity:1;fill:none;fill-opacity:1;stroke:#c8c8c8;stroke-width:17.85590553;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" + id="rect817" + width="360.09683" + height="220.61481" + x="176.20917" + y="401.52875" /> + <path + style="fill:none;stroke:#c8c8c8;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="m 364.4772,427.97815 v 173.777" + id="path833" + inkscape:connector-curvature="0" /> + <text + xml:space="preserve" + style="font-style:normal;font-weight:normal;font-size:52.13865662px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.30346632" + x="195.92052" + y="571.07178" + id="text837"><tspan + sodipodi:role="line" + id="tspan835" + x="195.92052" + y="571.07178" + style="font-size:173.79553223px;fill:#ffffff;fill-opacity:1;stroke-width:1.30346632"><tspan + style="fill:#e6e6e6;fill-opacity:1;stroke-width:1.30346632" + id="tspan843">fo</tspan><tspan + style="fill:#828282;fill-opacity:1;stroke-width:1.30346632" + id="tspan841">o</tspan></tspan></text> + </g> +</svg> diff --git a/tagit/assets/icons/scalable/filter/go_back.svg b/tagit/assets/icons/scalable/filter/go_back.svg new file mode 100644 index 0000000..a972c87 --- /dev/null +++ b/tagit/assets/icons/scalable/filter/go_back.svg @@ -0,0 +1,158 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="100mm" + height="100mm" + id="svg2" + version="1.1" + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="go_back.svg" + inkscape:export-filename="../../kivy/filter/go_back.png" + inkscape:export-xdpi="7.6199999" + inkscape:export-ydpi="7.6199999"> + <defs + id="defs4" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="2.8" + inkscape:cx="265.05952" + inkscape:cy="147.00905" + inkscape:document-units="mm" + inkscape:current-layer="layer1" + showgrid="false" + showguides="true" + inkscape:guide-bbox="true" + inkscape:snap-global="true" + inkscape:snap-bbox="true" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:window-width="1920" + inkscape:window-height="1031" + inkscape:window-x="0" + inkscape:window-y="25" + inkscape:window-maximized="1" + inkscape:pagecheckerboard="true" + units="mm" + inkscape:bbox-paths="true" + inkscape:bbox-nodes="true" + inkscape:snap-bbox-edge-midpoints="true" + inkscape:snap-bbox-midpoints="true" + inkscape:object-paths="true" + inkscape:snap-intersection-paths="true" + inkscape:snap-smooth-nodes="true" + inkscape:snap-midpoints="true" + inkscape:snap-object-midpoints="true" + inkscape:snap-center="true" + inkscape:snap-text-baseline="true" + inkscape:snap-page="true" + inkscape:lockguides="false"> + <sodipodi:guide + orientation="0,1" + position="13.637059,643.40404" + id="guide3788" + inkscape:locked="false" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="0,1" + id="guide1099" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="1,0" + id="guide1101" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="233.588,370" + orientation="1,0" + id="guide1107" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="144.36496,311.42857" + orientation="1,0" + id="guide1109" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="-77.142857,144.36496" + orientation="0,1" + id="guide1111" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="5.000315,233.58779" + orientation="0,1" + id="guide1113" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + </sodipodi:namedview> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-167.28122,-322.85977)"> + <g + id="g883" + transform="matrix(-1,0,0,1,711.07945,-5.2302858e-6)"> + <path + inkscape:transform-center-y="-2.0036887e-06" + inkscape:transform-center-x="-64.791235" + d="m 543.79823,511.83615 -107.19675,61.89008 -107.19675,61.89007 0,-123.78015 0,-123.78014 107.19676,61.89007 z" + inkscape:randomized="0" + inkscape:rounded="0" + inkscape:flatsided="false" + sodipodi:arg2="1.0471976" + sodipodi:arg1="0" + sodipodi:r2="71.4645" + sodipodi:r1="142.929" + sodipodi:cy="511.83615" + sodipodi:cx="400.86923" + sodipodi:sides="3" + id="path2995" + style="fill:#c8c8c8;fill-opacity:1;stroke:none;stroke-width:1.56328595;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + sodipodi:type="star" /> + <rect + y="462.98917" + x="165.84547" + height="97.693985" + width="231.62929" + id="rect4750" + style="opacity:1;fill:#c8c8c8;fill-opacity:1;stroke:none;stroke-width:2.08440304;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:4.16880612, 2.08440306;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" /> + </g> + </g> +</svg> diff --git a/tagit/assets/icons/scalable/filter/go_forth.svg b/tagit/assets/icons/scalable/filter/go_forth.svg new file mode 100644 index 0000000..f4a246d --- /dev/null +++ b/tagit/assets/icons/scalable/filter/go_forth.svg @@ -0,0 +1,154 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="100mm" + height="100mm" + id="svg2" + version="1.1" + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="go_forth.svg" + inkscape:export-filename="../../kivy/filter/go_forth.png" + inkscape:export-xdpi="7.6199999" + inkscape:export-ydpi="7.6199999"> + <defs + id="defs4" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="1.4" + inkscape:cx="-59.38416" + inkscape:cy="86.716973" + inkscape:document-units="mm" + inkscape:current-layer="layer1" + showgrid="false" + showguides="true" + inkscape:guide-bbox="true" + inkscape:snap-global="true" + inkscape:snap-bbox="true" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:window-width="1920" + inkscape:window-height="1031" + inkscape:window-x="0" + inkscape:window-y="25" + inkscape:window-maximized="1" + inkscape:pagecheckerboard="true" + units="mm" + inkscape:bbox-paths="true" + inkscape:bbox-nodes="true" + inkscape:snap-bbox-edge-midpoints="true" + inkscape:snap-bbox-midpoints="true" + inkscape:object-paths="true" + inkscape:snap-intersection-paths="true" + inkscape:snap-smooth-nodes="true" + inkscape:snap-midpoints="true" + inkscape:snap-object-midpoints="true" + inkscape:snap-center="true" + inkscape:snap-text-baseline="true" + inkscape:snap-page="true" + inkscape:lockguides="false"> + <sodipodi:guide + orientation="0,1" + position="13.637059,643.40404" + id="guide3788" + inkscape:locked="false" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="0,1" + id="guide1099" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="1,0" + id="guide1101" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="233.588,370" + orientation="1,0" + id="guide1107" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="144.36496,311.42857" + orientation="1,0" + id="guide1109" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="-77.142857,144.36496" + orientation="0,1" + id="guide1111" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="5.000315,233.58779" + orientation="0,1" + id="guide1113" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + </sodipodi:namedview> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title /> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-167.28122,-322.85977)"> + <path + sodipodi:type="star" + style="fill:#c8c8c8;fill-opacity:1;stroke:none;stroke-width:1.56328595;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="path2995" + sodipodi:sides="3" + sodipodi:cx="400.86923" + sodipodi:cy="511.83615" + sodipodi:r1="142.929" + sodipodi:r2="71.4645" + sodipodi:arg1="0" + sodipodi:arg2="1.0471976" + inkscape:flatsided="false" + inkscape:rounded="0" + inkscape:randomized="0" + d="m 543.79823,511.83615 -107.19675,61.89008 -107.19675,61.89007 0,-123.78015 0,-123.78014 107.19676,61.89007 z" + inkscape:transform-center-x="-64.791235" + inkscape:transform-center-y="-2.0036887e-06" /> + <rect + style="opacity:1;fill:#c8c8c8;fill-opacity:1;stroke:none;stroke-width:2.07793283;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:4.15586594, 2.07793297;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" + id="rect4750" + width="230.19354" + height="97.693985" + x="167.28122" + y="462.98917" /> + </g> +</svg> diff --git a/tagit/assets/icons/scalable/filter/shingles.svg b/tagit/assets/icons/scalable/filter/shingles.svg new file mode 100644 index 0000000..a07b6ea --- /dev/null +++ b/tagit/assets/icons/scalable/filter/shingles.svg @@ -0,0 +1,217 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="100mm" + height="100mm" + id="svg2" + version="1.1" + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="shingles.svg" + inkscape:export-filename="../../kivy/filter/shingles.png" + inkscape:export-xdpi="7.6199999" + inkscape:export-ydpi="7.6199999"> + <defs + id="defs4" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="0.98994949" + inkscape:cx="61.463033" + inkscape:cy="211.38181" + inkscape:document-units="mm" + inkscape:current-layer="layer1" + showgrid="false" + showguides="true" + inkscape:guide-bbox="true" + inkscape:snap-global="true" + inkscape:snap-bbox="true" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:window-width="1920" + inkscape:window-height="1151" + inkscape:window-x="0" + inkscape:window-y="25" + inkscape:window-maximized="1" + inkscape:pagecheckerboard="true" + units="mm" + inkscape:bbox-paths="true" + inkscape:bbox-nodes="true" + inkscape:snap-bbox-edge-midpoints="true" + inkscape:snap-bbox-midpoints="true" + inkscape:object-paths="true" + inkscape:snap-intersection-paths="true" + inkscape:snap-smooth-nodes="true" + inkscape:snap-midpoints="true" + inkscape:snap-object-midpoints="true" + inkscape:snap-center="true" + inkscape:snap-text-baseline="true" + inkscape:snap-page="true" + inkscape:lockguides="false"> + <sodipodi:guide + position="188.97638,188.97638" + orientation="0,1" + id="guide1099" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="1,0" + id="guide1101" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + </sodipodi:namedview> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title /> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-167.28122,-322.85977)"> + <path + style="opacity:1;fill:none;fill-opacity:1;stroke:#c8c8c8;stroke-width:15.81019115;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" + d="m 762.61553,105.82518 v 80.22827 c -0.003,0.0639 -0.007,0.12764 -0.0105,0.19146 -1.8e-4,30.94779 26.90552,56.03593 60.09519,56.03559 33.18967,3.4e-4 60.09537,-25.0878 60.0952,-56.03559 l 0.52701,-80.41973 z" + id="rect888-6-2-0-0" + inkscape:connector-curvature="0" + sodipodi:nodetypes="ccccccc" /> + <g + id="g1084" + transform="translate(139.39556,1.9249742)"> + <g + transform="matrix(1.0298281,0,0,1.0298281,-320.57448,-232.49785)" + id="g1010-6" /> + </g> + <g + id="g1077" + transform="translate(-135.36045,-275.77165)"> + <g + transform="matrix(1.0298281,0,0,1.0298281,-106.16686,-28.823189)" + id="g1066" /> + </g> + <g + id="g1077-9-8" + transform="translate(-235.90303,68.096044)"> + <g + transform="matrix(1.0298281,0,0,1.0298281,-106.16686,-28.823189)" + id="g1066-2-9" /> + </g> + <g + id="g1370" + transform="matrix(1.2737193,0,0,1.2737193,-97.514563,-83.896439)"> + <path + sodipodi:nodetypes="cccccc" + inkscape:connector-curvature="0" + id="rect888-6-2-3" + d="m 175.74036,319.44885 v 40.0184 c -0.003,0.0639 -0.007,0.12764 -0.0105,0.19146 -1.8e-4,30.94779 26.90552,56.03593 60.09519,56.03559 33.18967,3.4e-4 60.09537,-25.0878 60.0952,-56.03559 l 0.52701,-40.20986" + style="opacity:1;fill:none;fill-opacity:1;stroke:#c8c8c8;stroke-width:15.81019115;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" /> + <path + sodipodi:nodetypes="cccccc" + inkscape:connector-curvature="0" + id="rect888-6-2-3-2" + d="m 296.44726,319.44885 v 40.0184 c -0.003,0.0639 -0.007,0.12764 -0.0105,0.19146 -1.8e-4,30.94779 26.90552,56.03593 60.09519,56.03559 33.18967,3.4e-4 60.09537,-25.0878 60.0952,-56.03559 l 0.52701,-40.20986" + style="opacity:1;fill:none;fill-opacity:1;stroke:#c8c8c8;stroke-width:15.81019115;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" /> + <path + sodipodi:nodetypes="cccccc" + inkscape:connector-curvature="0" + id="rect888-6-2-3-9" + d="m 417.15416,319.44885 v 40.0184 c -0.003,0.0639 -0.007,0.12764 -0.0105,0.19146 -1.8e-4,30.94779 26.90552,56.03593 60.09519,56.03559 33.18967,3.4e-4 60.09537,-25.0878 60.09517,-56.03559 l 0.527,-40.20986" + style="opacity:1;fill:none;fill-opacity:1;stroke:#c8c8c8;stroke-width:15.81019115;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" /> + <path + sodipodi:nodetypes="cccccc" + inkscape:connector-curvature="0" + id="rect888-6-2-3-3" + d="m 235.82505,415.6943 v 40.0184 c -0.003,0.0639 -0.007,0.12764 -0.0105,0.19146 -1.8e-4,30.94779 26.90552,56.03593 60.09519,56.03559 33.18967,3.4e-4 60.09537,-25.0878 60.0952,-56.03559 l 0.52701,-40.20986" + style="opacity:1;fill:none;fill-opacity:1;stroke:#c8c8c8;stroke-width:15.81019115;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" /> + <path + sodipodi:nodetypes="cccccc" + inkscape:connector-curvature="0" + id="rect888-6-2-3-1" + d="m 356.53195,415.6943 v 40.0184 c -0.003,0.0639 -0.007,0.12764 -0.0105,0.19146 -1.8e-4,30.94779 26.90552,56.03593 60.09519,56.03559 33.18967,3.4e-4 60.09537,-25.0878 60.0952,-56.03559 l 0.52701,-40.20986" + style="opacity:1;fill:none;fill-opacity:1;stroke:#c8c8c8;stroke-width:15.81019115;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" /> + <path + sodipodi:nodetypes="cccccc" + inkscape:connector-curvature="0" + id="rect888-6-2-3-94" + d="m 477.23885,415.6943 v 40.0184 c -0.003,0.0639 -0.007,0.12764 -0.0105,0.19146 -1.8e-4,30.94779 26.90552,56.03593 60.09517,56.03559 33.1897,3.4e-4 60.0954,-25.0878 60.0952,-56.03559 l 0.527,-40.20986" + style="opacity:1;fill:none;fill-opacity:1;stroke:#c8c8c8;stroke-width:15.81019115;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" /> + <path + sodipodi:nodetypes="cccccc" + inkscape:connector-curvature="0" + id="rect888-6-2-3-7" + d="m 115.11815,415.6943 v 40.0184 c -0.003,0.0639 -0.007,0.12764 -0.0105,0.19146 -1.8e-4,30.94779 26.90552,56.03593 60.09519,56.03559 33.18967,3.4e-4 60.09537,-25.0878 60.0952,-56.03559 l 0.52701,-40.20986" + style="opacity:1;fill:none;fill-opacity:1;stroke:#c8c8c8;stroke-width:15.81019115;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" /> + <path + sodipodi:nodetypes="cccccc" + inkscape:connector-curvature="0" + id="rect888-6-2-3-8" + d="m 175.20284,511.93975 v 40.0184 c -0.003,0.0639 -0.007,0.12764 -0.0105,0.19146 -1.8e-4,30.94779 26.90552,56.03593 60.09519,56.03559 33.18967,3.4e-4 60.09537,-25.0878 60.0952,-56.03559 l 0.52701,-40.20986" + style="opacity:1;fill:none;fill-opacity:1;stroke:#c8c8c8;stroke-width:15.81019115;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" /> + <path + sodipodi:nodetypes="cccccc" + inkscape:connector-curvature="0" + id="rect888-6-2-3-4" + d="m 295.90974,511.93975 v 40.0184 c -0.003,0.0639 -0.007,0.12764 -0.0105,0.19146 -1.8e-4,30.94779 26.90552,56.03593 60.09519,56.03559 33.18967,3.4e-4 60.09537,-25.0878 60.0952,-56.03559 l 0.52701,-40.20986" + style="opacity:1;fill:none;fill-opacity:1;stroke:#c8c8c8;stroke-width:15.81019115;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" /> + <path + sodipodi:nodetypes="cccccc" + inkscape:connector-curvature="0" + id="rect888-6-2-3-5" + d="m 416.35313,512.03548 v 40.0184 c -0.003,0.0639 -0.007,0.12764 -0.0105,0.19146 -1.8e-4,30.94779 26.90552,56.03593 60.09519,56.03559 33.18966,3.4e-4 60.09539,-25.0878 60.09519,-56.03559 l 0.527,-40.20986" + style="opacity:1;fill:none;fill-opacity:1;stroke:#c8c8c8;stroke-width:15.81019115;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" /> + <path + sodipodi:nodetypes="cccccc" + inkscape:connector-curvature="0" + id="rect888-6-2-3-0" + d="m 114.58063,608.1852 v 40.0184 c -0.003,0.0639 -0.007,0.12764 -0.0105,0.19146 -1.8e-4,30.94779 26.90552,56.03593 60.09519,56.03559 33.18967,3.4e-4 60.09537,-25.0878 60.0952,-56.03559 l 0.52701,-40.20986" + style="opacity:1;fill:none;fill-opacity:1;stroke:#c8c8c8;stroke-width:15.81019115;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" /> + <path + sodipodi:nodetypes="cccccc" + inkscape:connector-curvature="0" + id="rect888-6-2-3-36" + d="m 235.02402,608.28093 v 40.0184 c -0.003,0.0639 -0.007,0.12764 -0.0105,0.19146 -1.8e-4,30.94779 26.90553,56.03593 60.09519,56.03559 33.18967,3.4e-4 60.09538,-25.0878 60.0952,-56.03559 l 0.52701,-40.20986" + style="opacity:1;fill:none;fill-opacity:1;stroke:#c8c8c8;stroke-width:15.81019115;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" /> + <path + sodipodi:nodetypes="cccccc" + inkscape:connector-curvature="0" + id="rect888-6-2-3-10" + d="m 355.73092,608.28093 v 40.0184 c -0.003,0.0639 -0.007,0.12764 -0.0105,0.19146 -1.8e-4,30.94779 26.90552,56.03593 60.09519,56.03559 33.18967,3.4e-4 60.09537,-25.0878 60.0952,-56.03559 l 0.52701,-40.20986" + style="opacity:1;fill:none;fill-opacity:1;stroke:#c8c8c8;stroke-width:15.81019115;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" /> + <path + sodipodi:nodetypes="cccccc" + inkscape:connector-curvature="0" + id="rect888-6-2-3-6" + d="m 476.43782,608.28093 v 40.0184 c -0.003,0.0639 -0.007,0.12764 -0.0105,0.19146 -1.8e-4,30.94779 26.90552,56.03593 60.0952,56.03559 33.1897,3.4e-4 60.0954,-25.0878 60.0952,-56.03559 l 0.527,-40.20986" + style="opacity:1;fill:none;fill-opacity:1;stroke:#c8c8c8;stroke-width:15.81019115;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" /> + </g> + <path + style="fill:none;stroke:#c8c8c8;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="M 126.32932,322.99173 H 587.5694" + id="path1391" + inkscape:connector-curvature="0" /> + </g> +</svg> diff --git a/tagit/assets/icons/scalable/grouping/add.svg b/tagit/assets/icons/scalable/grouping/add.svg new file mode 100644 index 0000000..3069273 --- /dev/null +++ b/tagit/assets/icons/scalable/grouping/add.svg @@ -0,0 +1,147 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="100mm" + height="100mm" + id="svg2" + version="1.1" + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="add.svg" + inkscape:export-filename="../../kivy/grouping/create.png" + inkscape:export-xdpi="7.6199999" + inkscape:export-ydpi="7.6199999"> + <defs + id="defs4" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="1.979899" + inkscape:cx="41.873283" + inkscape:cy="194.67797" + inkscape:document-units="mm" + inkscape:current-layer="layer1" + showgrid="false" + showguides="true" + inkscape:guide-bbox="true" + inkscape:snap-global="true" + inkscape:snap-bbox="true" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:window-width="1920" + inkscape:window-height="1151" + inkscape:window-x="0" + inkscape:window-y="25" + inkscape:window-maximized="1" + inkscape:pagecheckerboard="true" + units="mm" + inkscape:bbox-paths="true" + inkscape:bbox-nodes="true" + inkscape:snap-bbox-edge-midpoints="true" + inkscape:snap-bbox-midpoints="true" + inkscape:object-paths="true" + inkscape:snap-intersection-paths="true" + inkscape:snap-smooth-nodes="true" + inkscape:snap-midpoints="true" + inkscape:snap-object-midpoints="true" + inkscape:snap-center="true" + inkscape:snap-text-baseline="true" + inkscape:snap-page="true" + inkscape:lockguides="false"> + <sodipodi:guide + orientation="0,1" + position="13.637059,643.40404" + id="guide3788" + inkscape:locked="false" /> + <sodipodi:guide + position="-70.205601,188.97638" + orientation="0,1" + id="guide1099" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="188.97638,171.22086" + orientation="1,0" + id="guide1101" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + </sodipodi:namedview> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-167.28122,-322.85977)"> + <ellipse + style="opacity:1;fill:none;fill-opacity:1;stroke:#c8c8c8;stroke-width:17.38639641;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" + id="path889" + cx="356.2576" + cy="511.83615" + rx="180.28317" + ry="180.28319" /> + <ellipse + style="opacity:1;fill:#c8c8c8;fill-opacity:1;stroke:none;stroke-width:38.57448196;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:77.14896078, 38.57448039;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" + id="path891-5-0" + cx="355.72531" + cy="-422.3046" + transform="scale(1,-1)" + rx="54.830173" + ry="55.384014" /> + <ellipse + style="opacity:1;fill:none;fill-opacity:1;stroke:#c8c8c8;stroke-width:9.16811562;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" + id="path891-5-0-6" + cx="435.48053" + cy="-561.84009" + transform="scale(1,-1)" + rx="50.245941" + ry="50.799942" /> + <ellipse + style="opacity:1;fill:#c8c8c8;fill-opacity:1;stroke:none;stroke-width:38.57448196;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:77.14896078, 38.57448039;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" + id="path891-5-0-0" + cx="275.97003" + cy="-561.84003" + transform="scale(1,-1)" + rx="54.830173" + ry="55.384014" /> + <g + id="g841" + transform="matrix(0.6024096,0,0,0.6024096,433.43894,320.09479)"> + <path + inkscape:connector-curvature="0" + id="path822" + d="M -58.99295,400.79722 H 66.77104" + style="fill:none;stroke:#c8c8c8;stroke-width:25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + <path + inkscape:connector-curvature="0" + id="path822-3" + d="M 3.389045,338.41523 V 464.17921" + style="fill:none;stroke:#c8c8c8;stroke-width:25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + </g> + </g> +</svg> diff --git a/tagit/assets/icons/scalable/grouping/create.svg b/tagit/assets/icons/scalable/grouping/create.svg new file mode 100644 index 0000000..549f27e --- /dev/null +++ b/tagit/assets/icons/scalable/grouping/create.svg @@ -0,0 +1,135 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="100mm" + height="100mm" + id="svg2" + version="1.1" + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="create_group.svg" + inkscape:export-filename="../../kivy/grouping/create.png" + inkscape:export-xdpi="7.6199999" + inkscape:export-ydpi="7.6199999"> + <defs + id="defs4" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="1.979899" + inkscape:cx="196.9317" + inkscape:cy="198.71859" + inkscape:document-units="mm" + inkscape:current-layer="layer1" + showgrid="false" + showguides="true" + inkscape:guide-bbox="true" + inkscape:snap-global="true" + inkscape:snap-bbox="true" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:window-width="1920" + inkscape:window-height="1031" + inkscape:window-x="0" + inkscape:window-y="25" + inkscape:window-maximized="1" + inkscape:pagecheckerboard="true" + units="mm" + inkscape:bbox-paths="true" + inkscape:bbox-nodes="true" + inkscape:snap-bbox-edge-midpoints="true" + inkscape:snap-bbox-midpoints="true" + inkscape:object-paths="true" + inkscape:snap-intersection-paths="true" + inkscape:snap-smooth-nodes="true" + inkscape:snap-midpoints="true" + inkscape:snap-object-midpoints="true" + inkscape:snap-center="true" + inkscape:snap-text-baseline="true" + inkscape:snap-page="true" + inkscape:lockguides="false"> + <sodipodi:guide + orientation="0,1" + position="13.637059,643.40404" + id="guide3788" + inkscape:locked="false" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="0,1" + id="guide1099" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="1,0" + id="guide1101" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + </sodipodi:namedview> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title /> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-167.28122,-322.85977)"> + <ellipse + style="opacity:1;fill:none;fill-opacity:1;stroke:#c8c8c8;stroke-width:17.38639641;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" + id="path889" + cx="356.2576" + cy="511.83615" + rx="180.28317" + ry="180.28319" /> + <g + id="g1097" + transform="matrix(0.37263226,0,0,0.37639623,804.3104,348.85233)" + style="fill:#c8c8c8;fill-opacity:1"> + <circle + transform="scale(1,-1)" + r="147.14285" + cy="-195.14612" + cx="-1203.8279" + id="path891-5-0" + style="opacity:1;fill:#c8c8c8;fill-opacity:1;stroke:none;stroke-width:103;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:206.0000011, 103.00000055000000998;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" /> + <circle + transform="scale(1,-1)" + r="147.14285" + cy="-565.86047" + cx="-989.7959" + id="path891-5-0-6" + style="opacity:1;fill:#c8c8c8;fill-opacity:1;stroke:none;stroke-width:103;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:206.0000011, 103.00000055000000998;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" /> + <circle + transform="scale(1,-1)" + r="147.14285" + cy="-565.86041" + cx="-1417.86" + id="path891-5-0-0" + style="opacity:1;fill:#c8c8c8;fill-opacity:1;stroke:none;stroke-width:103;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:206.0000011, 103.00000055000000998;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" /> + </g> + </g> +</svg> diff --git a/tagit/assets/icons/scalable/grouping/group.svg b/tagit/assets/icons/scalable/grouping/group.svg new file mode 100644 index 0000000..975f413 --- /dev/null +++ b/tagit/assets/icons/scalable/grouping/group.svg @@ -0,0 +1,127 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="100mm" + height="100mm" + id="svg2" + version="1.1" + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="group.svg" + inkscape:export-filename="../../kivy/grouping/group.png" + inkscape:export-xdpi="7.6199999" + inkscape:export-ydpi="7.6199999"> + <defs + id="defs4" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="0.9899495" + inkscape:cx="298.903" + inkscape:cy="113.59541" + inkscape:document-units="mm" + inkscape:current-layer="layer1" + showgrid="false" + showguides="true" + inkscape:guide-bbox="true" + inkscape:snap-global="true" + inkscape:snap-bbox="true" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:window-width="1920" + inkscape:window-height="1031" + inkscape:window-x="0" + inkscape:window-y="25" + inkscape:window-maximized="1" + inkscape:pagecheckerboard="true" + units="mm" + inkscape:bbox-paths="true" + inkscape:bbox-nodes="true" + inkscape:snap-bbox-edge-midpoints="true" + inkscape:snap-bbox-midpoints="true" + inkscape:object-paths="true" + inkscape:snap-intersection-paths="true" + inkscape:snap-smooth-nodes="true" + inkscape:snap-midpoints="true" + inkscape:snap-object-midpoints="true" + inkscape:snap-center="true" + inkscape:snap-text-baseline="true" + inkscape:snap-page="true" + inkscape:lockguides="false"> + <sodipodi:guide + orientation="0,1" + position="13.637059,643.40404" + id="guide3788" + inkscape:locked="false" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="0,1" + id="guide1099" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="1,0" + id="guide1101" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + </sodipodi:namedview> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-167.28122,-322.85977)"> + <g + id="g895" + transform="translate(1.0798198,48.934962)"> + <rect + y="326.91025" + x="249.19569" + height="191.95821" + width="291.98779" + id="rect4221-6-2-3" + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#c8c8c8;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:8.10094929;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:23.10000038;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" /> + <rect + y="366.92203" + x="209.18385" + height="191.95819" + width="291.98776" + id="rect4221-6-2" + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#c8c8c8;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:8.10094929;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:23.10000038;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" /> + <rect + y="406.93393" + x="169.17207" + height="191.95819" + width="291.98776" + id="rect4221" + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#c8c8c8;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:8.10094929;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:23.10000038;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" /> + </g> + </g> +</svg> diff --git a/tagit/assets/icons/scalable/grouping/remove.svg b/tagit/assets/icons/scalable/grouping/remove.svg new file mode 100644 index 0000000..ebf8ed9 --- /dev/null +++ b/tagit/assets/icons/scalable/grouping/remove.svg @@ -0,0 +1,133 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="100mm" + height="100mm" + id="svg2" + version="1.1" + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="remove.svg" + inkscape:export-filename="../../kivy/grouping/create.png" + inkscape:export-xdpi="7.6199999" + inkscape:export-ydpi="7.6199999"> + <defs + id="defs4" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="1.979899" + inkscape:cx="41.873283" + inkscape:cy="194.67797" + inkscape:document-units="mm" + inkscape:current-layer="layer1" + showgrid="false" + showguides="true" + inkscape:guide-bbox="true" + inkscape:snap-global="true" + inkscape:snap-bbox="true" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:window-width="1920" + inkscape:window-height="1151" + inkscape:window-x="0" + inkscape:window-y="25" + inkscape:window-maximized="1" + inkscape:pagecheckerboard="true" + units="mm" + inkscape:bbox-paths="true" + inkscape:bbox-nodes="true" + inkscape:snap-bbox-edge-midpoints="true" + inkscape:snap-bbox-midpoints="true" + inkscape:object-paths="true" + inkscape:snap-intersection-paths="true" + inkscape:snap-smooth-nodes="true" + inkscape:snap-midpoints="true" + inkscape:snap-object-midpoints="true" + inkscape:snap-center="true" + inkscape:snap-text-baseline="true" + inkscape:snap-page="true" + inkscape:lockguides="false"> + <sodipodi:guide + orientation="0,1" + position="13.637059,643.40404" + id="guide3788" + inkscape:locked="false" /> + <sodipodi:guide + position="-70.205601,188.97638" + orientation="0,1" + id="guide1099" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="188.97638,171.22086" + orientation="1,0" + id="guide1101" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + </sodipodi:namedview> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title /> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-167.28122,-322.85977)"> + <ellipse + style="opacity:1;fill:none;fill-opacity:1;stroke:#c8c8c8;stroke-width:17.38639641;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" + id="path889" + cx="356.2576" + cy="511.83615" + rx="180.28317" + ry="180.28319" /> + <ellipse + style="opacity:1;fill:#c8c8c8;fill-opacity:1;stroke:none;stroke-width:38.57448196;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:77.14896078, 38.57448039;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" + id="path891-5-0" + cx="355.72531" + cy="-422.3046" + transform="scale(1,-1)" + rx="54.830173" + ry="55.384014" /> + <ellipse + style="opacity:1;fill:none;fill-opacity:1;stroke:#ff9191;stroke-width:9.16811562;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:18.33623145, 18.33623144999999965;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" + id="path891-5-0-6" + cx="435.48053" + cy="-561.84009" + transform="scale(1,-1)" + rx="50.245941" + ry="50.799942" /> + <ellipse + style="opacity:1;fill:#c8c8c8;fill-opacity:1;stroke:none;stroke-width:38.57448196;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:77.14896078, 38.57448039;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" + id="path891-5-0-0" + cx="275.97003" + cy="-561.84003" + transform="scale(1,-1)" + rx="54.830173" + ry="55.384014" /> + </g> +</svg> diff --git a/tagit/assets/icons/scalable/grouping/represent.svg b/tagit/assets/icons/scalable/grouping/represent.svg new file mode 100644 index 0000000..c7b5850 --- /dev/null +++ b/tagit/assets/icons/scalable/grouping/represent.svg @@ -0,0 +1,133 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="100mm" + height="100mm" + id="svg2" + version="1.1" + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="represent.svg" + inkscape:export-filename="../../kivy/grouping/create.png" + inkscape:export-xdpi="7.6199999" + inkscape:export-ydpi="7.6199999"> + <defs + id="defs4" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="1.979899" + inkscape:cx="119.40249" + inkscape:cy="196.69828" + inkscape:document-units="mm" + inkscape:current-layer="layer1" + showgrid="false" + showguides="true" + inkscape:guide-bbox="true" + inkscape:snap-global="true" + inkscape:snap-bbox="true" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:window-width="1920" + inkscape:window-height="1151" + inkscape:window-x="0" + inkscape:window-y="25" + inkscape:window-maximized="1" + inkscape:pagecheckerboard="true" + units="mm" + inkscape:bbox-paths="true" + inkscape:bbox-nodes="true" + inkscape:snap-bbox-edge-midpoints="true" + inkscape:snap-bbox-midpoints="true" + inkscape:object-paths="true" + inkscape:snap-intersection-paths="true" + inkscape:snap-smooth-nodes="true" + inkscape:snap-midpoints="true" + inkscape:snap-object-midpoints="true" + inkscape:snap-center="true" + inkscape:snap-text-baseline="true" + inkscape:snap-page="true" + inkscape:lockguides="false"> + <sodipodi:guide + orientation="0,1" + position="13.637059,643.40404" + id="guide3788" + inkscape:locked="false" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="0,1" + id="guide1099" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="1,0" + id="guide1101" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + </sodipodi:namedview> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-167.28122,-322.85977)"> + <ellipse + style="opacity:1;fill:none;fill-opacity:1;stroke:#c8c8c8;stroke-width:17.38639641;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" + id="path889" + cx="356.2576" + cy="511.83615" + rx="180.28317" + ry="180.28319" /> + <ellipse + style="opacity:1;fill:#c8c8c8;fill-opacity:1;stroke:none;stroke-width:38.57448196;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:77.14896078, 38.57448039;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" + id="path891-5-0" + cx="355.72531" + cy="-422.3046" + transform="scale(1,-1)" + rx="54.830173" + ry="55.384014" /> + <ellipse + style="opacity:1;fill:#ff9191;fill-opacity:1;stroke:none;stroke-width:38.57448196;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:77.14896078, 38.57448038999999795;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" + id="path891-5-0-6" + cx="435.48053" + cy="-561.84009" + transform="scale(1,-1)" + rx="54.830173" + ry="55.384014" /> + <ellipse + style="opacity:1;fill:#c8c8c8;fill-opacity:1;stroke:none;stroke-width:38.57448196;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:77.14896078, 38.57448039;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" + id="path891-5-0-0" + cx="275.97003" + cy="-561.84003" + transform="scale(1,-1)" + rx="54.830173" + ry="55.384014" /> + </g> +</svg> diff --git a/tagit/assets/icons/scalable/grouping/ungroup.svg b/tagit/assets/icons/scalable/grouping/ungroup.svg new file mode 100644 index 0000000..0e656b7 --- /dev/null +++ b/tagit/assets/icons/scalable/grouping/ungroup.svg @@ -0,0 +1,134 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="100mm" + height="100mm" + id="svg2" + version="1.1" + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="ungroup.svg" + inkscape:export-filename="../../kivy/grouping/ungroup.png" + inkscape:export-xdpi="7.6199999" + inkscape:export-ydpi="7.6199999"> + <defs + id="defs4" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="1.979899" + inkscape:cx="274.46091" + inkscape:cy="39.114495" + inkscape:document-units="mm" + inkscape:current-layer="layer1" + showgrid="false" + showguides="true" + inkscape:guide-bbox="true" + inkscape:snap-global="true" + inkscape:snap-bbox="true" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:window-width="1920" + inkscape:window-height="1031" + inkscape:window-x="0" + inkscape:window-y="25" + inkscape:window-maximized="1" + inkscape:pagecheckerboard="true" + units="mm" + inkscape:bbox-paths="true" + inkscape:bbox-nodes="true" + inkscape:snap-bbox-edge-midpoints="true" + inkscape:snap-bbox-midpoints="true" + inkscape:object-paths="true" + inkscape:snap-intersection-paths="true" + inkscape:snap-smooth-nodes="true" + inkscape:snap-midpoints="true" + inkscape:snap-object-midpoints="true" + inkscape:snap-center="true" + inkscape:snap-text-baseline="true" + inkscape:snap-page="true" + inkscape:lockguides="false"> + <sodipodi:guide + orientation="0,1" + position="13.637059,643.40404" + id="guide3788" + inkscape:locked="false" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="0,1" + id="guide1099" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="1,0" + id="guide1101" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + </sodipodi:namedview> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-167.28122,-322.85977)"> + <circle + style="opacity:1;fill:none;fill-opacity:1;stroke:#c8c8c8;stroke-width:17.683;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:17.683,70.732;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" + id="path889" + cx="356.2576" + cy="511.83615" + r="180.13463" /> + <g + id="g1097" + transform="matrix(0.37263226,0,0,0.37639623,804.3104,348.85233)" + style="fill:#c8c8c8;fill-opacity:1"> + <circle + transform="scale(1,-1)" + r="147.14285" + cy="-195.14612" + cx="-1203.8279" + id="path891-5-0" + style="opacity:1;fill:#c8c8c8;fill-opacity:1;stroke:none;stroke-width:103;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:206.0000011, 103.00000055000000998;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" /> + <circle + transform="scale(1,-1)" + r="147.14285" + cy="-565.86047" + cx="-989.7959" + id="path891-5-0-6" + style="opacity:1;fill:#c8c8c8;fill-opacity:1;stroke:none;stroke-width:103;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:206.0000011, 103.00000055000000998;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" /> + <circle + transform="scale(1,-1)" + r="147.14285" + cy="-565.86041" + cx="-1417.86" + id="path891-5-0-0" + style="opacity:1;fill:#c8c8c8;fill-opacity:1;stroke:none;stroke-width:103;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:206.0000011, 103.00000055000000998;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" /> + </g> + </g> +</svg> diff --git a/tagit/assets/icons/scalable/misc/clip_copy.svg b/tagit/assets/icons/scalable/misc/clip_copy.svg new file mode 100644 index 0000000..d90de47 --- /dev/null +++ b/tagit/assets/icons/scalable/misc/clip_copy.svg @@ -0,0 +1,204 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="100mm" + height="100mm" + id="svg2" + version="1.1" + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="clip_copy.svg" + inkscape:export-filename="../../kivy/browser/template.png" + inkscape:export-xdpi="7.6199999" + inkscape:export-ydpi="7.6199999"> + <defs + id="defs4"> + <inkscape:path-effect + effect="bspline" + id="path-effect840" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect818" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect818-3" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath927"> + <path + style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="M 302.48937,693.31254 V 393.37081 h 144.07756 v -63.9099 H 149.42408 v 387.85714 h 153.06529 z" + id="path929" + inkscape:connector-curvature="0" /> + </clipPath> + </defs> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="1.4" + inkscape:cx="266.96275" + inkscape:cy="137.84061" + inkscape:document-units="mm" + inkscape:current-layer="g894" + showgrid="false" + showguides="true" + inkscape:guide-bbox="true" + inkscape:snap-global="true" + inkscape:snap-bbox="true" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:window-width="1920" + inkscape:window-height="1056" + inkscape:window-x="0" + inkscape:window-y="1200" + inkscape:window-maximized="1" + inkscape:pagecheckerboard="true" + units="mm" + inkscape:bbox-paths="true" + inkscape:bbox-nodes="true" + inkscape:snap-bbox-edge-midpoints="true" + inkscape:snap-bbox-midpoints="true" + inkscape:object-paths="true" + inkscape:snap-intersection-paths="true" + inkscape:snap-smooth-nodes="true" + inkscape:snap-midpoints="true" + inkscape:snap-object-midpoints="true" + inkscape:snap-center="true" + inkscape:snap-text-baseline="true" + inkscape:snap-page="true" + inkscape:lockguides="false"> + <sodipodi:guide + position="188.97638,188.97638" + orientation="0,1" + id="guide1099" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="1,0" + id="guide1101" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="-126.52161,323.12254" + orientation="0,1" + id="guide845" + inkscape:locked="false" /> + </sodipodi:namedview> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title /> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-167.28122,-322.85977)"> + <g + id="g864" + transform="translate(5.1757812e-7,-31.505525)" + clip-path="url(#clipPath927)"> + <rect + y="361.8653" + x="174.78122" + height="331.44724" + width="240.41631" + id="rect824" + style="opacity:1;fill:none;fill-opacity:1;stroke:#c8c8c8;stroke-width:15;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" /> + <g + transform="translate(-61.268239,-19.117472)" + id="g894"> + <path + style="fill:none;stroke:#c8c8c8;stroke-width:20;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 281.93353,476.05359 H 427.3955" + id="path850" + inkscape:connector-curvature="0" /> + <path + style="fill:none;stroke:#c8c8c8;stroke-width:20;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 283.52662,546.70639 H 428.98859" + id="path850-7" + inkscape:connector-curvature="0" /> + <path + style="fill:none;stroke:#c8c8c8;stroke-width:20;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 285.11971,617.35919 H 430.58168" + id="path850-7-5" + inkscape:connector-curvature="0" /> + </g> + </g> + <g + transform="translate(127.70815)" + id="g864-3"> + <rect + y="361.8653" + x="174.78122" + height="331.44724" + width="240.41631" + id="rect824-6" + style="opacity:1;fill:none;fill-opacity:1;stroke:#c8c8c8;stroke-width:15;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" /> + <g + transform="translate(-61.268239,-19.117472)" + id="g894-7" + style="stroke-width:20;stroke-miterlimit:4;stroke-dasharray:none"> + <path + style="fill:none;stroke:#c8c8c8;stroke-width:20;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 281.93353,476.05359 H 427.3955" + id="path850-5" + inkscape:connector-curvature="0" /> + <path + style="fill:none;stroke:#c8c8c8;stroke-width:20;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 283.52662,546.70639 H 428.98859" + id="path850-7-3" + inkscape:connector-curvature="0" /> + <path + style="fill:none;stroke:#c8c8c8;stroke-width:20;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 285.11971,617.35919 H 430.58168" + id="path850-7-5-5" + inkscape:connector-curvature="0" /> + </g> + </g> + </g> +</svg> diff --git a/tagit/assets/icons/scalable/misc/clip_paste.svg b/tagit/assets/icons/scalable/misc/clip_paste.svg new file mode 100644 index 0000000..8747068 --- /dev/null +++ b/tagit/assets/icons/scalable/misc/clip_paste.svg @@ -0,0 +1,163 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="100mm" + height="100mm" + id="svg2" + version="1.1" + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="copy.svg" + inkscape:export-filename="../../kivy/browser/template.png" + inkscape:export-xdpi="7.6199999" + inkscape:export-ydpi="7.6199999"> + <defs + id="defs4"> + <inkscape:path-effect + effect="bspline" + id="path-effect840" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect818" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect818-3" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + </defs> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="1.979899" + inkscape:cx="329.30963" + inkscape:cy="146.06391" + inkscape:document-units="mm" + inkscape:current-layer="layer1" + showgrid="false" + showguides="true" + inkscape:guide-bbox="true" + inkscape:snap-global="true" + inkscape:snap-bbox="true" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:window-width="1920" + inkscape:window-height="1056" + inkscape:window-x="0" + inkscape:window-y="1200" + inkscape:window-maximized="1" + inkscape:pagecheckerboard="true" + units="mm" + inkscape:bbox-paths="true" + inkscape:bbox-nodes="true" + inkscape:snap-bbox-edge-midpoints="true" + inkscape:snap-bbox-midpoints="true" + inkscape:object-paths="true" + inkscape:snap-intersection-paths="true" + inkscape:snap-smooth-nodes="true" + inkscape:snap-midpoints="true" + inkscape:snap-object-midpoints="true" + inkscape:snap-center="true" + inkscape:snap-text-baseline="true" + inkscape:snap-page="true" + inkscape:lockguides="false"> + <sodipodi:guide + position="188.97638,188.97638" + orientation="0,1" + id="guide1099" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="1,0" + id="guide1101" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="-126.52161,323.12254" + orientation="0,1" + id="guide845" + inkscape:locked="false" /> + </sodipodi:namedview> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title /> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-167.28122,-322.85977)"> + <path + style="fill:none;stroke:#c8c8c8;stroke-width:15;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="m 327.19706,330.35977 c 0,17.33995 -0.001,34.68058 -4.63086,43.35156 -4.62963,8.67098 -14.22491,8.67008 -18.93946,12.03711 -4.71454,3.36703 -4.71338,10.10108 -0.75586,13.46875 2.50693,2.13328 6.62064,2.91065 13.41407,3.19727 l 14.69922,0.16992 h 50.54687 l 14.69922,-0.16992 c 6.79342,-0.28662 10.90714,-1.06399 13.41406,-3.19727 3.95752,-3.36767 3.95868,-10.10172 -0.75586,-13.46875 -4.71455,-3.36703 -14.31178,-3.36613 -18.94141,-12.03711 -4.62963,-8.67098 -4.6289,-26.01161 -4.6289,-43.35156 z" + id="path816" + inkscape:connector-curvature="0" + sodipodi:nodetypes="cscsccccccscc" /> + <path + style="fill:none;stroke:#c8c8c8;stroke-width:15;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none" + d="M 325.91441,361.86527 H 236.04945 V 693.3125 H 476.46576 V 361.86527 l -89.86627,0" + id="path848" + inkscape:connector-curvature="0" /> + <g + id="g894" + transform="translate(-5e-6,-5.555839)"> + <path + inkscape:connector-curvature="0" + id="path850" + d="M 281.93353,476.05359 H 427.3955" + style="fill:none;stroke:#c8c8c8;stroke-width:15;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + <path + inkscape:connector-curvature="0" + id="path850-7" + d="M 283.52662,546.70639 H 428.98859" + style="fill:none;stroke:#c8c8c8;stroke-width:15;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + <path + inkscape:connector-curvature="0" + id="path850-7-5" + d="M 285.11971,617.35919 H 430.58168" + style="fill:none;stroke:#c8c8c8;stroke-width:15;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + </g> + </g> +</svg> diff --git a/tagit/assets/icons/scalable/misc/console.svg b/tagit/assets/icons/scalable/misc/console.svg new file mode 100644 index 0000000..35f30e2 --- /dev/null +++ b/tagit/assets/icons/scalable/misc/console.svg @@ -0,0 +1,153 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="100mm" + height="100mm" + id="svg2" + version="1.1" + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="console.svg" + inkscape:export-filename="../../kivy/misc/shell.png" + inkscape:export-xdpi="7.6199999" + inkscape:export-ydpi="7.6199999"> + <defs + id="defs4" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="0.98994949" + inkscape:cx="189.79139" + inkscape:cy="105.81895" + inkscape:document-units="mm" + inkscape:current-layer="layer1" + showgrid="false" + showguides="true" + inkscape:guide-bbox="true" + inkscape:snap-global="true" + inkscape:snap-bbox="true" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:window-width="1920" + inkscape:window-height="1056" + inkscape:window-x="0" + inkscape:window-y="1200" + inkscape:window-maximized="1" + inkscape:pagecheckerboard="true" + units="mm" + inkscape:bbox-paths="true" + inkscape:bbox-nodes="true" + inkscape:snap-bbox-edge-midpoints="true" + inkscape:snap-bbox-midpoints="true" + inkscape:object-paths="true" + inkscape:snap-intersection-paths="true" + inkscape:snap-smooth-nodes="true" + inkscape:snap-midpoints="true" + inkscape:snap-object-midpoints="true" + inkscape:snap-center="true" + inkscape:snap-text-baseline="true" + inkscape:snap-page="true" + inkscape:lockguides="false"> + <sodipodi:guide + orientation="0,1" + position="13.637059,643.40404" + id="guide3788" + inkscape:locked="false" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="0,1" + id="guide1099" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="1,0" + id="guide1101" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="233.588,370" + orientation="1,0" + id="guide1107" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="144.36496,311.42857" + orientation="1,0" + id="guide1109" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="-77.142857,144.36496" + orientation="0,1" + id="guide1111" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="5.000315,233.58779" + orientation="0,1" + id="guide1113" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + </sodipodi:namedview> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title /> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-167.28122,-322.85977)"> + <rect + style="opacity:1;fill:#b6dfb1;fill-opacity:1;stroke:#c8c8c8;stroke-width:38.81450653;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" + id="rect823" + width="339.13824" + height="339.13824" + x="186.68848" + y="342.26703" /> + <g + id="g901" + transform="matrix(1.2475634,0,0,1.2475634,-41.412686,-126.71187)"> + <text + id="text5697" + y="551.11072" + x="214.79561" + style="font-style:normal;font-weight:normal;font-size:8.37658501px;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.69804883px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + xml:space="preserve"><tspan + style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:125.64877319px;line-height:1.25;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';stroke-width:0.69804883px" + y="551.11072" + x="214.79561" + id="tspan5699" + sodipodi:role="line" + dy="0 -20.559999">>_</tspan></text> + </g> + </g> +</svg> diff --git a/tagit/assets/icons/scalable/misc/help.svg b/tagit/assets/icons/scalable/misc/help.svg new file mode 100644 index 0000000..ae84a45 --- /dev/null +++ b/tagit/assets/icons/scalable/misc/help.svg @@ -0,0 +1,113 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="100mm" + height="100mm" + id="svg2" + version="1.1" + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="help.svg" + inkscape:export-filename="../../kivy/misc/help.png" + inkscape:export-xdpi="7.6199999" + inkscape:export-ydpi="7.6199999"> + <defs + id="defs4" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="0.9899495" + inkscape:cx="318.13476" + inkscape:cy="197.97507" + inkscape:document-units="mm" + inkscape:current-layer="layer1" + showgrid="false" + showguides="true" + inkscape:guide-bbox="true" + inkscape:snap-global="true" + inkscape:snap-bbox="true" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:window-width="1920" + inkscape:window-height="1031" + inkscape:window-x="0" + inkscape:window-y="25" + inkscape:window-maximized="1" + inkscape:pagecheckerboard="true" + units="mm" + inkscape:bbox-paths="true" + inkscape:bbox-nodes="true" + inkscape:snap-bbox-edge-midpoints="true" + inkscape:snap-bbox-midpoints="true" + inkscape:object-paths="true" + inkscape:snap-intersection-paths="true" + inkscape:snap-smooth-nodes="true" + inkscape:snap-midpoints="true" + inkscape:snap-object-midpoints="true" + inkscape:snap-center="true" + inkscape:snap-text-baseline="true" + inkscape:snap-page="true" + inkscape:lockguides="false"> + <sodipodi:guide + orientation="0,1" + position="13.637059,643.40404" + id="guide3788" + inkscape:locked="false" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="0,1" + id="guide1099" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="1,0" + id="guide1101" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + </sodipodi:namedview> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-167.28122,-322.85977)"> + <text + xml:space="preserve" + style="font-style:normal;font-weight:normal;font-size:238.12545776px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:5.95313644" + x="198.7773" + y="700.8125" + id="text817"><tspan + sodipodi:role="line" + id="tspan815" + x="198.7773" + y="700.8125" + style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:508.00097656px;font-family:FreeSans;-inkscape-font-specification:'FreeSans Semi-Bold';fill:#c8c8c8;fill-opacity:1;stroke-width:5.95313644">?</tspan></text> + </g> +</svg> diff --git a/tagit/assets/icons/scalable/misc/internal.svg b/tagit/assets/icons/scalable/misc/internal.svg new file mode 100644 index 0000000..9a585b2 --- /dev/null +++ b/tagit/assets/icons/scalable/misc/internal.svg @@ -0,0 +1,111 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="100mm" + height="100mm" + id="svg2" + version="1.1" + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="internal.svg" + inkscape:export-filename="../../kivy/misc/internal.png" + inkscape:export-xdpi="7.6199999" + inkscape:export-ydpi="7.6199999"> + <defs + id="defs4" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="0.9899495" + inkscape:cx="318.13476" + inkscape:cy="197.97507" + inkscape:document-units="mm" + inkscape:current-layer="layer1" + showgrid="false" + showguides="true" + inkscape:guide-bbox="true" + inkscape:snap-global="true" + inkscape:snap-bbox="true" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:window-width="1920" + inkscape:window-height="1151" + inkscape:window-x="0" + inkscape:window-y="25" + inkscape:window-maximized="1" + inkscape:pagecheckerboard="true" + units="mm" + inkscape:bbox-paths="true" + inkscape:bbox-nodes="true" + inkscape:snap-bbox-edge-midpoints="true" + inkscape:snap-bbox-midpoints="true" + inkscape:object-paths="true" + inkscape:snap-intersection-paths="true" + inkscape:snap-smooth-nodes="true" + inkscape:snap-midpoints="true" + inkscape:snap-object-midpoints="true" + inkscape:snap-center="true" + inkscape:snap-text-baseline="true" + inkscape:snap-page="true" + inkscape:lockguides="false"> + <sodipodi:guide + position="188.97638,188.97638" + orientation="0,1" + id="guide1099" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="1,0" + id="guide1101" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + </sodipodi:namedview> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-167.28122,-322.85977)"> + <g + id="g825" + transform="matrix(2.6854294,0,0,2.6854294,-621.01179,-880.57823)"> + <path + d="m 352.4576,587.03615 c -9,-2.8 -13.7,-7.8 -18.3,-19.4 -8.2,-20.4 -8.2,-20.7 -7.2,-22.4 0.6,-0.9 1.7,-1.7 2.6,-1.9 3.2,-0.6 6.2,4.1 9.1,14.5 1.5,5.6 3.4,10.7 4.2,11.4 1.5,1.3 3.4,0.4 3.4,-1.7 0,-1.7 1.4,-1.8 4.1,-0.4 1.1,0.7 2.1,2.6 2.5,5.1 0.6,3.3 1,4 2.8,3.7 1.4,-0.2 2.2,-1.2 2.6,-3.3 1.1,-6.6 6,-2.9 6,4.6 0,2.2 0.5,2.9 2,2.9 1.7,0 2,-0.7 2,-4.4 0,-7.4 5.8,-9.1 7,-2 0.8,4.5 3.3,3.6 4.2,-1.4 0.9,-5.4 1.4,-6.2 4.2,-6.2 1.6,0 2.6,0.7 2.9,2.1 1.2,4.4 3.3,0.8 6.4,-10.7 3.2,-11.7 4.7,-14.4 8.4,-14.4 0.9,0 2.2,0.7 2.9,1.5 1.1,1.3 1,2.2 -0.4,5.1 -0.9,1.9 -3.3,8.6 -5.2,14.7 -3.1,9.7 -4.2,11.8 -7.9,15.3 -6.7,6.5 -12.5,8.8 -23,9.1 -6.8,0.2 -10.5,-0.3 -15.3,-1.8 z" + id="path11" + inkscape:connector-curvature="0" + style="fill:#ff9191;fill-opacity:1;stroke:none;stroke-width:0.1" /> + <path + d="m 359.3576,558.23615 c -0.8,-0.5 -1.7,-3.1 -2,-5.8 -0.5,-3.9 -1,-4.8 -2.6,-4.8 -1.5,0 -2.1,0.8 -2.5,3.5 -1.1,7.4 -6.7,3.6 -7.9,-5.4 -0.4,-3.1 -1,-5.9 -1.4,-6.2 -1.2,-1.3 -8.6,-2.4 -16,-2.4 -7,0 -8,-0.3 -10.4,-2.6 -2.4,-2.4 -2.5,-3 -1.9,-8.5 0.4,-4.6 0.1,-7.4 -1.3,-11.7 -5.3,-15.6 -4.9,-26.1 1.4,-38.9 6.5,-13.1 17.4,-22.5 30,-25.8 8,-2 29,-2 37,0 15.5,4.1 24.6,11.5 31.7,26.3 3.9,8 4.3,9.5 4.6,18.2 0.3,7.4 -0.1,11.4 -1.7,17.8 -1.6,6 -1.9,8.7 -1.1,10.2 1.3,2.4 1.3,7.6 0,10 -1.3,2.5 -6.9,4 -14.8,4 -3.4,0 -7.8,0.7 -9.7,1.4 -3.4,1.4 -3.5,1.7 -3.5,6.8 0,5.4 -1.9,9.8 -4.4,9.8 -0.7,0 -1.9,-1.5 -2.7,-3.2 -2.1,-4.9 -4.3,-4.3 -5,1.5 -0.4,4.3 -1.9,6.6 -4.1,6.7 -1.2,0 -2.8,-3.7 -2.8,-6.3 0,-2.5 -2.2,-5.2 -3.3,-4 -0.4,0.3 -0.7,2.3 -0.7,4.3 0,2 -0.5,4.1 -1.2,4.8 -1.4,1.4 -1.8,1.5 -3.7,0.3 z m 14.2,-18 c 3.9,-1.5 3.9,-3.7 0.1,-12.9 -4.1,-9.9 -5.8,-12.2 -8.9,-12.2 -1.8,0 -2.8,1.4 -5.3,6.8 -4.6,10.3 -5.6,14.5 -4.3,17.1 1,1.9 2,2.1 8.6,2.1 4,0 8.5,-0.4 9.8,-0.9 z m -19.7,-18.5 c 2,-2.3 2.4,-3.8 2.4,-9.9 0,-11.7 -3.6,-15.7 -14.2,-15.7 -5.7,0 -10.1,2.2 -11.7,5.7 -1.7,3.8 -1.4,15.5 0.5,18.2 2.6,3.6 7.1,5.2 14.3,4.8 5.5,-0.3 6.7,-0.7 8.7,-3.1 z m 38.9,2.4 c 5.5,-1.3 7.2,-3.7 8,-11.9 0.8,-7.9 -0.3,-11.2 -4.8,-14.5 -5.3,-3.9 -17.9,-1.8 -20.7,3.3 -0.5,1.1 -1,5.3 -1,9.4 0,6 0.4,7.9 2.1,10.1 3.1,4 9.1,5.3 16.4,3.6 z" + id="path13" + inkscape:connector-curvature="0" + style="fill:#ff9191;fill-opacity:1;stroke:none;stroke-width:0.1" /> + </g> + </g> +</svg> diff --git a/tagit/assets/icons/scalable/misc/menu.svg b/tagit/assets/icons/scalable/misc/menu.svg new file mode 100644 index 0000000..e2db5ed --- /dev/null +++ b/tagit/assets/icons/scalable/misc/menu.svg @@ -0,0 +1,154 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="100mm" + height="100mm" + id="svg2" + version="1.1" + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="menu.svg" + inkscape:export-filename="../../kivy/misc/menu.png" + inkscape:export-xdpi="7.6199999" + inkscape:export-ydpi="7.6199999"> + <defs + id="defs4" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="0.98994949" + inkscape:cx="272.60226" + inkscape:cy="182.71361" + inkscape:document-units="mm" + inkscape:current-layer="layer1" + showgrid="false" + showguides="true" + inkscape:guide-bbox="true" + inkscape:snap-global="true" + inkscape:snap-bbox="true" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:window-width="1920" + inkscape:window-height="1031" + inkscape:window-x="0" + inkscape:window-y="25" + inkscape:window-maximized="1" + inkscape:pagecheckerboard="true" + units="mm" + inkscape:bbox-paths="true" + inkscape:bbox-nodes="true" + inkscape:snap-bbox-edge-midpoints="true" + inkscape:snap-bbox-midpoints="true" + inkscape:object-paths="true" + inkscape:snap-intersection-paths="true" + inkscape:snap-smooth-nodes="true" + inkscape:snap-midpoints="true" + inkscape:snap-object-midpoints="true" + inkscape:snap-center="true" + inkscape:snap-text-baseline="true" + inkscape:snap-page="true" + inkscape:lockguides="false"> + <sodipodi:guide + orientation="0,1" + position="13.637059,643.40404" + id="guide3788" + inkscape:locked="false" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="0,1" + id="guide1099" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="1,0" + id="guide1101" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="233.588,370" + orientation="1,0" + id="guide1107" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="144.36496,311.42857" + orientation="1,0" + id="guide1109" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="-77.142857,144.36496" + orientation="0,1" + id="guide1111" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="5.000315,233.58779" + orientation="0,1" + id="guide1113" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + </sodipodi:namedview> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-167.28122,-322.85977)"> + <rect + style="fill:#c8c8c8;fill-opacity:1;stroke:none;stroke-width:0.7009533" + id="rect3090-6-3-7" + width="377.95273" + height="75.590553" + x="-545.23395" + y="-398.45032" + transform="scale(-1)" /> + <rect + style="fill:#c8c8c8;fill-opacity:1;stroke:none;stroke-width:0.7009533" + id="rect3090-6-3-7-6" + width="377.95273" + height="75.590553" + x="-545.23395" + y="-700.8125" + transform="scale(-1)" /> + <rect + style="fill:#c8c8c8;fill-opacity:1;stroke:none;stroke-width:0.7009533" + id="rect3090-6-3-7-2" + width="377.95273" + height="75.590553" + x="-545.23395" + y="-549.63141" + transform="scale(-1)" /> + </g> +</svg> diff --git a/tagit/assets/icons/scalable/misc/open_external.svg b/tagit/assets/icons/scalable/misc/open_external.svg new file mode 100644 index 0000000..93e42cc --- /dev/null +++ b/tagit/assets/icons/scalable/misc/open_external.svg @@ -0,0 +1,198 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="100mm" + height="100mm" + id="svg2" + version="1.1" + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="open_external.svg" + inkscape:export-filename="../../kivy/browser/template.png" + inkscape:export-xdpi="7.6199999" + inkscape:export-ydpi="7.6199999"> + <defs + id="defs4"> + <marker + inkscape:stockid="Arrow2Lend" + orient="auto" + refY="0.0" + refX="0.0" + id="Arrow2Lend" + style="overflow:visible;" + inkscape:isstock="true"> + <path + id="path839" + style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#c8c8c8;stroke-opacity:1;fill:#c8c8c8;fill-opacity:1" + d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z " + transform="scale(1.1) rotate(180) translate(1,0)" /> + </marker> + <marker + inkscape:stockid="Arrow2Send" + orient="auto" + refY="0.0" + refX="0.0" + id="Arrow2Send" + style="overflow:visible;" + inkscape:isstock="true"> + <path + id="path851" + style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#c8c8c8;stroke-opacity:1;fill:#c8c8c8;fill-opacity:1" + d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z " + transform="scale(0.3) rotate(180) translate(-2.3,0)" /> + </marker> + <marker + inkscape:stockid="Arrow1Send" + orient="auto" + refY="0.0" + refX="0.0" + id="Arrow1Send" + style="overflow:visible;" + inkscape:isstock="true"> + <path + id="path833" + d="M 0.0,0.0 L 5.0,-5.0 L -12.5,0.0 L 5.0,5.0 L 0.0,0.0 z " + style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt;stroke-opacity:1;fill:#000000;fill-opacity:1" + transform="scale(0.2) rotate(180) translate(6,0)" /> + </marker> + <marker + inkscape:stockid="Arrow1Lend" + orient="auto" + refY="0.0" + refX="0.0" + id="Arrow1Lend" + style="overflow:visible;" + inkscape:isstock="true"> + <path + id="path821" + d="M 0.0,0.0 L 5.0,-5.0 L -12.5,0.0 L 5.0,5.0 L 0.0,0.0 z " + style="fill-rule:evenodd;stroke:#c8c8c8;stroke-width:1pt;stroke-opacity:1;fill:#c8c8c8;fill-opacity:1" + transform="scale(0.8) rotate(180) translate(12.5,0)" /> + </marker> + </defs> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="1.4" + inkscape:cx="162.03137" + inkscape:cy="119.8676" + inkscape:document-units="mm" + inkscape:current-layer="layer1" + showgrid="false" + showguides="true" + inkscape:guide-bbox="true" + inkscape:snap-global="true" + inkscape:snap-bbox="true" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:window-width="1920" + inkscape:window-height="1151" + inkscape:window-x="0" + inkscape:window-y="25" + inkscape:window-maximized="1" + inkscape:pagecheckerboard="true" + units="mm" + inkscape:bbox-paths="true" + inkscape:bbox-nodes="true" + inkscape:snap-bbox-edge-midpoints="true" + inkscape:snap-bbox-midpoints="true" + inkscape:object-paths="true" + inkscape:snap-intersection-paths="true" + inkscape:snap-smooth-nodes="true" + inkscape:snap-midpoints="true" + inkscape:snap-object-midpoints="true" + inkscape:snap-center="true" + inkscape:snap-text-baseline="true" + inkscape:snap-page="true" + inkscape:lockguides="false"> + <sodipodi:guide + position="188.97638,188.97638" + orientation="0,1" + id="guide1099" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="1,0" + id="guide1101" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="26.430236,18.897638" + orientation="0,1" + id="guide3497" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="18.897638,18.897638" + orientation="1,0" + id="guide3499" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="-64.285984,359.05512" + orientation="0,1" + id="guide3501" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="359.05512,359.05512" + orientation="1,0" + id="guide3503" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + </sodipodi:namedview> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-167.28122,-322.85977)"> + <path + style="fill:none;stroke:#c8c8c8;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow2Lend)" + d="M 364.03086,504.20708 532.52808,335.80924" + id="path816" + inkscape:connector-curvature="0" /> + <path + style="fill:none;stroke:#c8c8c8;stroke-width:20.9487114;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 356.25761,352.23177 H 196.65322 V 671.44053 H 515.86198 V 511.83616" + id="path2945" + inkscape:connector-curvature="0" /> + <path + style="fill:none;stroke:#c8c8c8;stroke-width:30;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 364.03086,504.20708 477.96301,390.34213" + id="path3495" + inkscape:connector-curvature="0" + sodipodi:nodetypes="cc" /> + </g> +</svg> diff --git a/tagit/assets/icons/scalable/misc/settings.svg b/tagit/assets/icons/scalable/misc/settings.svg new file mode 100644 index 0000000..b821de2 --- /dev/null +++ b/tagit/assets/icons/scalable/misc/settings.svg @@ -0,0 +1,109 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="100mm" + height="100mm" + id="svg2" + version="1.1" + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="settings.svg" + inkscape:export-filename="../../kivy/misc/settings.png" + inkscape:export-xdpi="7.6199999" + inkscape:export-ydpi="7.6199999"> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="0.9899495" + inkscape:cx="479.8023" + inkscape:cy="389.33964" + inkscape:document-units="mm" + inkscape:current-layer="layer1" + showgrid="false" + showguides="true" + inkscape:guide-bbox="true" + inkscape:snap-global="true" + inkscape:snap-bbox="true" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:window-width="1920" + inkscape:window-height="1151" + inkscape:window-x="0" + inkscape:window-y="25" + inkscape:window-maximized="1" + inkscape:pagecheckerboard="true" + units="mm" + inkscape:bbox-paths="true" + inkscape:bbox-nodes="true" + inkscape:snap-bbox-edge-midpoints="true" + inkscape:snap-bbox-midpoints="true" + inkscape:object-paths="true" + inkscape:snap-intersection-paths="true" + inkscape:snap-smooth-nodes="true" + inkscape:snap-midpoints="true" + inkscape:snap-object-midpoints="true" + inkscape:snap-center="true" + inkscape:snap-text-baseline="true" + inkscape:snap-page="true" + inkscape:lockguides="false"> + <sodipodi:guide + inkscape:color="rgb(0,0,255)" + inkscape:label="" + inkscape:locked="false" + id="guide1099" + orientation="0,1" + position="188.97638,188.97638" /> + <sodipodi:guide + inkscape:color="rgb(0,0,255)" + inkscape:label="" + inkscape:locked="false" + id="guide1101" + orientation="1,0" + position="188.97638,188.97638" /> + </sodipodi:namedview> + <defs + id="defs4" /> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + transform="translate(-167.28122,-322.85977)" + id="layer1" + inkscape:groupmode="layer" + inkscape:label="Layer 1"> + <g + id="g1240" + inkscape:label="Gear15" + transform="matrix(0.9780635,0,0,0.97255656,3.5313042,678.85021)" + style="stroke:#c8c8c8;stroke-width:10;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"> + <path + style="fill:none;stroke:#c8c8c8;stroke-width:10;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="m 665.23828,-423.16602 -14.54297,1.5293 -7.6914,23.99024 -0.18946,10.36328 2.71485,11.89648 -24.70899,8.0293 -4.79687,-11.2207 -6.2461,-8.27149 -20.32422,-14.88867 -12.6621,7.31055 2.73242,25.04492 4.03906,9.54492 7.32031,9.76367 -19.30859,17.38477 -8.94531,-8.29883 -9.06836,-5.01758 -24.62305,-5.33398 -8.5957,11.83007 12.68359,21.76954 7.57227,7.07421 10.6582,5.94336 -10.56641,23.73438 -11.54883,-3.94336 -10.32421,-0.89453 -24.66407,5.14258 -3.04101,14.30273 20.4414,14.72852 9.79493,3.38281 12.15429,1.09375 v 25.98242 l -12.15429,1.09375 -9.79493,3.38281 -20.4414,14.72852 3.04101,14.30273 24.66407,5.14063 10.32421,-0.89258 11.54883,-3.94336 10.56641,23.73438 -10.6582,5.94336 -7.57227,7.07421 -12.68359,21.76953 8.5957,11.83008 24.62305,-5.33593 9.06836,-5.01563 8.94531,-8.29883 19.30859,17.38477 -7.32031,9.76367 -4.03906,9.542969 -2.73242,25.046875 12.6621,7.310547 20.32422,-14.888672 6.2461,-8.271485 4.79687,-11.220704 24.70899,8.029298 -2.71485,11.896484 0.18946,10.361329 7.6914,23.992187 14.54297,1.527344 12.51172,-21.867188 2.33984,-10.095703 -0.18164,-12.201172 25.83789,-2.716797 2.35938,11.972657 4.38867,9.388672 16.78516,18.789062 13.90625,-4.517578 2.53515,-25.066406 -1.96875,-10.175781 -5.1289,-11.072268 22.5,-12.99023 7.02539,9.97851 7.82617,6.791019 22.97656,10.33789 10.86524,-9.783203 -7.87891,-23.931636 -5.93555,-8.49415 -9.18945,-8.02929 15.27148,-21.01953 10.47657,6.25781 9.91211,3.02148 25.19336,0.0996 5.94726,-13.35742 -16.92969,-18.6582 -8.8789,-5.3457 -11.66211,-3.59766 5.40234,-25.41211 12.11719,1.45508 10.28516,-1.27149 23.05468,-10.15625 v -14.62304 l -23.05468,-10.15625 -10.28516,-1.27344 -12.11719,1.45703 -5.40234,-25.41406 11.66211,-3.59766 8.8789,-5.3457 16.92969,-18.65625 -5.94726,-13.35742 -25.19336,0.0996 -9.91211,3.01953 -10.47657,6.25976 -15.27148,-21.01953 9.18945,-8.0293 5.93555,-8.49609 7.87891,-23.92969 -10.86524,-9.7832 -22.97656,10.33594 -7.82617,6.79297 -7.02539,9.97851 -22.5,-12.99023 5.1289,-11.07422 1.96875,-10.17383 -2.53515,-25.06641 -13.90625,-4.51953 -16.78516,18.78907 -4.38867,9.38867 -2.35938,11.97461 -25.83789,-2.7168 0.18164,-12.20117 -2.33984,-10.09571 z m -10.85351,81.62696 v 60.35156 a 55.107925,52.570538 0 0 0 -24.00196,22.35938 H 570.6543 a 110,110 0 0 1 83.73047,-82.71094 z m 49.28711,0.58594 a 110,110 0 0 1 81.14453,82.125 h -57.1543 a 55.107925,52.570538 0 0 0 -23.99023,-22.31055 z m -24.64454,77.97851 a 28.789347,28.789347 0 0 1 28.79102,28.78906 28.789347,28.789347 0 0 1 -28.79102,28.78907 28.789347,28.789347 0 0 1 -28.78906,-28.78907 28.789347,28.789347 0 0 1 28.78906,-28.78906 z m -108.34375,53.43164 h 59.71094 a 55.107925,52.570538 0 0 0 23.99024,22.31055 v 60.4668 a 110,110 0 0 1 -83.70118,-82.77735 z m 156.99024,0 h 57.17187 a 110,110 0 0 1 -81.17382,82.13672 v -59.77734 a 55.107925,52.570538 0 0 0 24.00195,-22.35938 z" + transform="translate(-318.13476,62.458924)" + id="path1236" + inkscape:connector-curvature="0" /> + </g> + </g> +</svg> diff --git a/tagit/assets/icons/scalable/misc/shell.svg b/tagit/assets/icons/scalable/misc/shell.svg new file mode 100644 index 0000000..eabd1a4 --- /dev/null +++ b/tagit/assets/icons/scalable/misc/shell.svg @@ -0,0 +1,152 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="100mm" + height="100mm" + id="svg2" + version="1.1" + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="shell.svg" + inkscape:export-filename="../../kivy/misc/shell.png" + inkscape:export-xdpi="7.6199999" + inkscape:export-ydpi="7.6199999"> + <defs + id="defs4" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="1.4" + inkscape:cx="56.157835" + inkscape:cy="249.27915" + inkscape:document-units="mm" + inkscape:current-layer="layer1" + showgrid="false" + showguides="true" + inkscape:guide-bbox="true" + inkscape:snap-global="true" + inkscape:snap-bbox="true" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:window-width="1920" + inkscape:window-height="1031" + inkscape:window-x="0" + inkscape:window-y="25" + inkscape:window-maximized="1" + inkscape:pagecheckerboard="true" + units="mm" + inkscape:bbox-paths="true" + inkscape:bbox-nodes="true" + inkscape:snap-bbox-edge-midpoints="true" + inkscape:snap-bbox-midpoints="true" + inkscape:object-paths="true" + inkscape:snap-intersection-paths="true" + inkscape:snap-smooth-nodes="true" + inkscape:snap-midpoints="true" + inkscape:snap-object-midpoints="true" + inkscape:snap-center="true" + inkscape:snap-text-baseline="true" + inkscape:snap-page="true" + inkscape:lockguides="false"> + <sodipodi:guide + orientation="0,1" + position="13.637059,643.40404" + id="guide3788" + inkscape:locked="false" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="0,1" + id="guide1099" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="1,0" + id="guide1101" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="233.588,370" + orientation="1,0" + id="guide1107" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="144.36496,311.42857" + orientation="1,0" + id="guide1109" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="-77.142857,144.36496" + orientation="0,1" + id="guide1111" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="5.000315,233.58779" + orientation="0,1" + id="guide1113" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + </sodipodi:namedview> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-167.28122,-322.85977)"> + <g + id="g901" + transform="matrix(1.2475634,0,0,1.2475634,-41.412695,-126.7119)"> + <rect + y="443.43799" + x="167.2812" + height="136.79633" + width="302.95276" + id="rect5695" + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#c8c8c8;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6.24963284;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" /> + <text + id="text5697" + y="548.50305" + x="167.2812" + style="font-style:normal;font-weight:normal;font-size:8.37658501px;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.69804883px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + xml:space="preserve"><tspan + style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:125.64877319px;line-height:1.25;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';stroke-width:0.69804883px" + y="548.50305" + x="167.2812" + id="tspan5699" + sodipodi:role="line">> ...</tspan></text> + </g> + </g> +</svg> diff --git a/tagit/assets/icons/scalable/objects/add_tag.svg b/tagit/assets/icons/scalable/objects/add_tag.svg new file mode 100644 index 0000000..6e73d56 --- /dev/null +++ b/tagit/assets/icons/scalable/objects/add_tag.svg @@ -0,0 +1,296 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="100mm" + height="100mm" + id="svg2" + version="1.1" + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="add_tag.svg" + inkscape:export-filename="../../kivy/objects/add_tag.png" + inkscape:export-xdpi="7.6199999" + inkscape:export-ydpi="7.6199999"> + <defs + id="defs4"> + <inkscape:path-effect + effect="bspline" + id="path-effect906" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect850" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect850-3" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect850-7" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect850-7-3" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect850-7-6" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect906-9" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect906-2" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect906-9-0" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect906-93" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect906-9-6" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect906-26" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect906-9-9" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect906-20" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + </defs> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="1.4" + inkscape:cx="-207.59181" + inkscape:cy="167.43915" + inkscape:document-units="mm" + inkscape:current-layer="layer1" + showgrid="false" + showguides="true" + inkscape:guide-bbox="true" + inkscape:snap-global="false" + inkscape:snap-bbox="true" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:window-width="1920" + inkscape:window-height="1151" + inkscape:window-x="0" + inkscape:window-y="25" + inkscape:window-maximized="1" + inkscape:pagecheckerboard="true" + units="mm" + inkscape:bbox-paths="true" + inkscape:bbox-nodes="true" + inkscape:snap-bbox-edge-midpoints="true" + inkscape:snap-bbox-midpoints="true" + inkscape:object-paths="true" + inkscape:snap-intersection-paths="true" + inkscape:snap-smooth-nodes="true" + inkscape:snap-midpoints="true" + inkscape:snap-object-midpoints="true" + inkscape:snap-center="true" + inkscape:snap-text-baseline="true" + inkscape:snap-page="true" + inkscape:lockguides="false"> + <sodipodi:guide + orientation="0,1" + position="13.637059,643.40404" + id="guide3788" + inkscape:locked="false" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="0,1" + id="guide1099" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="1,0" + id="guide1101" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + </sodipodi:namedview> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title /> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-167.28122,-322.85977)"> + <g + transform="matrix(0.846821,-1.0151333,1.0151333,0.846821,-484.44796,260.70334)" + id="g870" + style="stroke:#c8c8c8;stroke-width:14.89931583;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"> + <circle + r="23.687183" + cy="510.56808" + cx="261.76941" + id="path839" + style="opacity:1;fill:none;fill-opacity:1;stroke:#c8c8c8;stroke-width:14.89931583;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" /> + <g + style="stroke:#c8c8c8;stroke-width:14.89931583;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + transform="translate(-9.9999667e-7,6.2227844)" + id="g864"> + <path + style="fill:none;stroke:#c8c8c8;stroke-width:14.89931583;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="m 274.44346,437.98197 79.87503,66.36332 V 698.87342 H 169.22033 V 504.34529 l 80.13837,-66.36306" + id="path837" + inkscape:connector-curvature="0" + sodipodi:nodetypes="cccccc" /> + <path + style="opacity:1;fill:none;fill-opacity:1;stroke:#c8c8c8;stroke-width:14.89931583;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" + id="path839-3" + sodipodi:type="arc" + sodipodi:cx="-420.58212" + sodipodi:cy="-313.05319" + sodipodi:rx="20.524721" + sodipodi:ry="20.524721" + sodipodi:start="0.40543239" + sodipodi:end="1.917446" + sodipodi:open="true" + d="m -401.7213,-304.95791 a 20.524721,20.524721 0 0 1 -25.83407,11.20855" + transform="rotate(-156.62757)" /> + </g> + </g> + <g + id="g848" + transform="translate(342.14286,-91.176621)"> + <path + inkscape:original-d="m -17.767337,666.45535 c 30.71351,-37.05555 61.4269,-74.11251 92.14015,-111.17087" + inkscape:path-effect="#path-effect906-26" + inkscape:connector-curvature="0" + id="path904-18" + d="M -17.767337,666.45535 C 12.945407,629.39916 43.658794,592.3422 74.372813,555.28448" + style="fill:none;stroke:#c8c8c8;stroke-width:15.01094818;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + <path + inkscape:original-d="M 83.888173,656.93999 C 46.832623,626.22647 9.7756729,595.51308 -27.282697,564.79983" + inkscape:path-effect="#path-effect906-20" + inkscape:connector-curvature="0" + id="path904-2" + d="M 83.888173,656.93999 C 46.831985,626.22724 9.7750303,595.51386 -27.282697,564.79983" + style="fill:none;stroke:#c8c8c8;stroke-width:15.01094818;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + </g> + </g> +</svg> diff --git a/tagit/assets/icons/scalable/objects/edit_tag.svg b/tagit/assets/icons/scalable/objects/edit_tag.svg new file mode 100644 index 0000000..c7d64e1 --- /dev/null +++ b/tagit/assets/icons/scalable/objects/edit_tag.svg @@ -0,0 +1,303 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="100mm" + height="100mm" + id="svg2" + version="1.1" + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="edit_tag.svg" + inkscape:export-filename="../../kivy/objects/edit_tag.png" + inkscape:export-xdpi="7.6199999" + inkscape:export-ydpi="7.6199999"> + <defs + id="defs4"> + <inkscape:path-effect + effect="bspline" + id="path-effect906" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect850" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect850-3" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect850-7" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect850-7-3" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect850-7-6" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect906-9" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect906-2" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect906-9-0" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect906-93" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect906-9-6" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect906-26" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect906-9-9" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect906-20" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + </defs> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="1.4" + inkscape:cx="-97.948957" + inkscape:cy="170.29629" + inkscape:document-units="mm" + inkscape:current-layer="layer1" + showgrid="false" + showguides="true" + inkscape:guide-bbox="true" + inkscape:snap-global="true" + inkscape:snap-bbox="true" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:window-width="1920" + inkscape:window-height="1151" + inkscape:window-x="0" + inkscape:window-y="25" + inkscape:window-maximized="1" + inkscape:pagecheckerboard="true" + units="mm" + inkscape:bbox-paths="true" + inkscape:bbox-nodes="true" + inkscape:snap-bbox-edge-midpoints="true" + inkscape:snap-bbox-midpoints="true" + inkscape:object-paths="true" + inkscape:snap-intersection-paths="true" + inkscape:snap-smooth-nodes="true" + inkscape:snap-midpoints="true" + inkscape:snap-object-midpoints="true" + inkscape:snap-center="true" + inkscape:snap-text-baseline="true" + inkscape:snap-page="true" + inkscape:lockguides="false"> + <sodipodi:guide + orientation="0,1" + position="13.637059,643.40404" + id="guide3788" + inkscape:locked="false" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="0,1" + id="guide1099" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="1,0" + id="guide1101" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + </sodipodi:namedview> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title /> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-167.28122,-322.85977)"> + <g + id="g1018" + transform="matrix(0.77049115,0.63859513,-0.63859513,0.77049115,403.95705,-187.73351)"> + <g + style="stroke:#c8c8c8;stroke-width:14.89931583;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + id="g870" + transform="matrix(0.00420274,-1.3209979,1.3209979,0.00420274,-397.55803,911.5166)"> + <circle + style="opacity:1;fill:none;fill-opacity:1;stroke:#c8c8c8;stroke-width:14.89931583;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" + id="path839" + cx="261.76941" + cy="510.56808" + r="23.687183" /> + <g + id="g864" + transform="translate(-9.9999667e-7,6.2227844)" + style="stroke:#c8c8c8;stroke-width:14.89931583;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"> + <path + sodipodi:nodetypes="cccccc" + inkscape:connector-curvature="0" + id="path837" + d="m 274.44346,437.98197 79.87503,66.36332 V 698.87342 H 169.22033 V 504.34529 l 80.13837,-66.36306" + style="fill:none;stroke:#c8c8c8;stroke-width:14.89931583;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + <path + transform="rotate(-156.62757)" + d="m -401.7213,-304.95791 a 20.524721,20.524721 0 0 1 -25.83407,11.20855" + sodipodi:open="true" + sodipodi:end="1.917446" + sodipodi:start="0.40543239" + sodipodi:ry="20.524721" + sodipodi:rx="20.524721" + sodipodi:cy="-313.05319" + sodipodi:cx="-420.58212" + sodipodi:type="arc" + id="path839-3" + style="opacity:1;fill:none;fill-opacity:1;stroke:#c8c8c8;stroke-width:14.89931583;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" /> + </g> + </g> + <path + inkscape:original-d="m 500.54332,620.98029 c -48.09424,-10e-4 -96.18948,-10e-4 -144.28572,0" + inkscape:path-effect="#path-effect906" + inkscape:connector-curvature="0" + id="path904" + d="m 500.54332,620.98029 c -48.09424,0 -96.18948,0 -144.28572,0" + style="fill:none;stroke:#c8c8c8;stroke-width:15;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + <path + inkscape:original-d="m 500.54332,567.766 c -48.09424,-10e-4 -96.18948,-10e-4 -144.28572,0" + inkscape:path-effect="#path-effect906-26" + inkscape:connector-curvature="0" + id="path904-18" + d="m 500.54332,567.766 c -48.09424,0 -96.18948,0 -144.28572,0" + style="fill:none;stroke:#c8c8c8;stroke-width:15;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + <path + inkscape:original-d="m 500.54332,514.55171 c -48.09424,-10e-4 -96.18948,-10e-4 -144.28572,0" + inkscape:path-effect="#path-effect906-20" + inkscape:connector-curvature="0" + id="path904-2" + d="m 500.54332,514.55171 c -48.09424,0 -96.18948,0 -144.28572,0" + style="fill:none;stroke:#c8c8c8;stroke-width:15;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + </g> + </g> +</svg> diff --git a/tagit/assets/icons/scalable/search/apply.svg b/tagit/assets/icons/scalable/search/apply.svg new file mode 100644 index 0000000..6549ee6 --- /dev/null +++ b/tagit/assets/icons/scalable/search/apply.svg @@ -0,0 +1,144 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="100mm" + height="100mm" + id="svg2" + version="1.1" + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="apply.svg" + inkscape:export-filename="../../kivy/search/search.png" + inkscape:export-xdpi="7.6199999" + inkscape:export-ydpi="7.6199999"> + <defs + id="defs4" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="1.4" + inkscape:cx="43.605974" + inkscape:cy="205.85327" + inkscape:document-units="mm" + inkscape:current-layer="layer1" + showgrid="false" + showguides="true" + inkscape:guide-bbox="true" + inkscape:snap-global="true" + inkscape:snap-bbox="true" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:window-width="1920" + inkscape:window-height="1031" + inkscape:window-x="0" + inkscape:window-y="25" + inkscape:window-maximized="1" + inkscape:pagecheckerboard="true" + units="mm" + inkscape:bbox-paths="true" + inkscape:bbox-nodes="true" + inkscape:snap-bbox-edge-midpoints="true" + inkscape:snap-bbox-midpoints="true" + inkscape:object-paths="true" + inkscape:snap-intersection-paths="true" + inkscape:snap-smooth-nodes="true" + inkscape:snap-midpoints="true" + inkscape:snap-object-midpoints="true" + inkscape:snap-center="true" + inkscape:snap-text-baseline="true" + inkscape:snap-page="true" + inkscape:lockguides="false"> + <sodipodi:guide + orientation="0,1" + position="13.637059,643.40404" + id="guide3788" + inkscape:locked="false" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="0,1" + id="guide1099" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="1,0" + id="guide1101" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="233.588,370" + orientation="1,0" + id="guide1107" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="144.36496,311.42857" + orientation="1,0" + id="guide1109" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="-77.142857,144.36496" + orientation="0,1" + id="guide1111" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="5.000315,233.58779" + orientation="0,1" + id="guide1113" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + </sodipodi:namedview> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-167.28122,-322.85977)"> + <rect + style="opacity:1;fill:#c8c8c8;fill-opacity:1;stroke:none;stroke-width:17.3116684;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" + id="rect822" + width="60.920605" + height="170.42346" + x="-650.75458" + y="-357.06061" + transform="matrix(-0.71864414,-0.69537803,0.71864414,-0.69537803,0,0)" /> + <circle + style="opacity:1;fill:none;fill-opacity:1;stroke:#c8c8c8;stroke-width:30;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" + id="path824" + cx="400.86923" + cy="467.22452" + r="129.5289" /> + </g> +</svg> diff --git a/tagit/assets/icons/scalable/search/exclude_filter.svg b/tagit/assets/icons/scalable/search/exclude_filter.svg new file mode 100644 index 0000000..ff6ebcf --- /dev/null +++ b/tagit/assets/icons/scalable/search/exclude_filter.svg @@ -0,0 +1,173 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="100mm" + height="100mm" + id="svg2" + version="1.1" + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="exclude_filter.svg" + inkscape:export-filename="../../kivy/browser/select_single.png" + inkscape:export-xdpi="7.6199999" + inkscape:export-ydpi="7.6199999"> + <defs + id="defs4" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="2.8" + inkscape:cx="171.2059" + inkscape:cy="214.89061" + inkscape:document-units="mm" + inkscape:current-layer="layer1" + showgrid="false" + showguides="true" + inkscape:guide-bbox="true" + inkscape:snap-global="true" + inkscape:snap-bbox="true" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:window-width="1920" + inkscape:window-height="1151" + inkscape:window-x="0" + inkscape:window-y="25" + inkscape:window-maximized="1" + inkscape:pagecheckerboard="true" + units="mm" + inkscape:bbox-paths="true" + inkscape:bbox-nodes="true" + inkscape:snap-bbox-edge-midpoints="true" + inkscape:snap-bbox-midpoints="true" + inkscape:object-paths="true" + inkscape:snap-intersection-paths="true" + inkscape:snap-smooth-nodes="true" + inkscape:snap-midpoints="true" + inkscape:snap-object-midpoints="true" + inkscape:snap-center="true" + inkscape:snap-text-baseline="true" + inkscape:snap-page="true" + inkscape:lockguides="false"> + <sodipodi:guide + orientation="0,1" + position="13.637059,643.40404" + id="guide3788" + inkscape:locked="false" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="0,1" + id="guide1099" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="1,0" + id="guide1101" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="-25.75889,207.87402" + orientation="0,1" + id="guide837" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="-43.941635,170.07874" + orientation="0,1" + id="guide854" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="170.07874,284.018" + orientation="1,0" + id="guide877" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="207.87402,258.09397" + orientation="1,0" + id="guide879" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + </sodipodi:namedview> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title /> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-167.28122,-322.85977)"> + <rect + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#9d9ef3;fill-opacity:1;fill-rule:nonzero;stroke:#9d9df3;stroke-width:13.7840004;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:23.10000038;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + id="rect4136-3-5" + width="156.29443" + height="103.39367" + x="174.17337" + y="382.65268" /> + <rect + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#c8c8c8;stroke-width:13.7840004;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:23.10000038;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + id="rect4136-3-5-2" + width="156.29443" + height="103.39367" + x="382.04739" + y="382.65268" /> + <rect + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#c8c8c8;stroke-width:13.7840004;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:23.10000038;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + id="rect4136-3-5-9" + width="156.29443" + height="103.39367" + x="174.17337" + y="537.62592" /> + <rect + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#c8c8c8;stroke-width:13.7840004;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:23.10000038;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + id="rect4136-3-5-1" + width="156.29443" + height="103.39367" + x="382.04739" + y="537.62592" /> + <g + id="g842" + transform="translate(-212.19278,0.94769901)" + style="stroke:#ff2f2f;stroke-width:15;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"> + <path + inkscape:connector-curvature="0" + id="path823" + d="m 431.92408,400.81253 65.17857,65.17857" + style="fill:none;stroke:#ff2f2f;stroke-width:15;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + <path + inkscape:connector-curvature="0" + id="path823-3" + d="m 431.92408,465.9911 65.17857,-65.17857" + style="fill:none;stroke:#ff2f2f;stroke-width:15;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + </g> + </g> +</svg> diff --git a/tagit/assets/icons/scalable/search/exclusive_filter.svg b/tagit/assets/icons/scalable/search/exclusive_filter.svg new file mode 100644 index 0000000..d0539f1 --- /dev/null +++ b/tagit/assets/icons/scalable/search/exclusive_filter.svg @@ -0,0 +1,203 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="100mm" + height="100mm" + id="svg2" + version="1.1" + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="exclusive_filter.svg" + inkscape:export-filename="../../kivy/browser/select_single.png" + inkscape:export-xdpi="7.6199999" + inkscape:export-ydpi="7.6199999"> + <defs + id="defs4" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="2.8" + inkscape:cx="171.2059" + inkscape:cy="214.89061" + inkscape:document-units="mm" + inkscape:current-layer="layer1" + showgrid="false" + showguides="true" + inkscape:guide-bbox="true" + inkscape:snap-global="true" + inkscape:snap-bbox="true" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:window-width="1920" + inkscape:window-height="1151" + inkscape:window-x="0" + inkscape:window-y="25" + inkscape:window-maximized="1" + inkscape:pagecheckerboard="true" + units="mm" + inkscape:bbox-paths="true" + inkscape:bbox-nodes="true" + inkscape:snap-bbox-edge-midpoints="true" + inkscape:snap-bbox-midpoints="true" + inkscape:object-paths="true" + inkscape:snap-intersection-paths="true" + inkscape:snap-smooth-nodes="true" + inkscape:snap-midpoints="true" + inkscape:snap-object-midpoints="true" + inkscape:snap-center="true" + inkscape:snap-text-baseline="true" + inkscape:snap-page="true" + inkscape:lockguides="false"> + <sodipodi:guide + orientation="0,1" + position="13.637059,643.40404" + id="guide3788" + inkscape:locked="false" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="0,1" + id="guide1099" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="1,0" + id="guide1101" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="-25.75889,207.87402" + orientation="0,1" + id="guide837" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="-43.941635,170.07874" + orientation="0,1" + id="guide854" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="170.07874,284.018" + orientation="1,0" + id="guide877" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="207.87402,258.09397" + orientation="1,0" + id="guide879" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + </sodipodi:namedview> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title /> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-167.28122,-322.85977)"> + <rect + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#9d9ef3;fill-opacity:1;fill-rule:nonzero;stroke:#9d9df3;stroke-width:13.7840004;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:23.10000038;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + id="rect4136-3-5" + width="156.29443" + height="103.39367" + x="174.17337" + y="382.65268" /> + <rect + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#c8c8c8;stroke-width:13.7840004;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:23.10000038;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + id="rect4136-3-5-2" + width="156.29443" + height="103.39367" + x="382.04739" + y="382.65268" /> + <rect + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#c8c8c8;stroke-width:13.7840004;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:23.10000038;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + id="rect4136-3-5-9" + width="156.29443" + height="103.39367" + x="174.17337" + y="537.62592" /> + <rect + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#c8c8c8;stroke-width:13.7840004;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:23.10000038;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + id="rect4136-3-5-1" + width="156.29443" + height="103.39367" + x="382.04739" + y="537.62592" /> + <g + id="g842" + transform="translate(-4.3187544,0.94769901)" + style="stroke:#f39d9d;stroke-width:15;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"> + <path + inkscape:connector-curvature="0" + id="path823" + d="m 431.92408,400.81253 65.17857,65.17857" + style="fill:none;stroke:#f39d9d;stroke-width:15;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + <path + inkscape:connector-curvature="0" + id="path823-3" + d="m 431.92408,465.9911 65.17857,-65.17857" + style="fill:none;stroke:#f39d9d;stroke-width:15;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + </g> + <g + id="g842-3" + transform="translate(-4.3187697,155.92094)" + style="stroke:#f39d9d;stroke-width:15;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"> + <path + inkscape:connector-curvature="0" + id="path823-6" + d="m 431.92408,400.81253 65.17857,65.17857" + style="fill:none;stroke:#f39d9d;stroke-width:15;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + <path + inkscape:connector-curvature="0" + id="path823-3-7" + d="m 431.92408,465.9911 65.17857,-65.17857" + style="fill:none;stroke:#f39d9d;stroke-width:15;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + </g> + <g + id="g842-3-5" + transform="translate(-212.19278,155.92094)" + style="stroke:#f39d9d;stroke-width:15;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"> + <path + inkscape:connector-curvature="0" + id="path823-6-3" + d="m 431.92408,400.81253 65.17857,65.17857" + style="fill:none;stroke:#f39d9d;stroke-width:15;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + <path + inkscape:connector-curvature="0" + id="path823-3-7-5" + d="m 431.92408,465.9911 65.17857,-65.17857" + style="fill:none;stroke:#f39d9d;stroke-width:15;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + </g> + </g> +</svg> diff --git a/tagit/assets/icons/scalable/search/sort_key.svg b/tagit/assets/icons/scalable/search/sort_key.svg new file mode 100644 index 0000000..eda2b6b --- /dev/null +++ b/tagit/assets/icons/scalable/search/sort_key.svg @@ -0,0 +1,158 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="100mm" + height="100mm" + id="svg2" + version="1.1" + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="sort_key.svg" + inkscape:export-filename="../../kivy/search/sort_key.png" + inkscape:export-xdpi="7.6199999" + inkscape:export-ydpi="7.6199999"> + <defs + id="defs4" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="1.979899" + inkscape:cx="257.45035" + inkscape:cy="244.18321" + inkscape:document-units="mm" + inkscape:current-layer="layer1" + showgrid="false" + showguides="true" + inkscape:guide-bbox="true" + inkscape:snap-global="true" + inkscape:snap-bbox="true" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:window-width="1920" + inkscape:window-height="1031" + inkscape:window-x="0" + inkscape:window-y="25" + inkscape:window-maximized="1" + inkscape:pagecheckerboard="true" + units="mm" + inkscape:bbox-paths="true" + inkscape:bbox-nodes="true" + inkscape:snap-bbox-edge-midpoints="true" + inkscape:snap-bbox-midpoints="true" + inkscape:object-paths="true" + inkscape:snap-intersection-paths="true" + inkscape:snap-smooth-nodes="true" + inkscape:snap-midpoints="true" + inkscape:snap-object-midpoints="true" + inkscape:snap-center="true" + inkscape:snap-text-baseline="true" + inkscape:snap-page="true" + inkscape:lockguides="false"> + <sodipodi:guide + orientation="0,1" + position="13.637059,643.40404" + id="guide3788" + inkscape:locked="false" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="0,1" + id="guide1099" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="1,0" + id="guide1101" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="233.588,370" + orientation="1,0" + id="guide1107" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="144.36496,311.42857" + orientation="1,0" + id="guide1109" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="-77.142857,144.36496" + orientation="0,1" + id="guide1111" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="5.000315,233.58779" + orientation="0,1" + id="guide1113" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + </sodipodi:namedview> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-167.28122,-322.85977)"> + <rect + style="opacity:1;fill:#c8c8c8;fill-opacity:1;stroke:none;stroke-width:2.93663239;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" + id="rect819" + width="377.9527" + height="53.669292" + x="167.28125" + y="322.85977" /> + <rect + style="opacity:1;fill:#c8c8c8;fill-opacity:1;stroke:none;stroke-width:2.07651258;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" + id="rect819-3" + width="188.97635" + height="53.669292" + x="167.28125" + y="647.14325" /> + <rect + style="opacity:1;fill:#c8c8c8;fill-opacity:1;stroke:none;stroke-width:2.68023014;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" + id="rect819-3-6" + width="314.83466" + height="53.669292" + x="167.28122" + y="435.48969" /> + <rect + style="opacity:1;fill:#c8c8c8;fill-opacity:1;stroke:none;stroke-width:2.39655113;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" + id="rect819-3-7" + width="251.71654" + height="53.669292" + x="167.28122" + y="541.31647" /> + </g> +</svg> diff --git a/tagit/assets/icons/scalable/search/sort_order_down.svg b/tagit/assets/icons/scalable/search/sort_order_down.svg new file mode 100644 index 0000000..933a668 --- /dev/null +++ b/tagit/assets/icons/scalable/search/sort_order_down.svg @@ -0,0 +1,170 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="100mm" + height="100mm" + id="svg2" + version="1.1" + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="sort_order_down.svg" + inkscape:export-filename="../../kivy/search/sort_order_down.png" + inkscape:export-xdpi="7.6199999" + inkscape:export-ydpi="7.6199999"> + <defs + id="defs4" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="1.979899" + inkscape:cx="273.1116" + inkscape:cy="189.42948" + inkscape:document-units="mm" + inkscape:current-layer="layer1" + showgrid="false" + showguides="true" + inkscape:guide-bbox="true" + inkscape:snap-global="true" + inkscape:snap-bbox="true" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:window-width="1920" + inkscape:window-height="1031" + inkscape:window-x="0" + inkscape:window-y="25" + inkscape:window-maximized="1" + inkscape:pagecheckerboard="true" + units="mm" + inkscape:bbox-paths="true" + inkscape:bbox-nodes="true" + inkscape:snap-bbox-edge-midpoints="true" + inkscape:snap-bbox-midpoints="true" + inkscape:object-paths="true" + inkscape:snap-intersection-paths="true" + inkscape:snap-smooth-nodes="true" + inkscape:snap-midpoints="true" + inkscape:snap-object-midpoints="true" + inkscape:snap-center="true" + inkscape:snap-text-baseline="true" + inkscape:snap-page="true" + inkscape:lockguides="false"> + <sodipodi:guide + orientation="0,1" + position="13.637059,643.40404" + id="guide3788" + inkscape:locked="false" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="0,1" + id="guide1099" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="1,0" + id="guide1101" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="233.588,370" + orientation="1,0" + id="guide1107" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="144.36496,311.42857" + orientation="1,0" + id="guide1109" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="-77.142857,144.36496" + orientation="0,1" + id="guide1111" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="5.000315,233.58779" + orientation="0,1" + id="guide1113" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + </sodipodi:namedview> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-167.28122,-322.85977)"> + <g + id="g922" + transform="rotate(-180,356.2576,511.83615)"> + <path + inkscape:connector-curvature="0" + id="path2995" + d="m 234.80113,332.39648 v 202.50423 h -57.98394 l 47.89217,78.18799 47.89217,78.18799 47.89031,-78.18799 47.89216,-78.18799 H 310.40006 V 332.39648 Z" + style="fill:none;fill-opacity:1;stroke:#c8c8c8;stroke-width:19.07342148;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + <g + style="fill:#c8c8c8;fill-opacity:1" + id="g1182-6" + transform="matrix(-0.60812511,0,0,-0.59731394,691.90628,717.38738)"> + <path + sodipodi:type="star" + style="fill:#c8c8c8;fill-opacity:1;stroke:none;stroke-width:2.83461595;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="path2995-2" + sodipodi:sides="3" + sodipodi:cx="619.66785" + sodipodi:cy="345.2388" + sodipodi:r1="259.16489" + sodipodi:r2="129.58244" + sodipodi:arg1="0" + sodipodi:arg2="1.0471976" + inkscape:flatsided="false" + inkscape:rounded="0" + inkscape:randomized="0" + d="M 878.83273,345.2388 684.45906,457.46049 490.0854,569.68218 l 0,-224.44339 0,-224.44337 194.37367,112.2217 z" + inkscape:transform-center-x="-64.791235" + inkscape:transform-center-y="-2.2785204e-06" + transform="matrix(0,0.70615451,-0.73585447,0,660.39089,37.167262)" /> + <rect + style="opacity:1;fill:#c8c8c8;fill-opacity:1;stroke:none;stroke-width:3.10856915;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:6.21713833, 3.10856917;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" + id="rect4750-9" + width="386.10318" + height="130.35136" + x="27.748983" + y="-471.52106" + transform="rotate(90)" /> + </g> + </g> + </g> +</svg> diff --git a/tagit/assets/icons/scalable/search/sort_order_up.svg b/tagit/assets/icons/scalable/search/sort_order_up.svg new file mode 100644 index 0000000..96a70f7 --- /dev/null +++ b/tagit/assets/icons/scalable/search/sort_order_up.svg @@ -0,0 +1,166 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="100mm" + height="100mm" + id="svg2" + version="1.1" + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="sort_order_up.svg" + inkscape:export-filename="../../kivy/search/sort_order_up.png" + inkscape:export-xdpi="7.6199999" + inkscape:export-ydpi="7.6199999"> + <defs + id="defs4" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="1.979899" + inkscape:cx="195.58239" + inkscape:cy="187.40917" + inkscape:document-units="mm" + inkscape:current-layer="layer1" + showgrid="false" + showguides="true" + inkscape:guide-bbox="true" + inkscape:snap-global="true" + inkscape:snap-bbox="true" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:window-width="1920" + inkscape:window-height="1031" + inkscape:window-x="0" + inkscape:window-y="25" + inkscape:window-maximized="1" + inkscape:pagecheckerboard="true" + units="mm" + inkscape:bbox-paths="true" + inkscape:bbox-nodes="true" + inkscape:snap-bbox-edge-midpoints="true" + inkscape:snap-bbox-midpoints="true" + inkscape:object-paths="true" + inkscape:snap-intersection-paths="true" + inkscape:snap-smooth-nodes="true" + inkscape:snap-midpoints="true" + inkscape:snap-object-midpoints="true" + inkscape:snap-center="true" + inkscape:snap-text-baseline="true" + inkscape:snap-page="true" + inkscape:lockguides="false"> + <sodipodi:guide + orientation="0,1" + position="13.637059,643.40404" + id="guide3788" + inkscape:locked="false" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="0,1" + id="guide1099" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="1,0" + id="guide1101" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="233.588,370" + orientation="1,0" + id="guide1107" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="144.36496,311.42857" + orientation="1,0" + id="guide1109" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="-77.142857,144.36496" + orientation="0,1" + id="guide1111" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="5.000315,233.58779" + orientation="0,1" + id="guide1113" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + </sodipodi:namedview> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title /> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-167.28122,-322.85977)"> + <path + style="fill:none;fill-opacity:1;stroke:#c8c8c8;stroke-width:19.07342148;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + d="m 234.80113,332.39648 v 202.50423 h -57.98394 l 47.89217,78.18799 47.89217,78.18799 47.89031,-78.18799 47.89216,-78.18799 H 310.40006 V 332.39648 Z" + id="path2995" + inkscape:connector-curvature="0" /> + <g + transform="matrix(-0.60812511,0,0,-0.59731394,691.90628,717.38738)" + id="g1182-6" + style="fill:#c8c8c8;fill-opacity:1"> + <path + transform="matrix(0,0.70615451,-0.73585447,0,660.39089,37.167262)" + inkscape:transform-center-y="-2.2785204e-06" + inkscape:transform-center-x="-64.791235" + d="M 878.83273,345.2388 684.45906,457.46049 490.0854,569.68218 l 0,-224.44339 0,-224.44337 194.37367,112.2217 z" + inkscape:randomized="0" + inkscape:rounded="0" + inkscape:flatsided="false" + sodipodi:arg2="1.0471976" + sodipodi:arg1="0" + sodipodi:r2="129.58244" + sodipodi:r1="259.16489" + sodipodi:cy="345.2388" + sodipodi:cx="619.66785" + sodipodi:sides="3" + id="path2995-2" + style="fill:#c8c8c8;fill-opacity:1;stroke:none;stroke-width:2.83461595;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + sodipodi:type="star" /> + <rect + transform="rotate(90)" + y="-471.52106" + x="27.748983" + height="130.35136" + width="386.10318" + id="rect4750-9" + style="opacity:1;fill:#c8c8c8;fill-opacity:1;stroke:none;stroke-width:3.10856915;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:6.21713833, 3.10856916999999999;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" /> + </g> + </g> +</svg> diff --git a/tagit/assets/icons/scalable/session/open.svg b/tagit/assets/icons/scalable/session/open.svg new file mode 100644 index 0000000..0b2d29c --- /dev/null +++ b/tagit/assets/icons/scalable/session/open.svg @@ -0,0 +1,180 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="100mm" + height="100mm" + id="svg2" + version="1.1" + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="open.svg" + inkscape:export-filename="../../kivy/session/open.png" + inkscape:export-xdpi="7.6199999" + inkscape:export-ydpi="7.6199999"> + <defs + id="defs4"> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath868"> + <rect + style="opacity:1;fill:#c8c8c8;fill-opacity:1;stroke:none;stroke-width:40;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" + id="rect870" + width="422.74884" + height="230.81985" + x="144.88318" + y="556.44757" /> + </clipPath> + </defs> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="1.979899" + inkscape:cx="126.58161" + inkscape:cy="190.628" + inkscape:document-units="mm" + inkscape:current-layer="layer1" + showgrid="false" + showguides="true" + inkscape:guide-bbox="true" + inkscape:snap-global="true" + inkscape:snap-bbox="true" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:window-width="1920" + inkscape:window-height="1031" + inkscape:window-x="0" + inkscape:window-y="25" + inkscape:window-maximized="1" + inkscape:pagecheckerboard="true" + units="mm" + inkscape:bbox-paths="true" + inkscape:bbox-nodes="true" + inkscape:snap-bbox-edge-midpoints="true" + inkscape:snap-bbox-midpoints="true" + inkscape:object-paths="true" + inkscape:snap-intersection-paths="true" + inkscape:snap-smooth-nodes="true" + inkscape:snap-midpoints="true" + inkscape:snap-object-midpoints="true" + inkscape:snap-center="true" + inkscape:snap-text-baseline="true" + inkscape:snap-page="true" + inkscape:lockguides="false"> + <sodipodi:guide + orientation="0,1" + position="13.637059,643.40404" + id="guide3788" + inkscape:locked="false" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="0,1" + id="guide1099" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="1,0" + id="guide1101" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="233.588,370" + orientation="1,0" + id="guide1107" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="144.36496,311.42857" + orientation="1,0" + id="guide1109" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="-77.142857,144.36496" + orientation="0,1" + id="guide1111" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="5.000315,233.58779" + orientation="0,1" + id="guide1113" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + </sodipodi:namedview> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-167.28122,-322.85977)"> + <rect + style="fill:none;stroke:#c8c8c8;stroke-width:72.52050018;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect2985" + width="299.20886" + height="197.09306" + x="206.65317" + y="470.40216" + clip-path="url(#clipPath868)" + transform="matrix(1.0167417,0,0,0.98002182,-5.9643702,11.116803)" /> + <g + id="g840" + transform="matrix(0.51843117,0,0,0.4508665,223.54494,300.20217)"> + <rect + y="292.88489" + x="155.32295" + height="368.57141" + width="202.85715" + id="rect4389" + style="fill:#c8c8c8;fill-opacity:1;stroke:none" /> + <path + transform="matrix(0.57204501,-0.03460321,0.03460321,0.57204501,160.61503,278.02467)" + inkscape:transform-center-y="-50.478468" + inkscape:transform-center-x="-0.051265028" + d="M 460.00001,209.50504 155.15754,191.22021 -149.68493,172.93537 18.571437,-81.923531 186.8278,-336.78244 323.4139,-63.638699 Z" + inkscape:randomized="0" + inkscape:rounded="0" + inkscape:flatsided="false" + sodipodi:arg2="1.6307058" + sodipodi:arg1="0.58350825" + sodipodi:r2="176.3172" + sodipodi:r1="352.6344" + sodipodi:cy="15.219325" + sodipodi:cx="165.71429" + sodipodi:sides="3" + id="path4391" + style="fill:#c8c8c8;fill-opacity:1;stroke:#c8c8c8;stroke-width:100;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + sodipodi:type="star" /> + </g> + </g> +</svg> diff --git a/tagit/assets/icons/scalable/tagging/add_tag.svg b/tagit/assets/icons/scalable/tagging/add_tag.svg new file mode 100644 index 0000000..6e73d56 --- /dev/null +++ b/tagit/assets/icons/scalable/tagging/add_tag.svg @@ -0,0 +1,296 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="100mm" + height="100mm" + id="svg2" + version="1.1" + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="add_tag.svg" + inkscape:export-filename="../../kivy/objects/add_tag.png" + inkscape:export-xdpi="7.6199999" + inkscape:export-ydpi="7.6199999"> + <defs + id="defs4"> + <inkscape:path-effect + effect="bspline" + id="path-effect906" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect850" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect850-3" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect850-7" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect850-7-3" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect850-7-6" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect906-9" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect906-2" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect906-9-0" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect906-93" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect906-9-6" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect906-26" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect906-9-9" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect906-20" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + </defs> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="1.4" + inkscape:cx="-207.59181" + inkscape:cy="167.43915" + inkscape:document-units="mm" + inkscape:current-layer="layer1" + showgrid="false" + showguides="true" + inkscape:guide-bbox="true" + inkscape:snap-global="false" + inkscape:snap-bbox="true" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:window-width="1920" + inkscape:window-height="1151" + inkscape:window-x="0" + inkscape:window-y="25" + inkscape:window-maximized="1" + inkscape:pagecheckerboard="true" + units="mm" + inkscape:bbox-paths="true" + inkscape:bbox-nodes="true" + inkscape:snap-bbox-edge-midpoints="true" + inkscape:snap-bbox-midpoints="true" + inkscape:object-paths="true" + inkscape:snap-intersection-paths="true" + inkscape:snap-smooth-nodes="true" + inkscape:snap-midpoints="true" + inkscape:snap-object-midpoints="true" + inkscape:snap-center="true" + inkscape:snap-text-baseline="true" + inkscape:snap-page="true" + inkscape:lockguides="false"> + <sodipodi:guide + orientation="0,1" + position="13.637059,643.40404" + id="guide3788" + inkscape:locked="false" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="0,1" + id="guide1099" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="1,0" + id="guide1101" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + </sodipodi:namedview> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title /> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-167.28122,-322.85977)"> + <g + transform="matrix(0.846821,-1.0151333,1.0151333,0.846821,-484.44796,260.70334)" + id="g870" + style="stroke:#c8c8c8;stroke-width:14.89931583;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"> + <circle + r="23.687183" + cy="510.56808" + cx="261.76941" + id="path839" + style="opacity:1;fill:none;fill-opacity:1;stroke:#c8c8c8;stroke-width:14.89931583;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" /> + <g + style="stroke:#c8c8c8;stroke-width:14.89931583;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + transform="translate(-9.9999667e-7,6.2227844)" + id="g864"> + <path + style="fill:none;stroke:#c8c8c8;stroke-width:14.89931583;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="m 274.44346,437.98197 79.87503,66.36332 V 698.87342 H 169.22033 V 504.34529 l 80.13837,-66.36306" + id="path837" + inkscape:connector-curvature="0" + sodipodi:nodetypes="cccccc" /> + <path + style="opacity:1;fill:none;fill-opacity:1;stroke:#c8c8c8;stroke-width:14.89931583;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" + id="path839-3" + sodipodi:type="arc" + sodipodi:cx="-420.58212" + sodipodi:cy="-313.05319" + sodipodi:rx="20.524721" + sodipodi:ry="20.524721" + sodipodi:start="0.40543239" + sodipodi:end="1.917446" + sodipodi:open="true" + d="m -401.7213,-304.95791 a 20.524721,20.524721 0 0 1 -25.83407,11.20855" + transform="rotate(-156.62757)" /> + </g> + </g> + <g + id="g848" + transform="translate(342.14286,-91.176621)"> + <path + inkscape:original-d="m -17.767337,666.45535 c 30.71351,-37.05555 61.4269,-74.11251 92.14015,-111.17087" + inkscape:path-effect="#path-effect906-26" + inkscape:connector-curvature="0" + id="path904-18" + d="M -17.767337,666.45535 C 12.945407,629.39916 43.658794,592.3422 74.372813,555.28448" + style="fill:none;stroke:#c8c8c8;stroke-width:15.01094818;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + <path + inkscape:original-d="M 83.888173,656.93999 C 46.832623,626.22647 9.7756729,595.51308 -27.282697,564.79983" + inkscape:path-effect="#path-effect906-20" + inkscape:connector-curvature="0" + id="path904-2" + d="M 83.888173,656.93999 C 46.831985,626.22724 9.7750303,595.51386 -27.282697,564.79983" + style="fill:none;stroke:#c8c8c8;stroke-width:15.01094818;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + </g> + </g> +</svg> diff --git a/tagit/assets/icons/scalable/tagging/edit_tag.svg b/tagit/assets/icons/scalable/tagging/edit_tag.svg new file mode 100644 index 0000000..c7d64e1 --- /dev/null +++ b/tagit/assets/icons/scalable/tagging/edit_tag.svg @@ -0,0 +1,303 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="100mm" + height="100mm" + id="svg2" + version="1.1" + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="edit_tag.svg" + inkscape:export-filename="../../kivy/objects/edit_tag.png" + inkscape:export-xdpi="7.6199999" + inkscape:export-ydpi="7.6199999"> + <defs + id="defs4"> + <inkscape:path-effect + effect="bspline" + id="path-effect906" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect850" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect850-3" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect850-7" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect850-7-3" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect850-7-6" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect906-9" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect906-2" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect906-9-0" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect906-93" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect906-9-6" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect906-26" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect906-9-9" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + <inkscape:path-effect + effect="bspline" + id="path-effect906-20" + is_visible="true" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + </defs> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="1.4" + inkscape:cx="-97.948957" + inkscape:cy="170.29629" + inkscape:document-units="mm" + inkscape:current-layer="layer1" + showgrid="false" + showguides="true" + inkscape:guide-bbox="true" + inkscape:snap-global="true" + inkscape:snap-bbox="true" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:window-width="1920" + inkscape:window-height="1151" + inkscape:window-x="0" + inkscape:window-y="25" + inkscape:window-maximized="1" + inkscape:pagecheckerboard="true" + units="mm" + inkscape:bbox-paths="true" + inkscape:bbox-nodes="true" + inkscape:snap-bbox-edge-midpoints="true" + inkscape:snap-bbox-midpoints="true" + inkscape:object-paths="true" + inkscape:snap-intersection-paths="true" + inkscape:snap-smooth-nodes="true" + inkscape:snap-midpoints="true" + inkscape:snap-object-midpoints="true" + inkscape:snap-center="true" + inkscape:snap-text-baseline="true" + inkscape:snap-page="true" + inkscape:lockguides="false"> + <sodipodi:guide + orientation="0,1" + position="13.637059,643.40404" + id="guide3788" + inkscape:locked="false" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="0,1" + id="guide1099" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="1,0" + id="guide1101" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + </sodipodi:namedview> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title /> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-167.28122,-322.85977)"> + <g + id="g1018" + transform="matrix(0.77049115,0.63859513,-0.63859513,0.77049115,403.95705,-187.73351)"> + <g + style="stroke:#c8c8c8;stroke-width:14.89931583;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + id="g870" + transform="matrix(0.00420274,-1.3209979,1.3209979,0.00420274,-397.55803,911.5166)"> + <circle + style="opacity:1;fill:none;fill-opacity:1;stroke:#c8c8c8;stroke-width:14.89931583;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" + id="path839" + cx="261.76941" + cy="510.56808" + r="23.687183" /> + <g + id="g864" + transform="translate(-9.9999667e-7,6.2227844)" + style="stroke:#c8c8c8;stroke-width:14.89931583;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"> + <path + sodipodi:nodetypes="cccccc" + inkscape:connector-curvature="0" + id="path837" + d="m 274.44346,437.98197 79.87503,66.36332 V 698.87342 H 169.22033 V 504.34529 l 80.13837,-66.36306" + style="fill:none;stroke:#c8c8c8;stroke-width:14.89931583;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + <path + transform="rotate(-156.62757)" + d="m -401.7213,-304.95791 a 20.524721,20.524721 0 0 1 -25.83407,11.20855" + sodipodi:open="true" + sodipodi:end="1.917446" + sodipodi:start="0.40543239" + sodipodi:ry="20.524721" + sodipodi:rx="20.524721" + sodipodi:cy="-313.05319" + sodipodi:cx="-420.58212" + sodipodi:type="arc" + id="path839-3" + style="opacity:1;fill:none;fill-opacity:1;stroke:#c8c8c8;stroke-width:14.89931583;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" /> + </g> + </g> + <path + inkscape:original-d="m 500.54332,620.98029 c -48.09424,-10e-4 -96.18948,-10e-4 -144.28572,0" + inkscape:path-effect="#path-effect906" + inkscape:connector-curvature="0" + id="path904" + d="m 500.54332,620.98029 c -48.09424,0 -96.18948,0 -144.28572,0" + style="fill:none;stroke:#c8c8c8;stroke-width:15;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + <path + inkscape:original-d="m 500.54332,567.766 c -48.09424,-10e-4 -96.18948,-10e-4 -144.28572,0" + inkscape:path-effect="#path-effect906-26" + inkscape:connector-curvature="0" + id="path904-18" + d="m 500.54332,567.766 c -48.09424,0 -96.18948,0 -144.28572,0" + style="fill:none;stroke:#c8c8c8;stroke-width:15;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + <path + inkscape:original-d="m 500.54332,514.55171 c -48.09424,-10e-4 -96.18948,-10e-4 -144.28572,0" + inkscape:path-effect="#path-effect906-20" + inkscape:connector-curvature="0" + id="path904-2" + d="m 500.54332,514.55171 c -48.09424,0 -96.18948,0 -144.28572,0" + style="fill:none;stroke:#c8c8c8;stroke-width:15;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + </g> + </g> +</svg> diff --git a/tagit/assets/icons/scalable/template.svg b/tagit/assets/icons/scalable/template.svg new file mode 100644 index 0000000..d7824c6 --- /dev/null +++ b/tagit/assets/icons/scalable/template.svg @@ -0,0 +1,96 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="100mm" + height="100mm" + id="svg2" + version="1.1" + inkscape:version="0.92.3 (2405546, 2018-03-11)" + sodipodi:docname="template.svg" + inkscape:export-filename="../../kivy/browser/template.png" + inkscape:export-xdpi="7.6199999" + inkscape:export-ydpi="7.6199999"> + <defs + id="defs4" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="0.9899495" + inkscape:cx="318.13476" + inkscape:cy="440.41168" + inkscape:document-units="mm" + inkscape:current-layer="layer1" + showgrid="false" + showguides="true" + inkscape:guide-bbox="true" + inkscape:snap-global="true" + inkscape:snap-bbox="true" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:window-width="1920" + inkscape:window-height="1151" + inkscape:window-x="0" + inkscape:window-y="25" + inkscape:window-maximized="1" + inkscape:pagecheckerboard="true" + units="mm" + inkscape:bbox-paths="true" + inkscape:bbox-nodes="true" + inkscape:snap-bbox-edge-midpoints="true" + inkscape:snap-bbox-midpoints="true" + inkscape:object-paths="true" + inkscape:snap-intersection-paths="true" + inkscape:snap-smooth-nodes="true" + inkscape:snap-midpoints="true" + inkscape:snap-object-midpoints="true" + inkscape:snap-center="true" + inkscape:snap-text-baseline="true" + inkscape:snap-page="true" + inkscape:lockguides="false"> + <sodipodi:guide + position="188.97638,188.97638" + orientation="0,1" + id="guide1099" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + <sodipodi:guide + position="188.97638,188.97638" + orientation="1,0" + id="guide1101" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,0,255)" /> + </sodipodi:namedview> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title /> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-167.28122,-322.85977)" /> +</svg> diff --git a/tagit/assets/required_schema.nt b/tagit/assets/required_schema.nt new file mode 100644 index 0000000..d48f0bd --- /dev/null +++ b/tagit/assets/required_schema.nt @@ -0,0 +1,110 @@ +# common external prefixes +prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> +prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> +prefix xsd: <http://www.w3.org/2001/XMLSchema#> +prefix schema: <http://schema.org/> + +# common bsfs prefixes +prefix bsfs: <https://schema.bsfs.io/core/> +prefix bsl: <https://schema.bsfs.io/core/Literal/> +prefix bsn: <https://schema.bsfs.io/ie/Node/> +prefix bse: <https://schema.bsfs.io/ie/Node/Entity#> +prefix bst: <https://schema.bsfs.io/ie/Node/Tag#> +prefix bsg: <https://schema.bsfs.io/ie/Node/Group#> +prefix bsp: <https://schema.bsfs.io/ie/Node/Preview#> + +# essential nodes +bsn:Entity rdfs:subClassOf bsfs:Node . +bsn:Preview rdfs:subClassOf bsfs:Node . +bsn:Tag rdfs:subClassOf bsfs:Node . +bsn:Group rdfs:subClassOf bsfs:Node . + +# common definitions +bsl:BinaryBlob rdfs:subClassOf bsfs:Literal . +bsl:URI rdfs:subClassOf bsfs:Literal . +bsl:Number rdfs:subClassOf bsfs:Literal . +bsl:Time rdfs:subClassOf bsfs:Literal . +<https://schema.bsfs.io/ie/Literal/BinaryBlob/JPEG> rdfs:subClassOf bsl:BinaryBlob . +xsd:string rdfs:subClassOf bsfs:Literal . +xsd:integer rdfs:subClassOf bsl:Number . +xsd:float rdfs:subClassOf bsl:Number . + +bse:filename rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsn:Entity ; + rdfs:range xsd:string ; + bsfs:unique "true"^^xsd:boolean . + +bse:filesize rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsn:Entity ; + rdfs:range xsd:integer ; + bsfs:unique "true"^^xsd:boolean . + +bse:mime rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsn:Entity ; + rdfs:range xsd:string ; + bsfs:unique "true"^^xsd:boolean . + +bse:preview rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsn:Entity ; + rdfs:range bsn:Preview . + +bse:tag rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsn:Entity ; + rdfs:range bsn:Tag . + +bst:label rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsn:Tag ; + rdfs:range xsd:string ; + bsfs:unique "true"^^xsd:boolean . + +bse:comment rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:Node ; + rdfs:range xsd:string . + +bse:group rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsn:Entity ; + rdfs:range bsn:Group . + +bsg:label rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsn:Group ; + rdfs:range xsd:string ; + bsfs:unique "true"^^xsd:boolean . + +bsg:represented_by rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsn:Group ; + rdfs:range bsn:Entity ; + bsfs:unique "true"^^xsd:boolean . + +bse:longitude rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsn:Entity ; + rdfs:range xsd:float ; + bsfs:unique "true"^^xsd:boolean . + +bse:latitude rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsn:Entity ; + rdfs:range xsd:float ; + bsfs:unique "true"^^xsd:boolean . + + +## preview nodes + +bsp:width rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsn:Preview ; + rdfs:range xsd:integer ; + bsfs:unique "true"^^xsd:boolean . + +bsp:height rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsn:Preview ; + rdfs:range xsd:integer ; + bsfs:unique "true"^^xsd:boolean . + +bsp:orientation rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsn:Preview ; + rdfs:range xsd:integer ; + bsfs:unique "true"^^xsd:boolean . + +bsp:asset rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsn:Preview ; + rdfs:range <https://schema.bsfs.io/ie/Literal/BinaryBlob/JPEG> ; + bsfs:unique "true"^^xsd:boolean . + diff --git a/tagit/assets/themes/default/style.kv b/tagit/assets/themes/default/style.kv new file mode 100644 index 0000000..71b4cb7 --- /dev/null +++ b/tagit/assets/themes/default/style.kv @@ -0,0 +1,212 @@ + +# DEBUG: Draw borders around all widgets +#<Widget>: +# canvas.after: +# Line: +# rectangle: self.x+1,self.y+1,self.width-1,self.height-1 +# dash_offset: 5 +# dash_length: 3 + +# color definitions +#:set colors_background [0x1c/256, 0x1b/256, 0x22/256] # dark grey +#:set colors_text [0xc5/256, 0xc9/256, 0xc7/256] # silver +#:set colors_highlight [0xb5/256, 0x94/256, 0x10/256] # darkgold + +# generic styles + +<Label>: + # default text color + color: colors_text + + +# main window elements + +<MainWindow>: + # background color + canvas.before: + Color: + rgb: colors_background + Rectangle: + pos: self.pos + size: self.size + +<HGuide>: # Horizontal guide + canvas: + Color: + rgb: colors_text + Line: + points: self.x, self.center_y, self.x + self.width, self.center_y + width: 2 + + +# browser elements + +<BrowserItem>: + canvas.after: + # selection highlighting + Color: + rgba: colors_highlight + [1 if self.is_selected else 0] + + # checkmarks + #Ellipse: + # pos: self.x + 5, self.y + self.height - 30 + # size: 25, 25 + #Color: + # rgba: [1,1,1] + [1 if self.is_selected else 0] + #Line: + # width: 3 + # points: + # self.x + 12, self.y + self.height - 20, \ + # self.x + 17, self.y + self.height - 23, \ + # self.x + 22, self.y + self.height - 12 + + # border highlight + Line: + width: 2 + points: + self.x, self.y + self.height - 60, \ + self.x, self.y + self.height, \ + self.x + 60, self.y + self.height + Triangle: + points: + self.x, self.y + self.height - 40, \ + self.x, self.y + self.height, \ + self.x + 40, self.y + self.height + #Line: + # width: 2 + # points: + # self.x + self.width - 40, \ + # self.y, self.x + self.width, \ + # self.y, self.x + self.width, self.y + 40 + + # cursor highlighting + Color: + rgba: colors_text + [1 if self.is_cursor else 0] + Line: + width: 2 + rectangle: self.x, self.y, self.width, self.height + + +# filter elements + +<Addressbar>: + 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) + +<Shingle>: + canvas.before: + Color: + rgba: colors_highlight + [0.25 if root.active else 0] + RoundedRectangle: + pos: root.pos + size: root.size + + canvas.after: + Color: + rgb: colors_text + Line: + rounded_rectangle: self.x+1, self.y+1, self.width-1, self.height-1, self.height/2 + +<Avatar>: + canvas.before: + Color: + rgb: colors_background + Ellipse: + pos: self.pos + size: self.size + + canvas.after: + Color: + rgb: colors_text + Line: + width: 2 + circle: self.center_x, self.center_y, self.height/2.0 + + + +<ShingleText>: + canvas.after: + Color: + rgba: colors_background + [0.5 if not self.active else 0] + Rectangle: + pos: self.pos + size: self.size + +<ShingleRemove>: + background_color: colors_background + background_normal: '' + opacity: 0.5 + + +# other elements + +<TileTabularLine>: + spacing: 5 + +<TileTabularRow>: + spacing: 5 + +<Action>: + # 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 + + +<DialogueContentBase>: + canvas: + # mask main window + Color: + rgba: 0,0,0, 0.7 * self.parent._anim_alpha + Rectangle: + size: self.parent._window.size if self.parent._window else (0, 0) + + # solid background color + Color: + rgb: 1, 1, 1 + BorderImage: + source: self.parent.background + border: self.parent.border + pos: self.pos + size: self.size + +<DialogueTitle>: + font_size: '16sp' + bold: True + halign: 'center' + valing: 'middle' + canvas.before: + # Background + Color: + rgb: 0.2, 0.2, 0.2 + Rectangle: + size: self.size + pos: self.pos + + # top border + #Color: + # rgb: 0.5, 0.5, 0.5 + #Line: + # points: self.x, self.y + self.height, self.x + self.width, self.y + self.height + # width: 2 + + # bottom border + #Color: + # rgb: 0.5, 0.5, 0.5 + #Line: + # points: self.x, self.y, self.x + self.width, self.y + # width: 2 + + + +## EOF ## diff --git a/tagit/config/__init__.py b/tagit/config/__init__.py new file mode 100644 index 0000000..c9edb15 --- /dev/null +++ b/tagit/config/__init__.py @@ -0,0 +1,25 @@ +"""Configuration system. + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# standard imports +import typing + +# inner-module imports +from . import loader +from . import utils +from .loader import TAGITRC, DEFAULT_USER_CONFIG +from .schema import schema, declare, declare_title +from .settings import Settings, ConfigError +from .types import * + +# exports +__all__: typing.Sequence[str] = ( + 'Settings', + 'TAGITRC', + 'schema', + ) + +## EOF ## diff --git a/tagit/config/loader.py b/tagit/config/loader.py new file mode 100644 index 0000000..47a51fa --- /dev/null +++ b/tagit/config/loader.py @@ -0,0 +1,79 @@ +"""High-level config loading. + +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 shutil +import typing + +# inner-module imports +from .settings import Settings + +# constants + +TAGITRC = '.tagitrc' + +DEFAULT_USER_CONFIG = os.path.join(os.path.dirname(__file__), 'user-defaults.yaml') + +SETTINGS_PATH = [ + # user home + os.path.expanduser(os.path.join('~', TAGITRC)), + # installation directory + '/usr/share/tagit/settings', + '/usr/share/tagit/keybindings', + # module defaults + os.path.join(os.path.dirname(__file__), 'settings.yaml'), + ] + +# exports +__all__: typing.Sequence[str] = ( + 'load_settings', + ) + + +## code ## + +logger = logging.getLogger(__name__) + +def load_settings(path=None, verbose=0): + """Load application settings. + The settings are loaded from the specified *path* and from all default + search paths (see *SETTINGS_PATH*). More specific locations overwrite + less specific ones. Every config key comes with a default value that + applies if it is not specified in the config files. + """ + verbose = max(0, verbose) + + # build searchpaths + searchpaths = [] + searchpaths += [path] if path is not None else [] + searchpaths += SETTINGS_PATH + + # create default user config on first start + user_config = os.path.expanduser(os.path.join('~', TAGITRC)) + if os.path.exists(DEFAULT_USER_CONFIG) and not os.path.exists(user_config): + shutil.copy(DEFAULT_USER_CONFIG, user_config) + + # scan searchpaths + cfg = Settings() + for path in searchpaths[::-1]: + if verbose > 0 or cfg('session', 'verbose') > 0: + print(f'Loading settings from {path}') + + if path is not None and os.path.exists(path): + try: + cfg.update(Settings.Open(path, clear_defaults=False)) + + except TypeError as e: # schema violation + logger.critical(f'Encountered a config error while loading {path}: {e}') + raise e + + # update verbosity from argument + cfg.set(('session', 'verbose'), max(cfg('session', 'verbose'), verbose)) + return cfg + +## EOF ## diff --git a/tagit/config/schema.py b/tagit/config/schema.py new file mode 100644 index 0000000..7f1c17a --- /dev/null +++ b/tagit/config/schema.py @@ -0,0 +1,283 @@ +"""Definition of a configuration schema. + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# standard imports +from collections import abc +import logging +import typing + +# tagit imports +from tagit.utils import errors, fst + +# exports +__all__: typing.Sequence[str] = ( + 'ConfigSchema', + 'declare', + 'declare_title', + ) + + +## code ## + +logger = logging.getLogger(__name__) + +class IncompatibleTypes(Exception): + """Raised if a config key is declared multiple times with incompatible signatures.""" + pass + +class ConfigSchema(abc.Collection, abc.Callable, abc.Hashable): + """The config schema registers types, defaults, and documentation for + configuration keys. The specification of a config key can be accessed in + dict-style (schema[key]) or Settings-style (schema(key)). A global schema + is instantiated to be used by tagit modules to declare their config keys. + + In addition to config keys, the class supports titles for documentation + of configuration sections (essentially any part of a config key that has + no value assigned to it). + """ + def __init__(self): + self.config = dict() + self.titles = dict() + + ## interfaces + + def __hash__(self): + return hash((type(self), + tuple(sorted([(hash(k), hash(v)) for k, v in self.config.items()], key=fst)), + tuple(sorted([(hash(k), hash(v)) for k, v in self.titles.items()], key=fst)))) + + def __call__(self, *key): + """Return the definition of a *key*.""" + return self.config[key] + + def __getitem__(self, key): + """Return the definition of a *key*.""" + return self.config[key] + + def __contains__(self, key): + """Return True if the config key *key* was declared.""" + return key in self.config + + def __iter__(self): + """Iterate over all declared config keys.""" + return iter(self.config) + + def __len__(self): + """Return the number of declared config keys.""" + return len(self.config) + + def keys(self, titles=False): + """Return an iterator over all declared config keys. + If *titles* is True, also return the declared title keys. + """ + if titles: + return iter(set(self.config.keys()) | set(self.titles.keys())) + else: + return self.config.keys() + + ## titles extras + + def is_title(self, key): + """Return True if the *key* matches a section.""" + return key in self.titles and key not in self.config + + def get_title(self, key): + """Return the section title of *key*.""" + return self.titles[key] + + ## declaration interface + + def declare(self, key, type, default, + module=None, title=None, description=None, example=None): + """Declare a configuration key. + + A key cannot be declared multiple times unless it has the same type + annotation and default value. + + :param:`key` Configuration key as tuple + :param:`type` Value type definition + :param:`default` Default value + :param:`module` Declaring module + :param:`title` Reader friendly name + :param:`description` Verbose description of its effect + :param:`example` Usage example + + """ + if len(key) == 0: + raise errors.ProgrammingError('the config key must contain at least one item.') + + # FIXME: can't have a rule for a subkey + # e.g. ('session', ): String() and ('session', 'verbose'): Int() + key = tuple(key) + if key in self.config: + # declaration exists, check compatibility + if self.config[key].type == type and \ + self.config[key].default == default: + # types are compatible, set/overwrite values + self.config[key].modules = module + self.config[key].title = title + self.config[key].description = description + self.config[key].example = example + logger.warning(f'config schema: potentially overwriting key {key}') + else: + raise IncompatibleTypes(f'declaration of {key} violates a previous declaration') + + elif type.check(default): + self.config[key] = ConfigKey(key, type, default, module, title, description, example) + + else: + raise errors.ProgrammingError('default value violates value type specification') + + def declare_title(self, key, module, title, description=None): + """Declare a config section title. Section titles are only used for + documentation purposes. + + :param:`key` Configuration key as tuple + :param:`module` Declaring module + :param:`title` Reader friendly name + :param:`description` Verbose description of its effect + + """ + if len(key) == 0: + raise errors.ProgrammingError('the config key must contain at least one item.') + + key = tuple(key) + if key in self.titles: + self.titles[key].title = title + self.titles[key].modules = module + self.titles[key].description = description + logger.warn(f'config schema: potentially overwriting title {key}') + else: + self.titles[key] = ConfigTitle(key, title, module, description) + + +class ConfigTitle(abc.Hashable): + """Title and description of a config key. Used for documentation.""" + def __init__(self, key, title=None, module=None, description=None): + self._key = key + self._title = title + self._description = description + self._modules = {module} if module is not None else set() + + def __repr__(self): + return f'ConfigTitle({self.key}, {self.title})' + + def __eq__(self, other): + return isinstance(other, type(self)) and self._key == other._key + + def __hash__(self): + return hash((type(self), self._key)) + + @property + def branch(self): + """Return the branch.""" + return self._key[:-1] + + @property + def leaf(self): + """Return the leaf.""" + return self._key[-1] + + @property + def key(self): + """Return the key.""" + return self._key + + @property + def title(self): + """Return the key's title.""" + return self._title if self._title is not None else self.leaf + + @title.setter + def title(self, title): + """Overwrite the key's title.""" + if title is not None and title != '': + self._title = title + + @property + def description(self): + """Return the key's description.""" + return self._description if self._description is not None else '' + + @description.setter + def description(self, description): + """Overwrite the key's description.""" + if description is not None and description != '': + self._description = description + + @property + def modules(self): + """Return the module names that declared the key.""" + return self._modules + + @modules.setter + def modules(self, module): + """Add another declaring module.""" + if module is not None and module != '': + self._modules.add(module) + + +class ConfigKey(ConfigTitle): + """Define the type and default value of a configuration key.""" + def __init__(self, key, type, default, module=None, title=None, + description=None, example=None): + super(ConfigKey, self).__init__(key, title, module, description) + self._type = type + self._default = default + self._examples = {example} if example is not None else set() + + def __repr__(self): + return f'ConfigKey({self.key}, {self.type}, {self.default})' + + def __eq__(self, other): + return super(ConfigKey, self).__eq__(other) and \ + self._type == other._type and \ + self._default == other._default + + def __hash__(self): + return hash((super(ConfigKey, self).__hash__(), self._type, self._default)) + + def check(self, value): + """Return True if *value* adheres to the key's type specification.""" + return self.type.check(value) + + def backtrack(self, value): + """Return True if *value* matches the key's type, raises a TypeError otherwise.""" + self.type.backtrack(value, '.'.join(self.key)) + return True + + @property + def default(self): + """Return the default value.""" + return self._default + + @property + def type(self): + """Return the type definition.""" + return self._type + + @property + def example(self): + """Return an example value.""" + return ', '.join(self._examples) if len(self._examples) else self.type.example + + @example.setter + def example(self, example): + """Add more examples for the key.""" + if example is not None and example != '': + self._examples.add(example) + +## global instance + +schema = ConfigSchema() + +def declare(*args, **kwargs): + schema.declare(*args, **kwargs) + +def declare_title(*args, **kwargs): + schema.declare_title(*args, **kwargs) + +## EOF ## diff --git a/tagit/config/settings.py b/tagit/config/settings.py new file mode 100644 index 0000000..190268c --- /dev/null +++ b/tagit/config/settings.py @@ -0,0 +1,479 @@ +"""Configuration storage. + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# standard imports +from collections import abc +from copy import deepcopy +import io +import json +import os +import typing + +# external imports +import yaml # FIXME: mb/port/convenicence + +# tagit imports +from tagit.utils import errors, fst, is_list + +# inner-module imports +from . import types +from .schema import schema as global_schema +from .utils import key_starts_with, superkey_of, subkey_of + +# constants +INDENT = 4 + +# exports +__all__: typing.Sequence[str] = ( + 'Settings', + ) + + +## code ## + +class ConfigError(TypeError): pass + +class Settings(abc.MutableMapping, abc.Hashable, abc.Callable): + """Access and modify config keys in a dict-like manner. + + It's assumed that the schema might not be available for all config + elements. That's because it might be declared in code that is + not yet or will never be loaded. In such cases, any value is accepted. + If the schema is known, however, it is enforced. + """ + + ## construction + + def __init__(self, schema=None, prefix=None, data=None): + self.prefix = tuple(prefix) if prefix is not None else tuple() + self.schema = schema if schema is not None else global_schema + self.config = data if data is not None else dict() + + @classmethod + def Open(cls, source, schema=None, on_error='raise', clear_defaults=True): + schema = schema if schema is not None else global_schema + config_path = '' + # load source + if isinstance(source, dict): # dictionary + config = source + elif isinstance(source, str): # path or serialized + if os.path.exists(source): + config_path = os.path.realpath(source) + with open(source, 'r') as ifile: + #config = json.load(ifile) + config = yaml.safe_load(ifile) # FIXME: mb/port/convenicence + else: + #config = json.loads(source) + config = yaml.safe_load(source) # FIXME: mb/port/convenicence + elif isinstance(source, io.TextIOBase): # opened file + #config = json.load(source) + config = yaml.safe_load(source) # FIXME: mb/port/convenicence + else: + raise TypeError('expected dict, path, or file-like') + + # flatten and verify + data = None + if len(config) > 0: + data = cls.flatten_tree(config, schema, on_error=on_error) + if clear_defaults: + # filter defaults + data = {key: value + for key, value in data.items() + if key not in schema or value != schema[key].default} + + data['session', 'paths', 'config'] = config_path + return cls(schema=schema, data=data) + + def update(self, other): + for key, value in other.config.items(): + self.set(key, value) + return self + + def rebase(self, schema=None, on_error='raise', clear_defaults=True): + """Re-align the config with the current schema. + Should be done if the schema changes *after* the Settings was initialized. + Can also be used to enforce a new schema on the current config. + + Be aware that calling rebase will disconnect Settings instances from + each other. For example, this affects non-leaf key retrieval via + get such as cfg('session') + """ + schema = self.schema if schema is None else schema + # unroll + tree = dict() + for key, value in self.config.items(): + path, leaf = list(key[:-1]), key[-1] + # navigate through the path + branch = tree + while len(path): + curr = path.pop(0) + if curr not in branch: + branch[curr] = dict() + branch = branch[curr] + + branch[leaf] = value + + # flatten the unrolled config + flat = self.flatten_tree(tree, schema, on_error=on_error) + # remove defaults + if clear_defaults: + flat = {key: value + for key, value in flat.items() + if key not in schema or value != schema[key].default} + # set new schema and config + self.config = flat + self.schema = schema + + ## comparison + + def __eq__(self, other): + return isinstance(other, Settings) and \ + self.schema == other.schema and \ + self.config == other.config and \ + self.prefix == other.prefix + + def __hash__(self): + return hash((type(self), + self.prefix, + hash(self.schema), + hash(tuple(sorted(self.config.items(), key=fst))))) + + def __str__(self): + return str({k: v for k, v in self.config.items() if key_starts_with(k, self.prefix)}) + + def __repr__(self): + prefix = ','.join(self.prefix) + size_self = len([key for key in self.config if key_starts_with(key, self.prefix)]) + size_all = len(self) + return f'Settings(prefix=({prefix}), keys={size_self}, len={size_all})' + + ## conversion + + def clone(self): + return Settings(schema=self.schema, prefix=self.prefix, data=deepcopy(self.config)) + + @staticmethod + def flatten_tree(hdict, schema, on_error='raise', prefix=tuple()): + """Flattens a hierarchical dictionary by using schema information. + Returns a flat list of config keys and their values. + + If an invalid type was found and on_error is 'raise, a TypeError is raised. + Otherwise the invalid key is ignored. + """ + if len(hdict) == 0: + # not in schema, or passed the check + return {prefix: dict()} + + flat = dict() + for sub in hdict: + try: + key = prefix + (sub, ) + # check schema first, to preserve dict types + if key in schema and schema[key].backtrack(hdict[sub]): + # accept the value (also defaults!) + flat[key] = hdict[sub] + + elif isinstance(hdict[sub], dict): + flat.update(Settings.flatten_tree(hdict[sub], schema, on_error, key)) + + elif any(key_starts_with(k, key) for k in schema): + subkeys = [k[len(key):] for k in schema if key_starts_with(k, key)] + subkeys = ','.join('.'.join(k) for k in subkeys) + raise ConfigError( + f'found value {hdict[sub]} in {key}, expected keys ({subkeys})') + + else: + # terminal but not in schema; accept + flat[key] = hdict[sub] + + except TypeError as e: + if on_error == 'raise': + raise e + + return flat + + def to_tree(self, defaults=False): + """Return a nested dictionary with all config values. + If *defaults*, the schema defaults are included. + """ + tree = dict() + source = set(self.config.keys()) + if defaults: + source |= set(self.schema.keys()) + + for key in source: + if not key_starts_with(key, self.prefix): + continue + value = self.get(*key[len(self.prefix):]) + path, leaf = list(key[:-1]), key[-1] + path = path[len(self.prefix):] + # navigate through the path + branch = tree + while len(path): + curr = path.pop(0) + if curr not in branch: + branch[curr] = dict() + branch = branch[curr] + + branch[leaf] = value + + return tree + + def file_connected(self): + """Return True if the config is backed by a file.""" + return self('session', 'paths', 'config') is not None and \ + self('session', 'paths', 'config') != '' + + def save(self, uri=None): + """Save changes to a file at *uri*.""" + # pick defaults + uri = uri if uri is not None else self('session', 'paths', 'config') + if uri is None or uri == '': + raise ValueError('config saving requires a valid uri') + + # convert to tree + config = self.to_tree(defaults=False) + + # save to file + if isinstance(uri, io.TextIOBase): + json.dump(config, uri, indent=INDENT) + else: + with open(uri, 'w') as ofile: + json.dump(config, ofile, indent=INDENT) + + def diff(self, other): + """Return a config that includes only the keys which differ from *other*.""" + # keys in self that differ from other + config = {key: value + for key, value in self.config.items() + if key not in other.config or value != other.config[key] + } + # keys in other that differ from default + config.update({key: self.schema[key].default + for key, value in other.config.items() + if key not in self.config and \ + key in self.schema and \ + value != self.schema[key].default + }) + + return Settings(schema=self.schema, prefix=self.prefix, data=deepcopy(config)) + + + ## getting + + def __getitem__(self, key): + """Alias for *get*.""" + if is_list(key): + return self.get(*key) + else: + return self.get(key) + + def __call__(self, *key, default=None): + """Alias for *get*.""" + return self.get(*key, default=default) + + def get(self, *key, default=None): + key = self.prefix + key + + # known leaf + if key in self.config: + value = self.config[key] + if key in self.schema: + if self.schema[key].check(value): + return value + elif default is not None and self.schema[key].check(default): + return default + else: + return self.schema[key].default + else: + return value + + # unknown leaf + if key in self.schema: + if default is not None and self.schema[key].check(default): + return default + else: + return self.schema[key].default + + # branch + if any(key_starts_with(sub, key) for sub in self.config): + return Settings(schema=self.schema, prefix=key, data=self.config) + elif any(key_starts_with(sub, key) for sub in self.schema): + return Settings(schema=self.schema, prefix=key, data=self.config) + + if default is not None: + return default + + raise KeyError(key) + + ## checking + + def __contains__(self, key): + """Alias for *has*.""" + return self.has(*key) + + def has(self, *key): + key = self.prefix + key + + if key in self.config: + # key is a known leaf + return True + elif key in self.schema: + # key is an unknown leaf + return True + else: + # key might be a branch + for sub in self.config: + if key_starts_with(sub, key): + return True + + for sub in self.schema: + if key_starts_with(sub, key): + return True + + return False + + ## setting + + def __setitem__(self, key, value): + """Alias for *set*.""" + if is_list(key): + self.set(key, value) + else: + self.set((key, ), value) + + def set(self, key, value): + key = self.prefix + key + + if key in self.schema and self.schema[key].backtrack(value): + if self.schema[key].default != value: + self.config[key] = value + elif key in self.config: # value is default + # reset value to default, remove from config + del self.config[key] + # else: value was default but not present, ignore + + elif key in self.config: + self.config[key] = value + + elif isinstance(value, dict) and len(value) > 0: + # flatten value and set its items individually + subtree = self.flatten_tree(value, self.schema, prefix=key) + for subkey, subval in subtree.items(): + # defaults will be filtered by set + self.set(subkey[len(self.prefix):], subval) + + elif superkey_of(key, self.schema) or subkey_of(key, self.schema): + # schema violation in another branch + conflicts = {'.'.join(sub) + for sub in self.schema + if key_starts_with(sub, key) or key_starts_with(key, sub)} + raise ConfigError(f'{key} conflicts with schema keys {",".join(conflicts)}') + + elif superkey_of(key, self.config): + # it's allowed to overwrite dict-like config values + # Example: + # having defined session.paths.preview.files = 'somewhere' + # it's allowed to set session.paths.preview = {} + # It's admissible iff: + # * the value is an empty dict + # * no subkey is in the schema (already checked in the case above) + if value == dict(): + self.unset(*key) + self.config[key] = value + else: + conflicts = {'.'.join(sub) + for sub in self.schema + if key_starts_with(sub, key) or key_starts_with(key, sub)} + raise ConfigError(f'{key} conflicts with config keys {",".join(conflicts)}') + + elif subkey_of(key, self.config): + # it's allowed to overwrite dict-like config values + # Example: + # having defined session.paths.preview = {} + # it's allowed to set session.paths.preview.files = 'somewhere' + # It's admissible iff: + # * the superkey is an empty dict + # * no subkey of the superkey is in the schema + sups = [sup for sup in self.config if key_starts_with(key, sup)] + if len(sups) != 1: + # there can only be one super-key + raise errors.ProgrammingError(f'expected one superkey, found {len(sups)}') + + sup = sups[0] + if self.config[sup] == dict() and \ + len({sub for sub in self.schema if key_starts_with(sup, sub)}) == 0: + del self.config[sup] + self.config[key] = value + else: + # already have a superkey in the config that cannot be overwritten + conflicts = '.'.join(sup) + raise ConfigError(f'{key} conflicts with config keys {conflicts}') + + else: + self.config[key] = value + + return self + + ## removal + + def __delitem__(self, key): + """Alias for *unset*.""" + if is_list(key): + self.unset(*key) + else: + self.unset(key) + + def unset(self, *key): + key = self.prefix + key + + if key in self.config: + # key is a leaf + del self.config[key] + else: + # key might be a branch + subs = [sub for sub in self.config if key_starts_with(sub, key)] + for sub in subs: + del self.config[sub] + + return self + + ## iteration + + def __iter__(self): + """Alias for *keys*.""" + return self.keys() + + def keys(self): + for key in set(self.config.keys()) | set(self.schema.keys()): + if key_starts_with(key, self.prefix): + yield key[len(self.prefix):] + + def items(self): + for key in self.keys(): + yield key, self.get(*key) + + ## properties + + def __len__(self): + return len(list(self.keys())) + + +## config ## + +global_schema.declare(('session', 'verbose'), types.Unsigned(), 0, + __name__, 'Verbosity', 'Print additional information in various places of the application.') + +global_schema.declare(('session', 'debug'), types.Bool(), False, + __name__, 'Debug mode', 'Enable debug output and debug behaviour. Should be set to false in a productive environment.') + +global_schema.declare(('session', 'paths', 'config'), types.Path(), '', + __name__, 'Config path', "The path of the session's main configuration file. Is set automatically and for internal use only.") + +global_schema.declare(('storage', 'config', 'write_through'), types.Bool(), True, + __name__, 'Write-through', "Write the config to its file whenever it changes") + +## EOF ## diff --git a/tagit/config/settings.yaml b/tagit/config/settings.yaml new file mode 100644 index 0000000..e9264d4 --- /dev/null +++ b/tagit/config/settings.yaml @@ -0,0 +1,10 @@ +ui: + standalone: + keytriggers: [] + buttondocks: + navigation_left: [] + navigation_right: [] + filter: [] + status: [] + context: + root: [] diff --git a/tagit/config/types.py b/tagit/config/types.py new file mode 100644 index 0000000..3dc3d38 --- /dev/null +++ b/tagit/config/types.py @@ -0,0 +1,273 @@ +""" + +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 import errors, is_list + +# exports +__all__: typing.Sequence[str] = ( + 'ConfigTypeError', + # types + 'Any', + 'Bool', + 'Dict', + 'Enum', + 'Float', + 'Int', + 'Keybind', + 'List', + 'Numeric', + 'Path', + 'String', + 'Unsigned', + ) + +# TODO: Bounded int or range? (specify lo/hi bounds) +# TODO: File vs. Dir; existence condition? + +## code ## + +# base class + +class ConfigTypeError(TypeError): + """Raised if a type inconsistency is detected.""" + pass + +class ConfigType(object): + """A config type defines a constraint over admissible values in order to + perform a basic verification of user-entered config values. + """ + + # example values + example = '' + + # type description + description = '' + + def __str__(self): + return f'{type(self).__name__}' + + def __repr__(self): + return f'{type(self).__name__}()' + + def __eq__(self, other): + return isinstance(other, type(self)) + + def __hash__(self): + return hash(type(self)) + + def check(self, value): + """Return True if the *value* matches the type.""" + try: + self.backtrack(value, '') + return True + except ConfigTypeError: + return False + + def backtrack(self, value, key): + """Check *value* for errors. + Raises a ConfigTypeError with a detailed message if an inconsistency is detected. + """ + errors.abstract() + +# generic types + +class Any(ConfigType): + example = '1, "a", [1,2,"a"]' + description = 'Any type' + + def backtrack(self, value, key): + # accepts anything + pass + + +class Bool(ConfigType): + example = 'True, False' + description = 'Boolean' + + def backtrack(self, value, key): + if not isinstance(value, bool): + raise ConfigTypeError(f'found {value} in {key}, expected a boolean') + + +class Keybind(ConfigType): + example = '[("a", ["ctrl"], [])]' + description = 'A list of (key, required modifiers, excluded modifiers)-triples' + + def backtrack(self, value, key): + if not is_list(value): + raise ConfigTypeError(f'found {type(value)} in {key}, expected a list of bindings') + + modifiers = {'shift', 'alt', 'ctrl', 'cmd', 'altgr', 'rest', 'all'} + for idx, itm in enumerate(value): + if not is_list(itm) or len(itm) != 3: + raise ConfigTypeError(f'found {itm} in {key}[{idx}], expected a list of three') + + char, inc, exc = itm + if not isinstance(char, str) and \ + not isinstance(char, int) and \ + not isinstance(char, float): + raise ConfigTypeError( + f'found {char} in {key}[{idx}], expected a character or number') + if not is_list(inc) or not set(inc).issubset(modifiers): + mods = ','.join(modifiers) + raise ConfigTypeError(f'found {inc} in {key}[{idx}], expected some of ({mods})') + if not is_list(exc) or not set(exc).issubset(modifiers): + mods = ','.join(modifiers) + raise ConfigTypeError(f'found {exc} in {key}[{idx}], expected some of ({mods})') + + +# numeric types + +class Numeric(ConfigType): + pass + + +class Int(Numeric): + example = '-8, -1, 0, 1, 3' + description = 'Integer number' + + def backtrack(self, value, key): + if not isinstance(value, int): + raise ConfigTypeError(f'found {value} in {key}, expected an integer') + + +class Unsigned(Int): + example = '0, 1, 13, 32' + description = 'Non-negative integer number, including zero' + + def __str__(self): + return 'Unsigned int' + + def backtrack(self, value, key): + if not isinstance(value, int) or value < 0: + raise ConfigTypeError(f'found {value} in {key}, expeced an integer of at least zero') + + +class Float(Numeric): + example = '1.2, 3.4, 5, 6' + description = 'Integer or Decimal number' + + def backtrack(self, value, key): + if not isinstance(value, float) and not isinstance(value, int): + raise ConfigTypeError(f'found {value} in {key}, expected a number') + + +# string types + +class String(ConfigType): + example = '"hello world", "", "foobar"' + description = 'String' + + def backtrack(self, value, key): + if not isinstance(value, str): + raise ConfigTypeError(f'found {value} in {key}, expected a string') + + +class Path(String): + example = '"/tmp", "Pictures/trip", "~/.tagitrc"' + description = 'String, compliant with file system paths' + + +# compound types + +class Enum(ConfigType): + description = 'One out of a predefined set of values' + + @property + def example(self): + return ', '.join(str(o) for o in list(self.options)[:3]) + + def __init__(self, *options): + self.options = set(options[0] if len(options) == 1 and is_list(options[0]) else options) + + def __eq__(self, other): + return super(Enum, self).__eq__(other) and \ + self.options == other.options + + def __hash__(self): + return hash((super(Enum, self).__hash__(), tuple(self.options))) + + def __str__(self): + options = ', '.join(str(itm) for itm in self.options) + return f'One out of ({options})' + + def __repr__(self): + return f'{type(self).__name__}([{self.options}])' + + def backtrack(self, value, key): + try: + if value not in self.options: + raise Exception() + except Exception: + options = ','.join(str(itm) for itm in self.options) + raise ConfigTypeError(f'found {value} in {key}, expected one out of ({options})') + + +class List(ConfigType): + description = 'List of values' + + @property + def example(self): + return f'[{self.item_type.example}]' + + def __init__(self, item_type): + self.item_type = item_type + + def __eq__(self, other): + return super(List, self).__eq__(other) and \ + self.item_type == other.item_type + + def __hash__(self): + return hash((super(List, self).__hash__(), hash(self.item_type))) + + def __str__(self): + return f'List of {str(self.item_type)}' + + def __repr__(self): + return f'{type(self).__name__}({self.item_type})' + + def backtrack(self, value, key): + if not isinstance(value, list) and not isinstance(value, tuple): + raise ConfigTypeError(f'found {type(value)} in {key}, expected list') + for item in value: + self.item_type.backtrack(item, key) + + +class Dict(ConfigType): + example = '{"hello": "world"}; {"hello": 3}; {"hello": [1, 2, 3]}' + description = 'Map of keys/values' + + def __init__(self, key_type, value_type): + self.key_type = key_type + self.value_type = value_type + + def __eq__(self, other): + return super(Dict, self).__eq__(other) and \ + self.key_type == other.key_type and \ + self.value_type == other.value_type + + def __hash__(self): + return hash((super(Dict, self).__hash__(), hash(self.key_type), hash(self.value_type))) + + def __str__(self): + return f'Dict from {self.key_type} to {self.value_type}' + + + def __repr__(self): + return f'{type(self).__name__}({self.key_type}, {self.value_type})' + + def backtrack(self, value, key): + if not isinstance(value, dict): + raise ConfigTypeError(f'found {type(value)} in {key}, expected a dict') + for subkey, subval in value.items(): + self.key_type.backtrack(subkey, str(key) + '.' + str(subkey)) + self.value_type.backtrack(subval, str(key) + '.' + str(subkey)) + +## EOF ## diff --git a/tagit/config/user-defaults.yaml b/tagit/config/user-defaults.yaml new file mode 100644 index 0000000..447e10f --- /dev/null +++ b/tagit/config/user-defaults.yaml @@ -0,0 +1,107 @@ +session: + bsfs: + Graph: + backend: + SparqlStore: {} + user: 'http://example.com/me' + script: + - Search +ui: + standalone: + window_size: [1440, 810] + #maximize: True + keytriggers: + - MoveCursorUp + - MoveCursorDown + - MoveCursorLeft + - MoveCursorRight + - MoveCursorLast + - MoveCursorFirst + - NextPage + - PreviousPage + - ScrollDown + - ScrollUp + - ZoomIn + - ZoomOut + - Select + - SelectAll + - SelectNone + - SelectMulti + - SelectRange + - AddToken + - GoBack + - GoForth + - AddTag + - EditTag + - Search + - ShowSelected + - RemoveSelected + browser: + cols: 4 + rows: 3 + maxcols: 8 + maxrows: 8 + buttondocks: + navigation_left: + - MoveCursorFirst + - PreviousPage + - ScrollUp + navigation_right: + - ScrollDown + - NextPage + - MoveCursorLast + filter: + - GoBack + - GoForth + status: + - ZoomIn + - ZoomOut + sidebar_left: + - AddTag + - EditTag + - ShowSelected + - RemoveSelected + - SelectAll + - SelectNone + - SelectInvert + - SelectSingle + - SelectRange + - SelectMulti + - SelectAdditive + - SelectSubtractive + context: + app: + - ShowHelp + - ShowConsole + - ShowSettings + browser: + - ZoomIn + - ZoomOut + - MoveCursorFirst + - PreviousPage + - ScrollUp + - ScrollDown + - NextPage + - MoveCursorLast + search: + - AddToken + - SortOrder + - ShowSelected + - RemoveSelected + - GoForth + - GoBack + select: + - SelectAll + - SelectNone + - SelectInvert + - SelectSingle + - SelectMulti + - SelectRange + - SelectAdditive + - SelectSubtractive + tagging: + - AddTag + - EditTag + tiledocks: + sidebar_right: + Info: {} diff --git a/tagit/config/utils.py b/tagit/config/utils.py new file mode 100644 index 0000000..948f53a --- /dev/null +++ b/tagit/config/utils.py @@ -0,0 +1,104 @@ +"""Configuration system utilities. + +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 import import_all + +# inner-module imports +from . import types + +# exports +__all__: typing.Sequence[str] = ( + 'key_starts_with', + 'schema_key_sort', + 'schema_to_rst', + 'subkey_of', + 'superkey_of', + ) + + +## code ## + +def key_starts_with(key, prefix): + """Return whether a config *key* starts with (is a subclass of) *prefix*.""" + return key[:len(prefix)] == prefix + +def subkey_of(key, pool): + """Return True if *key* is at a lower level than some key in *pool*. + Example: session.debug is a subkey of session. + """ + for sup in pool: + if key_starts_with(key, sup): + return True + return False + +def superkey_of(key, pool): + """Return True if *key* is at a higher level than some key in *pool*. + Example: session is a superkey of session.debug. + """ + for sub in pool: + if key_starts_with(sub, key): + return True + return False + +def schema_key_sort(schema): + """Return a comparison function for sorting schema config or title keys. + To be used in sorted or sort as key function. + + >>> sorted(schema.keys(titles=True), key=schema_keys_sort(schema)) + + """ + def cmp(key): + """Return an unambiguous representation of schema config or title keys.""" + return ('.'.join(key[:-1]) + '..' + key[-1]) \ + if not schema.is_title(key) \ + else ('.'.join(key) + '..') + + return cmp + +def schema_to_rst(schema, print_modules=False, no_import=False): + """Creates a documentation page in ReST of the config schema. + Calling this method with *no_import* set to False imports all + tagit submodules. + """ + # import all modules + if not no_import: + import tagit + import_all(tagit, exclude={'.*\.external'}) + + header = '=-^~"' + + known_sections = set() + for key in sorted(schema.keys(titles=True), key=schema_key_sort(schema)): + # print headings + for idx, sec in enumerate(key): + heading = '.'.join(key[:idx+1]) + if heading not in known_sections: + print('') + print(heading) + print(header[idx] * len(heading)) + known_sections.add(heading) + + if schema.is_title(key): + print(schema.get_title(key).description + '\n') + + else: + print(schema[key].description + '\n') + + print(f':Format: {str(schema[key].type)} ({schema[key].example})') + print(f':Default: {schema[key].default}') + + if isinstance(schema[key].type, types.Enum): + print(f':Options: {schema[key].type.options}') + + if print_modules: + modules = ', '.join(f'`{str(m)}`_' for m in schema[key].modules) + print(f':Modules: {modules}') + +## EOF ## diff --git a/tagit/dialogues/__init__.py b/tagit/dialogues/__init__.py new file mode 100644 index 0000000..beed253 --- /dev/null +++ b/tagit/dialogues/__init__.py @@ -0,0 +1,42 @@ +"""Popup dialogues. + +A dialogue can be opened from the main application. +It appears on top of the application and prevent its use until the dialogue +is closed. A dialogue contains buttons whose presses can be captured. + +>>> dlg = LabelDialogue(text='Hello world') +>>> dlg.bind(on_ok=...) +>>> dlg.bind(on_cancel=...) +>>> dlg.open() + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# standard imports +import typing + +# inner-module imports +from .autoinput import AutoTextInput +from .console import Console +from .error import Error +from .file_picker import FilePicker +from .message import Message +from .numeric_input import NumericInput +from .path_picker import PathPicker +from .simple_input import SimpleInput +from .stoken import TokenEdit + +# exports +__all__: typing.Sequence[str] = ( + 'Console', + 'Error', + 'FilePicker', + 'Message', + 'NumericInput', + 'PathPicker', + 'SimpleInput', + 'TokenEdit', + ) + +## EOF ## diff --git a/tagit/dialogues/autoinput.py b/tagit/dialogues/autoinput.py new file mode 100644 index 0000000..a036ed4 --- /dev/null +++ b/tagit/dialogues/autoinput.py @@ -0,0 +1,73 @@ +"""This is a simple example of how to use suggestion text. + +In this example you setup a word_list at the begining. In this case +'the the quick brown fox jumps over the lazy old dog'. This list along +with any new word written word in the textinput is available as a +suggestion when you are typing. You can press tab to auto complete the text. + +Based on & thanks to akshayaurora: + https://gist.github.com/akshayaurora/fa5a68980af585e355668e5adce5f98b + +Part of the tagit module. +A copy of the license is provided with the project. +Modifications authored by: Matthias Baumgartner, 2022 +""" +# standard imports +from bisect import bisect + +# kivy imports +from kivy.uix.textinput import TextInput +import kivy.properties as kp + +# exports +__all__ = ('AutoTextInput', ) + + +## code ## + +class AutoTextInput(TextInput): + + sep = kp.StringProperty(',') + suffix = kp.StringProperty(' ') + vocabulary = kp.ListProperty() + + def on_suggestion_text(self, wx, value): + if not value: + return + + super(AutoTextInput, self).on_suggestion_text(wx, value) + + def keyboard_on_key_down(self, window, keycode, text, modifiers): + if self.suggestion_text and keycode[1] == 'tab': # complete suggestion_text + self.insert_text(self.suggestion_text + self.sep + self.suffix) + self.suggestion_text = '' + return True + return super(AutoTextInput, self).keyboard_on_key_down(window, keycode, text, modifiers) + + def on_text(self, wx, value): + # include all current text from textinput into the word list + # the kind of behavior sublime text has + + # what's on the current line + temp = value[:value.rfind(self.sep)].split(self.sep) + temp = [s.strip() for s in temp] + # combine with static vocabulary + wordlist = sorted(set(self.vocabulary + temp)) + + # get prefix + prefix = value[value.rfind(self.sep)+1:].strip() + if not prefix: + return + + # binary search on (sorted) wordlist + pos = bisect(wordlist, prefix) + + # check if matching string found + if pos == len(wordlist) or not wordlist[pos].startswith(prefix): + self.suggestion_text = '' + return + + # fetch suffix from wordlist + self.suggestion_text = wordlist[pos][len(prefix):] + +## EOF ## diff --git a/tagit/dialogues/console.kv b/tagit/dialogues/console.kv new file mode 100644 index 0000000..b68227d --- /dev/null +++ b/tagit/dialogues/console.kv @@ -0,0 +1,30 @@ + +<Console>: + text: '' + title: 'tagit log console' + ok_on_enter: False + init_at_bottom: True + + DialogueContentTitle: + title: root.title + size_hint_y: 0.6 + + ScrollView: + scroll_y: 0 if root.init_at_bottom else 1 + + Label: + text: root.text + size_hint_y: None + height: self.texture_size[1] + text_size: self.width, None + bold: True + font_name: resource_find('DejaVuSansMono.ttf') # monospace font + markup: True + multiline: True + halign: 'left' + + DialogueButtons_One: + ok_text: "close" + # FIXME: Inform that this action will not terminate the underlying process + +## EOF ## diff --git a/tagit/dialogues/console.py b/tagit/dialogues/console.py new file mode 100644 index 0000000..282b378 --- /dev/null +++ b/tagit/dialogues/console.py @@ -0,0 +1,37 @@ +""" + +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.clock import mainthread +from kivy.lang import Builder +import kivy.properties as kp + +# inner-module imports +from .dialogue import Dialogue + +# exports +__all__ = ('Console', ) + + +## code ## + +# load kv +Builder.load_file(os.path.join(os.path.dirname(__file__), 'console.kv')) + +# classes +class Console(Dialogue): + """Dialogue with console output.""" + + text = kp.StringProperty('') + + @mainthread + def update(self, sender, text): + self.text = '\n'.join(text) + +## EOF ## diff --git a/tagit/dialogues/dialogue.kv b/tagit/dialogues/dialogue.kv new file mode 100644 index 0000000..e2cab66 --- /dev/null +++ b/tagit/dialogues/dialogue.kv @@ -0,0 +1,77 @@ +#:import get_root tagit.utils.get_root +# FIXME: remove need for get_root + +<-Dialogue>: + auto_dismiss: True + ok_on_enter: True + +<DialogueContentBase>: + orientation: 'vertical' + padding: '12dp' + size_hint: 0.66, None + height: self.minimum_height + + +<DialogueContentNoTitle@DialogueContentBase>: + # nothing to do + + +<DialogueTitle@Label>: + +<DialogueContentTitle@DialogueContentBase>: + title: '' + title_color: 1,1,1,1 + + DialogueTitle: + text: root.title + size_hint_y: None + height: self.texture_size[1] + dp(16) + text_size: self.width - dp(16), None + color: root.title_color + + # small space + Label: + size_hint_y: None + height: 12 + + + +<DialogueButton@Button>: + +<DialogueButtonRow>: + orientation: 'vertical' + size_hint_y: None + height: dp(48+8) + + # small space + Label: + size_hint_y: None + height: dp(8) + + # here come the buttons + + +<DialogueButtons_One@DialogueButtonRow>: + ok_text: 'OK' + + DialogueButton: + text: root.ok_text + on_press: get_root(self).ok() + + +<DialogueButtons_Two@DialogueButtonRow>: + cancel_text: 'Cancel' + ok_text: 'OK' + ok_enabled: True + + BoxLayout: + orientation: 'horizontal' + DialogueButton: + text: root.cancel_text + on_press: get_root(self).cancel() + DialogueButton: + text: root.ok_text + on_press: get_root(self).ok() + disabled: not root.ok_enabled + +## EOF ## diff --git a/tagit/dialogues/dialogue.py b/tagit/dialogues/dialogue.py new file mode 100644 index 0000000..bf72a28 --- /dev/null +++ b/tagit/dialogues/dialogue.py @@ -0,0 +1,108 @@ +"""Popup dialogue. + +Rougly based on code from https://gist.github.com/kived/742397a80d61e6be225a +by Ryan Pessa. The license is provided in the source folder. + +Part of the tagit module. +A copy of the license is provided with the project. +Modifications authored by: Matthias Baumgartner, 2022 +""" +# standard imports +import os + +# kivy imports +from kivy.lang import Builder +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.popup import Popup +import kivy.properties as kp + +# exports +__all__ = ('Dialogue', ) + + +## code ## + +# Load kv +Builder.load_file(os.path.join(os.path.dirname(__file__), 'dialogue.kv')) + +# classes +class Dialogue(Popup): + """Popup dialogue base class. + + Use like below: + + >>> dlg = Dialogue() + >>> dlg.bind(on_ok=....) + >>> dlg.open() + + """ + + ok_on_enter = kp.BooleanProperty() + + __events__ = ('on_ok', 'on_cancel') + + def __init__(self, *args, **kwargs): + super(Dialogue, self).__init__(*args, **kwargs) + from kivy.core.window import Window + # assumes that the first widget created controls the keyboard + #Window.children[-1].request_exclusive_keyboard() + # Alternatively, you can bind a function (self._on_keyboard) to on_keyboard + # which returns True. This stops the event from being processed by the main + # window. + # However, this still does not keep the 'enter' from 'on_text_validate' from + # being processed by the main window. + Window.bind(on_keyboard=self._on_keyboard) + # By binding to on_key_down, the <enter> key can trigger the ok action. + # This also prevents the enter event to be processed by the main window, + # unlike the 'on_text_validate' of TextInput. + Window.bind(on_key_down=self._key_down) + + def _on_keyboard(self, *args, **kwargs): + # block events from processing in the main window + return True + + def _key_down(self, instance, key, scancode, codepoint, modifiers): + if key == 13 and self.ok_on_enter: + self.ok() + return True + # must not stop other events such that ctrl up/down reach the browser + + def ok(self): + """User pressed the OK button.""" + self.dispatch('on_ok') + self.dismiss() + + def cancel(self): + """User pressed the Cancel button.""" + self.dispatch('on_cancel') + self.dismiss() + + def on_dismiss(self): + from kivy.core.window import Window + # assumes that the first widget created controls the keyboard + #Window.children[-1].release_exclusive_keyboard() + Window.unbind(on_keyboard=self._on_keyboard) + Window.unbind(on_key_down=self._key_down) + super(Dialogue, self).on_dismiss() + + def on_ok(self): + """Event prototype.""" + pass + + def on_cancel(self): + """Event prototype.""" + pass + +# helper classes + +# content bases +class DialogueContentBase(BoxLayout): pass +class DialogueContentTitle(DialogueContentBase): pass +class DialogueContentNoTitle(DialogueContentBase): pass + +# buttons +class DialogueButtonRow(BoxLayout): pass +class DialogueButtons_One(DialogueButtonRow): pass +class DialogueButtons_Two(DialogueButtonRow): pass + +## EOF ## diff --git a/tagit/dialogues/error.py b/tagit/dialogues/error.py new file mode 100644 index 0000000..d93f853 --- /dev/null +++ b/tagit/dialogues/error.py @@ -0,0 +1,45 @@ +"""Dialogue to show an error message. + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# kivy imports +from kivy.lang import Builder +import kivy.properties as kp + +# inner-module imports +from .dialogue import Dialogue + +# exports +__all__ = ('Error', ) + + +## code ## + +# load kv +Builder.load_string(''' +<Error>: + text: '' + ok_on_enter: False + + DialogueContentNoTitle: + + Label: + markup: True + text: root.text + size_hint_y: None + color: 1, 0, 0, 1 + height: self.texture_size[1] + dp(16) + text_size: self.width - dp(16), None + halign: 'center' + + DialogueButtons_One: +''') + +# classes +class Error(Dialogue): + """Error message.""" + text = kp.StringProperty('') + +## EOF ## diff --git a/tagit/dialogues/file_picker.py b/tagit/dialogues/file_picker.py new file mode 100644 index 0000000..283adb6 --- /dev/null +++ b/tagit/dialogues/file_picker.py @@ -0,0 +1,39 @@ +"""Dialogue to pick a file. + +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 + +# inner-module imports +from .path_picker import PathPicker +from .error import Error + +# exports +__all__ = ('FilePicker', ) + + +## code ## + +# load kv +Builder.load_string(''' +<FilePicker>: + dirselect: False + title: 'Please select a file' +''') + +# classes +class FilePicker(PathPicker): + """Dialogue with a file browser to select a file.""" + def ok(self): + if not os.path.exists(self.path) or not os.path.isfile(self.path): + Error(text='Please select a file').open() + else: + super(FilePicker, self).ok() + +## EOF ## diff --git a/tagit/dialogues/license.t b/tagit/dialogues/license.t new file mode 100644 index 0000000..bbd2830 --- /dev/null +++ b/tagit/dialogues/license.t @@ -0,0 +1,33 @@ + +The dialogues are based on the following code: + +https://gist.github.com/kived/742397a80d61e6be225a + +It ships with license, provided below: + +>>> The following license shall apply to all Public Gists owned by account. It +>>> shall never apply to any Secret Gists, for which no license of any sort is +>>> granted. +>>> +>>> Copyright (c) 2015- Ryan Pessa +>>> +>>> Permission is hereby granted, free of charge, to any person obtaining a copy +>>> of this software and associated documentation files (the "Software"), to deal +>>> in the Software without restriction, including without limitation the rights +>>> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +>>> copies of the Software, and to permit persons to whom the Software is +>>> furnished to do so, subject to the following conditions: +>>> +>>> The above copyright notice and this permission notice shall be included in +>>> all copies or substantial portions of the Software. +>>> +>>> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +>>> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +>>> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +>>> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +>>> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +>>> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +>>> THE SOFTWARE. + +Code modification are subject to the license of the tagit software. + diff --git a/tagit/dialogues/message.py b/tagit/dialogues/message.py new file mode 100644 index 0000000..ab67180 --- /dev/null +++ b/tagit/dialogues/message.py @@ -0,0 +1,47 @@ +"""Dialogue to show some message. + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# kivy imports +from kivy.lang import Builder +import kivy.properties as kp + +# inner-module imports +from .dialogue import Dialogue + +# exports +__all__ = ('Message', ) + + +## code ## + +# load kv +Builder.load_string(''' +<Message>: + text: '' + align: 'center' + textcolor: 1,1,1,1 + + DialogueContentNoTitle: + Label: + text: root.text + size_hint_y: None + color: root.textcolor + height: self.texture_size[1] + dp(16) + text_size: self.width - dp(16), None + halign: root.align + markup: True + + DialogueButtons_One: +''') + +# classes +class Message(Dialogue): + """Dialogue to show a text message.""" + text = kp.StringProperty() + align = kp.StringProperty() + textcolor: kp.ListProperty() + +## EOF ## diff --git a/tagit/dialogues/numeric_input.py b/tagit/dialogues/numeric_input.py new file mode 100644 index 0000000..63491d2 --- /dev/null +++ b/tagit/dialogues/numeric_input.py @@ -0,0 +1,72 @@ +"""Dialogue with a slider. + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# kivy imports +from kivy.lang import Builder +import kivy.properties as kp + +# inner-module imports +from .dialogue import Dialogue + +# exports +__all__ = ('NumericInput', ) + + +## code ## + +# load kv +Builder.load_string(''' + +<NumericInput>: + value: int(slider.value) + init_value: 0 + + DialogueContentNoTitle: + + BoxLayout: + orientation: 'horizontal' + size_hint_y: None + height: 50 + + Label: + text: str(root.lo) + size_hint_x: None + width: self.texture_size[0] + + Slider: + id: slider + orientation: 'horizontal' + min: root.lo + max: root.hi + step: 1 + value: root.init_value + + Label: + text: str(root.hi) + size_hint_x: None + width: self.texture_size[0] + + Label: + text: str(root.value) + size_hint: 1, None + height: self.texture_size[1] + halign: 'center' + + DialogueButtons_Two: + +''') + +# classes +class NumericInput(Dialogue): + """Dialogue with a slider.""" + + lo = kp.NumericProperty(0) + hi = kp.NumericProperty(100) + value = kp.NumericProperty(0) + init_value = kp.NumericProperty(0) + + +## EOF ## diff --git a/tagit/dialogues/path_picker.kv b/tagit/dialogues/path_picker.kv new file mode 100644 index 0000000..1837b80 --- /dev/null +++ b/tagit/dialogues/path_picker.kv @@ -0,0 +1,27 @@ +#:import join os.path.join +#:import pwd os.path.curdir + +<PathPicker>: + path: '' + title: 'Please select a file or directory' + filters: [] + dirselect: True + + DialogueContentTitle: + title: root.title + size_hint_y: 0.8 + + FileChooserListView: + text_size: self.width - dp(16), None + halign: 'center' + dirselect: root.dirselect + path: pwd + filters: root.filters + + on_selection: root.path = join(self.path, self.selection[0]); buttons.ok_enabled = True + + DialogueButtons_Two: + id: buttons + ok_enabled: False + +## EOF ## diff --git a/tagit/dialogues/path_picker.py b/tagit/dialogues/path_picker.py new file mode 100644 index 0000000..25bbf32 --- /dev/null +++ b/tagit/dialogues/path_picker.py @@ -0,0 +1,41 @@ +"""Dialogue to pick a file or directory. + +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 .dialogue import Dialogue +from .error import Error + +# exports +__all__ = ('PathPicker', ) + + +## code ## + +# load kv +Builder.load_file(os.path.join(os.path.dirname(__file__), 'path_picker.kv')) + +# classes +class PathPicker(Dialogue): + """Dialogue with a file browser to select a file or directory.""" + + title = kp.StringProperty('') + path = kp.StringProperty('') + filters = kp.ListProperty() + + def ok(self): + if not os.path.exists(self.path): + Error(text='Please select a file or directory').open() + else: + super(PathPicker, self).ok() + +## EOF ## diff --git a/tagit/dialogues/simple_input.kv b/tagit/dialogues/simple_input.kv new file mode 100644 index 0000000..b7deb9c --- /dev/null +++ b/tagit/dialogues/simple_input.kv @@ -0,0 +1,30 @@ + +#:import AutoTextInput tagit.dialogues + +<SimpleInput>: + text: '' + ok_on_enter: True + cancel_on_defocus: True + + DialogueContentNoTitle: + + #AutoTextInput: + TextInput: + vocabulary: root.suggestions + sep: root.suggestion_sep + suffix: root.suggestion_suffix + focus: True + text: root.text + size_hint_y: None + multiline: False + height: self.minimum_height + text_size: self.width - dp(16), None + halign: 'center' + + on_text: root.text = self.text + on_focus: root.on_text_focus(*args) + #on_text_validate: root.ok() # handled via the ok_on_enter mechanism + + DialogueButtons_Two: + +## EOF ## diff --git a/tagit/dialogues/simple_input.py b/tagit/dialogues/simple_input.py new file mode 100644 index 0000000..d7cc69f --- /dev/null +++ b/tagit/dialogues/simple_input.py @@ -0,0 +1,55 @@ +"""Dialogue with a single-line text input field. + +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 .dialogue import Dialogue + +# exports +__all__ = ('SimpleInput', ) + + +## code ## + +# load kv +Builder.load_file(os.path.join(os.path.dirname(__file__), 'simple_input.kv')) + +# classes +class SimpleInput(Dialogue): + """Dialogue with a single-line text input field. + + Pass the default text as **text**. + + >>> SimpleInput(text='Hello world').open() + + In case of touch events, they need to be inhibited to change the focus. + + >>> FocusBehavior.ignored_touch.append(touch) + + """ + + # Defocus problem: + # Buttons defocus when on_press, but on_release is ok. + # Touch events must be blocked via FocusBehavior + + text = kp.StringProperty('') + cancel_on_defocus = kp.BooleanProperty(True) + suggestions = kp.ListProperty() + suggestion_sep = kp.StringProperty(',') + suggestion_suffix = kp.StringProperty(' ') + + + def on_text_focus(self, instance, focus): + if not focus and self.cancel_on_defocus: + self.dismiss() + +## EOF ## diff --git a/tagit/dialogues/stoken.py b/tagit/dialogues/stoken.py new file mode 100644 index 0000000..6e5427a --- /dev/null +++ b/tagit/dialogues/stoken.py @@ -0,0 +1,40 @@ +"""Search token editor + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 + +""" +# kivy imports +from kivy.lang import Builder +import kivy.properties as kp + +# inner-module imports +from .simple_input import SimpleInput + +# exports +__all__ = ('TokenEdit', ) + + +## code ## + +# Load kv +Builder.load_string(''' +#:import AutoTextInput tagit.dialogues + +<TokenEdit>: + text: '' + ok_on_enter: True + cancel_on_defocus: True +''') + +# classes +class TokenEdit(SimpleInput): + """Search token editor + """ + # TODO: Currently this is no different than SimpleInput. + # It should be extend to specify the type and getting help + # with editing ranges and alternative selection. + pass + +## EOF ## diff --git a/tagit/external/__init__.py b/tagit/external/__init__.py new file mode 100644 index 0000000..b973c86 --- /dev/null +++ b/tagit/external/__init__.py @@ -0,0 +1,15 @@ +""" + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# standard imports +import typing + +# constants + +# exports +__all__: typing.Sequence[str] = [] + +## EOF ## diff --git a/tagit/external/kivy_garden/__init__.py b/tagit/external/kivy_garden/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tagit/external/kivy_garden/__init__.py diff --git a/tagit/external/kivy_garden/contextmenu/__init__.py b/tagit/external/kivy_garden/contextmenu/__init__.py new file mode 100644 index 0000000..ac55bff --- /dev/null +++ b/tagit/external/kivy_garden/contextmenu/__init__.py @@ -0,0 +1,11 @@ +from .context_menu import ContextMenu, \ + AbstractMenu, \ + AbstractMenuItem, \ + AbstractMenuItemHoverable, \ + ContextMenuItem, \ + ContextMenuDivider, \ + ContextMenuText, \ + ContextMenuTextItem + +from .app_menu import AppMenu, \ + AppMenuTextItem
\ No newline at end of file diff --git a/tagit/external/kivy_garden/contextmenu/_version.py b/tagit/external/kivy_garden/contextmenu/_version.py new file mode 100644 index 0000000..3ce5ddd --- /dev/null +++ b/tagit/external/kivy_garden/contextmenu/_version.py @@ -0,0 +1 @@ +__version__ = '0.1.0.dev1' diff --git a/tagit/external/kivy_garden/contextmenu/app_menu.kv b/tagit/external/kivy_garden/contextmenu/app_menu.kv new file mode 100644 index 0000000..644c6e5 --- /dev/null +++ b/tagit/external/kivy_garden/contextmenu/app_menu.kv @@ -0,0 +1,25 @@ +<AppMenu>: + height: dp(30) + size_hint: 1, None + + canvas.before: + Color: + rgb: 0.2, 0.2, 0.2 + Rectangle: + pos: self.pos + size: self.size + + +<AppMenuTextItem>: + disabled: True + size_hint: None, None + on_children: self._check_submenu() + font_size: '15sp' + background_normal: "" + background_down: "" + background_color: root.hl_color if self.state == 'down' else (0.2, 0.2, 0.2, 1.0) + background_disabled_normal: "" + background_disabled_down: "" + border: (0, 0, 0, 0) + size: self.texture_size[0], dp(30) + padding_x: dp(10) diff --git a/tagit/external/kivy_garden/contextmenu/app_menu.py b/tagit/external/kivy_garden/contextmenu/app_menu.py new file mode 100644 index 0000000..5394ec0 --- /dev/null +++ b/tagit/external/kivy_garden/contextmenu/app_menu.py @@ -0,0 +1,118 @@ +from kivy.uix.relativelayout import RelativeLayout +from kivy.uix.stacklayout import StackLayout +from kivy.uix.behaviors import ToggleButtonBehavior +from kivy.uix.togglebutton import ToggleButton +from kivy.lang import Builder +import kivy.properties as kp +import os + +from .context_menu import AbstractMenu, AbstractMenuItem, AbstractMenuItemHoverable, HIGHLIGHT_COLOR + + +class AppMenu(StackLayout, AbstractMenu): + bounding_box = kp.ObjectProperty(None) + + def __init__(self, *args, **kwargs): + super(AppMenu, self).__init__(*args, **kwargs) + self.hovered_menu_item = None + + def update_height(self): + max_height = 0 + for widget in self.menu_item_widgets: + if widget.height > max_height: + max_height = widget.height + return max_height + + def on_children(self, obj, new_children): + for w in new_children: + # bind events that update app menu height when any of its children resize + w.bind(on_size=self.update_height) + w.bind(on_height=self.update_height) + + def get_context_menu_root_parent(self): + return self + + def self_or_submenu_collide_with_point(self, x, y): + collide_widget = None + + # Iterate all siblings and all children + for widget in self.menu_item_widgets: + widget_pos = widget.to_window(0, 0) + if widget.collide_point(x - widget_pos[0], y - widget_pos[1]) and not widget.disabled: + if self.hovered_menu_item is None: + self.hovered_menu_item = widget + + if self.hovered_menu_item != widget: + self.hovered_menu_item = widget + for sibling in widget.siblings: + sibling.state = 'normal' + + if widget.state == 'normal': + widget.state = 'down' + widget.on_release() + + for sib in widget.siblings: + sib.hovered = False + elif widget.get_submenu() is not None and not widget.get_submenu().visible: + widget.state = 'normal' + + return collide_widget + + def close_all(self): + for submenu in [w.get_submenu() for w in self.menu_item_widgets if w.get_submenu() is not None]: + submenu.hide() + for w in self.menu_item_widgets: + w.state = 'normal' + + def hide_app_menus(self, obj, pos): + if not self.collide_point(pos.x, pos.y): + for w in [w for w in self.menu_item_widgets if not w.disabled and w.get_submenu().visible]: + submenu = w.get_submenu() + if submenu.self_or_submenu_collide_with_point(pos.x, pos.y) is None: + self.close_all() + self._cancel_hover_timer() + + +class AppMenuTextItem(ToggleButton, AbstractMenuItem): + label = kp.ObjectProperty(None) + text = kp.StringProperty('') + font_size = kp.NumericProperty(14) + color = kp.ListProperty([1, 1, 1, 1]) + hl_color = kp.ListProperty(HIGHLIGHT_COLOR) + + def on_release(self): + submenu = self.get_submenu() + + if self.state == 'down': + root = self._root_parent + submenu.bounding_box_widget = root.bounding_box if root.bounding_box else root.parent + + submenu.bind(visible=self.on_visible) + submenu.show(self.x, self.y - 1) + + for sibling in self.siblings: + if sibling.get_submenu() is not None: + sibling.state = 'normal' + sibling.get_submenu().hide() + + self.parent._setup_hover_timer() + else: + self.parent._cancel_hover_timer() + submenu.hide() + + def on_visible(self, *args): + submenu = self.get_submenu() + if self.width > submenu.get_max_width(): + submenu.width = self.width + + def _check_submenu(self): + super(AppMenuTextItem, self)._check_submenu() + self.disabled = (self.get_submenu() is None) + + # def on_mouse_down(self): + # print('on_mouse_down') + # return True + + +_path = os.path.dirname(os.path.realpath(__file__)) +Builder.load_file(os.path.join(_path, 'app_menu.kv')) diff --git a/tagit/external/kivy_garden/contextmenu/context_menu.kv b/tagit/external/kivy_garden/contextmenu/context_menu.kv new file mode 100644 index 0000000..c3f7133 --- /dev/null +++ b/tagit/external/kivy_garden/contextmenu/context_menu.kv @@ -0,0 +1,125 @@ +<ContextMenu>: + cols: 1 + size_hint: None, None + spacing: 0, 0 + spacer: _spacer + on_visible: self._on_visible(args[1]) + on_parent: self._on_visible(self.visible) + + Widget: + id: _spacer + size_hint: 1, None + height: dp(3) + canvas.before: + Color: + rgb: root.hl_color + Rectangle: + pos: self.pos + size: self.size + + +<ContextMenuItem>: + size_hint: None, None + submenu_arrow: _submenu_arrow + on_children: self._check_submenu() + on_parent: self._check_submenu() + canvas.before: + Color: + rgb: (0.15, 0.15, 0.15) + Rectangle: + pos: 0,0 + size: self.size + + Widget: + id: _submenu_arrow + size_hint: None, None + width: dp(6) + height: dp(11) + pos: self.parent.width - self.width - dp(5), (self.parent.height - self.height) / 2 + canvas.before: + Translate: + xy: self.pos + Color: + rgb: (0.35, 0.35, 0.35) if self.disabled else (1, 1, 1) + Triangle: + points: [0,0, self.width,self.height/2, 0,self.height] + Translate: + xy: (-self.pos[0], -self.pos[1]) + + +<ContextMenuText>: + label: _label + width: self.parent.width if self.parent else 0 + height: dp(26) + font_size: '15sp' + + Label: + pos: 0,0 + id: _label + text: self.parent.text + color: self.parent.color + font_size: self.parent.font_size + padding: dp(10), 0 + halign: 'left' + valign: 'middle' + size: self.texture_size + size_hint: None, 1 + + +<AbstractMenuItemHoverable>: + on_hovered: self._on_hovered(args[1]) + canvas.before: + Color: + rgb: (0.25, 0.25, 0.25) if self.hovered and not self.disabled else (0.15, 0.15, 0.15) + Rectangle: + pos: 0,0 + size: self.size + + +<ContextMenuDivider>: + font_size: '10sp' + height: dp(20) if len(self.label.text) > 0 else dp(1) + canvas.before: + Color: + rgb: (0.25, 0.25, 0.25) + Rectangle: + pos: 0,self.height - 1 + size: self.width, 1 + + +<ContextMenuButton@Button>: + size_hint: None, None + font_size: '12sp' + height: dp(20) + background_normal: "" + background_down: "" + background_color: HIGHLIGHT_COLOR + border: (0, 0, 0, 0) + on_press: self.background_color = 0.10, 0.6, 0.8, 1.0 + on_release: self.background_color = HIGHLIGHT_COLOR + + +<ContextMenuToggleButton@ToggleButton>: + size_hint: None, None + font_size: '12sp' + size: dp(30), dp(20) + background_normal: "" + background_down: "" + background_color: HIGHLIGHT_COLOR if self.state == 'down' else (0.25, 0.25, 0.25, 1.0) + border: (0, 0, 0, 0) + on_press: self.background_color = 0.10, 0.6, 0.8, 1.0 + on_release: self.background_color = HIGHLIGHT_COLOR + + +<ContextMenuSmallLabel@Label>: + size: self.texture_size[0], dp(18) + size_hint: None, None + font_size: '12sp' + + +<ContextMenuTextInput@TextInput>: + size_hint: None, None + height: dp(22) + font_size: '12sp' + padding: dp(7), dp(3) + multiline: False diff --git a/tagit/external/kivy_garden/contextmenu/context_menu.py b/tagit/external/kivy_garden/contextmenu/context_menu.py new file mode 100644 index 0000000..1613756 --- /dev/null +++ b/tagit/external/kivy_garden/contextmenu/context_menu.py @@ -0,0 +1,287 @@ +from kivy.uix.gridlayout import GridLayout +from kivy.uix.relativelayout import RelativeLayout +from kivy.uix.behaviors import ButtonBehavior +from kivy.lang import Builder +from kivy.clock import Clock +from functools import partial + +import kivy.properties as kp +import os + + +HIGHLIGHT_COLOR = [0.2, 0.71, 0.9, 1] + + +class AbstractMenu(object): + cancel_handler_widget = kp.ObjectProperty(None) + bounding_box_widget = kp.ObjectProperty(None) + + def __init__(self, *args, **kwargs): + self.clock_event = None + + def add_item(self, widget): + self.add_widget(widget) + + def add_text_item(self, text, on_release=None): + item = ContextMenuTextItem(text=text) + if on_release: + item.bind(on_release=on_release) + self.add_item(item) + + def get_height(self): + height = 0 + for widget in self.children: + height += widget.height + return height + + def hide_submenus(self): + for widget in self.menu_item_widgets: + widget.hovered = False + widget.hide_submenu() + + def self_or_submenu_collide_with_point(self, x, y): + raise NotImplementedError() + + def on_cancel_handler_widget(self, obj, widget): + self.cancel_handler_widget.bind(on_touch_down=self.hide_app_menus) + + def hide_app_menus(self, obj, pos): + raise NotImplementedError() + + @property + def menu_item_widgets(self): + """ + Return all children that are subclasses of ContextMenuItem + """ + return [w for w in self.children if issubclass(w.__class__, AbstractMenuItem)] + + def _setup_hover_timer(self): + if self.clock_event is None: + self.clock_event = Clock.schedule_interval(partial(self._check_mouse_hover), 0.05) + + def _check_mouse_hover(self, obj): + from kivy.core.window import Window + self.self_or_submenu_collide_with_point(*Window.mouse_pos) + + def _cancel_hover_timer(self): + if self.clock_event: + self.clock_event.cancel() + self.clock_event = None + + +class ContextMenu(GridLayout, AbstractMenu): + visible = kp.BooleanProperty(False) + spacer = kp.ObjectProperty(None) + hl_color = kp.ListProperty(HIGHLIGHT_COLOR) + + + def __init__(self, *args, **kwargs): + super(ContextMenu, self).__init__(*args, **kwargs) + self.orig_parent = None + # self._on_visible(False) + + def hide(self): + self.visible = False + + def show(self, x=None, y=None): + self.visible = True + self._add_to_parent() + self.hide_submenus() + + root_parent = self.bounding_box_widget if self.bounding_box_widget is not None else self.get_context_menu_root_parent() + if root_parent is None: + return + + point_relative_to_root = root_parent.to_local(*self.to_window(x, y)) + + # Choose the best position to open the menu + if x is not None and y is not None: + if point_relative_to_root[0] + self.width < root_parent.width: + pos_x = x + else: + pos_x = x - self.width + if issubclass(self.parent.__class__, AbstractMenuItem): + pos_x -= self.parent.width + + if point_relative_to_root[1] - self.height < 0: + pos_y = y + if issubclass(self.parent.__class__, AbstractMenuItem): + pos_y -= self.parent.height + self.spacer.height + else: + pos_y = y - self.height + + self.pos = pos_x, pos_y + + def self_or_submenu_collide_with_point(self, x, y): + queue = self.menu_item_widgets + collide_widget = None + + # Iterate all siblings and all children + while len(queue) > 0: + widget = queue.pop(0) + submenu = widget.get_submenu() + if submenu is not None and widget.hovered: + queue += submenu.menu_item_widgets + + widget_pos = widget.to_window(0, 0) + if widget.collide_point(x - widget_pos[0], y - widget_pos[1]) and not widget.disabled: + widget.hovered = True + + collide_widget = widget + for sib in widget.siblings: + sib.hovered = False + elif submenu and submenu.visible: + widget.hovered = True + else: + widget.hovered = False + + return collide_widget + + def _on_visible(self, new_visibility): + if new_visibility: + self.size = self.get_max_width(), self.get_height() + self._add_to_parent() + # @todo: Do we need to remove self from self.parent.__context_menus? Probably not. + + elif self.parent and not new_visibility: + self.orig_parent = self.parent + + ''' + We create a set that holds references to all context menus in the parent widget. + It's necessary to keep at least one reference to this context menu. Otherwise when + removed from parent it might get de-allocated by GC. + ''' + if not hasattr(self.parent, '_ContextMenu__context_menus'): + self.parent.__context_menus = set() + self.parent.__context_menus.add(self) + + self.parent.remove_widget(self) + self.hide_submenus() + self._cancel_hover_timer() + + def _add_to_parent(self): + if not self.parent: + self.orig_parent.add_widget(self) + self.orig_parent = None + + # Create the timer on the outer most menu object + if self._get_root_context_menu() == self: + self._setup_hover_timer() + + def get_max_width(self): + max_width = 0 + for widget in self.menu_item_widgets: + width = widget.content_width if widget.content_width is not None else widget.width + if width is not None and width > max_width: + max_width = width + + return max_width + + def get_context_menu_root_parent(self): + """ + Return the bounding box widget for positioning sub menus. By default it's root context menu's parent. + """ + if self.bounding_box_widget is not None: + return self.bounding_box_widget + root_context_menu = self._get_root_context_menu() + return root_context_menu.bounding_box_widget if root_context_menu.bounding_box_widget else root_context_menu.parent + + def _get_root_context_menu(self): + """ + Return the outer most context menu object + """ + root = self + while issubclass(root.parent.__class__, ContextMenuItem) \ + or issubclass(root.parent.__class__, ContextMenu): + root = root.parent + return root + + def hide_app_menus(self, obj, pos): + return self.self_or_submenu_collide_with_point(pos.x, pos.y) is None and self.hide() + + +class AbstractMenuItem(object): + submenu = kp.ObjectProperty(None) + + def get_submenu(self): + return self.submenu if self.submenu != "" else None + + def show_submenu(self, x=None, y=None): + if self.get_submenu(): + self.get_submenu().show(*self._root_parent.to_local(x, y)) + + def hide_submenu(self): + submenu = self.get_submenu() + if submenu: + submenu.visible = False + submenu.hide_submenus() + + def _check_submenu(self): + if self.parent is not None and len(self.children) > 0: + submenus = [w for w in self.children if issubclass(w.__class__, ContextMenu)] + if len(submenus) > 1: + raise Exception('Menu item (ContextMenuItem) can have maximum one submenu (ContextMenu)') + elif len(submenus) == 1: + self.submenu = submenus[0] + + @property + def siblings(self): + return [w for w in self.parent.children if issubclass(w.__class__, AbstractMenuItem) and w != self] + + @property + def content_width(self): + return None + + @property + def _root_parent(self): + return self.parent.get_context_menu_root_parent() + + +class ContextMenuItem(RelativeLayout, AbstractMenuItem): + submenu_arrow = kp.ObjectProperty(None) + + def _check_submenu(self): + super(ContextMenuItem, self)._check_submenu() + if self.get_submenu() is None: + self.submenu_arrow.opacity = 0 + else: + self.submenu_arrow.opacity = 1 + + +class AbstractMenuItemHoverable(object): + hovered = kp.BooleanProperty(False) + + def _on_hovered(self, new_hovered): + if new_hovered: + spacer_height = self.parent.spacer.height if self.parent.spacer else 0 + self.show_submenu(self.width, self.height + spacer_height) + else: + self.hide_submenu() + + +class ContextMenuText(ContextMenuItem): + label = kp.ObjectProperty(None) + submenu_postfix = kp.StringProperty(' ...') + text = kp.StringProperty('') + font_size = kp.NumericProperty(14) + color = kp.ListProperty([1,1,1,1]) + + def __init__(self, *args, **kwargs): + super(ContextMenuText, self).__init__(*args, **kwargs) + + @property + def content_width(self): + # keep little space for eventual arrow for submenus + return self.label.texture_size[0] + 10 + + +class ContextMenuDivider(ContextMenuText): + pass + + +class ContextMenuTextItem(ButtonBehavior, ContextMenuText, AbstractMenuItemHoverable): + pass + + +_path = os.path.dirname(os.path.realpath(__file__)) +Builder.load_file(os.path.join(_path, 'context_menu.kv')) diff --git a/tagit/external/kivy_garden/mapview/__init__.py b/tagit/external/kivy_garden/mapview/__init__.py new file mode 100644 index 0000000..0db8a25 --- /dev/null +++ b/tagit/external/kivy_garden/mapview/__init__.py @@ -0,0 +1,27 @@ +# coding=utf-8 +""" +MapView +======= + +MapView is a Kivy widget that display maps. +""" +from .source import MapSource +from .types import Bbox, Coordinate +from .view import ( + MapLayer, + MapMarker, + MapMarkerPopup, + MapView, + MarkerMapLayer, +) + +__all__ = [ + "Coordinate", + "Bbox", + "MapView", + "MapSource", + "MapMarker", + "MapLayer", + "MarkerMapLayer", + "MapMarkerPopup", +] diff --git a/tagit/external/kivy_garden/mapview/_version.py b/tagit/external/kivy_garden/mapview/_version.py new file mode 100644 index 0000000..68cdeee --- /dev/null +++ b/tagit/external/kivy_garden/mapview/_version.py @@ -0,0 +1 @@ +__version__ = "1.0.5" diff --git a/tagit/external/kivy_garden/mapview/clustered_marker_layer.py b/tagit/external/kivy_garden/mapview/clustered_marker_layer.py new file mode 100644 index 0000000..c885fb2 --- /dev/null +++ b/tagit/external/kivy_garden/mapview/clustered_marker_layer.py @@ -0,0 +1,449 @@ +# coding=utf-8 +""" +Layer that support point clustering +=================================== +""" + +from math import atan, exp, floor, log, pi, sin, sqrt +from os.path import dirname, join + +from kivy.lang import Builder +from kivy.metrics import dp +from kivy.properties import ( + ListProperty, + NumericProperty, + ObjectProperty, + StringProperty, +) + +from .view import MapLayer, MapMarker + +Builder.load_string( + """ +<ClusterMapMarker>: + size_hint: None, None + source: root.source + size: list(map(dp, self.texture_size)) + allow_stretch: True + + Label: + color: root.text_color + pos: root.pos + size: root.size + text: "{}".format(root.num_points) + font_size: dp(18) +""" +) + + +# longitude/latitude to spherical mercator in [0..1] range +def lngX(lng): + return lng / 360.0 + 0.5 + + +def latY(lat): + if lat == 90: + return 0 + if lat == -90: + return 1 + s = sin(lat * pi / 180.0) + y = 0.5 - 0.25 * log((1 + s) / (1 - s)) / pi + return min(1, max(0, y)) + + +# spherical mercator to longitude/latitude +def xLng(x): + return (x - 0.5) * 360 + + +def yLat(y): + y2 = (180 - y * 360) * pi / 180 + return 360 * atan(exp(y2)) / pi - 90 + + +class KDBush: + """ + kdbush implementation from: + https://github.com/mourner/kdbush/blob/master/src/kdbush.js + """ + + def __init__(self, points, node_size=64): + self.points = points + self.node_size = node_size + + self.ids = ids = [0] * len(points) + self.coords = coords = [0] * len(points) * 2 + for i, point in enumerate(points): + ids[i] = i + coords[2 * i] = point.x + coords[2 * i + 1] = point.y + + self._sort(ids, coords, node_size, 0, len(ids) - 1, 0) + + def range(self, min_x, min_y, max_x, max_y): + return self._range( + self.ids, self.coords, min_x, min_y, max_x, max_y, self.node_size + ) + + def within(self, x, y, r): + return self._within(self.ids, self.coords, x, y, r, self.node_size) + + def _sort(self, ids, coords, node_size, left, right, depth): + if right - left <= node_size: + return + m = int(floor((left + right) / 2.0)) + self._select(ids, coords, m, left, right, depth % 2) + self._sort(ids, coords, node_size, left, m - 1, depth + 1) + self._sort(ids, coords, node_size, m + 1, right, depth + 1) + + def _select(self, ids, coords, k, left, right, inc): + swap_item = self._swap_item + while right > left: + if (right - left) > 600: + n = float(right - left + 1) + m = k - left + 1 + z = log(n) + s = 0.5 + exp(2 * z / 3.0) + sd = 0.5 * sqrt(z * s * (n - s) / n) * (-1 if (m - n / 2.0) < 0 else 1) + new_left = max(left, int(floor(k - m * s / n + sd))) + new_right = min(right, int(floor(k + (n - m) * s / n + sd))) + self._select(ids, coords, k, new_left, new_right, inc) + + t = coords[2 * k + inc] + i = left + j = right + + swap_item(ids, coords, left, k) + if coords[2 * right + inc] > t: + swap_item(ids, coords, left, right) + + while i < j: + swap_item(ids, coords, i, j) + i += 1 + j -= 1 + while coords[2 * i + inc] < t: + i += 1 + while coords[2 * j + inc] > t: + j -= 1 + + if coords[2 * left + inc] == t: + swap_item(ids, coords, left, j) + else: + j += 1 + swap_item(ids, coords, j, right) + + if j <= k: + left = j + 1 + if k <= j: + right = j - 1 + + def _swap_item(self, ids, coords, i, j): + swap = self._swap + swap(ids, i, j) + swap(coords, 2 * i, 2 * j) + swap(coords, 2 * i + 1, 2 * j + 1) + + def _swap(self, arr, i, j): + tmp = arr[i] + arr[i] = arr[j] + arr[j] = tmp + + def _range(self, ids, coords, min_x, min_y, max_x, max_y, node_size): + stack = [0, len(ids) - 1, 0] + result = [] + x = y = 0 + + while stack: + axis = stack.pop() + right = stack.pop() + left = stack.pop() + + if right - left <= node_size: + for i in range(left, right + 1): + x = coords[2 * i] + y = coords[2 * i + 1] + if x >= min_x and x <= max_x and y >= min_y and y <= max_y: + result.append(ids[i]) + continue + + m = int(floor((left + right) / 2.0)) + + x = coords[2 * m] + y = coords[2 * m + 1] + + if x >= min_x and x <= max_x and y >= min_y and y <= max_y: + result.append(ids[m]) + + nextAxis = (axis + 1) % 2 + + if min_x <= x if axis == 0 else min_y <= y: + stack.append(left) + stack.append(m - 1) + stack.append(nextAxis) + if max_x >= x if axis == 0 else max_y >= y: + stack.append(m + 1) + stack.append(right) + stack.append(nextAxis) + + return result + + def _within(self, ids, coords, qx, qy, r, node_size): + sq_dist = self._sq_dist + stack = [0, len(ids) - 1, 0] + result = [] + r2 = r * r + + while stack: + axis = stack.pop() + right = stack.pop() + left = stack.pop() + + if right - left <= node_size: + for i in range(left, right + 1): + if sq_dist(coords[2 * i], coords[2 * i + 1], qx, qy) <= r2: + result.append(ids[i]) + continue + + m = int(floor((left + right) / 2.0)) + + x = coords[2 * m] + y = coords[2 * m + 1] + + if sq_dist(x, y, qx, qy) <= r2: + result.append(ids[m]) + + nextAxis = (axis + 1) % 2 + + if (qx - r <= x) if axis == 0 else (qy - r <= y): + stack.append(left) + stack.append(m - 1) + stack.append(nextAxis) + if (qx + r >= x) if axis == 0 else (qy + r >= y): + stack.append(m + 1) + stack.append(right) + stack.append(nextAxis) + + return result + + def _sq_dist(self, ax, ay, bx, by): + dx = ax - bx + dy = ay - by + return dx * dx + dy * dy + + +class Cluster: + def __init__(self, x, y, num_points, id, props): + self.x = x + self.y = y + self.num_points = num_points + self.zoom = float("inf") + self.id = id + self.props = props + self.parent_id = None + self.widget = None + + # preprocess lon/lat + self.lon = xLng(x) + self.lat = yLat(y) + + +class Marker: + def __init__(self, lon, lat, cls=MapMarker, options=None): + self.lon = lon + self.lat = lat + self.cls = cls + self.options = options + + # preprocess x/y from lon/lat + self.x = lngX(lon) + self.y = latY(lat) + + # cluster information + self.id = None + self.zoom = float("inf") + self.parent_id = None + self.widget = None + + def __repr__(self): + return "<Marker lon={} lat={} source={}>".format( + self.lon, self.lat, self.source + ) + + +class SuperCluster: + """Port of supercluster from mapbox in pure python + """ + + def __init__(self, min_zoom=0, max_zoom=16, radius=40, extent=512, node_size=64): + self.min_zoom = min_zoom + self.max_zoom = max_zoom + self.radius = radius + self.extent = extent + self.node_size = node_size + + def load(self, points): + """Load an array of markers. + Once loaded, the index is immutable. + """ + from time import time + + self.trees = {} + self.points = points + + for index, point in enumerate(points): + point.id = index + + clusters = points + for z in range(self.max_zoom, self.min_zoom - 1, -1): + start = time() + print("build tree", z) + self.trees[z + 1] = KDBush(clusters, self.node_size) + print("kdbush", (time() - start) * 1000) + start = time() + clusters = self._cluster(clusters, z) + print(len(clusters)) + print("clustering", (time() - start) * 1000) + self.trees[self.min_zoom] = KDBush(clusters, self.node_size) + + def get_clusters(self, bbox, zoom): + """For the given bbox [westLng, southLat, eastLng, northLat], and + integer zoom, returns an array of clusters and markers + """ + tree = self.trees[self._limit_zoom(zoom)] + ids = tree.range(lngX(bbox[0]), latY(bbox[3]), lngX(bbox[2]), latY(bbox[1])) + clusters = [] + for i in range(len(ids)): + c = tree.points[ids[i]] + if isinstance(c, Cluster): + clusters.append(c) + else: + clusters.append(self.points[c.id]) + return clusters + + def _limit_zoom(self, z): + return max(self.min_zoom, min(self.max_zoom + 1, z)) + + def _cluster(self, points, zoom): + clusters = [] + c_append = clusters.append + trees = self.trees + r = self.radius / float(self.extent * pow(2, zoom)) + + # loop through each point + for i in range(len(points)): + p = points[i] + # if we've already visited the point at this zoom level, skip it + if p.zoom <= zoom: + continue + p.zoom = zoom + + # find all nearby points + tree = trees[zoom + 1] + neighbor_ids = tree.within(p.x, p.y, r) + + num_points = 1 + if isinstance(p, Cluster): + num_points = p.num_points + wx = p.x * num_points + wy = p.y * num_points + + props = None + + for j in range(len(neighbor_ids)): + b = tree.points[neighbor_ids[j]] + # filter out neighbors that are too far or already processed + if zoom < b.zoom: + num_points2 = 1 + if isinstance(b, Cluster): + num_points2 = b.num_points + # save the zoom (so it doesn't get processed twice) + b.zoom = zoom + # accumulate coordinates for calculating weighted center + wx += b.x * num_points2 + wy += b.y * num_points2 + num_points += num_points2 + b.parent_id = i + + if num_points == 1: + c_append(p) + else: + p.parent_id = i + c_append( + Cluster(wx / num_points, wy / num_points, num_points, i, props) + ) + return clusters + + +class ClusterMapMarker(MapMarker): + source = StringProperty(join(dirname(__file__), "icons", "cluster.png")) + cluster = ObjectProperty() + num_points = NumericProperty() + text_color = ListProperty([0.1, 0.1, 0.1, 1]) + + def on_cluster(self, instance, cluster): + self.num_points = cluster.num_points + + def on_touch_down(self, touch): + return False + + +class ClusteredMarkerLayer(MapLayer): + cluster_cls = ObjectProperty(ClusterMapMarker) + cluster_min_zoom = NumericProperty(0) + cluster_max_zoom = NumericProperty(16) + cluster_radius = NumericProperty("40dp") + cluster_extent = NumericProperty(512) + cluster_node_size = NumericProperty(64) + + def __init__(self, **kwargs): + self.cluster = None + self.cluster_markers = [] + super().__init__(**kwargs) + + def add_marker(self, lon, lat, cls=MapMarker, options=None): + if options is None: + options = {} + marker = Marker(lon, lat, cls, options) + self.cluster_markers.append(marker) + return marker + + def remove_marker(self, marker): + self.cluster_markers.remove(marker) + + def reposition(self): + if self.cluster is None: + self.build_cluster() + margin = dp(48) + mapview = self.parent + set_marker_position = self.set_marker_position + bbox = mapview.get_bbox(margin) + bbox = (bbox[1], bbox[0], bbox[3], bbox[2]) + self.clear_widgets() + for point in self.cluster.get_clusters(bbox, mapview.zoom): + widget = point.widget + if widget is None: + widget = self.create_widget_for(point) + set_marker_position(mapview, widget) + self.add_widget(widget) + + def build_cluster(self): + self.cluster = SuperCluster( + min_zoom=self.cluster_min_zoom, + max_zoom=self.cluster_max_zoom, + radius=self.cluster_radius, + extent=self.cluster_extent, + node_size=self.cluster_node_size, + ) + self.cluster.load(self.cluster_markers) + + def create_widget_for(self, point): + if isinstance(point, Marker): + point.widget = point.cls(lon=point.lon, lat=point.lat, **point.options) + elif isinstance(point, Cluster): + point.widget = self.cluster_cls(lon=point.lon, lat=point.lat, cluster=point) + return point.widget + + def set_marker_position(self, mapview, marker): + x, y = mapview.get_window_xy_from(marker.lat, marker.lon, mapview.zoom) + marker.x = int(x - marker.width * marker.anchor_x) + marker.y = int(y - marker.height * marker.anchor_y) diff --git a/tagit/external/kivy_garden/mapview/constants.py b/tagit/external/kivy_garden/mapview/constants.py new file mode 100644 index 0000000..b6998f8 --- /dev/null +++ b/tagit/external/kivy_garden/mapview/constants.py @@ -0,0 +1,5 @@ +MIN_LATITUDE = -90.0 +MAX_LATITUDE = 90.0 +MIN_LONGITUDE = -180.0 +MAX_LONGITUDE = 180.0 +CACHE_DIR = "cache" diff --git a/tagit/external/kivy_garden/mapview/downloader.py b/tagit/external/kivy_garden/mapview/downloader.py new file mode 100644 index 0000000..73ca3d1 --- /dev/null +++ b/tagit/external/kivy_garden/mapview/downloader.py @@ -0,0 +1,123 @@ +# coding=utf-8 + +__all__ = ["Downloader"] + +import logging +import traceback +from concurrent.futures import ThreadPoolExecutor, TimeoutError, as_completed +from os import environ, makedirs +from os.path import exists, join +from random import choice +from time import time + +import requests +from kivy.clock import Clock +from kivy.logger import LOG_LEVELS, Logger + +from .constants import CACHE_DIR + +if "MAPVIEW_DEBUG_DOWNLOADER" in environ: + Logger.setLevel(LOG_LEVELS['debug']) + +# user agent is needed because since may 2019 OSM gives me a 429 or 403 server error +# I tried it with a simpler one (just Mozilla/5.0) this also gets rejected +USER_AGENT = 'Kivy-garden.mapview' + + +class Downloader: + _instance = None + MAX_WORKERS = 5 + CAP_TIME = 0.064 # 15 FPS + + @staticmethod + def instance(cache_dir=None): + if Downloader._instance is None: + if not cache_dir: + cache_dir = CACHE_DIR + Downloader._instance = Downloader(cache_dir=cache_dir) + return Downloader._instance + + def __init__(self, max_workers=None, cap_time=None, **kwargs): + self.cache_dir = kwargs.get('cache_dir', CACHE_DIR) + if max_workers is None: + max_workers = Downloader.MAX_WORKERS + if cap_time is None: + cap_time = Downloader.CAP_TIME + self.is_paused = False + self.cap_time = cap_time + self.executor = ThreadPoolExecutor(max_workers=max_workers) + self._futures = [] + Clock.schedule_interval(self._check_executor, 1 / 60.0) + if not exists(self.cache_dir): + makedirs(self.cache_dir) + + def submit(self, f, *args, **kwargs): + future = self.executor.submit(f, *args, **kwargs) + self._futures.append(future) + + def download_tile(self, tile): + Logger.debug( + "Downloader: queue(tile) zoom={} x={} y={}".format( + tile.zoom, tile.tile_x, tile.tile_y + ) + ) + future = self.executor.submit(self._load_tile, tile) + self._futures.append(future) + + def download(self, url, callback, **kwargs): + Logger.debug("Downloader: queue(url) {}".format(url)) + future = self.executor.submit(self._download_url, url, callback, kwargs) + self._futures.append(future) + + def _download_url(self, url, callback, kwargs): + Logger.debug("Downloader: download(url) {}".format(url)) + response = requests.get(url, **kwargs) + response.raise_for_status() + return callback, (url, response) + + def _load_tile(self, tile): + if tile.state == "done": + return + cache_fn = tile.cache_fn + if exists(cache_fn): + Logger.debug("Downloader: use cache {}".format(cache_fn)) + return tile.set_source, (cache_fn,) + tile_y = tile.map_source.get_row_count(tile.zoom) - tile.tile_y - 1 + uri = tile.map_source.url.format( + z=tile.zoom, x=tile.tile_x, y=tile_y, s=choice(tile.map_source.subdomains) + ) + Logger.debug("Downloader: download(tile) {}".format(uri)) + response = requests.get(uri, headers={'User-agent': USER_AGENT}, timeout=5) + try: + response.raise_for_status() + data = response.content + with open(cache_fn, "wb") as fd: + fd.write(data) + Logger.debug("Downloaded {} bytes: {}".format(len(data), uri)) + return tile.set_source, (cache_fn,) + except Exception as e: + print("Downloader error: {!r}".format(e)) + + def _check_executor(self, dt): + start = time() + try: + for future in as_completed(self._futures[:], 0): + self._futures.remove(future) + try: + result = future.result() + except Exception: + traceback.print_exc() + # make an error tile? + continue + if result is None: + continue + callback, args = result + callback(*args) + + # capped executor in time, in order to prevent too much + # slowiness. + # seems to works quite great with big zoom-in/out + if time() - start > self.cap_time: + break + except TimeoutError: + pass diff --git a/tagit/external/kivy_garden/mapview/geojson.py b/tagit/external/kivy_garden/mapview/geojson.py new file mode 100644 index 0000000..5ce31ae --- /dev/null +++ b/tagit/external/kivy_garden/mapview/geojson.py @@ -0,0 +1,381 @@ +# coding=utf-8 +""" +Geojson layer +============= + +.. note:: + + Currently experimental and a work in progress, not fully optimized. + + +Supports: + +- html color in properties +- polygon geometry are cached and not redrawed when the parent mapview changes +- linestring are redrawed everymove, it's ugly and slow. +- marker are NOT supported + +""" + +__all__ = ["GeoJsonMapLayer"] + +import json + +from kivy.graphics import ( + Canvas, + Color, + Line, + MatrixInstruction, + Mesh, + PopMatrix, + PushMatrix, + Scale, + Translate, +) +from kivy.graphics.tesselator import TYPE_POLYGONS, WINDING_ODD, Tesselator +from kivy.metrics import dp +from kivy.properties import ObjectProperty, StringProperty +from kivy.utils import get_color_from_hex + +from .constants import CACHE_DIR +from .downloader import Downloader +from .view import MapLayer + +COLORS = { + 'aliceblue': '#f0f8ff', + 'antiquewhite': '#faebd7', + 'aqua': '#00ffff', + 'aquamarine': '#7fffd4', + 'azure': '#f0ffff', + 'beige': '#f5f5dc', + 'bisque': '#ffe4c4', + 'black': '#000000', + 'blanchedalmond': '#ffebcd', + 'blue': '#0000ff', + 'blueviolet': '#8a2be2', + 'brown': '#a52a2a', + 'burlywood': '#deb887', + 'cadetblue': '#5f9ea0', + 'chartreuse': '#7fff00', + 'chocolate': '#d2691e', + 'coral': '#ff7f50', + 'cornflowerblue': '#6495ed', + 'cornsilk': '#fff8dc', + 'crimson': '#dc143c', + 'cyan': '#00ffff', + 'darkblue': '#00008b', + 'darkcyan': '#008b8b', + 'darkgoldenrod': '#b8860b', + 'darkgray': '#a9a9a9', + 'darkgrey': '#a9a9a9', + 'darkgreen': '#006400', + 'darkkhaki': '#bdb76b', + 'darkmagenta': '#8b008b', + 'darkolivegreen': '#556b2f', + 'darkorange': '#ff8c00', + 'darkorchid': '#9932cc', + 'darkred': '#8b0000', + 'darksalmon': '#e9967a', + 'darkseagreen': '#8fbc8f', + 'darkslateblue': '#483d8b', + 'darkslategray': '#2f4f4f', + 'darkslategrey': '#2f4f4f', + 'darkturquoise': '#00ced1', + 'darkviolet': '#9400d3', + 'deeppink': '#ff1493', + 'deepskyblue': '#00bfff', + 'dimgray': '#696969', + 'dimgrey': '#696969', + 'dodgerblue': '#1e90ff', + 'firebrick': '#b22222', + 'floralwhite': '#fffaf0', + 'forestgreen': '#228b22', + 'fuchsia': '#ff00ff', + 'gainsboro': '#dcdcdc', + 'ghostwhite': '#f8f8ff', + 'gold': '#ffd700', + 'goldenrod': '#daa520', + 'gray': '#808080', + 'grey': '#808080', + 'green': '#008000', + 'greenyellow': '#adff2f', + 'honeydew': '#f0fff0', + 'hotpink': '#ff69b4', + 'indianred': '#cd5c5c', + 'indigo': '#4b0082', + 'ivory': '#fffff0', + 'khaki': '#f0e68c', + 'lavender': '#e6e6fa', + 'lavenderblush': '#fff0f5', + 'lawngreen': '#7cfc00', + 'lemonchiffon': '#fffacd', + 'lightblue': '#add8e6', + 'lightcoral': '#f08080', + 'lightcyan': '#e0ffff', + 'lightgoldenrodyellow': '#fafad2', + 'lightgray': '#d3d3d3', + 'lightgrey': '#d3d3d3', + 'lightgreen': '#90ee90', + 'lightpink': '#ffb6c1', + 'lightsalmon': '#ffa07a', + 'lightseagreen': '#20b2aa', + 'lightskyblue': '#87cefa', + 'lightslategray': '#778899', + 'lightslategrey': '#778899', + 'lightsteelblue': '#b0c4de', + 'lightyellow': '#ffffe0', + 'lime': '#00ff00', + 'limegreen': '#32cd32', + 'linen': '#faf0e6', + 'magenta': '#ff00ff', + 'maroon': '#800000', + 'mediumaquamarine': '#66cdaa', + 'mediumblue': '#0000cd', + 'mediumorchid': '#ba55d3', + 'mediumpurple': '#9370d8', + 'mediumseagreen': '#3cb371', + 'mediumslateblue': '#7b68ee', + 'mediumspringgreen': '#00fa9a', + 'mediumturquoise': '#48d1cc', + 'mediumvioletred': '#c71585', + 'midnightblue': '#191970', + 'mintcream': '#f5fffa', + 'mistyrose': '#ffe4e1', + 'moccasin': '#ffe4b5', + 'navajowhite': '#ffdead', + 'navy': '#000080', + 'oldlace': '#fdf5e6', + 'olive': '#808000', + 'olivedrab': '#6b8e23', + 'orange': '#ffa500', + 'orangered': '#ff4500', + 'orchid': '#da70d6', + 'palegoldenrod': '#eee8aa', + 'palegreen': '#98fb98', + 'paleturquoise': '#afeeee', + 'palevioletred': '#d87093', + 'papayawhip': '#ffefd5', + 'peachpuff': '#ffdab9', + 'peru': '#cd853f', + 'pink': '#ffc0cb', + 'plum': '#dda0dd', + 'powderblue': '#b0e0e6', + 'purple': '#800080', + 'red': '#ff0000', + 'rosybrown': '#bc8f8f', + 'royalblue': '#4169e1', + 'saddlebrown': '#8b4513', + 'salmon': '#fa8072', + 'sandybrown': '#f4a460', + 'seagreen': '#2e8b57', + 'seashell': '#fff5ee', + 'sienna': '#a0522d', + 'silver': '#c0c0c0', + 'skyblue': '#87ceeb', + 'slateblue': '#6a5acd', + 'slategray': '#708090', + 'slategrey': '#708090', + 'snow': '#fffafa', + 'springgreen': '#00ff7f', + 'steelblue': '#4682b4', + 'tan': '#d2b48c', + 'teal': '#008080', + 'thistle': '#d8bfd8', + 'tomato': '#ff6347', + 'turquoise': '#40e0d0', + 'violet': '#ee82ee', + 'wheat': '#f5deb3', + 'white': '#ffffff', + 'whitesmoke': '#f5f5f5', + 'yellow': '#ffff00', + 'yellowgreen': '#9acd32', +} + + +def flatten(lst): + return [item for sublist in lst for item in sublist] + + +class GeoJsonMapLayer(MapLayer): + + source = StringProperty() + geojson = ObjectProperty() + cache_dir = StringProperty(CACHE_DIR) + + def __init__(self, **kwargs): + self.first_time = True + self.initial_zoom = None + super().__init__(**kwargs) + with self.canvas: + self.canvas_polygon = Canvas() + self.canvas_line = Canvas() + with self.canvas_polygon.before: + PushMatrix() + self.g_matrix = MatrixInstruction() + self.g_scale = Scale() + self.g_translate = Translate() + with self.canvas_polygon: + self.g_canvas_polygon = Canvas() + with self.canvas_polygon.after: + PopMatrix() + + def reposition(self): + vx, vy = self.parent.delta_x, self.parent.delta_y + pzoom = self.parent.zoom + zoom = self.initial_zoom + if zoom is None: + self.initial_zoom = zoom = pzoom + if zoom != pzoom: + diff = 2 ** (pzoom - zoom) + vx /= diff + vy /= diff + self.g_scale.x = self.g_scale.y = diff + else: + self.g_scale.x = self.g_scale.y = 1.0 + self.g_translate.xy = vx, vy + self.g_matrix.matrix = self.parent._scatter.transform + + if self.geojson: + update = not self.first_time + self.on_geojson(self, self.geojson, update=update) + self.first_time = False + + def traverse_feature(self, func, part=None): + """Traverse the whole geojson and call the func with every element + found. + """ + if part is None: + part = self.geojson + if not part: + return + tp = part["type"] + if tp == "FeatureCollection": + for feature in part["features"]: + func(feature) + elif tp == "Feature": + func(part) + + @property + def bounds(self): + # return the min lon, max lon, min lat, max lat + bounds = [float("inf"), float("-inf"), float("inf"), float("-inf")] + + def _submit_coordinate(coord): + lon, lat = coord + bounds[0] = min(bounds[0], lon) + bounds[1] = max(bounds[1], lon) + bounds[2] = min(bounds[2], lat) + bounds[3] = max(bounds[3], lat) + + def _get_bounds(feature): + geometry = feature["geometry"] + tp = geometry["type"] + if tp == "Point": + _submit_coordinate(geometry["coordinates"]) + elif tp == "Polygon": + for coordinate in geometry["coordinates"][0]: + _submit_coordinate(coordinate) + elif tp == "MultiPolygon": + for polygon in geometry["coordinates"]: + for coordinate in polygon[0]: + _submit_coordinate(coordinate) + + self.traverse_feature(_get_bounds) + return bounds + + @property + def center(self): + min_lon, max_lon, min_lat, max_lat = self.bounds + cx = (max_lon - min_lon) / 2.0 + cy = (max_lat - min_lat) / 2.0 + return min_lon + cx, min_lat + cy + + def on_geojson(self, instance, geojson, update=False): + if self.parent is None: + return + if not update: + self.g_canvas_polygon.clear() + self._geojson_part(geojson, geotype="Polygon") + self.canvas_line.clear() + self._geojson_part(geojson, geotype="LineString") + + def on_source(self, instance, value): + if value.startswith(("http://", "https://")): + Downloader.instance(cache_dir=self.cache_dir).download( + value, self._load_geojson_url + ) + else: + with open(value, "rb") as fd: + geojson = json.load(fd) + self.geojson = geojson + + def _load_geojson_url(self, url, response): + self.geojson = response.json() + + def _geojson_part(self, part, geotype=None): + tp = part["type"] + if tp == "FeatureCollection": + for feature in part["features"]: + if geotype and feature["geometry"]["type"] != geotype: + continue + self._geojson_part_f(feature) + elif tp == "Feature": + if geotype and part["geometry"]["type"] == geotype: + self._geojson_part_f(part) + else: + # unhandled geojson part + pass + + def _geojson_part_f(self, feature): + properties = feature["properties"] + geometry = feature["geometry"] + graphics = self._geojson_part_geometry(geometry, properties) + for g in graphics: + tp = geometry["type"] + if tp == "Polygon": + self.g_canvas_polygon.add(g) + else: + self.canvas_line.add(g) + + def _geojson_part_geometry(self, geometry, properties): + tp = geometry["type"] + graphics = [] + if tp == "Polygon": + tess = Tesselator() + for c in geometry["coordinates"]: + xy = list(self._lonlat_to_xy(c)) + xy = flatten(xy) + tess.add_contour(xy) + + tess.tesselate(WINDING_ODD, TYPE_POLYGONS) + + color = self._get_color_from(properties.get("color", "FF000088")) + graphics.append(Color(*color)) + for vertices, indices in tess.meshes: + graphics.append( + Mesh(vertices=vertices, indices=indices, mode="triangle_fan") + ) + + elif tp == "LineString": + stroke = get_color_from_hex(properties.get("stroke", "#ffffff")) + stroke_width = dp(properties.get("stroke-width")) + xy = list(self._lonlat_to_xy(geometry["coordinates"])) + xy = flatten(xy) + graphics.append(Color(*stroke)) + graphics.append(Line(points=xy, width=stroke_width)) + + return graphics + + def _lonlat_to_xy(self, lonlats): + view = self.parent + zoom = view.zoom + for lon, lat in lonlats: + p = view.get_window_xy_from(lat, lon, zoom) + p = p[0] - self.parent.delta_x, p[1] - self.parent.delta_y + p = self.parent._scatter.to_local(*p) + yield p + + def _get_color_from(self, value): + color = COLORS.get(value.lower(), value) + color = get_color_from_hex(color) + return color diff --git a/tagit/external/kivy_garden/mapview/icons/cluster.png b/tagit/external/kivy_garden/mapview/icons/cluster.png Binary files differnew file mode 100644 index 0000000..a704756 --- /dev/null +++ b/tagit/external/kivy_garden/mapview/icons/cluster.png diff --git a/tagit/external/kivy_garden/mapview/icons/marker.png b/tagit/external/kivy_garden/mapview/icons/marker.png Binary files differnew file mode 100644 index 0000000..2824540 --- /dev/null +++ b/tagit/external/kivy_garden/mapview/icons/marker.png diff --git a/tagit/external/kivy_garden/mapview/mbtsource.py b/tagit/external/kivy_garden/mapview/mbtsource.py new file mode 100644 index 0000000..92399a7 --- /dev/null +++ b/tagit/external/kivy_garden/mapview/mbtsource.py @@ -0,0 +1,121 @@ +# coding=utf-8 +""" +MBTiles provider for MapView +============================ + +This provider is based on .mbfiles from MapBox. +See: http://mbtiles.org/ +""" + +__all__ = ["MBTilesMapSource"] + + +import io +import sqlite3 +import threading + +from kivy.core.image import Image as CoreImage +from kivy.core.image import ImageLoader + +from .downloader import Downloader +from .source import MapSource + + +class MBTilesMapSource(MapSource): + def __init__(self, filename, **kwargs): + super().__init__(**kwargs) + self.filename = filename + self.db = sqlite3.connect(filename) + + # read metadata + c = self.db.cursor() + metadata = dict(c.execute("SELECT * FROM metadata")) + if metadata["format"] == "pbf": + raise ValueError("Only raster maps are supported, not vector maps.") + self.min_zoom = int(metadata["minzoom"]) + self.max_zoom = int(metadata["maxzoom"]) + self.attribution = metadata.get("attribution", "") + self.bounds = bounds = None + cx = cy = 0.0 + cz = 5 + if "bounds" in metadata: + self.bounds = bounds = map(float, metadata["bounds"].split(",")) + if "center" in metadata: + cx, cy, cz = map(float, metadata["center"].split(",")) + elif self.bounds: + cx = (bounds[2] + bounds[0]) / 2.0 + cy = (bounds[3] + bounds[1]) / 2.0 + cz = self.min_zoom + self.default_lon = cx + self.default_lat = cy + self.default_zoom = int(cz) + self.projection = metadata.get("projection", "") + self.is_xy = self.projection == "xy" + + def fill_tile(self, tile): + if tile.state == "done": + return + Downloader.instance(self.cache_dir).submit(self._load_tile, tile) + + def _load_tile(self, tile): + # global db context cannot be shared across threads. + ctx = threading.local() + if not hasattr(ctx, "db"): + ctx.db = sqlite3.connect(self.filename) + + # get the right tile + c = ctx.db.cursor() + c.execute( + ( + "SELECT tile_data FROM tiles WHERE " + "zoom_level=? AND tile_column=? AND tile_row=?" + ), + (tile.zoom, tile.tile_x, tile.tile_y), + ) + row = c.fetchone() + if not row: + tile.state = "done" + return + + # no-file loading + try: + data = io.BytesIO(row[0]) + except Exception: + # android issue, "buffer" does not have the buffer interface + # ie row[0] buffer is not compatible with BytesIO on Android?? + data = io.BytesIO(bytes(row[0])) + im = CoreImage( + data, + ext='png', + filename="{}.{}.{}.png".format(tile.zoom, tile.tile_x, tile.tile_y), + ) + + if im is None: + tile.state = "done" + return + + return self._load_tile_done, (tile, im,) + + def _load_tile_done(self, tile, im): + tile.texture = im.texture + tile.state = "need-animation" + + def get_x(self, zoom, lon): + if self.is_xy: + return lon + return super().get_x(zoom, lon) + + def get_y(self, zoom, lat): + if self.is_xy: + return lat + return super().get_y(zoom, lat) + + def get_lon(self, zoom, x): + if self.is_xy: + return x + return super().get_lon(zoom, x) + + def get_lat(self, zoom, y): + if self.is_xy: + return y + return super().get_lat(zoom, y) diff --git a/tagit/external/kivy_garden/mapview/source.py b/tagit/external/kivy_garden/mapview/source.py new file mode 100644 index 0000000..2268d42 --- /dev/null +++ b/tagit/external/kivy_garden/mapview/source.py @@ -0,0 +1,213 @@ +# coding=utf-8 + +__all__ = ["MapSource"] + +import hashlib +from math import atan, ceil, cos, exp, log, pi, tan + +from kivy.metrics import dp + +from .constants import ( + CACHE_DIR, + MAX_LATITUDE, + MAX_LONGITUDE, + MIN_LATITUDE, + MIN_LONGITUDE, +) +from .downloader import Downloader +from .utils import clamp + + +class MapSource: + """Base class for implementing a map source / provider + """ + + attribution_osm = 'Maps & Data © [i][ref=http://www.osm.org/copyright]OpenStreetMap contributors[/ref][/i]' + attribution_thunderforest = 'Maps © [i][ref=http://www.thunderforest.com]Thunderforest[/ref][/i], Data © [i][ref=http://www.osm.org/copyright]OpenStreetMap contributors[/ref][/i]' + + # list of available providers + # cache_key: (is_overlay, minzoom, maxzoom, url, attribution) + providers = { + "osm": ( + 0, + 0, + 19, + "http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", + attribution_osm, + ), + "osm-hot": ( + 0, + 0, + 19, + "http://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png", + "", + ), + "osm-de": ( + 0, + 0, + 18, + "http://{s}.tile.openstreetmap.de/tiles/osmde/{z}/{x}/{y}.png", + "Tiles @ OSM DE", + ), + "osm-fr": ( + 0, + 0, + 20, + "http://{s}.tile.openstreetmap.fr/osmfr/{z}/{x}/{y}.png", + "Tiles @ OSM France", + ), + "cyclemap": ( + 0, + 0, + 17, + "http://{s}.tile.opencyclemap.org/cycle/{z}/{x}/{y}.png", + "Tiles @ Andy Allan", + ), + "thunderforest-cycle": ( + 0, + 0, + 19, + "http://{s}.tile.thunderforest.com/cycle/{z}/{x}/{y}.png", + attribution_thunderforest, + ), + "thunderforest-transport": ( + 0, + 0, + 19, + "http://{s}.tile.thunderforest.com/transport/{z}/{x}/{y}.png", + attribution_thunderforest, + ), + "thunderforest-landscape": ( + 0, + 0, + 19, + "http://{s}.tile.thunderforest.com/landscape/{z}/{x}/{y}.png", + attribution_thunderforest, + ), + "thunderforest-outdoors": ( + 0, + 0, + 19, + "http://{s}.tile.thunderforest.com/outdoors/{z}/{x}/{y}.png", + attribution_thunderforest, + ), + # no longer available + # "mapquest-osm": (0, 0, 19, "http://otile{s}.mqcdn.com/tiles/1.0.0/map/{z}/{x}/{y}.jpeg", "Tiles Courtesy of Mapquest", {"subdomains": "1234", "image_ext": "jpeg"}), + # "mapquest-aerial": (0, 0, 19, "http://oatile{s}.mqcdn.com/tiles/1.0.0/sat/{z}/{x}/{y}.jpeg", "Tiles Courtesy of Mapquest", {"subdomains": "1234", "image_ext": "jpeg"}), + # more to add with + # https://github.com/leaflet-extras/leaflet-providers/blob/master/leaflet-providers.js + # not working ? + # "openseamap": (0, 0, 19, "http://tiles.openseamap.org/seamark/{z}/{x}/{y}.png", + # "Map data @ OpenSeaMap contributors"), + } + + def __init__( + self, + url="http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", + cache_key=None, + min_zoom=0, + max_zoom=19, + tile_size=256, + image_ext="png", + attribution="© OpenStreetMap contributors", + subdomains="abc", + **kwargs + ): + if cache_key is None: + # possible cache hit, but very unlikely + cache_key = hashlib.sha224(url.encode("utf8")).hexdigest()[:10] + self.url = url + self.cache_key = cache_key + self.min_zoom = min_zoom + self.max_zoom = max_zoom + self.tile_size = tile_size + self.image_ext = image_ext + self.attribution = attribution + self.subdomains = subdomains + self.cache_fmt = "{cache_key}_{zoom}_{tile_x}_{tile_y}.{image_ext}" + #self.dp_tile_size = min(dp(self.tile_size), self.tile_size * 2) + self.dp_tile_size = 256 + self.default_lat = self.default_lon = self.default_zoom = None + self.bounds = None + self.cache_dir = kwargs.get('cache_dir', CACHE_DIR) + + @staticmethod + def from_provider(key, **kwargs): + provider = MapSource.providers[key] + cache_dir = kwargs.get('cache_dir', CACHE_DIR) + options = {} + is_overlay, min_zoom, max_zoom, url, attribution = provider[:5] + if len(provider) > 5: + options = provider[5] + return MapSource( + cache_key=key, + min_zoom=min_zoom, + max_zoom=max_zoom, + url=url, + cache_dir=cache_dir, + attribution=attribution, + **options + ) + + def get_x(self, zoom, lon): + """Get the x position on the map using this map source's projection + (0, 0) is located at the top left. + """ + lon = clamp(lon, MIN_LONGITUDE, MAX_LONGITUDE) + return ((lon + 180.0) / 360.0 * pow(2.0, zoom)) * self.dp_tile_size + + def get_y(self, zoom, lat): + """Get the y position on the map using this map source's projection + (0, 0) is located at the top left. + """ + lat = clamp(-lat, MIN_LATITUDE, MAX_LATITUDE) + lat = lat * pi / 180.0 + return ( + (1.0 - log(tan(lat) + 1.0 / cos(lat)) / pi) / 2.0 * pow(2.0, zoom) + ) * self.dp_tile_size + + def get_lon(self, zoom, x): + """Get the longitude to the x position in the map source's projection + """ + dx = x / float(self.dp_tile_size) + lon = dx / pow(2.0, zoom) * 360.0 - 180.0 + return clamp(lon, MIN_LONGITUDE, MAX_LONGITUDE) + + def get_lat(self, zoom, y): + """Get the latitude to the y position in the map source's projection + """ + dy = y / float(self.dp_tile_size) + n = pi - 2 * pi * dy / pow(2.0, zoom) + lat = -180.0 / pi * atan(0.5 * (exp(n) - exp(-n))) + return clamp(lat, MIN_LATITUDE, MAX_LATITUDE) + + def get_row_count(self, zoom): + """Get the number of tiles in a row at this zoom level + """ + if zoom == 0: + return 1 + return 2 << (zoom - 1) + + def get_col_count(self, zoom): + """Get the number of tiles in a col at this zoom level + """ + if zoom == 0: + return 1 + return 2 << (zoom - 1) + + def get_min_zoom(self): + """Return the minimum zoom of this source + """ + return self.min_zoom + + def get_max_zoom(self): + """Return the maximum zoom of this source + """ + return self.max_zoom + + def fill_tile(self, tile): + """Add this tile to load within the downloader + """ + if tile.state == "done": + return + Downloader.instance(cache_dir=self.cache_dir).download_tile(tile) diff --git a/tagit/external/kivy_garden/mapview/types.py b/tagit/external/kivy_garden/mapview/types.py new file mode 100644 index 0000000..622d8a9 --- /dev/null +++ b/tagit/external/kivy_garden/mapview/types.py @@ -0,0 +1,29 @@ +# coding=utf-8 + +__all__ = ["Coordinate", "Bbox"] + +from collections import namedtuple + +Coordinate = namedtuple("Coordinate", ["lat", "lon"]) + + +class Bbox(tuple): + def collide(self, *args): + if isinstance(args[0], Coordinate): + coord = args[0] + lat = coord.lat + lon = coord.lon + else: + lat, lon = args + lat1, lon1, lat2, lon2 = self[:] + + if lat1 < lat2: + in_lat = lat1 <= lat <= lat2 + else: + in_lat = lat2 <= lat <= lat2 + if lon1 < lon2: + in_lon = lon1 <= lon <= lon2 + else: + in_lon = lon2 <= lon <= lon2 + + return in_lat and in_lon diff --git a/tagit/external/kivy_garden/mapview/utils.py b/tagit/external/kivy_garden/mapview/utils.py new file mode 100644 index 0000000..1999715 --- /dev/null +++ b/tagit/external/kivy_garden/mapview/utils.py @@ -0,0 +1,51 @@ +# coding=utf-8 + +__all__ = ["clamp", "haversine", "get_zoom_for_radius"] + +from math import asin, cos, pi, radians, sin, sqrt + +from kivy.metrics import dp + + +def clamp(x, minimum, maximum): + return max(minimum, min(x, maximum)) + + +def haversine(lon1, lat1, lon2, lat2): + """ + Calculate the great circle distance between two points + on the earth (specified in decimal degrees) + + Taken from: http://stackoverflow.com/questions/4913349/haversine-formula-in-python-bearing-and-distance-between-two-gps-points + """ + # convert decimal degrees to radians + lon1, lat1, lon2, lat2 = map(radians, [lon1, lat1, lon2, lat2]) + # haversine formula + dlon = lon2 - lon1 + dlat = lat2 - lat1 + a = sin(dlat / 2) ** 2 + cos(lat1) * cos(lat2) * sin(dlon / 2) ** 2 + + c = 2 * asin(sqrt(a)) + km = 6367 * c + return km + + +def get_zoom_for_radius(radius_km, lat=None, tile_size=256.0): + """See: https://wiki.openstreetmap.org/wiki/Zoom_levels""" + #from kivy.core.window import Window + radius = radius_km * 1000.0 + if lat is None: + lat = 0.0 # Do not compensate for the latitude + + # Calculate the equatorial circumference based on the WGS-84 radius + earth_circumference = 2.0 * pi * 6378137.0 * cos(lat * pi / 180.0) + + # Check how many tiles that are currently in view + #nr_tiles_shown = min(Window.size) / dp(tile_size) + nr_tiles_shown = min(1024) / dp(tile_size) + + # Keep zooming in until we find a zoom level where the circle can fit inside the screen + zoom = 1 + while earth_circumference / (2 << (zoom - 1)) * nr_tiles_shown > 2 * radius: + zoom += 1 + return zoom - 1 # Go one zoom level back diff --git a/tagit/external/kivy_garden/mapview/view.py b/tagit/external/kivy_garden/mapview/view.py new file mode 100644 index 0000000..0f34e49 --- /dev/null +++ b/tagit/external/kivy_garden/mapview/view.py @@ -0,0 +1,999 @@ +# coding=utf-8 + +__all__ = ["MapView", "MapMarker", "MapMarkerPopup", "MapLayer", "MarkerMapLayer"] + +import webbrowser +from itertools import takewhile +from math import ceil +from os.path import dirname, join + +from kivy.clock import Clock +from kivy.compat import string_types +from kivy.graphics import Canvas, Color, Rectangle +from kivy.graphics.transformation import Matrix +from kivy.lang import Builder +from kivy.metrics import dp +from kivy.properties import ( + AliasProperty, + BooleanProperty, + ListProperty, + NumericProperty, + ObjectProperty, + StringProperty, +) +from kivy.uix.behaviors import ButtonBehavior +from kivy.uix.image import Image +from kivy.uix.label import Label +from kivy.uix.scatter import Scatter +from kivy.uix.widget import Widget + +from . import Bbox, Coordinate +from .constants import ( + CACHE_DIR, + MAX_LATITUDE, + MAX_LONGITUDE, + MIN_LATITUDE, + MIN_LONGITUDE, +) +from .source import MapSource +from .utils import clamp + +Builder.load_string( + """ +<MapMarker>: + size_hint: None, None + source: root.source + size: list(map(dp, self.texture_size)) + allow_stretch: True + +<MapView>: + canvas.before: + StencilPush + Rectangle: + pos: self.pos + size: self.size + StencilUse + Color: + rgba: self.background_color + Rectangle: + pos: self.pos + size: self.size + canvas.after: + StencilUnUse + Rectangle: + pos: self.pos + size: self.size + StencilPop + + ClickableLabel: + text: root.map_source.attribution if hasattr(root.map_source, "attribution") else "" + size_hint: None, None + size: self.texture_size[0] + sp(8), self.texture_size[1] + sp(4) + font_size: "10sp" + right: [root.right, self.center][0] + color: 0, 0, 0, 1 + markup: True + canvas.before: + Color: + rgba: .8, .8, .8, .8 + Rectangle: + pos: self.pos + size: self.size + + +<MapViewScatter>: + auto_bring_to_front: False + do_rotation: False + scale_min: 0.2 + scale_max: 3. + +<MapMarkerPopup>: + RelativeLayout: + id: placeholder + y: root.top + center_x: root.center_x + size: root.popup_size + +""" +) + + +class ClickableLabel(Label): + def on_ref_press(self, *args): + webbrowser.open(str(args[0]), new=2) + + +class Tile(Rectangle): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.cache_dir = kwargs.get('cache_dir', CACHE_DIR) + + @property + def cache_fn(self): + map_source = self.map_source + fn = map_source.cache_fmt.format( + image_ext=map_source.image_ext, + cache_key=map_source.cache_key, + **self.__dict__ + ) + return join(self.cache_dir, fn) + + def set_source(self, cache_fn): + self.source = cache_fn + self.state = "need-animation" + + +class MapMarker(ButtonBehavior, Image): + """A marker on a map, that must be used on a :class:`MapMarker` + """ + + anchor_x = NumericProperty(0.5) + """Anchor of the marker on the X axis. Defaults to 0.5, mean the anchor will + be at the X center of the image. + """ + + anchor_y = NumericProperty(0) + """Anchor of the marker on the Y axis. Defaults to 0, mean the anchor will + be at the Y bottom of the image. + """ + + lat = NumericProperty(0) + """Latitude of the marker + """ + + lon = NumericProperty(0) + """Longitude of the marker + """ + + source = StringProperty(join(dirname(__file__), "icons", "marker.png")) + """Source of the marker, defaults to our own marker.png + """ + + # (internal) reference to its layer + _layer = None + + def detach(self): + if self._layer: + self._layer.remove_widget(self) + self._layer = None + + +class MapMarkerPopup(MapMarker): + is_open = BooleanProperty(False) + placeholder = ObjectProperty(None) + popup_size = ListProperty([100, 100]) + + def add_widget(self, widget): + if not self.placeholder: + self.placeholder = widget + if self.is_open: + super().add_widget(self.placeholder) + else: + self.placeholder.add_widget(widget) + + def remove_widget(self, widget): + if widget is not self.placeholder: + self.placeholder.remove_widget(widget) + else: + super().remove_widget(widget) + + def on_is_open(self, *args): + self.refresh_open_status() + + def on_release(self, *args): + self.is_open = not self.is_open + + def refresh_open_status(self): + if not self.is_open and self.placeholder.parent: + super().remove_widget(self.placeholder) + elif self.is_open and not self.placeholder.parent: + super().add_widget(self.placeholder) + + +class MapLayer(Widget): + """A map layer, that is repositionned everytime the :class:`MapView` is + moved. + """ + + viewport_x = NumericProperty(0) + viewport_y = NumericProperty(0) + + def reposition(self): + """Function called when :class:`MapView` is moved. You must recalculate + the position of your children. + """ + pass + + def unload(self): + """Called when the view want to completly unload the layer. + """ + pass + + +class MarkerMapLayer(MapLayer): + """A map layer for :class:`MapMarker` + """ + + order_marker_by_latitude = BooleanProperty(True) + + def __init__(self, **kwargs): + self.markers = [] + super().__init__(**kwargs) + + def insert_marker(self, marker, **kwargs): + if self.order_marker_by_latitude: + before = list( + takewhile(lambda i_m: i_m[1].lat < marker.lat, enumerate(self.children)) + ) + if before: + kwargs['index'] = before[-1][0] + 1 + + super().add_widget(marker, **kwargs) + + def add_widget(self, marker): + marker._layer = self + self.markers.append(marker) + self.insert_marker(marker) + + def remove_widget(self, marker): + marker._layer = None + if marker in self.markers: + self.markers.remove(marker) + super().remove_widget(marker) + + def reposition(self): + if not self.markers: + return + mapview = self.parent + set_marker_position = self.set_marker_position + bbox = None + # reposition the markers depending the latitude + markers = sorted(self.markers, key=lambda x: -x.lat) + margin = max((max(marker.size) for marker in markers)) + bbox = mapview.get_bbox(margin) + for marker in markers: + if bbox.collide(marker.lat, marker.lon): + set_marker_position(mapview, marker) + if not marker.parent: + self.insert_marker(marker) + else: + super().remove_widget(marker) + + def set_marker_position(self, mapview, marker): + x, y = mapview.get_window_xy_from(marker.lat, marker.lon, mapview.zoom) + marker.x = int(x - marker.width * marker.anchor_x) + marker.y = int(y - marker.height * marker.anchor_y) + + def unload(self): + self.clear_widgets() + del self.markers[:] + + +class MapViewScatter(Scatter): + # internal + def on_transform(self, *args): + super().on_transform(*args) + self.parent.on_transform(self.transform) + + def collide_point(self, x, y): + return True + + +class MapView(Widget): + """MapView is the widget that control the map displaying, navigation, and + layers management. + """ + + lon = NumericProperty() + """Longitude at the center of the widget + """ + + lat = NumericProperty() + """Latitude at the center of the widget + """ + + zoom = NumericProperty(0) + """Zoom of the widget. Must be between :meth:`MapSource.get_min_zoom` and + :meth:`MapSource.get_max_zoom`. Default to 0. + """ + + map_source = ObjectProperty(MapSource()) + """Provider of the map, default to a empty :class:`MapSource`. + """ + + double_tap_zoom = BooleanProperty(False) + """If True, this will activate the double-tap to zoom. + """ + + pause_on_action = BooleanProperty(True) + """Pause any map loading / tiles loading when an action is done. + This allow better performance on mobile, but can be safely deactivated on + desktop. + """ + + snap_to_zoom = BooleanProperty(True) + """When the user initiate a zoom, it will snap to the closest zoom for + better graphics. The map can be blur if the map is scaled between 2 zoom. + Default to True, even if it doesn't fully working yet. + """ + + animation_duration = NumericProperty(100) + """Duration to animate Tiles alpha from 0 to 1 when it's ready to show. + Default to 100 as 100ms. Use 0 to deactivate. + """ + + delta_x = NumericProperty(0) + delta_y = NumericProperty(0) + background_color = ListProperty([181 / 255.0, 208 / 255.0, 208 / 255.0, 1]) + cache_dir = StringProperty(CACHE_DIR) + _zoom = NumericProperty(0) + _pause = BooleanProperty(False) + _scale = 1.0 + _disabled_count = 0 + + __events__ = ["on_map_relocated"] + + # Public API + + @property + def viewport_pos(self): + vx, vy = self._scatter.to_local(self.x, self.y) + return vx - self.delta_x, vy - self.delta_y + + @property + def scale(self): + if self._invalid_scale: + self._invalid_scale = False + self._scale = self._scatter.scale + return self._scale + + def get_bbox(self, margin=0): + """Returns the bounding box from the bottom/left (lat1, lon1) to + top/right (lat2, lon2). + """ + x1, y1 = self.to_local(0 - margin, 0 - margin) + x2, y2 = self.to_local((self.width + margin), (self.height + margin)) + c1 = self.get_latlon_at(x1, y1) + c2 = self.get_latlon_at(x2, y2) + return Bbox((c1.lat, c1.lon, c2.lat, c2.lon)) + + bbox = AliasProperty(get_bbox, None, bind=["lat", "lon", "_zoom"]) + + def unload(self): + """Unload the view and all the layers. + It also cancel all the remaining downloads. + """ + self.remove_all_tiles() + + def get_window_xy_from(self, lat, lon, zoom): + """Returns the x/y position in the widget absolute coordinates + from a lat/lon""" + scale = self.scale + vx, vy = self.viewport_pos + ms = self.map_source + x = ms.get_x(zoom, lon) - vx + y = ms.get_y(zoom, lat) - vy + x *= scale + y *= scale + x = x + self.pos[0] + y = y + self.pos[1] + return x, y + + def center_on(self, *args): + """Center the map on the coordinate :class:`Coordinate`, or a (lat, lon) + """ + map_source = self.map_source + zoom = self._zoom + + if len(args) == 1 and isinstance(args[0], Coordinate): + coord = args[0] + lat = coord.lat + lon = coord.lon + elif len(args) == 2: + lat, lon = args + else: + raise Exception("Invalid argument for center_on") + lon = clamp(lon, MIN_LONGITUDE, MAX_LONGITUDE) + lat = clamp(lat, MIN_LATITUDE, MAX_LATITUDE) + scale = self._scatter.scale + x = map_source.get_x(zoom, lon) - self.center_x / scale + y = map_source.get_y(zoom, lat) - self.center_y / scale + self.delta_x = -x + self.delta_y = -y + self.lon = lon + self.lat = lat + self._scatter.pos = 0, 0 + self.trigger_update(True) + + def set_zoom_at(self, zoom, x, y, scale=None): + """Sets the zoom level, leaving the (x, y) at the exact same point + in the view. + """ + zoom = clamp( + zoom, self.map_source.get_min_zoom(), self.map_source.get_max_zoom() + ) + if int(zoom) == int(self._zoom): + if scale is None: + return + elif scale == self.scale: + return + scale = scale or 1.0 + + # first, rescale the scatter + scatter = self._scatter + scale = clamp(scale, scatter.scale_min, scatter.scale_max) + rescale = scale * 1.0 / scatter.scale + scatter.apply_transform( + Matrix().scale(rescale, rescale, rescale), + post_multiply=True, + anchor=scatter.to_local(x, y), + ) + + # adjust position if the zoom changed + c1 = self.map_source.get_col_count(self._zoom) + c2 = self.map_source.get_col_count(zoom) + if c1 != c2: + f = float(c2) / float(c1) + self.delta_x = scatter.x + self.delta_x * f + self.delta_y = scatter.y + self.delta_y * f + # back to 0 every time + scatter.apply_transform( + Matrix().translate(-scatter.x, -scatter.y, 0), post_multiply=True + ) + + # avoid triggering zoom changes. + self._zoom = zoom + self.zoom = self._zoom + + def on_zoom(self, instance, zoom): + if zoom == self._zoom: + return + x = self.map_source.get_x(zoom, self.lon) - self.delta_x + y = self.map_source.get_y(zoom, self.lat) - self.delta_y + self.set_zoom_at(zoom, x, y) + self.center_on(self.lat, self.lon) + + def get_latlon_at(self, x, y, zoom=None): + """Return the current :class:`Coordinate` within the (x, y) widget + coordinate. + """ + if zoom is None: + zoom = self._zoom + vx, vy = self.viewport_pos + scale = self._scale + return Coordinate( + lat=self.map_source.get_lat(zoom, y / scale + vy), + lon=self.map_source.get_lon(zoom, x / scale + vx), + ) + + def add_marker(self, marker, layer=None): + """Add a marker into the layer. If layer is None, it will be added in + the default marker layer. If there is no default marker layer, a new + one will be automatically created + """ + if layer is None: + if not self._default_marker_layer: + layer = MarkerMapLayer() + self.add_layer(layer) + else: + layer = self._default_marker_layer + layer.add_widget(marker) + layer.set_marker_position(self, marker) + + def remove_marker(self, marker): + """Remove a marker from its layer + """ + marker.detach() + + def add_layer(self, layer, mode="window"): + """Add a new layer to update at the same time the base tile layer. + mode can be either "scatter" or "window". If "scatter", it means the + layer will be within the scatter transformation. It's perfect if you + want to display path / shape, but not for text. + If "window", it will have no transformation. You need to position the + widget yourself: think as Z-sprite / billboard. + Defaults to "window". + """ + assert mode in ("scatter", "window") + if self._default_marker_layer is None and isinstance(layer, MarkerMapLayer): + self._default_marker_layer = layer + self._layers.append(layer) + c = self.canvas + if mode == "scatter": + self.canvas = self.canvas_layers + else: + self.canvas = self.canvas_layers_out + layer.canvas_parent = self.canvas + super().add_widget(layer) + self.canvas = c + + def remove_layer(self, layer): + """Remove the layer + """ + c = self.canvas + self._layers.remove(layer) + self.canvas = layer.canvas_parent + super().remove_widget(layer) + self.canvas = c + + def sync_to(self, other): + """Reflect the lat/lon/zoom of the other MapView to the current one. + """ + if self._zoom != other._zoom: + self.set_zoom_at(other._zoom, *self.center) + self.center_on(other.get_latlon_at(*self.center)) + + # Private API + + def __init__(self, **kwargs): + from kivy.base import EventLoop + + EventLoop.ensure_window() + self._invalid_scale = True + self._tiles = [] + self._tiles_bg = [] + self._tilemap = {} + self._layers = [] + self._default_marker_layer = None + self._need_redraw_all = False + self._transform_lock = False + self.trigger_update(True) + self.canvas = Canvas() + self._scatter = MapViewScatter() + self.add_widget(self._scatter) + with self._scatter.canvas: + self.canvas_map = Canvas() + self.canvas_layers = Canvas() + with self.canvas: + self.canvas_layers_out = Canvas() + self._scale_target_anim = False + self._scale_target = 1.0 + self._touch_count = 0 + self.map_source.cache_dir = self.cache_dir + Clock.schedule_interval(self._animate_color, 1 / 60.0) + self.lat = kwargs.get("lat", self.lat) + self.lon = kwargs.get("lon", self.lon) + super().__init__(**kwargs) + + def _animate_color(self, dt): + # fast path + d = self.animation_duration + if d == 0: + for tile in self._tiles: + if tile.state == "need-animation": + tile.g_color.a = 1.0 + tile.state = "animated" + for tile in self._tiles_bg: + if tile.state == "need-animation": + tile.g_color.a = 1.0 + tile.state = "animated" + else: + d = d / 1000.0 + for tile in self._tiles: + if tile.state != "need-animation": + continue + tile.g_color.a += dt / d + if tile.g_color.a >= 1: + tile.state = "animated" + for tile in self._tiles_bg: + if tile.state != "need-animation": + continue + tile.g_color.a += dt / d + if tile.g_color.a >= 1: + tile.state = "animated" + + def add_widget(self, widget): + if isinstance(widget, MapMarker): + self.add_marker(widget) + elif isinstance(widget, MapLayer): + self.add_layer(widget) + else: + super().add_widget(widget) + + def remove_widget(self, widget): + if isinstance(widget, MapMarker): + self.remove_marker(widget) + elif isinstance(widget, MapLayer): + self.remove_layer(widget) + else: + super().remove_widget(widget) + + def on_map_relocated(self, zoom, coord): + pass + + def animated_diff_scale_at(self, d, x, y): + self._scale_target_time = 1.0 + self._scale_target_pos = x, y + if self._scale_target_anim is False: + self._scale_target_anim = True + self._scale_target = d + else: + self._scale_target += d + Clock.unschedule(self._animate_scale) + Clock.schedule_interval(self._animate_scale, 1 / 60.0) + + def _animate_scale(self, dt): + diff = self._scale_target / 3.0 + if abs(diff) < 0.01: + diff = self._scale_target + self._scale_target = 0 + else: + self._scale_target -= diff + self._scale_target_time -= dt + self.diff_scale_at(diff, *self._scale_target_pos) + ret = self._scale_target != 0 + if not ret: + self._pause = False + return ret + + def diff_scale_at(self, d, x, y): + scatter = self._scatter + scale = scatter.scale * (2 ** d) + self.scale_at(scale, x, y) + + def scale_at(self, scale, x, y): + scatter = self._scatter + scale = clamp(scale, scatter.scale_min, scatter.scale_max) + rescale = scale * 1.0 / scatter.scale + scatter.apply_transform( + Matrix().scale(rescale, rescale, rescale), + post_multiply=True, + anchor=scatter.to_local(x, y), + ) + + def on_touch_down(self, touch): + if not self.collide_point(*touch.pos): + return + if self.pause_on_action: + self._pause = True + if "button" in touch.profile and touch.button in ("scrolldown", "scrollup"): + d = 1 if touch.button == "scrollup" else -1 + self.animated_diff_scale_at(d, *touch.pos) + return True + elif touch.is_double_tap and self.double_tap_zoom: + self.animated_diff_scale_at(1, *touch.pos) + return True + touch.grab(self) + self._touch_count += 1 + if self._touch_count == 1: + self._touch_zoom = (self.zoom, self._scale) + return super().on_touch_down(touch) + + def on_touch_up(self, touch): + if touch.grab_current == self: + touch.ungrab(self) + self._touch_count -= 1 + if self._touch_count == 0: + # animate to the closest zoom + zoom, scale = self._touch_zoom + cur_zoom = self.zoom + cur_scale = self._scale + if cur_zoom < zoom or cur_scale < scale: + self.animated_diff_scale_at(1.0 - cur_scale, *touch.pos) + elif cur_zoom > zoom or cur_scale > scale: + self.animated_diff_scale_at(2.0 - cur_scale, *touch.pos) + self._pause = False + return True + return super().on_touch_up(touch) + + def on_transform(self, *args): + self._invalid_scale = True + if self._transform_lock: + return + self._transform_lock = True + # recalculate viewport + map_source = self.map_source + zoom = self._zoom + scatter = self._scatter + scale = scatter.scale + if scale >= 2.0: + zoom += 1 + scale /= 2.0 + elif scale < 1: + zoom -= 1 + scale *= 2.0 + zoom = clamp(zoom, map_source.min_zoom, map_source.max_zoom) + if zoom != self._zoom: + self.set_zoom_at(zoom, scatter.x, scatter.y, scale=scale) + self.trigger_update(True) + else: + if zoom == map_source.min_zoom and scatter.scale < 1.0: + scatter.scale = 1.0 + self.trigger_update(True) + else: + self.trigger_update(False) + + if map_source.bounds: + self._apply_bounds() + self._transform_lock = False + self._scale = self._scatter.scale + + def _apply_bounds(self): + # if the map_source have any constraints, apply them here. + map_source = self.map_source + zoom = self._zoom + min_lon, min_lat, max_lon, max_lat = map_source.bounds + xmin = map_source.get_x(zoom, min_lon) + xmax = map_source.get_x(zoom, max_lon) + ymin = map_source.get_y(zoom, min_lat) + ymax = map_source.get_y(zoom, max_lat) + + dx = self.delta_x + dy = self.delta_y + oxmin, oymin = self._scatter.to_local(self.x, self.y) + oxmax, oymax = self._scatter.to_local(self.right, self.top) + s = self._scale + cxmin = oxmin - dx + if cxmin < xmin: + self._scatter.x += (cxmin - xmin) * s + cymin = oymin - dy + if cymin < ymin: + self._scatter.y += (cymin - ymin) * s + cxmax = oxmax - dx + if cxmax > xmax: + self._scatter.x -= (xmax - cxmax) * s + cymax = oymax - dy + if cymax > ymax: + self._scatter.y -= (ymax - cymax) * s + + def on__pause(self, instance, value): + if not value: + self.trigger_update(True) + + def trigger_update(self, full): + self._need_redraw_full = full or self._need_redraw_full + Clock.unschedule(self.do_update) + Clock.schedule_once(self.do_update, -1) + + def do_update(self, dt): + zoom = self._zoom + scale = self._scale + self.lon = self.map_source.get_lon( + zoom, (self.center_x - self._scatter.x) / scale - self.delta_x + ) + self.lat = self.map_source.get_lat( + zoom, (self.center_y - self._scatter.y) / scale - self.delta_y + ) + self.dispatch("on_map_relocated", zoom, Coordinate(self.lon, self.lat)) + for layer in self._layers: + layer.reposition() + + if self._need_redraw_full: + self._need_redraw_full = False + self.move_tiles_to_background() + self.load_visible_tiles() + else: + self.load_visible_tiles() + + def bbox_for_zoom(self, vx, vy, w, h, zoom): + # return a tile-bbox for the zoom + map_source = self.map_source + size = map_source.dp_tile_size + scale = self._scale + + max_x_end = map_source.get_col_count(zoom) + max_y_end = map_source.get_row_count(zoom) + + x_count = int(ceil(w / scale / float(size))) + 1 + y_count = int(ceil(h / scale / float(size))) + 1 + + tile_x_first = int(clamp(vx / float(size), 0, max_x_end)) + tile_y_first = int(clamp(vy / float(size), 0, max_y_end)) + tile_x_last = tile_x_first + x_count + tile_y_last = tile_y_first + y_count + tile_x_last = int(clamp(tile_x_last, tile_x_first, max_x_end)) + tile_y_last = int(clamp(tile_y_last, tile_y_first, max_y_end)) + + x_count = tile_x_last - tile_x_first + y_count = tile_y_last - tile_y_first + return (tile_x_first, tile_y_first, tile_x_last, tile_y_last, x_count, y_count) + + def load_visible_tiles(self): + map_source = self.map_source + vx, vy = self.viewport_pos + zoom = self._zoom + dirs = [0, 1, 0, -1, 0] + bbox_for_zoom = self.bbox_for_zoom + size = map_source.dp_tile_size + + ( + tile_x_first, + tile_y_first, + tile_x_last, + tile_y_last, + x_count, + y_count, + ) = bbox_for_zoom(vx, vy, self.width, self.height, zoom) + + # Adjust tiles behind us + for tile in self._tiles_bg[:]: + tile_x = tile.tile_x + tile_y = tile.tile_y + + f = 2 ** (zoom - tile.zoom) + w = self.width / f + h = self.height / f + ( + btile_x_first, + btile_y_first, + btile_x_last, + btile_y_last, + _, + _, + ) = bbox_for_zoom(vx / f, vy / f, w, h, tile.zoom) + + if ( + tile_x < btile_x_first + or tile_x >= btile_x_last + or tile_y < btile_y_first + or tile_y >= btile_y_last + ): + tile.state = "done" + self._tiles_bg.remove(tile) + self.canvas_map.before.remove(tile.g_color) + self.canvas_map.before.remove(tile) + continue + + tsize = size * f + tile.size = tsize, tsize + tile.pos = (tile_x * tsize + self.delta_x, tile_y * tsize + self.delta_y) + + # Get rid of old tiles first + for tile in self._tiles[:]: + tile_x = tile.tile_x + tile_y = tile.tile_y + + if ( + tile_x < tile_x_first + or tile_x >= tile_x_last + or tile_y < tile_y_first + or tile_y >= tile_y_last + ): + tile.state = "done" + self.tile_map_set(tile_x, tile_y, False) + self._tiles.remove(tile) + self.canvas_map.remove(tile) + self.canvas_map.remove(tile.g_color) + else: + tile.size = (size, size) + tile.pos = (tile_x * size + self.delta_x, tile_y * size + self.delta_y) + + # Load new tiles if needed + x = tile_x_first + x_count // 2 - 1 + y = tile_y_first + y_count // 2 - 1 + arm_max = max(x_count, y_count) + 2 + arm_size = 1 + turn = 0 + while arm_size < arm_max: + for i in range(arm_size): + if ( + not self.tile_in_tile_map(x, y) + and y >= tile_y_first + and y < tile_y_last + and x >= tile_x_first + and x < tile_x_last + ): + self.load_tile(x, y, size, zoom) + + x += dirs[turn % 4 + 1] + y += dirs[turn % 4] + + if turn % 2 == 1: + arm_size += 1 + + turn += 1 + + def load_tile(self, x, y, size, zoom): + if self.tile_in_tile_map(x, y) or zoom != self._zoom: + return + self.load_tile_for_source(self.map_source, 1.0, size, x, y, zoom) + # XXX do overlay support + self.tile_map_set(x, y, True) + + def load_tile_for_source(self, map_source, opacity, size, x, y, zoom): + tile = Tile(size=(size, size), cache_dir=self.cache_dir) + tile.g_color = Color(1, 1, 1, 0) + tile.tile_x = x + tile.tile_y = y + tile.zoom = zoom + tile.pos = (x * size + self.delta_x, y * size + self.delta_y) + tile.map_source = map_source + tile.state = "loading" + if not self._pause: + map_source.fill_tile(tile) + self.canvas_map.add(tile.g_color) + self.canvas_map.add(tile) + self._tiles.append(tile) + + def move_tiles_to_background(self): + # remove all the tiles of the main map to the background map + # retain only the one who are on the current zoom level + # for all the tile in the background, stop the download if not yet started. + zoom = self._zoom + tiles = self._tiles + btiles = self._tiles_bg + canvas_map = self.canvas_map + tile_size = self.map_source.tile_size + + # move all tiles to background + while tiles: + tile = tiles.pop() + if tile.state == "loading": + tile.state = "done" + continue + btiles.append(tile) + + # clear the canvas + canvas_map.clear() + canvas_map.before.clear() + self._tilemap = {} + + # unsure if it's really needed, i personnally didn't get issues right now + # btiles.sort(key=lambda z: -z.zoom) + + # add all the btiles into the back canvas. + # except for the tiles that are owned by the current zoom level + for tile in btiles[:]: + if tile.zoom == zoom: + btiles.remove(tile) + tiles.append(tile) + tile.size = tile_size, tile_size + canvas_map.add(tile.g_color) + canvas_map.add(tile) + self.tile_map_set(tile.tile_x, tile.tile_y, True) + continue + canvas_map.before.add(tile.g_color) + canvas_map.before.add(tile) + + def remove_all_tiles(self): + # clear the map of all tiles. + self.canvas_map.clear() + self.canvas_map.before.clear() + for tile in self._tiles: + tile.state = "done" + del self._tiles[:] + del self._tiles_bg[:] + self._tilemap = {} + + def tile_map_set(self, tile_x, tile_y, value): + key = tile_y * self.map_source.get_col_count(self._zoom) + tile_x + if value: + self._tilemap[key] = value + else: + self._tilemap.pop(key, None) + + def tile_in_tile_map(self, tile_x, tile_y): + key = tile_y * self.map_source.get_col_count(self._zoom) + tile_x + return key in self._tilemap + + def on_size(self, instance, size): + for layer in self._layers: + layer.size = size + self.center_on(self.lat, self.lon) + self.trigger_update(True) + + def on_pos(self, instance, pos): + self.center_on(self.lat, self.lon) + self.trigger_update(True) + + def on_map_source(self, instance, source): + if isinstance(source, string_types): + self.map_source = MapSource.from_provider(source) + elif isinstance(source, (tuple, list)): + cache_key, min_zoom, max_zoom, url, attribution, options = source + self.map_source = MapSource( + url=url, + cache_key=cache_key, + min_zoom=min_zoom, + max_zoom=max_zoom, + attribution=attribution, + cache_dir=self.cache_dir, + **options + ) + elif isinstance(source, MapSource): + self.map_source = source + else: + raise Exception("Invalid map source provider") + self.zoom = clamp(self.zoom, self.map_source.min_zoom, self.map_source.max_zoom) + self.remove_all_tiles() + self.trigger_update(True) diff --git a/tagit/external/setproperty/README.md b/tagit/external/setproperty/README.md new file mode 100644 index 0000000..4849b6c --- /dev/null +++ b/tagit/external/setproperty/README.md @@ -0,0 +1,27 @@ + +# SetProperty + +Analigous to kivy.properties.ListProperty, SetProperty provides a wrapper +that extends python's set() with [Kivy](https://kivy.org) events. + +## Installation + +Note that you'll need Cython to run the following commands. +Install Cython via: + +$ pip install Cython + +or via your system's package manager (e.g., ``sudo apt install cython``). + +Then, cythonize the SetProperty with: + +$ python preprocess.py + +build the shared library from cythonized with: + +$ python build.py build_ext --inplace + +or perform both steps with one command: + +$ python setup.py build_ext --inplace + diff --git a/tagit/external/setproperty/__init__.py b/tagit/external/setproperty/__init__.py new file mode 100644 index 0000000..b8fe9c2 --- /dev/null +++ b/tagit/external/setproperty/__init__.py @@ -0,0 +1,3 @@ + +from .setproperty import SetProperty + diff --git a/tagit/external/setproperty/build.py b/tagit/external/setproperty/build.py new file mode 100644 index 0000000..9b3d6ab --- /dev/null +++ b/tagit/external/setproperty/build.py @@ -0,0 +1,5 @@ + +from distutils.core import Extension, setup +ext = Extension(name="setproperty", sources=["setproperty.c"]) +setup(ext_modules=[ext]) + diff --git a/tagit/external/setproperty/preprocess.py b/tagit/external/setproperty/preprocess.py new file mode 100644 index 0000000..7c85b86 --- /dev/null +++ b/tagit/external/setproperty/preprocess.py @@ -0,0 +1,8 @@ + +from distutils.core import Extension +from Cython.Build import cythonize + +ext = Extension(name="setproperty", sources=["setproperty.pyx"]) +cythonize(ext) + + diff --git a/tagit/external/setproperty/setproperty.pxd b/tagit/external/setproperty/setproperty.pxd new file mode 100644 index 0000000..51acb25 --- /dev/null +++ b/tagit/external/setproperty/setproperty.pxd @@ -0,0 +1,9 @@ + +from kivy.properties cimport Property, PropertyStorage +from kivy._event cimport EventDispatcher, EventObservers + + + +cdef class SetProperty(Property): + pass + diff --git a/tagit/external/setproperty/setproperty.pyx b/tagit/external/setproperty/setproperty.pyx new file mode 100644 index 0000000..21bacbb --- /dev/null +++ b/tagit/external/setproperty/setproperty.pyx @@ -0,0 +1,125 @@ + +from weakref import ref + +cdef inline void observable_set_dispatch(object self) except *: + cdef Property prop = self.prop + obj = self.obj() + if obj is not None: + prop.dispatch(obj) + + +class ObservableSet(set): + # Internal class to observe changes inside a native python set. + def __init__(self, *largs): + self.prop = largs[0] + self.obj = ref(largs[1]) + super(ObservableSet, self).__init__(*largs[2:]) + + def __iand__(self, *largs): + set.__iand__(self, *largs) + observable_set_dispatch(self) + + def __ior__(self, *largs): + set.__ior__(self, *largs) + observable_set_dispatch(self) + + def __isub__(self, *largs): + set.__isub__(self, *largs) + observable_set_dispatch(self) + + def __ixor__(self, *largs): + set.__ixor__(self, *largs) + observable_set_dispatch(self) + + def add(self, *largs): + set.add(self, *largs) + observable_set_dispatch(self) + + def clear(self): + set.clear(self) + observable_set_dispatch(self) + + def difference_update(self, *largs): + set.difference_update(self, *largs) + observable_set_dispatch(self) + + def discard(self, *largs): + set.discard(self, *largs) + observable_set_dispatch(self) + + def intersection_update(self, *largs): + set.intersection_update(self, *largs) + observable_set_dispatch(self) + + def pop(self, *largs): + cdef object result = set.pop(self, *largs) + observable_set_dispatch(self) + return result + + def remove(self, *largs): + set.remove(self, *largs) + observable_set_dispatch(self) + + def symmetric_difference_update(self, *largs): + set.symmetric_difference_update(self, *largs) + observable_set_dispatch(self) + + def update(self, *largs): + set.update(self, *largs) + observable_set_dispatch(self) + + +cdef class SetProperty(Property): + '''Property that represents a set. + + :Parameters: + `defaultvalue`: set, defaults to set() + Specifies the default value of the property. + + .. warning:: + + When assigning a set to a :class:`SetProperty`, the set stored in + the property is a shallow copy of the set and not the original set. This can + be demonstrated with the following example:: + + >>> class MyWidget(Widget): + >>> my_set = SetProperty([]) + + >>> widget = MyWidget() + >>> my_set = {1, 5, {'hi': 'hello'}} + >>> widget.my_set = my_set + >>> print(my_set is widget.my_set) + False + >>> my_set.add(10) + >>> print(my_set, widget.my_set) + {1, 5, {'hi': 'hello'}, 10} {1, 5, {'hi': 'hello'}} + + However, changes to nested levels will affect the property as well, + since the property uses a shallow copy of my_set. + + ''' + def __init__(self, defaultvalue=0, **kw): + defaultvalue = set() if defaultvalue == 0 else defaultvalue + + super(SetProperty, self).__init__(defaultvalue, **kw) + + cpdef PropertyStorage link(self, EventDispatcher obj, str name): + Property.link(self, obj, name) + cdef PropertyStorage ps = obj.__storage[self._name] + if ps.value is not None: + ps.value = ObservableSet(self, obj, ps.value) + + cdef check(self, EventDispatcher obj, value, PropertyStorage property_storage): + if Property.check(self, obj, value, property_storage): + return True + if type(value) is not ObservableSet: + raise ValueError('%s.%s accept only ObservableSet' % ( + obj.__class__.__name__, + self.name)) + + cpdef set(self, EventDispatcher obj, value): + if value is not None: + value = ObservableSet(self, obj, value) + Property.set(self, obj, value) + + diff --git a/tagit/external/setproperty/setup.py b/tagit/external/setproperty/setup.py new file mode 100644 index 0000000..bd95d70 --- /dev/null +++ b/tagit/external/setproperty/setup.py @@ -0,0 +1,7 @@ +from distutils.core import Extension, setup +from Cython.Build import cythonize + +# define an extension that will be cythonized and compiled +ext = Extension(name="setproperty", sources=["setproperty.pyx"]) +setup(ext_modules=cythonize(ext)) + diff --git a/tagit/external/setproperty/test.py b/tagit/external/setproperty/test.py new file mode 100644 index 0000000..e241786 --- /dev/null +++ b/tagit/external/setproperty/test.py @@ -0,0 +1,62 @@ +from kivy.app import App +from kivy.lang import Builder +from time import time +from kivy.uix.label import Label +from kivy.uix.boxlayout import BoxLayout +import kivy.properties as kp +from setproperty import SetProperty + +Builder.load_string(''' +<Foo>: + orientation: 'vertical' + text: '' + + BoxLayout: + orientation: 'horizontal' + + ToggleButton: + id: btn_add + group: 'action' + text: 'add' + state: 'down' + + ToggleButton: + group: 'action' + text: 'delete' + + TextInput + id: value + + Button: + on_press: root.update_dict(btn_add.state == 'down', value.text) + text: 'change set' + + Label: + id: dictout + text: root.text + +''') + + +class Foo(BoxLayout): + + text = kp.StringProperty() + my_set = SetProperty() + + def on_my_set(self, wx, my_set): + self.text = str(time()) + ' ' + str(my_set) + + def update_dict(self, add, value): + if add: + self.my_set.add(value) + else: + self.my_set.discard(value) + + +class TestApp(App): + def build(self): + return Foo() + +if __name__ == '__main__': + TestApp().run() + diff --git a/tagit/external/tooltip.kv b/tagit/external/tooltip.kv new file mode 100644 index 0000000..27c3ab7 --- /dev/null +++ b/tagit/external/tooltip.kv @@ -0,0 +1,12 @@ + +<Tooltip_Label>: + size_hint: None, None + size: self.texture_size[0]+5, self.texture_size[1]+5 + canvas.before: + Color: + rgb: 0.2, 0.2, 0.2 + Rectangle: + size: self.size + pos: self.pos + +## EOF ## diff --git a/tagit/external/tooltip.py b/tagit/external/tooltip.py new file mode 100644 index 0000000..2865206 --- /dev/null +++ b/tagit/external/tooltip.py @@ -0,0 +1,67 @@ +"""Tooltips. + +From: + http://stackoverflow.com/questions/34468909/how-to-make-tooltip-using-kivy + +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 +from kivy.uix.label import Label +from kivy.clock import Clock +# Cannot import kivy.core.window.Window here; Leads to a segfault. +# Doing it within the *Tooltip* class works just fine, though. + +# exports +__all__ = ('Tooltip', ) + + +## CODE ## + +Builder.load_file(os.path.join(os.path.dirname(__file__), 'tooltip.kv')) + +class Tooltip_Label(Label): + pass + +# FIXME: Tooltip makes the whole UI *way* too slow, hence it's body is disabled +class Tooltip(object): + def set_tooltip(self, text): + pass + +# if hasattr(self, '_tooltip_wx') and self._tooltip_wx is not None: +# self._tooltip_wx.text = text +# else: +# self._tooltip_wx = Tooltip_Label(text=text) +# from kivy.core.window import Window +# Window.bind(mouse_pos=self.on_mouse_pos) +# +# def on_mouse_pos(self, *args): +# if not self.get_root_window(): +# return +# +# pos_x, pos_y = pos = args[1] +# from kivy.core.window import Window +# pos_x = max(0, min(pos_x, Window.width - self._tooltip_wx.width)) +# pos_y = max(0, min(pos_y, Window.height - self._tooltip_wx.height)) +# self._tooltip_wx.pos = (pos_x, pos_y) +# +# Clock.unschedule(self.display_tooltip) # cancel scheduled event since I moved the cursor +# self.close_tooltip() # close if it's opened +# if self.collide_point(*pos): +# Clock.schedule_once(self.display_tooltip, 1) +# +# def close_tooltip(self, *args): +# from kivy.core.window import Window +# Window.remove_widget(self._tooltip_wx) +# +# def display_tooltip(self, *args): +# from kivy.core.window import Window +# Window.add_widget(self._tooltip_wx) + +## EOF ## diff --git a/tagit/logger/__init__.py b/tagit/logger/__init__.py new file mode 100644 index 0000000..1a2cf15 --- /dev/null +++ b/tagit/logger/__init__.py @@ -0,0 +1,15 @@ +""" + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# inner-module imports +from .logger import logger_config, TagitFormatter, Filter_Or, CallbackHandler +from .colors import Colors, ColorsTerminal, ColorsMarkup +from . import loader + +# exports +__all__ = ('logger_config', ) + +## EOF ## diff --git a/tagit/logger/colors.py b/tagit/logger/colors.py new file mode 100644 index 0000000..758807d --- /dev/null +++ b/tagit/logger/colors.py @@ -0,0 +1,129 @@ +""" + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# standard imports +import re + +# exports +__all__ = ('Colors', 'ColorsTerminal', 'ColorsMarkup') + + +## code ## + +class Colors(object): + """ + + Status: + * ok + * warn + * error + + Messages: + * title + * info + + """ + + ## status ## + + @classmethod + def ok(obj, text): + return obj.BOLD + obj.OK + text + obj.ENDC + obj.ENDB + + @classmethod + def warn(obj, text): + return obj.BOLD + obj.WARN + text + obj.ENDC + obj.ENDB + + @classmethod + def error(obj, text): + return obj.BOLD + obj.ERROR + text + obj.ENDC + obj.ENDB + + + ## ordinary text for logging ## + + @classmethod + def title(obj, text): + return obj.BOLD + obj.TITLE + text + obj.ENDC + obj.ENDB + + @classmethod + def info(obj, text): + return obj.INFO + text + obj.ENDC + + @classmethod + def debug(obj, text): + return obj.DEBUG + text + obj.ENDC + + + ## ordinary text formatting ## + + @classmethod + def highlight(obj, text): + return obj.BOLD + text + obj.ENDC + + + ## meta functions ## + + @classmethod + def uncolor(obj, text): + return re.sub(obj.UNCOL, '', text) + + @classmethod + def unescape(obj, text): + for v, k in obj.ESCAPE[::-1]: + text = text.replace(k, v) + return text + + @classmethod + def escape(obj, text): + for k, v in obj.ESCAPE: + text = text.replace(k, v) + return text + +class ColorsTerminal(Colors): + """Terminal colors.""" + OK = "\033[38;5;2m" # green + WARN = "\033[33m" # yellow + ERROR = "\033[31m" # red + TITLE = "" # white + INFO = "\033[38;5;5m" # magenta + DEBUG = "\033[1;36m" # light blue + BOLD = "\033[1m" # bold + ENDC = "\033[0m" # end color + ENDB = "\033[0m" # end bold + UNCOL = '\\033\[.*?m' + ESCAPE = [] + +class ColorsMarkup(Colors): + """Console colors. + """ + OK = "[color=#00FF00]" # green + WARN = "[color=#FFFF00]" # yellow + ERROR = "[color=#FF0000]" # red + TITLE = "[color=#FF0000]" # red + INFO = "[color=#A52292]" # magenta + DEBUG = "[color=#4CCBE4]" # light blue + BOLD = "[b]" + ENDC = "[/color]" + ENDB = "[/b]" + UNCOL = '\[/?(color|b)(=.*?)?\]' + #UNCOL = '\[/?(color|b)(=#[\dA-Fa-f]+)?\]' # only permits hex color values + ESCAPE = [ + ('&', '&'), + ('[', '&bl;' ), + (']', '&br;' ), + ] + + # markup removal + # advanced: search for proper tags, i.e. start and ending pairs + # needs to be repeated until no more text is replaced + # FIXME: doesn't work for "[size=<size>]..[/size] style tags + #rx3 = re.compile('\[([^\]]+)\](.*?)\[/\\1\]') # replace with '\\2' + # alt (to be checked) for "[size=<size>]..[/size] style tags + #rx3 = re.compile('\[([^\]]+)(=[^\]+)?\](.*?)\[/\\1\]') # replace with '\\3' + # simple: remove everything that looks like a tag + #rx3 = re.compile('\[[^\]]+\]') # replace with '' + +## EOF ## diff --git a/tagit/logger/loader.py b/tagit/logger/loader.py new file mode 100644 index 0000000..cbf555e --- /dev/null +++ b/tagit/logger/loader.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 logging + +# tagit imports +from tagit import config + +# exports +__all__ = ('load_logging', ) + + +## code ## + +def load_logging(cfg): + """Configure the main terminal logger.""" + if ('logging', ) in cfg: + from tagit import loghandler + from tagit.shared import ColorsTerminal + from .logger import logger_config + logger_config(loghandler, ColorsTerminal, cfg('logging').to_tree(defaults=True)) + + if not cfg('session', 'debug'): + # set to info, prevents any DEBUG message to pass through the system. + # the handler config cannot overwrite this. + logging.getLogger('tagit').setLevel(logging.INFO) + + +## config ## + +config.declare_title(('logging', ), __name__, 'Logging', + 'Logging is used in several places (terminal, GUI). Wherever used, logging can be configured through a dictionary. Also see view.status, view.console for default values.') + +config.declare(('logging', 'level'), + config.Enum('debug', 'info', 'warn', 'error', 'critical'), 'debug', + __name__, 'Log level', 'Maximal log level for which messages are shown. The order is: critical > error > warning > info > volatile > debug') + +config.declare(('logging', 'filter'), config.List(config.String()), ['tagit'], + __name__, 'Module filter', 'Module name for which log messages are accepted.') + +config.declare(('logging', 'fmt'), config.String(), '[{levelname}] [{name}] {title}{message}', + __name__, 'Log format', 'Log message formatting. The formatting follows the typical python format string where each item is enclosed in curly braces (e.g. "{message}"). For printable items see the `logging attributes <https://docs.python.org/3.6/library/logging.html#logrecord-attributes>`_. In addition to those, the item "title" is available, which is a placeholder for the formatted title (if present).') + +config.declare(('logging', 'title'), config.String(), '[{title}] ', + __name__, 'Title format', 'Title formatting.') + +config.declare(('logging', 'maxlen'), config.Unsigned(), 80, + __name__, 'Maximal line length', 'Maximal line length (e.g. console width). Use Infinity to set no line length limit.') + +config.declare(('logging', 'prefix'), config.String(), '', + __name__, 'Log prefix', 'A prefix before every log line (internal use only)') + +## EOF ## diff --git a/tagit/logger/logger.py b/tagit/logger/logger.py new file mode 100644 index 0000000..f705888 --- /dev/null +++ b/tagit/logger/logger.py @@ -0,0 +1,190 @@ +"""tagit logging facility. + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# standard imports +import logging +import re + +# exports +__all__ = ( + 'CallbackHandler', + 'Filter_Or', + 'TagitFormatter', + 'logger_config', + ) + + +## code ## + +class CallbackHandler(logging.Handler): + """Adapter for logging.Handler that delegates the message to a callback function.""" + def __init__(self, clbk, *args, **kwargs): + self._clbk = clbk + super(CallbackHandler, self).__init__(*args, **kwargs) + + def emit(self, record): + self.acquire() + try: + self._clbk(self.format, record) + except Exception: + self.handleError(record) + finally: + self.release() + +class Filter_Or(list): + """Lets the record pass if any of the child filters accept it.""" + def filter(self, record): + for itm in iter(self): + if itm.filter(record): + return True + return False + +class TagitFormatter(logging.Formatter): + """Default Formatter for tagit. + + This formatter implements the following features: + * Title awareness + * Colors + * Message truncating + + A message of the form 'Title: Message' will be split into the title and message + part. The title formatting is specified in *fmt_title* and only included if a title + is present. This format string expects a title argument. E.g. fmt_title='[{title}] ' + would produce a title '[Title] ' that would then be included in the formatted record. + + The overal format string (*fmt*) can also include a 'title' argument that specifies + how the title should be positioned. The curly-brace style is assumed. + + If a *maxlen* argument is given, the record's message will be truncated such that the + overall length of the log line does not exceed that limit. + + """ + def __init__(self, *args, fmt_title='{title}: ', colors=None, prefix='', + truncate='front', maxlen=float('inf'), **kwargs): + kwargs['style'] = '{' + super(TagitFormatter, self).__init__(*args, **kwargs) + self.fmt_title = fmt_title + self.cmap = colors + self.maxlen = maxlen + self.prefix = prefix + self.truncate = self.truncate_back if truncate == 'back' else self.truncate_front + + def color_levelname(self, levelname): + if self.cmap is None: + return levelname + elif levelname in ('ERROR', 'CRITICAL'): + return self.cmap.error(levelname) + elif levelname in ('WARNING', ): + return self.cmap.warn(levelname) + elif levelname in ('INFO', ): + return self.cmap.info(levelname) + elif levelname in ('DEBUG', ): + return self.cmap.debug(levelname) + else: + return levelname + + def color_title(self, title): + if self.cmap is None: return title + return self.cmap.title(title) + + def truncate_back(self, msg, maxlen=None): + """Truncate a string.""" + maxlen = maxlen if maxlen is not None else self.maxlen + if len(msg) > maxlen: + return msg[:int(maxlen-3)] + '...' + return msg + + def truncate_front(self, msg, maxlen=None): + """Truncate a string.""" + maxlen = maxlen if maxlen is not None else self.maxlen + if len(msg) > maxlen: + return '...' + msg[-int(maxlen-3):] + return msg + + def format_title(self, record): + # tagit title format "title: content" + rx1 = re.compile('^([^:\s]+(?:\s+[^:\s]+){0,1})\s*:(.*)$') + # kivy title format "[title ] content" + rx2 = re.compile('^\[(.{12})\](.*)$') + + msg = str(record.msg) % record.args + m1 = rx1.search(msg) + m2 = rx2.search(msg) + m = m1 if m1 is not None else m2 + if m is not None: # title + title, msg = m.groups() + return self.fmt_title.format(title=self.color_title(title.strip())), msg.strip() + else: # no title + return '', msg.strip() + + def format(self, record): + # reset level name since it might have been modified + record.levelname = logging.getLevelName(record.levelno) + # title + title, msg = self.format_title(record) + # level name + levelname = self.color_levelname(record.levelname) + + # escape message for coloring + if self.cmap is not None: + msg = self.cmap.escape(msg) + # adjust record + omsg, record.msg = record.msg, msg + olvl, record.levelname = record.levelname, levelname + record.title = title + # compile log line + logline = logging.Formatter(fmt=self._fmt, style='{').format(record) + + # get effective lengths of individual parts + + # truncate message to fixed length + if 0 < self.maxlen and self.maxlen < float('inf'): + if self.cmap is None: + record.msg = self.truncate(record.msg, + self.maxlen - len(logline) + len(record.msg) - 1) + else: + tlen = len(self.cmap.unescape(self.cmap.uncolor(logline))) + mlen = len(self.cmap.unescape(self.cmap.uncolor(record.msg))) + record.msg = self.cmap.escape(self.truncate(self.cmap.unescape(record.msg), + self.maxlen - tlen + mlen - 1)) + logline = logging.Formatter(fmt=self._fmt, style='{').format(record) + + # reset record + record.msg, record.levelname = omsg, olvl + + return self.prefix + logline + + +## logger configuration ## + +def logger_config(handler, colors, config): + """Configure a handler from a user-specified config. Returns the handler. + + The config is a dict with the following keys: + * level : Log level (level name) + * filter : Accept all specified modules (list of module names) + * fmt : Main format string + * title : Format string for the title + * maxlen : Maximum log entry line. No limit if unspecified. + * prefix : Log line prefix + """ + if 'level' in config: + handler.setLevel(config['level'].upper()) + + if 'filter' in config: + handler.addFilter(Filter_Or(map(logging.Filter, config['filter']))) + + handler.setFormatter(TagitFormatter( + fmt = colors.escape(config.get('fmt', '{title}{message}')), + fmt_title = colors.escape(config.get('title', '{title}: ')), + maxlen = float(config.get('maxlen', 0)), + colors = colors, + prefix = config.get('prefix', ''), + )) + + return handler + +## EOF ## diff --git a/tagit/parsing/__init__.py b/tagit/parsing/__init__.py new file mode 100644 index 0000000..86ad54f --- /dev/null +++ b/tagit/parsing/__init__.py @@ -0,0 +1,19 @@ +""" + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# inner-module imports +from .datefmt import parse_datetime +from . import filter +from .sort import Sort + +# exports +__all__ = ( + 'filter', + 'Sort', + 'parse_datetime', + ) + +## EOF ## diff --git a/tagit/parsing/datefmt.py b/tagit/parsing/datefmt.py new file mode 100644 index 0000000..49de1c0 --- /dev/null +++ b/tagit/parsing/datefmt.py @@ -0,0 +1,568 @@ +"""Parse and interpret date strings. + +Consider the following date notations (DMY=04.11.2012): + +DMY 04.11.12 europe +YMD 12.11.04 iso +MDY 11.04.12 US +YDM 12.04.11 reverse US +DYM 04.12.11 too uncommon, ignored +MYD 11.12.04 too uncommon, ignored + +There's the general problem of ambiguity between the DMY and MDY formats. +Here, we give precedence to the DMY format. + +Note that the MDY format can still be used in unambiguous settings or +with the month spelled out, e.g. "2012, 23th of November" + +Similarly, consider the following shortened date notations: + +DM 04.11 europe, current year +MY 11.12 quarters +YM 12.11 quarters +MD 11.04 us, current year +DY 23.12 too uncommon, ignored +YD 12.23 too uncommon, ignored + +In addition to the different spellings, month names can be spelled out +and the string can be cluttered with additional common words. + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# standard imports +from collections import Counter +from datetime import date as ddate, time as dtime, datetime, timedelta +from math import floor + +# external imports +from dateutil.relativedelta import relativedelta +from pyparsing import Combine, Group, Literal, Optional, Or, Word, nums, oneOf, ParseException + +# tagit imports +from tagit.utils import errors, Struct, flatten + +# exports +__all__ = ( + # default format strings + 'DATE_FMT', + 'TIME_FMT', + 'DATETIME_FMT', + # exceptions + 'DateParserError', + 'TimeParserError', + 'DateFormatError' + # parsing + 'parse_datetime', + 'guess_datetime', + # postprocessing + 'increment', + ) + +## constants ## +"""Default strftime format strings.""" +DATE_FMT = '%d.%m.%Y' +TIME_FMT = '%H:%M' +DATETIME_FMT = '%d.%m.%Y, %H:%M' + +# Literal months +MONTH_LIT = { + 'Jan' : 1, + 'January' : 1, + 'Feb' : 2, + 'February' : 2, + 'Mar' : 3, + 'March' : 3, + 'Apr' : 4, + 'April' : 4, + 'May' : 5, + 'Jun' : 6, + 'June' : 6, + 'Jul' : 7, + 'July' : 7, + 'Aug' : 8, + 'August' : 8, + 'Sep' : 9, + 'September' : 9, + 'Oct' : 10, + 'October' : 10, + 'Nov' : 11, + 'November' : 11, + 'Dec' : 12, + 'December' : 12, + } + + +## code ## + +class DatefmtError(errors.ParserError): pass + +class DateParserError(DatefmtError): pass + +class TimeParserError(DatefmtError): pass + +class DateFormatError(DatefmtError): pass + +class DF(str): + """date/time user-supplied format.""" + # indicator characters, highest to lowest. + _valid_chars = "YMDhmsn" + # explicit mapping from unit to character + year = 'Y' + month = 'M' + day = 'D' + hour = 'h' + minute = 'm' + second = 's' + microsecond = 'n' + + def valid(self): + return len(self) and len(set(self._valid_chars) & set(self)) + + def lsb(self): + """Smallest unit specified.""" + if not self.valid(): + raise DateFormatError( + 'An empty date format string has no least significant position.', self) + + return [i for i in self._valid_chars if i in self][-1] + + def msb(self): + """Highest unit specified.""" + if not self.valid(): + raise DateFormatError( + 'An empty date format string has no most significant position.', self) + + return [i for i in self._valid_chars if i in self][0] + + def is_time(self): + """Return true if only a time (hour/minute/second/ms) was specified.""" + return True if self.valid() and self.msb() not in 'YMD' else False + + def is_date(self): + """Return true if only a date (year/month/day) was specified.""" + return True if self.valid() and self.lsb() not in 'hmsn' else False + +# priorities +PRIORITIES_INT = { + 'p2': [ + DF(DF.day + DF.month), # DM + DF(DF.month + DF.year), # MY + DF(DF.year + DF.month), # YM + DF(DF.month + DF.day), # MD + DF(DF.day + DF.year), # DY + DF(DF.year + DF.day), # YD + ], + 'p3': [ + DF(DF.day + DF.month + DF.year), # DMY + DF(DF.year + DF.month + DF.day), # YMD + DF(DF.month + DF.day + DF.year), # MDY + DF(DF.year + DF.day + DF.month), # YDM + DF(DF.day + DF.year + DF.month), # DYM + DF(DF.month + DF.year + DF.day), # MYD + ] + } + +PRIORITIES_US = { + 'p2': [ + DF(DF.month + DF.day), + DF(DF.year + DF.month), + DF(DF.day + DF.month), + DF(DF.month + DF.year), + DF(DF.day + DF.year), + DF(DF.year + DF.day), + ], + 'p3': [ + DF(DF.month + DF.day + DF.year), + DF(DF.year + DF.day + DF.month), + DF(DF.day + DF.month + DF.year), + DF(DF.year + DF.month + DF.day), + DF(DF.day + DF.year + DF.month), + DF(DF.month + DF.year + DF.day), + ] + } + +def guess_date(tokens, priorities=None): + """Guess the date from a string in an unknown format. + + The method uses the following clues to guess the meaning of each part: + * 4-digits implies it's a year + * 1-digit discards year (since it's more common to write 04 instead of 4 as a shorthand to 2004 + * Literal month + * 'of' is preceeded by day and succeeded by the month + * Any of (st, nd, rd, th) on a number makes it a day + * Number > 12 can't be a month + * Number > 31 can't be a day + * Date inexistence (e.g. 29.02.2018) + * precedence DMY > YMD > MDY > YDM + * precedence DM > MY > YM > MD + """ + priorities = PRIORITIES_INT if priorities is None else priorities + + # We need to figure out which token corresponds to which component + # (D, M, Y). Since this is ambiguous, guesswork is needed. We do so + # by eliminating impossible options. + + # initially, all three components are viable + guesses = [Struct(tok=tok.strip(), fmt=DF.year + DF.month + DF.day) for tok in tokens] + + # check indicators for specific formats + for idx in range(len(guesses)): + tok, options = guesses[idx].tok, guesses[idx].fmt + + if len(tok) == 1 and tok in '.,;': + # delimiter tokens can be ignored + guesses[idx].fmt = '' + elif tok == 'of': + # an 'of' token indicates a 'day of month' structure + guesses[idx-1].fmt = DF.day + guesses[idx+1].fmt = DF.month + guesses[idx].fmt = '' + elif tok[-2:] in ('st', 'nd', 'rd', 'th'): + # suffix indicates a day + guesses[idx].fmt = DF.day + guesses[idx].tok = tok[:-2] + elif len(tok) == 4 and tok.isdigit(): + # four digits must be a year + guesses[idx].fmt = DF.year + elif tok in MONTH_LIT: + # spelled out month is - tadaaa - a month + guesses[idx].tok = str(MONTH_LIT[tok]) + guesses[idx].fmt = DF.month + + # remove jitter (of, delimiters) + guesses = [itm for itm in guesses if len(itm.fmt) > 0] + + # eliminate impossible options + for idx in range(len(guesses)): + tok, options = guesses[idx].tok, guesses[idx].fmt + + if len(tok) == 1: + # can't be a year + guesses[idx].fmt = guesses[idx].fmt.replace(DF.year, '') + if tok.isdigit() and int(tok) > 12: + # can't be a month + guesses[idx].fmt = guesses[idx].fmt.replace(DF.month, '') + if tok.isdigit() and int(tok) > 31: + # can't be a day + guesses[idx].fmt = guesses[idx].fmt.replace(DF.day, '') + + # define helper function + def create_date(year, month, day): + """Return a datetime for the given components or None if that is not possible.""" + # check format + if DF.year not in year.fmt or DF.month not in month.fmt or DF.day not in day.fmt: + return None + + if len(str(year.tok)) == 2: + # ten years into the future is still the current century, otherwise the previous one + threshold = ddate.today().year + 10 - 2000 + year = Struct( + tok='20'+str(year.tok) if int(year.tok) < threshold else '19'+str(year.tok), + fmt=year.fmt + ) + + try: + # create date + return ddate(year=int(year.tok), month=int(month.tok), day=int(day.tok)) + except ValueError: + return None + + # placeholders for unspecified tokens + pyear = Struct(tok=ddate.today().year, fmt=DF.year) + pday = Struct(tok=1, fmt=DF.day) + pmon = Struct(tok=1, fmt=DF.month) + + if len(guesses) == 1: # one-part date (Y) + itm = guesses[0] + date = create_date(itm, pmon, pday) + if date is not None: + return date, DF(DF.year) + else: + raise DateParserError('Two-digit date format must contain the year') + + elif len(guesses) == 2: # two-part date (DM, MY, YM, MD) + fst, snd = guesses + # check components + if len(set(fst.fmt + snd.fmt)) < 2: + raise DateParserError('Invalid two-digit date format') + + if len(fst.fmt) == 1 and len(snd.fmt) == 1: # fully determined + date = { + DF.year: pyear, + DF.month: pmon, + DF.day: pday, + } + date.update({ + fst.fmt: fst, + snd.fmt: snd, + }) + return create_date(date[DF.year], date[DF.month], date[DF.day]), DF(fst.fmt + snd.fmt) + + # walk through prioritized formats + formats = { + DF(DF.day + DF.month): create_date(pyear, snd, fst), # DM + DF(DF.month + DF.year): create_date(snd, fst, pday), # MY + DF(DF.year + DF.month): create_date(fst, snd, pday), # YM + DF(DF.month + DF.day): create_date(pyear, fst, snd), # MD + DF(DF.day + DF.year): create_date(snd, pmon, fst), # DY + DF(DF.year + DF.day): create_date(fst, pmon, snd), # YD + } + + for fmt in priorities['p2']: + if formats.get(fmt, None) is not None: + return formats[fmt], fmt + + raise DateParserError('Cannot guess roles of a two-digit date format') + + elif len(guesses) == 3: # three-part date (DMY, YMD, MDY, YMD) + + # eliminate options based on uniqueness of component assignment + changed = True + while changed: + # resolved guesses: item has only one possible component option + resolved = set([itm.fmt for itm in guesses if len(itm.fmt) == 1]) + # single choice: component has only one possible position + unique = {comp for comp, freq in + Counter(flatten([set(itm.fmt) for itm in guesses])).items() + if freq == 1} + # assume no changes + changed = False + for itm in guesses: + if unique & set(itm.fmt) and not set(itm.fmt).issubset(unique): + # itm is the only option for one component + itm.fmt = DF(''.join(unique & set(itm.fmt))) + changed = True + elif resolved & set(itm.fmt) and not set(itm.fmt).issubset(resolved): + # itm contains options that already taken by a different item + itm.fmt = itm.fmt.translate(str.maketrans('', '', ''.join(resolved))) + changed = True + + fst, snd, trd = guesses + + # check components + if len(set(fst.fmt + snd.fmt + trd.fmt)) < 3: + raise DateParserError('Invalid three-digit date format') + + if len(fst.fmt) == 1 and len(snd.fmt) == 1 and len(trd.fmt) == 1: # fully determined + date = { + fst.fmt: fst, + snd.fmt: snd, + trd.fmt: trd, + } + return (create_date(date[DF.year], date[DF.month], date[DF.day]), + DF(fst.fmt + snd.fmt + trd.fmt)) + + # walk through prioritized formats + formats = { + DF(DF.day + DF.month + DF.year): create_date(year=trd, month=snd, day=fst), # DMY + DF(DF.year + DF.month + DF.day): create_date(year=fst, month=snd, day=trd), # YMD + DF(DF.month + DF.day + DF.year): create_date(year=trd, month=fst, day=snd), # MDY + DF(DF.year + DF.day + DF.month): create_date(year=fst, month=trd, day=snd), # YDM + DF(DF.day + DF.year + DF.month): create_date(year=snd, month=trd, day=fst), # DYM + DF(DF.month + DF.year + DF.day): create_date(year=snd, month=fst, day=trd), # MYD + } + + for fmt in priorities['p3']: + if formats.get(fmt, None) is not None: + return formats[fmt], fmt + + raise DateParserError('Cannot guess the roles of a three-digit date format') + + raise DateParserError('Cannot parse the date format') + +def guess_time(tokens): + """Guess the time from a string in an unknown format. + + * Always sorted from hi (hour) to low (sec) + * 4 Terms -> hour, min, sec, ns + * 3 Terms -> hour, min, sec + * 2 Terms -> hour, min | min, sec + * both terms > 24 -> min, sec + * am or pm present -> hour, min + * Dot separation -> min, sec + * Colon separation -> hour, min + """ + # remove spearators + tokens = [tok.lower() for tok in tokens if tok not in '.,:'] + # check if the am/pm format was used + is_am = 'am' in tokens + is_pm = 'pm' in tokens + + # remove non-numbers + tokens = [tok for tok in tokens if tok.isdigit()] + if not len(tokens): + raise TimeParserError() + + # convert to int + ms = int(tokens[-1].ljust(6, '0')) + tokens = [int(tok) for tok in tokens] + + # guess format + try: + if len(tokens) == 4: # H:M:S.NS + tokens[-1] = ms + return dtime(*tokens), DF(DF.hour + DF.minute + DF.second + DF.microsecond) + elif len(tokens) == 3: # H:M:S + return dtime(*tokens), DF(DF.hour + DF.minute + DF.second) + elif len(tokens) == 2: # H:M or M:S + if is_am: # am/pm notation was used + return dtime(*tokens), DF(DF.hour + DF.minute) + elif is_pm: # am/pm notation was used + return dtime(tokens[0] + 12, tokens[1]), DF(DF.hour + DF.minute) + elif tokens[0] > 24: # min, sec + return dtime(0, *tokens), DF(DF.minute + DF.second) + else: # hour, sec + return dtime(*tokens), DF(DF.hour + DF.minute) + elif len(tokens) == 1: # H + if is_am: # am/pm notation was used + return dtime(tokens[0]), DF(DF.hour) + elif is_pm: # am/pm notation was used + return dtime(tokens[0] + 12), DF(DF.hour) + else: + return dtime(tokens[0], 0), DF(DF.hour) + + except ValueError: + # invalid value was supplied, e.g. hour=85 + raise TimeParserError('Invalid value', tokens) + + raise TimeParserError('Unknown time format', tokens) + +def guess_datetime(exp): + """Return a datetime instance by guessing the components of a DATETIME parsed + user-supplied date and/or time string. Guessing might be necessary since dates + like 10.11.12 are ambiguous. *exp* is supposed to be a pyparsing.ParseResults + instance as returned by DATETIME.parseString(...). + """ + # For now I assumed unique separators (dot for date, colon for time, comma to separate the two) + if 'date' in exp and 'time' in exp: # both parts present + date, dfmt = guess_date(exp.date) + time, tfmt = guess_time(exp.time) + return datetime.combine(date, time), DF(dfmt+tfmt) + elif 'date' in exp: # date present + date, dfmt = guess_date(exp.date) + return datetime(date.year, date.month, date.day), dfmt + elif 'time' in exp: # time present + time, tfmt = guess_time(exp.time) + return datetime.combine(ddate.fromtimestamp(0), time), tfmt + else: + raise DateFormatError('Neither a date nor a time was found.') + +def increment(date, fmt): + """Increment the LSB of a datetime instance by one.""" + if fmt == '' or not fmt.valid(): + raise DateFormatError('Invalid date format string', fmt) + elif fmt.lsb() == fmt.microsecond: # 5.11.2012, 06:24:18.25 -> 5.11.2012, 06:25:18.26 + return date + relativedelta(microseconds=1) + elif fmt.lsb() == fmt.second: # 5.11.2012, 06:24:18 -> 5.11.2012, 06:25:19 + return date + relativedelta(seconds=1, microsecond=0) + elif fmt.lsb() == fmt.minute: # 5.11.2012, 06:24 -> 5.11.2012, 06:25 + return date + relativedelta(minutes=1, second=0, microsecond=0) + elif fmt.lsb() == fmt.hour: # 5.11.2012, 06am -> 5.11.2012, 07:00 + return date + relativedelta(hours=1, minute=0, second=0, microsecond=0) + elif fmt.lsb() == fmt.day: # 5.11.2012 -> 6.11.2012, 00:00 + return date + relativedelta(days=1, hour=0, minute=0, second=0, microsecond=0) + elif fmt.lsb() == fmt.month: # 11.2012 -> 1.12.2012 + return date + relativedelta(months=1, day=1, hour=0, minute=0, second=0, microsecond=0) + else: # fmt.lsb() == fmt.year: # 2012 -> 1.1.2013, 00:00 + return date + relativedelta( + years=1, month=1, day=1, hour=0, minute=0, second=0, microsecond=0) + +class DateTimeParser(): + + DATE = None + TIME = None + DATETIME = None + + def build_parser(self): + """ + DATE := YMD | DMY | MDY | YDM + YMD := YEAR SEP MON SEP DAY + DMY := DAY SEP [of] MON SEP YEAR + MDY := MON SEP DAY SEP YEAR + YDM := YEAR SEP DAY [of] MON + DM := DAY SEP [of] MON + YM := YEAR SEP MON + MY := MON SEP YEAR + MD := MON SEP DAY + DAY := [D]D | [D]D st | [D]D nd | [D]D rd | [D]D th + MON := [M]M | [month] + YEAR := [YY]YY + SEP := . | , | [whitespace] + {D,M,Y} := [digit] + """ + # FIXME: Allow more patterns (e.g. 2012, 10; April, 5th; April, 2020) + sep = Literal('.') # FIXME: Allow '. - :' + year = Word(nums, exact=2) ^ Word(nums, exact=4) + month = Word(nums, min=1, max=2) ^ oneOf(list(MONTH_LIT.keys())) + day = Combine(Word(nums, min=1, max=2) + Optional(oneOf('st nd rd th'))) + # three-part-date + YMD = year + sep + month + sep + day + DMY = day + (sep ^ 'of') + month + sep + year + MDY = month + sep + day + sep + year + YDM = year + sep + day + (sep ^ 'of') + month + # two-part-date + DM = day + (sep ^ 'of')+ month + YM = year + sep + month + MY = month + sep + year + MD = month + sep + day + Y = Word(nums, exact=4) + # date parser + self.DATE = Group(YMD | DMY | YDM | MDY | DM | YM | MY | MD | Y).setResultsName('date') + + """ + TIME := HOUR SEP MIN [SEP SEC [. MS]] | HOUR SEP MIN | HOUR [SEP MIN] {am|pm} + HOUR := [H]H + MIN := [M]M + SEC := [S]S + {H,M,S} := [digit] + SEP := : | . | , + """ + sep = Literal(':') # FIXME: Allow '. : -' + HMS = Word(nums, min=1, max=2) + MS = Word(nums, min=1) + # time parser + self.TIME = Group(HMS + sep + HMS + sep + HMS + oneOf('. :') + MS \ + | HMS + sep + HMS + sep + HMS \ + | HMS + Optional(sep + HMS) + oneOf('am pm') \ + | HMS + sep + HMS ).setResultsName('time') + + """ + DATETIME := DATE | TIME | DATE SEP TIME | TIME SEP DATE + SEP := , [whitespace] + """ + self.DATETIME = Group( + self.DATE \ + ^ self.TIME \ + ^ self.DATE + Optional(',') + self.TIME \ + ^ self.TIME + Optional(',') + self.DATE \ + ).setResultsName('datetime') + return self + + def __call__(self, datestr): + if self.DATETIME is None: + self.build_parser() + + try: + date, fmt = guess_datetime(self.DATETIME.parseString(datestr, parseAll=True)[0]) + return date + + except ParseException as e: + raise errors.ParserError('Cannot parse query', e) + + +"""Default DateTimeParser instance. + +To produce an datetime, call + +>>> parse_datetime(datestring) + +Convenience shortcut for + +>>> DateTimeParser().parse(datestring) + +""" +parse_datetime = DateTimeParser().build_parser() + +## EOF ## diff --git a/tagit/parsing/filter/__init__.py b/tagit/parsing/filter/__init__.py new file mode 100644 index 0000000..defb332 --- /dev/null +++ b/tagit/parsing/filter/__init__.py @@ -0,0 +1,17 @@ +""" + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# inner-module imports +from .from_string import FromString +from .to_string import ToString + +# exports +__all__ = ( + 'FromString', + 'ToString', + ) + +## EOF ## diff --git a/tagit/parsing/filter/from_string.py b/tagit/parsing/filter/from_string.py new file mode 100644 index 0000000..60e5c47 --- /dev/null +++ b/tagit/parsing/filter/from_string.py @@ -0,0 +1,382 @@ +"""User-specified search query parsing. + +>>> q = "has mime / tag in (november, october) / ! Apfel / time < 10.10.2004 / iso in (100, 200)" +>>> ast = filter_from_string(q) + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# standard imports +from datetime import datetime + +# external imports +from pyparsing import CaselessKeyword, Combine, Group, Optional, Or, Word, delimitedList, nums, oneOf, ParseException, Literal, QuotedString, alphanums, alphas8bit, punc8bit + +# tagit imports +from tagit.parsing.datefmt import parse_datetime +from tagit.utils import bsfs, errors, ns, ttime +from tagit.utils.bsfs import ast, URI + +# constants +SEARCH_DELIM = '/' +VALUE_DELIM = ',' + +# exports +__all__ = ( + 'FromString', + ) + + +## code ## + +class FromString(): + + # parsers + _DATETIME_PREDICATES = None + _QUERY = None + + # current schema. + schema: bsfs.schema.Schema + + def __init__(self, schema: bsfs.schema.Schema): + self.schema = schema + + @property + def schema(self) -> bsfs.schema.Schema: + return self._schema + + @schema.setter + def schema(self, schema: bsfs.schema.Schema): + self._schema = schema + self.build_parser() + + def build_parser(self): + """ + """ + # FIXME: + # * range / type constraints + # * how to filter predicates + # * distinguish between prefix and label + """ + Categorical: string, float, integer; labelled node (tag, group); maybe boolean + Continuous: float, integer + Datetime: datetime + Existencial: all of the above, particularly bool; unllabelled node (preview) + -> rfds:range + + > Target: Entity (allow others?) -> rfds:domain + > Require: searchable as specified in backend AND user-searchable as specified in frontend + """ + # all relevant predicates + predicates = {pred for pred in self.schema.predicates() if pred.domain <= self.schema.node(ns.bsn.Entity)} + # filter through accept/reject lists + ... # FIXME + # shortcuts + self._abb2uri = {pred.uri.fragment: pred.uri for pred in predicates} # FIXME: tie-breaking for duplicates + self._uri2abb = {uri: fragment for fragment, uri in self._abb2uri.items()} + # all predicates + _PREDICATES = {self._uri2abb[pred.uri] for pred in predicates} | {'id', 'group'} # FIXME: properly document additions + # numeric predicates + _PREDICATES_NUMERIC = {self._uri2abb[pred.uri] for pred in predicates if pred.range <= self.schema.literal(ns.bsl.Number)} + # datetime predicates + self._DATETIME_PREDICATES = {pred.uri for pred in predicates if pred.range <= self.schema.literal(ns.bsl.Time)} + _PREDICATES_DATETIME = {self._uri2abb[pred] for pred in self._DATETIME_PREDICATES} + + + # terminal symbols + number = Group(Optional(oneOf('- +')) \ + + Combine(Word(nums) + Optional('.' + Optional(Word(nums))))) + words = QuotedString(quoteChar='"', escChar='\\') \ + ^ QuotedString(quoteChar="'", escChar='\\') \ + ^ Word(alphanums + alphas8bit + punc8bit + ' *#%&-.:;<=>?@^_`{}~') + # FIXME: allow escaped chars "( ) / , [ ]" + # FIXME: Non-ascii characters + + # predicates + predicate = Or([CaselessKeyword(p) for p in _PREDICATES]).setResultsName( + 'predicate') + date_predicate = Or([CaselessKeyword(p) for p in _PREDICATES_DATETIME]).setResultsName( + 'predicate') + num_predicate = Or([CaselessKeyword(p) for p in _PREDICATES_NUMERIC]).setResultsName( + 'predicate') + + # existence + """ + EXPR := has PREDICATE | has no PREDICATE + PREDICATE := [predicate] + """ + op = (CaselessKeyword('has') ^ CaselessKeyword('has no') ^ CaselessKeyword('has not')).setResultsName('op') + _EXISTENCE = Group(op + predicate).setResultsName('existence') + + + # continuous + """ + EXPR := PREDICATE CMP VALUE | VALUE CMP PREDICATE CMP VALUE | PREDICATE OP RANGE + PREDICATE := [predicate] + CMP := < | <= | = | >= | > + OP := : | = | in | not in + RANGE := BOPEN VALUE RSEP VALUE BCLOSE | BOPEN RSEP VALUE BLOSE | BOPEN VALUE RSEP BCLOSE + BOPEN := ( | [ | ] + BCLOSE := ) | ] | [ + RSEP := : | - + VALUE := [digit] | [date] + """ + # range style + rsepn = oneOf(': -') + rsepd = Literal('-') + bclose = oneOf(') ] [').setResultsName('bclose') + bopen = oneOf('( [ ]').setResultsName('bopen') + op = Or([':', '=', 'in']).setResultsName('op') + datefmt = parse_datetime.DATETIME + rngn = num_predicate + op + bopen + number('lo') + rsepn + number('hi') + bclose ^ \ + num_predicate + op + bopen + rsepn + number('hi') + bclose ^ \ + num_predicate + op + bopen + number('lo') + rsepn + bclose + rngd = date_predicate + op + bopen + datefmt('lo') + rsepd + datefmt('hi') + bclose ^ \ + date_predicate + op + bopen + rsepd + datefmt('hi') + bclose ^ \ + date_predicate + op + bopen + datefmt('lo') + rsepd + bclose + # equation style + cmp = oneOf('< <= = >= >').setResultsName('cmp') + eqn = num_predicate + cmp('cright') + number('vright') ^ \ + number('vleft') + cmp('cleft') + num_predicate ^ \ + number('vleft') + cmp('cleft') + num_predicate + cmp('cright') + number('vright') + eqd = date_predicate + cmp('cright') + datefmt('vright') ^ \ + datefmt('vleft') + cmp('cleft') + date_predicate ^ \ + datefmt('vleft') + cmp('cleft') + date_predicate + cmp('cright') + datefmt('vright') + # combined + _CONTINUOUS = Group( + Group(eqn).setResultsName('eq') ^ + Group(eqd).setResultsName('eq') ^ + Group(rngn).setResultsName('range') ^ \ + Group(rngd).setResultsName('range') \ + ).setResultsName('continuous') + + + # categorical + """ + EXPR := PREDICATE OP VALUE | PREDICATE OP (VALUE) + PREDICATE := [predicate] + OP := : | = | in | not in | != | ~ | !~ + VALUE := TERM | VALUE, + TERM := [word] + """ + op = (CaselessKeyword('in') ^ CaselessKeyword('not in') ^ ':' ^ '=' ^ '!=' ^ '~' ^ '!~').setResultsName('op') + value = delimitedList(words, delim=VALUE_DELIM).setResultsName('value') + _CATEGORICAL = Group(predicate + op + ('(' + value + ')' | value) ).setResultsName('categorical') + + + # tag shortcuts + """ + EXPR := OP VALUE | OP (VALUE) | VALUE | (VALUE) + OP := ! | ~ | !~ + VALUE := TERM | VALUE, + TERM := [word] + """ + op = oneOf('! ~ !~').setResultsName('op') + value = delimitedList(words, delim=VALUE_DELIM).setResultsName('value') + _TAG = Group(Optional(op) + '(' + value + ')' ^ Optional(op) + value).setResultsName('tag') + + + # overall query + """ + QUERY := QUERY / QUERY | EXPR + """ + self._QUERY = delimitedList(_EXISTENCE | _CONTINUOUS | _CATEGORICAL | _TAG, delim=SEARCH_DELIM) + return self + + def __call__(self, search): + try: + parsed = self._QUERY.parseString(search, parseAll=True) + except ParseException as e: + raise errors.ParserError('Cannot parse query', e) + + # convert to AST + tokens = [] + for exp in parsed: + if exp.getName() == 'existence': + pred = self._abb2uri[exp.predicate.lower()] + if 'op' not in exp: # prevented by grammar + raise errors.ParserError('Missing operator', exp) + elif exp.op == 'has': + tok = ast.filter.Has(pred) + elif exp.op in ('has no', 'has not'): + tok = ast.filter.Not(ast.filter.Has(pred)) + else: # prevented by grammar + raise errors.ParserError('Invalid operator ({})'.format(exp.op), exp) + tokens.append(tok) + + elif exp.getName() == 'categorical' and exp.predicate.lower() == 'id': + values = [URI(s.strip()) for s in exp.value] + tokens.append(ast.filter.IsIn(*values)) + + elif exp.getName() == 'categorical' and exp.predicate.lower() == 'group': + values = [URI(s.strip()) for s in exp.value] + tokens.append(ast.filter.Any(ns.bse.group, ast.filter.IsIn(*values))) + + elif exp.getName() == 'categorical': + pred = self._abb2uri[exp.predicate.lower()] + approx = False + values = [s.strip() for s in exp.value] + if 'op' not in exp: # prevented by grammar + raise errors.ParserError('Missing operator', exp) + if exp.op in ('~' '!~'): + approx = True + if exp.op in (':', '=', '~', 'in'): + tok = ast.filter.Any(pred, ast.filter.Includes(*values, approx=approx)) + elif exp.op in ('!=', '!~', 'not in'): + tok = ast.filter.All(pred, ast.filter.Excludes(*values, approx=approx)) + else: # prevented by grammar + raise errors.ParserError('Invalid operator ({})'.format(exp.op), exp) + tokens.append(tok) + + elif exp.getName() == 'tag': + values = [s.strip() for s in exp.value] + if 'op' not in exp: + outer = ast.filter.Any + cond = ast.filter.Includes(*values) + elif exp.op == '~': + outer = ast.filter.Any + cond = ast.filter.Includes(*values, approx=True) + elif exp.op == '!': + outer = ast.filter.All + cond = ast.filter.Excludes(*values) + elif exp.op == '!~': + outer = ast.filter.All + cond = ast.filter.Excludes(*values, approx=True) + else: # prevented by grammar + raise errors.ParserError('Invalid operator ({})'.format(exp.op), exp) + tokens.append(outer(ns.bse.tag, ast.filter.Any(ns.bst.label, cond))) + + elif exp.getName() == 'continuous': # FIXME: simplify and adapt bsfs.query.ast.filter.Between accordingly! + lo, hi = None, None + lo_inc, hi_inc = False, False + predicate = None + if 'eq' in exp: + # equation style + predicate = self._abb2uri[exp.eq.predicate.lower()] + + if ('>' in exp.eq.cleft and '<' in exp.eq.cright) or \ + ('<' in exp.eq.cleft and '>' in exp.eq.cright) or \ + (exp.eq.cleft == '=' and exp.eq.cright == '='): + # x > pred < y or x < pred > y or x = pred = y + raise errors.ParserError('Cannot have two lower or two upper bounds', exp) + + if '>' in exp.eq.cleft: + hi = exp.eq.vleft + hi_inc = '=' in exp.eq.cleft + elif '<' in exp.eq.cleft: + lo = exp.eq.vleft + lo_inc = '=' in exp.eq.cleft + elif exp.eq.cleft == '=': + hi = lo = exp.eq.vleft + lo_inc = hi_inc = True + + if '>' in exp.eq.cright: + lo = exp.eq.vright + lo_inc = '=' in exp.eq.cright + elif '<' in exp.eq.cright: + hi = exp.eq.vright + hi_inc = '=' in exp.eq.cright + elif exp.eq.cright == '=': + hi = lo = exp.eq.vright + lo_inc = hi_inc = True + + elif 'range' in exp: # value in [lo:hi] + predicate = self._abb2uri[exp.range.predicate.lower()] + + if 'lo' in exp.range: + lo = exp.range.lo + lo_inc = exp.range.bopen == '[' + if 'hi' in exp.range: + hi = exp.range.hi + hi_inc = exp.range.bclose == ']' + + else: # prevented by grammar + raise errors.ParserError('Expression is neither a range nor an equation', exp) + + # interpret values + if predicate in self._DATETIME_PREDICATES: + + # turn into datetime + lo, lfmt = datefmt.guess_datetime(lo) if lo is not None else (None, None) + hi, hfmt = datefmt.guess_datetime(hi) if hi is not None else (None, None) + + if lo is None and hi is None: # prevented by grammar + raise errors.ParserError('At least one bound must be present', exp) + + # turn the query into the format lo <= pred < hi by adjusting the boundaries + if hi == lo and lo_inc and hi_inc: + # example: pred = 2012 -> 1.1.2012 <= pred < 1.1.2013 + hi = datefmt.increment(lo, lfmt) + lo_inc = True + hi_inc = False + else: + if lo is not None: + # example: pred >= 2012 -> pred >= 1.1.2012, 00:00 + lo = datefmt.increment(lo, lfmt) if not lo_inc else lo + lo_inc = True + + if hi is not None: + # example: pred <= 2012 -> pred < 1.1.2013, 00:00 + hi = datefmt.increment(hi, hfmt) if hi_inc else hi + hi_inc = False + + # build the ast node + if (lo is not None and lfmt.is_time()) or (hi is not None and hfmt.is_time()): + # time specification + + if (lo is not None and not lfmt.is_time()) or \ + (hi is not None and not hfmt.is_time()): + # lo/hi must both be time specifications + raise errors.ParserError('Both bounds must be a time specification', (lo, hi)) + + if lo is None: + # example: pred < 5 am -> 0 <= pred < 05:00 + lo = ttime.from_timestamp_loc(0) + lo_inc = True + + if hi is None: + # example: pred > 5 am -> 06:00 <= pred <= 24:00 + hi = ttime.from_timestamp_loc(3600 * 24) + hi_inc = True + + # Check consistency + if not (lo < hi or (lo == hi and lo_inc and hi_inc)): + raise errors.ParserError('Lower bound must not exceed upper bound', (lo, hi)) + + tokens.append( + ast.filter.Any(predicate, + ast.filter.Between(lo, hi, not lo_inc, not hi_inc))) + + else: # date specification + # Check consistency + lo = lo if lo is not None else datetime.min + hi = hi if hi is not None else datetime.max + + if not (lo < hi or (lo == hi and lo_inc and hi_inc)): + raise errors.ParserError('Lower bound must not exceed upper bound', (lo, hi)) + + tokens.append( + ast.filter.Any(predicate, + ast.filter.Between(lo, hi, not lo_inc, not hi_inc))) + + else: + # number predicate + lo = float(''.join(lo)) if lo is not None else float('-inf') + hi = float(''.join(hi)) if hi is not None else float('inf') + + # Check consistency + if not (lo < hi or (lo == hi and lo_inc and hi_inc)): + raise errors.ParserError('Lower bound must not exceed upper bound', (lo, hi)) + + # FIXME: mb/port: Three times the same code... optimize + tokens.append( + ast.filter.Any(predicate, + ast.filter.Between(lo, hi, not lo_inc, not hi_inc))) + + else: # prevented by grammar + raise errors.ParserError('Invalid expression', exp) + + return ast.filter.And(tokens) + +## EOF ## diff --git a/tagit/parsing/filter/to_string.py b/tagit/parsing/filter/to_string.py new file mode 100644 index 0000000..6a1b035 --- /dev/null +++ b/tagit/parsing/filter/to_string.py @@ -0,0 +1,255 @@ +""" + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# tagit imports +from tagit.utils.bsfs import ast, matcher, URI +from tagit.utils import errors, ns + +# exports +__all__ = ('ToString', ) + + +## code ## + +class ToString(): + + def __init__(self, schema): + self.matches = matcher.Filter() + + self.schema = schema + predicates = {pred for pred in self.schema.predicates() if pred.domain <= self.schema.node(ns.bsn.Entity)} + # shortcuts + self._abb2uri = {pred.uri.fragment: pred.uri for pred in predicates} # FIXME: tie-breaking for duplicates + self._uri2abb = {uri: fragment for fragment, uri in self._abb2uri.items()} + + def __call__(self, query): + """ + """ + # FIXME: test query class type + if self.matches(query, ast.filter.And(matcher.Rest())): + return ' / '.join(self._parse(sub) for sub in query) + return self._parse(query) + + def _parse(self, query): + cases = ( + self._has, + self._entity, + self._group, + self._tag, + self._range, + self._categorical, + ) + for clbk in cases: + result = clbk(query) + if result is not None: + return result + + raise errors.BackendError() + + def _has(self, query): + # Has(<pred>) <-> has <pred> + # Not(Has(<pred>)) <-> has no <pred> + has = ast.filter.Has( + matcher.Partial(ast.filter.Predicate), + ast.filter.GreaterThan(1, strict=False)) + if self.matches(query, has): + # FIXME: guard against predicate mismatch + return f'has {self._uri2abb[query.predicate.predicate]}' + if self.matches(query, ast.filter.Not(has)): + # FIXME: guard against predicate mismatch + return f'has no {self._uri2abb[query.predicate.predicate]}' + return None + + def _categorical(self, query): + if not isinstance(query, ast.filter._Branch): + return None + + # shortcuts + expr = query.expr + pred = self._uri2abb.get(query.predicate.predicate, None) + if pred is None: + return None + + # positive constraints + if isinstance(query, ast.filter.Any): + # approximate positive constraint + # Any(<pred>, Includes(<values>, approx=True)) -> pred ~ ("...", ...) + if self.matches(expr, matcher.Partial(ast.filter.Substring)): + return f'{pred} ~ {expr.value}' + if self.matches(expr, ast.filter.Or(matcher.Rest(matcher.Partial(ast.filter.Substring)))): + values = '", "'.join(sub.value for sub in expr) + return f'{pred} ~ ("{values}")' + + # exact positive constraint + # ast.filter.Any(<pred>, ast.filter.Includes(<values>, approx=False)) -> pred = ("...", ...) + if self.matches(expr, matcher.Partial(ast.filter.Equals)): + return f'{pred} = {expr.value}' + if self.matches(query, ast.filter.Or(matcher.Rest(matcher.Partial(ast.filter.Equals)))): + values = '", "'.join(sub.value for sub in expr) + return f'{pred} = ("{values}")' + + # negative constraints + if isinstance(query, ast.filter.All): + # approximate negative constraint + # ast.filter.All(<pred>, ast.filter.Excludes(<values>, approx=True)) -> pred !~ ("...", ...) + if self.matches(query, ast.filter.Not(matcher.Partial(ast.filter.Substring))): + return f'{pred} !~ "{expr.value}"' + if self.matches(query, ast.filter.Not(ast.filter.Or(matcher.Rest(matcher.Partial(ast.filter.Substring))))): + values = '", "'.join(sub.value for sub in expr) + return f'{pred} !~ ("{values}")' + + # exact negative constraint + # ast.filter.All(<pred>, ast.filter.Excludes(<values>, approx=False)) -> pred != ("...", ...) + if self.matches(query, ast.filter.Not(matcher.Partial(ast.filter.Equals))): + return f'{pred} != "{expr.value}"' + if self.matches(query, ast.filter.Not(ast.filter.Or(matcher.Rest(matcher.Partial(ast.filter.Equals))))): + values = '", "'.join(sub.value for sub in expr) + return f'{pred} != ("{values}")' + + return None + + def _tag(self, query): + # positive constraint + # ast.filter.Any(ns.bse.tag, ast.filter.Any(ns.bst.label, ast.filter.Includes(..., approx=?))) <-> "...", ...; ~ "...", ... + if self.matches(query, ast.filter.Any(ns.bse.tag, ast.filter.Any(ns.bst.label, matcher.Any()))): + expr = query.expr.expr + # approximate positive constraint + if self.matches(expr, matcher.Partial(ast.filter.Substring)): + return f'~ {expr.value}' + if self.matches(expr, ast.filter.Or(matcher.Rest(matcher.Partial(ast.filter.Substring)))): + values = '", "'.join(sub.value for sub in expr) + return f'~ "{values}"' + # exact positive constraint + if self.matches(expr, matcher.Partial(ast.filter.Equals)): + return f'{expr.value}' + if self.matches(expr, ast.filter.Or(matcher.Rest(matcher.Partial(ast.filter.Equals)))): + values = '", "'.join(sub.value for sub in expr) + return f'"{values}"' + + # negative constraint + # ast.filter.All(ns.bse.tag, ast.filter.Any(ns.bst.label, ast.filter.Excludes(..., approx=?))) <-> ! "...", ... ; !~ "...", ... + if self.matches(query, ast.filter.All(ns.bse.tag, ast.filter.Any(ns.bst.label, ast.filter.Not(matcher.Any())))): + expr = query.expr.expr.expr + # approximate negative constraint + if self.matches(expr, matcher.Partial(ast.filter.Substring)): + return f'!~ {expr.value}' + if self.matches(expr, ast.filter.Or(matcher.Rest(matcher.Partial(ast.filter.Substring)))): + values = '", "'.join(sub.value for sub in expr) + return f'!~ "{values}"' + # exact negative constraint + if self.matches(expr, matcher.Partial(ast.filter.Equals)): + return f'! {expr.value}' + if self.matches(expr, ast.filter.Or(matcher.Rest(matcher.Partial(ast.filter.Equals)))): + values = '", "'.join(sub.value for sub in expr) + return f'! "{values}"' + + return None + + def _range(self, query): + # FIXME: handle dates and times! + # FIXME: use default/configurable separators from from_string + if not isinstance(query, ast.filter.Any): + return None + + expr = query.expr + pred = self._uri2abb.get(query.predicate.predicate, None) + if pred is None: + return None + + if self.matches(expr, matcher.Partial(ast.filter.Equals)): + return f'{pred} = {expr.value}' + if self.matches(expr, matcher.Partial(ast.filter.GreaterThan, strict=True)): + return f'{pred} > {expr.threshold}' + if self.matches(expr, matcher.Partial(ast.filter.GreaterThan, strict=False)): + return f'{pred} >= {expr.threshold}' + if self.matches(expr, matcher.Partial(ast.filter.LessThan, strict=True)): + return f'{pred} < {expr.threshold}' + if self.matches(expr, matcher.Partial(ast.filter.LessThan, strict=False)): + return f'{pred} <= {expr.threshold}' + if self.matches(expr, ast.filter.And( + matcher.Partial(ast.filter.GreaterThan), + matcher.Partial(ast.filter.LessThan))): + lo, hi = list(expr) + if self.matches(lo, matcher.Partial(ast.filter.LessThan)): + lo, hi = hi, lo + b_open = '(' if lo.strict else '[' + b_close = ')' if hi.strict else ']' + return f'{pred} = {b_open}{lo.threshold} - {hi.threshold}{b_close}' + """ + ast.filter.Any(<pred>, ast.filter.Between(lo, hi, lo_strict, hi_strict)) + pred <? hi + pred >? hi + pred = [lo, hi] + pred = (lo, hi) + pred = [lo, hi) + pred = (lo, hi] + """ + return None + + def _entity(self, query): + # defaults + negated = False + guids = set() + + def get_guids(value): + if isinstance(value, URI): + return {value} + else: # elif isinstance(query.value, Nodes): + return set(value.guids) + + if self.matches(query, matcher.Partial(ast.filter.Is)): + guids = get_guids(query.value) + elif self.matches(query, ast.filter.Or(matcher.Rest(matcher.Partial(ast.filter.Is)))): + guids = {guid for sub in query for guid in get_guids(sub.value) } + elif self.matches(query, ast.filter.Not(matcher.Partial(ast.filter.Is))): + negated = True + guids = get_guids(query.expr.value) + elif self.matches(query, ast.filter.Not(ast.filter.Or(matcher.Rest(matcher.Partial(ast.filter.Is))))): + negated = True + guids = {guid for sub in query.expr for guid in get_guids(sub.value) } + + if len(guids) == 0: + # no matches + return None + # some matches + cmp = 'not in' if negated else 'in' + values = '", "'.join(guids) + return f'id {cmp} "{values}"' + + def _group(self, query): + # ast.filter.Any(ns.bse.group, ast.filter.Is(...)) <-> group = ("...", ...) + if not self.matches(query, ast.filter.Any(ns.bse.group, matcher.Any())): + return None + + def get_guids(value): + if isinstance(value, URI): + return {value} + else: # elif isinstance(query.value, Nodes): + return set(value.guids) + + expr = query.expr + guids = set() + negated = False + + if self.matches(expr, matcher.Partial(ast.filter.Is)): + guids = get_guids(expr.value) + elif self.matches(expr, ast.filter.Or(matcher.Rest(matcher.Partial(ast.filter.Is)))): + guids = {guid for sub in expr for guid in get_guids(sub.value) } + elif self.matches(expr, ast.filter.Not(matcher.Partial(ast.filter.Is))): + negated = True + guids = get_guids(expr.value) + elif self.matches(expr, ast.filter.Not(ast.filter.Or(matcher.Rest(matcher.Partial(ast.filter.Is))))): + negated = True + guids = {guid for sub in expr for guid in get_guids(sub.value) } + + if len(guids) == 0: # no matches + return None + # some matches + cmp = 'not in' if negated else 'in' + values = '", "'.join(guids) + return f'group {cmp} "{values}"' + +## EOF ## diff --git a/tagit/parsing/sort.py b/tagit/parsing/sort.py new file mode 100644 index 0000000..75fa36c --- /dev/null +++ b/tagit/parsing/sort.py @@ -0,0 +1,179 @@ +""" + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# external imports +from pyparsing import CaselessKeyword, Group, Or, Word, delimitedList, oneOf, ParseException + +# tagit imports +from tagit.utils import errors, Struct + +# exports +__all__ = ( + 'Sort', + ) + + +## code ## + +class Sort(): + """Sort parser. + + A sort string can be as simple as a predicate, but also allows + a more verbose specification for more natural readability. + In brief and somewhat relaxed notation, the syntax is: + [sort [<type>] by] <predicate> [similarity to] [<anchor>] [<direction>] + Multiple sort terms are concatenated with a comma. + + Examples: + time + time asc + sort by time desc + sort numerically by time downwards + sort by tag similarity to AF39D281CE3 up + time, iso + """ + QUERY = None + PREDICATES = None + + def __init__(self, sortkeys): + self.sortkeys = sortkeys + + def __call__(self, query): + return self.parse(query) + + def build_parser(self, predicate=None): + # The *predicate* argument is for compatibility with predicate listener. + # It's not actually used here. + """ + The grammar is composed as follows: + + QUERY := EXPR | EXPR, EXPR + EXPR := PREFIX PREDICATE SUFFIX | PREDICATE SUFFIX | PREFIX PREDICATE | PREDICATE + PREFIX := sort TYPE by | sort by + SUFFIX := SIMILAR DIRECTION | SIMILAR | DIRECTION + SIMILAR := similarity to ANCHOR | ANCHOR + TYPE := numerically | alphabetically + PREDICATE := [predicate] + ANCHOR := [guid] + DIRECTION := up | down | asc | desc | ascending | descending | reversed | upwards | downwards + """ + # predicates from sortkeys + self.PREDICATES = self.sortkeys.scope.library | self.sortkeys.typedef.anchored + + ## terminals + # direction is just a list of keywords + direction = oneOf('up down asc desc ascending descending reversed upwards downwards', + caseless=True).setResultsName('direction') + # type is just a list of keywords + type_ = oneOf('numerically alphabetically').setResultsName('type') + # predicates are from an enum + predicate = Or([CaselessKeyword(p) for p in self.PREDICATES]).setResultsName('predicate') + # anchor is a hex digest + anchor = Word('abcdef0123456789ABCDEF').setResultsName('anchor') + + ## rules + similar = Or([CaselessKeyword('similarity to') + anchor, + anchor]) + suffix = Or([similar + direction, similar, + direction]) + prefix = Or([CaselessKeyword('sort') + type_ + CaselessKeyword('by'), + CaselessKeyword('sort by')]) + expr = Group(Or([prefix + predicate + suffix, + predicate + suffix, + prefix + predicate, + predicate])) + + self.QUERY = delimitedList(expr, delim=',') + return self + + def __del__(self): + if self.QUERY is not None: # remove listener + try: + self.sortkeys.ignore(self.build_parser) + except ImportError: + # The import fails if python is shutting down. + # In that case, the ignore becomes unnecessary anyway. + pass + + def parse(self, sort): + if self.QUERY is None: + # initialize parser + self.build_parser() + # attach listener to receive future updates + self.sortkeys.listen(self.build_parser) + + try: + parsed = self.QUERY.parseString(sort, parseAll=True) + except ParseException as e: + raise errors.ParserError('Cannot parse query', e) + + # convert to AST + tokens = [] + for exp in parsed: + args = Struct( + predicate=None, + type=None, + anchor=None, + direction='asc', + ) + args.update(**exp.asDict()) + + # check predicate + if args.predicate is None: # prevented by grammar + raise errors.ParserError('Missing sort key', exp) + if args.predicate not in self.sortkeys: # prevented by grammar + raise errors.ParserError('Invalid sort key', exp) + + # check direction + if args.direction in ('up', 'ascending', 'asc', 'upwards'): + reverse = False + elif args.direction in ('down', 'desc', 'descending', 'reversed', 'downwards'): + reverse = True + else: # prevented by grammar + raise errors.ParserError('Invalid direction', exp) + + # infer type from predicate if needed + if args.anchor is not None: + args.type = 'anchored' + elif args.type is None: + typedef = self.sortkeys.predicate(args.predicate).typedef + if not len(typedef): + raise errors.ParserError('Undefined type', exp) + elif len(typedef) == 1: + args.type = list(typedef)[0].lower() + else: + raise errors.ParserError('Ambiguous type', exp) + + # translate types + args.type = { + 'numerically': 'numerical', + 'alphabetically': 'alphabetical' + }.get(args.type, args.type) + + # check type compatibility + admissible_types = {t.lower() for t in self.sortkeys.predicate(args.predicate).typedef} + if args.type not in admissible_types: + raise errors.ParserError('Invalid type for predicate', exp) + elif args.type == 'anchored' and args.anchor is None: # type set if anchor isn't None + raise errors.ParserError('No anchor given', exp) + + # build AST + if args.type in ('anchored', ): + tokens.append(ast.AnchoredSort(args.predicate, args.anchor, reverse)) + elif args.type in ('alphabetical', 'alphabetically'): + tokens.append(ast.AlphabeticalSort(args.predicate, reverse)) + elif args.type in ('numerical', 'numerically'): + tokens.append(ast.NumericalSort(args.predicate, reverse)) + else: # prevented by grammar + raise errors.ParserError('Invalid type for predicate', exp) + + # aggregate if need be + if len(tokens) == 1: + return tokens[0] + else: + return ast.Order(*tokens) + +## EOF ## diff --git a/tagit/tiles/__init__.py b/tagit/tiles/__init__.py new file mode 100644 index 0000000..f51ee2a --- /dev/null +++ b/tagit/tiles/__init__.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 typing + +# tagit imports +from tagit.utils.builder import BuilderBase + +# inner-module imports +from .browser_tags import BrowserTags +from .buttons import Buttons +from .cursor_tags import CursorTags +#from .entity_histogram import EntityHistogram +from .geo import Map +from .info import Info +#from .libsummary import LibSummary +#from .searchtree import Searchtree +from .selection_tags import SelectionTags +#from .suggested_tags import SuggestedTags +#from .tag_distribution import TagDistribution +#from .tag_histogram import TagHistogram +#from .tag_tree import TagTree +#from .tagcloud import Tagcloud +#from .venn import Venn + +# exports +__all__: typing.Sequence[str] = ( + 'TileBuilder', + ) + + +## code ## + +class TileBuilder(BuilderBase): + _factories = { + 'BrowserTags': BrowserTags, + 'Buttons': Buttons, + 'CursorTags': CursorTags, +# 'EntityHistogram': EntityHistogram, + 'Geo': Map, + 'Info': Info, +# 'LibSummary': LibSummary, +# 'Searchtree': Searchtree, + 'SelectionTags': SelectionTags, +# 'SuggestedTags': SuggestedTags, +# 'TagDistribution': TagDistribution, +# 'TagHistogram': TagHistogram, +# 'TagTree': TagTree, +# 'Tagcloud': Tagcloud, +# 'Venn': Venn, + } + +## EOF ## diff --git a/tagit/tiles/browser_tags.py b/tagit/tiles/browser_tags.py new file mode 100644 index 0000000..3a9c25f --- /dev/null +++ b/tagit/tiles/browser_tags.py @@ -0,0 +1,108 @@ +""" + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# standard imports +from functools import reduce +import operator + +# kivy imports +from kivy.lang import Builder + +# tagit imports +from tagit.widgets.browser import BrowserAwareMixin + +# inner-module imports +from .tile import TileWithLabel + +# exports +__all__ = ('BrowserTags', ) + + +## code ## + +# load kv +Builder.load_string(''' +<BrowserTags>: + title: 'Tags' + tooltip: 'Tags of displayed items.' +''') + +# classes +class BrowserTags(TileWithLabel, BrowserAwareMixin): + """Show tags of displayed items. Tags of selected items are highlighted.""" + + displayed = None + + def on_browser(self, sender, browser): + # remove old binding + if self.browser is not None: + self.browser.unbind(cursor=self.update) + self.browser.unbind(selection=self.update) + self.browser.unbind(items=self.on_items) + # add new binding + self.browser = browser + if self.browser is not None: + self.browser.bind(cursor=self.update) + self.browser.bind(selection=self.update) + self.browser.bind(items=self.on_items) + # populate displayed first, then update + self.on_items(browser, browser.items) + self.update() + + def __del__(self): + if self.browser is not None: + self.browser.unbind(cursor=self.update) + self.browser.unbind(selection=self.update) + self.browser.unbind(items=self.on_items) + self.browser = None + + def on_items(self, browser, items): + # unfold + items = browser.unfold(items) + # get tags + self.displayed = items.tag.label(node=False) + # update view + self.update() + + def update(self, *args): + if not self.visible: + self.text = '' + + elif self.displayed is None: + self.on_items(self.root.browser, self.root.browser.items) + # calls update again with not-None self.displayed + + else: + browser = self.root.browser + + # handle cursor + if browser.cursor is None: + cursor = set() + else: + cursor = browser.cursor.tag.label() + + # handle selection + if len(browser.selection) == 0: + selected = set() + else: + selection = reduce(operator.add, browser.selection) + selected = selection.tag.label(node=False) + + # assemble tag list + tags = [] + for tag in sorted(self.displayed | selected | cursor): + pretty = tag + if tag in cursor: + pretty = f'[b]{pretty}[/b]' # bold + if tag in selected: + pretty = f'[color=#415bCD]{pretty}[/color]' # blue color + + tags.append(pretty) + + # Apply prefix and display + self.text = ', '.join(tags) + +## EOF ## diff --git a/tagit/tiles/buttons.py b/tagit/tiles/buttons.py new file mode 100644 index 0000000..2a13911 --- /dev/null +++ b/tagit/tiles/buttons.py @@ -0,0 +1,50 @@ +""" + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 + +""" +# kivy imports +from kivy.lang import Builder +import kivy.properties as kp + +# inner-module imports +from .tile import Tile + +# exports +__all__ = ('Buttons', ) + +## code ## + +# load kv +# NOTE: ButtonDock doesn't need to be imported... why?! +Builder.load_string(''' +<Buttons>: + title: 'Actions' + tooltip: 'Some buttons' + + # content + slim: False + btns: btns + ButtonDock: + root: root.root + orientation: 'lr-tb' + id: btns + # space between childs + spacing: (5, 0) if root.slim else (30, 5) + # space between ButtonDock and its children + padding: (0, 0) if root.slim else (10, 5) +''') + +# classes +class Buttons(Tile): + """A container for buttons to trigger some action.""" + buttons = kp.ListProperty() + + def update(self): + if self.visible and len(self.btns.children) == 0: + self.btns.clear_widgets() + self.btns.populate(self.buttons) + +## EOF ## diff --git a/tagit/tiles/cursor_tags.py b/tagit/tiles/cursor_tags.py new file mode 100644 index 0000000..2ab2f30 --- /dev/null +++ b/tagit/tiles/cursor_tags.py @@ -0,0 +1,60 @@ +""" + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# kivy imports +from kivy.lang import Builder + +# tagit imports +from tagit.widgets.browser import BrowserAwareMixin + +# inner-module imports +from .tile import TileWithLabel + +# exports +__all__ = ('CursorTags', ) + + +## code ## + +# load kv +Builder.load_string(''' +<CursorTags>: + title: "Cursor's tags" + tooltip: 'Tags at the cursor' +''') + + +# classes +class CursorTags(TileWithLabel, BrowserAwareMixin): + """Show tags of cursor item.""" + + def on_browser(self, sender, browser): + # remove old binding + if self.browser is not None: + self.browser.unbind(cursor=self.update) + # add new binding + self.browser = browser + if self.browser is not None: + self.browser.bind(cursor=self.update) + self.update() + + def __del__(self): + if self.browser is not None: + self.browser.unbind(cursor=self.update) + self.browser = None + + def update(self, *args): + cursor = self.root.browser.cursor + if not self.visible or cursor is None: + # no cursor, nothing to do + self.text = '' + else: + tags = cursor.tag.label() + tags = {tag.title() for tag in tags} # nice display + tags = sorted(tags) + self.text = ', '.join(tags) + +## EOF ## diff --git a/tagit/tiles/decoration.kv b/tagit/tiles/decoration.kv new file mode 100644 index 0000000..ae7e49d --- /dev/null +++ b/tagit/tiles/decoration.kv @@ -0,0 +1,87 @@ + +# NOTE: +# TileDecoration assumes as *cbox* property that identifies the widget +# to which the main content will be added. + +<TileDecorationVanilla>: + cbox: cbox + + RelativeLayout: + id: cbox + +<TileDecorationBorder>: + cbox: cbox + + canvas.after: + # tile shadow + Color: + rgb: 0.2,0.2,0.2 + Line: + rectangle: self.x+5,self.y+5,self.width-10,self.height-10 + width: 2 + + Color: + rgb: 0.6,0.6,0.6 + Line: + rectangle: self.x+7,self.y+7,self.width-14,self.height-14 + width: 1 + + RelativeLayout: + id: cbox + pos: 15, 15 + size: root.width-30, root.height-30 + size_hint: None, None + +<TileDecorationFilledRectangle>: + cbox: cbox + + Label: + text: root.client.title + size: root.width, 20 + size_hint: None, None + pos: 0, root.height - self.height - 5 + + RelativeLayout: + id: cbox + pos: 5, 5 + size: root.width-10, root.height-30 + size_hint: None, None + + canvas.before: + Color: + rgba: 1,0,0,0.5 + Rectangle: + pos: 0, 0 + size: self.size + + +<TileDecorationRoundedBorder>: + cbox: cbox + + Label: + text: root.client.title + size: root.width, 20 + size_hint: None, None + pos: 0, root.height - self.height - 3 + + RelativeLayout: + id: cbox + pos: 5, 3 + size: root.width-10, root.height-30 + size_hint: None, None + + canvas.before: + Color: + rgb: 0xc5/256, 0xc9/256, 0xc7/256 + RoundedRectangle: + pos: self.pos + #radius: [10, 10, 10, 10] + size: self.size + Color: + rgb: 0x1c/256, 0x1b/256, 0x22/256 # FIXME: re-use from MainWindow? + RoundedRectangle: + pos: self.pos[0] + 2, self.pos[1] + 2 + radius: [5, 5, 5, 5] + size: self.size[0] - 4, self.size[1] - 4 + +## EOF ## diff --git a/tagit/tiles/decoration.py b/tagit/tiles/decoration.py new file mode 100644 index 0000000..c772f64 --- /dev/null +++ b/tagit/tiles/decoration.py @@ -0,0 +1,77 @@ +""" + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# standard imports +import os +import typing + +# kivy imports +from kivy.lang import Builder +from kivy.uix.relativelayout import RelativeLayout +import kivy.properties as kp + +# exports +__all__: typing.Sequence[str] = ( + 'TileDecorationBorder', + 'TileDecorationRoundedBorder', + 'TileDecorationFilledRectangle', + 'TileDecorationVanilla', + ) + + +## code ## + +# load kv +Builder.load_file(os.path.join(os.path.dirname(__file__), 'decoration.kv')) + +# classes +class TileDecoration(RelativeLayout): + + cbox = kp.ObjectProperty(None) + client = kp.ObjectProperty(None) + + def __repr__(self): + return f'{self.__class__.__name__}({self.client})' + + def on_cbox(self, wx, cbox): + if cbox is not None and len(cbox.children) == 0: + cbox.add_widget(self.client) + + @property + def default_size(self): + return self.client.default_size + + +class TileDecorationVanilla(TileDecoration): + pass + + +class TileDecorationFilledRectangle(TileDecoration): + @property + def default_size(self): + width, height = self.client.default_size + width = None if width is None else width + 10 + height = None if height is None else height + 30 + return width, height + + +class TileDecorationBorder(TileDecoration): + @property + def default_size(self): + width, height = self.client.default_size + width = None if width is None else width + 30 + height = None if height is None else height + 30 + return width, height + +class TileDecorationRoundedBorder(TileDecoration): + @property + def default_size(self): + width, height = self.client.default_size + width = None if width is None else width + 30 + height = None if height is None else height + 30 + return width, height + +## EOF ## diff --git a/tagit/tiles/geo.py b/tagit/tiles/geo.py new file mode 100644 index 0000000..796a1c2 --- /dev/null +++ b/tagit/tiles/geo.py @@ -0,0 +1,140 @@ +""" + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# standard imports +import os + +# kivy ipmorts +from kivy.lang import Builder +from kivy.uix.label import Label +import kivy.properties as kp + +# tagit imports +# NOTE: the following line segfaults: +# mapview.source.py:128:self.dp_tile_size = min(dp(self.tile_size), self.tile_size * 2) +# setting it to a static value (e.g. 256) works. +from tagit.external.kivy_garden.mapview import MapView, MapMarkerPopup +from tagit.utils import ns +from tagit.widgets.browser import BrowserAwareMixin + +# inner-module imports +from .tile import Tile + +# exports +__all__ = ('Map', ) + + +## code ## + +Builder.load_string(''' +<Map>: + # meta + title: "Map" + tooltip: 'Location of an item' + # content + map_: map_ + MapView: + id: map_ + zoom: 9 + +<MapLabel>: + size: self.texture_size + size_hint: None, None + padding: 5, 5 + font_size: '15sp' + + canvas.before: + # background + Color: + rgba: 0,0,0,0.6 + RoundedRectangle: + size: self.size + pos: self.pos + radius: [10] # keep in sync with the line's radius + # border + Color: + rgba: 0,0,0,1 + Line: + rounded_rectangle: self.x, self.y, self.width, self.height, 10 +''') + +class MapLabel(Label): + pass + +class Map(Tile, BrowserAwareMixin): + """Draw a map which indicates visible items' locations.""" + + # list of map markers + markers = kp.ListProperty() + + def on_browser(self, sender, browser): + """Bind to browser properties.""" + # remove old binding + if self.browser is not None: + self.browser.unbind(cursor=self.update) + self.browser.unbind(items=self.update_markers) + # add new binding + self.browser = browser + if self.browser is not None: + self.browser.bind(cursor=self.update) + self.browser.bind(items=self.update_markers) + # initial calls + self.update_markers() + self.update() + + def __del__(self): + if self.browser is not None: + self.browser.unbind(cursor=self.update) + self.browser.unbind(items=self.update_markers) + self.browser = None + + def update_markers(self, *args): + """Draw markers for all browser items.""" + # remove old markers + for mark in self.markers: + self.map_.remove_marker(mark) + self.markers.clear() + + # get view data + data = self.root.browser.unfold(self.root.browser.items).get( + ns.bse.filename, + ns.bse.latitude, + ns.bse.longitude, + node=True, + ) + + # draw new markers + for ent, vdict in data.items(): + if ns.bse.latitude not in vdict: + continue + if ns.bse.longitude not in vdict: + continue + # TODO: cluster points, one marker for multiple items + lat = vdict[ns.bse.latitude] + lon = vdict[ns.bse.longitude] + # create popup marker + mark = MapMarkerPopup(lat=lat, lon=lon) + text = vdict.get(ns.bse.filename, + ', '.join(os.path.basename(guid) for guid in ent.guids)) + mark.add_widget(MapLabel(text=text)) + # add marker + self.markers.append(mark) + self.map_.add_marker(mark) + + def update(self, *args): + """Focus the map on the cursor.""" + if not self.visible: + return + + cursor = self.root.browser.cursor + if cursor is not None: + coords = cursor.get(ns.bse.latitude, ns.bse.longitude) + if set(coords.keys()) == {ns.bse.latitude, ns.bse.longitude}: + lat = coords[ns.bse.latitude] + lon = coords[ns.bse.longitude] + self.map_.center_on(lat, lon) + +## EOF ## diff --git a/tagit/tiles/info.py b/tagit/tiles/info.py new file mode 100644 index 0000000..9555b35 --- /dev/null +++ b/tagit/tiles/info.py @@ -0,0 +1,85 @@ +""" + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# standard imports +from collections import OrderedDict + +# kivy imports +from kivy.lang import Builder + +# tagit imports +from tagit.utils import ttime, ns, magnitude_fmt +from tagit.widgets.browser import BrowserAwareMixin +from tagit.widgets.session import StorageAwareMixin + +# inner-module imports +from .tile import TileTabular + +# exports +__all__ = ('Info', ) + + +## code ## + +# load kv +Builder.load_string(''' +<Info>: + title: 'Item info' + tooltip: 'Key properties of the cursor item' + # assuming 7 info items + #default_size: None, 7*self.font_size + 6*5 + keywidth: min(75, self.width * 0.4) +''') + + +# classes +class Info(TileTabular, BrowserAwareMixin, StorageAwareMixin): + """Show essential attributes about the cursor.""" + + def on_root(self, wx, root): + BrowserAwareMixin.on_root(self, wx, root) + StorageAwareMixin.on_root(self, wx, root) + + def on_browser(self, sender, browser): + # remove old binding + if self.browser is not None: + self.browser.unbind(cursor=self.update) + # add new binding + self.browser = browser + if self.browser is not None: + self.browser.bind(cursor=self.update) + self.update() + + def __del__(self): + if self.browser is not None: + self.browser.unbind(cursor=self.update) + self.browser = None + + def on_predicate_modified(self, *args): + self.update() + + def update(self, *args): + cursor = self.root.browser.cursor + if not self.visible or cursor is None: + # invisible or no cursor, nothing to show + self.tabledata = OrderedDict({}) + + else: + preds = cursor.get( + ns.bsfs.Node().t_created, + ns.bse.filesize, + ns.bse.filename, + (ns.bse.tag, ns.bst.label), + ) + self.tabledata = OrderedDict({ + 'Date' : ttime.from_timestamp_utc( + preds.get(ns.bsfs.Node().t_created, ttime.timestamp_min)).strftime('%d.%m.%y %H:%M'), + 'Filesize' : magnitude_fmt(preds.get(ns.bse.filesize, 0)), + 'Filename' : preds.get(ns.bse.filename, 'n/a'), + 'Tags' : ', '.join(sorted(preds.get((ns.bse.tag, ns.bst.label), [' ']))), + }) + +## EOF ## diff --git a/tagit/tiles/selection_tags.py b/tagit/tiles/selection_tags.py new file mode 100644 index 0000000..7951cfe --- /dev/null +++ b/tagit/tiles/selection_tags.py @@ -0,0 +1,60 @@ +""" + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# kivy imports +from kivy.lang import Builder + +# tagit imports +from tagit.widgets.browser import BrowserAwareMixin + +# inner-module imports +from .tile import TileWithLabel + +# exports +__all__ = ('SelectionTags', ) + + +## code ## + +# load kv +Builder.load_string(''' +<SelectionTags>: + title: "Selection's tags" + default_size: None, 50 +''') + +# classes +class SelectionTags(TileWithLabel, BrowserAwareMixin): + """Show tags of selected items.""" + + def on_browser(self, sender, browser): + # remove old binding + if self.browser is not None: + self.browser.unbind(selection=self.update) + # add new binding + self.browser = browser + if self.browser is not None: + self.browser.bind(selection=self.update) + self.update() + + def __del__(self): + if self.browser is not None: + self.browser.unbind(selection=self.update) + self.browser = None + + def update(self, *args): + browser = self.root.browser + selection = browser.unfold(browser.selection) + if not self.visible or len(selection) == 0: + # nothing selected, nothing to do + self.text = '' + else: + tags = selection.tag.label(node=False) + tags = {tag.title() for tag in tags} # nice display + tags = sorted(tags) + self.text = ', '.join(tags) + +## EOF ## diff --git a/tagit/tiles/tile.kv b/tagit/tiles/tile.kv new file mode 100644 index 0000000..fcd9821 --- /dev/null +++ b/tagit/tiles/tile.kv @@ -0,0 +1,45 @@ + +<Tile>: + title: '' + tooltip: '' + default_size: None, None + +<TileTabular>: + # config + font_size: sp(15) + keywidth: 0.5 + + # table + rows: rows + TileTabularLine: + id: rows + orientation: 'tb-lr' + size_hint: 1, 1 + +<TileTabularLine@StackLayout>: + spacing: 10 + +<TileTabularRow>: + orientation: 'horizontal' + size_hint: 1, None + height: self.minimum_height + spacing: 10 + +<TileTabularCell>: + valign: 'top' + height: self.texture_size[1] + text_size: self.width, None + +<TileWithLabel>: + text: '' + text_align: 'left', 'top' + font_size: sp(15) + Label: + text: root.text + markup: True + text_size: root.size + font_size: root.font_size + halign: root.text_align[0] + valign: root.text_align[1] + +## EOF ## diff --git a/tagit/tiles/tile.py b/tagit/tiles/tile.py new file mode 100644 index 0000000..981d45b --- /dev/null +++ b/tagit/tiles/tile.py @@ -0,0 +1,85 @@ +""" + +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 +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.label import Label +from kivy.uix.relativelayout import RelativeLayout +import kivy.properties as kp + +# exports +__all__ = ('Tile', 'TileWithLabel', 'TileTabular') + + +## code ## + +# load kv +Builder.load_file(os.path.join(os.path.dirname(__file__), 'tile.kv')) + +# classes +class Tile(RelativeLayout): + visible = kp.BooleanProperty(False) + root = kp.ObjectProperty(None) + + def on_visible(self, wx, visible): + if visible: + self.update() + + def update(self, *args, **kwargs): + abstract() + + +class TileWithLabel(Tile): + pass + +class TileTabularRow(BoxLayout): + pass + +class TileTabularCell(Label): + pass + +class TileTabular(Tile): + + tabledata = kp.ObjectProperty() + keywidth = kp.NumericProperty(0.5) + + def on_tabledata(self, wx, data): + # set items + self.rows.clear_widgets() + for t_key, t_value in data.items(): + # row + row = TileTabularRow() + # left column (keys) + key = TileTabularCell( + text=t_key, + halign='right', + font_size = self.font_size, + size_hint=(None, 1), + width=self.width * self.keywidth if self.keywidth < 1 else self.keywidth, + ) + # right column (values) + value = TileTabularCell( + text=str(t_value), + halign='left', + font_size = self.font_size, + size_hint=(1, None), + ) + # adjust key's width and height dynamically. + # value's width and height are adjusted automatically + self.bind(width=lambda wx, width, key=key: setattr(key, 'width', + width * self.keywidth if self.keywidth < 1 else self.keywidth)) + key.bind(height=lambda wx, height, key=key: setattr(key, 'text_size', + (key.width, height))) + # add widgets + row.add_widget(key) + row.add_widget(value) + self.rows.add_widget(row) + +## EOF ## diff --git a/tagit/utils/__init__.py b/tagit/utils/__init__.py new file mode 100644 index 0000000..daa9eab --- /dev/null +++ b/tagit/utils/__init__.py @@ -0,0 +1,23 @@ +""" + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# imports +import typing + +# inner-module imports +from . import bsfs +from . import namespaces as ns +from . import rmatcher +from . import time as ttime +from .frame import Frame +from .shared import * # FIXME: port properly + +# exports +__all__: typing.Sequence[str] = ( + 'bsfs', + ) + +## EOF ## diff --git a/tagit/utils/bsfs.py b/tagit/utils/bsfs.py new file mode 100644 index 0000000..ab8baa5 --- /dev/null +++ b/tagit/utils/bsfs.py @@ -0,0 +1,26 @@ +"""BSFS bridge, provides BSFS bindings for tagit. + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# imports +import typing + +# bsfs imports +from bsfs import schema, Open +from bsfs.namespace import Namespace +from bsfs.query import ast, matcher +from bsfs.utils import URI, uuid + +# exports +__all__: typing.Sequence[str] = ( + 'Namespace', + 'Open', + 'URI', + 'ast', + 'schema', + 'uuid' + ) + +## EOF ## diff --git a/tagit/utils/builder.py b/tagit/utils/builder.py new file mode 100644 index 0000000..f6c5818 --- /dev/null +++ b/tagit/utils/builder.py @@ -0,0 +1,82 @@ +""" + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# standard imports +from collections import abc +from functools import partial +from inspect import isclass +import typing + +# exports +__all__: typing.Sequence[str] = ( + 'BuilderBase', + 'InvalidFactoryName', + ) + + +## code ## + +class InvalidFactoryName(KeyError): pass + +class BuilderBase(abc.Mapping, abc.Hashable): + _factories = dict() + + def __getitem__(self, key): + return self.get(key) + + def __contains__(self, key): + return key in self._factories + + def __iter__(self): + return iter(self._factories.keys()) + + def __hash__(self): + return hash(frozenset(self._factories.items())) + + def __len__(self): + return len(self._factories) + + def __eq__(self, other): + return type(self) == type(other) and self._factories == other._factories + + + def get(self, key): + if key not in self._factories: + raise InvalidFactoryName(key) + return self._factories[key] + + @classmethod + def keys(self): + return self._factories.keys() + + @classmethod + def items(self): + return self._factories.items() + + @classmethod + def describe(cls, key): + if key not in cls._factories: + raise InvalidFactoryName(key) + desc = cls._factories[key].__doc__ + return desc if desc is not None else '' + + def prepare(self, key, *args, **kwargs): + # If building is to be customized, overwrite this function. + return partial(self[key], *args, **kwargs) + + def build(self, key, *args, **kwargs): + fu = self.prepare(key, *args, **kwargs) + return fu() + + def key_from_instance(self, cls): + for key, clbk in self._factories.items(): + if isclass(clbk) and isinstance(cls, clbk): + return key + if not isclass(clbk) and cls == clbk: + return key + raise KeyError(type(cls)) + +## EOF ## diff --git a/tagit/utils/errors.py b/tagit/utils/errors.py new file mode 100644 index 0000000..8b5e21a --- /dev/null +++ b/tagit/utils/errors.py @@ -0,0 +1,60 @@ +"""Module-wide errors. + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# exports +__all__ = ( + 'EmptyFileError', + 'LoaderError', + 'NotAFileError', + 'ProgrammingError', + 'UserError', + 'abstract', + ) + + +## code ## + +def abstract(): + """Marks that a method has to be implemented in a child class.""" + raise NotImplementedError('abstract method that must be implemented in a subclass') + +class ProgrammingError(Exception): + """Reached a program state that shouldn't be reachable.""" + pass + +class UserError(ValueError): + """Found an illegal value that was specified directly by the user.""" + pass + +class NotAFileError(OSError): + """A file-system object is not a regular file.""" + pass + +class EmptyFileError(OSError): + """A file is unexpectedly empty.""" + pass + +class LoaderError(Exception): + """Failed to load or initialize a critical data structure.""" + pass + +class ParserFrontendError(Exception): + """Generic parser frontend error.""" + pass + +class ParserBackendError(Exception): + """Generic parser backend error.""" + pass + +class ParserError(Exception): + """String parsing failure.""" + pass + +class BackendError(Exception): + """Generic backend error.""" + pass + +## EOF ## diff --git a/tagit/utils/frame.py b/tagit/utils/frame.py new file mode 100644 index 0000000..c6bdc1e --- /dev/null +++ b/tagit/utils/frame.py @@ -0,0 +1,84 @@ +""" + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# standard imports +import json + +# exports +__all__ = ('Frame', ) + + +## code ## + +class Frame(dict): + def __init__(self, cursor=None, selection=None, offset=0, **kwargs): + super(Frame, self).__init__(**kwargs) + selection = selection if selection is not None else [] + self['cursor'] = cursor + self['selection'] = selection + self['offset'] = offset + + @property + def selection(self): + return self['selection'] + + @property + def cursor(self): + return self['cursor'] + + @property + def offset(self): + return self['offset'] + + def copy(self): + return Frame(**super(Frame, self).copy()) + + def serialize(self): + return json.dumps({ + 'cursor': self.cursor.guid if self.cursor is not None else 'None', + 'group': self.cursor.group if hasattr(self.cursor, 'group') else 'None', + 'selection': [img.guid for img in self.selection], + 'offset': self.offset + }) + + @staticmethod + def from_serialized(lib, serialized, ignore_errors=True): + d = json.loads(serialized) + + # load cursor + cursor = None + try: + if 'cursor' in d and d['cursor'] is not None and d['cursor'].lower() != 'none': + cursor = lib.entity(d['cursor']) + except KeyError as err: + if not ignore_errors: + raise err + + if 'group' in d and d['group'] is not None and d['group'].lower() != 'none': + try: + # FIXME: late import; breaks module dependency structure + from tagit.storage.library.entity import Representative + cursor = Representative.Representative(lib, d['group']) + except ValueError: + # group doesn't exist anymore; ignore + pass + + # load selection + selection = [] + for guid in d.get('selection', []): + try: + selection.append(lib.entity(guid)) + except KeyError as err: + if not ignore_errors: + raise err + + return Frame( + cursor = cursor, + selection = selection, + offset = d.get('offset', 0) + ) + +## EOF ## diff --git a/tagit/utils/namespaces.py b/tagit/utils/namespaces.py new file mode 100644 index 0000000..a17a927 --- /dev/null +++ b/tagit/utils/namespaces.py @@ -0,0 +1,41 @@ +"""Default namespaces used throughout tagit. + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# standard imports +import typing + +# inner-module imports +from . import bsfs as _bsfs + +# generic namespaces +xsd = _bsfs.Namespace('http://www.w3.org/2001/XMLSchema')() + +# core bsfs namespaces +bsfs = _bsfs.Namespace('https://schema.bsfs.io/core') +bsie = _bsfs.Namespace('https://schema.bsfs.io/ie') + +# auxiliary bsfs namespaces +bsn = bsie.Node +bse = bsn.Entity() +bsg = bsn.Group() +bsl = bsfs.Literal +bsp = bsn.Preview() +bst = bsn.Tag() + +# export +__all__: typing.Sequence[str] = ( + 'bse', + 'bsfs', + 'bsg', + 'bsie', + 'bsl', + 'bsn', + 'bsp', + 'bst', + 'xsd', + ) + +## EOF ## diff --git a/tagit/utils/rmatcher.py b/tagit/utils/rmatcher.py new file mode 100644 index 0000000..b5bb802 --- /dev/null +++ b/tagit/utils/rmatcher.py @@ -0,0 +1,53 @@ +""" + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# exports +__all__ = ( + 'by_area', + 'by_area_min', + ) + + +## code ## + +def by_area(target, candidates): + """Pick the item from *candidates* whose area is most similar to *target*.""" + target_area = target[0] * target[1] + scores = [ + (key, abs(target_area - res[0] * res[1])) + for key, res in candidates + ] + best_key, best_score = min(scores, key=lambda key_score: key_score[1]) + return best_key + + +def by_area_min(target, candidates): + """Pick the item from *candidates* whose area is at least that of *target*.""" + # rank the candidates by area difference + # a positive score means that the candidate is larger than the target. + target_area = target[0] * target[1] + scores = [(key, res[0] * res[1] - target_area) for key, res in candidates] + + # identify the two items with + # a) the smallest positive score (kmin), or + # b) the largest negative score (kmax) + kmin, kmax = None, None + cmin, cmax = float('inf'), float('-inf') + for key, score in scores: + if score >= 0 and score < cmin: + kmin, cmin = key, score + elif score < 0 and score > cmax: + kmax, cmax = key, score + + # prefer positive over negative scores + if cmin < float('inf'): + return kmin + if cmax > float('-inf'): + return kmax + # no viable resolution found + raise IndexError('list contains no valid element') + +## EOF ## diff --git a/tagit/utils/shared.py b/tagit/utils/shared.py new file mode 100644 index 0000000..b5ab421 --- /dev/null +++ b/tagit/utils/shared.py @@ -0,0 +1,155 @@ +# FIXME: port properly! +"""Shared functionality. + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# standard imports +from collections import namedtuple +import logging +import os +import pkgutil +import platform +import re +import typing +import warnings + +# exports +__all__: typing.Sequence[str] = ( + 'Resolution', + 'Struct', + 'clamp', + 'fileopen', + 'flatten', + 'fst', + 'import_all', + 'is_hex', + 'is_list', + 'magnitude_fmt', + 'truncate_dir', + 'get_root', + ) + + +## code ## + +logger = logging.getLogger(__name__) + +fst = lambda lst: lst[0] + +def is_list(cand): + """Return true if *cand* is a list, a set, or a tuple""" + return isinstance(cand, list) or isinstance(cand, set) or isinstance(cand, tuple) + +def import_all(module, exclude=None, verbose=False): + """Recursively import all submodules of *module*. + *exclude* is a set of submodule names which will + be omitted. With *verbose*, all imports are logged + with level info. Returns all imported modules. + + >>> import tagit + >>> import_all(tagit, exclude={'tagit.shared.external'}) + + """ + exclude = set([] if exclude is None else exclude) + imports = [] + for importer, name, ispkg in pkgutil.iter_modules(module.__path__, module.__name__ + '.'): + if ispkg and all(re.match(exl, name) is None for exl in exclude): + if verbose: + logger.info(f'importing: {name}') + try: + module = __import__(name, fromlist='dummy') + imports.append(module) + imports += import_all(module, exclude, verbose) + except Exception as e: + logger.error(f'importing: {name}') + + return imports + +def clamp(value, hi, lo=0): + """Restrain a *value* to the range *lo* to *hi*.""" + return max(lo, min(hi, value)) + +Resolution = namedtuple('resolution', ('width', 'height')) + +def truncate_dir(path, cutoff=3): + """Remove path up to last *cutoff* directories""" + if cutoff < 0: raise ValueError('path cutoff must be positive') + dirs = os.path.dirname(path).split(os.path.sep) + last_dirs = dirs[max(0, len(dirs) - cutoff):] + prefix = '' + if os.path.isabs(path) and len(last_dirs) == len(dirs): + prefix = os.path.sep + + return prefix + os.path.join(*(last_dirs + [os.path.basename(path)])) + +def magnitude_fmt(num, suffix='iB', scale=1024): + """Human-readable number format. + + adapted from Sridhar Ratnakumar, 2009 + https://stackoverflow.com/questions/1094841/reusable-library-to-get-human-readable-version-of-file-size + """ + for unit in ['','K','M','G','T','P','E','Z']: + if abs(num) < scale: + return "%3.1f%s%s" % (num, unit, suffix) + num /= scale + return "%.1f%s%s" % (num, 'Y', suffix) + +class Struct(dict): + """Dict with item access as members. + + >>> tup = Struct(timestamp=123, series=['1','2','3']) + >>> tup.timestamp + 123 + >>> tup['timestamp'] + 123 + + """ + def __getattr__(self, name): + return self[name] + def __setattr__(self, name, value): + self[name] = value + +def flatten(lst): + flat = [] + for itm in lst: + flat.extend(list(itm)) + return flat + +def is_hex(string): + """Return True if the *string* can be interpreted as a hex value.""" + try: + int(string, 16) + return True + except ValueError: + return False + except TypeError: + return False + +def fileopen(pth): + """Open a file in the preferred application as a subprocess. + This operation is platform dependent. + """ + try: + binopen = { + "Linux" : "xdg-open", # Linux + "darwin" : "open", # MAX OS X + "Windows" : "start", # Windows + }.get(platform.system()) + subprocess.call((binopen, pth)) + except KeyError: + warnings.warn('Unknown platform {}'.format(platform.system())) + + +def get_root(obj): + """Traverse the widget tree upwards until the root is found.""" + while obj.parent is not None and obj.parent != obj.parent.parent: + if hasattr(obj, 'root') and obj.root is not None: + return obj.root + + obj = obj.parent + + return obj + +## EOF ## diff --git a/tagit/utils/time.py b/tagit/utils/time.py new file mode 100644 index 0000000..4260ac7 --- /dev/null +++ b/tagit/utils/time.py @@ -0,0 +1,63 @@ +"""Time helpers. + +* Camera local +* System local +* UTC + +Timestamp to datetime + * Timestamp + * in UTC + * Timezone + * Implicit system local timezone + * No known timezone + * Known timezone + +Datetime to timestamp + * always store as local time + * optionally with UTC offset + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# standard imports +from datetime import timezone, datetime, tzinfo, timedelta + +# exports +__all__ = ('timestamp', 'from_timestamp') + + +## code ## + +timestamp_loc = lambda dt: dt.replace(tzinfo=timezone.utc).timestamp() + +timestamp_utc = lambda dt: dt.timestamp() + +from_timestamp_loc = lambda ts: datetime.utcfromtimestamp(ts) + +from_timestamp_utc = lambda ts: datetime.fromtimestamp(ts) + +now = datetime.now + +timestamp_min = timestamp_loc(datetime.min) + +timestamp_max = timestamp_loc(datetime.max) + +def utcoffset(dt): + if dt.tzinfo is None: + return local_tzo(dt) + elif dt.tzinfo is NoTimeZone: + return None + else: + return dt.tzinfo.utcoffset(dt).total_seconds() / 3600 + +NoTimeZone = timezone(timedelta(0), 'NoTimeZone') + +def local_tzo(dt=None): + """Return the offset between the local time and UTC. + (i.e. return the x of UTC+x). + """ + dt = datetime.now() if dt is None else dt + return (timestamp_loc(dt) - dt.timestamp()) / 3600 + +## EOF ## diff --git a/tagit/widgets/__init__.py b/tagit/widgets/__init__.py new file mode 100644 index 0000000..2899f85 --- /dev/null +++ b/tagit/widgets/__init__.py @@ -0,0 +1,10 @@ +""" + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# inner-module imports +from .bindings import Binding + +## EOF ## diff --git a/tagit/widgets/bindings.py b/tagit/widgets/bindings.py new file mode 100644 index 0000000..3192c4e --- /dev/null +++ b/tagit/widgets/bindings.py @@ -0,0 +1,278 @@ +"""Configurable keybindings. + +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 import errors + +# exports +__all__: typing.Sequence[str] = ( + 'Binding', + ) + + +## code ## + +class Binding(object): + """Handle keybindings. + + A keybinding is a set of three constraints: + * Key code + * Inclusive modifiers + * Exclusive modifiers + + Inclusive modifiers must be present, exclusive ones must not be present. + Modifiers occuring in neither of the two lists are ignored. + + Modifiers are always lowercase strings. Additionally to SHIFT, CTRL and ALT, + the modifiers "all" and "rest" can be used. + "all" is a shortcut for all of the modifiers known. + "rest" means all modifiers not consumed by the other list yet. "rest" can + therefore only occur in at most one of the lists. + + Usage example: + + >>> # From settings, with PGUP w/o modifiers as default + >>> Binding.check(evt, self.cfg("bindings", "browser", "page_prev", + ... default=Binding.simple(Binding.PGUP, None, Binding.mALL))) + + >>> # ESC or CTRL + SHIFT + a + >>> Binding.check(evt, Binding.multi((Binding.ESC, ), + ... (97, (Binding.mCTRL, Binding.mSHIFT), Binding.mREST)))) + + """ + + # Modifiers + mSHIFT = 'shift' + mCTRL = 'ctrl' + mALT = 'alt' + mCMD = 'cmd' + mALTGR = 'altgr' + mNUMLOCK = 'numlock' + mCAPSLOCK = 'capslock' + # Modifier specials + mALL = 'all' + mREST = 'rest' + # Special keys + BACKSPACE = 8 + TAB = 9 + ENTER = 13 + ESC = 27 + SPACEBAR = 32 + DEL = 127 + UP = 273 + DOWN = 274 + RIGHT = 275 + LEFT = 276 + INSERT = 277 + HOME = 278 + END = 279 + PGUP = 280 + PGDN = 281 + F1 = 282 + F2 = 283 + F3 = 284 + F4 = 285 + F5 = 286 + F6 = 287 + F7 = 288 + F8 = 289 + F9 = 290 + F10 = 291 + F11 = 292 + F12 = 293 + CAPSLOCK = 301 + RIGHT_SHIFT = 303 + LEFT_SHIFT = 304 + LEFT_CTRL = 305 + RIGHT_CTRL = 306 + ALTGR = 307 + ALT = 308 + CMD = 309 + + @staticmethod + def simple(code, inclusive=None, exclusive=None): + """Create a binding constraint.""" + # handle strings + inclusive = (inclusive, ) if isinstance(inclusive, str) else inclusive + exclusive = (exclusive, ) if isinstance(exclusive, str) else exclusive + + # handle None, ensure tuple + inclusive = tuple(inclusive) if inclusive is not None else tuple() + exclusive = tuple(exclusive) if exclusive is not None else tuple() + + # handle code + code = Binding.str_to_key(code.lower()) if isinstance(code, str) else code + if code is None: + raise errors.ProgrammingError('invalid key code') + + # build constraint + return [(code, inclusive, exclusive)] + + @staticmethod + def multi(*args): + """Return binding for multiple constraints.""" + return [Binding.simple(*arg)[0] for arg in args] + + @staticmethod + def from_string(string): + mods = (Binding.mSHIFT, Binding.mCTRL, Binding.mALT, Binding.mCMD, + Binding.mALTGR, Binding.mNUMLOCK, Binding.mCAPSLOCK) + + bindings = [] + for kcombo in (itm.strip() for itm in string.split(';')): + strokes = [key.lower().strip() for key in kcombo.split('+')] + + # modifiers; ignore lock modifiers + inc = [key for key in strokes if key in mods] + inc = [key for key in inc if key not in (Binding.mNUMLOCK, Binding.mCAPSLOCK)] + # key + code = [key for key in strokes if key not in mods] + if len(code) != 1: + raise errors.ProgrammingError('there must be exactly one key code in a keybinding') + code = Binding.str_to_key(code[0]) + if code is None: + raise errors.ProgrammingError('invalid key code') + + bindings.append((code, tuple(inc), (Binding.mREST, ))) + + return bindings + + @staticmethod + def to_string(constraints): + values = [] + for code, inc, exc in constraints: + values.append( + ' + '.join([m.upper() for m in inc] + [Binding.key_to_str(code)])) + return '; '.join(values) + + @staticmethod + def check(stroke, constraint): + """Return True if *evt* matches the *constraint*.""" + code, char, modifiers = stroke + all_ = {Binding.mSHIFT, Binding.mCTRL, Binding.mALT, Binding.mCMD, Binding.mALTGR} + for key, inclusive, exclusive in constraint: + inclusive, exclusive = set(inclusive), set(exclusive) + + if key in (code, char): # Otherwise, we don't have to process the modifiers + # Handle specials + if 'all' in inclusive: + inclusive = all_ + if 'all' in exclusive: + exclusive = all_ + if 'rest' in inclusive: + inclusive = all_ - exclusive + if 'rest' in exclusive: + exclusive = all_ - inclusive + + if (all([mod in modifiers for mod in inclusive]) and + all([mod not in modifiers for mod in exclusive])): + # Code and modifiers match + return True + + # No matching constraint found + return False + + @staticmethod + def key_to_str(code, default='?'): + if isinstance(code, str): + return code + + if 32 <= code and code <= 226 and code != 127: + return chr(code) + + return { + Binding.BACKSPACE : 'BACKSPACE', + Binding.TAB : 'TAB', + Binding.ENTER : 'ENTER', + Binding.ESC : 'ESC', + Binding.SPACEBAR : 'SPACEBAR', + Binding.DEL : 'DEL', + Binding.UP : 'UP', + Binding.DOWN : 'DOWN', + Binding.RIGHT : 'RIGHT', + Binding.LEFT : 'LEFT', + Binding.INSERT : 'INSERT', + Binding.HOME : 'HOME', + Binding.END : 'END', + Binding.PGUP : 'PGUP', + Binding.PGDN : 'PGDN', + Binding.F1 : 'F1', + Binding.F2 : 'F2', + Binding.F3 : 'F3', + Binding.F4 : 'F4', + Binding.F5 : 'F5', + Binding.F6 : 'F6', + Binding.F7 : 'F7', + Binding.F8 : 'F8', + Binding.F9 : 'F9', + Binding.F10 : 'F10', + Binding.F11 : 'F11', + Binding.F12 : 'F12', + Binding.CAPSLOCK : 'CAPSLOCK', + Binding.RIGHT_SHIFT : 'RIGHT_SHIFT', + Binding.LEFT_SHIFT : 'LEFT_SHIFT', + Binding.LEFT_CTRL : 'LEFT_CTRL', + Binding.RIGHT_CTRL : 'RIGHT_CTRL', + Binding.ALTGR : 'ALTGR', + Binding.ALT : 'ALT', + Binding.CMD : 'CMD', + }.get(code, default) + + @staticmethod + def str_to_key(char, default=None): + if isinstance(char, int): + return char + + try: + # check if ascii + code = ord(char) + if 32 <= code and code <= 226: + return code + except TypeError: + pass + + return { + 'BACKSPACE' : Binding.BACKSPACE, + 'TAB' : Binding.TAB, + 'ENTER' : Binding.ENTER, + 'ESC' : Binding.ESC, + 'SPACEBAR' : Binding.SPACEBAR, + 'DEL' : Binding.DEL, + 'UP' : Binding.UP, + 'DOWN' : Binding.DOWN, + 'RIGHT' : Binding.RIGHT, + 'LEFT' : Binding.LEFT, + 'INSERT' : Binding.INSERT, + 'HOME' : Binding.HOME, + 'END' : Binding.END, + 'PGUP' : Binding.PGUP, + 'PGDN' : Binding.PGDN, + 'F1' : Binding.F1, + 'F2' : Binding.F2, + 'F3' : Binding.F3, + 'F4' : Binding.F4, + 'F5' : Binding.F5, + 'F6' : Binding.F6, + 'F7' : Binding.F7, + 'F8' : Binding.F8, + 'F9' : Binding.F9, + 'F10' : Binding.F10, + 'F11' : Binding.F11, + 'F12' : Binding.F12, + 'CAPSLOCK' : Binding.CAPSLOCK, + 'RIGHT_SHIFT' : Binding.RIGHT_SHIFT, + 'LEFT_SHIFT' : Binding.LEFT_SHIFT, + 'LEFT_CTRL' : Binding.LEFT_CTRL, + 'RIGHT_CTRL' : Binding.RIGHT_CTRL, + 'ALTGR' : Binding.ALTGR, + 'ALT' : Binding.ALT, + 'CMD' : Binding.CMD, + }.get(char, default) + +## EOF ## diff --git a/tagit/widgets/browser.kv b/tagit/widgets/browser.kv new file mode 100644 index 0000000..63495be --- /dev/null +++ b/tagit/widgets/browser.kv @@ -0,0 +1,90 @@ +#-- #:import OpenGroup tagit.actions.grouping + +<Browser>: + root: None + spacing: 10 + size_hint: 1.0, 1.0 + page_size: self.cols * self.rows + # must not define rows and cols + +<BrowserItem>: + is_cursor: False + is_selected: False + + +<BrowserImage>: # This be an image + preview: image + + AsyncBufferImage: + id: image + size_hint: None, None + # actual size is set in code + pos: 0, 0 + # coordinates of the (actual) image's top-right corner + tr_x: self.center_x + self.texture.width / 2.0 if self.texture is not None else None + tr_y: self.center_y + self.texture.height / 2.0 if self.texture is not None else None + + # FIXME: mb/port + #OpenGroup: + # root: root.browser.root + # # positioning: + # # (1) top right corner of the root (inside root) + # #x: root.width - self.width + # #y: root.height - self.height + # # (2) top right corner of the root (inside root) + # #pos_hint: {'top': 1.0, 'right': 1.0} + # # (3) top right corner of the image (outside the image) + # #x: image.tx is not None and image.tx or float('inf') + # #y: image.ty is not None and image.ty or float('inf') + # # (4) top right corner of the image (inside root, outside the image if possible) + # tr_x: root.width - self.width + # tr_y: root.height - self.height + # x: min(self.tr_x, image.tr_x is not None and image.tr_x or float('inf')) + # y: min(self.tr_y, image.tr_y is not None and image.tr_y or float('inf')) + # + # 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 + + AsyncBufferImage: + id: image + size_hint: None, 1 + # actual size is set in code + pos: 0, 0 + + BrowserDescriptionLabel: + text: root.text + markup: True + size_hint: None, 1 + width: root.width - image.width - root.spacer - 35 + pos: root.height + root.spacer, 0 + +<AsyncBufferImage>: + mirror: False + angle: 0 + opacity: 0 + + canvas.before: + PushMatrix + Rotate: + angle: self.mirror and 180 or 0 + origin: self.center + axis: (0, 1, 0) + + Rotate: + angle: self.angle + origin: self.center + axis: (0, 0, 1) + + canvas.after: + PopMatrix + +## EOF ## diff --git a/tagit/widgets/browser.py b/tagit/widgets/browser.py new file mode 100644 index 0000000..17d99ed --- /dev/null +++ b/tagit/widgets/browser.py @@ -0,0 +1,735 @@ +""" + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# standard imports +from collections import defaultdict +from functools import reduce, partial +import io +import logging +import math +import operator +import os +import typing + +# kivy imports +from kivy.clock import Clock +from kivy.core.image.img_pil import ImageLoaderPIL +from kivy.lang import Builder +from kivy.resources import resource_find +from kivy.uix.gridlayout import GridLayout +from kivy.uix.image import AsyncImage +from kivy.uix.relativelayout import RelativeLayout +import kivy.properties as kp + +# tagit imports +from tagit import config +from tagit.external.setproperty import SetProperty +from tagit.utils import Frame, Resolution, Struct, clamp, ns, ttime, rmatcher +from tagit.utils.bsfs import ast + +# inner-module imports +from .loader import Loader +from .session import StorageAwareMixin, ConfigAwareMixin + +# exports +__all__: typing.Sequence[str] = ( + 'Browser', + ) + + +## code ## + +logger = logging.getLogger(__name__) + +# load kv +Builder.load_file(os.path.join(os.path.dirname(__file__), 'browser.kv')) + +# classes + +class ImageLoaderTagit(ImageLoaderPIL): + def load(self, filename): + data = super(ImageLoaderTagit, self).load(filename) + if len(data) > 1: + # source features multiple images + res = [(im.width, im.height) for im in data] + if len(set(res)) > 1: + # images have different resolutions; I'm guessing + # it's multiple previews embedded in the same image file. + # keep only the largest one. + idx = res.index(max(res, key=lambda wh: wh[0]*wh[1])) + data = [data[idx]] + + return data + +class ItemIndex(list): + """A list with constant time in index and contains operations. + List items must be hashable. Assumes the list is to be immutable. + Trades space for time by constructing an index and set at creation time. + """ + def __init__(self, items): + super(ItemIndex, self).__init__(items) + self._item_set = set(items) # FIXME: mb/port: collect into a nodes instance? + self._index = {itm: idx for idx, itm in enumerate(items)} + + def index(self, item): + return self._index[item] + + def __contains__(self, value): + return value in self._item_set + + def as_set(self): + return self._item_set + +class Browser(GridLayout, StorageAwareMixin, ConfigAwareMixin): + """The browser displays a grid of item previews.""" + # root reference + root = kp.ObjectProperty(None) + + # select modes + SELECT_SINGLE = 0 + SELECT_MULTI = 1 + SELECT_RANGE = 2 + SELECT_ADDITIVE = 4 + SELECT_SUBTRACTIVE = 8 + # selection extras + range_base = [] + range_origin = None + # mode + select_mode = kp.NumericProperty(SELECT_SINGLE) + + # content + change_view = kp.BooleanProperty(False) + change_grid = kp.BooleanProperty(True) + items = kp.ObjectProperty(ItemIndex([])) + folds = kp.DictProperty() + + # frame + offset = kp.NumericProperty(0) + cursor = kp.ObjectProperty(None, allownone=True) + selection = SetProperty() + + # grid mode + GRIDMODE_GRID = 'grid' + GRIDMODE_LIST = 'list' + gridmode = kp.OptionProperty('grid', options=[GRIDMODE_GRID, GRIDMODE_LIST]) + # grid size + cols = kp.NumericProperty(3) + rows = kp.NumericProperty(3) + # page_size is defined in kivy such that it updates automatically + + # delayed view update event + _draw_view_evt = None + + ## initialization + + def on_root(self, wx, root): + StorageAwareMixin.on_root(self, wx, root) + ConfigAwareMixin.on_root(self, wx, root) + + def on_config_changed(self, session, key, value): + with self: + if key == ('ui', 'standalone', 'browser', 'cols'): + self.cols = max(1, value) + elif key == ('ui', 'standalone', 'browser', 'rows'): + self.rows = max(1, value) + elif key == ('ui', 'standalone', 'browser', 'gridmode'): + self.gridmode = value + elif key == ('ui', 'standalone', 'browser', 'fold_threshold'): + self.redraw() # FIXME: redraw doesn't exist + elif key == ('ui', 'standalone', 'browser', 'select_color'): + self.change_grid = True + + def on_cfg(self, wx, cfg): + with self: + self.cols = max(1, cfg('ui', 'standalone', 'browser', 'cols')) + self.rows = max(1, cfg('ui', 'standalone', 'browser', 'rows')) + self.gridmode = cfg('ui', 'standalone', 'browser', 'gridmode') + + def on_storage(self, wx, storage): + with self: + self.frame = Frame() + self.items = ItemIndex([]) + + + ## functions + + def set_items(self, items): + """Set the items. Should be used instead of setting items directly + to get the correct folding behaviour. + """ + items, folds = self.fold(items) + self.folds = folds + self.items = ItemIndex(items) + self.change_view = True + + def fold(self, items): + """Replace items in *items* if they are grouped. + Return the new item list and the dict of representatives. + """ + # get groups and their shadow (group's members in items) + groups = defaultdict(set) + 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 + fold_threshold = self.root.session.cfg('ui', 'standalone', 'browser', 'fold_threshold') + groups = {grp: objs for grp, objs in groups.items() if len(objs) > fold_threshold} + # don't fold groups that make up all items + groups = {grp: objs for grp, objs in groups.items() if len(objs) < len(items)} + + def superset_exists(grp): + """Helper fu to detect subsets.""" + for objs in groups.values(): + if objs != groups[grp] and groups[grp].issubset(objs): + return True + return False + + # create folds + folds = { + grp.represented_by(): Struct( + group=grp, + shadow=objs, + ) + for grp, objs in groups.items() + if not superset_exists(grp) + } + + # add representatives + for rep in folds: + # add representative in place of the first of its members + idx = min([items.index(obj) for obj in folds[rep].shadow]) + items.insert(idx, rep) + + # remove folded items + for obj in {obj for fold in folds.values() for obj in fold.shadow}: + items.remove(obj) + + return items, folds + + def unfold(self, items): + """Replace group representatives by their group members.""" + # fetch each item or their shadow if applicable + unfolded = set() + for itm in items: + if itm in self.folds: + unfolded |= self.folds[itm].shadow + else: + unfolded |= {itm} + 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.""" + if self.cursor in self.selection: + # set cursor to nearest neighbor + cur_idx = self.items.index(self.cursor) + sel_idx = {self.items.index(obj) for obj in self.selection} + + # find available items + n_right = {clamp(idx + 1, self.n_items - 1) for idx in sel_idx} + n_left = {clamp(idx - 1, self.n_items - 1) for idx in sel_idx} + cand = sorted((n_left | n_right) - sel_idx) + + # find closest to cursor + c_dist = [abs(idx - cur_idx) for idx in cand] + if len(c_dist) == 0: + return None + else: + # set cursor to item at candidate with minimum distance to cursor + return self.items[cand[c_dist.index(min(c_dist))]] + + else: + # cursor isn't selected + return self.cursor + + + ## properties + + @property + def frame(self): + return Frame(self.cursor, self.selection, self.offset) + + @frame.setter + def frame(self, frame): + self.offset = frame.offset + self.cursor = frame.cursor + self.selection = frame.selection + + @property + def n_items(self): + return len(self.items) + + @property + def max_offset(self): + return max(0, + self.n_items + (self.cols - (self.n_items % self.cols)) % self.cols - self.page_size) + + ## property listeners + + def on_cols(self, sender, cols): + #self.page_size = self.cols * self.rows + self.change_grid = True + + def on_rows(self, sender, rows): + #self.page_size = self.cols * self.rows + self.change_grid = True + + def on_offset(self, sender, offset): + self.change_view = True + + def on_cursor(self, sender, cursor): + if cursor is not None: + self.root.status.dispatch('on_status', cursor.filename(default='')) + + def on_items(self, sender, items): + self.change_view = True + + # items might have changed; start caching + #if self.root.session.cfg('ui', 'standalone', 'browser', 'cache_all'): + # self._preload_all() + + def on_gridmode(self, sender, mode): + self.change_grid = True + + # resolution might have changed; start caching + #if self.root.session.cfg('ui', 'standalone', 'browser', 'cache_all'): + # self._preload_all() + + ## context + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + # ensure valid values for cursor, selection, and offset + # necessary if old frames were loaded while search filters have changed + if self.root.session.cfg('session', 'verbose') > 0: + # warn about changes + if self.cursor is not None and self.cursor not in self.items: + logger.warn(f'Fixing: cursor ({self.cursor})') + + if not self.selection.issubset(self.items.as_set()): + logger.warn('Fixing: selection') + if self.offset > self.max_offset or self.offset < 0: + logger.warn(f'Fixing: offset ({self.offset} not in [0, {self.max_offset}])') + + self.cursor = self.cursor if self.cursor in self.items else None + self.selection = self.items.as_set() & self.selection + self.offset = clamp(self.offset, self.max_offset) + + # issue redraw + if self.change_grid: + # grid change requires view change + self.draw_grid() + self.draw_view() + elif self.change_view: + timeout = self.root.session.cfg('ui', 'standalone', 'browser', 'page_delay') / 1000 + if timeout > 0: + self._draw_view_evt = Clock.schedule_once(lambda dt: self.draw_view(), timeout) + else: + self.draw_view() + + # reset flags + self.change_grid = False + self.change_view = False + + + def draw_grid(self): + if self.gridmode == self.GRIDMODE_LIST: + factory = BrowserDescription + elif self.gridmode == self.GRIDMODE_GRID: + factory = BrowserImage + else: + raise UserError(f'gridmode has to be {self.GRIDMODE_GRID} or {self.GRIDMODE_LIST}') + + self.clear_widgets() + for itm in range(self.page_size): + wx = factory(browser=self) + self.bind(selection=wx.on_selection) + self.bind(cursor=wx.on_cursor) + self.add_widget(wx) + + def _cell_resolution(self): + return Resolution(self.width/self.cols, self.height/self.rows) + + def on_change_view(self, wx, change_view): + # the view will be updated, hence preloading should be interrupted + # if it were active. That's done here since to capture the earliest + # time where a view change becomes apparent. + if change_view and self._draw_view_evt is not None: + self._draw_view_evt.cancel() + self._draw_view_evt = None + + def draw_view(self): + self._draw_view_evt = None + # revoke images that are still wait to being loaded + Loader.clear() + #if not self.root.session.cfg('ui', 'standalone', 'browser', 'cache_all'): + # Loader.clear() + + # fetch items + items = self.items[self.offset:self.offset+self.page_size] + childs = iter(self.children) # reversed since child widgets are in reverse order + + # preload neighbouring pages + n_pages = self.root.session.cfg('ui', 'standalone', 'browser', 'cache_items') + n_pages = math.ceil(n_pages / self.page_size) + if n_pages > 0: + lo = clamp(self.offset - n_pages * self.page_size, self.n_items) + cu = clamp(self.offset + self.page_size, self.n_items) + hi = clamp(self.offset + (n_pages + 1) * self.page_size, self.n_items) + # load previous page + # previous before next such that scrolling downwards is prioritised + self._preload_items(self.items[lo:self.offset]) + # load next page + # reversed such that the loader prioritises earlier previews + self._preload_items(reversed(self.items[cu:hi])) + + # clear unused cells + for _ in range(self.page_size - len(items)): + next(childs).clear() + + 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) # FIXME: empty previews + # fetch preview resolutions + res_preview = previews.get(ns.bsp.width, ns.bsp.height, node=True) + # select a preview for each item + chosen = {} + for ent in items: + try: + # get previews and their resolution for this ent + options = [] + for preview in node_preview[ent]: + # unpack resolution + res = res_preview[preview] + width = res.get(ns.bsp.width, 0) + height = res.get(ns.bsp.height, 0) + options.append((preview, Resolution(width, height))) + # select the best fitting preview + chosen[ent] = rmatcher.by_area_min(resolution, options) + except (KeyError, IndexError): + # 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 + # self._preload_items(reversed(self.items)) + + def _preload_items(self, items, resolution=None): + """Load an item into the kivy *Cache* without displaying the image anywhere.""" + 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) + + 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): + """Widget that binds to the browser.""" + browser = None + def on_root(self, wx, root): + root.bind(browser=self.on_browser) + if root.browser is not None: + # initialize with the current browser + # Going through the event dispatcher ensures that the object + # is initialized properly before on_browser is called. + Clock.schedule_once(lambda dt: self.on_browser(root, root.browser)) + + def on_browser(self, sender, browser): + pass + + +class BrowserItem(RelativeLayout): + """Just some space for an object.""" + browser = kp.ObjectProperty() + obj = kp.ObjectProperty(allownone=True) + is_cursor = kp.BooleanProperty(False) + is_selected = kp.BooleanProperty(False) + is_group = kp.BooleanProperty(False) + + def update(self, obj): + self.obj = obj + + def clear(self): + self.obj = None + + def on_obj(self, wx, obj): + self.on_cursor(self.browser, self.browser.cursor) + self.on_selection(self.browser, self.browser.selection) + self.is_group = obj in self.browser.folds if obj is not None else False + + def on_cursor(self, browser, cursor): + self.is_cursor = (cursor == self.obj) if self.obj is not None else False + + def on_selection(self, browser, selection): + self.is_selected = self.obj in selection if self.obj is not None else False + + def on_touch_down(self, touch): + """Click on item.""" + if self.obj is not None and self.collide_point(*touch.pos): + if touch.button == 'left': + # shift counts as double tap + if touch.is_double_tap and not self.browser.root.keys.shift_pressed: + # open + logger.debug('Item: Double touch in {}'.format(str(self.obj))) + if not self.is_selected: + self.browser.root.trigger('Select', self.obj) + self.browser.root.trigger('OpenExternal') + else: + # set cursor + logger.debug('Item: Touchdown in {}'.format(str(self.obj))) + self.browser.root.trigger('SetCursor', self.obj) + + # must call the parent's method to ensure OpenGroup gets a chance to handle + # the mouse event. Also, this must happen *after* processing the event here + # so that the cursor is set correctly. + return super(BrowserItem, self).on_touch_down(touch) + + def on_touch_move(self, touch): + """Move over item.""" + if self.obj is not None and self.collide_point(*touch.pos): + if touch.button == 'left': + if not self.collide_point(*touch.ppos): + self.browser.root.trigger('Select', self.obj) + return super(BrowserItem, self).on_touch_move(touch) + + +class BrowserImage(BrowserItem): + def update(self, obj, buffer, source): + super(BrowserImage, self).update(obj) + self.preview.load_image(buffer, source, 1) + #self.preview.load_image(buffer, source, obj.orientation(default=1)) # FIXME: mb/port + + self.preview.set_size(self.size) + + def clear(self): + super(BrowserImage, self).clear() + self.preview.clear_image() + + def on_size(self, wx, size): + self.preview.set_size(self.size) + + +class BrowserDescription(BrowserItem): + text = kp.StringProperty() + + def update(self, obj, buffer, source): + super(BrowserDescription, self).update(obj) + self.preview.load_image(buffer, source, 1) + #self.preview.load_image(buffer, source, obj.orientation(default=1)) # FIXME: mb/port + self.preview.set_size((self.height, self.height)) + + def clear(self): + super(BrowserDescription, self).clear() + self.preview.clear_image() + + def on_size(self, wx, size): + self.preview.set_size((self.height, self.height)) + + def on_obj(self, wx, obj): + super(BrowserDescription, self).on_obj(wx, obj) + if self.is_group: + # get group and its members + 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.bsn.Entity, + ast.filter.Any(ns.bse.group, ast.filter.Is(grp))) + # get group member's tags + member_tags = members.tag.label(node=True) + tags_all = set.intersection(*member_tags.values()) + tags_any = {tag for tags in member_tags.values() for tag in tags} + # get remaining info from representative + preds = self.obj.get( + ns.bse.mime, + ns.bsm.t_created, + ) + self.text = '{name} [size=20sp]x{count}[/size], {mime}, {time}\n[color=8f8f8f][b]{tags_all}[/b], [size=14sp]{tags_any}[/size][/color]'.format( + name=os.path.basename(next(iter(grp.guids))), + count=len(members), + mime=preds.get(ns.bse.mime, ''), + time=ttime.from_timestamp_loc( + preds.get(ns.bsm.t_created, ttime.timestamp_min)).strftime('%Y-%m-%d %H:%M'), + tags_all=', '.join(sorted(tags_all)), + tags_any=', '.join(sorted(tags_any - tags_all)), + ) + elif self.obj is not None: + preds = self.obj.get( + ns.bse.filename, + ns.bse.filesize, + ns.bse.mime, + ns.bsm.t_created, + (ns.bse.tag, ns.bst.label), + ) + self.text = '[size=20sp]{filename}[/size], [size=18sp][i]{mime}[/i][/size] -- {time} -- {filesize}\n[color=8f8f8f][size=14sp]{tags}[/size][/color]'.format( + filename=preds.get(ns.bse.filename, 'n/a'), + mime=preds.get(ns.bse.mime, ''), + time=ttime.from_timestamp_loc( + preds.get(ns.bsm.t_created, ttime.timestamp_min)).strftime('%Y-%m-%d %H:%M'), + filesize=preds.get(ns.bse.filesize, 0), + tags=', '.join(sorted(preds.get((ns.bse.tag, ns.bst.label), []))), + ) + else: + self.text = '' + + +class AsyncBufferImage(AsyncImage): + """Replacement for kivy.uix.image.AsyncImage that allows to pass a *load_callback* + method. The load_callback (fu(filename) -> ImageLoaderTagit) can be used to read a file + from something else than a path. However, note that if caching is desired, a filename + (i.e. source) should still be given. + """ + orientation = kp.NumericProperty(1) + buffer = kp.ObjectProperty(None, allownone=True) + mirror = kp.BooleanProperty(False) + angle = kp.NumericProperty(0) + + def load_image(self, buffer, source, orientation): + self.orientation = orientation + self.buffer = buffer + # triggers actual loading + self.source = source + # make visible + self.opacity = 1 + + def clear_image(self): + # make invisible + self.opacity = 0 + + def set_size(self, size): + width, height = size + # swap dimensions if the image is rotated + self.size = (height, width) if self.orientation in (5,6,7,8) else (width, height) + # ensure the correct positioning via the center + self.center = width / 2.0, height / 2.0 + # note that the widget's bounding box will be overlapping with other grid + # cells, however the content will be confined in the correct grid box. + + def on_orientation(self, wx, orientation): + if orientation in (2, 4, 5, 7): # Mirror + self.mirror = True + if orientation in (3, 4): # Rotate 180deg + self.angle = 180 + elif orientation in (5, 6): # Rotate clockwise, 90 deg + self.angle = -90 + elif orientation in (7, 8): # Rotate counter-clockwise, 90 deg + self.angle = 90 + else: + self.angle = 0 + self.mirror = False + + @staticmethod + def 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) + + def _load_source(self, *args): + # overwrites method from parent class + source = self.source + if not source: + if self._coreimage is not None: + self._coreimage.unbind(on_texture=self._on_tex_change) + self._coreimage.unbind(on_load=self.post_source_load) + self.texture = None + self._coreimage = None + else: + if self._coreimage is not None: + # unbind old image + self._coreimage.unbind(on_load=self._on_source_load) + self._coreimage.unbind(on_error=self._on_source_error) + self._coreimage.unbind(on_texture=self._on_tex_change) + del self._coreimage + self._coreimage = None + + self._coreimage = image = Loader.image(self.source, + nocache=self.nocache, mipmap=self.mipmap, + anim_delay=self.anim_delay, + load_callback=partial(self.loader, self.buffer), # mb: pass load_callback + ) + + # bind new image + image.bind(on_load=self._on_source_load) + image.bind(on_error=self._on_source_error) + image.bind(on_texture=self._on_tex_change) + self.texture = image.texture + + +## config ## + +config.declare(('ui', 'standalone', 'browser', 'cols'), config.Unsigned(), 3, + __name__, 'Browser columns', 'Default number of columns in the browser. Is at least one. Note that an odd number of columns and rows gives a more intuitive behaviour when zooming. A large value might make the program respond slowly.') + +config.declare(('ui', 'standalone', 'browser', 'rows'), config.Unsigned(), 3, + __name__, 'Browser rows', 'Default number of rows in the grid view. Is at least one. Note that an odd number of columns and rows gives a more intuitive behaviour when zooming. A large value might make the program respond slowly.') + +config.declare(('ui', 'standalone', 'browser', 'fold_threshold'), config.Unsigned(), 1, + __name__, 'Folding', "Define at which threshold groups will be folded. The default (1) folds every group unless it consists of only a single item (which isn't really a group anyhow).") + +config.declare(('ui', 'standalone', 'browser', 'gridmode'), + config.Enum(Browser.GRIDMODE_GRID, Browser.GRIDMODE_LIST), Browser.GRIDMODE_GRID, + __name__, 'Display style', 'The grid mode shows only the preview image of each item. The list mode shows the preview and some additional information of each item. Note that rows and cols can be specified for both options. It is recommended that they are set to the same value in grid mode, and to a single column in list mode.') + +config.declare(('ui', 'standalone', 'browser', 'cache_items'), config.Unsigned(), 20, + __name__, 'Page pre-loading', 'Number of items that are loaded into the cache before they are actually shown. The effective number of loaded items the specified value rounded up to the page size times two (since it affects pages before and after the current one). E.g. a value of one loads the page before and after the current one irrespective of the page size. If zero, preemptive caching is disabled.') + +config.declare(('ui', 'standalone', 'browser', 'page_delay'), config.Unsigned(), 50, + __name__, 'Page setup delay', 'Quickly scrolling through pages incurs an overhead due to loading images that will be discarded shortly afterwards. This overhead can be reduced by delaying the browser page setup for a short amount of time. If small enough the delay will not be noticable. Specify in milliseconds. Set to zero to disable the delay completely.') + +# FIXME: Also add select_alpha or maybe even select_style (left/right/over/under bar; overlay; recolor; others?) +# FIXME: Also add cursor style config (left/right/under/over bar; borders; others?) +config.declare(('ui', 'standalone', 'browser', 'select_color'), + config.List(config.Unsigned()), [0,0,1], + __name__, '', '') # FIXME + +#config.declare(('ui', 'standalone', 'browser', 'cache_all'), config.Bool(), False, +# __name__, 'Cache everything', 'Cache all preview images in the background. The cache size (`ui.standalone.browser.cache_size`) should be large enough to hold the library at least once (some reserve for different resolutions is advised). Can incur a small delay when opening the library. May consume a lot of memory.') + +## EOF ## diff --git a/tagit/widgets/context.kv b/tagit/widgets/context.kv new file mode 100644 index 0000000..75f5267 --- /dev/null +++ b/tagit/widgets/context.kv @@ -0,0 +1,25 @@ +#:import ContextMenu tagit.external.kivy_garden.contextmenu.ContextMenu + +<Context>: + menu: context_menu + visible: False + # the root widget should set these two to itself + bounding_box_widget: self + cancel_handler_widget: self + # button config + button_width: 200 + button_height: dp(35) + button_show: 'text', 'image' + + ContextMenu: # the actual menu + id: context_menu + visible: False + cancel_handler_widget: root + bounding_box_widget: root.bounding_box_widget + width: root.button_width + +<ContextMenuAction>: + width: self.parent.width if self.parent else 0 + size_hint: 1, None + +## EOF ## diff --git a/tagit/widgets/context.py b/tagit/widgets/context.py new file mode 100644 index 0000000..2affbed --- /dev/null +++ b/tagit/widgets/context.py @@ -0,0 +1,148 @@ +""" + +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.lang import Builder +from kivy.uix.floatlayout import FloatLayout +import kivy.properties as kp + +# tagit imports +from tagit import config +from tagit.utils.builder import InvalidFactoryName +from tagit.actions import ActionBuilder +from tagit.external.kivy_garden.contextmenu import ContextMenuItem, AbstractMenuItemHoverable, ContextMenuTextItem, ContextMenu + +# inner-module imports +from .dock import DockBase + +# exports +__all__ = ('Context', ) + + +## code ## + +logger = logging.getLogger(__name__) + +# load kv +Builder.load_file(os.path.join(os.path.dirname(__file__), 'context.kv')) + +# classes +class ContextMenuAction(ContextMenuItem, AbstractMenuItemHoverable): + """Wraps a context menu item around an action buttons.""" + # menu requirements + submenu_postfix = kp.StringProperty(' ...') + color = kp.ListProperty([1,1,1,1]) + # action requirements + action = kp.ObjectProperty(None) + hide_fu = kp.ObjectProperty(None) + + @property + def content_width(self): + """Forward the width from the action button.""" + if self.action is None: + return 0 + return self.action.width + + def set_action(self, action): + """Add the action button.""" + self.add_widget(action) + self.action = action + return self + + def on_touch_up(self, touch): + """Close the menu when an action is triggered.""" + if self.collide_point(*touch.pos) and \ + touch.button == 'left' and \ + self.hide_fu is not None: + self.action.on_release() + self.hide_fu() + return super(ContextMenuAction, self).on_touch_up(touch) + + +class Context(FloatLayout, DockBase): + """Context menu.""" + root = kp.ObjectProperty(None) + + def show(self, x, y): + """Open the menu.""" + self.menu.show(x, y) + + def on_touch_down(self, touch): + """Open the menu via click.""" + if touch.button == 'right': + self.show(*touch.pos) + return super(Context, self).on_touch_down(touch) + + def on_config_changed(self, session, key, value): + if key == ('ui', 'standalone', 'context'): + self.on_cfg(session, session.cfg) + + def on_cfg(self, wx, cfg): + """Construct the menu from config.""" + self.populate(cfg('ui', 'standalone', 'context')) + + def populate(self, actions): + """Construct the menu.""" + # clear old menu items + childs = [child for child in self.menu.children if isinstance(child, ContextMenuTextItem)] + childs += [child for child in self.menu.children if isinstance(child, ContextMenuAction)] + for child in childs: + self.menu.remove_widget(child) + + # add new menu items + builder = ActionBuilder() + for menu, args in actions.items(): + if menu == 'root': + # add directly to the context menu + wx = self.menu + else: + # create and add a submenu + head = ContextMenuTextItem(text=menu) + self.menu.add_widget(head) + wx = ContextMenu(width=self.button_width) + head.add_widget(wx) + wx._on_visible(False) + + for action in args: + try: + cls = builder.get(action) + if action == 'SortKey': + # special case: add as submenu + btn = cls(root=self.root) + head = ContextMenuTextItem(text=btn.text) + wx.add_widget(head) + head.add_widget(btn.menu) + btn.menu._on_visible(False) + + else: + wx.add_widget(ContextMenuAction( + # args to the action wrapper + hide_fu=self.menu.hide, + height=self.button_height, + ).set_action(cls( + # args to the button + root=self.root, + autowidth=False, + size=(self.button_width, self.button_height), + size_hint=(1, None), + show=self.button_show, + ))) + + except InvalidFactoryName: + logger.error(f'invalid button name: {action}') + + +## config ## + +config.declare(('ui', 'standalone', 'context'), + config.Dict(config.String(), config.List(config.Enum(set(ActionBuilder.keys())))), {}, + __name__, 'Context menu structure', 'The context menu consists of groups of actions, similar to the button dock. Each group consists of a name and a list of actions. To add actions to the menu directly, use "root" for the group name.', '{"root": ["ShowDashboard", "ShowBrowsing"], "search": ["GoBack", "GoForth"]}') + +## EOF ## diff --git a/tagit/widgets/dock.kv b/tagit/widgets/dock.kv new file mode 100644 index 0000000..4d82ac3 --- /dev/null +++ b/tagit/widgets/dock.kv @@ -0,0 +1,20 @@ +#:import TileDecorationVanilla tagit.tiles.decoration.TileDecorationVanilla + +<TileDock>: + cols: 3 + rows: 3 + decoration: TileDecorationVanilla + visible: False + tile_height: None + tile_width: None + name: '' + +<ButtonDock>: + orientation: 'lr-tb' + button_height: 30 + button_width: self.button_height + button_show: 'image', + n_buttons_max: None + name: '' + +## EOF ## diff --git a/tagit/widgets/dock.py b/tagit/widgets/dock.py new file mode 100644 index 0000000..41ff642 --- /dev/null +++ b/tagit/widgets/dock.py @@ -0,0 +1,239 @@ +""" + +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.lang import Builder +from kivy.uix.gridlayout import GridLayout +from kivy.uix.stacklayout import StackLayout +from kivy.uix.widget import Widget +import kivy.properties as kp + +# tagit imports +from tagit import config +from tagit.actions import ActionBuilder +from tagit.tiles import TileBuilder +from tagit.utils import errors +from tagit.utils.builder import InvalidFactoryName + +# inner-module imports +from .session import ConfigAwareMixin + +# exports +__all__ = ('Dock', ) + + +## code ## + +logger = logging.getLogger(__name__) + +# load kv +Builder.load_file(os.path.join(os.path.dirname(__file__), 'dock.kv')) + +# classes +class DockBase(Widget, ConfigAwareMixin): + """A Dock is a container that holds configurable items.""" + # root reference + root = kp.ObjectProperty(None) + + def on_cfg(self, wx, cfg): + """Construct the dock from config.""" + errors.abstract() + + def populate(self, config): + """Fill the dock with content.""" + errors.abstract() + + +class TileDock(GridLayout, DockBase): + """A TileDock holds a number of Tiles.""" + + # dock's name for loading from config + name = kp.StringProperty('') + # tile decoration + decoration = kp.ObjectProperty(None) + # tile visiblity + visible = kp.BooleanProperty(False) + + def on_config_changed(self, session, key, value): + if key == ('ui', 'standalone', 'tiledocks'): + self.on_cfg(session, session.cfg) + + def on_cfg(self, wx, cfg): + """Construct the Tiles from the config item matching dock's name.""" + if self.name != '': + self.populate(cfg('ui', 'standalone', 'tiledocks').get(self.name, {})) + # FIXME: Since dictionaries are not ordered, the tiles might change + # their position at every application start. Switching to a list would + # solve this issue. E.g. [{tile: 'tile name', **kwargs}] + + def populate(self, tiles): + """Construct the Tiles.""" + # clear old items + self.clear_widgets() + + # add new items + n_tiles_max = self.cols * self.rows + builder = TileBuilder() + for idx, tid in enumerate(sorted(tiles)): + if idx >= n_tiles_max: + logger.warn(f'number of tiles exceeds space ({len(tiles)} > {n_tiles_max})') + break + + try: + kwargs = tiles[tid] + tile = builder.build(tid, root=self.root, **kwargs) + self.add_widget(self.decoration(client=tile)) + except InvalidFactoryName: + logger.error(f'invalid tile name: {tid}') + + # create and attach widgets before setting visibility + # to ensure that the widget initialization has finished. + self.on_visible(self, self.visible) + + def on_size(self, *args): + # FIXME: If dashboard is loaded, resizing the window becomes painfully slow. + # Something to do with the code here, e.g. delayed sizing? + for child in self.children: + # TODO: Allow default_size or tile_size to specify relative sizes (<1) + # determine size + width = self.tile_width + width = child.default_size[0] if width is None else width + #width = self.width if width is None and self.size_hint_x is None else width + height = self.tile_height + height = child.default_size[1] if height is None else height + #height = self.height if height is None and self.size_hint_y is None else height + size = width if width is not None else 1, height if height is not None else 1 + size_hint = None if width is not None else 1, None if height is not None else 1 + # set size; will be propagated from the decorator to the client + child.size = size + child.size_hint = size_hint + + def on_visible(self, wx, visible): + """Propagate visibility update to Tiles.""" + for child in self.children: + child.client.visible = visible + + # FIXME: move events in the browser are only triggered if the move event is also + # handled here with an empty body (no super!). + # No idea why this happens (e.g. doing it in desktop or tab doesn't work). + def on_touch_move(self, touch): + pass + + +class ButtonDock(StackLayout, DockBase): + """A ButtonDock holds a number of Actions.""" + + # dock's name for loading from config + name = kp.StringProperty('') + + def on_config_changed(self, session, key, value): + if key == ('ui', 'standalone', 'buttondocks'): + self.on_cfg(session, session.cfg) + + def on_cfg(self, wx, cfg): + """Construct the Actions from config item matching the dock's name.""" + if self.name != '': + # name is empty if created via the Buttons tile + self.populate(cfg('ui', 'standalone', 'buttondocks').get(self.name, [])) + + def populate(self, actions): + """Construct the Actions.""" + # clear old items + self.clear_widgets() + + # add new items + n_buttons_max = float('inf') if self.n_buttons_max is None else self.n_buttons_max + builder = ActionBuilder() + for idx, action in enumerate(actions): + if idx >= n_buttons_max: + logger.warn(f'number of buttons exceeds space ({len(actions)} > {n_buttons_max})') + break + + try: + self.add_widget(builder.build(action, + root=self.root, + size=(self.button_width, self.button_height), + show=self.button_show, + autowidth=False, + )) + except InvalidFactoryName: + logger.error(f'invalid button name: {action}') + + +class KeybindDock(DockBase): + """The KeybindDock holds a number of invisible Actions that can be triggered by key presses.""" + + def on_config_changed(self, session, key, value): + if key == ('ui', 'standalone', 'keytriggers'): + self.on_cfg(session, session.cfg) + + def on_cfg(self, wx, cfg): + """Construct the Actions from config.""" + self.populate(cfg('ui', 'standalone', 'keytriggers')) + + def populate(self, actions): + """Construct the Actions.""" + # clear old items + self.clear_widgets() + + # add new items + builder = ActionBuilder() + for action in actions: + try: + self.add_widget(builder.build( + action, + root=self.root, + # process key events only + touch_trigger=False, + key_trigger=True, + # no need to specify show (default is empty) + )) + + except InvalidFactoryName: + logger.error(f'invalid button name: {action}') + + +## config ## + +config.declare(('ui', 'standalone', 'keytriggers'), + config.List(config.Enum(set(ActionBuilder.keys()))), [], + __name__, 'Key triggers', + 'Actions that can be triggered by a key but have no visible button', '') + +config.declare(('ui', 'standalone', 'tiledocks'), + config.Dict(config.String(), config.Dict(config.String(), config.Dict(config.String(), config.Any()))), {}, + __name__, 'Tile docks', '''Tiles can be placed in several locations of the UI. A tile usually displays some information about the current program state, such as information about the library in general, visible or selected items, etc. + +The configuration of a tile consists the its name as string and additional parameters to that tile as a dict. A tile dock is configured by a dictionary with the tile names as key and their parameters as value: + +{ + "Hints": {}, + "ButtonDock": {"buttons: ["Save", "SaveAs", "Index"]} +} + +The order of the items in the UI is generally the same as in the config dict. + +To show a list of available tiles, execute: + +$ tagger info tile + +''') + +config.declare(('ui', 'standalone', 'buttondocks'), + config.Dict(config.String(), config.List(config.Enum(set(ActionBuilder.keys())))), {}, + __name__, 'Buttons', '''Every possible action in the UI is triggered via a button. Hence, buttons are found in various places in the UI, organized in button docks. Each dock is identified by name and lists the names of the buttons it contains. + +To show a list of available buttons, execute: + +$ tagger info action + +''') + +## EOF ## diff --git a/tagit/widgets/filter.kv b/tagit/widgets/filter.kv new file mode 100644 index 0000000..5407610 --- /dev/null +++ b/tagit/widgets/filter.kv @@ -0,0 +1,89 @@ +#:import SearchmodeSwitch tagit.actions.filter +#:import AddToken tagit.actions.filter +#-- #:import SortKey tagit.actions.search + +<Filter>: + root: None + orientation: 'horizontal' + spacing: 5 + tokens: tokens + + 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 + + # FIXME: Temporarily disabled + #SearchmodeSwitch: + # show: 'image', + # root: root.root + + #SortKey: + # show: 'image', + # root: root.root + + SortOrder: + show: 'image', + root: root.root + + ButtonDock: + root: root.root + name: 'filter' + orientation: 'lr-tb' + # 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 + 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 + active: root.active + width: (self.texture_size[0] + dp(20)) if self.text != '' else 0 + size_hint_x: None + +<Addressbar>: + multiline: False + +## EOF ## diff --git a/tagit/widgets/filter.py b/tagit/widgets/filter.py new file mode 100644 index 0000000..1382c43 --- /dev/null +++ b/tagit/widgets/filter.py @@ -0,0 +1,334 @@ +""" + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# standard imports +from functools import partial +import logging +import os + +# kivy imports +from kivy.clock import Clock +from kivy.config import Config as KivyConfig +from kivy.lang import Builder +from kivy.uix.behaviors import ButtonBehavior +from kivy.uix.behaviors import FocusBehavior +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.image import Image +from kivy.uix.textinput import TextInput +import kivy.properties as kp + +# tagit imports +from tagit import config +from tagit.utils import bsfs, errors, ns +from tagit.utils.bsfs import ast, matcher + +# inner-module imports +from .session import ConfigAwareMixin + +# exports +__all__ = ('Filter', ) + + +## code ## + +logger = logging.getLogger(__name__) + +# load kv +Builder.load_file(os.path.join(os.path.dirname(__file__), 'filter.kv')) + +# classes +class Filter(BoxLayout, ConfigAwareMixin): + """ + A filter tracks a sequence of searches building on top of each other. Each + item in that sequence is defined by a part of the overall search query + (token). In addition, the filter also tracks the viewport at each point in + the sequence (frames). + + In addition, the sequence can be navigated back-and-forth, so that the + current search includes a number of items, starting at the front, but not + necessarily all. Hence, some tokens are present in the current + search (head), while others are not (tail). + """ + # root reference + root = kp.ObjectProperty(None) + + # change notification + changed = kp.BooleanProperty(False) + run_search = kp.BooleanProperty(False) + + # appearance + MODE_SHINGLES = 'shingles' + MODE_ADDRESS = 'address' + searchmode = kp.OptionProperty(MODE_SHINGLES, options=[MODE_SHINGLES, MODE_ADDRESS]) + + ''' + To track head, tail, tokens, and frames, four properties are used for + the relevant pairwise combinations. + + For heads, the frame is the last known viewport before applying the + next filter token. I.e. f_head[1] corresponds to the search including + tokens t_head[:1]. The viewport of the current search is maintained + in the browser. + + For tails, the frame is the last viewport before switching to the previous + filter token. I.e. f_tail[1] corresponds to the search including + tokens t_tail[:2] (i.e. the lists are aligned). + + Consider the following scheme. + The current search is indicated by the "v". The first search includes + no tokens (all items). Note the offset between tokens and frames in + the head part. + + v + view 0 1 2 3 4 + token - 0 1 2 3 0 1 + frame 0 1 2 3 - 0 1 + + Although the lists are not necessarily aligned, they always have to have + the same size. This constraint is enforced. + + ''' + # tokens + t_head = kp.ListProperty() + t_tail = kp.ListProperty() + + # frames + f_head = kp.ListProperty() + f_tail = kp.ListProperty() + + # sort + #sortkey = kp.ObjectProperty(partial(ast.NumericalSort, 'time')) + sortkey = kp.ObjectProperty(None) # FIXME: mb/port + sortdir = kp.BooleanProperty(False) # False means ascending + + + ## exposed methods + + def get_query(self): + query = bsfs.ast.filter.And(self.t_head[:]) if len(self.t_head) > 0 else None + sort = None + return query, sort + # FIXME: mb/port.parsing + query = ast.AND(self.t_head[:]) if len(self.t_head) else None + # sort order is always set to False so that changing the sort order + # won't trigger a new query which can be very expensive. The sort + # order is instead applied in uix.kivy.actions.search.Search. + sort = self.sortkey(False) if self.sortkey is not None else None + 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 '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 '=' + 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 '—' + 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()[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() + searchbar = Addressbar(self.t_head, root=self.root) + self.tokens.add_widget(searchbar) + searchbar.focus = True + + + ## initialization + + def on_config_changed(self, session, key, value): + if key == ('ui', 'standalone', 'filter', 'searchbar'): + self.on_cfg(session, session.cfg) + + def on_cfg(self, wx, cfg): + with self: + self.searchmode = cfg('ui', 'standalone', 'filter', 'searchbar') + + ## filter as context + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + if not(len(self.t_head) == len(self.f_head)): + raise errors.ProgrammingError('head sizes differ') + if not(len(self.t_tail) == len(self.f_tail)): + raise errors.ProgrammingError('tail sizes differ') + + # issue redraw + if self.changed: + self.redraw() + # issue search + if self.run_search: + self.root.trigger('Search') + + def redraw(self): + self.tokens.clear_widgets() + if self.searchmode == self.MODE_ADDRESS: + # add address bar + self.tokens.add_widget(Addressbar(self.t_head, root=self.root)) + + elif self.searchmode == self.MODE_SHINGLES: + # add shingles + for tok in self.t_head + self.t_tail: + self.tokens.add_widget( + Shingle( + tok, + active=(tok in self.t_head), + avatar=self.abbreviate(tok), + text=self.tok_label(tok), + root=self.root + )) + + ## property access + + def on_t_head(self, sender, t_head): + self.changed = True + self.run_search = True + + def on_t_tail(self, sender, t_tail): + self.changed = True + + def on_searchmode(self, sender, mode): + self.changed = True + + def on_sortdir(self, sender, sortdir): + self.run_search = True + + def on_sortkey(self, sender, sortkey): + self.run_search = True + + +class FilterAwareMixin(object): + """Tile that binds to the filter.""" + filter = None + def on_root(self, wx, root): + root.bind(filter=self.on_filter) + if root.filter is not None: + # initialize with the current filter + # Going through the event dispatcher ensures that the object + # is initialized properly before on_filter is called. + Clock.schedule_once(lambda dt: self.on_filter(root, root.filter)) + + def on_filter(self, sender, filter): + pass + + +class Shingle(BoxLayout): + """A sequence of filter tokens. Tokens can be edited individually.""" + # root reference + root = kp.ObjectProperty(None) + + # content + active = kp.BooleanProperty(False) + text = kp.StringProperty('') + avatar = kp.StringProperty('') + + # touch behaviour + _single_tap_action = None + + def __init__(self, token, **kwargs): + super(Shingle, self).__init__(**kwargs) + self.token = token + + def remove(self, *args, **kwargs): + """Remove shingle.""" + self.root.trigger('RemoveToken', self.token) + + def on_touch_down(self, touch): + """Edit shingle when touched.""" + 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 + if self._single_tap_action is not None: + self._single_tap_action.cancel() + self._single_tap_action = None + FocusBehavior.ignored_touch.append(touch) + self.root.trigger('EditToken', self.token) + return True + else: # jump to filter + # delay executing the action until we're sure it's not a double tap + self._single_tap_action = Clock.schedule_once( + lambda dt: self.root.trigger('JumpToToken', self.token), + KivyConfig.getint('postproc', 'double_tap_time') / 1000) + return True + + return super(Shingle, self).on_touch_down(touch) + +class Addressbar(TextInput): + """An address bar where a search query can be entered and edited. + Edits are accepted by pressing Enter and rejected by pressing Esc. + """ + # root reference + root = kp.ObjectProperty() + + def __init__(self, tokens, **kwargs): + super(Addressbar, self).__init__(**kwargs) + self.text = self.root.session.filter_to_string(bsfs.ast.filter.And(tokens)) + self._last_text = self.text + + def on_text_validate(self): + """Accept text as search string.""" + self.root.trigger('SetToken', self.text) + self._last_text = self.text + + def on_keyboard(self, *args, **kwargs): + """Block key propagation to other widgets.""" + return True + + def on_focus(self, wx, focus): + from kivy.core.window import Window + if focus: + # fetch keyboard + Window.bind(on_keyboard=self.on_keyboard) + # keep a copy of the current text + self._last_text = self.text + else: + # release keyboard + Window.unbind(on_keyboard=self.on_keyboard) + # set last accepted text + self.text = self._last_text + + +## config ## + +config.declare(('ui', 'standalone', 'filter', 'searchbar'), + config.Enum('shingles', 'address'), 'shingles', + __name__, 'Searchbar mode', 'Show either list of shingles, one per search token, or a freely editable address bar.') + +## EOF ## diff --git a/tagit/widgets/keyboard.py b/tagit/widgets/keyboard.py new file mode 100644 index 0000000..2cae7d6 --- /dev/null +++ b/tagit/widgets/keyboard.py @@ -0,0 +1,142 @@ +""" + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# kivy imports +from kivy.uix.widget import Widget +import kivy.properties as kp + +# exports +__all__ = [] + + +## code ## + +class Keyboard(Widget): + """Captures key events and turns them into simplified events. + Keeps a record of currently pressed modifiers (CTRL, SHIFT, etc.). + """ + + # modifiers + MODIFIERS_NONE = 0b00000 # 0 + MODIFIERS_CTRL = 0b00001 # 1 + MODIFIERS_SHIFT = 0b00010 # 2 + MODIFIERS_ALT = 0b00100 # 4 + MODIFIERS_ALTGR = 0b01000 # 8 + MODIFIERS_CMD = 0b10000 # 16 + + # modifier keymaps + keymap = { + 303: MODIFIERS_SHIFT, # right shift + 304: MODIFIERS_SHIFT, # left shift + 305: MODIFIERS_CTRL, # left ctrl + 306: MODIFIERS_CTRL, # right ctrl + 307: MODIFIERS_ALTGR, + 308: MODIFIERS_ALT, + 309: MODIFIERS_CMD, # a.k.a. windows key + } + + modemap = { + MODIFIERS_SHIFT: (303, 304), + MODIFIERS_CTRL: (305, 306), + MODIFIERS_ALTGR: (307, ), + MODIFIERS_ALT: (308, ), + MODIFIERS_CMD: (309, ), + } + + # current mode + mode = kp.NumericProperty(MODIFIERS_NONE) + + # state access via properties + + @property + def none_pressed(self): + return self.mode & self.MODIFIERS_NONE + + @property + def ctrl_pressed(self): + return self.mode & self.MODIFIERS_CTRL + + @property + def shift_pressed(self): + return self.mode & self.MODIFIERS_SHIFT + + @property + def alt_pressed(self): + return self.mode & self.MODIFIERS_ALT + + @property + def altgr_pressed(self): + return self.mode & self.MODIFIERS_ALTGR + + @property + def cmd_pressed(self): + return self.mode & self.MODIFIERS_CMD + + + ## outbound events + + __events__ = ('on_press', 'on_release') + + def on_press(sender, evt): + """Key press event prototype.""" + pass + + def on_release(sender, evt): + """Key release event prototype.""" + pass + + + ## event rewriting + + def __init__ (self, **kwargs): + super(Keyboard, self).__init__(**kwargs) + # keybindings + from kivy.core.window import Window + Window.bind(on_key_up=self.on_key_up) + Window.bind(on_key_down=self.on_key_down) + Window.bind(on_keyboard=self.on_keyboard) + + def __del__(self): + from kivy.core.window import Window + Window.unbind(on_key_up=self.on_key_up) + Window.unbind(on_key_down=self.on_key_down) + Window.unbind(on_keyboard=self.on_keyboard) + + def on_key_up(self, wx, key, scancode): + """Record modifier release.""" + mode = self.keymap.get(key, self.MODIFIERS_NONE) + self.mode -= self.mode & mode + self.dispatch('on_release', key) + + def on_key_down(self, wx, key, scancode, char, modifiers): + """Record modifiers press.""" + mode = self.keymap.get(key, self.MODIFIERS_NONE) + self.mode |= mode + + def on_keyboard(self, wx, key, scancode, char, modifiers): + """Forward key presses Handles keybindings. Is called when a key press is detected. + + *key* : ASCII or ASCII-like value + *scancode* : Key code returned by the input provider (e.g. keyboard) + *char* : String representation (if A-Z, a-z) + *modifiers* : 'ctrl', 'shift', 'alt', or any combination thereof, if pressed + + """ + if False: + # print key event for debugging + print(f"""Keybindings: Event + Key : {key} + Scancode : {scancode} + Codepoint : {char} + Modifiers : {modifiers} + """) + + # forward compact event to widgets + self.dispatch('on_press', (key, char, modifiers)) + # prevent further event propagation + return True + +## EOF ## diff --git a/tagit/widgets/loader.py b/tagit/widgets/loader.py new file mode 100644 index 0000000..9c0ffaf --- /dev/null +++ b/tagit/widgets/loader.py @@ -0,0 +1,200 @@ +""" + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# standard imports +import time +import typing + +# kivy imports +from kivy.cache import Cache +from kivy.clock import Clock +from kivy.compat import queue +from kivy.loader import _Worker, LoaderThreadPool, ProxyImage, LoaderBase + +# exports +__all__: typing.Sequence[str] = ( + 'Loader', + ) + + +## code ## + +class _ThreadPool(object): + """Pool of threads consuming tasks from a queue. + Identical to kivy.loader._ThreadPool except for the queue type.""" + def __init__(self, num_threads): + super(_ThreadPool, self).__init__() + self.running = True + self.tasks = queue.LifoQueue() # mb: replace Queue with LifoQueue + for _ in range(num_threads): + _Worker(self, self.tasks) + + def add_task(self, func, *args, **kargs): + self.tasks.put((func, args, kargs)) + + def stop(self): + self.running = False + self.tasks.join() + + +class TagitImageLoader(LoaderThreadPool): + """Threaded Loader that prioritises recentness. + This is useful if a user skips through browser pages because then the preview loading + finishes only after the user has already switched to the next page. Instead of waiting + until all images up to the target page were loaded, prioritsation makes more recent + images to load first. + + Mostly copied from kivy.loader.Loader. + """ + def start(self): + LoaderBase.start(self) # mb: skip LoaderThreadPool.start + self.pool = _ThreadPool(self._num_workers) + Clock.schedule_interval(self.run, 0) + + def image(self, filename, load_callback=None, post_callback=None, + **kwargs): + data = Cache.get('kv.loader', filename) + if data not in (None, False): + # found image, if data is not here, need to reload. + return ProxyImage(data, + loading_image=self.loading_image, + loaded=True, **kwargs) + + client = ProxyImage(self.loading_image, + loading_image=self.loading_image, **kwargs) + self._client.append((filename, client)) + + if data is None: + # if data is None, this is really the first time + self._q_load.appendleft({ + 'filename': filename, + 'load_callback': load_callback, + 'post_callback': post_callback, + 'request_time': Clock.get_time(), # mb: also pass time of original request + 'kwargs': kwargs}) + if not kwargs.get('nocache', False): + Cache.append('kv.loader', filename, False) + self._start_wanted = True + self._trigger_update() + else: + # already queued for loading + pass + + return client + + def _clear(self): + if self.pool is not None: + tbr = set() + + # clear loader queue + while len(self._q_load): + kargs = self._q_load.pop() + tbr.add(kargs['filename']) + + # clear task queue + while not self.pool.tasks.empty(): + func, args, kargs = self.pool.tasks.get() + if len(args) and 'filename' in args[0]: + tbr.add(args[0]['filename']) + self.pool.tasks.task_done() + + # remove spurious entries from cache + for key in tbr: + # remove directly from Cache if _clear is run from the main thread + Cache.remove('kv.loader', key) + # otherwise go via _q_done + #self._q_done.appendleft(key, None, 0)) + + # remove spurious clients + for key in ((name, client) for name, client in self._client if name in tbr): + self._client.remove(key) + + def clear(self): + """Empty the queue without loading the images.""" + # execute in main thread + self._clear() + # schedule as event (no real benefit) + #if self.pool is not None: + # self.pool.add_task(self._clear) + + def _load(self, kwargs): + while len(self._q_done) >= ( + self.max_upload_per_frame * self._num_workers): + time.sleep(0.1) + + self._wait_for_resume() + + filename = kwargs['filename'] + load_callback = kwargs['load_callback'] + post_callback = kwargs['post_callback'] + try: + proto = filename.split(':', 1)[0] + except: + # if blank filename then return + return + if load_callback is not None: + data = load_callback(filename) + elif proto in ('http', 'https', 'ftp', 'smb'): + data = self._load_urllib(filename, kwargs['kwargs']) + else: + data = self._load_local(filename, kwargs['kwargs']) + + if post_callback: + data = post_callback(data) + + # mb: also pass request_time + self._q_done.appendleft((filename, data, kwargs['request_time'])) + self._trigger_update() + + def _update(self, *largs): + # want to start it ? + if self._start_wanted: + if not self._running: + self.start() + self._start_wanted = False + + # in pause mode, don't unqueue anything. + if self._paused: + self._trigger_update() + return + + for x in range(self.max_upload_per_frame): + try: + filename, data, timestamp = self._q_done.pop() + except IndexError: + return + + # create the image + image = data # ProxyImage(data) + + if image is None: # mb: discard items + # remove cache and client entries + Cache.remove('kv.loader', filename) + for key in ((name, client) for name, client in self._client if name == filename): + self._client.remove(key) + continue + + if not image.nocache: + Cache.append('kv.loader', filename, image) + # mb: fix cache times + Cache._objects['kv.loader'][filename]['lastaccess'] = timestamp + Cache._objects['kv.loader'][filename]['timestamp'] = timestamp + + # update client + for c_filename, client in self._client[:]: + if filename != c_filename: + continue + # got one client to update + client.image = image + client.loaded = True + client.dispatch('on_load') + self._client.remove((c_filename, client)) + + self._trigger_update() + +Loader = TagitImageLoader() + +## EOF ## diff --git a/tagit/widgets/session.py b/tagit/widgets/session.py new file mode 100644 index 0000000..30dfe51 --- /dev/null +++ b/tagit/widgets/session.py @@ -0,0 +1,162 @@ +""" + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# standard imports +import os +import typing + +# kivy imports +from kivy.clock import Clock +from kivy.uix.widget import Widget +import kivy.properties as kp + +# tagit imports +from tagit import parsing +from tagit.config.loader import load_settings +from tagit.utils import bsfs + +# exports +__all__: typing.Sequence[str] = ( + 'ConfigAwareMixin', + 'Session', + ) + + +## code ## + +class Session(Widget): + storage = kp.ObjectProperty(None) + cfg = kp.ObjectProperty(None) + + __events__ = ('on_storage_modified', 'on_predicate_modified', 'on_config_changed') + + def __init__(self, cfg, storage, log, **kwargs): + super(Session, self).__init__(**kwargs) + self.cfg = cfg + self.storage = storage + self.log = log + # derived members + self.filter_from_string = parsing.filter.FromString(self.storage.schema) + self.filter_to_string = parsing.filter.ToString(self.storage.schema) + #self.sort_from_string = parsing.Sort(self.storage.schema) # FIXME: mb/port/parsing + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + def clone(self, cfg): + """Clone the session and load the clone.""" + # clone storages to new location + liburi = cfg('session', 'paths', 'library') + numuri = cfg('session', 'paths', 'numerical') + storage = Broker.Clone(self.storage, liburi, numuri, None, cfg) + log = load_log(cfg) # not cloned + # switch to new storage + self.cfg = cfg + self.log = log + self.storage = storage + + def load(self, cfg): + """Load the session from configuration *cfg*.""" + #self.log = load_log(cfg) # FIXME: mb/port + # initialize storages from config + # open BSFS storage + store = bsfs.Open(cfg('session', 'bsfs')) + # check storage schema + with open(resource_find('required_schema.nt'), 'rt') as ifile: + required_schema = bsfs.schema.from_string(ifile.read()) + 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 + + def update_settings_key(self, key, value): + # change setting + self.cfg.set(key, value) + + # update settings file + # FIXME: file_connected is also true if it loaded config from user home! + if self.cfg.file_connected() and self.cfg('storage', 'config', 'write_through'): + # store only difference to baseline (i.e. session config) + local_config = self.cfg.diff(load_settings()) + local_config.save() + + # trigger update event + self.dispatch('on_config_changed', key, value) + + def on_config_changed(sender, key, value): + """Event prototype.""" + pass + + def on_storage(self, wx, storage): + # fire event if the storage was replaced + self.dispatch('on_storage_modified') + + def on_storage_modified(sender): + """Event prototype. + Triggered when items are added or removed + """ + pass + + def on_predicate_modified(sender, predicate, objects, diff): + """Event prototype. + Triggered when a predicate to one or several objects have been changed. + """ + pass + +class StorageAwareMixin(object): + def on_root(self, wx, root): + session = root.session + # storage has been changed as a whole + session.bind(storage=self.on_storage) + # some parts of the storage have changed + session.bind(on_storage_modified=self.on_storage_modified) + session.bind(on_predicate_modified=self.on_predicate_modified) + if session.storage is not None: + # initialize with the current storage + # Going through the event dispatcher ensures that the object + # is initialized properly before on_storage is called. + Clock.schedule_once(lambda dt: self.on_storage(session, session.storage)) + + def on_storage(self, sender, storage): + """Default event handler.""" + pass + + def on_storage_modified(self, sender): + """Default event handler.""" + pass + + def on_predicate_modified(self, sender, predicate, objects, diff): + """Default event handler.""" + pass + +class ConfigAwareMixin(object): + def on_root(self, wx, root): + session = root.session + # config changes as a whole + session.bind(cfg=self.on_cfg) + # individual config entries have been changed + session.bind(on_config_changed=self.on_config_changed) + if session.cfg is not None: + # initialize with the current config + # Going through the event dispatcher ensures that the object + # is initialized properly before on_cfg is called. + Clock.schedule_once(lambda dt: self.on_cfg(session, session.cfg)) + + def on_config_changed(self, sender, key, value): + """Default event handler.""" + pass + + def on_cfg(self, sender, cfg): + """Default event handler.""" + pass + +## EOF ## diff --git a/tagit/widgets/status.kv b/tagit/widgets/status.kv new file mode 100644 index 0000000..0a680ab --- /dev/null +++ b/tagit/widgets/status.kv @@ -0,0 +1,63 @@ +#-- #: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: '' + navigation: '' + status_label: status_label + navigation_label: navigation_label + + ButtonDock: + root: root.root + orientation: 'lr-tb' + size_hint: None, 1 + name: 'navigation_left' + # space for three buttons + width: 3*30 + 2*5 + spacing: 5 + button_height: 30 + button_show: 'image', + + NavigationLabel: + id: navigation_label + size_hint: None, 1 + width: 180 + text: root.navigation + + ButtonDock: + root: root.root + size_hint: None, 1 + orientation: 'lr-tb' + name: 'navigation_right' + # space for three buttons + width: 3*30 + 2*5 + spacing: 5 + button_height: 30 + button_show: 'image', + + StatusLabel: + # gets remaining size + id: status_label + text_size: self.size + text: root.status + + ButtonDock: + root: root.root + orientation: 'lr-tb' + size_hint: None, 1 + name: 'status' + # space for three buttons + width: 3*30 + 2*5 + spacing: 5 + button_height: 30 + button_show: 'image', + +## EOF ## diff --git a/tagit/widgets/status.py b/tagit/widgets/status.py new file mode 100644 index 0000000..e83b8d8 --- /dev/null +++ b/tagit/widgets/status.py @@ -0,0 +1,206 @@ +"""Status line. + +Provides space for some buttons (typically navigation buttons), +information about the current viewport, and a status line. + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# standard imports +import os +import logging + +# kivy imports +from kivy.clock import mainthread +from kivy.lang import Builder +from kivy.uix.boxlayout import BoxLayout +import kivy.properties as kp + +# tagit imports +from tagit import config, dialogues, logger + +# inner-module imports +from .browser import BrowserAwareMixin +from .session import ConfigAwareMixin + +# exports +__all__ = ('Status', ) + + +## code ## + +# load kv +Builder.load_file(os.path.join(os.path.dirname(__file__), 'status.kv')) + +# classes +class Status(BoxLayout, BrowserAwareMixin, ConfigAwareMixin): + """Status line.""" + # root reference + root = kp.ObjectProperty(None) + # log history + history = kp.ListProperty() + # log handlers + handler_history = None + handler_status = None + + # events + + __events__ = ('on_status', ) + + def on_status(sender, status): + """Event prototype""" + pass + + + # bindings to others + + def on_root(self, wx, root): + """Bind events.""" + # bind to browser and config + BrowserAwareMixin.on_root(self, wx, root) + ConfigAwareMixin.on_root(self, wx, root) + # bind to status update event + self.bind(on_status=self.status_from_event) + + def on_browser(self, wx, browser): + """Bind to current browser properties.""" + # remove old binding + if self.browser is not None: + self.browser.unbind(page_size=self.on_navigation) + self.browser.unbind(items=self.on_navigation) + self.browser.unbind(offset=self.on_navigation) + # add new binding + self.browser = browser + if self.browser is not None: + self.browser.bind(page_size=self.on_navigation) + self.browser.bind(items=self.on_navigation) + self.browser.bind(offset=self.on_navigation) + self.on_navigation(browser, browser.offset) + + def on_config_changed(self, session, key, value): + if key in (('ui', 'standalone', 'logging', 'status'), + ('ui', 'standalone', 'logging', 'console')): + self.on_cfg(session, session.cfg) + + def on_cfg(self, wx, cfg): + """Register handlers according to config.""" + if self.handler_status is not None: + logging.getLogger().root.removeHandler(self.handler_status) + if self.handler_history is not None: + logging.getLogger().root.removeHandler(self.handler_history) + + # status log event + self.handler_status = logger.logger_config( + logger.CallbackHandler(self.status_from_log), + logger.ColorsMarkup, + cfg('ui', 'standalone', 'logging', 'status').to_tree(defaults=True)) + logging.getLogger().root.addHandler(self.handler_status) + + # history (console) + self.handler_history = logger.logger_config( + logger.CallbackHandler(self.update_history), + logger.ColorsMarkup, + cfg('ui', 'standalone', 'logging', 'console').to_tree(defaults=True)) + logging.getLogger().root.addHandler(self.handler_history) + + def __del__(self): + if self.browser is not None: + self.browser.unbind(page_size=self.on_navigation) + self.browser.unbind(items=self.on_navigation) + self.browser.unbind(offset=self.on_navigation) + self.browser = None + + if self.handler_status is not None: + logging.getLogger().root.removeHandler(self.handler_status) + self.handler_status = None + + if self.handler_history is not None: + logging.getLogger().root.removeHandler(self.handler_history) + self.handler_history = None + + + # console + + def on_touch_down(self, touch): + """Open console dialogue when clicked on the status label.""" + if self.status_label.collide_point(*touch.pos): + self.console() # show console + return True + elif self.navigation_label.collide_point(*touch.pos): + self.root.trigger('JumpToPage') # show page dialogue + return True + return super(Status, self).on_touch_down(touch) + + def console(self): + """Open console dialogue.""" + dlg = dialogues.Console() + self.bind(history=dlg.update) + dlg.update(self, self.history) + dlg.open() + + + # content updates + + def on_navigation(self, browser, value): + """Update the navigation label if the browser changes.""" + first = browser.offset + 1 # first on page + last = min(browser.offset + browser.page_size, browser.n_items) # last on page + total = browser.n_items # total results + self.navigation = f'{first} - {last} of {total}' + + @mainthread + def update_history(self, fmt, record): + """Update the history from the logger.""" + self.history.append(fmt(record)) + + def status_from_event(self, wx, status): + """Update the status line from the status event.""" + self.status = status + + @mainthread + def status_from_log(self, fmt, record): + """Update the status line from the logger.""" + self.status = fmt(record) + + +## config ## + +# status +config.declare(('ui', 'standalone', 'logging', 'status', 'level'), + config.Enum('debug', 'info', 'warn', 'error', 'critical'), 'info', + __name__, 'Log level', 'Maximal log level for which messages are shown. The order is: critical > error > warning > info > volatile > debug') + +config.declare(('ui', 'standalone', 'logging', 'status', 'filter'), + config.List(config.String()), ['tagit'], + __name__, 'Module filter', 'Module name for which log messages are accepted.') + +config.declare(('ui', 'standalone', 'logging', 'status', 'fmt'), config.String(), '{title}{message}', + __name__, 'Log format', 'Log message formatting. The formatting follows the typical python format string where each item is enclosed in curly braces (e.g. "{message}"). For printable items see the `logging attributes <https://docs.python.org/3.6/library/logging.html#logrecord-attributes>`_. In addition to those, the item "title" is available, which is a placeholder for the formatted title (if present).') + +config.declare(('ui', 'standalone', 'logging', 'status', 'title'), config.String(), '{title}: ', + __name__, 'Title format', 'Title formatting.') + +config.declare(('ui', 'standalone', 'logging', 'status', 'maxlen'), config.Unsigned(), 40, + __name__, 'Maximal line length', 'Maximal line length (e.g. console width). Use Infinity to set no line length limit.') + +# console +config.declare(('ui', 'standalone', 'logging', 'console', 'level'), + config.Enum('debug', 'info', 'warn', 'error', 'critical'), 'info', + __name__, 'Log level', 'Maximal log level for which messages are shown. The order is: critical > error > warning > info > volatile > debug') + +config.declare(('ui', 'standalone', 'logging', 'console', 'filter'), + config.List(config.String()), ['tagit'], + __name__, 'Module filter', 'Module name for which log messages are accepted.') + +config.declare(('ui', 'standalone', 'logging', 'console', 'fmt'), + config.String(), '[{levelname}] {title}{message}', + __name__, 'Log format', 'Log message formatting. The formatting follows the typical python format string where each item is enclosed in curly braces (e.g. "{message}"). For printable items see the `logging attributes <https://docs.python.org/3.6/library/logging.html#logrecord-attributes>`_. In addition to those, the item "title" is available, which is a placeholder for the formatted title (if present).') + +config.declare(('ui', 'standalone', 'logging', 'console', 'title'), config.String(), '[{title}]', + __name__, 'Title format', 'Title formatting.') + +config.declare(('ui', 'standalone', 'logging', 'console', 'maxlen'), config.Unsigned(), 0, + __name__, 'Maximal line length', 'Maximal line length (e.g. console width). Use zero or infinity to set no line length limit.') + +## EOF ## diff --git a/tagit/windows/__init__.py b/tagit/windows/__init__.py new file mode 100644 index 0000000..c3ec3c0 --- /dev/null +++ b/tagit/windows/__init__.py @@ -0,0 +1,10 @@ +""" + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# inner-module imports +from .desktop import MainWindow + +## EOF ## diff --git a/tagit/windows/desktop.kv b/tagit/windows/desktop.kv new file mode 100644 index 0000000..d2ca0e7 --- /dev/null +++ b/tagit/windows/desktop.kv @@ -0,0 +1,107 @@ +#:import TileDecorationRoundedBorder tagit.tiles.decoration.TileDecorationRoundedBorder +#:import TileDecorationBorder tagit.tiles.decoration.TileDecorationBorder +#:import TileDecorationFilledRectangle tagit.tiles.decoration.TileDecorationFilledRectangle + +<HGuide@Widget>: + + +<MainWindow>: + # main content + # required by most tiles and actions + browser: browser + filter: filter + status: status + # required by Menu + context: context + + BoxLayout: + orientation: 'vertical' + + Widget: + height: 5 + size_hint: 1, None + + Filter: + id: filter + root: root + size_hint: 1, None + height: 40 + + HGuide: + height: 20 + size_hint: 1, None + + Widget: # spacer + height: 20 + size_hint: 1, None + + BoxLayout: + orientation: 'horizontal' + + ButtonDock: # one column of buttons on the left + root: root + orientation: 'lr-tb' + # one column of buttons + width: 1*30 + 2*10 + name: 'sidebar_left' + spacing: 10 + padding: 10 + size_hint: None, None + button_height: 30 + button_show: 'image', + # adjust height automatically to content + height: self.minimum_height + pos_hint: {'center_y': 0.5} + + Widget: # spacer + width: 20 # ButtonDock already has a space of 10px + size_hint: None, 1 + + Browser: # browsing space + id: browser + root: root + size_hint: 1, 1 + + Widget: # spacer + width: 30 + size_hint: None, 1 + + TileDock: # context info to the right + root: root + name: 'sidebar_right' + decoration: TileDecorationRoundedBorder + visible: True + cols: 1 + rows: 1 + width: 220 + size_hint: None, 0.5 + pos_hint: {'center_y': 0.5} + + Widget: # spacer + height: 20 + size_hint: 1, None + + HGuide: + height: 20 + size_hint: 1, None + + Status: + id: status + root: root + size_hint: 1, None + height: 30 + + Context: # context menu + id: context + root: root + cancel_handler_widget: root + bounding_box_widget: root + name: 'context' + + KeybindDock: + # key-only actions + root: root + size_hint: None, None + size: 0, 0 + +## EOF ## diff --git a/tagit/windows/desktop.py b/tagit/windows/desktop.py new file mode 100644 index 0000000..2c087b2 --- /dev/null +++ b/tagit/windows/desktop.py @@ -0,0 +1,151 @@ +"""Main container of the tagit UI. + +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 typing + +# kivy imports +from kivy.clock import Clock +from kivy.lang import Builder +from kivy.uix.floatlayout import FloatLayout +import kivy.properties as kp + +# import Image and Loader to overwrite their caches later on +from kivy.cache import Cache +from kivy.loader import Loader +from kivy.resources import resource_find + +# tagit imports +from tagit import actions, config, dialogues +from tagit.widgets.browser import Browser +from tagit.widgets.context import Context +from tagit.widgets.dock import TileDock, ButtonDock, KeybindDock +from tagit.widgets.filter import Filter +from tagit.widgets.keyboard import Keyboard +from tagit.widgets.session import Session +from tagit.widgets.status import Status + +# exports +__all__: typing.Sequence[str] = ( + 'KIVY_IMAGE_CACHE_SIZE', + 'KIVY_IMAGE_CACHE_TIMEOUT', + 'MainWindow', + ) + + +## code ## + +logger = logging.getLogger(__name__) + +# load kv +Builder.load_file(os.path.join(os.path.dirname(__file__), 'desktop.kv')) +# load styles +Builder.load_file(resource_find('default/style.kv')) + +# classes +class MainWindow(FloatLayout): + """A self-contained user interface for desktop usage. + See `tagit.apps.gui` for an example of how to invoke it. + """ + + keys = kp.ObjectProperty(None) + + # unnecessary but nicely explicit + browser = kp.ObjectProperty(None) + filter = kp.ObjectProperty(None) + keytriggers = kp.ObjectProperty(None) + + # FIXME: log actions and and replay them + action_log = kp.ListProperty() + + def __init__ (self, cfg, stor, log, **kwargs): + # initialize the session + self._session = Session(cfg, stor, log) + # initialize key-only actions + self.keys = Keyboard() + + # initialize the cache + cache_size = max(0, cfg('ui', 'standalone', 'browser', 'cache_size')) + cache_size = cache_size if cache_size > 0 else None + cache_timeout = max(0, cfg('ui', 'standalone', 'browser', 'cache_timeout')) + cache_timeout = cache_timeout if cache_timeout > 0 else None + Cache.register('kv.loader', limit=cache_size, timeout=cache_timeout) + + # initialize the widget + super(MainWindow, self).__init__(**kwargs) + + # bind pre-close checks + from kivy.core.window import Window + Window.bind(on_request_close=self.on_request_close) + Window.size = tuple(cfg('ui', 'standalone', 'window_size')) + if cfg('ui', 'standalone', 'maximize'): + Window.maximize() + + + ## properties + + @property + def session(self): + return self._session + + def trigger(self, action, *args, **kwargs): + """Trigger an action once.""" + actions.ActionBuilder().get(action).single_shot(self, *args, **kwargs) + + + ## startup and shutdown + + def on_startup(self): + # run script + for args in self.session.cfg('session', 'script'): + if isinstance(args, str): + cmd, args = args, [] + else: + cmd = args.pop(0) + Clock.schedule_once( + lambda dt, cmd=cmd, args=args: self.trigger(cmd, *args), + self.session.cfg('session', 'script_delay')) + + # FIXME: mb/port: debugging only + #return + #Clock.schedule_once(lambda dt: self.trigger('Search'), 0) + #Clock.schedule_once(lambda dt: self.trigger('ZoomOut'), 0) + #Clock.schedule_once(lambda dt: self.trigger('ZoomOut'), 0) + #from kivy.app import App + #App.get_running_app().stop() + + def on_request_close(self, *args): + #with open('.action_history', 'a') as ofile: + # for itm in self.action_log: + # ofile.write(f'{itm}\n') + #App.get_running_app().stop() # FIXME: mb/port: from CloseSessionAndExit + return False + + +## config ## + +config.declare(('session', 'script'), config.List(config.Any()), [], + __name__, 'start script', 'Actions to run after startup. Intended for testing.') + +config.declare(('session', 'script_delay'), config.Unsigned(), 0, + __name__, 'script delay', 'Start script execution delay in seconds.') + +config.declare(('ui', 'standalone', 'maximize'), config.Bool(), False, + __name__, 'Window maximization', 'Maximize the window upon startup.') + +config.declare(('ui', 'standalone', 'window_size'), config.List(config.Unsigned()), (1024, 768), + __name__, 'Wndow size', 'Set the window size upon startup.') + +config.declare(('ui', 'standalone', 'browser', 'cache_size'), config.Unsigned(), 1000, + __name__, 'Cache size', 'Number of preview images that are held in the cache. Should be high or zero if memory is not an issue. Set to a small value to preserve memory, but should be at least the most common page size. It is advised to set a value in accordance with `ui.standalone.browser.cache_items`. If zero, no limit applies.') + +config.declare(('ui', 'standalone', 'browser', 'cache_timeout'), config.Unsigned(), 0, + __name__, 'Cache timeout', 'Number of seconds until cached items are discarded. Should be high or zero if memory is not an issue. Set it to a small value to preserve memory when browsing through many images. If zero, no limit applies. Specify in seconds.') + +## EOF ## |