This commit is contained in:
Kovid Goyal 2023-12-18 19:56:54 +05:30
commit 988759188a
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
6 changed files with 248 additions and 68 deletions

View File

@ -423,6 +423,8 @@ def create_defs():
defs['tb_search_order'] = {'0': 1, '1': 2, '2': 3, '3': 4, '4': 0} defs['tb_search_order'] = {'0': 1, '1': 2, '2': 3, '3': 4, '4': 0}
defs['search_tool_bar_shows_text'] = True defs['search_tool_bar_shows_text'] = True
defs['allow_keyboard_search_in_library_views'] = True defs['allow_keyboard_search_in_library_views'] = True
defs['show_links_in_tag_brouser'] = False
defs['show_notes_in_tag_brouser'] = False
def migrate_tweak(tweak_name, pref_name): def migrate_tweak(tweak_name, pref_name):
# If the tweak has been changed then leave the tweak in the file so # If the tweak has been changed then leave the tweak in the file so

View File

@ -646,6 +646,8 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
r('language', prefs, choices=choices, restart_required=True, setting=LanguageSetting) r('language', prefs, choices=choices, restart_required=True, setting=LanguageSetting)
r('show_avg_rating', config) r('show_avg_rating', config)
r('show_links_in_tag_brouser', gprefs)
r('show_notes_in_tag_brouser', gprefs)
r('disable_animations', config) r('disable_animations', config)
r('systray_icon', config, restart_required=True) r('systray_icon', config, restart_required=True)
r('show_splash_screen', gprefs) r('show_splash_screen', gprefs)

View File

@ -1290,66 +1290,99 @@ structure and you want to use the same column order for each one.&lt;/p&gt;</str
</item> </item>
</layout> </layout>
</item> </item>
<item row="5" column="0"> <item row="5" column="0" colspan="3">
<widget class="QCheckBox" name="opt_show_avg_rating"> <layout class="QGridLayout" name="gridlayout_22">
<property name="text"> <item row="0" column="0">
<string>Show &amp;average ratings</string> <widget class="QCheckBox" name="opt_show_avg_rating">
</property> <property name="text">
<property name="checked"> <string>Show &amp;average ratings</string>
<bool>true</bool> </property>
</property> <property name="toolTip">
</widget> <string>Show the average rating per item indication in the Tag browser</string>
</item> </property>
<item row="5" column="2"> <property name="checked">
<widget class="QCheckBox" name="opt_tag_browser_show_tooltips"> <bool>true</bool>
<property name="text"> </property>
<string>Show &amp;tooltips</string> </widget>
</property> </item>
</widget> <item row="0" column="1">
</item> <widget class="QCheckBox" name="opt_show_links_in_tag_brouser">
<item row="6" column="0"> <property name="text">
<widget class="QCheckBox" name="opt_tag_browser_show_counts"> <string>Show &amp;links icons</string>
<property name="toolTip"> </property>
<string>&lt;p&gt;Show counts for items in the Tag browser. Such as the number of books <property name="toolTip">
<string>Show an icon if the item has an attached link</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QCheckBox" name="opt_tag_browser_show_tooltips">
<property name="text">
<string>Show &amp;tooltips</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="opt_tag_browser_show_counts">
<property name="toolTip">
<string>&lt;p&gt;Show counts for items in the Tag browser. Such as the number of books
by each author, the number of authors, etc. If you turn it off, you can still by each author, the number of authors, etc. If you turn it off, you can still
see the counts by hovering your mouse over any item.&lt;/p&gt;</string> see the counts by hovering your mouse over any item.&lt;/p&gt;</string>
</property> </property>
<property name="text"> <property name="text">
<string>Show &amp;counts</string> <string>Show &amp;counts</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="6" column="2"> <item row="1" column="1">
<widget class="QCheckBox" name="opt_tag_browser_old_look"> <widget class="QCheckBox" name="opt_show_notes_in_tag_brouser">
<property name="text"> <property name="text">
<string>Use &amp;alternating row colors</string> <string>Show &amp;notes icons</string>
</property> </property>
</widget> <property name="toolTip">
</item> <string>Show an icon if the item has an attached note</string>
<item row="7" column="0"> </property>
<widget class="QCheckBox" name="opt_tag_browser_hide_empty_categories"> <property name="checked">
<property name="toolTip"> <bool>true</bool>
<string>&lt;p&gt;When checked, calibre will automatically hide any category </property>
</widget>
</item>
<item row="1" column="2">
<widget class="QCheckBox" name="opt_tag_browser_old_look">
<property name="text">
<string>Use &amp;alternating row colors</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="opt_tag_browser_hide_empty_categories">
<property name="toolTip">
<string>&lt;p&gt;When checked, calibre will automatically hide any category
(a column, custom or standard) that has no items to show. For example, some (a column, custom or standard) that has no items to show. For example, some
categories might not have values when using Virtual libraries. Checking this categories might not have values when using Virtual libraries. Checking this
box will cause these empty categories to be hidden.&lt;/p&gt;</string> box will cause these empty categories to be hidden.&lt;/p&gt;</string>
</property> </property>
<property name="text"> <property name="text">
<string>Hide empt&amp;y categories (columns)</string> <string>Hide empt&amp;y categories (columns)</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="7" column="2"> <item row="2" column="2">
<widget class="QCheckBox" name="opt_tag_browser_always_autocollapse"> <widget class="QCheckBox" name="opt_tag_browser_always_autocollapse">
<property name="toolTip"> <property name="toolTip">
<string>&lt;p&gt;When checked, Find in the Tag browser will show all items <string>&lt;p&gt;When checked, Find in the Tag browser will show all items
that match the search instead of the first one. If Hide empty categories is that match the search instead of the first one. If Hide empty categories is
also checked then only categories containing a matched item will be shown.&lt;/p&gt;</string> also checked then only categories containing a matched item will be shown.&lt;/p&gt;</string>
</property> </property>
<property name="text"> <property name="text">
<string>Find &amp;shows all items that match</string> <string>Find &amp;shows all items that match</string>
</property> </property>
</widget> </widget>
</item>
</layout>
</item> </item>
<item row="8" column="0" colspan="3"> <item row="8" column="0" colspan="3">
<layout class="QVBoxLayout" name="verticalLayout"> <layout class="QVBoxLayout" name="verticalLayout">

