rework theme system

This commit is contained in:
Izalia Mae 2022-08-27 14:04:44 -04:00
parent 5f5bdc43b1
commit e451ce8b48

View file

@ -1,20 +1,34 @@
import traceback
from configparser import ConfigParser
from izzylib.logging import LogLevel
from izzylib.misc import class_name
from izzylib.object_base import ObjectBase
from izzylib.path import Path
from izzylib.url import Url
from izzylib_http_async import Template
from watchdog.events import FileSystemEventHandler
from watchdog.observers import Observer
from . import var, __version__ as version, __software__ as swname
from .enums import StylePriority
from .functions import ComponentBase
class Themes(DotDict):
class Themes(ComponentBase, ObjectBase):
def __init__(self, window):
super().__init__()
self._window = window
self._context = Gtk.StyleContext()
self._main = None
self._current_theme = None
self._watcher = None
ComponentBase.__init__(self)
ObjectBase.__init__(self,
window = window,
context = Gtk.StyleContext(),
main = None,
current = None,
current_system = None,
watcher = None,
user = DotDict(),
system = DotDict(),
readonly_props = ['window', 'context', 'user', 'system']
)
self.setup()
@ -27,128 +41,314 @@ class Themes(DotDict):
@property
def app(self):
return self._window.app
def systempath(self):
return self.app.path.script.join('systhemes')
@property
def current(self):
return self._current_theme
def userpath(self):
return self.app.path.themes
@property
def window(self):
return self._window
def get_theme_by_file(self, path, system=False):
themes = self.system.values() if system else self.user.values()
for theme in themes:
if theme.path in path:
return theme
def watcher_start(self):
if self._watcher:
self.stop()
self._watcher = Observer()
self._watcher.schedule(WatchHandler(self), self.app.path.resources, recursive=True)
self._watcher.start()
def watcher_stop(self):
if not self._watcher:
return
self._watcher.stop()
self._watcher.join()
self._watcher = None
def load(self, name):
logging.verbose('Loading theme:', name)
if name in self:
self.unload(name)
cssfile = Gio.File.new_for_path(self.app.path.themes.join(f'{name}.css'))
self[name] = Gtk.CssProvider()
self[name].load_from_file(cssfile)
def unload(self, name):
self[name]
del self[name]
logging.verbose('Unloading theme:', name)
def set(self, name):
self.unset()
if name.lower() == 'default':
return
self._context.add_provider(self[name], StylePriority.USER)
self._current_theme = name
def unset(self):
if not self._current_theme:
return
self._context.remove_provider(self[self._current_theme])
self._current_theme = None
raise KeyError(f'Path not in any theme: {path}')
def list_unloaded(self):
themes = []
for path in self.app.path.themes.listdir():
name = path.stem
if path.suffix == 'css' and name not in self:
themes.append(name)
return themes
for path in self.userpath.listdir(recursive=False):
if path.isdir() and path.name not in self.user:
yield path
def load_main(self):
if self._main:
self._context.remove_provider(self._main)
if self.main:
self.context.remove_provider(self.main)
self._main = Gtk.CssProvider()
self._main.load_from_file(Gio.File.new_for_path(self.app.path.resources.join('main.css')))
self.main = Gtk.CssProvider()
self.main.load_from_file(Gio.File.new_for_path(self.app.path.resources.join('main.css')))
self._context.add_provider(self._main, StylePriority.FALLBACK)
self.context.add_provider(self.main, StylePriority.FALLBACK)
def set(self, path):
if self.current == path:
return
theme = self.user[path]
self.unset()
if theme.base and theme.base != self.current_system:
self.set_system(theme.base.lower())
self.context.add_provider(theme.get_provider(), StylePriority.USER)
self.current = theme.path
def set_system(self, name):
theme = self.system[name]
if self.current_system == name:
return
elif self.current_system:
self.unset_system()
self.context.add_provider(theme.get_provider(), StylePriority.APPLICATION)
self.current_system = name
def setup(self):
self.load_main()
for name in self.list_unloaded():
self.load(name)
for path in self.systempath.listdir(recursive=False):
if path.isdir():
theme = Theme(path)
self.system[theme.name.lower()] = theme
for path in self.list_unloaded():
try:
self.user[path] = Theme(path)
except Exception as e:
traceback.print_exc()
with self.db.session as s:
type, _, theme = s.get_config('theme').partition(':')
if type == 'system':
self.set_system(theme)
elif type == 'user':
self.set_system(theme)
class WatchHandler(FileSystemEventHandler):
def unset(self):
if not self.current:
return
self.context.remove_provider(self.user[self.current].provider)
def unset_system(self):
if not self.current_system:
return
self.context.remove_provider(self.system[self.current_system].provider)
def watcher_start(self):
if self.watcher:
return
self.watcher = Observer()
self.watcher.schedule(MainWatchHandler(self), self.app.path.resources, recursive=False)
self.watcher.schedule(SystemWatchHandler(self), self.systempath, recursive=True)
self.watcher.schedule(UserWatchHandler(self), self.userpath, recursive=True)
self.watcher.start()
def watcher_stop(self):
if not self.watcher:
return
self.watcher.stop()
self.watcher.join()
self.watcher = None
self.current = None
class Theme(ObjectBase):
def __init__(self, path):
ObjectBase.__init__(self,
path = path,
renderer = None,
provider = Gtk.CssProvider(),
name = None,
author = None,
url = None,
license = None,
entry = None,
base = None
)
self.renderer = Template(
autoescape = False,
context = self.handle_template_context,
search = [path],
global_vars = {
'len': len,
'str': str,
'int': int,
'float': float,
'bool': bool,
'app': self,
'var': var,
'version': version,
'swname': swname,
}
)
self.load_manifest()
self.provider.connect('parsing-error', self.handle_parsing_error)
def _parse_property(self, key, value):
if value == None:
return
if key == 'path':
if not isinstance(value, Path):
return path(value)
elif key == 'base':
if not value or value.lower().strip() in ['', 'null', 'none']:
return None
elif key == 'url':
if not isinstance(value, Url):
return Url(value)
return value
def get_provider(self):
if not self.provider.to_string():
self.load()
return self.provider()
def load(self):
data = self.renderer.render(self.entry)
self.provider.load_from_data(data.encode('utf-8'))
def load_manifest(self):
path = self.path.join('manifest.ini')
if not path.exists():
raise FileNotFoundError(f'Cannot find manifest for theme: {self.path.name}')
config = ConfigParser()
config.read(self.path.join('manifest.ini'))
try:
info = config['info']
settings = config['settings']
self.name = info['name']
self.author = info['author']
self.url = info.get('url')
self.license = info.get('license')
self.entry = settings['entry']
self.base = settings.get('base')
except KeyError as e:
raise KeyError(f'Failed to parse manifest for "{self.path.name}": {e}') from None
if not self.entry:
raise ValueError(f'No entry css specified for theme: {self.name}')
def handle_parsing_error(self, provider, section, error):
logging.error(f'[{self.name}] {error.message} {section.get_start_line()}')
def handle_template_context(self, context):
context['theme'] = self
return context
class WatcherBase(ComponentBase, FileSystemEventHandler):
def __init__(self, themes):
super().__init__()
FileSystemEventHandler.__init__(self)
self.themes = themes
@property
def path(self):
return self.themes.app.path
return self.app.path
def on_any_event(self, event):
if event.event_type != 'modified':
return
@property
def systempath(self):
return self.themes.systempath
@property
def userpath(self):
return self.themes.userpath
def handle_theme_modified(self, theme, path):
if path.name == 'manifest.ini':
theme.load_manifest()
logging.verbose(f'Reloaded manifest for theme: {theme.name}')
elif path.suffix in ['css', 'svg', 'png', 'gif', 'jpg']:
theme.load()
logging.verbose(f'Reloaded theme: {theme.name}')
class MainWatchHandler(WatcherBase):
def on_modified(self, event):
path = Path(event.src_path)
if path.suffix != 'css':
return
if path.startswith(self.path.resources) and path.name == 'main.css':
if path.name == 'main.css':
logging.verbose('Reloading main css')
self.themes.load_main()
elif path.startswith(self.path.themes):
pass
class SystemWatchHandler(WatcherBase):
def on_modified(self, event):
path = Path(event.src_path)
theme = self.themes.get_theme_by_file(path, system=True)
self.handle_theme_modified(theme, path)
class UserWatchHandler(WatcherBase):
def on_modified(self, event):
path = Path(event.src_path)
try:
theme = self.themes.get_theme_by_file(path)
except KeyError:
return
self.handle_theme_modified(theme, path)
def on_deleted(self, event):
path = Path(event.src_path)
theme = self.themes.get_theme_by_file(path)
if path.name == 'manifest.ini':
if self.themes.current == theme.path:
self.themes.unset()
del self.themes.user[theme.path]
logging.verbose(f'Deleted theme: {theme.name}')
def on_created(self, event):
path = Path(event.src_path)
if path.name == 'manifest.ini' and self.userpath.join(path.parent).isdir():
theme = Theme(path.parent)
self.themes.user[theme.path] = theme
logging.verbose(f'Created new theme: {theme.name}')