Implement changing of icon theme

This commit is contained in:
Kovid Goyal 2022-01-12 13:34:41 +05:30
parent 6b9bcd001e
commit 121b7eb7f2
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
2 changed files with 100 additions and 82 deletions

View File

@ -167,8 +167,14 @@ class IconResourceManager:
return QIcon(q)
return QIcon.fromTheme(os.path.splitext(name.replace('\\', '__').replace('/', '__'))[0])
def set_theme(self, is_dark_theme):
QIcon.setThemeName(self.dark_theme_name if is_dark_theme else self.light_theme_name)
def set_theme(self):
current = QIcon.themeName()
new = self.dark_theme_name if QApplication.instance().is_dark_theme else self.light_theme_name
if current == new and current not in (self.default_dark_theme_name, self.default_light_theme_name):
# force reload of user icons by first changing theme to default and
# then to user
QIcon.setThemeName(self.default_dark_theme_name if QApplication.instance().is_dark_theme else self.default_light_theme_name)
QIcon.setThemeName(new)
icon_resource_manager = IconResourceManager()
@ -1269,7 +1275,7 @@ class Application(QApplication):
self.palette_changed.emit()
def update_icon_theme(self):
icon_resource_manager.set_theme(self.is_dark_theme)
icon_resource_manager.set_theme()
def stylesheet_for_line_edit(self, is_error=False):
return 'QLineEdit { border: 2px solid %s; border-radius: 3px }' % (

View File

@ -10,8 +10,8 @@ import importlib
import json
import math
import os
import shutil
import sys
import tempfile
from functools import lru_cache
from io import BytesIO
from itertools import count
@ -27,7 +27,7 @@ from qt.core import (
from threading import Event, Thread
from calibre import detect_ncpus as cpu_count, fit_image, human_readable, walk
from calibre.constants import cache_dir, config_dir
from calibre.constants import cache_dir
from calibre.customize.ui import interface_actions
from calibre.gui2 import (
choose_dir, choose_save_file, empty_index, error_dialog, gprefs,
@ -597,6 +597,10 @@ def default_theme():
}
def is_default_theme(t):
return t.get('name') == default_theme()['name']
class ChooseThemeWidget(QWidget):
sync_sorts = pyqtSignal(int)
@ -724,7 +728,6 @@ class ChooseTheme(Dialog):
self.cover_downloaded.connect(self.set_cover, type=Qt.ConnectionType.QueuedConnection)
self.keep_downloading = True
self.commit_changes = None
self.new_theme_title = None
def on_finish(self):
self.dialog_closed = True
@ -833,94 +836,103 @@ class ChooseTheme(Dialog):
tab.set_current_theme(default_theme()['name'])
def accept(self):
if self.theme_list.currentRow() < 0:
return error_dialog(self, _('No theme selected'), _(
'You must first select an icon theme'), show=True)
theme = self.theme_list.currentItem().data(Qt.ItemDataRole.UserRole)
url = BASE_URL + theme['icons-url']
size = theme['compressed-size']
theme = {k:theme.get(k, '') for k in 'name title version'.split()}
themes_to_download = {}
themes_to_remove = set()
for tab in (self.tabs.widget(i) for i in range(self.tabs.count())):
t = tab.current_theme
if is_default_theme(t):
themes_to_remove.add(tab.for_theme)
else:
themes_to_download[t['name']] = t
t.setdefault('for_themes', []).append(tab.for_theme)
self.keep_downloading = True
d = DownloadProgress(self, size)
d.canceled_signal.connect(lambda : setattr(self, 'keep_downloading', False))
self.downloaded_theme = None
self.err_traceback = None
def download():
self.downloaded_theme = buf = BytesIO()
try:
response = get_https_resource_securely(url, get_response=True)
while self.keep_downloading:
raw = response.read(1024)
if not raw:
break
buf.write(raw)
d.downloaded(buf.tell())
d.queue_accept()
except Exception:
import traceback
self.downloaded_theme = traceback.format_exc()
d.queue_reject()
dc = 0
for theme in themes_to_download.values():
buf = BytesIO()
try:
url = BASE_URL + theme['icons-url']
response = get_https_resource_securely(url, get_response=True)
while self.keep_downloading:
raw = response.read(1024)
if not raw:
break
buf.write(raw)
dc += len(raw)
d.downloaded(dc)
except Exception:
import traceback
self.err_traceback = traceback.format_exc()
d.queue_reject()
return
import lzma
data = lzma.decompress(buf.getvalue())
theme['buf'] = BytesIO(data)
d.queue_accept()
t = Thread(name='DownloadIconTheme', target=download)
t.daemon = True
t.start()
ret = d.exec()
if themes_to_download:
size = sum(t['compressed-size'] for t in themes_to_download.values())
d = DownloadProgress(self, size)
d.canceled_signal.connect(lambda : setattr(self, 'keep_downloading', False))
t = Thread(name='DownloadIconTheme', target=download)
t.daemon = True
t.start()
ret = d.exec()
if self.err_traceback:
return error_dialog(self, _('Download failed'), _(
'Failed to download icon themes, click "Show details" for more information.'), show=True, det_msg=self.err_traceback)
if ret == QDialog.DialogCode.Rejected or not self.keep_downloading or d.canceled:
return
if self.downloaded_theme and not isinstance(self.downloaded_theme, BytesIO):
return error_dialog(self, _('Download failed'), _(
'Failed to download icon theme, click "Show details" for more information.'), show=True, det_msg=self.downloaded_theme)
if ret == QDialog.DialogCode.Rejected or not self.keep_downloading or d.canceled or self.downloaded_theme is None:
return
dt = self.downloaded_theme
self.commit_changes = CommitChanges(tuple(themes_to_download.values()), themes_to_remove)
return super().accept()
def commit_changes():
import lzma
dt.seek(0)
f = BytesIO(lzma.decompress(dt.getvalue()))
f.seek(0)
# remove_icon_theme()
install_icon_theme(theme, f)
self.commit_changes = commit_changes
self.new_theme_title = theme['title']
return Dialog.accept(self)
@property
def new_theme_title(self):
if QApplication.instance().is_dark_theme:
order = 'dark', 'any', 'light'
else:
order = 'light', 'any', 'dark'
tm = {tab.for_theme: tab for tab in (self.tabs.widget(i) for i in range(self.tabs.count()))}
for x in order:
tab = tm[x]
t = tab.current_theme
if not is_default_theme(t):
return t['title']
# }}}
def safe_copy(src, destpath):
tpath = destpath + '-temp'
with open(tpath, 'wb') as dest:
shutil.copyfileobj(src, dest)
atomic_rename(tpath, destpath)
class CommitChanges:
def __init__(self, downloaded_themes, themes_to_remove):
self.downloaded_themes = downloaded_themes
self.themes_to_remove = themes_to_remove
def __call__(self):
for x in self.themes_to_remove:
icon_resource_manager.remove_user_theme(x)
for theme in self.downloaded_themes:
for x in theme['for_themes']:
icon_resource_manager.remove_user_theme(x)
path = icon_resource_manager.user_theme_resource_file(x)
t = {k: theme[k] for k in 'name title version'.split()}
install_icon_theme(t, theme['buf'], path, x)
icon_resource_manager.register_user_resource_files()
icon_resource_manager.set_theme()
def install_icon_theme(theme, f):
icdir = os.path.abspath(os.path.join(config_dir, 'resources', 'images'))
if not os.path.exists(icdir):
os.makedirs(icdir)
theme['files'] = set()
metadata_file = os.path.join(icdir, 'icon-theme.json')
with ZipFile(f) as zf:
for name in zf.namelist():
if '..' in name or name == 'blank.png':
continue
base = icdir
if '/' in name:
base = os.path.join(icdir, os.path.dirname(name))
if not os.path.exists(base):
os.makedirs(base)
destpath = os.path.abspath(os.path.join(base, os.path.basename(name)))
if not destpath.startswith(icdir):
continue
with zf.open(name) as src:
safe_copy(src, destpath)
theme['files'].add(name)
theme['files'] = tuple(theme['files'])
buf = BytesIO(as_bytes(json.dumps(theme, indent=2)))
buf.seek(0)
safe_copy(buf, metadata_file)
def install_icon_theme(theme, f, rcc_path, for_theme):
from calibre.utils.rcc import compile_icon_dir_as_themes
with ZipFile(f) as zf, tempfile.TemporaryDirectory() as tdir:
zf.extractall(tdir)
with open(os.path.join(tdir, 'metadata.json'), 'w') as f:
json.dump(theme, f)
inherits = 'calibre-default' if for_theme == 'any' else f'calibre-default-{for_theme}'
compile_icon_dir_as_themes(
tdir, rcc_path, theme_name=f'calibre-user-{for_theme}', inherits=inherits, for_theme=for_theme)
if __name__ == '__main__':