View File

@ -235,10 +235,16 @@ class TagTreeItem: # {{{
ar = self.average_rating ar = self.average_rating
if ar: if ar:
tt.append(_('Average rating for books in this category: %.1f') % ar) tt.append(_('Average rating for books in this category: %.1f') % ar)
elif self.type == self.TAG and ar is not None: elif self.type == self.TAG:
tt.append(_('Books in this category are unrated')) if ar is not None:
if self.type == self.TAG and tag.category != 'search': tt.append(_('Books in this category are unrated'))
tt.append(_('Number of books: %s') % self.item_count) if tag.category != 'search':
tt.append(_('Number of books: %s') % self.item_count)
from calibre.gui2.ui import get_gui
db = get_gui().current_db.new_api
link = db.get_link_map(tag.category).get(tag.original_name)
if link:
tt.append(_('Link: %s') % link)
return '\n'.join(tt) return '\n'.join(tt)
return None return None
if role == DRAG_IMAGE_ROLE: if role == DRAG_IMAGE_ROLE:
@ -365,6 +371,7 @@ class TagsModel(QAbstractItemModel): # {{{
self._build_in_progress = False self._build_in_progress = False
self.reread_collapse_model({}, rebuild=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) self.show_error_after_event_loop_tick_signal.connect(self.on_show_error_after_event_loop_tick, type=Qt.ConnectionType.QueuedConnection)
self.reset_notes_and_link_maps()
@property @property
def gui_parent(self): def gui_parent(self):
@ -441,6 +448,43 @@ class TagsModel(QAbstractItemModel): # {{{
self._run_rebuild() self._run_rebuild()
self.endResetModel() self.endResetModel()
def _cached_notes_map(self, category):
if self.notes_map is None:
self.notes_map = {}
if category not in self.notes_map:
try:
self.notes_map[category] = (self.db.new_api.get_all_items_that_have_notes(category),
self.db.new_api.get_item_name_map(category))
except:
self.notes_map[category] = (frozenset(), {})
return self.notes_map[category]
def _cached_link_map(self, category):
if self.link_map is None:
self.link_map = {}
if category not in self.link_map:
try:
self.link_map[category] = self.db.new_api.get_link_map(category)
except Exception:
self.link_map[category] = {}
return self.link_map[category]
def category_has_notes(self, category):
return len(self._cached_notes_map(category)[0]) > 0
def item_has_note(self, category, item_name):
notes_map, item_id_map = self._cached_notes_map(category)
return item_id_map.get(item_name) in notes_map
def category_has_links(self, category):
return len(self._cached_link_map(category)) > 0
def item_has_link(self, category, item_name):
return item_name in self._cached_link_map(category)
def reset_notes_and_link_maps(self):
self.link_map = self.notes_map = None
def rebuild_node_tree(self, state_map={}): def rebuild_node_tree(self, state_map={}):
if self._build_in_progress: if self._build_in_progress:
print('Tag browser build already in progress') print('Tag browser build already in progress')
@ -455,6 +499,7 @@ class TagsModel(QAbstractItemModel): # {{{
self._build_in_progress = False self._build_in_progress = False
def _run_rebuild(self, state_map={}): def _run_rebuild(self, state_map={}):
self.reset_notes_and_link_maps()
for node in itervalues(self.node_map): for node in itervalues(self.node_map):
node.break_cycles() node.break_cycles()
del node # Clear reference to node in the current frame del node # Clear reference to node in the current frame

