From 477d947899332fd303b3b646cac3d62c8e4a6f35 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Wed, 2 Jun 2010 23:47:54 +0100 Subject: [PATCH 1/7] Handle attempts to set tags to the empty string --- src/calibre/gui2/dialogs/tag_list_editor.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/calibre/gui2/dialogs/tag_list_editor.py b/src/calibre/gui2/dialogs/tag_list_editor.py index c2cc1d7116..427e7f44a5 100644 --- a/src/calibre/gui2/dialogs/tag_list_editor.py +++ b/src/calibre/gui2/dialogs/tag_list_editor.py @@ -37,6 +37,11 @@ class TagListEditor(QDialog, Ui_TagListEditor): self.connect(self.available_tags, SIGNAL('itemChanged(QListWidgetItem *)'), self.finish_editing) def finish_editing(self, item): + if not item.text(): + error_dialog(self, 'Tag is blank', + 'A tag cannot be set to nothing. Delete it instead.'%(item.text())).exec_() + item.setText(self.item_before_editing.text()) + return if item.text() != self.item_before_editing.text(): if item.text() in self.all_tags.keys() or item.text() in self.to_rename.keys(): error_dialog(self, 'Tag already used', From 40d054d465140be0838f736ccc1bd970c9e67f45 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Thu, 3 Jun 2010 08:54:36 +0100 Subject: [PATCH 2/7] Fix tag_list_editor.py to use ids when deleting tags --- src/calibre/gui2/dialogs/tag_list_editor.py | 9 +++++---- src/calibre/library/database2.py | 18 ++++++++++-------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/calibre/gui2/dialogs/tag_list_editor.py b/src/calibre/gui2/dialogs/tag_list_editor.py index 427e7f44a5..bccd0deee9 100644 --- a/src/calibre/gui2/dialogs/tag_list_editor.py +++ b/src/calibre/gui2/dialogs/tag_list_editor.py @@ -48,7 +48,7 @@ class TagListEditor(QDialog, Ui_TagListEditor): 'The tag %s is already used.'%(item.text())).exec_() item.setText(self.item_before_editing.text()) return - id,ign = self.item_before_editing.data(Qt.UserRole).toInt() + (id,_) = self.item_before_editing.data(Qt.UserRole).toInt() self.to_rename[item.text()] = id def rename_tag(self): @@ -82,13 +82,14 @@ class TagListEditor(QDialog, Ui_TagListEditor): deletes += confirms for item in deletes: - self.to_delete.append(item) + (id,_) = item.data(Qt.UserRole).toInt() + self.to_delete.append(id) self.available_tags.takeItem(self.available_tags.row(item)) def accept(self): for text in self.to_rename: - self.db.rename_tag(self.to_rename[text], unicode(text)) + self.db.rename_tag(id=self.to_rename[text], new_name=unicode(text)) for item in self.to_delete: - self.db.delete_tag(unicode(item.text())) + self.db.delete_tag_using_id(item) QDialog.accept(self) diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 0544293095..f76ae9c77a 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -987,16 +987,13 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): # Convenience method for tags_list_editor def get_tags_with_ids(self): - result = self.conn.get('SELECT * FROM tags') + result = self.conn.get('SELECT id,name FROM tags') if not result: - return {} - r = [] - for k,v in result: - r.append((k,v)) - return r + return [] + return result - def rename_tag(self, id, new): - self.conn.execute('UPDATE tags SET name=? WHERE id=?', (new, id)) + def rename_tag(self, id, new_name): + self.conn.execute('UPDATE tags SET name=? WHERE id=?', (new_name, id)) self.conn.commit() def get_tags(self, id): @@ -1083,6 +1080,11 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): self.conn.execute('DELETE FROM tags WHERE id=?', (id,)) self.conn.commit() + def delete_tag_using_id(self, id): + if id: + self.conn.execute('DELETE FROM books_tags_link WHERE tag=?', (id,)) + self.conn.execute('DELETE FROM tags WHERE id=?', (id,)) + self.conn.commit() def set_series(self, id, series, notify=True): self.conn.execute('DELETE FROM books_series_link WHERE book=?',(id,)) From 55c0f6f289be93c3861d47f331277e88399e8c61 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Thu, 3 Jun 2010 10:58:21 +0100 Subject: [PATCH 3/7] 1) Add 'managers' for series and publisher. 2) Option for manager now appears only if the right-click is on/in that category 3) Avoid the cleanup/repaints if no work was done --- src/calibre/gui2/dialogs/tag_list_editor.py | 86 +++++++++++++-------- src/calibre/gui2/dialogs/tag_list_editor.ui | 8 +- src/calibre/gui2/tag_view.py | 30 +++++-- src/calibre/gui2/ui.py | 9 ++- src/calibre/library/database2.py | 55 ++++++++++--- 5 files changed, 132 insertions(+), 56 deletions(-) diff --git a/src/calibre/gui2/dialogs/tag_list_editor.py b/src/calibre/gui2/dialogs/tag_list_editor.py index bccd0deee9..7ee616fe1b 100644 --- a/src/calibre/gui2/dialogs/tag_list_editor.py +++ b/src/calibre/gui2/dialogs/tag_list_editor.py @@ -6,12 +6,11 @@ from PyQt4.QtGui import QDialog, QListWidgetItem from calibre.gui2.dialogs.tag_list_editor_ui import Ui_TagListEditor from calibre.gui2 import question_dialog, error_dialog +from calibre.ebooks.metadata import title_sort + class TagListEditor(QDialog, Ui_TagListEditor): - def tag_cmp(self, x, y): - return cmp(x.lower(), y.lower()) - - def __init__(self, window, db, tag_to_match): + def __init__(self, window, db, tag_to_match, category): QDialog.__init__(self, window) Ui_TagListEditor.__init__(self) self.setupUi(self) @@ -20,9 +19,20 @@ class TagListEditor(QDialog, Ui_TagListEditor): self.to_delete = [] self.db = db self.all_tags = {} - for k,v in db.get_tags_with_ids(): + self.category = category + if category == 'tags': + result = db.get_tags_with_ids() + compare = (lambda x,y:cmp(x.lower(), y.lower())) + elif category == 'series': + result = db.get_series_with_ids() + compare = (lambda x,y:cmp(title_sort(x).lower(), title_sort(y).lower())) + elif category == 'publishers': + result = db.get_publishers_with_ids() + compare = (lambda x,y:cmp(x.lower(), y.lower())) + + for k,v in result: self.all_tags[v] = k - for tag in sorted(self.all_tags.keys(), cmp=self.tag_cmp): + for tag in sorted(self.all_tags.keys(), cmp=compare): item = QListWidgetItem(tag) item.setData(Qt.UserRole, self.all_tags[tag]) self.available_tags.addItem(item) @@ -38,17 +48,17 @@ class TagListEditor(QDialog, Ui_TagListEditor): def finish_editing(self, item): if not item.text(): - error_dialog(self, 'Tag is blank', - 'A tag cannot be set to nothing. Delete it instead.'%(item.text())).exec_() + error_dialog(self, 'Item is blank', + 'An item cannot be set to nothing. Delete it instead.'%(item.text())).exec_() item.setText(self.item_before_editing.text()) return if item.text() != self.item_before_editing.text(): if item.text() in self.all_tags.keys() or item.text() in self.to_rename.keys(): - error_dialog(self, 'Tag already used', - 'The tag %s is already used.'%(item.text())).exec_() + error_dialog(self, 'Item already used', + 'The item %s is already used.'%(item.text())).exec_() item.setText(self.item_before_editing.text()) return - (id,_) = self.item_before_editing.data(Qt.UserRole).toInt() + (id,ign) = self.item_before_editing.data(Qt.UserRole).toInt() self.to_rename[item.text()] = id def rename_tag(self): @@ -57,39 +67,49 @@ class TagListEditor(QDialog, Ui_TagListEditor): def _rename_tag(self, item): if item is None: - error_dialog(self, 'No tag selected', 'You must select one tag from the list of Available tags.').exec_() + error_dialog(self, 'No item selected', 'You must select one item from the list of Available items.').exec_() return self.item_before_editing = item.clone() item.setFlags (item.flags() | Qt.ItemIsEditable); self.available_tags.editItem(item) def delete_tags(self, item=None): - confirms, deletes = [], [] - items = self.available_tags.selectedItems() if item is None else [item] - if not items: - error_dialog(self, 'No tags selected', 'You must select at least one tag from the list of Available tags.').exec_() + deletes = self.available_tags.selectedItems() if item is None else [item] + if not deletes: + error_dialog(self, 'No items selected', 'You must select at least one items from the list.').exec_() + return + ct = ', '.join([unicode(item.text()) for item in deletes]) + if not question_dialog(self, _('Are your sure?'), + '

