rework theme system
This commit is contained in:
parent
5f5bdc43b1
commit
e451ce8b48
|
@ -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}')
|
||||
|
|
Reference in a new issue