View File

@ -514,6 +514,7 @@ class TagBrowserMixin: # {{{
m = self.library_view.model() m = self.library_view.model()
ids = [m.id(r) for r in rows] ids = [m.id(r) for r in rows]
self.tags_view.model().reset_notes_and_link_maps()
m.refresh(reset=False) m.refresh(reset=False)
m.research() m.research()
self.library_view.select_rows(ids) self.library_view.select_rows(ids)
@ -778,12 +779,26 @@ class TagBrowserWidget(QFrame): # {{{
_('Configure Tag browser'), default_keys=(), _('Configure Tag browser'), default_keys=(),
action=ac, group=_('Tag browser')) action=ac, group=_('Tag browser'))
ac.triggered.connect(l.showMenu) ac.triggered.connect(l.showMenu)
l.m.aboutToShow.connect(self.about_to_show_configure_menu) l.m.aboutToShow.connect(self.about_to_show_configure_menu)
# Show/hide counts
l.m.show_counts_action = ac = l.m.addAction('counts') l.m.show_counts_action = ac = l.m.addAction('counts')
ac.triggered.connect(self.toggle_counts) ac.triggered.connect(self.toggle_counts)
# Show/hide average rating
l.m.show_avg_rating_action = ac = l.m.addAction(QIcon.ic('rating.png'), 'avg rating') l.m.show_avg_rating_action = ac = l.m.addAction(QIcon.ic('rating.png'), 'avg rating')
ac.triggered.connect(self.toggle_avg_rating) ac.triggered.connect(self.toggle_avg_rating)
# Show/hide notes icon
l.m.show_notes_icon_action = ac = l.m.addAction(QIcon.ic('notes.png'), 'notes icon')
ac.triggered.connect(self.toggle_notes_icon)
parent.keyboard.register_shortcut('tag browser toggle notes',
_('Toggle notes icons'), default_keys=(),
action=ac, group=_('Tag browser'))
# Show/hide links icon
l.m.show_links_icon_action = ac = l.m.addAction(QIcon.ic('external-link.png'), 'links icon')
ac.triggered.connect(self.toggle_links_icon)
parent.keyboard.register_shortcut('tag browser toggle links',
_('Toggle links icons'), default_keys=(),
action=ac, group=_('Tag browser'))
sb = l.m.addAction(QIcon.ic('sort.png'), _('Sort by')) sb = l.m.addAction(QIcon.ic('sort.png'), _('Sort by'))
sb.m = l.sort_menu = QMenu(l.m) sb.m = l.sort_menu = QMenu(l.m)
sb.setMenu(sb.m) sb.setMenu(sb.m)
@ -857,6 +872,12 @@ class TagBrowserWidget(QFrame): # {{{
ac = self.alter_tb.m.show_avg_rating_action ac = self.alter_tb.m.show_avg_rating_action
ac.setText(_('Hide average rating') if config['show_avg_rating'] else _('Show average rating')) ac.setText(_('Hide average rating') if config['show_avg_rating'] else _('Show average rating'))
ac.setIcon(QIcon.ic('minus.png' if config['show_avg_rating'] else 'plus.png')) ac.setIcon(QIcon.ic('minus.png' if config['show_avg_rating'] else 'plus.png'))
ac = self.alter_tb.m.show_notes_icon_action
ac.setText(_('Hide notes icon') if gprefs['show_notes_in_tag_brouser'] else _('Show notes icon'))
ac.setIcon(QIcon.ic('minus.png' if gprefs['show_notes_in_tag_brouser'] else 'plus.png'))
ac = self.alter_tb.m.show_links_icon_action
ac.setText(_('Hide links icon') if gprefs['show_links_in_tag_brouser'] else _('Show links icon'))
ac.setIcon(QIcon.ic('minus.png' if gprefs['show_links_in_tag_brouser'] else 'plus.png'))
def filter_book_list(self): def filter_book_list(self):
self.tags_view.model().set_in_tag_browser() self.tags_view.model().set_in_tag_browser()
@ -864,9 +885,19 @@ class TagBrowserWidget(QFrame): # {{{
def toggle_counts(self): def toggle_counts(self):
gprefs['tag_browser_show_counts'] ^= True gprefs['tag_browser_show_counts'] ^= True
self.tags_view.recount_with_position_based_index()
def toggle_avg_rating(self): def toggle_avg_rating(self):
config['show_avg_rating'] ^= True config['show_avg_rating'] ^= True
self.tags_view.recount_with_position_based_index()
def toggle_notes_icon(self):
gprefs['show_notes_in_tag_brouser'] ^= True
self.tags_view.recount_with_position_based_index()
def toggle_links_icon(self):
gprefs['show_links_in_tag_brouser'] ^= True
self.tags_view.recount_with_position_based_index()
def save_state(self): def save_state(self):
gprefs.set('tag browser search box visible', self.toggle_search_button.isChecked()) gprefs.set('tag browser search box visible', self.toggle_search_button.isChecked())

