aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore4
-rw-r--r--CHANGELOG.md20
-rw-r--r--MANIFEST.in53
-rw-r--r--README57
-rw-r--r--README.md62
-rw-r--r--doc/Makefile20
-rw-r--r--doc/make.bat35
-rw-r--r--doc/source/_static/mainview.jpgbin0 -> 148770 bytes
-rw-r--r--doc/source/_static/usage.webmbin0 -> 4599471 bytes
-rw-r--r--doc/source/actions.rst72
-rw-r--r--doc/source/conf.py37
-rw-r--r--doc/source/core.rst75
-rw-r--r--doc/source/development.rst14
-rw-r--r--doc/source/index.rst25
-rw-r--r--doc/source/installation.rst66
-rw-r--r--doc/source/tiles.rst77
-rw-r--r--doc/source/usage.rst114
-rw-r--r--setup.py51
-rwxr-xr-x[-rw-r--r--]tagit.app45
-rw-r--r--tagit.toml11
-rw-r--r--tagit/__init__.py5
-rw-r--r--tagit/actions/__init__.py9
-rw-r--r--tagit/actions/action.kv15
-rw-r--r--tagit/actions/filter.py4
-rw-r--r--tagit/actions/grouping.py7
-rw-r--r--tagit/actions/misc.py2
-rw-r--r--tagit/actions/planes.kv15
-rw-r--r--tagit/actions/planes.py57
-rw-r--r--tagit/actions/search.py2
-rw-r--r--tagit/actions/tagging.py21
-rw-r--r--tagit/apps/__init__.py44
-rw-r--r--tagit/apps/desktop.py55
-rw-r--r--tagit/apps/port-config.yaml154
-rw-r--r--tagit/apps/port_data.py127
-rw-r--r--tagit/assets/icons/scalable/objects/add_tag.svg296
-rw-r--r--tagit/assets/icons/scalable/objects/edit_tag.svg303
-rw-r--r--tagit/assets/icons/scalable/planes/browsing.svg157
-rw-r--r--tagit/assets/icons/scalable/planes/codash.svg147
-rw-r--r--tagit/assets/icons/scalable/planes/dashboard.svg142
-rw-r--r--tagit/assets/required_schema.nt (renamed from tagit/apps/port-schema.nt)86
-rw-r--r--tagit/assets/themes/default/style.kv212
-rw-r--r--tagit/config/loader.py4
-rw-r--r--tagit/config/settings.json22
-rw-r--r--tagit/config/settings.yaml10
-rw-r--r--tagit/config/user-defaults.json134
-rw-r--r--tagit/config/user-defaults.yaml132
-rw-r--r--tagit/dialogues/dialogue.kv63
-rw-r--r--tagit/dialogues/dialogue.py6
-rw-r--r--tagit/external/setproperty/README.md24
-rw-r--r--tagit/external/setproperty/build.py5
-rw-r--r--tagit/external/setproperty/preprocess.py8
-rw-r--r--tagit/external/setproperty/setup.py1
-rw-r--r--tagit/parsing/filter/from_string.py6
-rw-r--r--tagit/parsing/filter/to_string.py6
-rw-r--r--tagit/tiles/decoration.kv29
-rw-r--r--tagit/tiles/decoration.py9
-rw-r--r--tagit/tiles/info.py32
-rw-r--r--tagit/tiles/tile.kv26
-rw-r--r--tagit/tiles/tile.py61
-rw-r--r--tagit/utils/namespaces.py22
-rw-r--r--tagit/widgets/browser.kv23
-rw-r--r--tagit/widgets/browser.py92
-rw-r--r--tagit/widgets/filter.kv105
-rw-r--r--tagit/widgets/filter.py34
-rw-r--r--tagit/widgets/session.py12
-rw-r--r--tagit/widgets/status.kv16
-rw-r--r--tagit/windows/desktop.kv145
-rw-r--r--tagit/windows/desktop.py41
68 files changed, 2155 insertions, 1611 deletions
diff --git a/.gitignore b/.gitignore
index ba73cdf..bb8fbaf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,7 +9,7 @@ __pycache__
.coverage
.pypirc
.mypy_cache
-bsfs.egg-info
+tagit.egg-info
htmlcov
tags
dev/
@@ -36,7 +36,7 @@ tagit/assets/icons/kivy/browser*
tagit/assets/icons/kivy/filter*
tagit/assets/icons/kivy/grouping*
tagit/assets/icons/kivy/misc*
-tagit/assets/icons/kivy/planes*
+tagit/assets/icons/kivy/objects*
tagit/assets/icons/kivy/search*
tagit/assets/icons/kivy/session*
tagit/assets/icons/kivy/tagging*
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..4508589
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,20 @@
+
+# Changelog
+
+## 0.23.03 (Initial release)
+
+### Added
+
+- Desktop application
+- Desktop window
+- Widgets
+ - Browser
+ - Filter
+ - Status
+- Actions and Tiles
+- Assets (buttons, placeholder)
+- Query parsing
+- Config system
+- Logging system
+
+
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..9964a30
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,53 @@
+include tagit/actions/action.kv
+include tagit/actions/browser.kv
+include tagit/actions/filter.kv
+include tagit/actions/grouping.kv
+include tagit/actions/misc.kv
+include tagit/actions/search.kv
+include tagit/actions/session.kv
+include tagit/actions/tagging.kv
+include tagit/assets/fonts/kivy/Unifont.ttf
+include tagit/assets/icons/kivy/browser-0.png
+include tagit/assets/icons/kivy/browser.atlas
+include tagit/assets/icons/kivy/filter-0.png
+include tagit/assets/icons/kivy/filter.atlas
+include tagit/assets/icons/kivy/grouping-0.png
+include tagit/assets/icons/kivy/grouping.atlas
+include tagit/assets/icons/kivy/misc-0.png
+include tagit/assets/icons/kivy/misc.atlas
+include tagit/assets/icons/kivy/no_preview.png
+include tagit/assets/icons/kivy/objects-0.png
+include tagit/assets/icons/kivy/objects.atlas
+include tagit/assets/icons/kivy/search-0.png
+include tagit/assets/icons/kivy/search.atlas
+include tagit/assets/icons/kivy/session-0.png
+include tagit/assets/icons/kivy/session.atlas
+include tagit/assets/icons/kivy/tagging-0.png
+include tagit/assets/icons/kivy/tagging.atlas
+include tagit/assets/required_schema.nt
+include tagit/assets/themes/default/style.kv
+include tagit/config/settings.yaml
+include tagit/config/user-defaults.yaml
+include tagit/dialogues/console.kv
+include tagit/dialogues/dialogue.kv
+include tagit/dialogues/license.t
+include tagit/dialogues/path_picker.kv
+include tagit/dialogues/simple_input.kv
+include tagit/external/kivy_garden/contextmenu/app_menu.kv
+include tagit/external/kivy_garden/contextmenu/context_menu.kv
+include tagit/external/kivy_garden/mapview/icons/cluster.png
+include tagit/external/kivy_garden/mapview/icons/marker.png
+exclude tagit/external/setproperty/*
+include tagit/external/setproperty/README.md
+include tagit/external/setproperty/__init__.py
+include tagit/external/setproperty/build.py
+include tagit/external/setproperty/setproperty.c
+include tagit/external/tooltip.kv
+include tagit/tiles/decoration.kv
+include tagit/tiles/tile.kv
+include tagit/widgets/browser.kv
+include tagit/widgets/context.kv
+include tagit/widgets/dock.kv
+include tagit/widgets/filter.kv
+include tagit/widgets/status.kv
+include tagit/windows/desktop.kv
diff --git a/README b/README
deleted file mode 100644
index 79f4b56..0000000
--- a/README
+++ /dev/null
@@ -1,57 +0,0 @@
-
-tagit - the BSFS frontend
-=========================
-
-
-### Developer tools setup
-
-#### Test coverage (coverage)
-
-Resources:
-* https://coverage.readthedocs.io/en/6.5.0/index.html
-* https://nedbatchelder.com/blog/200710/flaws_in_coverage_measurement.html
-
-Commands:
-$ pip install coverage
-$ coverage run ; coverage html ; xdg-open .htmlcov/index.html
-
-
-
-#### Static code analysis (pylint)
-
-Resources:
-* https://github.com/PyCQA/pylint
-* https://pylint.org/
-* https://pylint.pycqa.org/en/latest/user_guide/messages/messages_overview.html#messages-overview
-
-Commands:
-$ pip install pylint
-$ pylint tagit
-
-
-
-#### Type analysis (mypy)
-
-Resources:
-* https://github.com/python/mypy
-* https://mypy.readthedocs.io/en/stable/
-
-Commands:
-$ pip install mypy
-$ mypy
-
-
-
-#### Documentation (sphinx)
-
-Resources:
-*
-*
-
-Commands:
-$ pip install ...
-$
-
-
-
-
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..98134b0
--- /dev/null
+++ b/README.md
@@ -0,0 +1,62 @@
+
+tagit - the BSFS browser
+========================
+
+tagit is a graphical interface to browse through a BSFS storage. It is designed
+with image collections in mind, therefore provides a means to quickly navigate a
+collection by user-assigned tags, and to easily edit images' tags.
+
+
+## Installation
+
+You can install tagit via pip:
+
+ $ pip install --extra-index-url https://pip.bsfs.io tagit
+
+
+## Development
+
+Set up a virtual environment:
+
+ $ virtualenv env
+ $ source env/bin/activate
+
+Install tagit as editable from the git repository:
+
+ $ git clone https://git.bsfs.io/tagit.git
+ $ cd tagit
+ $ pip install -e .
+
+If you want to develop (*dev*), run the tests (*test*), edit the
+documentation (*doc*), or build a distributable (*build*),
+install bsfs with the respective extras (in addition to file format extras):
+
+ $ pip install -e .[dev,doc,build,test]
+
+Or, you can manually install the following packages besides tagit:
+
+ $ pip install coverage mypy pylint
+ $ pip install sphinx sphinx-copybutton sphinxcontrib-video furo
+ $ pip install build
+ $ pip install pyexiv2
+
+To ensure code style discipline, run the following commands:
+
+ $ coverage run ; coverage html ; xdg-open .htmlcov/index.html
+ $ pylint tagit
+ $ mypy
+
+To build the package, do:
+
+ $ python -m build
+
+To run only the tests (without coverage), run the following command from the **test folder**:
+
+ $ python -m unittest
+
+To build the documentation, run the following commands from the **doc folder**:
+
+ $ make html
+ $ xdg-open build/html/index.html
+
+
diff --git a/doc/Makefile b/doc/Makefile
new file mode 100644
index 0000000..d0c3cbf
--- /dev/null
+++ b/doc/Makefile
@@ -0,0 +1,20 @@
+# Minimal makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line, and also
+# from the environment for the first two.
+SPHINXOPTS ?=
+SPHINXBUILD ?= sphinx-build
+SOURCEDIR = source
+BUILDDIR = build
+
+# Put it first so that "make" without argument is like "make help".
+help:
+ @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+.PHONY: help Makefile
+
+# Catch-all target: route all unknown targets to Sphinx using the new
+# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
+%: Makefile
+ @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
diff --git a/doc/make.bat b/doc/make.bat
new file mode 100644
index 0000000..747ffb7
--- /dev/null
+++ b/doc/make.bat
@@ -0,0 +1,35 @@
+@ECHO OFF
+
+pushd %~dp0
+
+REM Command file for Sphinx documentation
+
+if "%SPHINXBUILD%" == "" (
+ set SPHINXBUILD=sphinx-build
+)
+set SOURCEDIR=source
+set BUILDDIR=build
+
+%SPHINXBUILD% >NUL 2>NUL
+if errorlevel 9009 (
+ echo.
+ echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
+ echo.installed, then set the SPHINXBUILD environment variable to point
+ echo.to the full path of the 'sphinx-build' executable. Alternatively you
+ echo.may add the Sphinx directory to PATH.
+ echo.
+ echo.If you don't have Sphinx installed, grab it from
+ echo.https://www.sphinx-doc.org/
+ exit /b 1
+)
+
+if "%1" == "" goto help
+
+%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
+goto end
+
+:help
+%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
+
+:end
+popd
diff --git a/doc/source/_static/mainview.jpg b/doc/source/_static/mainview.jpg
new file mode 100644
index 0000000..7543b6f
--- /dev/null
+++ b/doc/source/_static/mainview.jpg
Binary files differ
diff --git a/doc/source/_static/usage.webm b/doc/source/_static/usage.webm
new file mode 100644
index 0000000..5de921f
--- /dev/null
+++ b/doc/source/_static/usage.webm
Binary files differ
diff --git a/doc/source/actions.rst b/doc/source/actions.rst
new file mode 100644
index 0000000..2b41286
--- /dev/null
+++ b/doc/source/actions.rst
@@ -0,0 +1,72 @@
+
+.. _Actions:
+
+Actions
+=======
+
+*Actions* provide small, specific, isolated functionality like creating a search filter
+or adding a tag to a selected item.
+The advantage of Actions is their lightweight implementation, so that the code can be reused easily, and added or removed to the application on demand.
+For example, a feature that introduces large dependencies could be isolated in this manner, only being offered if the dependencies are met.
+Or, the user can configure the appearance and button toolbars.
+
+
+Triggers and execution
+----------------------
+
+An action is triggered by some event like a key press or a touch event.
+Implementation-wise, touch triggers are implemented via kivy's default `on_touch_down` functionality.
+Key events are handled by overwriting the `tagit.action.Action.ktrigger` method.
+It is supposed to return a boolean that indicates whether or not the key event triggers
+the action. The `tagit.widget.Binding` class helps you to check if a specific key event
+matches your default binding.
+Note that the binding needs not to be unique, i.e., multiple actions may be triggered
+by the same key event. You can use this to your advantage, but should carefully regard
+other actions when adding new default bindings.
+
+Once triggered, the `tagit.action.Action.apply` function of the action is invoked.
+Actions are automatically linked to the root window (*self.root*) and can use
+*widget contexts* to query or manipulate them.
+
+See the following snipped as an example::
+
+ from tagit import config
+ from tagit.actions.action import Action
+ from tagit.utils import clamp
+ from tagit.widgets import Binding
+
+ class NextPage(Action):
+ """Scroll one page downwards without moving the cursor."""
+
+ def ktrigger(self, evt):
+ """Check if *evt* matches the configured key binding for this action (PGDN)."""
+ return Binding.check(evt, self.cfg('bindings', 'browser', 'page_next'))
+
+ def apply(self):
+ """Execute the NextPage action."""
+ with self.root.browser as browser:
+ browser.offset = clamp(browser.offset + browser.page_size, browser.max_offset)
+
+ config.declare(('bindings', 'browser', 'page_next'),
+ config.Keybind(), Binding.simple(Binding.PGDN))
+
+
+
+Action appearance
+-----------------
+
+The action appearance is normally defined in a respective *.kv* file.
+Any actions may be displayed as text and/or as image, hence you should provide both options.
+The `tagit.actions.Action` base class provides the configuration flags to control the
+display style. The configuration is usually specified by the parent element like a widget,
+or a dock (e.g., `tagit.widgets.dock.ButtonDock`).
+
+The following snipped defines the appearance of the *NextPage* action::
+
+ #:import resource_find kivy.resources.resource_find
+
+ <NextPage>:
+ source: resource_find('atlas://browser/next_page')
+ text: 'Next Page'
+
+.. EOF ..
diff --git a/doc/source/conf.py b/doc/source/conf.py
new file mode 100644
index 0000000..96ab2d5
--- /dev/null
+++ b/doc/source/conf.py
@@ -0,0 +1,37 @@
+# Configuration file for the Sphinx documentation builder.
+#
+# For the full list of built-in configuration values, see the documentation:
+# https://www.sphinx-doc.org/en/master/usage/configuration.html
+
+# -- Project information -----------------------------------------------------
+# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
+
+project = 'tagit'
+copyright = '2023, Matthias Baumgartner'
+author = 'Matthias Baumgartner'
+release = '0.5'
+
+# -- General configuration ---------------------------------------------------
+# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
+
+extensions = [
+ 'sphinxcontrib.video',
+ 'sphinx_copybutton',
+ ]
+
+templates_path = ['_templates']
+exclude_patterns = []
+
+
+
+# -- Options for HTML output -------------------------------------------------
+# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
+
+html_theme = 'furo'
+html_static_path = ['_static']
+
+html_title = 'tagit'
+html_theme_options = {
+ 'announcement': '<em>This project is under heavy development and subject to rapid changes. Use at your own discretion.</em>',
+ }
+
diff --git a/doc/source/core.rst b/doc/source/core.rst
new file mode 100644
index 0000000..b4f8ab3
--- /dev/null
+++ b/doc/source/core.rst
@@ -0,0 +1,75 @@
+
+Core
+====
+
+*tagit* provides a number of submodules that are
+
+Windows
+-------
+
+A window arranges several UI elements into a consistent view,
+optimized for a specific platform.
+Currently, the only window instance (`tagit.window.desktop`) that exists is tuned for desktop environments.
+
+
+Widgets
+-------
+
+*tagit* defines three main widgets:
+
+* `tagit.widgets.browser.Browser`
+* `tagit.widgets.filter.Filter`
+* `tagit.widgets.session.Session`
+
+Each of those widgets can be used as context,
+whereas the widget updates if its state was changed when the context expires.
+Clients (e.g. Actions, Tiles) can thus safely access and modify the widgets directly.
+
+Additionally, there exists widgets for key bindings (`tagit.widgets.bindings.Binding`), a status bar (`tagit.widgets.status.Status`), and docks for :ref:`Actions` and :ref:`Tiles` (`taigt.widgets.dock`).
+These widgets are mostly used internally to arrange UI elements, or to provide additional input methods to the user.
+
+
+config
+------
+
+*tagit* comes with an intricate configuration system.
+The core idea is to provide a means to define config keys and defaults in the code files where they are used,
+but also have the ability to check a config file's validity upon application startup,
+and to provide reasonable and meaningful documentation for each config item.
+
+Configuration items are declared and documented in the source file that typically uses them::
+
+ from tagit import config
+
+ config.declare(('ui', 'standalone', 'browser', 'cols'), config.Unsigned(), 3,
+ __name__, 'Browser columns', 'Default number of columns in the browser.')
+
+ config.declare(('ui', 'standalone', 'browser', 'rows'), config.Unsigned(), 3,
+ __name__, 'Browser rows', 'Default number of rows in the grid view.')
+
+ def n_items(cfg):
+ """Return the number of items shown on one page."""
+ # config values are automatically converted to integers
+ cols = cfg('ui', 'standalone', 'browser', 'cols')
+ rows = cfg('ui', 'standalone', 'browser', 'rows')
+ return cols * rows
+
+With `config.declare`, you specify the config key(*('ui', 'standalone', 'browser', 'cols')*),
+a type (*config.Unsigned()*),
+the default value (*3*),
+and some documentation arguments (module name, abstract, description).
+
+On application-level, loading (and type checking) a config file is as simple as calling::
+
+ from tagit.config import Settings
+ cfg = Settings.Open(path)
+
+The user can the write a config file in json, yaml, or another structured file format::
+
+ ui:
+ standalone:
+ browser:
+ cols: 4
+ rows: 3
+
+
diff --git a/doc/source/development.rst b/doc/source/development.rst
new file mode 100644
index 0000000..a7b1bd0
--- /dev/null
+++ b/doc/source/development.rst
@@ -0,0 +1,14 @@
+
+Developer information
+=====================
+
+*tagit* builds upon the `Kivy <https://kivy.org/>`_ framework that offers a great integration for desktop and mobile environments.
+To build the UI in a flexible and extensible manner, *tagit* implements several mechanisms to streamline and re-use UI elements. The following pages explain the core ideas behind the *tagit* code structure.
+
+.. toctree::
+ :maxdepth: 1
+
+ core
+ actions
+ tiles
+
diff --git a/doc/source/index.rst b/doc/source/index.rst
new file mode 100644
index 0000000..0a93a81
--- /dev/null
+++ b/doc/source/index.rst
@@ -0,0 +1,25 @@
+
+tagit: The BSFS browser
+=======================
+
+*tagit* is a graphical interface to browse through a *bsfs* storage.
+It is designed with image collections in mind.
+It provides a means to quickly navigate a collection by user-assigned tags,
+and to easily edit images' tags.
+
+.. video:: _static/usage.webm
+ :width: 640
+
+
+
+Contents
+--------
+
+.. toctree::
+ :maxdepth: 1
+
+ usage
+ installation
+ development
+
+
diff --git a/doc/source/installation.rst b/doc/source/installation.rst
new file mode 100644
index 0000000..12d8898
--- /dev/null
+++ b/doc/source/installation.rst
@@ -0,0 +1,66 @@
+
+Installation
+============
+
+Install *tagit* via pip::
+
+ pip install --extra-index-url https://pip.bsfs.io tagit
+
+This installs the `tagit` python package as well as the `tagit.app` command.
+It is recommended to install *tagit* in a virtual environment (via ``virtualenv``).
+Please note that you may have to separately install the `BSIE <https://www.bsfs.io/bsie>`_
+package into the same environment.
+
+
+License
+-------
+
+This project is released under the terms of the 3-clause BSD License.
+By downloading or using the application you agree to the license's terms and conditions.
+
+.. literalinclude:: ../../LICENSE
+
+
+Demo
+----
+
+The following commands install *tagit*, its dependencies, and the demo files,
+then starts *tagit*. Of course, you can also do this in a ``virtualenv``::
+
+ pip install --extra-index-url https://pip.bsfs.io/ tagit
+ wget https://www.bsfs.io/demo/tagitrc -O ~/.tagitrc
+ wget https://www.bsfs.io/demo/demo-triples.pkl -O ~/demo-triples.pkl
+ wget https://www.bsfs.io/demo/demo-schema.pkl -O ~/demo-schema.pkl
+ tagit
+
+Or, you can run the demo in a docker container (although it will be relatively slow)::
+
+ mkdir bsfs-demo; cd bsfs-demo
+ wget https://www.bsfs.io/demo/Dockerfile
+ docker build -t bsfs/demo .
+ xhost +local:*
+ docker run --rm --ipc=host -e DISPLAY -v /tmp/.X11-unix/:/tmp/.X11-unix -it bsfs/demo
+
+
+Source
+------
+
+Check out our git repository::
+
+ git clone https://git.bsfs.io/tagit.git
+
+You can further install *tagit* via the ususal `setuptools <https://setuptools.pypa.io/en/latest/index.html>`_ commands from your tagit source directory::
+
+ python setup.py develop
+
+For development, you also need to install some additional dependencies::
+
+ # code style discipline
+ pip install mypy coverage pylint
+
+ # documentation
+ pip install sphinx sphinx-copybutton sphinxcontrib-video furo
+
+ # packaging
+ pip install build
+
diff --git a/doc/source/tiles.rst b/doc/source/tiles.rst
new file mode 100644
index 0000000..e0f9cd0
--- /dev/null
+++ b/doc/source/tiles.rst
@@ -0,0 +1,77 @@
+
+.. _Tiles:
+
+Tiles
+=====
+
+Tiles are containers to display information about the file system, or specific items therein.
+Their main purpose is to assist the user in navigating and tag their files.
+They can also display additional information about selected files,
+e.g., by showing metadata or placing geo-tagged files on a map.
+
+
+Content update
+--------------
+
+A Tile is responsible to update its content itself, whenever appropriate.
+The mixin classes `tagit.widgets.browser.BrowserAwareMixin`, `tagit.widgets.session.SessionAwareMixin`, and `tagit.widgets.filter.FilterAwareMixin` provide a series of events that trigger when the UI or storage state changes.
+A Tile can trigger its content update by binding to these events.
+In addition, a Tile's *visibility* indicates if it is actually visible to the user.
+The visibility flag is controlled by the parent widget (mostly a `tagit.widget.dock.TileDock`) and the tile updates automatically if its visibility changes.
+Similar to actions, a `tagit.tiles.Tile` is automatically linked to the root window so that it can retrieve contextual information about the UI's state, or to query the storage backed for additional file properties.
+
+For example, the info tile updates when the browser state changes (e.g., the user moves the cursor),
+or when the storage content changes (e.g., the item information changes).
+In such an event, the tile fetches the current information from the storage and displays it as a table::
+
+ from collections import OrderedDict
+ from tagit.utils import ns, magnitude_fmt
+ from tagit.widgets.browser import BrowserAwareMixin
+ from tagit.widgets.session import StorageAwareMixin
+ from .tile import TileTabular
+
+ class Info(TileTabular, BrowserAwareMixin, StorageAwareMixin):
+ """Show essential attributes about the cursor."""
+
+ # bind to events so that the tile updates when the context changes
+ def on_browser(self, sender, browser):
+ ...
+ def on_predicate_modified(self, *args):
+ ...
+
+ def update(self, *args):
+ """Update the tile's content."""
+ cursor = self.root.browser.cursor
+ if not self.visible or cursor is None:
+ # invisible or no cursor, nothing to show
+ self.tabledata = OrderedDict({})
+ else:
+ # fetch information about the cursor from the storage
+ data = cursor.get(
+ ns.bse.filesize,
+ ns.bse.filename,
+ (ns.bse.tag, ns.bst.label),
+ )
+ # display the information as a two-column table
+ self.tabledata = OrderedDict({
+ 'Filesize' : magnitude_fmt(data.get(ns.bse.filesize, 0)),
+ 'Filename' : data.get(ns.bse.filename, 'n/a'),
+ 'Tags' : ', '.join(sorted(data.get((ns.bse.tag, ns.bst.label), [' ']))),
+ })
+
+
+Appearance
+----------
+
+Tiles can appear at any place in the UI and they could also be re-used in multiple locations.
+To enable this, they are typically located within a `tagit.widgets.dock.TileDock` which controls the tile's appearance, position, and size.
+
+The tile itself only has to define the appearance of its content.
+For example, for the info tile the following kivy language snippet suffices::
+
+ <Info>:
+ title: 'Item info'
+ keywidth: min(75, self.width * 0.4)
+
+
+.. EOF ..
diff --git a/doc/source/usage.rst b/doc/source/usage.rst
new file mode 100644
index 0000000..260f311
--- /dev/null
+++ b/doc/source/usage.rst
@@ -0,0 +1,114 @@
+
+Usage
+=====
+
+.. image:: _static/mainview.jpg
+ :width: 640
+
+
+The *tagit* screen mostly shows a search bar and a browser.
+
+The *search bar* lists the tokens of your search query in the order that you've added them (via the filter button |buttons_filter_add|).
+You can nagivate through the tokens (use the arrow buttons |buttons_filter_back| |buttons_filter_forth|) to switch between different searches.
+Note that *tagit* remembers your selection and focus at every step.
+The active search tokens are highlighted and dangling tokens are greyed out.
+
+The *browser* shows the search results and lets you scroll and select them.
+On the left hand side, you'll find some buttons to add or edit image's tags (
+|buttons_tagging_add_tag|,
+|buttons_tagging_edit_tag|),
+to create search tokens from your selection (
+|buttons_search_exclusive_filter|,
+|buttons_search_exclude_filter|
+),
+and to navigate and edit your selection (
+|buttons_browser_select_all|,
+|buttons_browser_select_none|,
+|buttons_browser_select_invert|,
+|buttons_browser_select_single|,
+|buttons_browser_select_range|,
+|buttons_browser_select_multi|,
+|buttons_browser_select_add|,
+|buttons_browser_select_sub|
+).
+Note that on a desktop environment you can use your keyboard to select items like in most browsers, e.g, CTRL to select individual items or SHIFT to select a range.
+The box on the right shows you some information about the currently selected item.
+The navigation buttons at the bottom let you scroll through the browser's pages (
+|buttons_browser_cursor_first|,
+|buttons_browser_previous_page|,
+|buttons_browser_scroll_up|
+|buttons_browser_scroll_down|,
+|buttons_browser_next_page|,
+|buttons_browser_cursor_last|,
+).
+
+The following video guides you through the some common functionality:
+
+.. video:: _static/usage.webm
+ :width: 640
+
+
+
+.. |buttons_filter_add| image:: ../../tagit/assets/icons/kivy/filter/add.png
+ :width: 20
+
+.. |buttons_filter_back| image:: ../../tagit/assets/icons/kivy/filter/go_back.png
+ :width: 20
+
+.. |buttons_filter_forth| image:: ../../tagit/assets/icons/kivy/filter/go_forth.png
+ :width: 20
+
+.. |buttons_browser_cursor_first| image:: ../../tagit/assets/icons/kivy/browser/cursor_first.png
+ :width: 20
+
+.. |buttons_browser_cursor_last| image:: ../../tagit/assets/icons/kivy/browser/cursor_last.png
+ :width: 20
+
+.. |buttons_browser_next_page| image:: ../../tagit/assets/icons/kivy/browser/next_page.png
+ :width: 20
+
+.. |buttons_browser_previous_page| image:: ../../tagit/assets/icons/kivy/browser/previous_page.png
+ :width: 20
+
+.. |buttons_browser_scroll_down| image:: ../../tagit/assets/icons/kivy/browser/scroll_down.png
+ :width: 20
+
+.. |buttons_browser_scroll_up| image:: ../../tagit/assets/icons/kivy/browser/scroll_up.png
+ :width: 20
+
+.. |buttons_browser_select_all| image:: ../../tagit/assets/icons/kivy/browser/select_all.png
+ :width: 20
+
+.. |buttons_browser_select_none| image:: ../../tagit/assets/icons/kivy/browser/select_none.png
+ :width: 20
+
+.. |buttons_browser_select_invert| image:: ../../tagit/assets/icons/kivy/browser/select_invert.png
+ :width: 20
+
+.. |buttons_browser_select_single| image:: ../../tagit/assets/icons/kivy/browser/select_single.png
+ :width: 20
+
+.. |buttons_browser_select_range| image:: ../../tagit/assets/icons/kivy/browser/select_range.png
+ :width: 20
+
+.. |buttons_browser_select_multi| image:: ../../tagit/assets/icons/kivy/browser/select_multi.png
+ :width: 20
+
+.. |buttons_browser_select_add| image:: ../../tagit/assets/icons/kivy/browser/select_add.png
+ :width: 20
+
+.. |buttons_browser_select_sub| image:: ../../tagit/assets/icons/kivy/browser/select_sub.png
+ :width: 20
+
+.. |buttons_tagging_add_tag| image:: ../../tagit/assets/icons/kivy/tagging/add_tag.png
+ :width: 20
+
+.. |buttons_tagging_edit_tag| image:: ../../tagit/assets/icons/kivy/tagging/edit_tag.png
+ :width: 20
+
+.. |buttons_search_exclusive_filter| image:: ../../tagit/assets/icons/kivy/search/exclusive_filter.png
+ :width: 20
+
+.. |buttons_search_exclude_filter| image:: ../../tagit/assets/icons/kivy/search/exclude_filter.png
+ :width: 20
+
diff --git a/setup.py b/setup.py
index 7cc00a8..485a632 100644
--- a/setup.py
+++ b/setup.py
@@ -1,27 +1,58 @@
-from setuptools import setup
+from setuptools import setup, find_packages, Extension
import os
setup(
+ # package metadata
name='tagit',
- version='0.0.1',
+ version='0.23.03',
author='Matthias Baumgartner',
- author_email='dev@igsor.net',
- description='A frontend to the Black Star File System.',
- long_description=open(os.path.join(os.path.dirname(__file__), 'README')).read(),
+ author_email='dev@bsfs.io',
+ description='The Black Star File System (BSFS) browser and tagger.',
+ long_description=open(os.path.join(os.path.dirname(__file__), 'README.md')).read(),
license='BSD',
license_files=('LICENSE', ),
- url='https://www.igsor.net/projects/blackstar/tagit/',
+ url='https://www.bsfs.io/tagit/',
download_url='https://pip.igsor.net',
- packages=('tagit', ),
+
+ # packages
+ packages=find_packages(include=['tagit']),
+ package_dir={'tagit': 'tagit'},
+ # data files are included if mentioned in MANIFEST.in
+ include_package_data=True,
+ # setproperty needs to be compiled
+ # NOTE: Assumes that setproperty has already been cythonized!
+ ext_modules=[
+ Extension(
+ name='tagit.external.setproperty.setproperty',
+ sources=['tagit/external/setproperty/setproperty.c'],
+ ),
+ ],
+
+ # entrypoints
+ entry_points={
+ 'console_scripts': [
+ 'tagit = tagit.apps:main',
+ ],
+ },
+
+ # dependencies
+ python_requires=">=3.7",
install_requires=(
+ 'bsfs',
+ 'pillow',
'kivy',
- #'pyexiv2', (for test/utils/test_time.py)
- 'pyparsing'
+ 'pyparsing',
'python-dateutil',
'pyyaml',
'requests',
),
- python_requires=">=3.7",
+
+ extras_require={
+ 'dev': ['coverage', 'mypy', 'pylint'],
+ 'doc': ['sphinx', 'sphinx-copybutton', 'sphinxcontrib-video', 'furo'],
+ 'test': ['pyexiv2'],
+ 'build': ['build'],
+ },
)
diff --git a/tagit.app b/tagit.app
index e18bbc0..42a35a0 100644..100755
--- a/tagit.app
+++ b/tagit.app
@@ -1,42 +1,7 @@
-#!/usr/bin/python3.6
-"""The tagit application.
-
-Part of the tagit module.
-A copy of the license is provided with the project.
-Author: Matthias Baumgartner, 2022
-"""
-# imports
-import argparse
-import sys
-
-# module imports
-import tagit
-import tagit.apps
-
-# config
-apps = {
- 'desktop' : tagit.apps.desktop,
- }
-
-
-## code ##
-
+#!/usr/bin/env python3
if __name__ == '__main__':
- parser = argparse.ArgumentParser(description='Image tagger tool.', prog='tagit')
- # version
- parser.add_argument('--version', action='version',
- version='%(prog)s {}.{}.{}'.format(*tuple(tagit.version_info)))
- # 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
- try:
- apps[args.app](args.rest)
- except Exception as e:
- print(str(e))
-
+ import tagit.apps
+ import sys
+ tagit.apps.main(sys.argv[1:])
+
## EOF ##
diff --git a/tagit.toml b/tagit.toml
deleted file mode 100644
index 8412da7..0000000
--- a/tagit.toml
+++ /dev/null
@@ -1,11 +0,0 @@
-[project]
-name = "tagit"
-description = "A frontend to the Black Star File System."
-version = "0.0.1"
-license = {text = "BSD 3-Clause License"}
-authors = [{name='Matthias Baumgartner', email="dev@igsor.net"}]
-dependencies = [
- "kivy",
-]
-requires-python = ">=3.7"
-
diff --git a/tagit/__init__.py b/tagit/__init__.py
index ec5f1c2..8c4285a 100644
--- a/tagit/__init__.py
+++ b/tagit/__init__.py
@@ -20,6 +20,9 @@ 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
@@ -40,6 +43,8 @@ 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
diff --git a/tagit/actions/__init__.py b/tagit/actions/__init__.py
index b2ab6bd..b8b65b8 100644
--- a/tagit/actions/__init__.py
+++ b/tagit/actions/__init__.py
@@ -16,7 +16,6 @@ from . import filter
from . import grouping
from . import misc
#from . import objects
-from . import planes
from . import search
from . import session
from . import tagging
@@ -80,8 +79,8 @@ class ActionBuilder(BuilderBase):
'ShowConsole': misc.ShowConsole,
'ShowHelp': misc.ShowHelp,
'ShowSettings': misc.ShowSettings,
- 'ClipboardCopy': misc.ClipboardCopy,
- 'ClipboardPaste': misc.ClipboardPaste,
+ #'ClipboardCopy': misc.ClipboardCopy,
+ #'ClipboardPaste': misc.ClipboardPaste,
## objects
#'RotateLeft': objects.RotateLeft,
#'RotateRight': objects.RotateRight,
@@ -93,10 +92,6 @@ class ActionBuilder(BuilderBase):
#'SetRank3': objects.SetRank3,
#'SetRank4': objects.SetRank4,
#'SetRank5': objects.SetRank5,
- ## planes
- 'ShowDashboard': planes.ShowDashboard,
- 'ShowBrowsing': planes.ShowBrowsing,
- 'ShowCodash': planes.ShowCodash,
## search
'Search': search.Search,
'ShowSelected': search.ShowSelected,
diff --git a/tagit/actions/action.kv b/tagit/actions/action.kv
index 5352964..807dd18 100644
--- a/tagit/actions/action.kv
+++ b/tagit/actions/action.kv
@@ -27,19 +27,4 @@
# to be disabled **after** show.
show: []
- # decoration
- canvas.before:
- Color:
- rgba: 17 / 255, 32 / 255, 148 / 255, self.selected_alpha
- Rectangle:
- pos: self.x, self.y + 1
- size: self.size
-
- canvas.after:
- Color:
- rgba: 17 / 255, 32 / 255, 148 / 255, self.selected_alpha
- Line:
- width: 2
- rectangle: self.x, self.y, self.width, self.height
-
## EOF ##
diff --git a/tagit/actions/filter.py b/tagit/actions/filter.py
index c5cc912..52f5817 100644
--- a/tagit/actions/filter.py
+++ b/tagit/actions/filter.py
@@ -90,7 +90,7 @@ class AddToken(Action):
def apply(self, token=None):
if token is None:
- sugg = self.root.session.storage.all(ns.bsfs.Tag).label(node=False)
+ 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()
@@ -125,7 +125,7 @@ class EditToken(Action):
text = kp.StringProperty('Edit token')
def apply(self, token):
- sugg = self.root.session.storage.all(ns.bsfs.Tag).label(node=False)
+ 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))
diff --git a/tagit/actions/grouping.py b/tagit/actions/grouping.py
index 05c651e..301af20 100644
--- a/tagit/actions/grouping.py
+++ b/tagit/actions/grouping.py
@@ -23,7 +23,7 @@ from tagit.widgets import Binding
from .action import Action
# constants
-GROUP_PREFIX = Namespace('http://example.com/me/group')
+GROUP_PREFIX = Namespace('http://example.com/me/group')()
# exports
__all__ = []
@@ -57,7 +57,8 @@ class CreateGroup(Action):
with self.root.browser as browser, \
self.root.session as session:
# create group
- grp = session.storage.node(ns.bsfs.Group, GROUP_PREFIX[uuid.UUID()()])
+ grp = session.storage.node(ns.bsn.Group,
+ getattr(GROUP_PREFIX, uuid.UUID()()))
if label is not None:
grp.set(ns.bsg.label, label)
@@ -99,7 +100,7 @@ class DissolveGroup(Action):
cursor = browser.cursor
if cursor is not None and cursor in browser.folds:
grp = browser.folds[cursor].group
- ents = session.storage.get(ns.bsfs.Entity,
+ 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
diff --git a/tagit/actions/misc.py b/tagit/actions/misc.py
index b7d0a87..f74e0f9 100644
--- a/tagit/actions/misc.py
+++ b/tagit/actions/misc.py
@@ -27,7 +27,7 @@ from tagit.widgets import Binding
from .action import Action
# constants
-HELP_URL = 'https://www.igsor.net/projects/tagit/'
+HELP_URL = 'https://www.bsfs.io/'
# exports
__all__ = []
diff --git a/tagit/actions/planes.kv b/tagit/actions/planes.kv
deleted file mode 100644
index 184f949..0000000
--- a/tagit/actions/planes.kv
+++ /dev/null
@@ -1,15 +0,0 @@
-#:import resource_find kivy.resources.resource_find
-
-<ShowDashboard>:
- source: resource_find('atlas://planes/dashboard')
- tooltip: 'Switch to the Dashboard'
-
-<ShowBrowsing>:
- source: resource_find('atlas://planes/browsing')
- tooltip: 'Switch to the browsing plane'
-
-<ShowCodash>:
- source: resource_find('atlas://planes/codash')
- tooltip: 'Switch to the contextual dashboard'
-
-## EOF ##
diff --git a/tagit/actions/planes.py b/tagit/actions/planes.py
deleted file mode 100644
index 89f93bb..0000000
--- a/tagit/actions/planes.py
+++ /dev/null
@@ -1,57 +0,0 @@
-"""
-
-Part of the tagit module.
-A copy of the license is provided with the project.
-Author: Matthias Baumgartner, 2022
-"""
-# standard imports
-import os
-
-# kivy imports
-from kivy.lang import Builder
-import kivy.properties as kp
-
-# inner-module imports
-from .action import Action
-
-# exports
-__all__ = []
-
-
-## code ##
-
-# load kv
-Builder.load_file(os.path.join(os.path.dirname(__file__), 'planes.kv'))
-
-# classes
-
-class ShowDashboard(Action):
- """Switch to the dashboard."""
- text = kp.StringProperty('Dashboard')
-
- def apply(self):
- planes = self.root.planes
- if planes.current_slide != planes.dashboard:
- planes.load_slide(planes.dashboard)
-
-
-class ShowBrowsing(Action):
- """Switch to the browsing plane."""
- text = kp.StringProperty('Browsing')
-
- def apply(self):
- planes = self.root.planes
- if planes.current_slide != planes.browsing:
- planes.load_slide(planes.browsing)
-
-
-class ShowCodash(Action):
- """Switch to the contextual dashboard."""
- text = kp.StringProperty('Context')
-
- def apply(self):
- planes = self.root.planes
- if planes.current_slide != planes.codash:
- planes.load_slide(planes.codash)
-
-## EOF ##
diff --git a/tagit/actions/search.py b/tagit/actions/search.py
index ca36a51..dd6c50c 100644
--- a/tagit/actions/search.py
+++ b/tagit/actions/search.py
@@ -128,7 +128,7 @@ class Search(Action, StorageAwareMixin, ConfigAwareMixin):
items = Cache.get(self._CACHE_CATEGORY, (query, sort), None)
if items is None:
# FIXME: mb/port: consider sort
- items = list(session.storage.get(ns.bsfs.File, query))
+ 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
diff --git a/tagit/actions/tagging.py b/tagit/actions/tagging.py
index 8a20702..ab25cb8 100644
--- a/tagit/actions/tagging.py
+++ b/tagit/actions/tagging.py
@@ -9,6 +9,9 @@ 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
@@ -24,7 +27,7 @@ from .action import Action
# constants
TAGS_SEPERATOR = ','
-TAG_PREFIX = Namespace('http://example.com/me/tag')
+TAG_PREFIX = Namespace('http://example.com/me/tag')()
# exports
__all__ = []
@@ -45,7 +48,7 @@ class AddTag(Action):
def apply(self):
if len(self.root.browser.selection) > 0:
- tags = self.root.session.storage.all(ns.bsfs.Tag).label(node=True)
+ tags = self.root.session.storage.all(ns.bsn.Tag).label(node=True)
dlg = dialogues.SimpleInput(
suggestions=set(tags.values()),
suggestion_sep=TAGS_SEPERATOR)
@@ -67,7 +70,8 @@ class AddTag(Action):
# FIXME: replace with proper tag factory
for lbl in labels:
if lbl not in lut:
- tag = self.root.session.storage.node(ns.bsfs.Tag, TAG_PREFIX[lbl])
+ 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, \
@@ -75,7 +79,7 @@ class AddTag(Action):
# get objects
ents = browser.unfold(browser.selection)
# collect tags
- tags = reduce(operator.add, tags) # FIXME: mb/port: pass set once supported by Nodes.set
+ 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)
@@ -93,7 +97,7 @@ class EditTag(Action):
with self.root.browser as browser:
if len(browser.selection) > 0:
# get all known tags
- all_tags = self.root.session.storage.all(ns.bsfs.Tag).label(node=True)
+ 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:
@@ -126,7 +130,7 @@ class EditTag(Action):
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)
+ 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()}
@@ -134,10 +138,11 @@ class EditTag(Action):
# FIXME: replace with proper tag factory
for lbl in added_labels:
if lbl not in lut:
- tag = self.root.session.storage.node(ns.bsfs.Tag, TAG_PREFIX[lbl])
+ 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)
+ 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:
diff --git a/tagit/apps/__init__.py b/tagit/apps/__init__.py
index 4c64128..84d0bf1 100644
--- a/tagit/apps/__init__.py
+++ b/tagit/apps/__init__.py
@@ -1,10 +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
index 7b21336..149bf30 100644
--- a/tagit/apps/desktop.py
+++ b/tagit/apps/desktop.py
@@ -10,6 +10,7 @@ import typing
# kivy imports
from kivy.app import App
+from kivy.resources import resource_find
from kivy.uix.settings import SettingsWithSidebar
# tagit imports
@@ -26,6 +27,26 @@ __all__: typing.Sequence[str] = (
## 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."""
@@ -34,41 +55,29 @@ class TagitApp(App):
self.settings_cls = SettingsWithSidebar
# set title
- self.title = 'tagit v2.0'
+ self.title = 'tagit v0.23.03'
- # FIXME: mb/port
- # load essentials
+ # load config
+ from tagit.config.loader import load_settings, TAGITRC
+ cfg = load_settings(TAGITRC, 0)
- #from tagit.config.loader import load_settings, TAGITRC
- #cfg = load_settings(TAGITRC, 0)
- cfg = config.Settings.Open(os.path.join(os.path.dirname(__file__), 'port-config.yaml'))
-
- # FIXME: mb/port/bsfs
# open BSFS storage
- store = bsfs.Open(cfg('session', 'bsfs'))
+ store = load_data_hook(cfg, bsfs.Open(cfg('session', 'bsfs'))) # FIXME: mb/port: data hook
# check storage schema
- # FIXME: Move somewhere else?!
- with open(os.path.join(os.path.dirname(__file__), 'port-schema.nt'), 'rt') as ifile:
+ with open(resource_find('required_schema.nt'), 'rt') as ifile:
required_schema = bsfs.schema.from_string(ifile.read())
- # FIXME: Since the store isn't persistent, we migrate to the required one here.
- #if not required_schema <= store.schema:
- # raise Exception('')
- store.migrate(required_schema)
-
- # FIXME: debug: add some data to the storage
- from . import port_data
- port_data.add_port_data(store)
+ 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: expects cfg, stor, log arguments
+ return desktop.MainWindow(cfg, store, None) # FIXME: mb/port: expects log arguments
def on_start(self):
# trigger startup operations
self.root.on_startup()
- # FIXME: mb/port
- #def on_stop(self):
- # self.root.session.storage.close()
def main(argv):
"""Start the tagit GUI. Opens a window to browse images."""
diff --git a/tagit/apps/port-config.yaml b/tagit/apps/port-config.yaml
deleted file mode 100644
index a9907b7..0000000
--- a/tagit/apps/port-config.yaml
+++ /dev/null
@@ -1,154 +0,0 @@
-session:
- first_start: false
- paths:
- searchlog: ~/.tagit.log
- bsfs: # FIXME: mb/port: rename to storage, but that space is currently polluted
- Graph:
- backend:
- SparqlStore: {}
- user: 'http://example.com/me'
- script:
- #- ShowBrowsing
- - [AddToken, 'hello']
-storage:
- index:
- preview_size:
- - 50
- - 200
- - 400
-ui:
- standalone:
- plane: browsing
- keytriggers:
- - CreateGroup
- - DissolveGroup
- - AddToGroup
- - MoveCursorUp
- - MoveCursorDown
- - MoveCursorLeft
- - MoveCursorRight
- - MoveCursorLast
- - MoveCursorFirst
- - NextPage
- - PreviousPage
- - ScrollDown
- - ScrollUp
- - ZoomIn
- - ZoomOut
- - Select
- - SelectAll
- - SelectNone
- - SelectMulti
- - SelectRange
- - AddToken
- - GoBack
- - GoForth
- - SearchByAddressOnce
- - SearchmodeSwitch
- - AddTag
- - EditTag
- - OpenGroup
- #- RepresentGroup
- - Search
- - ShowSelected
- - RemoveSelected
- - OpenExternal
- - ShowHelp
- browser:
- maxcols: 8
- maxrows: 8
- buttondocks:
- sidebar_left:
- - Menu
- - ShowDashboard
- - AddToken
- - AddTag
- - EditTag
- - CreateGroup
- - DissolveGroup
- - SelectAll
- - SelectNone
- - SelectInvert
- - SelectAdditive
- - SelectSubtractive
- - SelectSingle
- - SelectMulti
- - SelectRange
- - ShellDrop
- filter:
- - AddToken
- - EditToken
- context:
- app:
- - ShowSettings
- - ShowHelp
- - ShowConsole
- - ShowBrowsing
- browser:
- - ZoomIn
- - ZoomOut
- # clipboard:
- # - ClipboardCopy
- # - ClipboardPaste
- grouping:
- - CreateGroup
- - DissolveGroup
- - AddToGroup
- # - RepresentGroup
- # - RemoveFromGroup
- root:
- - LoadSession
- # search:
- # - ShowSelected
- # - RemoveSelected
- select:
- - SelectAll
- - SelectNone
- - SelectInvert
- - SelectSingle
- - SelectMulti
- - SelectRange
- - SelectAdditive
- - SelectSubtractive
- tagging:
- - AddTag
- - EditTag
- # - SetRank1
- # - SetRank3
- # - SetRank5
- search:
- sort_blacklist:
- - entity
- - flash
- - latitude
- - longitude
- - mime
- - author
- - camera
- - attributes
- tiledocks:
- dashboard: {}
- #Buttons:
- # buttons:
- # - ShowBrowsing
- # - CreateSession
- # - CreateTempSession
- # - LoadSession
- # - ReloadSession
- # - ImportObjects
- # - ItemExport
- # - UpdateSelectedObjects
- # - ShowHelp
- # - ShowSettings
- #Hints: {}
- #LibSummary: {}
- #Searchtree: {}
- #TagHistogram: {}
- #Tagcloud: {}
- sidebar_right:
- Info: {}
- CursorTags: {}
- BrowserTags: {}
- #SelectionTags: {}
- #Geo: {}
- window_size: 1024x768
diff --git a/tagit/apps/port_data.py b/tagit/apps/port_data.py
deleted file mode 100644
index cda2d63..0000000
--- a/tagit/apps/port_data.py
+++ /dev/null
@@ -1,127 +0,0 @@
-import os
-from tagit.utils import ns
-from tagit.utils.bsfs import URI
-
-def add_port_data(store):
- # tags
- t_hello = store.node(ns.bsfs.Tag, URI('http://example.com/me/tag#hello')) \
- .set(ns.bst.label, 'hello')
- t_world = store.node(ns.bsfs.Tag, URI('http://example.com/me/tag#world')) \
- .set(ns.bst.label, 'world')
- t_foobar = store.node(ns.bsfs.Tag, URI('http://example.com/me/tag#foobar')) \
- .set(ns.bst.label, 'foobar')
-
- # entities
- n0 = store.node(ns.bsfs.File, URI('http://example.com/me/entity#01')) \
- .set(ns.bse.filename, 'textfile.t') \
- .set(ns.bse.filesize, 100) \
- .set(ns.bse.tag, t_hello) \
- .set(ns.bse.tag, t_foobar) \
- .set(ns.bse.latitude, 47.374444) \
- .set(ns.bse.longitude, 8.541111)
- n1 = store.node(ns.bsfs.File, URI('http://example.com/me/entity#02')) \
- .set(ns.bse.filename, 'document.pdf') \
- .set(ns.bse.filesize, 200) \
- .set(ns.bse.tag, t_world) \
- .set(ns.bse.tag, t_foobar)
- n2 = store.node(ns.bsfs.File, URI('http://example.com/me/entity#03')) \
- .set(ns.bse.filename, 'document.odt') \
- .set(ns.bse.filesize, 300) \
- .set(ns.bse.tag, t_world)
- n3 = store.node(ns.bsfs.File, URI('http://example.com/me/entity#04')) \
- .set(ns.bse.filename, 'image.jpg') \
- .set(ns.bse.comment, 'some image') \
- .set(ns.bse.tag, t_hello) \
- .set(ns.bse.tag, t_foobar)
- n4 = store.node(ns.bsfs.File, URI('http://example.com/me/entity#05')) \
- .set(ns.bse.filename, 'image.png') \
- .set(ns.bse.comment, 'another image') \
- .set(ns.bse.tag, t_hello)
-
- # groups
- grp = store.node(ns.bsfs.Group, URI('http://example.com/me/group#1234'))
- grp.set(ns.bsg.represented_by, n0)
- n0.set(ns.bse.group, grp)
- n1.set(ns.bse.group, grp)
- n3.set(ns.bse.group, grp)
-
- # previews
- base = os.path.join(os.path.dirname(__file__), 'port-data')
- n0.set(ns.bse.preview,
- store.node(ns.bsfs.Preview, URI('http://example.com/me/preview#ent01_w100_h100')) \
- .set(ns.bsp.width, 100) \
- .set(ns.bsp.height, 100) \
- .set(ns.bsp.orientation, 1) \
- .set(ns.bsp.asset, open(os.path.join(base, 'ent01_w100_h100.jpg'), 'rb').read())
- )
- n0.set(ns.bse.preview,
- store.node(ns.bsfs.Preview, str('http://example.com/me/preview#ent01_w400_h200')) \
- .set(ns.bsp.width, 200) \
- .set(ns.bsp.height, 400) \
- .set(ns.bsp.orientation, 1) \
- .set(ns.bsp.asset, open(os.path.join(base, 'ent01_w400_h200.jpg'), 'rb').read())
- )
- n0.set(ns.bse.preview,
- store.node(ns.bsfs.Preview, URI('http://example.com/me/preview#ent01_w400_h400')) \
- .set(ns.bsp.width, 400) \
- .set(ns.bsp.height, 400) \
- .set(ns.bsp.orientation, 1) \
- .set(ns.bsp.asset, open(os.path.join(base, 'ent01_w400_h400.jpg'), 'rb').read())
- )
- n1.set(ns.bse.preview,
- store.node(ns.bsfs.Preview, URI('http://example.com/me/preview#ent02_w100_h100')) \
- .set(ns.bsp.width, 100) \
- .set(ns.bsp.height, 100) \
- .set(ns.bsp.orientation, 1) \
- .set(ns.bsp.asset, open(os.path.join(base, 'ent02_w100_h100.jpg'), 'rb').read())
- )
- n1.set(ns.bse.preview,
- store.node(ns.bsfs.Preview, URI('http://example.com/me/preview#ent02_w400_h200')) \
- .set(ns.bsp.width, 200) \
- .set(ns.bsp.height, 400) \
- .set(ns.bsp.orientation, 1) \
- .set(ns.bsp.asset, open(os.path.join(base, 'ent02_w400_h200.jpg'), 'rb').read())
- )
- n2.set(ns.bse.preview,
- store.node(ns.bsfs.Preview, URI('http://example.com/me/preview#ent03_w100_h100')) \
- .set(ns.bsp.width, 100) \
- .set(ns.bsp.height, 100) \
- .set(ns.bsp.orientation, 1) \
- .set(ns.bsp.asset, open(os.path.join(base, 'ent03_w100_h100.jpg'), 'rb').read())
- )
- n2.set(ns.bse.preview,
- store.node(ns.bsfs.Preview, URI('http://example.com/me/preview#ent03_w400_h200')) \
- .set(ns.bsp.width, 200) \
- .set(ns.bsp.height, 400) \
- .set(ns.bsp.orientation, 1) \
- .set(ns.bsp.asset, open(os.path.join(base, 'ent03_w400_h200.jpg'), 'rb').read())
- )
- n3.set(ns.bse.preview,
- store.node(ns.bsfs.Preview, URI('http://example.com/me/preview#ent04_w100_h100')) \
- .set(ns.bsp.width, 100) \
- .set(ns.bsp.height, 100) \
- .set(ns.bsp.orientation, 1) \
- .set(ns.bsp.asset, open(os.path.join(base, 'ent04_w100_h100.png'), 'rb').read())
- )
- n3.set(ns.bse.preview,
- store.node(ns.bsfs.Preview, URI('http://example.com/me/preview#ent04_w400_h200')) \
- .set(ns.bsp.width, 200) \
- .set(ns.bsp.height, 400) \
- .set(ns.bsp.orientation, 1) \
- .set(ns.bsp.asset, open(os.path.join(base, 'ent04_w400_h200.png'), 'rb').read())
- )
- n4.set(ns.bse.preview,
- store.node(ns.bsfs.Preview, URI('http://example.com/me/preview#ent05_w100_h100')) \
- .set(ns.bsp.width, 100) \
- .set(ns.bsp.height, 100) \
- .set(ns.bsp.orientation, 1) \
- .set(ns.bsp.asset, open(os.path.join(base, 'ent05_w100_h100.jpg'), 'rb').read())
- )
- n4.set(ns.bse.preview,
- store.node(ns.bsfs.Preview, URI('http://example.com/me/preview#ent05_w400_h200')) \
- .set(ns.bsp.width, 200) \
- .set(ns.bsp.height, 400) \
- .set(ns.bsp.orientation, 1) \
- .set(ns.bsp.asset, open(os.path.join(base, 'ent05_w400_h200.jpg'), 'rb').read())
- )
-
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/planes/browsing.svg b/tagit/assets/icons/scalable/planes/browsing.svg
deleted file mode 100644
index f502c36..0000000
--- a/tagit/assets/icons/scalable/planes/browsing.svg
+++ /dev/null
@@ -1,157 +0,0 @@
-<?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="browsing.svg"
- inkscape:export-filename="../../kivy/misc/browsing.png"
- inkscape:export-xdpi="7.6199999"
- inkscape:export-ydpi="7.6199999">
- <defs
- id="defs4">
- <marker
- inkscape:stockid="Arrow2Mend"
- orient="auto"
- refY="0"
- refX="0"
- id="Arrow2Mend"
- style="overflow:visible"
- inkscape:isstock="true">
- <path
- id="path847"
- 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)"
- inkscape:connector-curvature="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="79.928267"
- 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)">
- <circle
- style="fill:none;fill-opacity:1;stroke:#c8c8c8;stroke-width:21.39488792;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
- id="path4522"
- cx="356.2576"
- cy="511.83612"
- r="178.27893" />
- <path
- style="fill:#c8c8c8;stroke:#c8c8c8;stroke-width:25.67386436;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow2Mend);fill-opacity:1"
- d="M 294.33569,574.10683 413.77539,454.66718"
- id="path818"
- inkscape:connector-curvature="0" />
- </g>
-</svg>
diff --git a/tagit/assets/icons/scalable/planes/codash.svg b/tagit/assets/icons/scalable/planes/codash.svg
deleted file mode 100644
index b25c2b0..0000000
--- a/tagit/assets/icons/scalable/planes/codash.svg
+++ /dev/null
@@ -1,147 +0,0 @@
-<?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="codash.svg"
- inkscape:export-filename="../../kivy/misc/dashboard.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="63.505754"
- inkscape:cy="116.58788"
- 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)">
- <circle
- style="fill:none;fill-opacity:1;stroke:#c8c8c8;stroke-width:21.39488792;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
- id="path4522"
- cx="356.2576"
- cy="511.83615"
- r="178.27893" />
- <text
- xml:space="preserve"
- style="font-style:normal;font-weight:normal;font-size:103.40699768px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#c8c8c8;fill-opacity:1;stroke:none;stroke-width:2.5851748"
- x="209.33694"
- y="611.10687"
- id="text823"><tspan
- sodipodi:role="line"
- id="tspan821"
- x="209.33694"
- y="611.10687"
- style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:496.35357666px;font-family:'Berenis ADF Pro';-inkscape-font-specification:'Berenis ADF Pro Bold Italic';fill:#c8c8c8;fill-opacity:1;stroke-width:2.5851748">c</tspan></text>
- </g>
-</svg>
diff --git a/tagit/assets/icons/scalable/planes/dashboard.svg b/tagit/assets/icons/scalable/planes/dashboard.svg
deleted file mode 100644
index 6f7e4a3..0000000
--- a/tagit/assets/icons/scalable/planes/dashboard.svg
+++ /dev/null
@@ -1,142 +0,0 @@
-<?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="dashboard.svg"
- inkscape:export-filename="../../kivy/misc/dashboard.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="218.56417"
- inkscape:cy="120.62849"
- 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
- d="m 356.64939,617.51072 c 0,0 -8.76851,5.82936 -14.76239,5.80268 -0.53358,0.24456 -0.84928,0.38685 -0.84928,0.38685 l 0.0133,-0.38685 c -1.25835,-0.0578 -2.52562,-0.23566 -3.80175,-0.55581 l -1.89422,-0.46688 c -10.46706,-2.59676 -16.0074,-12.9749 -13.40175,-23.43752 l 14.57119,-58.74275 6.57637,-26.53229 c 6.11838,-24.63362 -19.40009,5.23798 -24.63363,-6.10949 -3.45493,-7.50125 19.84919,-23.24187 36.87485,-35.10959 0,0 8.75961,-5.82047 14.75794,-5.79824 0.53803,-0.249 0.85373,-0.38684 0.85373,-0.38684 l -0.0222,0.38684 c 1.26725,0.0578 2.5345,0.23567 3.81065,0.55137 l 1.89421,0.47133 c 10.46707,2.59675 16.84335,13.18388 14.2466,23.6465 l -14.56675,58.7472 -6.58972,26.53228 c -6.10949,24.63363 18.98657,-5.35359 24.21566,6.00723 3.45493,7.49235 -20.27161,23.12626 -37.29282,34.99398 z m 36.93264,-183.6139 c -3.63724,14.65123 -18.46187,23.58425 -33.10866,19.94701 -14.65566,-3.63724 -23.5887,-18.45743 -19.95146,-33.11311 3.63725,-14.65567 18.46189,-23.58425 33.10866,-19.94701 14.65569,3.63279 23.58426,18.45744 19.95146,33.11311 z"
- id="path2"
- inkscape:connector-curvature="0"
- style="fill:#c8c8c8;fill-opacity:1;stroke-width:4.44650316"
- sodipodi:nodetypes="ccccccccccccccccccccccccscc" />
- <circle
- style="fill:none;fill-opacity:1;stroke:#c8c8c8;stroke-width:21.39488792;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
- id="path4522"
- cx="356.2576"
- cy="511.83615"
- r="178.27893" />
- </g>
-</svg>
diff --git a/tagit/apps/port-schema.nt b/tagit/assets/required_schema.nt
index 7569052..d48f0bd 100644
--- a/tagit/apps/port-schema.nt
+++ b/tagit/assets/required_schema.nt
@@ -5,57 +5,55 @@ prefix xsd: <http://www.w3.org/2001/XMLSchema#>
prefix schema: <http://schema.org/>
# common bsfs prefixes
-prefix bsfs: <http://bsfs.ai/schema/>
-prefix bse: <http://bsfs.ai/schema/Entity#>
-prefix bst: <http://bsfs.ai/schema/Tag#>
-prefix bsg: <http://bsfs.ai/schema/Group#>
-prefix bsp: <http://bsfs.ai/schema/Preview#>
+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
-bsfs:Entity rdfs:subClassOf bsfs:Node .
-bsfs:Preview rdfs:subClassOf bsfs:Node .
-bsfs:File rdfs:subClassOf bsfs:Entity .
-bsfs:Tag rdfs:subClassOf bsfs:Node .
-bsfs:Group rdfs:subClassOf bsfs:Node .
+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
-bsfs:BinaryBlob rdfs:subClassOf bsfs:Literal .
-bsfs:URI rdfs:subClassOf bsfs:Literal .
-bsfs:Number rdfs:subClassOf bsfs:Literal .
-bsfs:Time rdfs:subClassOf bsfs:Literal .
-bsfs:JPEG rdfs:subClassOf bsfs:BinaryBlob .
+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 bsfs:Number .
+xsd:integer rdfs:subClassOf bsl:Number .
+xsd:float rdfs:subClassOf bsl:Number .
bse:filename rdfs:subClassOf bsfs:Predicate ;
- rdfs:domain bsfs:File ;
+ rdfs:domain bsn:Entity ;
rdfs:range xsd:string ;
- rdfs:label "File name"^^xsd:string ;
- schema:description "Filename of entity in some filesystem."^^xsd:string ;
bsfs:unique "true"^^xsd:boolean .
bse:filesize rdfs:subClassOf bsfs:Predicate ;
- rdfs:domain bsfs:File ;
+ rdfs:domain bsn:Entity ;
rdfs:range xsd:integer ;
- rdfs:label "File size"^^xsd:string ;
- schema:description "File size of entity in some filesystem."^^xsd:string ;
bsfs:unique "true"^^xsd:boolean .
bse:mime rdfs:subClassOf bsfs:Predicate ;
- rdfs:domain bsfs:File ;
+ rdfs:domain bsn:Entity ;
rdfs:range xsd:string ;
bsfs:unique "true"^^xsd:boolean .
bse:preview rdfs:subClassOf bsfs:Predicate ;
- rdfs:domain bsfs:Entity ;
- rdfs:range bsfs:Preview .
+ rdfs:domain bsn:Entity ;
+ rdfs:range bsn:Preview .
bse:tag rdfs:subClassOf bsfs:Predicate ;
- rdfs:domain bsfs:File ;
- rdfs:range bsfs:Tag .
+ rdfs:domain bsn:Entity ;
+ rdfs:range bsn:Tag .
bst:label rdfs:subClassOf bsfs:Predicate ;
- rdfs:domain bsfs:Tag ;
+ rdfs:domain bsn:Tag ;
rdfs:range xsd:string ;
bsfs:unique "true"^^xsd:boolean .
@@ -64,45 +62,49 @@ bse:comment rdfs:subClassOf bsfs:Predicate ;
rdfs:range xsd:string .
bse:group rdfs:subClassOf bsfs:Predicate ;
- rdfs:domain bsfs:Entity ;
- rdfs:range bsfs:Group .
+ 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 bsfs:Group ;
- rdfs:range bsfs:File ;
+ rdfs:domain bsn:Group ;
+ rdfs:range bsn:Entity ;
bsfs:unique "true"^^xsd:boolean .
bse:longitude rdfs:subClassOf bsfs:Predicate ;
- rdfs:domain bsfs:File ;
- rdfs:range xsd:integer ;
+ rdfs:domain bsn:Entity ;
+ rdfs:range xsd:float ;
bsfs:unique "true"^^xsd:boolean .
bse:latitude rdfs:subClassOf bsfs:Predicate ;
- rdfs:domain bsfs:File ;
- rdfs:range xsd:integer ;
+ rdfs:domain bsn:Entity ;
+ rdfs:range xsd:float ;
bsfs:unique "true"^^xsd:boolean .
## preview nodes
bsp:width rdfs:subClassOf bsfs:Predicate ;
- rdfs:domain bsfs:Preview ;
+ rdfs:domain bsn:Preview ;
rdfs:range xsd:integer ;
bsfs:unique "true"^^xsd:boolean .
bsp:height rdfs:subClassOf bsfs:Predicate ;
- rdfs:domain bsfs:Preview ;
+ rdfs:domain bsn:Preview ;
rdfs:range xsd:integer ;
bsfs:unique "true"^^xsd:boolean .
bsp:orientation rdfs:subClassOf bsfs:Predicate ;
- rdfs:domain bsfs:Preview ;
+ rdfs:domain bsn:Preview ;
rdfs:range xsd:integer ;
bsfs:unique "true"^^xsd:boolean .
bsp:asset rdfs:subClassOf bsfs:Predicate ;
- rdfs:domain bsfs:Preview ;
- rdfs:range bsfs:JPEG ;
+ 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/loader.py b/tagit/config/loader.py
index 87ac328..47a51fa 100644
--- a/tagit/config/loader.py
+++ b/tagit/config/loader.py
@@ -54,10 +54,8 @@ def load_settings(path=None, verbose=0):
searchpaths += SETTINGS_PATH
# create default user config on first start
- first_start = False
user_config = os.path.expanduser(os.path.join('~', TAGITRC))
if os.path.exists(DEFAULT_USER_CONFIG) and not os.path.exists(user_config):
- first_start = True
shutil.copy(DEFAULT_USER_CONFIG, user_config)
# scan searchpaths
@@ -76,8 +74,6 @@ def load_settings(path=None, verbose=0):
# update verbosity from argument
cfg.set(('session', 'verbose'), max(cfg('session', 'verbose'), verbose))
- # set first start according to previous user config existence
- cfg.set(('session', 'first_start'), first_start)
return cfg
## EOF ##
diff --git a/tagit/config/settings.json b/tagit/config/settings.json
deleted file mode 100644
index 8fd4754..0000000
--- a/tagit/config/settings.json
+++ /dev/null
@@ -1,22 +0,0 @@
-{
- "ui": {
- "standalone": {
- "keytriggers": [
- ],
- "buttondocks": {
- "filter": [
- ],
- "navigation_left": [
- ],
- "navigation_right": [
- ],
- "status": [
- ]
- },
- "context": {
- "root": [
- ]
- }
- }
- }
-}
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/user-defaults.json b/tagit/config/user-defaults.json
deleted file mode 100644
index 1ff7ab4..0000000
--- a/tagit/config/user-defaults.json
+++ /dev/null
@@ -1,134 +0,0 @@
-{
- "session": {
- "first_start": false,
- "paths": {
- "searchlog": "~/.tagit.log"
- }
- },
- "storage": {
- "index": {
- "preview_size": [
- 50,
- 200,
- 400
- ]
- }
- },
- "ui": {
- "standalone": {
- "window_size": "1024x768",
- "browser": {
- "maxrows": 8,
- "maxcols": 8
- },
- "buttondocks": {
- "sidebar_left": [
- "Menu",
- "ShowDashboard",
- "AddTag",
- "EditTag",
- "CreateGroup",
- "DissolveGroup",
- "SelectAll",
- "SelectNone",
- "SelectInvert",
- "SelectAdditive",
- "SelectSubtractive",
- "SelectSingle",
- "SelectMulti",
- "SelectRange"
- ]
- },
- "tiledocks": {
- "dashboard": {
- "Buttons": {
- "buttons": [
- "ShowBrowsing",
- "CreateSession",
- "CreateTempSession",
- "LoadSession",
- "ReloadSession",
- "ImportObjects",
- "ItemExport",
- "UpdateSelectedObjects",
- "ShowHelp",
- "ShowSettings"
- ]
- },
- "LibSummary": {},
- "Hints": {},
- "TagHistogram": {},
- "Tagcloud": {},
- "Searchtree": {}
- },
- "sidebar_right": {
- "Info": {},
- "CursorTags": {},
- "Venn": {}
- }
- },
- "search": {
- "sort_blacklist": [
- "entity",
- "flash",
- "latitude",
- "longitude",
- "mime",
- "author",
- "camera",
- "attributes"
- ]
- },
- "context": {
- "app": [
- "ShowSettings",
- "ShowHelp",
- "ShowConsole"
- ],
- "session": [
- "ItemExport",
- "ImportObjects"
- ],
- "search": [
- "ShowSelected",
- "RemoveSelected"
- ],
- "browser": [
- "ZoomIn",
- "ZoomOut"
- ],
- "select": [
- "SelectAll",
- "SelectNone",
- "SelectInvert",
- "SelectSingle",
- "SelectMulti",
- "SelectRange",
- "SelectAdditive",
- "SelectSubtractive"
- ],
- "clipboard": [
- "ClipboardCopy",
- "ClipboardPaste"
- ],
- "tagging": [
- "AddTag",
- "EditTag",
- "SetRank1",
- "SetRank3",
- "SetRank5"
- ],
- "grouping": [
- "CreateGroup",
- "DissolveGroup",
- "AddToGroup",
- "RepresentGroup",
- "RemoveFromGroup"
- ],
- "root": [
- "CloseSessionAndExit"
- ]
- }
- }
- }
-}
diff --git a/tagit/config/user-defaults.yaml b/tagit/config/user-defaults.yaml
index 3ec23d0..447e10f 100644
--- a/tagit/config/user-defaults.yaml
+++ b/tagit/config/user-defaults.yaml
@@ -1,56 +1,95 @@
session:
- first_start: false
- paths:
- searchlog: ~/.tagit.log
-storage:
- index:
- preview_size:
- - 50
- - 200
- - 400
+ 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:
- - Menu
- - ShowDashboard
- AddTag
- EditTag
- - CreateGroup
- - DissolveGroup
+ - ShowSelected
+ - RemoveSelected
- SelectAll
- SelectNone
- SelectInvert
- - SelectAdditive
- - SelectSubtractive
- SelectSingle
- - SelectMulti
- SelectRange
+ - SelectMulti
+ - SelectAdditive
+ - SelectSubtractive
context:
app:
- - ShowSettings
- ShowHelp
- ShowConsole
+ - ShowSettings
browser:
- ZoomIn
- ZoomOut
- clipboard:
- - ClipboardCopy
- - ClipboardPaste
- grouping:
- - CreateGroup
- - DissolveGroup
- - AddToGroup
- - RepresentGroup
- - RemoveFromGroup
- root:
- - CloseSessionAndExit
+ - MoveCursorFirst
+ - PreviousPage
+ - ScrollUp
+ - ScrollDown
+ - NextPage
+ - MoveCursorLast
search:
+ - AddToken
+ - SortOrder
- ShowSelected
- RemoveSelected
+ - GoForth
+ - GoBack
select:
- SelectAll
- SelectNone
@@ -60,46 +99,9 @@ ui:
- SelectRange
- SelectAdditive
- SelectSubtractive
- session:
- - ItemExport
- - ImportObjects
tagging:
- AddTag
- EditTag
- - SetRank1
- - SetRank3
- - SetRank5
- search:
- sort_blacklist:
- - entity
- - flash
- - latitude
- - longitude
- - mime
- - author
- - camera
- - attributes
tiledocks:
- dashboard:
- Buttons:
- buttons:
- - ShowBrowsing
- - CreateSession
- - CreateTempSession
- - LoadSession
- - ReloadSession
- - ImportObjects
- - ItemExport
- - UpdateSelectedObjects
- - ShowHelp
- - ShowSettings
- Hints: {}
- LibSummary: {}
- Searchtree: {}
- TagHistogram: {}
- Tagcloud: {}
sidebar_right:
- CursorTags: {}
Info: {}
- Venn: {}
- window_size: 1024x768
diff --git a/tagit/dialogues/dialogue.kv b/tagit/dialogues/dialogue.kv
index e23f0db..e2cab66 100644
--- a/tagit/dialogues/dialogue.kv
+++ b/tagit/dialogues/dialogue.kv
@@ -6,68 +6,28 @@
ok_on_enter: True
<DialogueContentBase>:
-
orientation: 'vertical'
padding: '12dp'
size_hint: 0.66, None
height: self.minimum_height
- 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
-
<DialogueContentNoTitle@DialogueContentBase>:
# nothing to do
+
+<DialogueTitle@Label>:
+
<DialogueContentTitle@DialogueContentBase>:
title: ''
title_color: 1,1,1,1
- Label:
+ DialogueTitle:
text: root.title
size_hint_y: None
height: self.texture_size[1] + dp(16)
text_size: self.width - dp(16), None
- font_size: '16sp'
color: root.title_color
- 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
# small space
Label:
@@ -75,7 +35,10 @@
height: 12
-<DialogueButtons>:
+
+<DialogueButton@Button>:
+
+<DialogueButtonRow>:
orientation: 'vertical'
size_hint_y: None
height: dp(48+8)
@@ -88,25 +51,25 @@
# here come the buttons
-<DialogueButtons_One@DialogueButtons>:
+<DialogueButtons_One@DialogueButtonRow>:
ok_text: 'OK'
- Button:
+ DialogueButton:
text: root.ok_text
on_press: get_root(self).ok()
-<DialogueButtons_Two@DialogueButtons>:
+<DialogueButtons_Two@DialogueButtonRow>:
cancel_text: 'Cancel'
ok_text: 'OK'
ok_enabled: True
BoxLayout:
orientation: 'horizontal'
- Button:
+ DialogueButton:
text: root.cancel_text
on_press: get_root(self).cancel()
- Button:
+ DialogueButton:
text: root.ok_text
on_press: get_root(self).ok()
disabled: not root.ok_enabled
diff --git a/tagit/dialogues/dialogue.py b/tagit/dialogues/dialogue.py
index 1aa0e9a..bf72a28 100644
--- a/tagit/dialogues/dialogue.py
+++ b/tagit/dialogues/dialogue.py
@@ -101,8 +101,8 @@ class DialogueContentTitle(DialogueContentBase): pass
class DialogueContentNoTitle(DialogueContentBase): pass
# buttons
-class DialogueButtons(BoxLayout): pass
-class DialogueButtons_One(DialogueButtons): pass
-class DialogueButtons_Two(DialogueButtons): pass
+class DialogueButtonRow(BoxLayout): pass
+class DialogueButtons_One(DialogueButtonRow): pass
+class DialogueButtons_Two(DialogueButtonRow): pass
## EOF ##
diff --git a/tagit/external/setproperty/README.md b/tagit/external/setproperty/README.md
index e579132..4849b6c 100644
--- a/tagit/external/setproperty/README.md
+++ b/tagit/external/setproperty/README.md
@@ -1,5 +1,27 @@
-build with
+# 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/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/setup.py b/tagit/external/setproperty/setup.py
index 8500340..bd95d70 100644
--- a/tagit/external/setproperty/setup.py
+++ b/tagit/external/setproperty/setup.py
@@ -4,3 +4,4 @@ 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/parsing/filter/from_string.py b/tagit/parsing/filter/from_string.py
index ed24f63..60e5c47 100644
--- a/tagit/parsing/filter/from_string.py
+++ b/tagit/parsing/filter/from_string.py
@@ -69,7 +69,7 @@ class FromString():
> 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.bsfs.Entity)}
+ predicates = {pred for pred in self.schema.predicates() if pred.domain <= self.schema.node(ns.bsn.Entity)}
# filter through accept/reject lists
... # FIXME
# shortcuts
@@ -78,9 +78,9 @@ class FromString():
# 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.bsfs.Number)}
+ _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.bsfs.Time)}
+ 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}
diff --git a/tagit/parsing/filter/to_string.py b/tagit/parsing/filter/to_string.py
index 0b1a3e1..6a1b035 100644
--- a/tagit/parsing/filter/to_string.py
+++ b/tagit/parsing/filter/to_string.py
@@ -20,7 +20,7 @@ class ToString():
self.matches = matcher.Filter()
self.schema = schema
- predicates = {pred for pred in self.schema.predicates() if pred.domain <= self.schema.node(ns.bsfs.Entity)}
+ 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()}
@@ -206,10 +206,10 @@ class ToString():
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.value)
+ 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 for guid in get_guids(sub.value) }
+ guids = {guid for sub in query.expr for guid in get_guids(sub.value) }
if len(guids) == 0:
# no matches
diff --git a/tagit/tiles/decoration.kv b/tagit/tiles/decoration.kv
index a53d013..ae7e49d 100644
--- a/tagit/tiles/decoration.kv
+++ b/tagit/tiles/decoration.kv
@@ -55,4 +55,33 @@
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
index 471058d..c772f64 100644
--- a/tagit/tiles/decoration.py
+++ b/tagit/tiles/decoration.py
@@ -16,6 +16,7 @@ import kivy.properties as kp
# exports
__all__: typing.Sequence[str] = (
'TileDecorationBorder',
+ 'TileDecorationRoundedBorder',
'TileDecorationFilledRectangle',
'TileDecorationVanilla',
)
@@ -65,4 +66,12 @@ class TileDecorationBorder(TileDecoration):
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/info.py b/tagit/tiles/info.py
index 725e098..9555b35 100644
--- a/tagit/tiles/info.py
+++ b/tagit/tiles/info.py
@@ -11,8 +11,9 @@ from collections import OrderedDict
from kivy.lang import Builder
# tagit imports
-from tagit.utils import ttime, ns
+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
@@ -29,15 +30,19 @@ Builder.load_string('''
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(65, self.width * 0.4)
+ #default_size: None, 7*self.font_size + 6*5
+ keywidth: min(75, self.width * 0.4)
''')
# classes
-class Info(TileTabular, BrowserAwareMixin):
+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:
@@ -53,6 +58,9 @@ class Info(TileTabular, BrowserAwareMixin):
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:
@@ -61,23 +69,17 @@ class Info(TileTabular, BrowserAwareMixin):
else:
preds = cursor.get(
- ns.bsm.t_created,
+ ns.bsfs.Node().t_created,
ns.bse.filesize,
ns.bse.filename,
- ns.bse.comment,
+ (ns.bse.tag, ns.bst.label),
)
self.tabledata = OrderedDict({
'Date' : ttime.from_timestamp_utc(
- preds.get(ns.bsm.t_created, ttime.timestamp_min)).strftime('%d.%m.%y %H:%M'),
- #'Size' : f'{preds.get(ns.bse.width, "?")} x {preds.get(ns.bse.height, "?")}',
- #'ISO' : preds.get(ns.bse.iso, '?'),
- 'Filesize' : preds.get(ns.bse.filesize, 'n/a'),
+ 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'),
- 'Comment' : '; '.join(preds.get(ns.bse.comment, [])),
- #'Exposure' : f'1 / {preds.get(ns.bse.exposure, 0):0.2f}',
- #'Aperture' : preds.get(ns.bse.aperture, '?'),
- #'Fx' : preds.get(ns.bse.focal_length_35, '?'),
- #'Flash' : 'Yes' if preds.get(ns.bse.flash, False) else 'No',
+ 'Tags' : ', '.join(sorted(preds.get((ns.bse.tag, ns.bst.label), [' ']))),
})
## EOF ##
diff --git a/tagit/tiles/tile.kv b/tagit/tiles/tile.kv
index 277b23d..fcd9821 100644
--- a/tagit/tiles/tile.kv
+++ b/tagit/tiles/tile.kv
@@ -7,16 +7,28 @@
<TileTabular>:
# config
font_size: sp(15)
- spacer: 10
keywidth: 0.5
# table
- grid: grid
- GridLayout:
- id: grid
- cols: 2
- size: root.size
- size_hint: None, None
+ 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: ''
diff --git a/tagit/tiles/tile.py b/tagit/tiles/tile.py
index 7ea690a..981d45b 100644
--- a/tagit/tiles/tile.py
+++ b/tagit/tiles/tile.py
@@ -9,6 +9,7 @@ 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
@@ -38,51 +39,47 @@ class Tile(RelativeLayout):
class TileWithLabel(Tile):
pass
+class TileTabularRow(BoxLayout):
+ pass
+
+class TileTabularCell(Label):
+ pass
class TileTabular(Tile):
tabledata = kp.ObjectProperty()
- spacer = kp.NumericProperty(10)
keywidth = kp.NumericProperty(0.5)
- def on_size(self, wx, size):
- if not self.visible or len(self.grid.children) == 0:
- return
-
- height = self.height / (len(self.grid.children) / 2)
- kwidth = self.width * self.keywidth if self.keywidth < 1 else self.keywidth
- vwidth = self.width - kwidth - self.spacer
-
- # adjust size
- for idx, child in enumerate(self.grid.children):
- if idx % 2 == 1: # key
- child.text_size = kwidth, height
- child.width = kwidth
- else: # label
- child.text_size = vwidth, height
-
def on_tabledata(self, wx, data):
# set items
- self.grid.clear_widgets()
- for key, value in data.items():
+ self.rows.clear_widgets()
+ for t_key, t_value in data.items():
+ # row
+ row = TileTabularRow()
# left column (keys)
- self.grid.add_widget(Label(
- text=key,
+ key = TileTabularCell(
+ text=t_key,
halign='right',
- valign='top',
font_size = self.font_size,
- size_hint = (None, 1),
- ))
-
+ size_hint=(None, 1),
+ width=self.width * self.keywidth if self.keywidth < 1 else self.keywidth,
+ )
# right column (values)
- self.grid.add_widget(Label(
- text=str(value),
+ value = TileTabularCell(
+ text=str(t_value),
halign='left',
- valign='top',
font_size = self.font_size,
- ))
-
- # set sizes
- self.on_size(self, self.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/namespaces.py b/tagit/utils/namespaces.py
index dbdb853..a17a927 100644
--- a/tagit/utils/namespaces.py
+++ b/tagit/utils/namespaces.py
@@ -11,29 +11,31 @@ import typing
from . import bsfs as _bsfs
# generic namespaces
-xsd = _bsfs.Namespace('http://www.w3.org/2001/XMLSchema')
+xsd = _bsfs.Namespace('http://www.w3.org/2001/XMLSchema')()
# core bsfs namespaces
-bsfs = _bsfs.Namespace('http://bsfs.ai/schema', fsep='/')
-bsm = _bsfs.Namespace('http://bsfs.ai/schema/Meta')
+bsfs = _bsfs.Namespace('https://schema.bsfs.io/core')
+bsie = _bsfs.Namespace('https://schema.bsfs.io/ie')
# auxiliary bsfs namespaces
-bse = _bsfs.Namespace('http://bsfs.ai/schema/Entity')
-bsg = _bsfs.Namespace('http://bsfs.ai/schema/Group')
-bsp = _bsfs.Namespace('http://bsfs.ai/schema/Preview')
-bst = _bsfs.Namespace('http://bsfs.ai/schema/Tag')
+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',
- 'bsm',
+ 'bsie',
+ 'bsl',
+ 'bsn',
'bsp',
'bst',
'xsd',
)
## EOF ##
-
-
diff --git a/tagit/widgets/browser.kv b/tagit/widgets/browser.kv
index 8b4c8c3..63495be 100644
--- a/tagit/widgets/browser.kv
+++ b/tagit/widgets/browser.kv
@@ -11,19 +11,6 @@
is_cursor: False
is_selected: False
- canvas.after:
- Color:
- rgba: 1,1,1, 1 if self.is_cursor else 0
- Line:
- width: 2
- rectangle: self.x, self.y, self.width, self.height
-
- Color:
- rgba: self.scolor + [0.5 if self.is_selected else 0]
- Rectangle:
- pos: self.x, self.center_y - int(self.height) / 2
- size: self.width, self.height
-
<BrowserImage>: # This be an image
preview: image
@@ -58,6 +45,11 @@
# opacity: root.is_group and 1.0 or 0.0
# show: 'image',
+<BrowserDescriptionLabel@Label>:
+ halign: 'left'
+ valign: 'center'
+ text_size: self.size
+
<BrowserDescription>: # This be a list item
spacer: 20
preview: image
@@ -68,12 +60,9 @@
# actual size is set in code
pos: 0, 0
- Label:
+ BrowserDescriptionLabel:
text: root.text
markup: True
- halign: 'left'
- valign: 'center'
- text_size: self.size
size_hint: None, 1
width: root.width - image.width - root.spacer - 35
pos: root.height + root.spacer, 0
diff --git a/tagit/widgets/browser.py b/tagit/widgets/browser.py
index 1e42c9c..17d99ed 100644
--- a/tagit/widgets/browser.py
+++ b/tagit/widgets/browser.py
@@ -171,7 +171,8 @@ class Browser(GridLayout, StorageAwareMixin, ConfigAwareMixin):
"""
# get groups and their shadow (group's members in items)
groups = defaultdict(set)
- for obj, grp in reduce(operator.add, items).group(node=True, view=list):
+ all_items = reduce(operator.add, items, self.root.session.storage.empty(ns.bsn.Entity))
+ for obj, grp in all_items.group(node=True, view=list):
groups[grp].add(obj)
# don't fold groups if few members
@@ -218,7 +219,7 @@ class Browser(GridLayout, StorageAwareMixin, ConfigAwareMixin):
unfolded |= self.folds[itm].shadow
else:
unfolded |= {itm}
- return reduce(operator.add, unfolded) # FIXME: What if items is empty?
+ return reduce(operator.add, unfolded, self.root.session.storage.empty(ns.bsn.Entity))
def neighboring_unselected(self):
"""Return the item closest to the cursor and not being selected. May return None."""
@@ -281,7 +282,6 @@ class Browser(GridLayout, StorageAwareMixin, ConfigAwareMixin):
def on_cursor(self, sender, cursor):
if cursor is not None:
- #self.root.status.dispatch('on_status', os.path.basename(next(iter(cursor.guids))))
self.root.status.dispatch('on_status', cursor.filename(default=''))
def on_items(self, sender, items):
@@ -347,10 +347,7 @@ class Browser(GridLayout, StorageAwareMixin, ConfigAwareMixin):
self.clear_widgets()
for itm in range(self.page_size):
- wx = factory(
- browser=self,
- scolor=self.root.session.cfg('ui', 'standalone', 'browser', 'select_color'),
- )
+ wx = factory(browser=self)
self.bind(selection=wx.on_selection)
self.bind(cursor=wx.on_cursor)
self.add_widget(wx)
@@ -398,18 +395,30 @@ class Browser(GridLayout, StorageAwareMixin, ConfigAwareMixin):
if len(items) == 0: # FIXME: mb/port
return
+ resolution = self._cell_resolution()
+ previews = self._fetch_previews(items, resolution)
+ default = resource_find('no_preview.png')
+ for ent, child in zip(reversed(items), childs):
+ if ent in previews:
+ buf = previews[ent]
+ else:
+ buf = open(default, 'rb')
+ child.update(ent, buf, f'{ent}x{resolution}')
+
+ def _fetch_previews(self, items, resolution):
+ """Fetch previews matching *resolution* for *items*.
+ Return a dict with items as key and a BytesIO as value.
+ Items without valid asset are omitted from the dict.
+ """
# fetch previews
node_preview = reduce(operator.add, items).get(ns.bse.preview, node=True)
previews = {p for previews in node_preview.values() for p in previews}
- previews = reduce(operator.add, previews)
+ previews = reduce(operator.add, previews) # FIXME: empty previews
# fetch preview resolutions
res_preview = previews.get(ns.bsp.width, ns.bsp.height, node=True)
- # get target resolution
- resolution = self._cell_resolution()
- # get default preview
- default = resource_find('no_preview.png')
# select a preview for each item
- for ent, child in zip(reversed(items), childs):
+ chosen = {}
+ for ent in items:
try:
# get previews and their resolution for this ent
options = []
@@ -420,19 +429,21 @@ class Browser(GridLayout, StorageAwareMixin, ConfigAwareMixin):
height = res.get(ns.bsp.height, 0)
options.append((preview, Resolution(width, height)))
# select the best fitting preview
- chosen = rmatcher.by_area_min(resolution, options)
- # open the preview file, default if no asset is available
- thumb_data = chosen.asset(default=None) # FIXME: get all assets in one call
- if thumb_data is None:
- raise KeyError()
- thumb = io.BytesIO(thumb_data)
-
+ chosen[ent] = rmatcher.by_area_min(resolution, options)
except (KeyError, IndexError):
- # KeyError:
- # no viable resolution found
- thumb = open(default, 'rb')
- # update the image in the child widget
- child.update(ent, thumb, f'{ent}x{resolution}')
+ # skip objects w/o preview (KeyError in node_preview)
+ # skip objects w/o valid preview (IndexError in rmatcher)
+ pass
+
+ # fetch assets
+ assets = reduce(operator.add, chosen.values()).asset(node=True) # FIXME: empty chosen
+ # build ent -> asset mapping and convert raw data to io buffer
+ return {
+ ent: io.BytesIO(assets[thumb])
+ for ent, thumb
+ in chosen.items()
+ if thumb in assets
+ }
#def _preload_all(self):
# # prefer loading from start to end
@@ -440,26 +451,24 @@ class Browser(GridLayout, StorageAwareMixin, ConfigAwareMixin):
def _preload_items(self, items, resolution=None):
"""Load an item into the kivy *Cache* without displaying the image anywhere."""
- resolution = resolution if resolution is not None else self._cell_resolution()
-
def _buf_loader(buffer, fname):
# helper method to load the image from a raw buffer
with buffer as buf:
return ImageLoaderTagit(filename=fname, inline=True, rawdata=buf)
- for obj in items:
- try:
- buffer = obj.get('preview').best_match(resolution)
- source = f'{obj.guid}x{resolution}'
-
- Loader.image(source,
- nocache=False, mipmap=False,
- anim_delay=0,
- load_callback=partial(_buf_loader, buffer) # mb: pass load_callback
- )
-
- except PredicateNotSet:
- pass
+ resolution = resolution if resolution is not None else self._cell_resolution()
+ try:
+ foo = self._fetch_previews(items, resolution) # FIXME: _fetch_previews fails on empty previews/chosen
+ except TypeError:
+ return
+ for obj, buffer in foo.items():
+ guid = ','.join(obj.guids)
+ source = f'{guid}x{resolution}'
+ Loader.image(source,
+ nocache=False, mipmap=False,
+ anim_delay=0,
+ load_callback=partial(_buf_loader, buffer) # mb: pass load_callback
+ )
class BrowserAwareMixin(object):
@@ -484,7 +493,6 @@ class BrowserItem(RelativeLayout):
is_cursor = kp.BooleanProperty(False)
is_selected = kp.BooleanProperty(False)
is_group = kp.BooleanProperty(False)
- scolor = kp.ListProperty([1, 0, 0]) # FIXME: set from config
def update(self, obj):
self.obj = obj
@@ -572,7 +580,7 @@ class BrowserDescription(BrowserItem):
grp = self.browser.folds[self.obj].group
# FIXME: Here we could actually use a predicate reversal for Nodes.get
# members = grp.get(ast.fetch.Node(ast.fetch.Predicate(ns.bse.group, reverse=True)))
- members = self.browser.root.session.storage.get(ns.bsfs.File,
+ members = self.browser.root.session.storage.get(ns.bsn.Entity,
ast.filter.Any(ns.bse.group, ast.filter.Is(grp)))
# get group member's tags
member_tags = members.tag.label(node=True)
diff --git a/tagit/widgets/filter.kv b/tagit/widgets/filter.kv
index b638570..5407610 100644
--- a/tagit/widgets/filter.kv
+++ b/tagit/widgets/filter.kv
@@ -1,4 +1,5 @@
#:import SearchmodeSwitch tagit.actions.filter
+#:import AddToken tagit.actions.filter
#-- #:import SortKey tagit.actions.search
<Filter>:
@@ -7,20 +8,36 @@
spacing: 5
tokens: tokens
- BoxLayout:
- orientation: 'horizontal'
- spacing: 10
- id: tokens
-
- # Tokens will be inserted here
-
- SearchmodeSwitch:
- show: 'image',
+ Widget:
+ size_hint_x: None
+ width: 5
+
+ ScrollView:
+ do_scroll_x: True
+ do_scroll_y: False
+ size_hint: 1, 1
+
+ BoxLayout:
+ orientation: 'horizontal'
+ spacing: 10
+ id: tokens
+ size_hint: None, None
+ height: 35
+ width: self.minimum_width
+ # Tokens will be inserted here
+
+ AddToken:
+ show: 'image',
root: root.root
- SortKey:
- show: 'image',
- root: root.root
+ # FIXME: Temporarily disabled
+ #SearchmodeSwitch:
+ # show: 'image',
+ # root: root.root
+
+ #SortKey:
+ # show: 'image',
+ # root: root.root
SortOrder:
show: 'image',
@@ -30,55 +47,43 @@
root: root.root
name: 'filter'
orientation: 'lr-tb'
- # space for 2 buttons
- width: 3*30 + 2*5
- size_hint: None, 1.0
+ # space for two buttons
+ width: 2*30 + 5
spacing: 5
+ size_hint: None, None
+ height: 35
button_height: 30
button_show: 'image',
+<Avatar@Label>:
+ active: False
+
+<ShingleText@Label>:
+ active: False
+
<Shingle>:
orientation: 'horizontal'
label: tlabel
-
- canvas.before:
- Color:
- rgba: 0,0,1, 0.25 if root.active else 0
- Rectangle:
- pos: root.pos
- size: root.size
-
- canvas.after:
- Color:
- rgba: 1,1,1,1
- Line:
- rectangle: self.x+1, self.y+1, self.width-1, self.height-1
-
- Label:
+ size_hint: None, None
+ width: self.minimum_width
+ height: 30
+
+ Avatar:
+ id: avatar
+ size_hint: None, None
+ text: root.avatar
+ width: self.parent.height
+ height: self.parent.height
+ active: root.active
+
+ ShingleText:
id: tlabel
text: root.text
-
- canvas.after:
- Color:
- rgba: 0,0,0,0.5 if not root.active else 0
- Rectangle:
- pos: self.pos
- size: self.size
-
-
- Button:
- text: 'x'
- bold: True
- opacity: 0.5
- width: 20
- size_hint: None, 1.0
- background_color: [0,0,0,0]
- background_normal: ''
- on_press: root.remove()
+ active: root.active
+ width: (self.texture_size[0] + dp(20)) if self.text != '' else 0
+ size_hint_x: None
<Addressbar>:
multiline: False
- background_color: (0.2,0.2,0.2,1) if self.focus else (0.15,0.15,0.15,1)
- foreground_color: (1,1,1,1)
## EOF ##
diff --git a/tagit/widgets/filter.py b/tagit/widgets/filter.py
index 15aefd6..1382c43 100644
--- a/tagit/widgets/filter.py
+++ b/tagit/widgets/filter.py
@@ -120,26 +120,46 @@ class Filter(BoxLayout, ConfigAwareMixin):
return query, sort
def abbreviate(self, token):
+ # FIXME: Return image
matches = matcher.Filter()
if matches(token, ast.filter.Any(ns.bse.tag, ast.filter.Any(ns.bst.label, matcher.Any()))):
# tag token
- return self.root.session.filter_to_string(token)
+ return 'T'
if matches(token, matcher.Partial(ast.filter.Is)) or \
matches(token, ast.filter.Or(matcher.Rest(matcher.Partial(ast.filter.Is)))):
# exclusive token
- return 'E'
+ return '='
if matches(token, ast.filter.Not(matcher.Partial(ast.filter.Is))) or \
matches(token, ast.filter.Not(ast.filter.Or(matcher.Rest(matcher.Partial(ast.filter.Is))))):
# reduce token
- return 'R'
+ return '—'
if matches(token, ast.filter.Any(ns.bse.group, matcher.Any())):
# group token
return 'G'
if matches(token, ast.filter.Any(matcher.Partial(ast.filter.Predicate), matcher.Any())):
# generic token
- return token.predicate.predicate.get('fragment', '?').title()
+ #return token.predicate.predicate.get('fragment', '?').title()[0]
+ return 'P'
return '?'
+ def tok_label(self, token):
+ matches = matcher.Filter()
+ if matches(token, ast.filter.Any(ns.bse.tag, ast.filter.Any(ns.bst.label, matcher.Any()))):
+ # tag token
+ return self.root.session.filter_to_string(token)
+ if matches(token, matcher.Partial(ast.filter.Is)) or \
+ matches(token, ast.filter.Not(matcher.Partial(ast.filter.Is))):
+ return '1'
+ if matches(token, ast.filter.Or(matcher.Rest(matcher.Partial(ast.filter.Is)))):
+ return str(len(token))
+ if matches(token, ast.filter.Not(ast.filter.Or(matcher.Rest(matcher.Partial(ast.filter.Is))))):
+ return str(len(token.expr))
+ if matches(token, ast.filter.Any(matcher.Partial(ast.filter.Predicate), matcher.Any())):
+ # generic token
+ #return self.root.session.filter_to_string(token)
+ return token.predicate.predicate.get('fragment', '')
+ return ''
+
def show_address_once(self):
"""Single-shot address mode without changing the search mode."""
self.tokens.clear_widgets()
@@ -189,7 +209,8 @@ class Filter(BoxLayout, ConfigAwareMixin):
Shingle(
tok,
active=(tok in self.t_head),
- text=self.abbreviate(tok),
+ avatar=self.abbreviate(tok),
+ text=self.tok_label(tok),
root=self.root
))
@@ -235,6 +256,7 @@ class Shingle(BoxLayout):
# content
active = kp.BooleanProperty(False)
text = kp.StringProperty('')
+ avatar = kp.StringProperty('')
# touch behaviour
_single_tap_action = None
@@ -249,7 +271,7 @@ class Shingle(BoxLayout):
def on_touch_down(self, touch):
"""Edit shingle when touched."""
- if self.label.collide_point(*touch.pos):
+ if self.collide_point(*touch.pos):
if touch.is_double_tap: # edit filter
# ignore touch, such that the dialogue
# doesn't loose the focus immediately after open
diff --git a/tagit/widgets/session.py b/tagit/widgets/session.py
index c233a15..30dfe51 100644
--- a/tagit/widgets/session.py
+++ b/tagit/widgets/session.py
@@ -68,13 +68,13 @@ class Session(Widget):
# open BSFS storage
store = bsfs.Open(cfg('session', 'bsfs'))
# check storage schema
- # FIXME: how to properly load the required schema?
- with open(os.path.join(os.path.dirname(__file__), '..', 'apps', 'port-schema.nt'), 'rt') as ifile:
+ with open(resource_find('required_schema.nt'), 'rt') as ifile:
required_schema = bsfs.schema.from_string(ifile.read())
- # FIXME: Since the store isn't persistent, we migrate to the required one here.
- #if not required_schema <= store.schema:
- # raise Exception('')
- store.migrate(required_schema)
+ if not required_schema.consistent_with(store.schema):
+ raise Exception("The storage's schema is incompatible with tagit's requirements")
+ if not required_schema <= store.schema:
+ store.migrate(required_schema | store.schema)
+
# replace current with new storage
self.storage = store
diff --git a/tagit/widgets/status.kv b/tagit/widgets/status.kv
index 2d49b15..0a680ab 100644
--- a/tagit/widgets/status.kv
+++ b/tagit/widgets/status.kv
@@ -1,5 +1,13 @@
#-- #:import ButtonDock tagit.widgets.dock.ButtonDock # FIXME: mb/port
+<NavigationLabel@Label>:
+ markup: True
+
+<StatusLabel@Label>:
+ markup: True
+ valign: 'middle'
+ halign: 'center'
+
<Status>:
orientation: 'horizontal'
status: ''
@@ -18,11 +26,10 @@
button_height: 30
button_show: 'image',
- Label:
+ NavigationLabel:
id: navigation_label
size_hint: None, 1
width: 180
- markup: True
text: root.navigation
ButtonDock:
@@ -36,13 +43,10 @@
button_height: 30
button_show: 'image',
- Label:
+ StatusLabel:
# gets remaining size
id: status_label
text_size: self.size
- markup: True
- valign: 'middle'
- halign: 'left'
text: root.status
ButtonDock:
diff --git a/tagit/windows/desktop.kv b/tagit/windows/desktop.kv
index cbc5c48..d2ca0e7 100644
--- a/tagit/windows/desktop.kv
+++ b/tagit/windows/desktop.kv
@@ -1,13 +1,9 @@
+#:import TileDecorationRoundedBorder tagit.tiles.decoration.TileDecorationRoundedBorder
#:import TileDecorationBorder tagit.tiles.decoration.TileDecorationBorder
#:import TileDecorationFilledRectangle tagit.tiles.decoration.TileDecorationFilledRectangle
-# 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
+<HGuide@Widget>:
+
<MainWindow>:
# main content
@@ -15,110 +11,85 @@
browser: browser
filter: filter
status: status
- # required by actions.planes
- planes: planes
# required by Menu
context: context
- Carousel:
- id: planes
- loop: True
- scroll_timeout: 0 # disables switching by touch event
- # plane references
- dashboard: dashboard
- browsing: browsing
- codash: codash
+ BoxLayout:
+ orientation: 'vertical'
- # planes
+ Widget:
+ height: 5
+ size_hint: 1, None
- TileDock: # static dashboard plane
- id: dashboard
+ Filter:
+ id: filter
root: root
- # plane config
- size_hint: 1, 1
- visible: planes.current_slide == self
- # dock config
- name: 'dashboard'
- decoration: TileDecorationBorder
- cols: 3
- rows: 2
- # self.size won't be updated correctly
- tile_width: self.width / self.cols
- tile_height: self.height / self.rows
-
- BoxLayout: # browsing plane
- id: browsing
+ size_hint: 1, None
+ height: 40
+
+ HGuide:
+ height: 20
+ size_hint: 1, None
+
+ Widget: # spacer
+ height: 20
+ size_hint: 1, None
+
+ BoxLayout:
orientation: 'horizontal'
- visible: planes.current_slide == self
ButtonDock: # one column of buttons on the left
root: root
- orientation: 'tb-lr'
+ orientation: 'lr-tb'
# one column of buttons
- width: 30 + 2*10
+ width: 1*30 + 2*10
name: 'sidebar_left'
spacing: 10
padding: 10
- size_hint: None, 1
+ 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}
- BoxLayout: # main content
- orientation: 'vertical'
+ 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
- BoxLayout:
- orientation: 'horizontal'
- size_hint: 1, 1
- current: 0
-
- BoxLayout:
- orientation: 'vertical'
- size_hint: 1, 1
-
- Filter:
- id: filter
- root: root
- size_hint: 1, None
- height: 30
-
- Browser:
- id: browser
- root: root
- size_hint: 1, 0.96
-
- Status:
- id: status
- root: root
- size_hint: 1, None
- height: 30
-
+ Widget: # spacer
+ width: 30
+ size_hint: None, 1
+
TileDock: # context info to the right
root: root
- visible: planes.current_slide == self.parent
name: 'sidebar_right'
- decoration: TileDecorationFilledRectangle
+ decoration: TileDecorationRoundedBorder
+ visible: True
cols: 1
- rows: 3
- # self.height won't be updated correctly
- #tile_height: self.size[1] / 4
- width: 180
- size_hint: None, 1
-
- TileDock: # contextual dashboard
- id: codash
+ 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
- # plane config
- size_hint: 1, 1
- visible: planes.current_slide == self
- # dock config
- name: 'codash'
- decoration: TileDecorationBorder
- cols: 4
- rows: 2
- # self.size won't be update correctly
- tile_width: self.width / 4
- tile_height: self.height / 2
+ size_hint: 1, None
+ height: 30
Context: # context menu
id: context
diff --git a/tagit/windows/desktop.py b/tagit/windows/desktop.py
index 42b279e..2c087b2 100644
--- a/tagit/windows/desktop.py
+++ b/tagit/windows/desktop.py
@@ -17,8 +17,9 @@ from kivy.uix.floatlayout import FloatLayout
import kivy.properties as kp
# import Image and Loader to overwrite their caches later on
-from kivy.loader import Loader
from kivy.cache import Cache
+from kivy.loader import Loader
+from kivy.resources import resource_find
# tagit imports
from tagit import actions, config, dialogues
@@ -44,6 +45,8 @@ 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):
@@ -80,6 +83,9 @@ class MainWindow(FloatLayout):
# 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
@@ -96,14 +102,6 @@ class MainWindow(FloatLayout):
## startup and shutdown
def on_startup(self):
- # switch to starting plane - if it's the dashboard no action is needed
- if self.session.cfg('ui', 'standalone', 'plane') == 'browsing':
- self.trigger('ShowBrowsing')
-
- # show welcome message
- if self.session.cfg('session', 'first_start'): # FIXME: mb/port: move to starting app
- self.display_welcome()
-
# run script
for args in self.session.cfg('session', 'script'):
if isinstance(args, str):
@@ -123,35 +121,26 @@ class MainWindow(FloatLayout):
#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')
+ #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
- def display_welcome(self):
- """Display a welcome dialogue on the first start."""
- message = """
-[size=20sp]Welcome to [b]tagit[/b]![/size]
-
-Since you see this message, it's time to configure tagit. It's a good idea to get familiar with the configuration. Hit F1 or the config button to see all relevant settings. There, you can also get rid of this message. If you desire more flexibility, you can edit the config file directly. Check out the project homepage for more details.
-""" # FIXME!
- dialogues.Message(text=message, align='left').open()
-
## config ##
-config.declare(('session', 'first_start'), config.Bool(), True,
- __name__, 'First start', 'Show the welcome message typically shown when tagit is started the first time.')
-
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', 'plane'), config.Enum('browsing', 'dashboard'), 'dashboard',
- __name__, 'Initial plane', 'Start with the dashboard or browsing plane.')
+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.')