diff --git a/src/calibre/ebooks/metadata/book/__init__.py b/src/calibre/ebooks/metadata/book/__init__.py
index 82de7400d7..033a78d611 100644
--- a/src/calibre/ebooks/metadata/book/__init__.py
+++ b/src/calibre/ebooks/metadata/book/__init__.py
@@ -83,6 +83,10 @@ CALIBRE_METADATA_FIELDS = frozenset([
'application_id', # An application id, currently set to the db_id.
'db_id', # the calibre primary key of the item.
'formats', # list of formats (extensions) for this book
+ # a dict of user category names, where the value is a list of item names
+ # from the book that are in that category
+ 'user_categories',
+
]
)
diff --git a/src/calibre/ebooks/metadata/book/base.py b/src/calibre/ebooks/metadata/book/base.py
index 4cca94a6c6..b47cc373a7 100644
--- a/src/calibre/ebooks/metadata/book/base.py
+++ b/src/calibre/ebooks/metadata/book/base.py
@@ -30,6 +30,7 @@ NULL_VALUES = {
'author_sort_map': {},
'authors' : [_('Unknown')],
'title' : _('Unknown'),
+ 'user_categories' : {},
'language' : 'und'
}
diff --git a/src/calibre/ebooks/metadata/opf2.py b/src/calibre/ebooks/metadata/opf2.py
index dfb902b5b9..d34a563110 100644
--- a/src/calibre/ebooks/metadata/opf2.py
+++ b/src/calibre/ebooks/metadata/opf2.py
@@ -470,6 +470,13 @@ def serialize_user_metadata(metadata_elem, all_user_metadata, tail='\n'+(' '*8))
metadata_elem.append(meta)
+def dump_user_categories(cats):
+ if not cats:
+ cats = {}
+ from calibre.ebooks.metadata.book.json_codec import object_to_unicode
+ return json.dumps(object_to_unicode(cats), ensure_ascii=False,
+ skipkeys=True)
+
class OPF(object): # {{{
MIMETYPE = 'application/oebps-package+xml'
@@ -524,6 +531,9 @@ class OPF(object): # {{{
publication_type = MetadataField('publication_type', is_dc=False)
timestamp = MetadataField('timestamp', is_dc=False,
formatter=parse_date, renderer=isoformat)
+ user_categories = MetadataField('user_categories', is_dc=False,
+ formatter=json.loads,
+ renderer=dump_user_categories)
def __init__(self, stream, basedir=os.getcwdu(), unquote_urls=True,
@@ -994,7 +1004,7 @@ class OPF(object): # {{{
for attr in ('title', 'authors', 'author_sort', 'title_sort',
'publisher', 'series', 'series_index', 'rating',
'isbn', 'tags', 'category', 'comments',
- 'pubdate'):
+ 'pubdate', 'user_categories'):
val = getattr(mi, attr, None)
if val is not None and val != [] and val != (None, None):
setattr(self, attr, val)
@@ -1175,6 +1185,10 @@ class OPFCreator(Metadata):
a(CAL_ELEM('calibre:timestamp', self.timestamp.isoformat()))
if self.publication_type is not None:
a(CAL_ELEM('calibre:publication_type', self.publication_type))
+ if self.user_categories:
+ from calibre.ebooks.metadata.book.json_codec import object_to_unicode
+ a(CAL_ELEM('calibre:user_categories',
+ json.dumps(object_to_unicode(self.user_categories))))
manifest = E.manifest()
if self.manifest is not None:
for ref in self.manifest:
@@ -1299,6 +1313,8 @@ def metadata_to_opf(mi, as_string=True):
meta('publication_type', mi.publication_type)
if mi.title_sort:
meta('title_sort', mi.title_sort)
+ if mi.user_categories:
+ meta('user_categories', dump_user_categories(mi.user_categories))
serialize_user_metadata(metadata, mi.get_all_user_metadata(False))
diff --git a/src/calibre/gui2/dialogs/tag_categories.py b/src/calibre/gui2/dialogs/tag_categories.py
index 307baffb5b..b8a7e67c72 100644
--- a/src/calibre/gui2/dialogs/tag_categories.py
+++ b/src/calibre/gui2/dialogs/tag_categories.py
@@ -73,16 +73,17 @@ class TagCategories(QDialog, Ui_TagCategories):
if idx == 0:
continue
for n in category_values[idx]():
- t = Item(name=n, label=label, index=len(self.all_items),icon=category_icons[idx], exists=True)
+ t = Item(name=n, label=label, index=len(self.all_items),
+ icon=category_icons[idx], exists=True)
self.all_items.append(t)
- self.all_items_dict[label+':'+n] = t
+ self.all_items_dict[icu_lower(label+':'+n)] = t
self.categories = dict.copy(db.prefs.get('user_categories', {}))
if self.categories is None:
self.categories = {}
for cat in self.categories:
for item,l in enumerate(self.categories[cat]):
- key = ':'.join([l[1], l[0]])
+ key = icu_lower(':'.join([l[1], l[0]]))
t = self.all_items_dict.get(key, None)
if l[1] in self.category_labels:
if t is None:
diff --git a/src/calibre/gui2/preferences/look_feel.py b/src/calibre/gui2/preferences/look_feel.py
index 196ef16b08..f7d76f2b70 100644
--- a/src/calibre/gui2/preferences/look_feel.py
+++ b/src/calibre/gui2/preferences/look_feel.py
@@ -7,17 +7,19 @@ __docformat__ = 'restructuredtext en'
from PyQt4.Qt import QApplication, QFont, QFontInfo, QFontDialog
-from calibre.gui2.preferences import ConfigWidgetBase, test_widget
+from calibre.gui2.preferences import ConfigWidgetBase, test_widget, CommaSeparatedList
from calibre.gui2.preferences.look_feel_ui import Ui_Form
from calibre.gui2 import config, gprefs, qt_app
from calibre.utils.localization import available_translations, \
get_language, get_lang
from calibre.utils.config import prefs
+from calibre.utils.icu import sort_key
class ConfigWidget(ConfigWidgetBase, Ui_Form):
def genesis(self, gui):
self.gui = gui
+ db = gui.library_view.model().db
r = self.register
@@ -61,6 +63,15 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
r('tags_browser_partition_method', gprefs, choices=choices)
r('tags_browser_collapse_at', gprefs)
+ choices = set([k for k in db.field_metadata.all_field_keys()
+ if db.field_metadata[k]['is_category'] and
+ db.field_metadata[k]['datatype'] in ['text', 'series', 'enumeration']])
+ choices -= set(['authors', 'publisher', 'formats', 'news'])
+ self.opt_categories_using_hierarchy.update_items_cache(choices)
+ r('categories_using_hierarchy', db.prefs, setting=CommaSeparatedList,
+ choices=sorted(list(choices), key=sort_key))
+
+
self.current_font = None
self.change_font_button.clicked.connect(self.change_font)
diff --git a/src/calibre/gui2/preferences/look_feel.ui b/src/calibre/gui2/preferences/look_feel.ui
index 3f2bb3e145..bc965b89fa 100644
--- a/src/calibre/gui2/preferences/look_feel.ui
+++ b/src/calibre/gui2/preferences/look_feel.ui
@@ -7,7 +7,7 @@
0
0
670
- 392
+ 422
@@ -136,7 +136,7 @@
-
- Tags browser category partitioning method:
+ Tags browser category &partitioning method:
opt_tags_browser_partition_method
@@ -157,7 +157,7 @@ if you never want subcategories
-
- Collapse when more items than:
+ &Collapse when more items than:
opt_tags_browser_collapse_at
@@ -190,6 +190,28 @@ up into sub-categories. If the partition method is set to disable, this value is
+ -
+
+
+ Categories with &hierarchical items:
+
+
+ opt_categories_using_hierarchy
+
+
+
+ -
+
+
+ A comma-separated list of columns in which items containing
+periods are displayed in the tag browser trees. For example, if
+this box contains 'tags' then tags of the form 'Mystery.English'
+and 'Mystery.Thriller' will be displayed with English and Thriller
+both under 'Mystery'. If 'tags' is not in this box,
+then the tags will be displayed each on their own line.
+
+
+
-
@@ -275,6 +297,13 @@ up into sub-categories. If the partition method is set to disable, this value is
+
+
+ MultiCompleteLineEdit
+ QLineEdit
+
+
+
diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py
index 3af3271921..1033957656 100644
--- a/src/calibre/gui2/tag_view.py
+++ b/src/calibre/gui2/tag_view.py
@@ -7,7 +7,7 @@ __docformat__ = 'restructuredtext en'
Browsing book collection by tags.
'''
-import traceback
+import traceback, copy
from itertools import izip
from functools import partial
@@ -258,13 +258,17 @@ class TagsView(QTreeView): # {{{
if item.type == TagTreeItem.TAG:
tag_item = item
- tag_name = item.tag.name
- tag_id = item.tag.id
- item = item.parent
+ t = item.tag
+ tag_name = t.name
+ tag_id = t.id
+ can_edit = getattr(t, 'can_edit', True)
+ while item.type != TagTreeItem.CATEGORY:
+ item = item.parent
if item.type == TagTreeItem.CATEGORY:
- while item.parent != self._model.root_item:
- item = item.parent
+ if not item.category_key.startswith('@'):
+ while item.parent != self._model.root_item:
+ item = item.parent
category = unicode(item.name.toString())
key = item.category_key
# Verify that we are working with a field that we know something about
@@ -275,13 +279,14 @@ class TagsView(QTreeView): # {{{
if tag_name:
# If the user right-clicked on an editable item, then offer
# the possibility of renaming that item.
- if key in ['authors', 'tags', 'series', 'publisher', 'search'] or \
+ if can_edit and \
+ key in ['authors', 'tags', 'series', 'publisher', 'search'] or \
(self.db.field_metadata[key]['is_custom'] and \
self.db.field_metadata[key]['datatype'] != 'rating'):
# Add the 'rename' items
self.context_menu.addAction(_('Rename %s')%tag_name,
- partial(self.context_menu_handler, action='edit_item',
- category=tag_item, index=index))
+ partial(self.context_menu_handler, action='edit_item',
+ category=tag_item, index=index))
if key == 'authors':
self.context_menu.addAction(_('Edit sort for %s')%tag_name,
partial(self.context_menu_handler,
@@ -515,7 +520,13 @@ class TagTreeItem(object): # {{{
name = tag.sort
tt_author = True
else:
- name = tag.name
+ p = self
+ while p.parent.type != self.ROOT:
+ p = p.parent
+ if p.category_key.startswith('@'):
+ name = getattr(tag, 'original_name', tag.name)
+ else:
+ name = tag.name
tt_author = False
if role == Qt.DisplayRole:
if tag.count == 0:
@@ -523,7 +534,7 @@ class TagTreeItem(object): # {{{
else:
return QVariant('[%d] %s'%(tag.count, name))
if role == Qt.EditRole:
- return QVariant(tag.name)
+ return QVariant(getattr(tag, 'original_name', tag.name))
if role == Qt.DecorationRole:
return self.icon_state_map[tag.state]
if role == Qt.ToolTipRole:
@@ -550,12 +561,12 @@ class TagTreeItem(object): # {{{
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)
+ def recurse(nodes, res):
+ for t in nodes:
+ if t.type != TagTreeItem.CATEGORY:
+ res.append(t)
+ recurse(t.children, res)
+ recurse(self.children, res)
return res
# }}}
@@ -590,6 +601,10 @@ class TagsModel(QAbstractItemModel): # {{{
data = self.get_node_tree(config['sort_tags_by'])
gst = db.prefs.get('grouped_search_terms', {})
self.root_item = TagTreeItem()
+ self.category_nodes = []
+
+ last_category_node = None
+ category_node_map = {}
for i, r in enumerate(self.row_map):
if self.hidden_categories and self.categories[i] in self.hidden_categories:
continue
@@ -599,10 +614,32 @@ class TagsModel(QAbstractItemModel): # {{{
tt = ''
else:
tt = _(u'The lookup/search name is "{0}"').format(r)
- TagTreeItem(parent=self.root_item,
- data=self.categories[i],
- category_icon=self.category_icon_map[r],
- tooltip=tt, category_key=r)
+
+ if r.startswith('@') and r.find('.') >= 0:
+ path_parts = [p.strip() for p in r.split('.') if p.strip()]
+ path = ''
+ for i,p in enumerate(path_parts):
+ path += p
+ if path not in category_node_map:
+ node = TagTreeItem(parent=last_category_node,
+ data=p[1:] if i == 0 else p,
+ category_icon=self.category_icon_map[r],
+ tooltip=tt if path == r else path,
+ category_key=path)
+ last_category_node = node
+ category_node_map[path] = node
+ self.category_nodes.append(node)
+ else:
+ last_category_node = category_node_map[path]
+ path += '.'
+ else:
+ node = TagTreeItem(parent=self.root_item,
+ data=self.categories[i],
+ category_icon=self.category_icon_map[r],
+ tooltip=tt, category_key=r)
+ category_node_map[r] = node
+ last_category_node = node
+ self.category_nodes.append(node)
self.refresh(data=data)
def break_cycles(self):
@@ -754,10 +791,15 @@ class TagsModel(QAbstractItemModel): # {{{
for user_cat in sorted(self.db.prefs.get('user_categories', {}).keys(),
key=sort_key):
cat_name = '@' + user_cat # add the '@' to avoid name collision
- try:
- tb_cats.add_user_category(label=cat_name, name=user_cat)
- except ValueError:
- traceback.print_exc()
+ while True:
+ try:
+ tb_cats.add_user_category(label=cat_name, name=user_cat)
+ dot = cat_name.rfind('.')
+ if dot < 0:
+ break
+ cat_name = cat_name[:dot]
+ except ValueError:
+ break
for cat in sorted(self.db.prefs.get('grouped_search_terms', {}).keys(),
key=sort_key):
@@ -794,7 +836,7 @@ class TagsModel(QAbstractItemModel): # {{{
data = self.get_node_tree(sort_by) # get category data
if data is None:
return False
- row_index = -1
+
collapse = gprefs['tags_browser_collapse_at']
collapse_model = self.collapse_model
if collapse == 0:
@@ -810,53 +852,42 @@ class TagsModel(QAbstractItemModel): # {{{
collapse_template = tweaks['categories_collapsed_popularity_template']
collapse_letter = collapse_letter_sk = 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 = []
- 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())
+ def process_one_node(category, state_map, collapse_letter, collapse_letter_sk):
+ category_index = self.createIndex(category.row(), 0, category)
category_node = category_index.internalPointer()
- if len(category.children) > 0:
- self.beginRemoveRows(category_index, 0,
- len(category.children)-1)
- category.children = []
- self.endRemoveRows()
- cat_len = len(data[r])
+ key = category_node.category_key
+ if key not in data:
+ return ((collapse_letter, collapse_letter_sk))
+ cat_len = len(data[key])
if cat_len <= 0:
- continue
+ return ((collapse_letter, collapse_letter_sk))
- 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' \
+ clear_rating = True if key not in self.categories_with_ratings and \
+ not self.db.field_metadata[key]['is_custom'] and \
+ not self.db.field_metadata[key]['kind'] == 'user' \
else False
- tt = r if self.db.field_metadata[r]['kind'] == 'user' else None
- for idx,tag in enumerate(data[r]):
+ tt = key if self.db.field_metadata[key]['kind'] == 'user' else None
+ for idx,tag in enumerate(data[key]):
if clear_rating:
tag.avg_rating = None
- tag.state = state_map.get(tag.name, 0)
+ tag.state = state_map.get((tag.name, tag.category), 0)
if collapse_model != 'disable' 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]
+ d['last'] = data[key][idx+collapse-1]
else:
- d['last'] = data[r][cat_len-1]
+ d['last'] = data[key][cat_len-1]
name = eval_formatter.safe_format(collapse_template,
d, 'TAG_VIEW', None)
+ self.beginInsertRows(category_index, 999999, 1) #len(data[key])-1)
sub_cat = TagTreeItem(parent=category,
data = name, tooltip = None,
category_icon = category_node.icon,
category_key=category_node.category_key)
+ self.endInsertRows()
else:
ts = tag.sort
if not ts:
@@ -877,12 +908,60 @@ class TagsModel(QAbstractItemModel): # {{{
category_icon = category_node.icon,
tooltip = None,
category_key=category_node.category_key)
- t = TagTreeItem(parent=sub_cat, data=tag, tooltip=tt,
- icon_map=self.icon_state_map)
+ node_parent = sub_cat
else:
- t = TagTreeItem(parent=category, data=tag, tooltip=tt,
+ node_parent = category
+
+ components = [t for t in tag.name.split('.')]
+ if key not in self.db.prefs.get('categories_using_hierarchy', []) \
+ or len(components) == 1 or \
+ self.db.field_metadata[key]['kind'] == 'user':
+ self.beginInsertRows(category_index, 999999, 1)
+ TagTreeItem(parent=node_parent, data=tag, tooltip=tt,
icon_map=self.icon_state_map)
- self.endInsertRows()
+ self.endInsertRows()
+ else:
+ for i,comp in enumerate(components):
+ child_map = dict([(t.tag.name, t) for t in node_parent.children
+ if t.type != TagTreeItem.CATEGORY])
+ if comp in child_map:
+ node_parent = child_map[comp]
+ node_parent.tag.count += tag.count
+ else:
+ if i < len(components)-1:
+ t = copy.copy(tag)
+ t.original_name = '.'.join(components[:i+1])
+ t.can_edit = False
+ else:
+ t = tag
+ t.original_name = t.name
+ t.can_edit = True
+ t.use_prefix = True
+ t.name = comp
+ self.beginInsertRows(category_index, 999999, 1)
+ node_parent = TagTreeItem(parent=node_parent, data=t,
+ tooltip=tt, icon_map=self.icon_state_map)
+ self.endInsertRows()
+
+ return ((collapse_letter, collapse_letter_sk))
+
+ for category in self.category_nodes:
+ if len(category.children) > 0:
+ child_map = category.children
+ states = [c.tag.state for c in category.child_tags()]
+ names = [(c.tag.name, c.tag.category) for c in category.child_tags()]
+ state_map = dict(izip(names, states))
+ ctags = [c for c in child_map if c.type == TagTreeItem.CATEGORY]
+ start = len(ctags)
+ self.beginRemoveRows(self.createIndex(category.row(), 0, category),
+ start, len(child_map)-1)
+ category.children = ctags
+ self.endRemoveRows()
+ else:
+ state_map = {}
+
+ collapse_letter, collapse_letter_sk = process_one_node(category,
+ state_map, collapse_letter, collapse_letter_sk)
return True
def columnCount(self, parent):
@@ -907,7 +986,10 @@ class TagsModel(QAbstractItemModel): # {{{
_('An item cannot be set to nothing. Delete it instead.')).exec_()
return False
item = index.internalPointer()
- key = item.parent.category_key
+ itm = item.parent
+ while itm.type != TagTreeItem.CATEGORY:
+ itm = itm.parent
+ key = itm.category_key
# make certain we know about the item's category
if key not in self.db.field_metadata:
return False
@@ -1022,27 +1104,31 @@ class TagsModel(QAbstractItemModel): # {{{
def reset_all_states(self, except_=None):
update_list = []
- def process_tag(tag_index, 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)
+ def process_tag(tag_item):
+ if tag_item.type != TagTreeItem.CATEGORY:
+ tag = tag_item.tag
+ if tag is except_:
+ tag_index = self.createIndex(tag_item.row(), 0, tag_item)
+ self.dataChanged.emit(tag_index, tag_index)
+ elif tag.state != 0 or tag in update_list:
+ tag_index = self.createIndex(tag_item.row(), 0, tag_item)
+ tag.state = 0
+ update_list.append(tag)
+ self.dataChanged.emit(tag_index, tag_index)
+ for t in tag_item.children:
+ process_tag(t)
- def process_level(category_index):
- for j in xrange(self.rowCount(category_index)):
- tag_index = self.index(j, 0, category_index)
- tag_item = tag_index.internalPointer()
- if tag_item.type == TagTreeItem.CATEGORY:
- process_level(tag_index)
- else:
- process_tag(tag_index, tag_item)
+# def process_level(category_index):
+# for j in xrange(self.rowCount(category_index)):
+# tag_index = self.index(j, 0, category_index)
+# tag_item = tag_index.internalPointer()
+# if tag_item.type == TagTreeItem.CATEGORY:
+# process_level(tag_index)
+# else:
+# process_tag(tag_index, tag_item)
- for i in xrange(self.rowCount(QModelIndex())):
- process_level(self.index(i, 0, QModelIndex()))
+ for t in self.root_item.children:
+ process_tag(t)
def clear_state(self):
self.reset_all_states()
@@ -1073,14 +1159,10 @@ class TagsModel(QAbstractItemModel): # {{{
# They will be 'checked' in both places, but we want to put the node
# into the search string only once. The nodes_seen set helps us do that
nodes_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
- row_index += 1
- category_item = self.root_item.children[row_index]
- for tag_item in category_item.child_tags():
+ for node in self.category_nodes:
+ key = node.category_key
+ for tag_item in node.child_tags():
tag = tag_item.tag
if tag.state != TAG_SEARCH_STATES['clear']:
prefix = ' not ' if tag.state == TAG_SEARCH_STATES['mark_minus'] \
@@ -1089,15 +1171,18 @@ class TagsModel(QAbstractItemModel): # {{{
if tag.name and tag.name[0] == u'\u2605': # char is a star. Assume rating
ans.append('%s%s:%s'%(prefix, category, len(tag.name)))
else:
+ name = getattr(tag, 'original_name', tag.name)
+ use_prefix = getattr(tag, 'use_prefix', False)
if category == 'tags':
- if tag.name in tags_seen:
+ if name in tags_seen:
continue
- tags_seen.add(tag.name)
+ tags_seen.add(name)
if tag in nodes_seen:
continue
nodes_seen.add(tag)
- ans.append('%s%s:"=%s"'%(prefix, category,
- tag.name.replace(r'"', r'\"')))
+ ans.append('%s%s:"=%s%s"'%(prefix, category,
+ '.' if use_prefix else '',
+ name.replace(r'"', r'\"')))
return ans
def find_item_node(self, key, txt, start_path):
diff --git a/src/calibre/library/caches.py b/src/calibre/library/caches.py
index 45b96bb69f..318183eb10 100644
--- a/src/calibre/library/caches.py
+++ b/src/calibre/library/caches.py
@@ -124,9 +124,15 @@ def _match(query, value, matchkind):
for t in value:
t = icu_lower(t)
try: ### ignore regexp exceptions, required because search-ahead tries before typing is finished
- if ((matchkind == EQUALS_MATCH and query == t) or
- (matchkind == REGEXP_MATCH and re.search(query, t, re.I)) or ### search unanchored
- (matchkind == CONTAINS_MATCH and query in t)):
+ if (matchkind == EQUALS_MATCH):
+ if query[0] == '.':
+ if t.startswith(query[1:]):
+ ql = len(query) - 1
+ return (len(t) == ql) or (t[ql:ql+1] == '.')
+ elif query == t:
+ return True
+ elif ((matchkind == REGEXP_MATCH and re.search(query, t, re.I)) or ### search unanchored
+ (matchkind == CONTAINS_MATCH and query in t)):
return True
except re.error:
pass
@@ -415,13 +421,23 @@ class ResultCache(SearchQueryParser): # {{{
if self.db_prefs is None:
return res
user_cats = self.db_prefs.get('user_categories', [])
- if location not in user_cats:
- return res
c = set(candidates)
- for (item, category, ign) in user_cats[location]:
- s = self.get_matches(category, '=' + item, candidates=c)
- c -= s
- res |= s
+ l = location.rfind('.')
+ if l > 0:
+ alt_loc = location[0:l]
+ alt_item = location[l+1:]
+ for key in user_cats:
+ if key == location or key.startswith(location + '.'):
+ for (item, category, ign) in user_cats[key]:
+ s = self.get_matches(category, '=' + item, candidates=c)
+ c -= s
+ res |= s
+ elif key == alt_loc:
+ for (item, category, ign) in user_cats[key]:
+ if item == alt_item:
+ s = self.get_matches(category, '=' + item, candidates=c)
+ c -= s
+ res |= s
if query == 'false':
return candidates - res
return res
diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py
index bd89e12044..dce0b34aef 100644
--- a/src/calibre/library/database2.py
+++ b/src/calibre/library/database2.py
@@ -174,6 +174,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
self.prefs = DBPrefs(self)
defs = self.prefs.defaults
defs['gui_restriction'] = defs['cs_restriction'] = ''
+ defs['categories_using_hierarchy'] = []
# Migrate saved search and user categories to db preference scheme
def migrate_preference(key, default):
@@ -812,6 +813,21 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
index_is_id=index_is_id),
extra=self.get_custom_extra(idx, label=meta['label'],
index_is_id=index_is_id))
+
+ user_cats = self.prefs['user_categories']
+ user_cat_vals = {}
+ for ucat in user_cats:
+ res = []
+ for name,cat,ign in user_cats[ucat]:
+ v = mi.get(cat, None)
+ if isinstance(v, list):
+ if name in v:
+ res.append([name,cat])
+ elif name == v:
+ res.append([name,cat])
+ user_cat_vals[ucat] = res
+ mi.user_categories = user_cat_vals
+
if get_cover:
mi.cover = self.cover(id, index_is_id=True, as_path=True)
return mi
@@ -1406,7 +1422,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
# temporarily duplicating the categories lists.
taglist = {}
for c in categories.keys():
- taglist[c] = dict(map(lambda t:(t.name, t), categories[c]))
+ taglist[c] = dict(map(lambda t:(icu_lower(t.name), t), categories[c]))
muc = self.prefs.get('grouped_search_make_user_categories', [])
gst = self.prefs.get('grouped_search_terms', {})
@@ -1422,8 +1438,9 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
for user_cat in sorted(user_categories.keys(), key=sort_key):
items = []
for (name,label,ign) in user_categories[user_cat]:
- if label in taglist and name in taglist[label]:
- items.append(taglist[label][name])
+ n = icu_lower(name)
+ if label in taglist and n in taglist[label]:
+ items.append(taglist[label][n])
# else: do nothing, to not include nodes w zero counts
cat_name = '@' + user_cat # add the '@' to avoid name collision
# Not a problem if we accumulate entries in the icon map
diff --git a/src/calibre/library/field_metadata.py b/src/calibre/library/field_metadata.py
index 9b481a89d0..aff2803452 100644
--- a/src/calibre/library/field_metadata.py
+++ b/src/calibre/library/field_metadata.py
@@ -32,7 +32,7 @@ category_icon_map = {
'news' : 'news.png',
'tags' : 'tags.png',
'custom:' : 'column.png',
- 'user:' : 'drawer.png',
+ 'user:' : 'tb_folder.png',
'search' : 'search.png'
}
diff --git a/src/calibre/utils/formatter_functions.py b/src/calibre/utils/formatter_functions.py
index 518f2ed140..03491c038a 100644
--- a/src/calibre/utils/formatter_functions.py
+++ b/src/calibre/utils/formatter_functions.py
@@ -396,6 +396,34 @@ class BuiltinListitem(BuiltinFormatterFunction):
except:
return ''
+class BuiltinSublist(BuiltinFormatterFunction):
+ name = 'sublist'
+ arg_count = 4
+ doc = _('sublist(val, start_index, end_index, separator) -- interpret the '
+ ' value as a list of items separated by `separator`, returning a '
+ ' new list made from the `start_index`th to the `end_index`th item. '
+ 'The first item is number zero. If an index is negative, then it '
+ 'counts from the end of the list. As a special case, an end_index '
+ 'of zero is assumed to be the length of the list. Examples using '
+ 'basic template mode and assuming a #genre value if A.B.C: '
+ '{#genre:sublist(-1,0,.)} returns C
'
+ '{#genre:sublist(0,1,.)} returns A
'
+ '{#genre:sublist(0,-1,.)} returns A.B')
+
+ def evaluate(self, formatter, kwargs, mi, locals, val, start_index, end_index, sep):
+ if not val:
+ return ''
+ si = int(start_index)
+ ei = int(end_index)
+ val = val.split(sep)
+ try:
+ if ei == 0:
+ return sep.join(val[si:])
+ else:
+ return sep.join(val[si:ei])
+ except:
+ return ''
+
class BuiltinUppercase(BuiltinFormatterFunction):
name = 'uppercase'
arg_count = 1
@@ -447,6 +475,7 @@ builtin_re = BuiltinRe()
builtin_shorten = BuiltinShorten()
builtin_strcat = BuiltinStrcat()
builtin_strcmp = BuiltinStrcmp()
+builtin_sublist = BuiltinSublist()
builtin_substr = BuiltinSubstr()
builtin_subtract = BuiltinSubtract()
builtin_switch = BuiltinSwitch()