mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Work on UI for color specific icon themes
This commit is contained in:
parent
83d1f1a6c6
commit
a56e168f70
@ -14,13 +14,14 @@ import shutil
|
|||||||
import sys
|
import sys
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from itertools import count
|
from itertools import count
|
||||||
|
from functools import lru_cache
|
||||||
from multiprocessing.pool import ThreadPool
|
from multiprocessing.pool import ThreadPool
|
||||||
from qt.core import (
|
from qt.core import (
|
||||||
QAbstractItemView, QApplication, QComboBox, QDialog, QDialogButtonBox,
|
QAbstractItemView, QApplication, QComboBox, QDialog, QDialogButtonBox,
|
||||||
QFormLayout, QGridLayout, QGroupBox, QIcon, QImage, QImageReader, QLabel,
|
QFormLayout, QGroupBox, QHBoxLayout, QIcon, QImage, QImageReader, QLabel,
|
||||||
QLineEdit, QListWidget, QListWidgetItem, QPen, QPixmap, QProgressDialog, QSize,
|
QLineEdit, QListWidget, QListWidgetItem, QPen, QPixmap, QProgressDialog, QSize,
|
||||||
QSpinBox, QSplitter, QStackedLayout, QStaticText, QStyle, QStyledItemDelegate,
|
QSpinBox, QSplitter, QStackedLayout, QStaticText, QStyle, QStyledItemDelegate,
|
||||||
Qt, QTextEdit, QVBoxLayout, QWidget, pyqtSignal, sip
|
Qt, QTabWidget, QTextEdit, QVBoxLayout, QWidget, pyqtSignal, sip
|
||||||
)
|
)
|
||||||
from threading import Event, Thread
|
from threading import Event, Thread
|
||||||
|
|
||||||
@ -28,8 +29,8 @@ 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, config_dir
|
||||||
from calibre.customize.ui import interface_actions
|
from calibre.customize.ui import interface_actions
|
||||||
from calibre.gui2 import (
|
from calibre.gui2 import (
|
||||||
choose_dir, choose_save_file, empty_index, error_dialog, gprefs, must_use_qt,
|
choose_dir, choose_save_file, empty_index, error_dialog, gprefs,
|
||||||
question_dialog, safe_open_url
|
icon_resource_manager, must_use_qt, question_dialog, safe_open_url
|
||||||
)
|
)
|
||||||
from calibre.gui2.dialogs.progress import ProgressDialog
|
from calibre.gui2.dialogs.progress import ProgressDialog
|
||||||
from calibre.gui2.progress_indicator import ProgressIndicator
|
from calibre.gui2.progress_indicator import ProgressIndicator
|
||||||
@ -167,7 +168,7 @@ def default_cover_icons(cols=5):
|
|||||||
count += 1
|
count += 1
|
||||||
|
|
||||||
|
|
||||||
def create_cover(report, icons=(), cols=5, size=120, padding=16):
|
def create_cover(report=None, icons=(), cols=5, size=120, padding=16):
|
||||||
icons = icons or tuple(default_cover_icons(cols))
|
icons = icons or tuple(default_cover_icons(cols))
|
||||||
rows = int(math.ceil(len(icons) / cols))
|
rows = int(math.ceil(len(icons) / cols))
|
||||||
with Canvas(cols * (size + padding), rows * (size + padding), bgcolor='#eee') as canvas:
|
with Canvas(cols * (size + padding), rows * (size + padding), bgcolor='#eee') as canvas:
|
||||||
@ -545,7 +546,7 @@ class Delegate(QStyledItemDelegate):
|
|||||||
<h1>{title}</h1>
|
<h1>{title}</h1>
|
||||||
<p>by <i>{author}</i> with <b>{number}</b> icons [{size}]</p>
|
<p>by <i>{author}</i> with <b>{number}</b> icons [{size}]</p>
|
||||||
<p>{description}</p>
|
<p>{description}</p>
|
||||||
<p>Version: {version} Number of users: {usage}</p>
|
<p>Version: {version} Number of users: {usage:n}</p>
|
||||||
<p><i>{visit}</i></p>
|
<p><i>{visit}</i></p>
|
||||||
''').format(title=theme.get('title', _('Unknown')), author=theme.get('author', _('Unknown')),
|
''').format(title=theme.get('title', _('Unknown')), author=theme.get('author', _('Unknown')),
|
||||||
number=theme.get('number', 0), description=theme.get('description', ''),
|
number=theme.get('number', 0), description=theme.get('description', ''),
|
||||||
@ -579,16 +580,111 @@ class DownloadProgress(ProgressDialog):
|
|||||||
self.rej.emit()
|
self.rej.emit()
|
||||||
|
|
||||||
|
|
||||||
|
def specialised_theme_name(for_theme):
|
||||||
|
return icon_resource_manager.user_icon_theme_metadata(for_theme).get('name')
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=2)
|
||||||
|
def default_theme():
|
||||||
|
dc = 0
|
||||||
|
for name in walk(P('images')):
|
||||||
|
if name.endswith('.png') and '/textures/' not in name.replace(os.sep, '/'):
|
||||||
|
dc += 1
|
||||||
|
p = QPixmap()
|
||||||
|
p.loadFromData(create_cover())
|
||||||
|
return {
|
||||||
|
'name': 'default', 'title': _('Default icons'),
|
||||||
|
'user_msg': _('Use the calibre default icons'),
|
||||||
|
'usage': 3_000_000, 'author': 'Kovid Goyal', 'number': dc,
|
||||||
|
'cover-pixmap': p, 'compressed-size': os.path.getsize(P('icons.rcc', allow_user_override=False))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ChooseThemeWidget(QWidget):
|
||||||
|
|
||||||
|
def __init__(self, for_theme='any', parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.vl = vl = QVBoxLayout(self)
|
||||||
|
self.for_theme = for_theme
|
||||||
|
self.default_theme = default_theme()
|
||||||
|
if self.for_theme == 'any':
|
||||||
|
msg = _('Choose an icon theme below. It will be used for both light and dark color'
|
||||||
|
' themes unless a color specific theme is chosen in one of the other tabs.')
|
||||||
|
elif self.for_theme == 'light':
|
||||||
|
msg = _('Choose an icon theme below. It will be used preferentially for light color themes.')
|
||||||
|
elif self.for_theme == 'dark':
|
||||||
|
msg = _('Choose an icon theme below. It will be used preferentially for dark color themes.')
|
||||||
|
self.current_theme = specialised_theme_name(self.for_theme) or 'default'
|
||||||
|
self.msg = la = QLabel(msg)
|
||||||
|
la.setWordWrap(True)
|
||||||
|
vl.addWidget(la)
|
||||||
|
self.sort_by = sb = QComboBox(self)
|
||||||
|
self.hl = hl = QHBoxLayout()
|
||||||
|
vl.addLayout(hl)
|
||||||
|
self.sl = sl = QLabel(_('&Sort by:'))
|
||||||
|
sl.setBuddy(sb)
|
||||||
|
hl.addWidget(sl), hl.addWidget(sb), hl.addStretch(10)
|
||||||
|
sb.addItems([_('Number of icons'), _('Popularity'), _('Name'),])
|
||||||
|
sb.setEditable(False), sb.setCurrentIndex(gprefs.get('choose_icon_theme_sort_by', 1))
|
||||||
|
sb.currentIndexChanged.connect(self.re_sort)
|
||||||
|
sb.currentIndexChanged.connect(lambda : gprefs.set('choose_icon_theme_sort_by', sb.currentIndex()))
|
||||||
|
self.theme_list = tl = QListWidget(self)
|
||||||
|
vl.addWidget(tl)
|
||||||
|
tl.setVerticalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel)
|
||||||
|
self.delegate = Delegate(tl)
|
||||||
|
tl.setItemDelegate(self.delegate)
|
||||||
|
tl.itemPressed.connect(self.item_clicked)
|
||||||
|
|
||||||
|
def item_clicked(self, item):
|
||||||
|
if QApplication.mouseButtons() & Qt.MouseButton.RightButton:
|
||||||
|
theme = item.data(Qt.ItemDataRole.UserRole) or {}
|
||||||
|
url = theme.get('url')
|
||||||
|
if url:
|
||||||
|
safe_open_url(url)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sort_on(self):
|
||||||
|
return {0:'number', 1:'usage', 2:'title'}[self.sort_by.currentIndex()]
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
for i in range(self.theme_list.count()):
|
||||||
|
yield self.theme_list.item(i)
|
||||||
|
|
||||||
|
def item_from_name(self, name):
|
||||||
|
for item in self:
|
||||||
|
if item.data(Qt.ItemDataRole.UserRole)['name'] == name:
|
||||||
|
return item
|
||||||
|
|
||||||
|
def set_cover(self, name, pixmap):
|
||||||
|
item = self.item_from_name(name)
|
||||||
|
if item is not None:
|
||||||
|
item.setData(Qt.ItemDataRole.DecorationRole, pixmap)
|
||||||
|
|
||||||
|
def show_themes(self, themes):
|
||||||
|
self.themes = [self.default_theme] + list(themes)
|
||||||
|
self.re_sort()
|
||||||
|
|
||||||
|
def re_sort(self):
|
||||||
|
self.themes.sort(key=lambda x:sort_key(x.get('title', '')))
|
||||||
|
field = self.sort_on
|
||||||
|
if field == 'number':
|
||||||
|
self.themes.sort(key=lambda x:x.get('number', 0), reverse=True)
|
||||||
|
elif field == 'usage':
|
||||||
|
self.themes.sort(key=lambda x:x.get('usage', 0), reverse=True)
|
||||||
|
self.theme_list.clear()
|
||||||
|
for theme in self.themes:
|
||||||
|
i = QListWidgetItem(theme.get('title', '') + ' {} {}'.format(theme.get('number'), theme.get('usage', 0)), self.theme_list)
|
||||||
|
i.setData(Qt.ItemDataRole.UserRole, theme)
|
||||||
|
if 'cover-pixmap' in theme:
|
||||||
|
i.setData(Qt.ItemDataRole.DecorationRole, theme['cover-pixmap'])
|
||||||
|
|
||||||
|
|
||||||
class ChooseTheme(Dialog):
|
class ChooseTheme(Dialog):
|
||||||
|
|
||||||
cover_downloaded = pyqtSignal(object, object)
|
cover_downloaded = pyqtSignal(object, object)
|
||||||
themes_downloaded = pyqtSignal()
|
themes_downloaded = pyqtSignal()
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
try:
|
|
||||||
self.current_theme = json.loads(I('icon-theme.json', data=True))['title']
|
|
||||||
except Exception:
|
|
||||||
self.current_theme = None
|
|
||||||
Dialog.__init__(self, _('Choose an icon theme'), 'choose-icon-theme-dialog', parent)
|
Dialog.__init__(self, _('Choose an icon theme'), 'choose-icon-theme-dialog', parent)
|
||||||
self.finished.connect(self.on_finish)
|
self.finished.connect(self.on_finish)
|
||||||
self.dialog_closed = False
|
self.dialog_closed = False
|
||||||
@ -623,51 +719,20 @@ class ChooseTheme(Dialog):
|
|||||||
self.start_spinner()
|
self.start_spinner()
|
||||||
|
|
||||||
l.addWidget(c)
|
l.addWidget(c)
|
||||||
self.w = w = QWidget(self)
|
self.tabs = QTabWidget(self)
|
||||||
l.addWidget(w)
|
l.addWidget(self.tabs)
|
||||||
w.l = l = QGridLayout(w)
|
self.all_colors = ChooseThemeWidget(parent=self)
|
||||||
|
self.tabs.addTab(self.all_colors, _('For light and dark'))
|
||||||
def add_row(x, y=None):
|
self.light_colors = ChooseThemeWidget(for_theme='light', parent=self)
|
||||||
if isinstance(x, str):
|
self.tabs.addTab(self.light_colors, _('For light only'))
|
||||||
x = QLabel(x)
|
self.dark_colors = ChooseThemeWidget(for_theme='dark', parent=self)
|
||||||
row = l.rowCount()
|
self.tabs.addTab(self.dark_colors, _('For dark only'))
|
||||||
if y is None:
|
self.tabs.setCurrentIndex(0)
|
||||||
if isinstance(x, QLabel):
|
|
||||||
x.setWordWrap(True)
|
|
||||||
l.addWidget(x, row, 0, 1, 2)
|
|
||||||
else:
|
|
||||||
if isinstance(x, QLabel):
|
|
||||||
x.setBuddy(y)
|
|
||||||
l.addWidget(x, row, 0), l.addWidget(y, row, 1)
|
|
||||||
add_row(_(
|
|
||||||
'Choose an icon theme below. You will need to restart'
|
|
||||||
' calibre to see the new icons.'))
|
|
||||||
add_row(_('Current icon theme:') + '\xa0<b>' + (self.current_theme or 'None'))
|
|
||||||
self.sort_by = sb = QComboBox(self)
|
|
||||||
add_row(_('&Sort by:'), sb)
|
|
||||||
sb.addItems([_('Number of icons'), _('Popularity'), _('Name'),])
|
|
||||||
sb.setEditable(False), sb.setCurrentIndex(gprefs.get('choose_icon_theme_sort_by', 1))
|
|
||||||
sb.currentIndexChanged.connect(self.re_sort)
|
|
||||||
sb.currentIndexChanged.connect(lambda : gprefs.set('choose_icon_theme_sort_by', sb.currentIndex()))
|
|
||||||
self.theme_list = tl = QListWidget(self)
|
|
||||||
tl.setVerticalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel)
|
|
||||||
self.delegate = Delegate(tl)
|
|
||||||
tl.setItemDelegate(self.delegate)
|
|
||||||
tl.itemDoubleClicked.connect(self.accept)
|
|
||||||
tl.itemPressed.connect(self.item_clicked)
|
|
||||||
add_row(tl)
|
|
||||||
|
|
||||||
t = Thread(name='GetIconThemes', target=self.get_themes)
|
t = Thread(name='GetIconThemes', target=self.get_themes)
|
||||||
t.daemon = True
|
t.daemon = True
|
||||||
t.start()
|
t.start()
|
||||||
|
|
||||||
def item_clicked(self, item):
|
|
||||||
if QApplication.mouseButtons() & Qt.MouseButton.RightButton:
|
|
||||||
theme = item.data(Qt.ItemDataRole.UserRole) or {}
|
|
||||||
url = theme.get('url')
|
|
||||||
if url:
|
|
||||||
safe_open_url(url)
|
|
||||||
|
|
||||||
def start_spinner(self, msg=None):
|
def start_spinner(self, msg=None):
|
||||||
self.pi.startAnimation()
|
self.pi.startAnimation()
|
||||||
self.stack.setCurrentIndex(0)
|
self.stack.setCurrentIndex(0)
|
||||||
@ -677,24 +742,6 @@ class ChooseTheme(Dialog):
|
|||||||
self.pi.stopAnimation()
|
self.pi.stopAnimation()
|
||||||
self.stack.setCurrentIndex(1)
|
self.stack.setCurrentIndex(1)
|
||||||
|
|
||||||
@property
|
|
||||||
def sort_on(self):
|
|
||||||
return {0:'number', 1:'usage', 2:'title'}[self.sort_by.currentIndex()]
|
|
||||||
|
|
||||||
def re_sort(self):
|
|
||||||
self.themes.sort(key=lambda x:sort_key(x.get('title', '')))
|
|
||||||
field = self.sort_on
|
|
||||||
if field == 'number':
|
|
||||||
self.themes.sort(key=lambda x:x.get('number', 0), reverse=True)
|
|
||||||
elif field == 'usage':
|
|
||||||
self.themes.sort(key=lambda x:self.usage.get(x.get('name'), 0), reverse=True)
|
|
||||||
self.theme_list.clear()
|
|
||||||
for theme in self.themes:
|
|
||||||
i = QListWidgetItem(theme.get('title', '') + ' {} {}'.format(theme.get('number'), self.usage.get(theme.get('name'))), self.theme_list)
|
|
||||||
i.setData(Qt.ItemDataRole.UserRole, theme)
|
|
||||||
if 'cover-pixmap' in theme:
|
|
||||||
i.setData(Qt.ItemDataRole.DecorationRole, theme['cover-pixmap'])
|
|
||||||
|
|
||||||
def get_themes(self):
|
def get_themes(self):
|
||||||
|
|
||||||
self.usage = {}
|
self.usage = {}
|
||||||
@ -729,30 +776,18 @@ class ChooseTheme(Dialog):
|
|||||||
return
|
return
|
||||||
for theme in self.themes:
|
for theme in self.themes:
|
||||||
theme['usage'] = self.usage.get(theme['name'], 0)
|
theme['usage'] = self.usage.get(theme['name'], 0)
|
||||||
self.re_sort()
|
for tab in (self.tabs.widget(i) for i in range(self.tabs.count())):
|
||||||
|
tab.show_themes(self.themes)
|
||||||
get_covers(self.themes, self)
|
get_covers(self.themes, self)
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
for i in range(self.theme_list.count()):
|
|
||||||
yield self.theme_list.item(i)
|
|
||||||
|
|
||||||
def item_from_name(self, name):
|
|
||||||
for item in self:
|
|
||||||
if item.data(Qt.ItemDataRole.UserRole)['name'] == name:
|
|
||||||
return item
|
|
||||||
|
|
||||||
def set_cover(self, theme, cdata):
|
def set_cover(self, theme, cdata):
|
||||||
theme['cover-pixmap'] = p = QPixmap()
|
theme['cover-pixmap'] = p = QPixmap()
|
||||||
try:
|
dpr = self.devicePixelRatioF()
|
||||||
dpr = self.devicePixelRatioF()
|
|
||||||
except AttributeError:
|
|
||||||
dpr = self.devicePixelRatio()
|
|
||||||
if isinstance(cdata, bytes):
|
if isinstance(cdata, bytes):
|
||||||
p.loadFromData(cdata)
|
p.loadFromData(cdata)
|
||||||
p.setDevicePixelRatio(dpr)
|
p.setDevicePixelRatio(dpr)
|
||||||
item = self.item_from_name(theme['name'])
|
for tab in (self.tabs.widget(i) for i in range(self.tabs.count())):
|
||||||
if item is not None:
|
tab.set_cover(theme['name'], p)
|
||||||
item.setData(Qt.ItemDataRole.DecorationRole, p)
|
|
||||||
|
|
||||||
def restore_defaults(self):
|
def restore_defaults(self):
|
||||||
if self.current_theme is not None:
|
if self.current_theme is not None:
|
||||||
@ -760,7 +795,7 @@ class ChooseTheme(Dialog):
|
|||||||
'Are you sure you want to remove the <b>%s</b> icon theme'
|
'Are you sure you want to remove the <b>%s</b> icon theme'
|
||||||
' and return to the stock icons?') % self.current_theme):
|
' and return to the stock icons?') % self.current_theme):
|
||||||
return
|
return
|
||||||
self.commit_changes = remove_icon_theme
|
# self.commit_changes = lambda: remove_icon_theme('all')
|
||||||
Dialog.accept(self)
|
Dialog.accept(self)
|
||||||
|
|
||||||
def accept(self):
|
def accept(self):
|
||||||
@ -810,7 +845,7 @@ class ChooseTheme(Dialog):
|
|||||||
dt.seek(0)
|
dt.seek(0)
|
||||||
f = BytesIO(lzma.decompress(dt.getvalue()))
|
f = BytesIO(lzma.decompress(dt.getvalue()))
|
||||||
f.seek(0)
|
f.seek(0)
|
||||||
remove_icon_theme()
|
# remove_icon_theme()
|
||||||
install_icon_theme(theme, f)
|
install_icon_theme(theme, f)
|
||||||
self.commit_changes = commit_changes
|
self.commit_changes = commit_changes
|
||||||
self.new_theme_title = theme['title']
|
self.new_theme_title = theme['title']
|
||||||
@ -819,25 +854,6 @@ class ChooseTheme(Dialog):
|
|||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
|
||||||
def remove_icon_theme():
|
|
||||||
icdir = os.path.join(config_dir, 'resources', 'images')
|
|
||||||
metadata_file = os.path.join(icdir, 'icon-theme.json')
|
|
||||||
try:
|
|
||||||
with open(metadata_file, 'rb') as f:
|
|
||||||
metadata = json.load(f)
|
|
||||||
except OSError as e:
|
|
||||||
if e.errno != errno.ENOENT:
|
|
||||||
raise
|
|
||||||
return
|
|
||||||
for name in metadata['files']:
|
|
||||||
try:
|
|
||||||
os.remove(os.path.join(icdir, *name.split('/')))
|
|
||||||
except OSError as e:
|
|
||||||
if e.errno != errno.ENOENT:
|
|
||||||
raise
|
|
||||||
os.remove(metadata_file)
|
|
||||||
|
|
||||||
|
|
||||||
def safe_copy(src, destpath):
|
def safe_copy(src, destpath):
|
||||||
tpath = destpath + '-temp'
|
tpath = destpath + '-temp'
|
||||||
with open(tpath, 'wb') as dest:
|
with open(tpath, 'wb') as dest:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user