Skip to content

Commit

Permalink
Merge branch 'master' of https://github.com/cbhaley/calibre
Browse files Browse the repository at this point in the history
  • Loading branch information
kovidgoyal committed Jan 21, 2025
2 parents 296b0d3 + df10de2 commit abaf039
Show file tree
Hide file tree
Showing 7 changed files with 85 additions and 43 deletions.
4 changes: 3 additions & 1 deletion manual/gui.rst
Original file line number Diff line number Diff line change
Expand Up @@ -739,8 +739,10 @@ To choose icons for values in categories, right-click on a value then choose `Ma

* ``category``: the lookup name of the category, for example ``authors``, ``series``, ``#mycolumn``.
* ``value``: the value of the item within the category.
* ``count``: the number of books with this value. If the value is part of a hierarchy then the count includes the children.
* ``avg_rating``: the average rating for books with this value. If the value is part of a hierarchy then the average includes the children.

Book metadata such as title is not available.
Book metadata such as title is not available. Template database functions such as book_count() and book_values() will work, but the performance might not be acceptable. Python templates have full access to the calibre database API.

For example, this template specifies that any value in the clicked-on category beginning with `History` will have an icon named ``flower.png``::

Expand Down
5 changes: 4 additions & 1 deletion src/calibre/gui2/dialogs/template_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -1083,6 +1083,8 @@ def display_values(self, txt):
tv = self.template_value
l = self.template_value.selectionModel().selectedRows()
break_on_mi = 0 if len(l) == 0 else l[0].row()
from calibre.gui2.ui import get_gui
db = get_gui().current_db
for r,mi in enumerate(self.mi):
w = tv.cellWidget(r, 0)
w.setText(mi.get('title', _('No title provided')))
Expand All @@ -1096,7 +1098,8 @@ def display_values(self, txt):
mi, global_vars=self.global_vars,
template_functions=self.all_functions,
break_reporter=self.break_reporter if r == break_on_mi else None,
python_context_object=self.python_context_object)
python_context_object=self.python_context_object,
database=db)
w = tv.cellWidget(r, 2)
w.setText(v.translate(translate_table))
w.setCursorPosition(0)
Expand Down
11 changes: 8 additions & 3 deletions src/calibre/gui2/tag_browser/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ class TagTreeItem: # {{{
icon_config_dir = {}
file_icon_provider = None
eval_formatter = EvalFormatter()
database = None

def __init__(self, data=None, is_category=False, icon_map=None,
parent=None, tooltip=None, category_key=None, temporary=False,
Expand Down Expand Up @@ -143,9 +144,12 @@ def ensure_icon(self):
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]:
v = {'category': category, 'value': self.tag.original_name,
'count': getattr(self.tag, 'count', ''),
'avg_rating': getattr(self.tag, 'avg_rating', '')}
t = self.eval_formatter.safe_format(self.value_icons[category][TEMPLATE_ICON_INDICATOR][0],
{'category': category, 'value': self.tag.original_name},
'VALUE_ICON_TEMPLATE_ERROR', {})
v, 'VALUE_ICON_TEMPLATE_ERROR', {},
database=self.database)
if t:
val_icon = (os.path.join('template_icons', t), False)
else:
Expand Down Expand Up @@ -406,8 +410,8 @@ def __init__(self, parent, prefs=gprefs):
self.filter_categories_by = None
self.collapse_model = 'disable'
self.row_map = []
self.root_item = self.create_node(icon_map=self.icon_state_map)
self.db = None
self.root_item = self.create_node(icon_map=self.icon_state_map)
self._build_in_progress = False
self.reread_collapse_model({}, rebuild=False)
self.show_error_after_event_loop_tick_signal.connect(self.on_show_error_after_event_loop_tick, type=Qt.ConnectionType.QueuedConnection)
Expand Down Expand Up @@ -1483,6 +1487,7 @@ def create_node(self, *args, **kwargs):
node.value_icons = self.value_icons
node.value_icon_cache = self.value_icon_cache
node.icon_config_dir = self.icon_config_dir
node.database = self.db
return node

def get_node(self, idx):
Expand Down
11 changes: 9 additions & 2 deletions src/calibre/gui2/tag_browser/view.py
Original file line number Diff line number Diff line change
Expand Up @@ -686,14 +686,21 @@ def make_icon_name(key, index):
if action == 'set_icon':
if category is None:
if index is not None:
current_item = self._model.get_node(index).tag.original_name
tag = self._model.get_node(index).tag
current_item = tag.original_name
count = tag.count
avg_rating = tag.avg_rating
else:
current_item = _('No value available')
count = ''
avg_rating = ''
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
v = {'title': key, 'category': key, 'value': current_item,
'count': count, 'avg_rating': avg_rating}
d = TemplateDialog(parent=self, text=template,
mi={'title': key, 'category': key, 'value': current_item},
mi=v,
doing_emblem=True,
# fm=None, color_field=None, icon_field_key=None,
# icon_rule_kind=None, text_is_placeholder=False,
Expand Down
20 changes: 12 additions & 8 deletions src/calibre/srv/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,8 @@ def get_gpref(name: str, defval = None):
return gprefs.get(name, defval)


def get_icon_for_node(node, parent, node_to_tag_map, tag_map, eval_formatter):
def get_icon_for_node(node, parent, node_to_tag_map, tag_map, eval_formatter, db):
# This needs a legacy database so legacy formatter functions work
category = node['category']
if category in ('search', 'formats') or category.startswith('@'):
return
Expand All @@ -190,10 +191,13 @@ def name_for_icon(node):
if val_icon is not None and for_children:
break
par = pd
val_icon = None
if val_icon is None and TEMPLATE_ICON_INDICATOR in value_icons.get(category, {}):
v = {'category': category, 'value': name_for_icon(node),
'count': node.get('count', ''), 'avg_rating': node.get('avg_rating', '')}
t = eval_formatter.safe_format(
value_icons[category][TEMPLATE_ICON_INDICATOR][0], {'category': category, 'value': name_for_icon(node)},
'VALUE_ICON_TEMPLATE_ERROR', {})
value_icons[category][TEMPLATE_ICON_INDICATOR][0], v,
'VALUE_ICON_TEMPLATE_ERROR', {}, database=db)
if t:
# Use POSIX path separator
val_icon = 'template_icons/' + t
Expand Down Expand Up @@ -428,7 +432,7 @@ def collapse_first_letter(collapse_nodes, items, category_node, cl_list, idx, is
def process_category_node(
category_node, items, category_data, eval_formatter, field_metadata,
opts, tag_map, hierarchical_tags, node_to_tag_map, collapse_nodes,
intermediate_nodes, hierarchical_items):
intermediate_nodes, hierarchical_items, db):
category = items[category_node['id']]['category']
if category not in category_data:
# This can happen for user categories that are hierarchical and missing their parent.
Expand Down Expand Up @@ -469,7 +473,7 @@ def create_tag_node(tag, parent):
node = {'id':node_id, 'children':[]}
parent['children'].append(node)
try:
get_icon_for_node(node_data, parent, node_to_tag_map, tag_map, eval_formatter)
get_icon_for_node(node_data, parent, node_to_tag_map, tag_map, eval_formatter, db)
except Exception:
import traceback
traceback.print_exc()
Expand Down Expand Up @@ -555,7 +559,7 @@ def iternode_descendants(node):
yield from iternode_descendants(child)


def fillout_tree(root, items, node_id_map, category_nodes, category_data, field_metadata, opts, book_rating_map):
def fillout_tree(root, items, node_id_map, category_nodes, category_data, field_metadata, opts, book_rating_map, db):
eval_formatter = EvalFormatter()
tag_map, hierarchical_tags, node_to_tag_map = {}, set(), {}
first, later, collapse_nodes, intermediate_nodes, hierarchical_items = [], [], [], {}, set()
Expand All @@ -572,7 +576,7 @@ def fillout_tree(root, items, node_id_map, category_nodes, category_data, field_
process_category_node(
cnode, items, category_data, eval_formatter, field_metadata,
opts, tag_map, hierarchical_tags, node_to_tag_map,
collapse_nodes, intermediate_nodes, hierarchical_items)
collapse_nodes, intermediate_nodes, hierarchical_items, db)

# Do not store id_set in the tag items as it is a lot of data, with not
# much use. Instead only update the ratings and counts based on id_set
Expand Down Expand Up @@ -600,7 +604,7 @@ def render_categories(opts, db, category_data):
items = {}
with db.safe_read_lock:
root, node_id_map, category_nodes, recount_nodes = create_toplevel_tree(category_data, items, db.field_metadata, opts, db)
fillout_tree(root, items, node_id_map, category_nodes, category_data, db.field_metadata, opts, db.fields['rating'].book_value_map)
fillout_tree(root, items, node_id_map, category_nodes, category_data, db.field_metadata, opts, db.fields['rating'].book_value_map, db)
for node in recount_nodes:
item = items[node['id']]
item['count'] = sum(1 for x in iternode_descendants(node) if not items[x['id']].get('is_category', False))
Expand Down
17 changes: 11 additions & 6 deletions src/calibre/utils/formatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -1651,6 +1651,7 @@ def __init__(self):
self.recursion_level = -1
self._caller = None
self.python_context_object = None
self.database = None

def _do_format(self, val, fmt):
if not fmt or not val:
Expand Down Expand Up @@ -1759,7 +1760,8 @@ def _eval_python_template(self, template, column_name):
def _run_python_template(self, compiled_template, arguments):
try:
self.python_context_object.set_values(
db=get_database(self.book, get_database(self.book, None)),
db=(self.database if self.database is not None
else get_database(self.book, get_database(self.book, None))),
globals=self.global_vars,
arguments=arguments,
formatter=self,
Expand Down Expand Up @@ -1914,7 +1916,8 @@ def save_state(self):
self.funcs,
self.locals,
self._caller,
self.python_context_object))
self.python_context_object,
self.database))

def restore_state(self, state):
self.recursion_level -= 1
Expand All @@ -1929,7 +1932,8 @@ def restore_state(self, state):
self.funcs,
self.locals,
self._caller,
self.python_context_object) = state
self.python_context_object,
self.database) = state

# Allocate an interpreter if the formatter encounters a GPM or TPM template.
# We need to allocate additional interpreters if there is composite recursion
Expand Down Expand Up @@ -1980,12 +1984,13 @@ def safe_format(self, fmt, kwargs, error_value, book,
column_name=None, template_cache=None,
strip_results=True, template_functions=None,
global_vars=None, break_reporter=None,
python_context_object=None):
python_context_object=None, database=None):
state = self.save_state()
if self.recursion_level == 0:
# Initialize the composite values dict if this is the base-level
# call. Recursive calls will use the same dict.
# Initialize the composite values dict and database if this is the
# base-level call. Recursive calls will use the same dict.
self.composite_values = {}
self.database = database
try:
self._caller = FormatterFuncsCaller(self)
self.strip_results = strip_results
Expand Down
Loading

0 comments on commit abaf039

Please sign in to comment.