View File

@ -8,6 +8,7 @@ __docformat__ = 'restructuredtext en'
import os import os
import re import re
import traceback import traceback
from collections import defaultdict
from contextlib import suppress from contextlib import suppress
from functools import partial from functools import partial
from qt.core import ( from qt.core import (
@ -22,7 +23,7 @@ from calibre.constants import config_dir
from calibre.ebooks.metadata import rating_to_stars from calibre.ebooks.metadata import rating_to_stars
from calibre.gui2 import ( from calibre.gui2 import (
FunctionDispatcher, choose_files, config, empty_index, gprefs, pixmap_to_data, FunctionDispatcher, choose_files, config, empty_index, gprefs, pixmap_to_data,
question_dialog, rating_font, question_dialog, rating_font, safe_open_url,
) )
from calibre.gui2.dialogs.edit_category_notes import EditNoteDialog from calibre.gui2.dialogs.edit_category_notes import EditNoteDialog
from calibre.gui2.complete2 import EditWithComplete from calibre.gui2.complete2 import EditWithComplete
@ -43,6 +44,9 @@ class TagDelegate(QStyledItemDelegate): # {{{
self.rating_pat = re.compile(r'[%s]' % rating_to_stars(3, True)) self.rating_pat = re.compile(r'[%s]' % rating_to_stars(3, True))
self.rating_font = QFont(rating_font()) self.rating_font = QFont(rating_font())
self.tags_view = tags_view self.tags_view = tags_view
self.links_icon = QIcon.ic('external-link.png')
self.notes_icon = QIcon.ic('notes.png')
self.blank_icon = QIcon()
def draw_average_rating(self, item, style, painter, option, widget): def draw_average_rating(self, item, style, painter, option, widget):
rating = item.average_rating rating = item.average_rating
@ -86,6 +90,30 @@ class TagDelegate(QStyledItemDelegate): # {{{
hover = option.state & QStyle.StateFlag.State_MouseOver hover = option.state & QStyle.StateFlag.State_MouseOver
is_search = (True if item.type == TagTreeItem.TAG and is_search = (True if item.type == TagTreeItem.TAG and
item.tag.category == 'search' else False) item.tag.category == 'search' else False)
show_notes = gprefs['show_notes_in_tag_brouser']
show_links = gprefs['show_links_in_tag_brouser']
if item.type == TagTreeItem.TAG:
category = item.tag.category
name = item.tag.original_name
tv = self.tags_view
m = tv._model
if show_links and m.category_has_links(category):
icon = self.links_icon if m.item_has_link(category, name) else self.blank_icon
width = int(tr.height()/2)
r = QRect(tr)
r.setRight(r.right() - 1), r.setLeft(r.right() - width - 4)
tv.category_button_positions[category]['links'] = (r.left(), r.left()+r.width())
icon.paint(painter, r, option.decorationAlignment, QIcon.Mode.Normal, QIcon.State.On)
tr.setRight(r.left() - 1)
if show_notes and m.category_has_notes(category):
icon = self.notes_icon if m.item_has_note(category, name) else self.blank_icon
width = int(tr.height()/2)
r = QRect(tr)
r.setRight(r.right() - 1), r.setLeft(r.right() - width - 4)
tv.category_button_positions[category]['notes'] = (r.left(), r.left()+r.width())
icon.paint(painter, r, option.decorationAlignment, QIcon.Mode.Normal, QIcon.State.On)
tr.setRight(r.left() - 1)
if not is_search and (hover or gprefs['tag_browser_show_counts']): if not is_search and (hover or gprefs['tag_browser_show_counts']):
count = str(index.data(COUNT_ROLE)) count = str(index.data(COUNT_ROLE))
width = painter.fontMetrics().boundingRect(count).width() width = painter.fontMetrics().boundingRect(count).width()
@ -219,6 +247,12 @@ class TagsView(QTreeView): # {{{
self.plus_icon = QIcon.ic('plus.png') self.plus_icon = QIcon.ic('plus.png')
self.minus_icon = QIcon.ic('minus.png') self.minus_icon = QIcon.ic('minus.png')
# Dict for recording the positions of the fake buttons for category tag
# lines. It is recorded per category because we can't guarantee the
# order that items are painted. The numbers get updated whenever an item
# is painted, which deals with resizing.
self.category_button_positions = defaultdict(dict)
self._model = TagsModel(self) self._model = TagsModel(self)
self._model.search_item_renamed.connect(self.search_item_renamed) self._model.search_item_renamed.connect(self.search_item_renamed)
self._model.refresh_required.connect(self.refresh_required, self._model.refresh_required.connect(self.refresh_required,
@ -332,7 +366,7 @@ class TagsView(QTreeView): # {{{
match_pop = 0 match_pop = 0
self.alter_tb.match_menu.actions()[match_pop].setChecked(True) self.alter_tb.match_menu.actions()[match_pop].setChecked(True)
if not self.made_connections: if not self.made_connections:
self.clicked.connect(self.toggle) self.clicked.connect(self.toggle_on_mouse_click)
self.customContextMenuRequested.connect(self.show_context_menu) self.customContextMenuRequested.connect(self.show_context_menu)
self.refresh_required.connect(self.recount, type=Qt.ConnectionType.QueuedConnection) self.refresh_required.connect(self.recount, type=Qt.ConnectionType.QueuedConnection)
self.alter_tb.sort_menu.triggered.connect(self.sort_changed) self.alter_tb.sort_menu.triggered.connect(self.sort_changed)
@ -430,6 +464,8 @@ class TagsView(QTreeView): # {{{
def mousePressEvent(self, event): def mousePressEvent(self, event):
if event.buttons() & Qt.MouseButton.LeftButton: if event.buttons() & Qt.MouseButton.LeftButton:
# Record the press point for processing during the clicked signal
self.mouse_clicked_point = event.pos()
# Only remember a possible drag start if the item is drag enabled # Only remember a possible drag start if the item is drag enabled
dex = self.indexAt(event.pos()) dex = self.indexAt(event.pos())
if self._model.flags(dex) & Qt.ItemFlag.ItemIsDragEnabled: if self._model.flags(dex) & Qt.ItemFlag.ItemIsDragEnabled:
@ -486,11 +522,37 @@ class TagsView(QTreeView): # {{{
joiner = ' and ' if self.match_all else ' or ' joiner = ' and ' if self.match_all else ' or '
return joiner.join(tokens) return joiner.join(tokens)
def click_in_button_range(self, val, category, kind):
range_tuple = self.category_button_positions[category].get(kind)
return range_tuple and range_tuple[0] <= val <= range_tuple[1]
def toggle_current_index(self): def toggle_current_index(self):
ci = self.currentIndex() ci = self.currentIndex()
if ci.isValid(): if ci.isValid():
self.toggle(ci) self.toggle(ci)
def toggle_on_mouse_click(self, index):
# Check if one of the link or note icons was clicked. If so, deal with
# it here and don't do the real toggle
t = self._model.data(index, Qt.UserRole)
if t.type == TagTreeItem.TAG:
db = self._model.db.new_api
category = t.tag.category
orig_name = t.tag.original_name
x = self.mouse_clicked_point.x()
if self.click_in_button_range(x, category, 'notes'):
from calibre.gui2.dialogs.show_category_note import ShowNoteDialog
item_id = db.get_item_id(category, orig_name)
if db.notes_for(category, item_id):
ShowNoteDialog(category, item_id, db, parent=self).show()
return
if self.click_in_button_range(x, category, 'links'):
link = db.get_link_map(category).get(orig_name)
if link:
safe_open_url(link)
return
self._toggle(index, None)
def toggle(self, index): def toggle(self, index):
self._toggle(index, None) self._toggle(index, None)
@ -811,6 +873,19 @@ class TagsView(QTreeView): # {{{
self.context_menu.addAction(_('Edit link for %s')%display_name(tag), self.context_menu.addAction(_('Edit link for %s')%display_name(tag),
partial(self.context_menu_handler, partial(self.context_menu_handler,
action='edit_author_link', index=tag.id)).setIcon(QIcon.ic('insert-link.png')) action='edit_author_link', index=tag.id)).setIcon(QIcon.ic('insert-link.png'))
elif self.db.new_api.has_link_map(key):
self.context_menu.addAction(_('Edit link for %s')%display_name(tag),
partial(self.context_menu_handler, action='open_editor',
category=tag.original_name if tag else None,
key=key))
if self.db.new_api.field_supports_notes(key):
item_id = self.db.new_api.get_item_id(tag.category, tag.original_name)
has_note = self._model.item_has_note(key, tag.original_name) #bool(self.db.new_api.notes_for(tag.category, item_id))
self.context_menu.addAction(self.edit_metadata_icon,
(_('Edit note for %s') if has_note else _('Create note for %s'))%display_name(tag),
partial(self.context_menu_handler, action='edit_note',
index=index, extra=item_id, category=tag.category))
# is_editable is also overloaded to mean 'can be added # is_editable is also overloaded to mean 'can be added
# to a User category' # to a User category'
@ -848,14 +923,6 @@ class TagsView(QTreeView): # {{{
m.addAction(self.minus_icon, m.addAction(self.minus_icon,
_('Remove %s from selected books') % display_name(tag), _('Remove %s from selected books') % display_name(tag),
partial(self.context_menu_handler, action='remove_tag', index=index)) partial(self.context_menu_handler, action='remove_tag', index=index))
item_id = self.db.new_api.get_item_id(tag.category, tag.original_name)
has_note = bool(self.db.new_api.notes_for(tag.category, item_id))
self.context_menu.addAction(self.edit_metadata_icon,
(_('Edit note for %s') if has_note else _('Create note for %s'))%display_name(tag),
partial(self.context_menu_handler, action='edit_note',
index=index, extra=item_id, category=tag.category))
elif key == 'search' and tag.is_searchable: elif key == 'search' and tag.is_searchable:
self.context_menu.addAction(self.rename_icon, self.context_menu.addAction(self.rename_icon,
_('Rename %s')%display_name(tag), _('Rename %s')%display_name(tag),