Source code for editor.theme_manager

"""This module provide theme manage functions.

Typical usage example:

.. code-block:: python
   :linenos:

   from editor import theme_manager
   theme_manager.loadTheme('dark')
   theme_manager.setupThemeWatcher()
"""

import os, re, time, platform
from PySide6.QtCore import QSettings, QTimer

from pathlib import Path
from editor.common.logger import log
from editor.common.file_watcher import RegexWatcher
from editor.common.util import getIde


_themeFolder = os.environ[ 'DEAR_THEME_PATH' ]
_themeWatcher = None
_activeTheme = None
_lastModifyTime = 0


#####################  UTILS  #####################
def _gatherStyleSheets(name):
	qssDir = f'{_themeFolder}/{name}/qss/'
	qssFiles = {}
	for currDir, dirs, files in os.walk(qssDir):
		if currDir.endswith('__qsscache__'): continue
		for f in files:
			if not f.endswith('.qss'): continue
			qssFiles[f] = os.path.join(currDir, f)

	qssFiles = dict(sorted(qssFiles.items(), key = lambda kv: kv[1]))
	return qssFiles

def _parseSingleQss(srcPath, cachePath, sysParser, macroParser, urlParser):
	srcText = None
	with open(srcPath, 'r') as file:
		srcText = file.read()
		file.close()

	parsed = re.sub(r'/\*.*?\*/', '', srcText, flags = re.S)
	parsed = re.sub(r'\n+', '\n', parsed, flags = re.S)
	parsed = re.sub(r'#if\s+(\S+)\s*\n(.*?)\n\s*#end', sysParser, parsed, flags = re.S)
	parsed = re.sub(r'&\[(.*)\]', macroParser, parsed)
	parsed = re.sub(r'url\((.*)\)', urlParser, parsed)

	with open(cachePath, 'w') as file:
		file.write(parsed)
		file.close()
		# log(f'{qss} parsed successfully!'

	return parsed

def _mergeGeneratedQss(name):
	cacheDir = f'{_themeFolder}/{name}/qss/__qsscache__'

	mergedText = ''
	for f in _gatherStyleSheets(name):
		qssPath = f'{cacheDir}/{f}'
		with open(qssPath, 'r') as file:
			mergedText += file.read()
			file.close()

	return mergedText

def _gatherDirtyItems(name):
	cacheDir = f'{_themeFolder}/{name}/qss/__qsscache__'
	qssTable = _gatherStyleSheets(name)
	if not os.path.isdir(cacheDir):
		os.makedirs(cacheDir)
		return qssTable

	needReparse = {}
	settingPath = f'{_themeFolder}/{name}/macro'
	getmtime = os.path.getmtime
	mtSetting = getmtime(settingPath)

	for qssName, qssPath in qssTable.items():
		cachePath = f'{cacheDir}/{qssName}'
		if not os.path.isfile(cachePath):
			needReparse[qssName] = qssPath
		else:
			mtSrc = getmtime(qssPath)
			mtCache = getmtime(cachePath)
			if mtCache < mtSrc or mtCache < mtSetting:
				needReparse[qssName] = qssPath
	return needReparse

def _parseTheme(name):
	items = _gatherDirtyItems(name)
	if items:
		settingPath = f'{_themeFolder}/{name}/macro'
		themeSetting = QSettings(settingPath, QSettings.IniFormat)

		macroParser = lambda m: themeSetting.value(m.group(1))
		urlParser = lambda m: f'url({_themeFolder}/{name}/{m.group(1)})'
		sysParser = lambda m: m.group(2) if m.group(1) == platform.system() else ''

		for qssName, qssPath in items.items():
			cachePath = f'{_themeFolder}/{name}/qss/__qsscache__/{qssName}'
			_parseSingleQss(qssPath, cachePath, sysParser, macroParser, urlParser)

	return _mergeGeneratedQss(name)

def _onThemeModified(evt):
	global _lastModifyTime
	currTime = time.time()
	if currTime - _lastModifyTime <= 0.1: return

	log('theme modify checked, reloading theme...')
	rootPath = Path(os.path.abspath(activeThemeFolder()))
	mfilePath = Path(os.path.abspath(evt.src_path))
	if rootPath in mfilePath.parents:
		reloadTheme = lambda: loadTheme(_activeTheme, True)
		QTimer.singleShot(10, reloadTheme)

	_lastModifyTime = currTime


######################  APIS  #####################
[docs]def listThemes(): """List all available themes. Returns: list[str]: All available themes. """ return [ f.name for f in os.scandir(_themeFolder) if f.is_dir() ]
[docs]def activeTheme(): """Get current active theme. Returns: str: Active theme name. """ return _activeTheme
[docs]def activeThemeFolder(): """Get current active theme source folder. Returns: str: Active theme folder path. """ return f'{_themeFolder}/{_activeTheme}'
[docs]def loadTheme(name, reset = False): """Load theme by certain name. Args: str name: Theme name to load. bool reset: Reset exsiting style state. (Mainly used by theme watcher for hot-reloading usage.) """ assert name in listThemes() global _activeTheme _activeTheme = name initScript = f'{_themeFolder}/{name}/init.py' if os.path.exists(initScript): exec(open(initScript).read()) ide = getIde() if reset: ide.setStyleSheet(None) ide.setStyleSheet(_parseTheme(name))
[docs]def setupThemeWatcher(): """Setup theme watcher on all themes. """ global _themeWatcher # to keep alive ignores = ['.*__qsscache__.*', '.*img.*'] _themeWatcher = RegexWatcher(ignoreRegexes = ignores, onModified = _onThemeModified) _themeWatcher.start(_themeFolder)
[docs]def disposeThemeWatcher(): """Shutdown current theme watcher """ global _themeWatcher if not _themeWatcher: return _themeWatcher.stop() _themeWatcher = None
###################### TEST ##################### if __name__ == '__main__': print(_gatherStyleSheets('dark')) # print(_parseTheme('dark'))