The tag browser notes/links icon stuff.

Clicking the "buttons" works because of how mouse tracking works. The user must move the mouse over/into the current item line before clicking a button, which causes that line to be painted. When the line is painted the horizontal positions of the buttons on the line are recorded. If/when a click happens, the click X point is checked to see if it is in the ranges defined by the horizontal positions.
This commit is contained in:
Charles Haley 2023-12-17 13:41:20 +00:00
parent 96211be30e
commit 91a46f99a9
6 changed files with 202 additions and 66 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['search_tool_bar_shows_text'] = 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):
# 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('show_avg_rating', config)
r('show_links_in_tag_brouser', gprefs)
r('show_notes_in_tag_brouser', gprefs)
r('disable_animations', config)
r('systray_icon', config, restart_required=True)
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>
</layout>
</item>
<item row="5" column="0">
<widget class="QCheckBox" name="opt_show_avg_rating">
<property name="text">
<string>Show &amp;average ratings</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="5" column="2">
<widget class="QCheckBox" name="opt_tag_browser_show_tooltips">
<property name="text">
<string>Show &amp;tooltips</string>
</property>
</widget>
</item>
<item row="6" 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
<item row="5" column="0" colspan="3">
<layout class="QGridLayout" name="gridlayout_22">
<item row="0" column="0">
<widget class="QCheckBox" name="opt_show_avg_rating">
<property name="text">
<string>Show &amp;average ratings</string>
</property>
<property name="toolTip">
<string>Show the average rating per item indication in the Tag browser</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QCheckBox" name="opt_show_links_in_tag_brouser">
<property name="text">
<string>Show &amp;links icons</string>
</property>
<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
see the counts by hovering your mouse over any item.&lt;/p&gt;</string>
</property>
<property name="text">
<string>Show &amp;counts</string>
</property>
</widget>
</item>
<item row="6" 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="7" 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
</property>
<property name="text">
<string>Show &amp;counts</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QCheckBox" name="opt_show_notes_in_tag_brouser">
<property name="text">
<string>Show &amp;notes icons</string>
</property>
<property name="toolTip">
<string>Show an icon if the item has an attached note</string>
</property>
<property name="checked">
<bool>true</bool>
</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
categories might not have values when using Virtual libraries. Checking this
box will cause these empty categories to be hidden.&lt;/p&gt;</string>
</property>
<property name="text">
<string>Hide empt&amp;y categories (columns)</string>
</property>
</widget>
</item>
<item row="7" column="2">
<widget class="QCheckBox" name="opt_tag_browser_always_autocollapse">
<property name="toolTip">
<string>&lt;p&gt;When checked, Find in the Tag browser will show all items
</property>
<property name="text">
<string>Hide empt&amp;y categories (columns)</string>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QCheckBox" name="opt_tag_browser_always_autocollapse">
<property name="toolTip">
<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
also checked then only categories containing a matched item will be shown.&lt;/p&gt;</string>
</property>
<property name="text">
<string>Find &amp;shows all items that match</string>
</property>
</widget>
</property>
<property name="text">
<string>Find &amp;shows all items that match</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="8" column="0" colspan="3">
<layout class="QVBoxLayout" name="verticalLayout">

View File

@ -235,10 +235,16 @@ class TagTreeItem: # {{{
ar = self.average_rating
if ar:
tt.append(_('Average rating for books in this category: %.1f') % ar)
elif self.type == self.TAG and ar is not None:
tt.append(_('Books in this category are unrated'))
if self.type == self.TAG and tag.category != 'search':
tt.append(_('Number of books: %s') % self.item_count)
elif self.type == self.TAG:
if ar is not None:
tt.append(_('Books in this category are unrated'))
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 None
if role == DRAG_IMAGE_ROLE:
@ -365,6 +371,7 @@ class TagsModel(QAbstractItemModel): # {{{
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)
self.reset_notes_and_link_maps()
@property
def gui_parent(self):
@ -441,6 +448,43 @@ class TagsModel(QAbstractItemModel): # {{{
self._run_rebuild()
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={}):
if self._build_in_progress:
print('Tag browser build already in progress')
@ -455,6 +499,7 @@ class TagsModel(QAbstractItemModel): # {{{
self._build_in_progress = False
def _run_rebuild(self, state_map={}):
self.reset_notes_and_link_maps()
for node in itervalues(self.node_map):
node.break_cycles()
del node # Clear reference to node in the current frame

