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_sort'
|
||||||
categories_use_field_for_author_name = 'author'
|
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.
|
# Set whether boolean custom columns are two- or three-valued.
|
||||||
# Two-values for true booleans
|
# 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.ebooks.metadata import title_sort
|
||||||
from calibre.gui2 import config, NONE
|
from calibre.gui2 import config, NONE
|
||||||
from calibre.library.field_metadata import TagsIcons, category_icon_map
|
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.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.search_query_parser import saved_searches
|
||||||
|
from calibre.utils.formatter import eval_formatter
|
||||||
from calibre.gui2 import error_dialog
|
from calibre.gui2 import error_dialog
|
||||||
from calibre.gui2.dialogs.confirm_delete import confirm
|
from calibre.gui2.dialogs.confirm_delete import confirm
|
||||||
from calibre.gui2.dialogs.tag_categories import TagCategories
|
from calibre.gui2.dialogs.tag_categories import TagCategories
|
||||||
@ -400,7 +402,7 @@ class TagTreeItem(object): # {{{
|
|||||||
|
|
||||||
def category_data(self, role):
|
def category_data(self, role):
|
||||||
if role == Qt.DisplayRole:
|
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:
|
if role == Qt.DecorationRole:
|
||||||
return self.icon
|
return self.icon
|
||||||
if role == Qt.FontRole:
|
if role == Qt.FontRole:
|
||||||
@ -441,6 +443,15 @@ class TagTreeItem(object): # {{{
|
|||||||
if self.type == self.TAG:
|
if self.type == self.TAG:
|
||||||
self.tag.state = (self.tag.state + 1)%3
|
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): # {{{
|
class TagsModel(QAbstractItemModel): # {{{
|
||||||
@ -477,19 +488,11 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
tt = _('The lookup/search name is "{0}"').format(r)
|
tt = _('The lookup/search name is "{0}"').format(r)
|
||||||
else:
|
else:
|
||||||
tt = ''
|
tt = ''
|
||||||
c = TagTreeItem(parent=self.root_item,
|
TagTreeItem(parent=self.root_item,
|
||||||
data=self.categories[i],
|
data=self.categories[i],
|
||||||
category_icon=self.category_icon_map[r],
|
category_icon=self.category_icon_map[r],
|
||||||
tooltip=tt, category_key=r)
|
tooltip=tt, category_key=r)
|
||||||
# This duplicates code in refresh(). Having it here as well
|
self.refresh(data=data)
|
||||||
# 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)
|
|
||||||
|
|
||||||
def mimeTypes(self):
|
def mimeTypes(self):
|
||||||
return ["application/calibre+from_library"]
|
return ["application/calibre+from_library"]
|
||||||
@ -652,33 +655,83 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
return None
|
return None
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def refresh(self):
|
def refresh(self, data=None):
|
||||||
data = self.get_node_tree(config['sort_tags_by']) # get category data
|
sort_by = config['sort_tags_by']
|
||||||
|
if data is None:
|
||||||
|
data = self.get_node_tree(sort_by) # get category data
|
||||||
if data is None:
|
if data is None:
|
||||||
return False
|
return False
|
||||||
row_index = -1
|
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):
|
for i, r in enumerate(self.row_map):
|
||||||
if self.hidden_categories and self.categories[i] in self.hidden_categories:
|
if self.hidden_categories and self.categories[i] in self.hidden_categories:
|
||||||
continue
|
continue
|
||||||
row_index += 1
|
row_index += 1
|
||||||
category = self.root_item.children[row_index]
|
category = self.root_item.children[row_index]
|
||||||
names = [t.tag.name for t in category.children]
|
names = []
|
||||||
states = [t.tag.state for t in category.children]
|
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))
|
state_map = dict(izip(names, states))
|
||||||
category_index = self.index(row_index, 0, QModelIndex())
|
category_index = self.index(row_index, 0, QModelIndex())
|
||||||
|
category_node = category_index.internalPointer()
|
||||||
if len(category.children) > 0:
|
if len(category.children) > 0:
|
||||||
self.beginRemoveRows(category_index, 0,
|
self.beginRemoveRows(category_index, 0,
|
||||||
len(category.children)-1)
|
len(category.children)-1)
|
||||||
category.children = []
|
category.children = []
|
||||||
self.endRemoveRows()
|
self.endRemoveRows()
|
||||||
if len(data[r]) > 0:
|
cat_len = len(data[r])
|
||||||
|
if cat_len <= 0:
|
||||||
|
continue
|
||||||
|
|
||||||
self.beginInsertRows(category_index, 0, len(data[r])-1)
|
self.beginInsertRows(category_index, 0, len(data[r])-1)
|
||||||
for tag in data[r]:
|
clear_rating = True if r not in self.categories_with_ratings and \
|
||||||
if r not in self.categories_with_ratings and \
|
|
||||||
not self.db.field_metadata[r]['is_custom'] and \
|
not self.db.field_metadata[r]['is_custom'] and \
|
||||||
not self.db.field_metadata[r]['kind'] == 'user':
|
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.avg_rating = None
|
||||||
tag.state = state_map.get(tag.name, 0)
|
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)
|
t = TagTreeItem(parent=category, data=tag, icon_map=self.icon_state_map)
|
||||||
self.endInsertRows()
|
self.endInsertRows()
|
||||||
return True
|
return True
|
||||||
@ -824,19 +877,28 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
|
|
||||||
def reset_all_states(self, except_=None):
|
def reset_all_states(self, except_=None):
|
||||||
update_list = []
|
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())):
|
for i in xrange(self.rowCount(QModelIndex())):
|
||||||
category_index = self.index(i, 0, QModelIndex())
|
category_index = self.index(i, 0, QModelIndex())
|
||||||
for j in xrange(self.rowCount(category_index)):
|
for j in xrange(self.rowCount(category_index)):
|
||||||
tag_index = self.index(j, 0, category_index)
|
tag_index = self.index(j, 0, category_index)
|
||||||
tag_item = tag_index.internalPointer()
|
tag_item = tag_index.internalPointer()
|
||||||
tag = tag_item.tag
|
if tag_item.type == TagTreeItem.CATEGORY:
|
||||||
if tag is except_:
|
for k in xrange(self.rowCount(tag_index)):
|
||||||
self.dataChanged.emit(tag_index, tag_index)
|
ti = self.index(k, 0, tag_index)
|
||||||
continue
|
ti = ti.internalPointer()
|
||||||
if tag.state != 0 or tag in update_list:
|
process_tag(ti)
|
||||||
tag.state = 0
|
else:
|
||||||
update_list.append(tag)
|
process_tag(tag_item)
|
||||||
self.dataChanged.emit(tag_index, tag_index)
|
|
||||||
|
|
||||||
def clear_state(self):
|
def clear_state(self):
|
||||||
self.reset_all_states()
|
self.reset_all_states()
|
||||||
@ -856,6 +918,7 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
ans = []
|
ans = []
|
||||||
tags_seen = set()
|
tags_seen = set()
|
||||||
row_index = -1
|
row_index = -1
|
||||||
|
|
||||||
for i, key in enumerate(self.row_map):
|
for i, key in enumerate(self.row_map):
|
||||||
if self.hidden_categories and self.categories[i] in self.hidden_categories:
|
if self.hidden_categories and self.categories[i] in self.hidden_categories:
|
||||||
continue
|
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
|
if key.endswith(':'): # User category, so skip it. The tag will be marked in its real category
|
||||||
continue
|
continue
|
||||||
category_item = self.root_item.children[row_index]
|
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
|
tag = tag_item.tag
|
||||||
if tag.state > 0:
|
if tag.state > 0:
|
||||||
prefix = ' not ' if tag.state == 2 else ''
|
prefix = ' not ' if tag.state == 2 else ''
|
||||||
|
Loading…
x
Reference in New Issue
Block a user