mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Subcategories in tag browser
This commit is contained in:
parent
6f718a4dc9
commit
62b4676cb8
@ -55,6 +55,32 @@ author_sort_copy_method = 'invert'
|
||||
# categories_use_field_for_author_name = 'author_sort'
|
||||
categories_use_field_for_author_name = 'author'
|
||||
|
||||
# Control how the tags pane displays categories containing many items. If the
|
||||
# number of items is larger than categories_collapse_more_than, a sub-category
|
||||
# will be added. If sorting by name, then the subcategories can be organized by
|
||||
# first letter (categories_collapse_model = 'first letter') or into equal-sized
|
||||
# groups (categories_collapse_model = 'partition'). If sorting by average rating
|
||||
# or by popularity, then 'partition' is always used. The addition of
|
||||
# subcategories can be disabled by setting categories_collapse_more_than = 0.
|
||||
# When using partition, the format of the subcategory label is controlled by a
|
||||
# template: categories_collapsed_name_template if sorting by name,
|
||||
# categories_collapsed_rating_template if sorting by average rating, and
|
||||
# categories_collapsed_popularity_template if sorting by popularity. There are
|
||||
# two variables available to the template: first and last. The variable 'first'
|
||||
# is the initial item in the subcategory, and the variable 'last' is the final
|
||||
# item in the subcategory. Both variables are 'objects'; they each have multiple
|
||||
# values that are obtained by using a suffix. For example, first.name for an
|
||||
# author category will be the name of the author. The sub-values available are:
|
||||
# name: the printable name of the item
|
||||
# count: the number of books that references this item
|
||||
# avg_rating: the averate rating of all the books referencing this item
|
||||
# sort: the sort value. For authors, this is the author_sort for that author
|
||||
# category: the category (e.g., authors, series) that the item is in.
|
||||
categories_collapse_more_than = 50
|
||||
categories_collapsed_name_template = '{first.name:shorten(4,'',0)}{last.name::shorten(4,'',0)| - |}'
|
||||
categories_collapsed_rating_template = '{first.avg_rating:4.2f}{last.avg_rating:4.2f| - |}'
|
||||
categories_collapsed_popularity_template = '{first.count:d}{last.count:d| - |}'
|
||||
categories_collapse_model = 'first letter'
|
||||
|
||||
# Set whether boolean custom columns are two- or three-valued.
|
||||
# Two-values for true booleans
|
||||
|
@ -18,9 +18,11 @@ from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, \
|
||||
from calibre.ebooks.metadata import title_sort
|
||||
from calibre.gui2 import config, NONE
|
||||
from calibre.library.field_metadata import TagsIcons, category_icon_map
|
||||
from calibre.library.database2 import Tag
|
||||
from calibre.utils.config import tweaks
|
||||
from calibre.utils.icu import sort_key
|
||||
from calibre.utils.icu import sort_key, upper
|
||||
from calibre.utils.search_query_parser import saved_searches
|
||||
from calibre.utils.formatter import eval_formatter
|
||||
from calibre.gui2 import error_dialog
|
||||
from calibre.gui2.dialogs.confirm_delete import confirm
|
||||
from calibre.gui2.dialogs.tag_categories import TagCategories
|
||||
@ -400,7 +402,7 @@ class TagTreeItem(object): # {{{
|
||||
|
||||
def category_data(self, role):
|
||||
if role == Qt.DisplayRole:
|
||||
return QVariant(self.py_name + ' [%d]'%len(self.children))
|
||||
return QVariant(self.py_name + ' [%d]'%len(self.child_tags()))
|
||||
if role == Qt.DecorationRole:
|
||||
return self.icon
|
||||
if role == Qt.FontRole:
|
||||
@ -441,6 +443,15 @@ class TagTreeItem(object): # {{{
|
||||
if self.type == self.TAG:
|
||||
self.tag.state = (self.tag.state + 1)%3
|
||||
|
||||
def child_tags(self):
|
||||
res = []
|
||||
for t in self.children:
|
||||
if t.type == TagTreeItem.CATEGORY:
|
||||
for c in t.children:
|
||||
res.append(c)
|
||||
else:
|
||||
res.append(t)
|
||||
return res
|
||||
# }}}
|
||||
|
||||
class TagsModel(QAbstractItemModel): # {{{
|
||||
@ -477,19 +488,11 @@ class TagsModel(QAbstractItemModel): # {{{
|
||||
tt = _('The lookup/search name is "{0}"').format(r)
|
||||
else:
|
||||
tt = ''
|
||||
c = TagTreeItem(parent=self.root_item,
|
||||
TagTreeItem(parent=self.root_item,
|
||||
data=self.categories[i],
|
||||
category_icon=self.category_icon_map[r],
|
||||
tooltip=tt, category_key=r)
|
||||
# This duplicates code in refresh(). Having it here as well
|
||||
# can save seconds during startup, because we avoid a second
|
||||
# call to get_node_tree.
|
||||
for tag in data[r]:
|
||||
if r not in self.categories_with_ratings and \
|
||||
not self.db.field_metadata[r]['is_custom'] and \
|
||||
not self.db.field_metadata[r]['kind'] == 'user':
|
||||
tag.avg_rating = None
|
||||
TagTreeItem(parent=c, data=tag, icon_map=self.icon_state_map)
|
||||
self.refresh(data=data)
|
||||
|
||||
def mimeTypes(self):
|
||||
return ["application/calibre+from_library"]
|
||||
@ -652,35 +655,85 @@ class TagsModel(QAbstractItemModel): # {{{
|
||||
return None
|
||||
return data
|
||||
|
||||
def refresh(self):
|
||||
data = self.get_node_tree(config['sort_tags_by']) # get category data
|
||||
def refresh(self, data=None):
|
||||
sort_by = config['sort_tags_by']
|
||||
if data is None:
|
||||
data = self.get_node_tree(sort_by) # get category data
|
||||
if data is None:
|
||||
return False
|
||||
row_index = -1
|
||||
empty_tag = Tag('')
|
||||
collapse = tweaks['categories_collapse_more_than']
|
||||
collapse_model = tweaks['categories_collapse_model']
|
||||
if sort_by == 'name':
|
||||
collapse_template = tweaks['categories_collapsed_name_template']
|
||||
elif sort_by == 'rating':
|
||||
collapse_model = 'partition'
|
||||
collapse_template = tweaks['categories_collapsed_rating_template']
|
||||
else:
|
||||
collapse_model = 'partition'
|
||||
collapse_template = tweaks['categories_collapsed_popularity_template']
|
||||
collapse_letter = None
|
||||
|
||||
for i, r in enumerate(self.row_map):
|
||||
if self.hidden_categories and self.categories[i] in self.hidden_categories:
|
||||
continue
|
||||
row_index += 1
|
||||
category = self.root_item.children[row_index]
|
||||
names = [t.tag.name for t in category.children]
|
||||
states = [t.tag.state for t in category.children]
|
||||
names = []
|
||||
states = []
|
||||
children = category.child_tags()
|
||||
states = [t.tag.state for t in children]
|
||||
names = [t.tag.name for names in children]
|
||||
state_map = dict(izip(names, states))
|
||||
category_index = self.index(row_index, 0, QModelIndex())
|
||||
category_node = category_index.internalPointer()
|
||||
if len(category.children) > 0:
|
||||
self.beginRemoveRows(category_index, 0,
|
||||
len(category.children)-1)
|
||||
category.children = []
|
||||
self.endRemoveRows()
|
||||
if len(data[r]) > 0:
|
||||
self.beginInsertRows(category_index, 0, len(data[r])-1)
|
||||
for tag in data[r]:
|
||||
if r not in self.categories_with_ratings and \
|
||||
cat_len = len(data[r])
|
||||
if cat_len <= 0:
|
||||
continue
|
||||
|
||||
self.beginInsertRows(category_index, 0, len(data[r])-1)
|
||||
clear_rating = True if r not in self.categories_with_ratings and \
|
||||
not self.db.field_metadata[r]['is_custom'] and \
|
||||
not self.db.field_metadata[r]['kind'] == 'user':
|
||||
tag.avg_rating = None
|
||||
tag.state = state_map.get(tag.name, 0)
|
||||
not self.db.field_metadata[r]['kind'] == 'user' \
|
||||
else False
|
||||
for idx,tag in enumerate(data[r]):
|
||||
if clear_rating:
|
||||
tag.avg_rating = None
|
||||
tag.state = state_map.get(tag.name, 0)
|
||||
|
||||
if collapse > 0 and cat_len > collapse:
|
||||
if collapse_model == 'partition':
|
||||
if (idx % collapse) == 0:
|
||||
d = {'first': tag}
|
||||
if cat_len > idx + collapse:
|
||||
d['last'] = data[r][idx+collapse-1]
|
||||
else:
|
||||
d['last'] = empty_tag
|
||||
name = eval_formatter.safe_format(collapse_template,
|
||||
d, 'TAG_VIEW', None)
|
||||
sub_cat = TagTreeItem(parent=category,
|
||||
data = name, tooltip = None,
|
||||
category_icon = category_node.icon,
|
||||
category_key=category_node.category_key)
|
||||
else:
|
||||
if upper(tag.name[0]) != collapse_letter:
|
||||
collapse_letter = upper(tag.name[0])
|
||||
sub_cat = TagTreeItem(parent=category,
|
||||
data = collapse_letter,
|
||||
category_icon = category_node.icon,
|
||||
tooltip = None,
|
||||
category_key=category_node.category_key)
|
||||
t = TagTreeItem(parent=sub_cat, data=tag,
|
||||
icon_map=self.icon_state_map)
|
||||
else:
|
||||
t = TagTreeItem(parent=category, data=tag, icon_map=self.icon_state_map)
|
||||
self.endInsertRows()
|
||||
self.endInsertRows()
|
||||
return True
|
||||
|
||||
def columnCount(self, parent):
|
||||
@ -824,19 +877,28 @@ class TagsModel(QAbstractItemModel): # {{{
|
||||
|
||||
def reset_all_states(self, except_=None):
|
||||
update_list = []
|
||||
def process_tag(tag_item):
|
||||
tag = tag_item.tag
|
||||
if tag is except_:
|
||||
self.dataChanged.emit(tag_index, tag_index)
|
||||
return
|
||||
if tag.state != 0 or tag in update_list:
|
||||
tag.state = 0
|
||||
update_list.append(tag)
|
||||
self.dataChanged.emit(tag_index, tag_index)
|
||||
|
||||
for i in xrange(self.rowCount(QModelIndex())):
|
||||
category_index = self.index(i, 0, QModelIndex())
|
||||
for j in xrange(self.rowCount(category_index)):
|
||||
tag_index = self.index(j, 0, category_index)
|
||||
tag_item = tag_index.internalPointer()
|
||||
tag = tag_item.tag
|
||||
if tag is except_:
|
||||
self.dataChanged.emit(tag_index, tag_index)
|
||||
continue
|
||||
if tag.state != 0 or tag in update_list:
|
||||
tag.state = 0
|
||||
update_list.append(tag)
|
||||
self.dataChanged.emit(tag_index, tag_index)
|
||||
if tag_item.type == TagTreeItem.CATEGORY:
|
||||
for k in xrange(self.rowCount(tag_index)):
|
||||
ti = self.index(k, 0, tag_index)
|
||||
ti = ti.internalPointer()
|
||||
process_tag(ti)
|
||||
else:
|
||||
process_tag(tag_item)
|
||||
|
||||
def clear_state(self):
|
||||
self.reset_all_states()
|
||||
@ -856,6 +918,7 @@ class TagsModel(QAbstractItemModel): # {{{
|
||||
ans = []
|
||||
tags_seen = set()
|
||||
row_index = -1
|
||||
|
||||
for i, key in enumerate(self.row_map):
|
||||
if self.hidden_categories and self.categories[i] in self.hidden_categories:
|
||||
continue
|
||||
@ -863,7 +926,7 @@ class TagsModel(QAbstractItemModel): # {{{
|
||||
if key.endswith(':'): # User category, so skip it. The tag will be marked in its real category
|
||||
continue
|
||||
category_item = self.root_item.children[row_index]
|
||||
for tag_item in category_item.children:
|
||||
for tag_item in category_item.child_tags():
|
||||
tag = tag_item.tag
|
||||
if tag.state > 0:
|
||||
prefix = ' not ' if tag.state == 2 else ''
|
||||
|
Loading…
x
Reference in New Issue
Block a user