View File

@ -514,6 +514,7 @@ class TagBrowserMixin: # {{{
m = self.library_view.model()
ids = [m.id(r) for r in rows]
self.tags_view.model().reset_notes_and_link_maps()
m.refresh(reset=False)
m.research()
self.library_view.select_rows(ids)

View File

@ -22,7 +22,7 @@ from calibre.constants import config_dir
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,
question_dialog, rating_font, safe_open_url,
)
from calibre.gui2.dialogs.edit_category_notes import EditNoteDialog
from calibre.gui2.complete2 import EditWithComplete
@ -43,6 +43,9 @@ class TagDelegate(QStyledItemDelegate): # {{{
self.rating_pat = re.compile(r'[%s]' % rating_to_stars(3, True))
self.rating_font = QFont(rating_font())
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):
rating = item.average_rating
@ -86,6 +89,30 @@ class TagDelegate(QStyledItemDelegate): # {{{
hover = option.state & QStyle.StateFlag.State_MouseOver
is_search = (True if item.type == TagTreeItem.TAG and
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
m = self.tags_view._model
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)
self.tags_view.current_note_button_position = (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_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)
self.tags_view.current_link_button_position = (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']):
count = str(index.data(COUNT_ROLE))
width = painter.fontMetrics().boundingRect(count).width()
@ -202,6 +229,8 @@ class TagsView(QTreeView): # {{{
self.setTabKeyNavigation(True)
self.setAnimated(True)
self.setHeaderHidden(True)
self.current_note_button_position = (-1, -1)
self.current_link_button_position = (-1, -1)
self.setItemDelegate(TagDelegate(tags_view=self))
self.made_connections = False
self.setAcceptDrops(True)
@ -428,10 +457,29 @@ class TagsView(QTreeView): # {{{
except:
pass
def number_in_range(self, val, range_tuple):
return range_tuple[0] <= val <= range_tuple[1]
def mousePressEvent(self, event):
if event.buttons() & Qt.MouseButton.LeftButton:
# Only remember a possible drag start if the item is drag enabled
dex = self.indexAt(event.pos())
t = self._model.data(dex, Qt.UserRole)
if t.type == TagTreeItem.TAG:
db = self._model.db.new_api
tag = t.tag
x = event.pos().x()
if self.number_in_range(x, self.current_note_button_position):
from calibre.gui2.dialogs.show_category_note import ShowNoteDialog
item_id = db.get_item_id(tag.category, tag.original_name)
if db.notes_for(tag.category, item_id):
ShowNoteDialog(tag.category, item_id, db, parent=self).show()
return
elif self.number_in_range(x, self.current_link_button_position):
link = db.get_link_map(tag.category).get(tag.original_name)
if link:
safe_open_url(link)
return
if self._model.flags(dex) & Qt.ItemFlag.ItemIsDragEnabled:
self.possible_drag_start = event.pos()
else:
@ -811,6 +859,19 @@ class TagsView(QTreeView): # {{{
self.context_menu.addAction(_('Edit link for %s')%display_name(tag),
partial(self.context_menu_handler,
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
# to a User category'
@ -848,14 +909,6 @@ class TagsView(QTreeView): # {{{
m.addAction(self.minus_icon,
_('Remove %s from selected books') % display_name(tag),
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:
self.context_menu.addAction(self.rename_icon,
_('Rename %s')%display_name(tag),