mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Merge branch 'master' of https://github.com/cbhaley/calibre
This commit is contained in:
commit
97efb777c8
@ -420,6 +420,7 @@ def create_defs():
|
||||
defs['bd_show_cover'] = True
|
||||
defs['bd_overlay_cover_size'] = False
|
||||
defs['tags_browser_category_icons'] = {}
|
||||
defs['tags_browser_value_icons'] = {}
|
||||
defs['cover_browser_reflections'] = True
|
||||
defs['book_list_extra_row_spacing'] = 0
|
||||
defs['refresh_book_list_on_bulk_edit'] = True
|
||||
|
@ -488,11 +488,16 @@ class TemplateDialog(QDialog, Ui_TemplateDialog):
|
||||
icon_field_key=None, icon_rule_kind=None, doing_emblem=False,
|
||||
text_is_placeholder=False, dialog_is_st_editor=False,
|
||||
global_vars=None, all_functions=None, builtin_functions=None,
|
||||
python_context_object=None, dialog_number=None):
|
||||
python_context_object=None, dialog_number=None,
|
||||
formatter=SafeFormat, icon_dir='cc_icons'):
|
||||
# If dialog_number isn't None then we want separate non-modal windows
|
||||
# that don't stay on top of the main dialog. This lets Alt-Tab work to
|
||||
# switch between them. dialog_number must be set only by the template
|
||||
# tester, not the rules dialogs etc that depend on modality.
|
||||
|
||||
# doing_emblem is also used for tag browser value icon rules in order to
|
||||
# show the icon selection widgets.
|
||||
|
||||
if dialog_number is None:
|
||||
QDialog.__init__(self, parent, flags=Qt.WindowType.Dialog)
|
||||
else:
|
||||
@ -502,6 +507,8 @@ class TemplateDialog(QDialog, Ui_TemplateDialog):
|
||||
self.setupUi(self)
|
||||
self.setWindowIcon(self.windowIcon())
|
||||
|
||||
self.formatter = formatter
|
||||
self.icon_dir = icon_dir
|
||||
self.ffml = FFMLProcessor()
|
||||
self.dialog_number = dialog_number
|
||||
self.coloring = color_field is not None
|
||||
@ -544,7 +551,7 @@ class TemplateDialog(QDialog, Ui_TemplateDialog):
|
||||
for n1, k1 in cols:
|
||||
self.icon_field.addItem(f'{n1} ({k1})', k1)
|
||||
self.icon_file_names = []
|
||||
d = os.path.join(config_dir, 'cc_icons')
|
||||
d = os.path.join(config_dir, icon_dir)
|
||||
if os.path.exists(d):
|
||||
for icon_file in os.listdir(d):
|
||||
icon_file = icu_lower(icon_file)
|
||||
@ -766,7 +773,7 @@ class TemplateDialog(QDialog, Ui_TemplateDialog):
|
||||
for r in range(0, len(mi)):
|
||||
w = QLineEdit(tv)
|
||||
w.setReadOnly(True)
|
||||
w.setText(mi[r].title)
|
||||
w.setText(mi[r].get('title', _('No title provided')))
|
||||
tv.setCellWidget(r, 0, w)
|
||||
tb = QToolButton()
|
||||
tb.setContentsMargins(0, 0, 0, 0)
|
||||
@ -992,7 +999,7 @@ def evaluate(book, context):
|
||||
self.update_filename_box()
|
||||
try:
|
||||
p = QIcon(icon_path).pixmap(QSize(128, 128))
|
||||
d = os.path.join(config_dir, 'cc_icons')
|
||||
d = os.path.join(config_dir, self.icon_dir)
|
||||
if not os.path.exists(os.path.join(d, icon_name)):
|
||||
if not os.path.exists(d):
|
||||
os.makedirs(d)
|
||||
@ -1012,7 +1019,7 @@ def evaluate(book, context):
|
||||
self.icon_files.addItem('')
|
||||
self.icon_files.addItems(self.icon_file_names)
|
||||
for i,filename in enumerate(self.icon_file_names):
|
||||
icon = QIcon(os.path.join(config_dir, 'cc_icons', filename))
|
||||
icon = QIcon(os.path.join(config_dir, self.icon_dir, filename))
|
||||
self.icon_files.setItemIcon(i+1, icon)
|
||||
|
||||
def color_to_clipboard(self):
|
||||
@ -1078,14 +1085,14 @@ def evaluate(book, context):
|
||||
break_on_mi = 0 if len(l) == 0 else l[0].row()
|
||||
for r,mi in enumerate(self.mi):
|
||||
w = tv.cellWidget(r, 0)
|
||||
w.setText(mi.title)
|
||||
w.setText(mi.get('title', _('No title provided')))
|
||||
w.setCursorPosition(0)
|
||||
if self.break_box.isChecked() and r == break_on_mi and self.is_python:
|
||||
sys.settrace(self.trace_calls)
|
||||
else:
|
||||
sys.settrace(None)
|
||||
try:
|
||||
v = SafeFormat().safe_format(txt, mi, _('EXCEPTION:'),
|
||||
v = self.formatter().safe_format(txt, mi, _('EXCEPTION:'),
|
||||
mi, global_vars=self.global_vars,
|
||||
template_functions=self.all_functions,
|
||||
break_reporter=self.break_reporter if r == break_on_mi else None,
|
||||
|
@ -30,6 +30,7 @@ TAG_SEARCH_STATES = {'clear': 0, 'mark_plus': 1, 'mark_plusplus': 2,
|
||||
'mark_minus': 3, 'mark_minusminus': 4}
|
||||
DRAG_IMAGE_ROLE = Qt.ItemDataRole.UserRole + 1000
|
||||
COUNT_ROLE = DRAG_IMAGE_ROLE + 1
|
||||
TEMPLATE_ICON_INDICATOR = ' template ' # Item values cannot start or end with space
|
||||
|
||||
_bf = None
|
||||
|
||||
@ -49,7 +50,11 @@ class TagTreeItem: # {{{
|
||||
TAG = 1
|
||||
ROOT = 2
|
||||
category_custom_icons = {}
|
||||
value_icons = {}
|
||||
value_icon_cache = {}
|
||||
icon_config_dir = {}
|
||||
file_icon_provider = None
|
||||
eval_formatter = EvalFormatter()
|
||||
|
||||
def __init__(self, data=None, is_category=False, icon_map=None,
|
||||
parent=None, tooltip=None, category_key=None, temporary=False,
|
||||
@ -60,6 +65,7 @@ class TagTreeItem: # {{{
|
||||
self.children = []
|
||||
self.blank = QIcon()
|
||||
self.is_gst = is_gst
|
||||
self.icon = None
|
||||
self.boxed = False
|
||||
self.temporary = False
|
||||
self.can_be_edited = False
|
||||
@ -117,9 +123,40 @@ class TagTreeItem: # {{{
|
||||
if self.is_gst:
|
||||
cc = self.category_custom_icons.get(self.root_node().category_key, None)
|
||||
elif self.tag.category == 'search' and not self.tag.is_searchable:
|
||||
cc = self.category_custom_icons.get('search_folder:', None)
|
||||
cc = self.category_custom_icons.get('search_folder', None)
|
||||
else:
|
||||
cc = self.category_custom_icons.get(self.tag.category, None)
|
||||
if self.icon is None:
|
||||
node = self
|
||||
val_icon = None
|
||||
category = node.tag.category
|
||||
if category in self.value_icons:
|
||||
while True:
|
||||
val_icon = self.value_icons.get(category, {}).get(node.tag.original_name)
|
||||
if val_icon is not None:
|
||||
# Have an icon. Use it if value exact match or
|
||||
# it applies to children
|
||||
if node != self and not val_icon[1]:
|
||||
val_icon = None
|
||||
break
|
||||
node = node.parent
|
||||
if node.type != self.TAG or node.type == self.ROOT:
|
||||
break
|
||||
if val_icon is None and TEMPLATE_ICON_INDICATOR in self.value_icons[category]:
|
||||
t = self.eval_formatter.safe_format(self.value_icons[category][TEMPLATE_ICON_INDICATOR][0],
|
||||
{'category': category, 'value': node.tag.original_name},
|
||||
'VALUE_ICON_TEMPLATE_ERROR', None)
|
||||
if t:
|
||||
val_icon = (os.path.join('template_icons', t), False)
|
||||
if val_icon is not None:
|
||||
cc = self.value_icon_cache.get(val_icon[0])
|
||||
if cc is None:
|
||||
cc = QIcon.ic(os.path.join(self.icon_config_dir, val_icon[0]))
|
||||
self.value_icon_cache[val_icon[0]] = cc
|
||||
self.icon = cc
|
||||
else:
|
||||
cc = self.category_custom_icons.get(self.tag.category, None)
|
||||
else:
|
||||
cc = self.icon
|
||||
elif self.type == self.CATEGORY:
|
||||
cc = self.category_custom_icons.get(self.category_key, None)
|
||||
self.icon_state_map[0] = cc or QIcon()
|
||||
@ -350,8 +387,11 @@ class TagsModel(QAbstractItemModel): # {{{
|
||||
self.node_map = {}
|
||||
self.category_nodes = []
|
||||
self.category_custom_icons = {}
|
||||
self.value_icons = {}
|
||||
self.value_icon_cache = {}
|
||||
self.icon_config_dir = os.path.join(config_dir, 'tb_icons')
|
||||
for k, v in iteritems(self.prefs['tags_browser_category_icons']):
|
||||
icon = QIcon(os.path.join(config_dir, 'tb_icons', v))
|
||||
icon = QIcon(os.path.join(self.icon_config_dir, v))
|
||||
if len(icon.availableSizes()) > 0:
|
||||
self.category_custom_icons[k] = icon
|
||||
self.categories_with_ratings = ['authors', 'series', 'publisher', 'tags']
|
||||
@ -387,9 +427,44 @@ class TagsModel(QAbstractItemModel): # {{{
|
||||
new_icon = new_key + ext
|
||||
new_path = os.path.join(config_dir, 'tb_icons', new_icon)
|
||||
os.replace(old_path, new_path)
|
||||
self.set_custom_category_icon(new_key, new_path)
|
||||
self.set_custom_category_icon(new_key, new_icon)
|
||||
self.set_custom_category_icon(old_key, None)
|
||||
|
||||
def set_value_icon(self, key, value, file_name, children):
|
||||
'''
|
||||
Add a 'rule' for an icon for a value in the tag browser as a dict entry:
|
||||
value_icons[key] = {value: (file_name, children)}
|
||||
|
||||
:param key: the lookup name for the tag browser category
|
||||
:param value: the item value in the category. If the value is
|
||||
TEMPLATE_ICON_INDICATOR then the rule applies to all items
|
||||
that don't have a specific rule.
|
||||
:param file_name: the name of the icon file to use for this value. If
|
||||
this is a template rule then this is the text of the template.
|
||||
:param children: for specific (non-template) rules: if True then the rule
|
||||
is to be used for any children of the item that don't have
|
||||
a specific rule. If False then this rule is used only for
|
||||
the specified item.
|
||||
'''
|
||||
v = self.value_icons = self.prefs['tags_browser_value_icons']
|
||||
if key not in v:
|
||||
self.value_icons[key] = {value: (file_name, children)}
|
||||
else:
|
||||
self.value_icons[key].update({value: (file_name, children)})
|
||||
self.value_icon_cache.pop(file_name, None)
|
||||
self.prefs['tags_browser_value_icons'] = self.value_icons
|
||||
|
||||
def remove_value_icon(self, key, value, file_name):
|
||||
self.value_icons = self.prefs['tags_browser_value_icons']
|
||||
self.value_icons.get(key).pop(value, None)
|
||||
self.prefs['tags_browser_value_icons'] =self.value_icons
|
||||
if file_name is not None:
|
||||
path = os.path.join(config_dir, 'tb_icons', file_name)
|
||||
try:
|
||||
os.remove(path)
|
||||
except:
|
||||
pass
|
||||
|
||||
def set_custom_category_icon(self, key, path):
|
||||
d = self.prefs['tags_browser_category_icons']
|
||||
if path:
|
||||
@ -403,8 +478,8 @@ class TagsModel(QAbstractItemModel): # {{{
|
||||
os.remove(path)
|
||||
except:
|
||||
pass
|
||||
del d[key]
|
||||
del self.category_custom_icons[key]
|
||||
d.pop(key, None)
|
||||
self.category_custom_icons.pop(key, None)
|
||||
self.prefs['tags_browser_category_icons'] = d
|
||||
|
||||
def reread_collapse_model(self, state_map, rebuild=True):
|
||||
@ -417,6 +492,7 @@ class TagsModel(QAbstractItemModel): # {{{
|
||||
|
||||
def set_database(self, db, hidden_categories=None):
|
||||
self.beginResetModel()
|
||||
self.value_icons = self.prefs['tags_browser_value_icons']
|
||||
hidden_cats = db.new_api.pref('tag_browser_hidden_categories', None)
|
||||
# migrate from config to db prefs
|
||||
if hidden_cats is None:
|
||||
@ -514,8 +590,8 @@ class TagsModel(QAbstractItemModel): # {{{
|
||||
data = self._get_category_nodes(config['sort_tags_by'])
|
||||
gst = self.db.new_api.pref('grouped_search_terms', {})
|
||||
|
||||
if self.category_custom_icons.get('search_folder:', None) is None:
|
||||
self.category_custom_icons['search_folder:'] = QIcon.ic('folder_saved_search')
|
||||
if self.category_custom_icons.get('search_folder', None) is None:
|
||||
self.category_custom_icons['search_folder'] = QIcon.ic('folder_saved_search')
|
||||
last_category_node = None
|
||||
category_node_map = {}
|
||||
self.user_category_node_tree = {}
|
||||
@ -1393,6 +1469,9 @@ class TagsModel(QAbstractItemModel): # {{{
|
||||
node = TagTreeItem(*args, **kwargs)
|
||||
self.node_map[id(node)] = node
|
||||
node.category_custom_icons = self.category_custom_icons
|
||||
node.value_icons = self.value_icons
|
||||
node.value_icon_cache = self.value_icon_cache
|
||||
node.icon_config_dir = self.icon_config_dir
|
||||
return node
|
||||
|
||||
def get_node(self, idx):
|
||||
|
@ -48,7 +48,8 @@ from calibre.ebooks.metadata import rating_to_stars
|
||||
from calibre.gui2 import FunctionDispatcher, choose_files, config, empty_index, gprefs, pixmap_to_data, question_dialog, rating_font, safe_open_url
|
||||
from calibre.gui2.complete2 import EditWithComplete
|
||||
from calibre.gui2.dialogs.edit_category_notes import EditNoteDialog
|
||||
from calibre.gui2.tag_browser.model import COUNT_ROLE, DRAG_IMAGE_ROLE, TAG_SEARCH_STATES, TagsModel, TagTreeItem, rename_only_in_vl_question
|
||||
from calibre.gui2.tag_browser.model import COUNT_ROLE, DRAG_IMAGE_ROLE, TAG_SEARCH_STATES, TEMPLATE_ICON_INDICATOR
|
||||
from calibre.gui2.tag_browser.model import TagsModel, TagTreeItem, rename_only_in_vl_question
|
||||
from calibre.gui2.widgets import EnLineEdit
|
||||
from calibre.utils.icu import sort_key
|
||||
from calibre.utils.serialize import json_loads
|
||||
@ -634,6 +635,12 @@ class TagsView(QTreeView): # {{{
|
||||
key=None, index=None, search_state=None,
|
||||
is_first_letter=False, ignore_vl=False,
|
||||
extra=None):
|
||||
'''
|
||||
action: a string specifying the operation
|
||||
category: the human readable label for the category
|
||||
key: the lookup name for the category
|
||||
index: the index of the item, if there is one.
|
||||
'''
|
||||
if not action:
|
||||
return
|
||||
from calibre.gui2.ui import get_gui
|
||||
@ -654,28 +661,79 @@ class TagsView(QTreeView): # {{{
|
||||
self.db.prefs.set('tag_browser_dont_collapse', extra)
|
||||
self.recount()
|
||||
return
|
||||
|
||||
# category is None if the user asked to specify a template
|
||||
# index is None if the user clicked on a category (top level) node
|
||||
# extra is a tuple: (icon_file_name: string or None, children: True or False)
|
||||
def make_icon_name(key, index):
|
||||
icon_file_name = 'icon_' + sanitize_file_name(key)
|
||||
if index is not None:
|
||||
item_val = self._model.get_node(index).tag.original_name
|
||||
icon_file_name = icon_file_name + '@@' + sanitize_file_name(item_val)
|
||||
else:
|
||||
item_val = None
|
||||
icon_file_name += '.png'
|
||||
return item_val, icon_file_name
|
||||
|
||||
if action == 'set_icon':
|
||||
try:
|
||||
path = choose_files(self, 'choose_category_icon',
|
||||
_('Change icon for: %s')%key, filters=[
|
||||
('Images', ['png', 'gif', 'jpg', 'jpeg'])],
|
||||
all_files=False, select_only_single_file=True)
|
||||
if path:
|
||||
path = path[0]
|
||||
p = QIcon(path).pixmap(QSize(128, 128))
|
||||
d = os.path.join(config_dir, 'tb_icons')
|
||||
if not os.path.exists(d):
|
||||
os.makedirs(d)
|
||||
with open(os.path.join(d, 'icon_' + sanitize_file_name(key)+'.png'), 'wb') as f:
|
||||
f.write(pixmap_to_data(p, format='PNG'))
|
||||
path = os.path.basename(f.name)
|
||||
self._model.set_custom_category_icon(key, str(path))
|
||||
if category is None:
|
||||
if index is not None:
|
||||
current_item = self._model.get_node(index).tag.original_name
|
||||
else:
|
||||
current_item = _('No value available')
|
||||
template = self._model.value_icons.get(key, {}).get(TEMPLATE_ICON_INDICATOR, ('', False))[0]
|
||||
from calibre.gui2.dialogs.template_dialog import TemplateDialog
|
||||
from calibre.utils.formatter import EvalFormatter
|
||||
d = TemplateDialog(parent=self, text=template,
|
||||
mi={'title': key, 'category': key, 'value': current_item},
|
||||
doing_emblem=True,
|
||||
# fm=None, color_field=None, icon_field_key=None,
|
||||
# icon_rule_kind=None, text_is_placeholder=False,
|
||||
# dialog_is_st_editor=False,
|
||||
# global_vars=None, all_functions=None, builtin_functions=None,
|
||||
# python_context_object=None, dialog_number=None,
|
||||
formatter=EvalFormatter, icon_dir='tb_icons/template_icons')
|
||||
if d.exec():
|
||||
self._model.set_value_icon(key, TEMPLATE_ICON_INDICATOR, d.rule[2], False)
|
||||
self.recount()
|
||||
except:
|
||||
traceback.print_exc()
|
||||
return
|
||||
(icon_file_name, for_children) = extra
|
||||
item_val, desired_file_name = make_icon_name(key, index)
|
||||
if icon_file_name is None:
|
||||
# User wants to specify a specific icon
|
||||
try:
|
||||
icon_file_name = desired_file_name
|
||||
path = choose_files(self, 'choose_category_icon',
|
||||
_('Change icon for: %s')%key, filters=[
|
||||
('Images', ['png', 'gif', 'jpg', 'jpeg'])],
|
||||
all_files=False, select_only_single_file=True)
|
||||
if path:
|
||||
path = path[0]
|
||||
p = QIcon(path).pixmap(QSize(128, 128))
|
||||
d = os.path.join(config_dir, 'tb_icons')
|
||||
if not os.path.exists(d):
|
||||
os.makedirs(d)
|
||||
with open(os.path.join(d, icon_file_name), 'wb') as f:
|
||||
f.write(pixmap_to_data(p, format='PNG'))
|
||||
except:
|
||||
traceback.print_exc()
|
||||
else:
|
||||
# Already have an icon. User wants to change whether it applies to children
|
||||
icon_file_name = desired_file_name
|
||||
if index is None: # category icon
|
||||
self._model.set_custom_category_icon(key, str(icon_file_name))
|
||||
self.recount()
|
||||
else: # value icon
|
||||
self._model.set_value_icon(key, item_val, icon_file_name, bool(for_children))
|
||||
self.recount()
|
||||
return
|
||||
if action == 'clear_icon':
|
||||
self._model.set_custom_category_icon(key, None)
|
||||
if index is not None:
|
||||
val, icon_name = make_icon_name(key, index)
|
||||
self._model.remove_value_icon(key, val, icon_name)
|
||||
else:
|
||||
self._model.set_custom_category_icon(key, None)
|
||||
self._model.remove_value_icon(key, TEMPLATE_ICON_INDICATOR, None)
|
||||
self.recount()
|
||||
return
|
||||
|
||||
@ -1163,9 +1221,9 @@ class TagsView(QTreeView): # {{{
|
||||
category=key)).setIcon(QIcon.ic('minus.png'))
|
||||
add_show_hidden_categories()
|
||||
|
||||
cm = self.context_menu
|
||||
cm.addSeparator()
|
||||
if tag is None:
|
||||
cm = self.context_menu
|
||||
cm.addSeparator()
|
||||
acategory = category.replace('&', '&&')
|
||||
sm = cm.addAction(_('Change {} category icon').format(acategory),
|
||||
partial(self.context_menu_handler, action='set_icon',
|
||||
@ -1178,13 +1236,56 @@ class TagsView(QTreeView): # {{{
|
||||
if key == 'search' and 'search' in self.db.new_api.pref('categories_using_hierarchy', ()):
|
||||
sm = cm.addAction(_('Change Saved searches folder icon'),
|
||||
partial(self.context_menu_handler, action='set_icon',
|
||||
key='search_folder:', category=_('Saved searches folder')))
|
||||
key='search_folder', category=_('Saved searches folder')))
|
||||
sm.setIcon(QIcon.ic('icon_choose.png'))
|
||||
sm = cm.addAction(_('Restore Saved searches folder default icon'),
|
||||
partial(self.context_menu_handler, action='clear_icon',
|
||||
key='search_folder:', category=_('Saved searches folder')))
|
||||
key='search_folder', category=_('Saved searches folder')))
|
||||
sm.setIcon(QIcon.ic('edit-clear.png'))
|
||||
if key not in ('search', 'formats') and not key.startswith('@'):
|
||||
im = cm.addMenu(_('Manage value icons'))
|
||||
def get_rule_data(tag, key):
|
||||
if tag is None:
|
||||
return (None, None, None)
|
||||
name = tag.original_name
|
||||
cat_rules = self._model.value_icons.get(key, {})
|
||||
icon_name, for_child = cat_rules.get(name, (None, None))
|
||||
return (name, icon_name, for_child)
|
||||
|
||||
name,icon_name,for_child = get_rule_data(tag, key)
|
||||
if name is not None:
|
||||
im.addSection(_('Current value: {}').format(name))
|
||||
else:
|
||||
im.addSection(_('No value available'))
|
||||
im.addSeparator
|
||||
ma = im.addAction(_('Choose an icon for this value but not its children'),
|
||||
partial(self.context_menu_handler, action='set_icon',
|
||||
key=key, index=index, category=category, extra=(None, False)))
|
||||
ma.setEnabled(name is not None)
|
||||
ma = im.addAction(_('Choose an icon for this value and children'),
|
||||
partial(self.context_menu_handler, action='set_icon',
|
||||
key=key, index=index, category=category, extra=(None, True)))
|
||||
ma.setEnabled(name is not None)
|
||||
im.addSeparator()
|
||||
ma = im.addAction(_('Use the existing icon for this value but not its children'),
|
||||
partial(self.context_menu_handler, action='set_icon',
|
||||
key=key, index=index, category=category, extra=(icon_name, False)))
|
||||
ma.setEnabled(icon_name is not None and for_child)
|
||||
ma = im.addAction(_('Use the existing icon for this value and its children'),
|
||||
partial(self.context_menu_handler, action='set_icon',
|
||||
key=key, index=index, category=category, extra=(icon_name, True)))
|
||||
ma.setEnabled(icon_name is not None and not for_child)
|
||||
im.addAction(_('Use the default icon for this value'),
|
||||
partial(self.context_menu_handler, action='clear_icon',
|
||||
key=key, index=index, category=category))
|
||||
im.addSection(_('Defaults'))
|
||||
im.addAction(_('Use a template to choose the default value icon'),
|
||||
partial(self.context_menu_handler, action='set_icon',
|
||||
key=key, index=index, category=None, extra=(None, None)))
|
||||
im.addAction(_('Use the category icon for the default value icon'),
|
||||
partial(self.context_menu_handler, action='clear_icon',
|
||||
key=key, index=None, category=category))
|
||||
im.addSeparator()
|
||||
# Always show the User categories editor
|
||||
self.context_menu.addSeparator()
|
||||
if key.startswith('@') and \
|
||||
|
Loading…
x
Reference in New Issue
Block a user