diff --git a/src/calibre/ebooks/metadata/book/base.py b/src/calibre/ebooks/metadata/book/base.py
index 41b913455a..c5a8a82db1 100644
--- a/src/calibre/ebooks/metadata/book/base.py
+++ b/src/calibre/ebooks/metadata/book/base.py
@@ -92,8 +92,6 @@ class Metadata(object):
def is_null(self, field):
null_val = NULL_VALUES.get(field, None)
val = getattr(self, field, None)
- if val is False or val in (0, 0.0):
- return True
return not val or val == null_val
def __getattribute__(self, field):
@@ -129,6 +127,8 @@ class Metadata(object):
field, val = self._clean_identifier(field, val)
_data['identifiers'].update({field: val})
elif field == 'identifiers':
+ if not val:
+ val = copy.copy(NULL_VALUES.get('identifiers', None))
self.set_identifiers(val)
elif field in STANDARD_METADATA_FIELDS:
if val is None:
diff --git a/src/calibre/gui2/dialogs/saved_search_editor.py b/src/calibre/gui2/dialogs/saved_search_editor.py
index 1143a6f06a..c9f843109a 100644
--- a/src/calibre/gui2/dialogs/saved_search_editor.py
+++ b/src/calibre/gui2/dialogs/saved_search_editor.py
@@ -9,12 +9,13 @@ from PyQt4.QtGui import QDialog
from calibre.gui2.dialogs.saved_search_editor_ui import Ui_SavedSearchEditor
from calibre.utils.search_query_parser import saved_searches
from calibre.utils.icu import sort_key
+from calibre.gui2 import error_dialog
from calibre.gui2.dialogs.confirm_delete import confirm
class SavedSearchEditor(QDialog, Ui_SavedSearchEditor):
- def __init__(self, window, initial_search=None):
- QDialog.__init__(self, window)
+ def __init__(self, parent, initial_search=None):
+ QDialog.__init__(self, parent)
Ui_SavedSearchEditor.__init__(self)
self.setupUi(self)
@@ -22,12 +23,13 @@ class SavedSearchEditor(QDialog, Ui_SavedSearchEditor):
self.connect(self.search_name_box, SIGNAL('currentIndexChanged(int)'),
self.current_index_changed)
self.connect(self.delete_search_button, SIGNAL('clicked()'), self.del_search)
+ self.rename_button.clicked.connect(self.rename_search)
self.current_search_name = None
self.searches = {}
- self.searches_to_delete = []
for name in saved_searches().names():
self.searches[name] = saved_searches().lookup(name)
+ self.search_names = set([icu_lower(n) for n in saved_searches().names()])
self.populate_search_list()
if initial_search is not None and initial_search in self.searches:
@@ -42,6 +44,11 @@ class SavedSearchEditor(QDialog, Ui_SavedSearchEditor):
search_name = unicode(self.input_box.text()).strip()
if search_name == '':
return False
+ if icu_lower(search_name) in self.search_names:
+ error_dialog(self, _('Saved search already exists'),
+ _('The saved search %s already exists, perhaps with '
+ 'different case')%search_name).exec_()
+ return False
if search_name not in self.searches:
self.searches[search_name] = ''
self.populate_search_list()
@@ -57,10 +64,25 @@ class SavedSearchEditor(QDialog, Ui_SavedSearchEditor):
+'
', 'saved_search_editor_delete', self):
return
del self.searches[self.current_search_name]
- self.searches_to_delete.append(self.current_search_name)
self.current_search_name = None
self.search_name_box.removeItem(self.search_name_box.currentIndex())
+ def rename_search(self):
+ new_search_name = unicode(self.input_box.text()).strip()
+ if new_search_name == '':
+ return False
+ if icu_lower(new_search_name) in self.search_names:
+ error_dialog(self, _('Saved search already exists'),
+ _('The saved search %s already exists, perhaps with '
+ 'different case')%new_search_name).exec_()
+ return False
+ if self.current_search_name in self.searches:
+ self.searches[new_search_name] = self.searches[self.current_search_name]
+ del self.searches[self.current_search_name]
+ self.populate_search_list()
+ self.select_search(new_search_name)
+ return True
+
def select_search(self, name):
self.search_name_box.setCurrentIndex(self.search_name_box.findText(name))
@@ -78,7 +100,7 @@ class SavedSearchEditor(QDialog, Ui_SavedSearchEditor):
def accept(self):
if self.current_search_name:
self.searches[self.current_search_name] = unicode(self.search_text.toPlainText())
- for name in self.searches_to_delete:
+ for name in saved_searches().names():
saved_searches().delete(name)
for name in self.searches:
saved_searches().add(name, self.searches[name])
diff --git a/src/calibre/gui2/dialogs/saved_search_editor.ui b/src/calibre/gui2/dialogs/saved_search_editor.ui
index 3ba37bdf10..99672b5b8e 100644
--- a/src/calibre/gui2/dialogs/saved_search_editor.ui
+++ b/src/calibre/gui2/dialogs/saved_search_editor.ui
@@ -134,6 +134,20 @@
+ -
+
+
+ Rename the current search to what is in the box
+
+
+ ...
+
+
+
+ :/images/edit-undo.png:/images/edit-undo.png
+
+
+
-
diff --git a/src/calibre/gui2/preferences/look_feel.py b/src/calibre/gui2/preferences/look_feel.py
index 15d5666978..206f2b97fb 100644
--- a/src/calibre/gui2/preferences/look_feel.py
+++ b/src/calibre/gui2/preferences/look_feel.py
@@ -67,6 +67,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
if db.field_metadata[k]['is_category'] and
db.field_metadata[k]['datatype'] in ['text', 'series', 'enumeration']])
choices -= set(['authors', 'publisher', 'formats', 'news', 'identifiers'])
+ choices |= set(['search'])
self.opt_categories_using_hierarchy.update_items_cache(choices)
r('categories_using_hierarchy', db.prefs, setting=CommaSeparatedList,
choices=sorted(list(choices), key=sort_key))
diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py
index c4871880a4..12a29a469c 100644
--- a/src/calibre/gui2/tag_view.py
+++ b/src/calibre/gui2/tag_view.py
@@ -533,7 +533,9 @@ class TagsView(QTreeView): # {{{
self.setModel(self._model)
except:
# The DB must be gone. Set the model to None and hope that someone
- # will call set_database later. I don't know if this in fact works
+ # will call set_database later. I don't know if this in fact works.
+ # But perhaps a Bad Thing Happened, so print the exception
+ traceback.print_exc()
self._model = None
self.setModel(None)
# }}}
@@ -678,7 +680,8 @@ class TagTreeItem(object): # {{{
break
elif self.tag.state == TAG_SEARCH_STATES['mark_plusplus'] or\
self.tag.state == TAG_SEARCH_STATES['mark_minusminus']:
- if self.tag.is_hierarchical and len(self.children):
+ if self.tag.is_searchable and self.tag.is_hierarchical \
+ and len(self.children):
break
else:
break
@@ -1258,19 +1261,22 @@ class TagsModel(QAbstractItemModel): # {{{
if t.type != TagTreeItem.CATEGORY])
if (comp,tag.category) in child_map:
node_parent = child_map[(comp,tag.category)]
- node_parent.tag.is_hierarchical = True
+ node_parent.tag.is_hierarchical = key != 'search'
else:
if i < len(components)-1:
t = copy.copy(tag)
t.original_name = '.'.join(components[:i+1])
- # This 'manufactured' intermediate node can
- # be searched, but cannot be edited.
- t.is_editable = False
+ if key != 'search':
+ # This 'manufactured' intermediate node can
+ # be searched, but cannot be edited.
+ t.is_editable = False
+ else:
+ t.is_searchable = t.is_editable = False
else:
t = tag
if not in_uc:
t.original_name = t.name
- t.is_hierarchical = True
+ t.is_hierarchical = key != 'search'
t.name = comp
self.beginInsertRows(category_index, 999999, 1)
node_parent = TagTreeItem(parent=node_parent, data=t,
diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py
index e46f9b818d..e70a746b15 100644
--- a/src/calibre/library/database2.py
+++ b/src/calibre/library/database2.py
@@ -56,7 +56,7 @@ class Tag(object):
self.is_hierarchical = False
self.is_editable = is_editable
self.is_searchable = is_searchable
- self.id_set = id_set
+ self.id_set = id_set if id_set is not None else set([])
self.avg_rating = avg/2.0 if avg is not None else 0
self.sort = sort
if self.avg_rating > 0:
@@ -1691,10 +1691,19 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
return books_to_refresh
def set_metadata(self, id, mi, ignore_errors=False, set_title=True,
- set_authors=True, commit=True, force_cover=False,
- force_tags=False):
+ set_authors=True, commit=True, force_changes=False):
'''
Set metadata for the book `id` from the `Metadata` object `mi`
+
+ Setting force_changes=True will force set_metadata to update fields even
+ if mi contains empty values. In this case, 'None' is distinguished from
+ 'empty'. If mi.XXX is None, the XXX is not replaced, otherwise it is.
+ The tags, identifiers, and cover attributes are special cases. Tags and
+ identifiers cannot be set to None so then will always be replaced if
+ force_changes is true. You must ensure that mi contains the values you
+ want the book to have. Covers are always changed if a new cover is
+ provided, but are never deleted. Also note that force_changes has no
+ effect on setting title or authors.
'''
if callable(getattr(mi, 'to_book_metadata', None)):
# Handle code passing in a OPF object instead of a Metadata object
@@ -1708,12 +1717,18 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
traceback.print_exc()
else:
raise
- # force_changes has no role to play in setting title or author
+
+ def should_replace_field(attr):
+ return (force_changes and (mi.get(attr, None) is not None)) or \
+ not mi.is_null(attr)
+
path_changed = False
- if set_title and not mi.is_null('title'):
+ if set_title and mi.title:
self._set_title(id, mi.title)
path_changed = True
- if set_authors and not mi.is_null('authors'):
+ if set_authors:
+ if not mi.authors:
+ mi.authors = [_('Unknown')]
authors = []
for a in mi.authors:
authors += string_to_authors(a)
@@ -1722,17 +1737,20 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
if path_changed:
self.set_path(id, index_is_id=True)
- if not mi.is_null('author_sort'):
+ if should_replace_field('author_sort'):
doit(self.set_author_sort, id, mi.author_sort, notify=False,
commit=False)
- if not mi.is_null('publisher'):
+ if should_replace_field('publisher'):
doit(self.set_publisher, id, mi.publisher, notify=False,
commit=False)
- if not mi.is_null('rating'):
+
+ # Setting rating to zero is acceptable.
+ if mi.rating is not None:
doit(self.set_rating, id, mi.rating, notify=False, commit=False)
- if not mi.is_null('series'):
+ if should_replace_field('series'):
doit(self.set_series, id, mi.series, notify=False, commit=False)
+ # force_changes has no effect on cover manipulation
if mi.cover_data[1] is not None:
doit(self.set_cover, id, mi.cover_data[1], commit=False)
elif mi.cover is not None:
@@ -1741,36 +1759,45 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
raw = f.read()
if raw:
doit(self.set_cover, id, raw, commit=False)
- elif force_cover:
- doit(self.remove_cover, id, notify=False, commit=False)
- if force_tags or not mi.is_null('tags'):
+ # if force_changes is true, tags are always replaced because the
+ # attribute cannot be set to None.
+ if should_replace_field('tags'):
doit(self.set_tags, id, mi.tags, notify=False, commit=False)
- if not mi.is_null('comments'):
+
+ if should_replace_field('comments'):
doit(self.set_comment, id, mi.comments, notify=False, commit=False)
- if not mi.is_null('series_index'):
+
+ # Setting series_index to zero is acceptable
+ if mi.series_index is not None:
doit(self.set_series_index, id, mi.series_index, notify=False,
commit=False)
- if not mi.is_null('pubdate'):
+ if should_replace_field('pubdate'):
doit(self.set_pubdate, id, mi.pubdate, notify=False, commit=False)
if getattr(mi, 'timestamp', None) is not None:
doit(self.set_timestamp, id, mi.timestamp, notify=False,
commit=False)
+ # identifiers will always be replaced if force_changes is True
mi_idents = mi.get_identifiers()
- if mi_idents:
+ if force_changes:
+ self.set_identifiers(id, mi_idents, notify=False, commit=False)
+ elif mi_idents:
identifiers = self.get_identifiers(id, index_is_id=True)
for key, val in mi_idents.iteritems():
- if val and val.strip():
+ if val and val.strip(): # Don't delete an existing identifier
identifiers[icu_lower(key)] = val
self.set_identifiers(id, identifiers, notify=False, commit=False)
+
user_mi = mi.get_all_user_metadata(make_copy=False)
for key in user_mi.iterkeys():
if key in self.field_metadata and \
user_mi[key]['datatype'] == self.field_metadata[key]['datatype']:
- doit(self.set_custom, id, val=mi.get(key), commit=False,
- extra=mi.get_extra(key), label=user_mi[key]['label'])
+ val = mi.get(key, None)
+ if force_changes or val is not None:
+ doit(self.set_custom, id, val=val, extra=mi.get_extra(key),
+ label=user_mi[key]['label'], commit=False)
if commit:
self.conn.commit()
self.notify('metadata', [id])