Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improvements to value icons: #2622

Merged
merged 3 commits into from
Jan 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading