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])