diff options
author | Matthias Baumgartner <dev@igsor.net> | 2023-01-06 12:14:51 +0100 |
---|---|---|
committer | Matthias Baumgartner <dev@igsor.net> | 2023-01-06 12:14:51 +0100 |
commit | 0ba7a15c124d3a738a45247e78381dd56f7f1fa9 (patch) | |
tree | a3b4397ce6f7819148fb20a372a062a546e02bc4 /tagit | |
parent | 11e0cc65dfa7158b987c25557775732c5eafba87 (diff) | |
download | tagit-0ba7a15c124d3a738a45247e78381dd56f7f1fa9.tar.gz tagit-0ba7a15c124d3a738a45247e78381dd56f7f1fa9.tar.bz2 tagit-0ba7a15c124d3a738a45247e78381dd56f7f1fa9.zip |
desktop widget clone
Diffstat (limited to 'tagit')
-rw-r--r-- | tagit/__init__.py | 2 | ||||
-rw-r--r-- | tagit/widgets/__init__.py | 10 | ||||
-rw-r--r-- | tagit/widgets/desktop.kv | 130 | ||||
-rw-r--r-- | tagit/widgets/desktop.py | 254 |
4 files changed, 395 insertions, 1 deletions
diff --git a/tagit/__init__.py b/tagit/__init__.py index 7197091..dda8ea7 100644 --- a/tagit/__init__.py +++ b/tagit/__init__.py @@ -4,7 +4,7 @@ Part of the tagit module. A copy of the license is provided with the project. Author: Matthias Baumgartner, 2022 """ -# imports +# standard imports import collections import typing diff --git a/tagit/widgets/__init__.py b/tagit/widgets/__init__.py new file mode 100644 index 0000000..c3ec3c0 --- /dev/null +++ b/tagit/widgets/__init__.py @@ -0,0 +1,10 @@ +""" + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# inner-module imports +from .desktop import MainWindow + +## EOF ## diff --git a/tagit/widgets/desktop.kv b/tagit/widgets/desktop.kv new file mode 100644 index 0000000..5d6c8f2 --- /dev/null +++ b/tagit/widgets/desktop.kv @@ -0,0 +1,130 @@ +#:import TileDecorationBorder tagit.uix.kivy.tiles.decoration.TileDecorationBorder +#:import TileDecorationFilledRectangle tagit.uix.kivy.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 + +<MainWindow>: + # main content + tabs: tabs + # required by most tiles and actions + browser: tabs.children[tabs.current].browser + filter: tabs.children[tabs.current].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 + + # planes + + TileDock: # static dashboard plane + id: dashboard + 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 + orientation: 'horizontal' + visible: planes.current_slide == self + + ButtonDock: # one column of buttons on the left + root: root + orientation: 'tb-lr' + # one column of buttons + width: 30 + 2*10 + name: 'sidebar_left' + spacing: 10 + padding: 10 + size_hint: None, 1 + button_height: 30 + button_show: 'image', + + BoxLayout: # main content + orientation: 'vertical' + size_hint: 1, 1 + + BoxLayout: + id: tabs + orientation: 'horizontal' + size_hint: 1, 1 + current: 0 + + # Here come the browsing tabs + + Tab: + root: root + active: True + # one tab is always present + + Status: + id: status + root: root + size_hint: 1, None + height: 30 + + TileDock: # context info to the right + root: root + visible: planes.current_slide == self.parent + name: 'sidebar_right' + decoration: TileDecorationFilledRectangle + 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 + 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 + + Context: # context menu + id: context + root: root + cancel_handler_widget: root + bounding_box_widget: root + name: 'context' + + KeybindDock: + # key-only actions + root: root + size_hint: None, None + size: 0, 0 + +## EOF ## diff --git a/tagit/widgets/desktop.py b/tagit/widgets/desktop.py new file mode 100644 index 0000000..364c4ec --- /dev/null +++ b/tagit/widgets/desktop.py @@ -0,0 +1,254 @@ +"""Main container of the tagit UI. + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2016 + +""" +# imports +from kivy.clock import Clock +from kivy.lang import Builder +from kivy.uix.floatlayout import FloatLayout +from os.path import join, dirname +import kivy.properties as kp +import logging + +# import Image and Loader to overwrite their caches later on +from kivy.loader import Loader +from kivy.cache import Cache + +# inner-module imports +from tagit import config +import tagit.uix.kivy.dialogues as dialogue +# tagit widget imports +from .actions import ActionBuilder +from .browser import Browser +from .context import Context +from .dock import TileDock, ButtonDock, KeybindDock +from .filter import Filter +from .keyboard import Keyboard +from .session import Session +from .status import Status +from .tabs import Tab + +# exports +__all__ = ('MainWindow', 'KIVY_IMAGE_CACHE_SIZE', 'KIVY_IMAGE_CACHE_TIMEOUT') + + +## code ## + +logger = logging.getLogger(__name__) + +# load kv +Builder.load_file(join(dirname(__file__), 'desktop.kv')) + +# classes +class MainWindow(FloatLayout): + """A self-contained user interface for desktop usage. + See `tagit.apps.gui` for an example of how to invoke it. + """ + + keys = kp.ObjectProperty(None) + + # unnecessary but nicely explicit + browser = kp.ObjectProperty(None) + filter = kp.ObjectProperty(None) + keytriggers = kp.ObjectProperty(None) + + # FIXME: log actions and and replay them + action_log = kp.ListProperty() + + def __init__ (self, cfg, stor, log, **kwargs): + # initialize the session + self._session = Session(cfg, stor, log) + # initialize key-only actions + self.keys = Keyboard() + + # initialize the cache + cache_size = max(0, cfg('ui', 'standalone', 'browser', 'cache_size')) + cache_size = cache_size if cache_size > 0 else None + cache_timeout = max(0, cfg('ui', 'standalone', 'browser', 'cache_timeout')) + cache_timeout = cache_timeout if cache_timeout > 0 else None + Cache.register('kv.loader', limit=cache_size, timeout=cache_timeout) + + # initialize the widget + super(MainWindow, self).__init__(**kwargs) + + # bind pre-close checks + from kivy.core.window import Window + Window.bind(on_request_close=self.on_request_close) + + + ## properties + + @property + def session(self): + return self._session + + def trigger(self, action, *args, **kwargs): + """Trigger an action once.""" + ActionBuilder().get(action).single_shot(self, *args, **kwargs) + + + ## functions + + def autoindex(self, *args): + self.trigger('AutoImport') + + def autoupdate(self, *args): + self.trigger('AutoUpdate') + + def autosync(self, *args): + self.trigger('AutoSync') + + def autosave(self, *args): + if not self.session.storage.file_connected(): + return + + try: + self.trigger('SaveLibrary') + logger.info('Database: Autosaved') + except Exception as e: + logger.error(f'Database: Autosave failed ({e})') + + + ## startup and shutdown + + def on_startup(self): + # start autosave + autosave = self.session.cfg('storage', 'library', 'autosave') + if autosave > 0: + # autosave is in minutes + Clock.schedule_interval(self.autosave, autosave * 60) + + # start index + autoindex = self.session.cfg('storage', 'index', 'autoindex') + autoindex = 0 if autoindex == float('inf') else autoindex + if autoindex > 0: + # autoindex is in minutes + Clock.schedule_interval(self.autoindex, autoindex * 60) + + # start update + autoupdate = self.session.cfg('storage', 'index', 'autoupdate') + autoupdate = 0 if autoupdate == float('inf') else autoupdate + if autoupdate > 0: + # autoupdate is in minutes + Clock.schedule_interval(self.autoupdate, autoupdate * 60) + + # start sync + autosync = self.session.cfg('storage', 'index', 'autosync') + autosync = 0 if autosync == float('inf') else autosync + if autosync > 0: + # autosync is in minutes + Clock.schedule_interval(self.autosync, autosync * 60) + + # trigger operations on startup + if self.session.cfg('storage', 'index', 'index_on_startup'): + self.autoindex() + + if self.session.cfg('storage', 'index', 'update_on_startup'): + self.autoupdate() + + if self.session.cfg('storage', 'index', 'sync_on_startup'): + self.autosync() + + # 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'): + self.display_welcome() + + # script + return + Clock.schedule_once(lambda dt: self.trigger('Search'), 0) + Clock.schedule_once(lambda dt: self.trigger('MoveCursorFirst'), 0) + Clock.schedule_once(lambda dt: self.trigger('SortKey', 'fid:image.colors_spatial:726b4e8ea45546e55dfcd4216b276284'), 0) + from kivy.app import App + App.get_running_app().stop() + Clock.schedule_once(lambda dt: self.trigger('ZoomOut'), 0) + Clock.schedule_once(lambda dt: self.trigger('ZoomOut'), 0) + Clock.schedule_once(lambda dt: self.trigger('NextPage'), 0) + Clock.schedule_once(lambda dt: self.trigger('NextPage'), 0) + Clock.schedule_once(lambda dt: self.trigger('NextPage'), 0) + Clock.schedule_once(lambda dt: self.trigger('NextPage'), 0) + Clock.schedule_once(lambda dt: self.trigger('NextPage'), 0) + Clock.schedule_once(lambda dt: self.trigger('NextPage'), 0) + Clock.schedule_once(lambda dt: self.trigger('NextPage'), 0) + Clock.schedule_once(lambda dt: self.trigger('NextPage'), 0) + Clock.schedule_once(lambda dt: self.trigger('NextPage'), 0) + Clock.schedule_once(lambda dt: self.trigger('NextPage'), 0) + Clock.schedule_once(lambda dt: self.trigger('NextPage'), 0) + Clock.schedule_once(lambda dt: self.trigger('NextPage'), 0) + Clock.schedule_once(lambda dt: self.trigger('NextPage'), 0) + Clock.schedule_once(lambda dt: self.trigger('NextPage'), 0) + Clock.schedule_once(lambda dt: self.trigger('NextPage'), 0) + Clock.schedule_once(lambda dt: self.trigger('NextPage'), 0) + Clock.schedule_once(lambda dt: self.trigger('NextPage'), 0) + Clock.schedule_once(lambda dt: self.trigger('NextPage'), 0) + Clock.schedule_once(lambda dt: self.trigger('NextPage'), 0) + Clock.schedule_once(lambda dt: self.trigger('NextPage'), 0) + Clock.schedule_once(lambda dt: self.trigger('NextPage'), 0) + #from kivy.app import App + #App.get_running_app().stop() + + def on_request_close(self, *args): + + with open('.action_history', 'a') as ofile: + for itm in self.action_log: + ofile.write(f'{itm}\n') + + if self.session.storage.changed() and not self.session.cfg('session', 'debug'): + # save and close + self.trigger('CloseSessionAndExit') + return True + # close w/o saving + 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! + dialogue.Message(text=message, align='left').open() + + +## config ## + +config.declare(('storage', 'library', 'autosave'), config.Float(), 0, + __name__, 'Autosave', 'Time interval in minutes at which the library is saved to disk while running the GUI. A value of 0 means that the feature is disabled.') + +config.declare(('storage', 'index', 'autoindex'), config.Float(), 0, + __name__, 'Autoindex', 'Time interval in minutes at which indexing is triggered while running the GUI. A value of 0 means that the feature is disabled. Also configure the index watchlist.') + +config.declare(('storage', 'index', 'autoupdate'), config.Float(), 0, + __name__, 'Autoupdate', 'Time interval in minutes at which updating is triggered while running the GUI. A value of 0 means that the feature is disabled.') + +config.declare(('storage', 'index', 'autosync'), config.Float(), 0, + __name__, 'Autosync', 'Time interval in minutes at which synchronization is triggered while running the GUI. A value of 0 means that the feature is disabled.') + +config.declare(('storage', 'index', 'index_on_startup'), config.Bool(), False, + __name__, 'Index on startup', 'Trigger indexing when the GUI is started. Also configure the index watchlist') + +config.declare(('storage', 'index', 'update_on_startup'), config.Bool(), False, + __name__, 'Update on startup', 'Trigger updating when the GUI is started.') + +config.declare(('storage', 'index', 'sync_on_startup'), config.Bool(), False, + __name__, 'Sync on startup', 'Trigger synchronization when the GUI is started.') + +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(('ui', 'standalone', 'plane'), config.Enum('browsing', 'dashboard'), 'dashboard', + __name__, 'Initial plane', 'Start with the dashboard or browsing plane.') + +config.declare(('ui', 'standalone', 'browser', 'cache_size'), config.Unsigned(), 1000, + __name__, 'Cache size', 'Number of preview images that are held in the cache. Should be high or zero if memory is not an issue. Set to a small value to preserve memory, but should be at least the most common page size. It is advised to set a value in accordance with `ui.standalone.browser.cache_items`. If zero, no limit applies.') + +config.declare(('ui', 'standalone', 'browser', 'cache_timeout'), config.Unsigned(), 0, + __name__, 'Cache timeout', 'Number of seconds until cached items are discarded. Should be high or zero if memory is not an issue. Set it to a small value to preserve memory when browsing through many images. If zero, no limit applies. Specify in seconds.') + +## EOF ## |