'+_('Are you certain you want to delete the following items?')+'
'+ct): return - for item in items: - if self.db.is_tag_used(unicode(item.text())): - confirms.append(item) - else: - deletes.append(item) - if confirms: - ct = ', '.join([unicode(item.text()) for item in confirms]) - if question_dialog(self, _('Are your sure?'), - '

'+_('The following tags are used by one or more books. ' - 'Are you certain you want to delete them?')+'
'+ct): - deletes += confirms for item in deletes: - (id,_) = item.data(Qt.UserRole).toInt() + (id,ign) = item.data(Qt.UserRole).toInt() self.to_delete.append(id) self.available_tags.takeItem(self.available_tags.row(item)) def accept(self): - for text in self.to_rename: - self.db.rename_tag(id=self.to_rename[text], new_name=unicode(text)) - for item in self.to_delete: - self.db.delete_tag_using_id(item) - QDialog.accept(self) + rename_func = None + if self.category == 'tags': + rename_func = self.db.rename_tag + delete_func = self.db.delete_tag_using_id + elif self.category == 'series': + rename_func = self.db.rename_series + delete_func = self.db.delete_series_using_id + elif self.category == 'publishers': + rename_func = self.db.rename_publisher + delete_func = self.db.delete_publisher_using_id + + work_done = False + if rename_func: + for text in self.to_rename: + work_done = True + rename_func(id=self.to_rename[text], new_name=unicode(text)) + for item in self.to_delete: + work_done = True + delete_func(item) + if not work_done: + QDialog.reject(self) + else: + QDialog.accept(self) diff --git a/src/calibre/gui2/dialogs/tag_list_editor.ui b/src/calibre/gui2/dialogs/tag_list_editor.ui index 383dc875ac..4f57af745b 100644 --- a/src/calibre/gui2/dialogs/tag_list_editor.ui +++ b/src/calibre/gui2/dialogs/tag_list_editor.ui @@ -11,7 +11,7 @@ - Tag Editor + Category Editor @@ -25,7 +25,7 @@ - Tags in use + Items in use available_tags @@ -54,7 +54,7 @@ - Delete tag from database. This will unapply the tag from all books and then remove it from the database. + Delete item from database. This will unapply the item from all books and then remove it from the database. ... @@ -74,7 +74,7 @@ - Rename the tag everywhere it is used. + Rename the item in every book where it is used. ... diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index 11db157ed4..fd232bb750 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -24,7 +24,7 @@ class TagsView(QTreeView): # {{{ restriction_set = pyqtSignal(object) tags_marked = pyqtSignal(object, object) user_category_edit = pyqtSignal(object) - tag_list_edit = pyqtSignal(object) + tag_list_edit = pyqtSignal(object, object) saved_search_edit = pyqtSignal(object) def __init__(self, *args): @@ -91,7 +91,13 @@ class TagsView(QTreeView): # {{{ return try: if action == 'manage_tags': - self.tag_list_edit.emit(category) + self.tag_list_edit.emit(category, 'tags') + return + if action == 'manage_series': + self.tag_list_edit.emit(category, 'series') + return + if action == 'manage_publishers': + self.tag_list_edit.emit(category, 'publishers') return if action == 'manage_categories': self.user_category_edit.emit(category) @@ -136,10 +142,24 @@ class TagsView(QTreeView): # {{{ partial(self.context_menu_handler, action='defaults')) self.context_menu.addSeparator() - self.context_menu.addAction(_('Manage Tags'), + if category == _('Tags'): + self.context_menu.addAction(_('Manage Tags'), partial(self.context_menu_handler, action='manage_tags', category=tag_name)) + elif category == _('Searches'): + self.context_menu.addAction(_('Manage Saved Searches'), + partial(self.context_menu_handler, action='manage_searches', + category=tag_name)) + elif category == _('Publishers'): + self.context_menu.addAction(_('Manage Publishers'), + partial(self.context_menu_handler, action='manage_publishers', + category=tag_name)) + elif category == _('Series'): + self.context_menu.addAction(_('Manage Series'), + partial(self.context_menu_handler, action='manage_series', + category=tag_name)) + self.context_menu.addSeparator() if category in prefs['user_categories'].keys(): self.context_menu.addAction(_('Manage User Categories'), partial(self.context_menu_handler, action='manage_categories', @@ -149,10 +169,6 @@ class TagsView(QTreeView): # {{{ partial(self.context_menu_handler, action='manage_categories', category=None)) - self.context_menu.addAction(_('Manage Saved Searches'), - partial(self.context_menu_handler, action='manage_searches', - category=tag_name)) - self.context_menu.popup(self.mapToGlobal(point)) return True diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index 773f44acd2..63172fc7a5 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -660,13 +660,16 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): self.tags_view.set_new_model() self.tags_view.recount() - def do_tags_list_edit(self, tag): - d = TagListEditor(self, self.library_view.model().db, tag) + def do_tags_list_edit(self, tag, category): + d = TagListEditor(self, self.library_view.model().db, tag, category) d.exec_() if d.result() == d.Accepted: + # Clean up everything, as information could have changed for many books. + self.library_view.model().refresh() self.tags_view.set_new_model() self.tags_view.recount() - self.library_view.model().refresh() + self.saved_search.clear_to_help() + self.search.clear_to_help() def do_saved_search_edit(self, search): d = SavedSearchEditor(self, search) diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index f76ae9c77a..c01d6fd4d6 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -731,8 +731,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): icon=icon, tooltip = tooltip) for r in data if item_not_zero_func(r)] if category == 'series': - categories[category].sort(cmp=lambda x,y:cmp(title_sort(x.name), - title_sort(y.name))) + categories[category].sort(cmp=lambda x,y:cmp(title_sort(x.name).lower(), + title_sort(y.name).lower())) # We delayed computing the standard formats category because it does not # use a view, but is computed dynamically @@ -985,7 +985,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): if notify: self.notify('metadata', [id]) - # Convenience method for tags_list_editor + # Convenience methods for tags_list_editor def get_tags_with_ids(self): result = self.conn.get('SELECT id,name FROM tags') if not result: @@ -996,6 +996,49 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): self.conn.execute('UPDATE tags SET name=? WHERE id=?', (new_name, id)) self.conn.commit() + def delete_tag_using_id(self, id): + if id: + self.conn.execute('DELETE FROM books_tags_link WHERE tag=?', (id,)) + self.conn.execute('DELETE FROM tags WHERE id=?', (id,)) + self.conn.commit() + + def get_series_with_ids(self): + result = self.conn.get('SELECT id,name FROM series') + if not result: + return [] + return result + + def rename_series(self, id, new_name): + self.conn.execute('UPDATE series SET name=? WHERE id=?', (new_name, id)) + self.conn.commit() + + def delete_series_using_id(self, id): + if id: + books = self.conn.get('SELECT book from books_series_link WHERE series=?', (id,)) + for (book_id,) in books: + self.conn.execute('UPDATE books SET series_index=1.0 WHERE id=?', (book_id,)) + self.conn.execute('DELETE FROM books_series_link WHERE series=?', (id,)) + self.conn.execute('DELETE FROM series WHERE id=?', (id,)) + self.conn.commit() + + def get_publishers_with_ids(self): + result = self.conn.get('SELECT id,name FROM publishers') + if not result: + return [] + return result + + def rename_publisher(self, id, new_name): + self.conn.execute('UPDATE publishers SET name=? WHERE id=?', (new_name, id)) + self.conn.commit() + + def delete_publisher_using_id(self, id): + if id: + self.conn.execute('DELETE FROM books_publishers_link WHERE publisher=?', (id,)) + self.conn.execute('DELETE FROM publishers WHERE id=?', (id,)) + self.conn.commit() + + # end convenience methods + def get_tags(self, id): result = self.conn.get( 'SELECT name FROM tags WHERE id IN (SELECT tag FROM books_tags_link WHERE book=?)', @@ -1080,12 +1123,6 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): self.conn.execute('DELETE FROM tags WHERE id=?', (id,)) self.conn.commit() - def delete_tag_using_id(self, id): - if id: - self.conn.execute('DELETE FROM books_tags_link WHERE tag=?', (id,)) - self.conn.execute('DELETE FROM tags WHERE id=?', (id,)) - self.conn.commit() - def set_series(self, id, series, notify=True): self.conn.execute('DELETE FROM books_series_link WHERE book=?',(id,)) self.conn.execute('DELETE FROM series WHERE (SELECT COUNT(id) FROM books_series_link WHERE series=series.id) < 1') From 07837f6d4742c701b7c1629091d4efe768b89076 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Thu, 3 Jun 2010 13:10:40 +0100 Subject: [PATCH 4/7] 1) On tag pane editing of series, publisher, tags, search 2) a few cleanups, such as sorting the hidden category list --- src/calibre/gui2/dialogs/tag_list_editor.py | 4 +- src/calibre/gui2/tag_view.py | 81 ++++++++++++++++----- src/calibre/gui2/ui.py | 2 + src/calibre/utils/search_query_parser.py | 6 ++ 4 files changed, 74 insertions(+), 19 deletions(-) diff --git a/src/calibre/gui2/dialogs/tag_list_editor.py b/src/calibre/gui2/dialogs/tag_list_editor.py index 7ee616fe1b..3741226fc9 100644 --- a/src/calibre/gui2/dialogs/tag_list_editor.py +++ b/src/calibre/gui2/dialogs/tag_list_editor.py @@ -26,7 +26,7 @@ class TagListEditor(QDialog, Ui_TagListEditor): elif category == 'series': result = db.get_series_with_ids() compare = (lambda x,y:cmp(title_sort(x).lower(), title_sort(y).lower())) - elif category == 'publishers': + elif category == 'publisher': result = db.get_publishers_with_ids() compare = (lambda x,y:cmp(x.lower(), y.lower())) @@ -96,7 +96,7 @@ class TagListEditor(QDialog, Ui_TagListEditor): elif self.category == 'series': rename_func = self.db.rename_series delete_func = self.db.delete_series_using_id - elif self.category == 'publishers': + elif self.category == 'publisher': rename_func = self.db.rename_publisher delete_func = self.db.delete_publisher_using_id diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index fd232bb750..8179188a87 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -20,12 +20,14 @@ from calibre.utils.search_query_parser import saved_searches class TagsView(QTreeView): # {{{ - need_refresh = pyqtSignal() + refresh_required = pyqtSignal() restriction_set = pyqtSignal(object) tags_marked = pyqtSignal(object, object) user_category_edit = pyqtSignal(object) tag_list_edit = pyqtSignal(object, object) saved_search_edit = pyqtSignal(object) + tag_item_renamed = pyqtSignal() + search_item_renamed = pyqtSignal() def __init__(self, *args): QTreeView.__init__(self, *args) @@ -36,7 +38,8 @@ class TagsView(QTreeView): # {{{ def set_database(self, db, tag_match, popularity, restriction): self.hidden_categories = config['tag_browser_hidden_categories'] - self._model = TagsModel(db, parent=self, hidden_categories=self.hidden_categories) + self._model = TagsModel(db, parent=self, + hidden_categories=self.hidden_categories) self.popularity = popularity self.restriction = restriction self.tag_match = tag_match @@ -48,12 +51,12 @@ class TagsView(QTreeView): # {{{ self.popularity.setChecked(config['sort_by_popularity']) self.popularity.stateChanged.connect(self.sort_changed) self.restriction.activated[str].connect(self.search_restriction_set) - self.need_refresh.connect(self.recount, type=Qt.QueuedConnection) + self.refresh_required.connect(self.recount, type=Qt.QueuedConnection) db.add_listener(self.database_changed) self.saved_searches_changed(recount=False) def database_changed(self, event, ids): - self.need_refresh.emit() + self.refresh_required.emit() @property def match_all(self): @@ -80,16 +83,23 @@ class TagsView(QTreeView): # {{{ if event.button() == Qt.LeftButton: QTreeView.mouseReleaseEvent(self, event) + def mouseDoubleClickEvent(self, event): + # swallow these to avoid toggling and editing at the same time + pass + def toggle(self, index): modifiers = int(QApplication.keyboardModifiers()) exclusive = modifiers not in (Qt.CTRL, Qt.SHIFT) if self._model.toggle(index, exclusive): self.tags_marked.emit(self._model.tokens(), self.match_all) - def context_menu_handler(self, action=None, category=None): + def context_menu_handler(self, action=None, category=None, index=None): if not action: return try: + if action == 'edit_item': + self.edit(index) + return if action == 'manage_tags': self.tag_list_edit.emit(category, 'tags') return @@ -97,7 +107,7 @@ class TagsView(QTreeView): # {{{ self.tag_list_edit.emit(category, 'series') return if action == 'manage_publishers': - self.tag_list_edit.emit(category, 'publishers') + self.tag_list_edit.emit(category, 'publisher') return if action == 'manage_categories': self.user_category_edit.emit(category) @@ -123,24 +133,31 @@ class TagsView(QTreeView): # {{{ item = index.internalPointer() tag_name = '' if item.type == TagTreeItem.TAG: + tag_item = item tag_name = item.tag.name item = item.parent if item.type == TagTreeItem.CATEGORY: category = unicode(item.name.toString()) self.context_menu = QMenu(self) - self.context_menu.addAction(_('Hide %s') % category, - partial(self.context_menu_handler, action='hide', category=category)) - - if self.hidden_categories: + # If the user right-clicked on a tag/series/publisher, then offer + # the possibility of renaming that item + if tag_name and item.category_key in ['tags', 'series', 'publisher', 'search']: + self.context_menu.addAction(_('Rename item') + " '" + tag_name + "'", + partial(self.context_menu_handler, action='edit_item', + category=tag_item, index=index)) self.context_menu.addSeparator() + # Hide/Show/Restore categories + self.context_menu.addAction(_('Hide category %s') % category, + partial(self.context_menu_handler, action='hide', category=category)) + if self.hidden_categories: m = self.context_menu.addMenu(_('Show category')) - for col in self.hidden_categories: + for col in sorted(self.hidden_categories, cmp=lambda x,y: cmp(x.lower(), y.lower())): m.addAction(col, partial(self.context_menu_handler, action='show', category=col)) - self.context_menu.addSeparator() - self.context_menu.addAction(_('Restore defaults'), + self.context_menu.addAction(_('Show all categories'), partial(self.context_menu_handler, action='defaults')) + # Offer specific editors for tags/series/publishers/saved searches self.context_menu.addSeparator() if category == _('Tags'): self.context_menu.addAction(_('Manage Tags'), @@ -159,6 +176,7 @@ class TagsView(QTreeView): # {{{ partial(self.context_menu_handler, action='manage_series', category=tag_name)) + # Always show the user categories editor self.context_menu.addSeparator() if category in prefs['user_categories'].keys(): self.context_menu.addAction(_('Manage User Categories'), @@ -219,7 +237,8 @@ class TagTreeItem(object): # {{{ TAG = 1 ROOT = 2 - def __init__(self, data=None, category_icon=None, icon_map=None, parent=None, tooltip=None): + def __init__(self, data=None, category_icon=None, icon_map=None, + parent=None, tooltip=None, category_key=None): self.parent = parent self.children = [] if self.parent is not None: @@ -234,6 +253,7 @@ class TagTreeItem(object): # {{{ self.bold_font = QFont() self.bold_font.setBold(True) self.bold_font = QVariant(self.bold_font) + self.category_key = category_key elif self.type == self.TAG: icon_map[0] = data.icon self.tag, self.icon_state_map = data, list(map(QVariant, icon_map)) @@ -279,6 +299,8 @@ class TagTreeItem(object): # {{{ return QVariant('%s'%(self.tag.name)) else: return QVariant('[%d] %s'%(self.tag.count, self.tag.name)) + if role == Qt.EditRole: + return QVariant(self.tag.name) if role == Qt.DecorationRole: return self.icon_state_map[self.tag.state] if role == Qt.ToolTipRole and self.tag.tooltip is not None: @@ -293,7 +315,7 @@ class TagTreeItem(object): # {{{ class TagsModel(QAbstractItemModel): # {{{ - def __init__(self, db, parent=None, hidden_categories=None): + def __init__(self, db, parent, hidden_categories=None): QAbstractItemModel.__init__(self, parent) # must do this here because 'QPixmap: Must construct a QApplication @@ -313,6 +335,7 @@ class TagsModel(QAbstractItemModel): # {{{ self.icon_state_map = [None, QIcon(I('plus.svg')), QIcon(I('minus.svg'))] self.db = db + self.tags_view = parent self.hidden_categories = hidden_categories self.search_restriction = '' self.ignore_next_search = 0 @@ -340,7 +363,7 @@ class TagsModel(QAbstractItemModel): # {{{ c = TagTreeItem(parent=self.root_item, data=self.categories[i], category_icon=self.category_icon_map[r], - tooltip=tt) + tooltip=tt, category_key=r) for tag in data[r]: TagTreeItem(parent=c, data=tag, icon_map=self.icon_state_map) @@ -398,11 +421,35 @@ class TagsModel(QAbstractItemModel): # {{{ item = index.internalPointer() return item.data(role) + def setData(self, index, value, role=Qt.EditRole): + if not index.isValid(): + return NONE + val = unicode(value.toString()) + item = index.internalPointer() + if item.parent.category_key == 'series': + self.db.rename_series(item.tag.id, val) + item.tag.name = val + self.tags_view.tag_item_renamed.emit() + elif item.parent.category_key == 'publisher': + self.db.rename_publisher(item.tag.id, val) + item.tag.name = val + self.tags_view.tag_item_renamed.emit() + elif item.parent.category_key == 'tags': + self.db.rename_tag(item.tag.id, val) + item.tag.name = val + self.tags_view.tag_item_renamed.emit() + elif item.parent.category_key == 'search': + saved_searches.rename(unicode(item.data(role).toString()), val) + item.tag.name = val + self.tags_view.search_item_renamed.emit() + self.dataChanged.emit(index, index) + return True + def headerData(self, *args): return NONE def flags(self, *args): - return Qt.ItemIsEnabled|Qt.ItemIsSelectable + return Qt.ItemIsEnabled|Qt.ItemIsSelectable|Qt.ItemIsEditable def path_for_index(self, index): ans = [] diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index 63172fc7a5..4d1555a84a 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -553,6 +553,8 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): self.tags_view.tag_list_edit.connect(self.do_tags_list_edit) self.tags_view.user_category_edit.connect(self.do_user_categories_edit) self.tags_view.saved_search_edit.connect(self.do_saved_search_edit) + self.tags_view.tag_item_renamed.connect(self.library_view.model().refresh) + self.tags_view.search_item_renamed.connect(self.saved_search.clear_to_help) self.search.search.connect(self.tags_view.model().reinit) for x in (self.location_view.count_changed, self.tags_view.recount, self.restriction_count_changed): diff --git a/src/calibre/utils/search_query_parser.py b/src/calibre/utils/search_query_parser.py index 509adb49d4..d6bf932b76 100644 --- a/src/calibre/utils/search_query_parser.py +++ b/src/calibre/utils/search_query_parser.py @@ -52,6 +52,12 @@ class SavedSearchQueries(object): self.queries.pop(self.force_unicode(name), False) prefs[self.opt_name] = self.queries + def rename(self, old_name, new_name): + self.queries[self.force_unicode(new_name)] = \ + self.queries.get(self.force_unicode(old_name), None) + self.queries.pop(self.force_unicode(old_name), False) + prefs[self.opt_name] = self.queries + def names(self): return sorted(self.queries.keys(), cmp=lambda x,y: cmp(x.lower(), y.lower())) From 8233bd3a8974e62ceddafa444f8059ea32f54be6 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Thu, 3 Jun 2010 14:37:55 +0100 Subject: [PATCH 5/7] In-place editing of author names --- src/calibre/gui2/dialogs/tag_list_editor.py | 15 ++++---- src/calibre/gui2/tag_view.py | 29 ++++++++++++--- src/calibre/library/database2.py | 40 +++++++++++++++++---- 3 files changed, 66 insertions(+), 18 deletions(-) diff --git a/src/calibre/gui2/dialogs/tag_list_editor.py b/src/calibre/gui2/dialogs/tag_list_editor.py index 3741226fc9..dc06a8897e 100644 --- a/src/calibre/gui2/dialogs/tag_list_editor.py +++ b/src/calibre/gui2/dialogs/tag_list_editor.py @@ -48,14 +48,14 @@ class TagListEditor(QDialog, Ui_TagListEditor): def finish_editing(self, item): if not item.text(): - error_dialog(self, 'Item is blank', - 'An item cannot be set to nothing. Delete it instead.'%(item.text())).exec_() + error_dialog(self, _('Item is blank'), + _('An item cannot be set to nothing. Delete it instead.')).exec_() item.setText(self.item_before_editing.text()) return if item.text() != self.item_before_editing.text(): if item.text() in self.all_tags.keys() or item.text() in self.to_rename.keys(): - error_dialog(self, 'Item already used', - 'The item %s is already used.'%(item.text())).exec_() + error_dialog(self, _('Item already used'), + _('The item %s is already used.')%(item.text())).exec_() item.setText(self.item_before_editing.text()) return (id,ign) = self.item_before_editing.data(Qt.UserRole).toInt() @@ -67,7 +67,8 @@ class TagListEditor(QDialog, Ui_TagListEditor): def _rename_tag(self, item): if item is None: - error_dialog(self, 'No item selected', 'You must select one item from the list of Available items.').exec_() + error_dialog(self, _('No item selected'), + _('You must select one item from the list of Available items.')).exec_() return self.item_before_editing = item.clone() item.setFlags (item.flags() | Qt.ItemIsEditable); @@ -76,7 +77,8 @@ class TagListEditor(QDialog, Ui_TagListEditor): def delete_tags(self, item=None): deletes = self.available_tags.selectedItems() if item is None else [item] if not deletes: - error_dialog(self, 'No items selected', 'You must select at least one items from the list.').exec_() + error_dialog(self, _('No items selected'), + _('You must select at least one items from the list.')).exec_() return ct = ', '.join([unicode(item.text()) for item in deletes]) if not question_dialog(self, _('Are your sure?'), @@ -112,4 +114,3 @@ class TagListEditor(QDialog, Ui_TagListEditor): QDialog.reject(self) else: QDialog.accept(self) - diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index 8179188a87..6946f337d6 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -17,6 +17,7 @@ from calibre.gui2 import config, NONE from calibre.utils.config import prefs from calibre.library.field_metadata import TagsIcons from calibre.utils.search_query_parser import saved_searches +from calibre.gui2 import error_dialog class TagsView(QTreeView): # {{{ @@ -141,7 +142,7 @@ class TagsView(QTreeView): # {{{ self.context_menu = QMenu(self) # If the user right-clicked on a tag/series/publisher, then offer # the possibility of renaming that item - if tag_name and item.category_key in ['tags', 'series', 'publisher', 'search']: + if tag_name and item.category_key in ['authors', 'tags', 'series', 'publisher', 'search']: self.context_menu.addAction(_('Rename item') + " '" + tag_name + "'", partial(self.context_menu_handler, action='edit_item', category=tag_item, index=index)) @@ -381,8 +382,12 @@ class TagsModel(QAbstractItemModel): # {{{ data = self.db.get_categories(sort_on_count=sort, icon_map=self.category_icon_map) tb_categories = self.db.field_metadata + self.category_items = {} for category in tb_categories: if category in data: # They should always be there, but ... + # make a map of sets of names per category for duplicate + # checking when editing + self.category_items[category] = [tag.name for tag in data[category]] self.row_map.append(category) self.categories.append(tb_categories[category]['name']) @@ -425,23 +430,37 @@ class TagsModel(QAbstractItemModel): # {{{ if not index.isValid(): return NONE val = unicode(value.toString()) + if not val: + error_dialog(self.tags_view, _('Item is blank'), + _('An item cannot be set to nothing. Delete it instead.')).exec_() + return False + item = index.internalPointer() - if item.parent.category_key == 'series': + key = item.parent.category_key + if val in self.category_items[key]: + error_dialog(self.tags_view, 'Duplicate item', + _('The name %s is already used.')%val).exec_() + return False + if key == 'series': self.db.rename_series(item.tag.id, val) item.tag.name = val self.tags_view.tag_item_renamed.emit() - elif item.parent.category_key == 'publisher': + elif key == 'publisher': self.db.rename_publisher(item.tag.id, val) item.tag.name = val self.tags_view.tag_item_renamed.emit() - elif item.parent.category_key == 'tags': + elif key == 'tags': self.db.rename_tag(item.tag.id, val) item.tag.name = val self.tags_view.tag_item_renamed.emit() - elif item.parent.category_key == 'search': + elif key == 'search': saved_searches.rename(unicode(item.data(role).toString()), val) item.tag.name = val self.tags_view.search_item_renamed.emit() + elif key == 'authors': + self.db.rename_author(item.tag.id, val) + item.tag.name = val + self.tags_view.tag_item_renamed.emit() self.dataChanged.emit(index, index) return True diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index c01d6fd4d6..860859061c 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -993,8 +993,9 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): return result def rename_tag(self, id, new_name): - self.conn.execute('UPDATE tags SET name=? WHERE id=?', (new_name, id)) - self.conn.commit() + if id: + self.conn.execute('UPDATE tags SET name=? WHERE id=?', (new_name, id)) + self.conn.commit() def delete_tag_using_id(self, id): if id: @@ -1009,8 +1010,9 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): return result def rename_series(self, id, new_name): - self.conn.execute('UPDATE series SET name=? WHERE id=?', (new_name, id)) - self.conn.commit() + if id: + self.conn.execute('UPDATE series SET name=? WHERE id=?', (new_name, id)) + self.conn.commit() def delete_series_using_id(self, id): if id: @@ -1028,8 +1030,9 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): return result def rename_publisher(self, id, new_name): - self.conn.execute('UPDATE publishers SET name=? WHERE id=?', (new_name, id)) - self.conn.commit() + if id: + self.conn.execute('UPDATE publishers SET name=? WHERE id=?', (new_name, id)) + self.conn.commit() def delete_publisher_using_id(self, id): if id: @@ -1037,6 +1040,31 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): self.conn.execute('DELETE FROM publishers WHERE id=?', (id,)) self.conn.commit() + def rename_author(self, id, new_name): + if id: + # Make sure that any commas in new_name are changed to '|'! + new_name = new_name.replace(',', '|') + self.conn.execute('UPDATE authors SET name=? WHERE id=?', (new_name, id)) + self.conn.commit() + # now must fix up the books + books = self.conn.get('SELECT book from books_authors_link WHERE author=?', (id,)) + for (book_id,) in books: + # First, must refresh the cache to see the new authors + self.data.refresh_ids(self, [book_id]) + # now fix the filesystem paths + self.set_path(book_id, index_is_id=True) + # Next fix the author sort. Reset it to the default + authors = self.conn.get(''' + SELECT authors.name + FROM authors, books_authors_link as bl + WHERE bl.book = ? and bl.author = authors.id + ''' , (book_id,)) + # unpack the double-list structure + for i,aut in enumerate(authors): + authors[i] = aut[0] + ss = authors_to_sort_string(authors) + self.conn.execute('UPDATE books SET author_sort=? WHERE id=?', (ss, id)) + # end convenience methods def get_tags(self, id): From f12b0ff54ba54b8897dcade7f70fb1bd0f1a48eb Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Thu, 3 Jun 2010 15:31:29 +0100 Subject: [PATCH 6/7] 1) add in-place editing of author in tags view 2) editor and in-place editing of custom column 3) changed strings in tag_list_edit to be translatable 4) various cleanups --- src/calibre/gui2/dialogs/tag_list_editor.py | 11 +++- src/calibre/gui2/tag_view.py | 70 +++++++++------------ src/calibre/gui2/ui.py | 8 ++- src/calibre/library/custom_columns.py | 34 ++++++++++ 4 files changed, 80 insertions(+), 43 deletions(-) diff --git a/src/calibre/gui2/dialogs/tag_list_editor.py b/src/calibre/gui2/dialogs/tag_list_editor.py index dc06a8897e..fa05518992 100644 --- a/src/calibre/gui2/dialogs/tag_list_editor.py +++ b/src/calibre/gui2/dialogs/tag_list_editor.py @@ -1,11 +1,12 @@ __license__ = 'GPL v3' __copyright__ = '2008, Kovid Goyal ' + +from functools import partial from PyQt4.QtCore import SIGNAL, Qt from PyQt4.QtGui import QDialog, QListWidgetItem from calibre.gui2.dialogs.tag_list_editor_ui import Ui_TagListEditor from calibre.gui2 import question_dialog, error_dialog - from calibre.ebooks.metadata import title_sort class TagListEditor(QDialog, Ui_TagListEditor): @@ -29,6 +30,11 @@ class TagListEditor(QDialog, Ui_TagListEditor): elif category == 'publisher': result = db.get_publishers_with_ids() compare = (lambda x,y:cmp(x.lower(), y.lower())) + else: # should be a custom field + self.cc_label = db.field_metadata[category]['label'] + print 'here', self.cc_label + result = self.db.get_custom_items_with_ids(label=self.cc_label) + compare = (lambda x,y:cmp(x.lower(), y.lower())) for k,v in result: self.all_tags[v] = k @@ -101,6 +107,9 @@ class TagListEditor(QDialog, Ui_TagListEditor): elif self.category == 'publisher': rename_func = self.db.rename_publisher delete_func = self.db.delete_publisher_using_id + else: + rename_func = partial(self.db.rename_custom_item, label=self.cc_label) + delete_func = partial(self.db.delete_custom_item_using_id, label=self.cc_label) work_done = False if rename_func: diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index 6946f337d6..e9e5228dfc 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -94,21 +94,16 @@ class TagsView(QTreeView): # {{{ if self._model.toggle(index, exclusive): self.tags_marked.emit(self._model.tokens(), self.match_all) - def context_menu_handler(self, action=None, category=None, index=None): + def context_menu_handler(self, action=None, category=None, + key=None, index=None): if not action: return try: if action == 'edit_item': self.edit(index) return - if action == 'manage_tags': - self.tag_list_edit.emit(category, 'tags') - return - if action == 'manage_series': - self.tag_list_edit.emit(category, 'series') - return - if action == 'manage_publishers': - self.tag_list_edit.emit(category, 'publisher') + if action == 'open_editor': + self.tag_list_edit.emit(category, key) return if action == 'manage_categories': self.user_category_edit.emit(category) @@ -139,10 +134,13 @@ class TagsView(QTreeView): # {{{ item = item.parent if item.type == TagTreeItem.CATEGORY: category = unicode(item.name.toString()) + key = item.category_key self.context_menu = QMenu(self) # If the user right-clicked on a tag/series/publisher, then offer # the possibility of renaming that item - if tag_name and item.category_key in ['authors', 'tags', 'series', 'publisher', 'search']: + if tag_name and \ + (key in ['authors', 'tags', 'series', 'publisher', 'search'] or \ + self.db.field_metadata[key]['is_custom']): self.context_menu.addAction(_('Rename item') + " '" + tag_name + "'", partial(self.context_menu_handler, action='edit_item', category=tag_item, index=index)) @@ -160,22 +158,15 @@ class TagsView(QTreeView): # {{{ # Offer specific editors for tags/series/publishers/saved searches self.context_menu.addSeparator() - if category == _('Tags'): - self.context_menu.addAction(_('Manage Tags'), - partial(self.context_menu_handler, action='manage_tags', - category=tag_name)) - elif category == _('Searches'): + if key in ['tags', 'publisher', 'series'] or \ + self.db.field_metadata[key]['is_custom']: + self.context_menu.addAction(_('Manage ') + category, + partial(self.context_menu_handler, action='open_editor', + category=tag_name, key=key)) + elif key == 'search': self.context_menu.addAction(_('Manage Saved Searches'), partial(self.context_menu_handler, action='manage_searches', category=tag_name)) - elif category == _('Publishers'): - self.context_menu.addAction(_('Manage Publishers'), - partial(self.context_menu_handler, action='manage_publishers', - category=tag_name)) - elif category == _('Series'): - self.context_menu.addAction(_('Manage Series'), - partial(self.context_menu_handler, action='manage_series', - category=tag_name)) # Always show the user categories editor self.context_menu.addSeparator() @@ -441,27 +432,24 @@ class TagsModel(QAbstractItemModel): # {{{ error_dialog(self.tags_view, 'Duplicate item', _('The name %s is already used.')%val).exec_() return False - if key == 'series': - self.db.rename_series(item.tag.id, val) - item.tag.name = val - self.tags_view.tag_item_renamed.emit() - elif key == 'publisher': - self.db.rename_publisher(item.tag.id, val) - item.tag.name = val - self.tags_view.tag_item_renamed.emit() - elif key == 'tags': - self.db.rename_tag(item.tag.id, val) - item.tag.name = val - self.tags_view.tag_item_renamed.emit() - elif key == 'search': + if key == 'search': saved_searches.rename(unicode(item.data(role).toString()), val) - item.tag.name = val self.tags_view.search_item_renamed.emit() - elif key == 'authors': - self.db.rename_author(item.tag.id, val) - item.tag.name = val + else: + if key == 'series': + self.db.rename_series(item.tag.id, val) + elif key == 'publisher': + self.db.rename_publisher(item.tag.id, val) + elif key == 'tags': + self.db.rename_tag(item.tag.id, val) + elif key == 'authors': + self.db.rename_author(item.tag.id, val) + elif self.db.field_metadata[key]['is_custom']: + self.db.rename_custom_item(item.tag.id, val, + label=self.db.field_metadata[key]['label']) self.tags_view.tag_item_renamed.emit() - self.dataChanged.emit(index, index) + item.tag.name = val + self.refresh() return True def headerData(self, *args): diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index 4d1555a84a..7546e461d6 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -553,7 +553,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): self.tags_view.tag_list_edit.connect(self.do_tags_list_edit) self.tags_view.user_category_edit.connect(self.do_user_categories_edit) self.tags_view.saved_search_edit.connect(self.do_saved_search_edit) - self.tags_view.tag_item_renamed.connect(self.library_view.model().refresh) + self.tags_view.tag_item_renamed.connect(self.do_tag_item_renamed) self.tags_view.search_item_renamed.connect(self.saved_search.clear_to_help) self.search.search.connect(self.tags_view.model().reinit) for x in (self.location_view.count_changed, self.tags_view.recount, @@ -673,6 +673,12 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): self.saved_search.clear_to_help() self.search.clear_to_help() + def do_tag_item_renamed(self): + # Clean up library view and search + self.library_view.model().refresh() + self.saved_search.clear_to_help() + self.search.clear_to_help() + def do_saved_search_edit(self, search): d = SavedSearchEditor(self, search) d.exec_() diff --git a/src/calibre/library/custom_columns.py b/src/calibre/library/custom_columns.py index 83e6b029cb..64a261d935 100644 --- a/src/calibre/library/custom_columns.py +++ b/src/calibre/library/custom_columns.py @@ -171,6 +171,40 @@ class CustomColumns(object): ans.sort(cmp=lambda x,y:cmp(x.lower(), y.lower())) return ans + # convenience methods for tag editing + def get_custom_items_with_ids(self, label=None, num=None): + if label is not None: + data = self.custom_column_label_map[label] + if num is not None: + data = self.custom_column_num_map[num] + table, lt = self.custom_table_names(data['num']) + if not data['normalized']: + return [] + ans = self.conn.get('SELECT id, value FROM %s'%table) + return ans + + def rename_custom_item(self, id, new_name, label=None, num=None): + if id: + if label is not None: + data = self.custom_column_label_map[label] + if num is not None: + data = self.custom_column_num_map[num] + table, lt = self.custom_table_names(data['num']) + self.conn.execute('UPDATE %s SET value=? WHERE id=?'%table, (new_name, id)) + self.conn.commit() + + def delete_custom_item_using_id(self, id, label=None, num=None): + if id: + if label is not None: + data = self.custom_column_label_map[label] + if num is not None: + data = self.custom_column_num_map[num] + table, lt = self.custom_table_names(data['num']) + self.conn.execute('DELETE FROM %s WHERE value=?'%lt, (id,)) + self.conn.execute('DELETE FROM %s WHERE id=?'%table, (id,)) + self.conn.commit() + # end convenience methods + def all_custom(self, label=None, num=None): if label is not None: data = self.custom_column_label_map[label] From eb205aee6f8eb67fbd26ffef7fb660fe6b4189b2 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Thu, 3 Jun 2010 19:25:23 +0100 Subject: [PATCH 7/7] A few changes to improve robustness. Also some cosmetic changes. --- src/calibre/gui2/dialogs/tag_list_editor.py | 9 ++++++--- src/calibre/gui2/tag_view.py | 18 ++++++++++++++---- src/calibre/library/custom_columns.py | 6 +++--- src/calibre/library/database2.py | 8 ++++++-- 4 files changed, 29 insertions(+), 12 deletions(-) diff --git a/src/calibre/gui2/dialogs/tag_list_editor.py b/src/calibre/gui2/dialogs/tag_list_editor.py index fa05518992..1ec80f4b4a 100644 --- a/src/calibre/gui2/dialogs/tag_list_editor.py +++ b/src/calibre/gui2/dialogs/tag_list_editor.py @@ -31,9 +31,12 @@ class TagListEditor(QDialog, Ui_TagListEditor): result = db.get_publishers_with_ids() compare = (lambda x,y:cmp(x.lower(), y.lower())) else: # should be a custom field - self.cc_label = db.field_metadata[category]['label'] - print 'here', self.cc_label - result = self.db.get_custom_items_with_ids(label=self.cc_label) + self.cc_label = None + if category in db.field_metadata: + self.cc_label = db.field_metadata[category]['label'] + result = self.db.get_custom_items_with_ids(label=self.cc_label) + else: + result = [] compare = (lambda x,y:cmp(x.lower(), y.lower())) for k,v in result: diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index e9e5228dfc..49ed03945f 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -135,8 +135,12 @@ class TagsView(QTreeView): # {{{ if item.type == TagTreeItem.CATEGORY: category = unicode(item.name.toString()) key = item.category_key + # Verify that we are working with a field that we know something about + if key not in self.db.field_metadata: + return True + self.context_menu = QMenu(self) - # If the user right-clicked on a tag/series/publisher, then offer + # If the user right-clicked on an editable item, then offer # the possibility of renaming that item if tag_name and \ (key in ['authors', 'tags', 'series', 'publisher', 'search'] or \ @@ -378,7 +382,7 @@ class TagsModel(QAbstractItemModel): # {{{ if category in data: # They should always be there, but ... # make a map of sets of names per category for duplicate # checking when editing - self.category_items[category] = [tag.name for tag in data[category]] + self.category_items[category] = set([tag.name for tag in data[category]]) self.row_map.append(category) self.categories.append(tb_categories[category]['name']) @@ -425,13 +429,16 @@ class TagsModel(QAbstractItemModel): # {{{ error_dialog(self.tags_view, _('Item is blank'), _('An item cannot be set to nothing. Delete it instead.')).exec_() return False - item = index.internalPointer() key = item.parent.category_key + # make certain we know about the category + if key not in self.db.field_metadata: + return if val in self.category_items[key]: error_dialog(self.tags_view, 'Duplicate item', _('The name %s is already used.')%val).exec_() return False + oldval = item.tag.name if key == 'search': saved_searches.rename(unicode(item.data(role).toString()), val) self.tags_view.search_item_renamed.emit() @@ -449,7 +456,10 @@ class TagsModel(QAbstractItemModel): # {{{ label=self.db.field_metadata[key]['label']) self.tags_view.tag_item_renamed.emit() item.tag.name = val - self.refresh() + self.dataChanged.emit(index, index) + # replace the old value in the duplicate detection map with the new one + self.category_items[key].discard(oldval) + self.category_items[key].add(val) return True def headerData(self, *args): diff --git a/src/calibre/library/custom_columns.py b/src/calibre/library/custom_columns.py index 64a261d935..4d2c8970b6 100644 --- a/src/calibre/library/custom_columns.py +++ b/src/calibre/library/custom_columns.py @@ -177,7 +177,7 @@ class CustomColumns(object): data = self.custom_column_label_map[label] if num is not None: data = self.custom_column_num_map[num] - table, lt = self.custom_table_names(data['num']) + table,lt = self.custom_table_names(data['num']) if not data['normalized']: return [] ans = self.conn.get('SELECT id, value FROM %s'%table) @@ -189,7 +189,7 @@ class CustomColumns(object): data = self.custom_column_label_map[label] if num is not None: data = self.custom_column_num_map[num] - table, lt = self.custom_table_names(data['num']) + table,lt = self.custom_table_names(data['num']) self.conn.execute('UPDATE %s SET value=? WHERE id=?'%table, (new_name, id)) self.conn.commit() @@ -199,7 +199,7 @@ class CustomColumns(object): data = self.custom_column_label_map[label] if num is not None: data = self.custom_column_num_map[num] - table, lt = self.custom_table_names(data['num']) + table,lt = self.custom_table_names(data['num']) self.conn.execute('DELETE FROM %s WHERE value=?'%lt, (id,)) self.conn.execute('DELETE FROM %s WHERE id=?'%table, (id,)) self.conn.commit() diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 860859061c..e7bb5c2060 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -986,6 +986,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): self.notify('metadata', [id]) # Convenience methods for tags_list_editor + # Note: we generally do not need to refresh_ids because library_view will + # refresh everything. def get_tags_with_ids(self): result = self.conn.get('SELECT id,name FROM tags') if not result: @@ -1017,11 +1019,11 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): def delete_series_using_id(self, id): if id: books = self.conn.get('SELECT book from books_series_link WHERE series=?', (id,)) - for (book_id,) in books: - self.conn.execute('UPDATE books SET series_index=1.0 WHERE id=?', (book_id,)) self.conn.execute('DELETE FROM books_series_link WHERE series=?', (id,)) self.conn.execute('DELETE FROM series WHERE id=?', (id,)) self.conn.commit() + for (book_id,) in books: + self.conn.execute('UPDATE books SET series_index=1.0 WHERE id=?', (book_id,)) def get_publishers_with_ids(self): result = self.conn.get('SELECT id,name FROM publishers') @@ -1040,6 +1042,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): self.conn.execute('DELETE FROM publishers WHERE id=?', (id,)) self.conn.commit() + # There is no editor for author, so we do not need get_authors_with_ids or + # delete_author_using_id. def rename_author(self, id, new_name): if id: # Make sure that any commas in new_name are changed to '|'!