diff --git a/src/calibre/devices/prs505/sony_cache.py b/src/calibre/devices/prs505/sony_cache.py index 289147482c..9bd005d71c 100644 --- a/src/calibre/devices/prs505/sony_cache.py +++ b/src/calibre/devices/prs505/sony_cache.py @@ -6,7 +6,6 @@ __copyright__ = '2010, Kovid Goyal ' __docformat__ = 'restructuredtext en' import os, time -from pprint import pprint from base64 import b64decode from uuid import uuid4 from lxml import etree @@ -179,8 +178,8 @@ class XMLCache(object): path = record.get('path', None) if path: if path not in playlist_map: - playlist_map[path] = set() - playlist_map[path].add(title) + playlist_map[path] = [] + playlist_map[path].append(title) debug_print('Finish build_id_playlist_map. Found', len(playlist_map)) return playlist_map @@ -309,14 +308,14 @@ class XMLCache(object): book.thumbnail = raw break break - book.device_collections = list(playlist_map.get(book.lpath, set())) + book.device_collections = playlist_map.get(book.lpath, []) debug_print('Finished updating JSON cache:', bl_index) # }}} # Update XML from JSON {{{ def update(self, booklists, collections_attributes): - debug_print('In update. Starting update XML from JSON') + debug_print('Starting update', collections_attributes) for i, booklist in booklists.items(): playlist_map = self.build_id_playlist_map(i) debug_print('Updating XML Cache:', i) @@ -332,8 +331,7 @@ class XMLCache(object): # this book if book.device_collections is None: book.device_collections = [] - book.device_collections = list(set(book.device_collections) | - playlist_map.get(book.lpath, set())) + book.device_collections = playlist_map.get(book.lpath, []) self.update_playlists(i, root, booklist, collections_attributes) # Update the device collections because update playlist could have added # some new ones. @@ -341,10 +339,9 @@ class XMLCache(object): for i, booklist in booklists.items(): playlist_map = self.build_id_playlist_map(i) for book in booklist: - book.device_collections = list(set(book.device_collections) | - playlist_map.get(book.lpath, set())) + book.device_collections = playlist_map.get(book.lpath, []) self.fix_ids() - debug_print('Finished update XML from JSON') + debug_print('Finished update') def rebuild_collections(self, booklist, bl_index): if bl_index not in self.record_roots: diff --git a/src/calibre/devices/usbms/books.py b/src/calibre/devices/usbms/books.py index 996ce683c2..7108fa3f00 100644 --- a/src/calibre/devices/usbms/books.py +++ b/src/calibre/devices/usbms/books.py @@ -11,6 +11,7 @@ from calibre.devices.mime import mime_type_ext from calibre.devices.interface import BookList as _BookList from calibre.constants import filesystem_encoding, preferred_encoding from calibre import isbytestring +from calibre.utils.config import prefs class Book(MetaInformation): @@ -76,7 +77,7 @@ class Book(MetaInformation): in C{other} takes precedence, unless the information in C{other} is NULL. ''' - MetaInformation.smart_update(self, other) + MetaInformation.smart_update(self, other, replace_tags=True) for attr in self.BOOK_ATTRS: if hasattr(other, attr): @@ -132,7 +133,9 @@ class CollectionsBookList(BookList): def get_collections(self, collection_attributes): collections = {} series_categories = set([]) - collection_attributes = list(collection_attributes)+['device_collections'] + collection_attributes = list(collection_attributes) + if prefs['preserve_user_collections']: + collection_attributes += ['device_collections'] for attr in collection_attributes: attr = attr.strip() for book in self: diff --git a/src/calibre/ebooks/metadata/__init__.py b/src/calibre/ebooks/metadata/__init__.py index 4d126fda9d..0dbffd5f7f 100644 --- a/src/calibre/ebooks/metadata/__init__.py +++ b/src/calibre/ebooks/metadata/__init__.py @@ -268,7 +268,7 @@ class MetaInformation(object): ): prints(x, getattr(self, x, 'None')) - def smart_update(self, mi): + def smart_update(self, mi, replace_tags=False): ''' Merge the information in C{mi} into self. In case of conflicts, the information in C{mi} takes precedence, unless the information in mi is NULL. @@ -291,7 +291,10 @@ class MetaInformation(object): setattr(self, attr, val) if mi.tags: - self.tags += mi.tags + if replace_tags: + self.tags = mi.tags + else: + self.tags += mi.tags self.tags = list(set(self.tags)) if mi.author_sort_map: diff --git a/src/calibre/gui2/dialogs/config/add_save.py b/src/calibre/gui2/dialogs/config/add_save.py index aff995d84f..b1f5621f44 100644 --- a/src/calibre/gui2/dialogs/config/add_save.py +++ b/src/calibre/gui2/dialogs/config/add_save.py @@ -45,6 +45,7 @@ class AddSave(QTabWidget, Ui_TabWidget): self.metadata_box.layout().insertWidget(0, self.filename_pattern) self.opt_swap_author_names.setChecked(prefs['swap_author_names']) self.opt_add_formats_to_existing.setChecked(prefs['add_formats_to_existing']) + self.preserve_user_collections.setChecked(prefs['preserve_user_collections']) help = '\n'.join(textwrap.wrap(c.get_option('template').help, 75)) self.save_template.initialize('save_to_disk', opts.template, help) self.send_template.initialize('send_to_device', opts.send_template, help) @@ -71,6 +72,7 @@ class AddSave(QTabWidget, Ui_TabWidget): prefs['filename_pattern'] = pattern prefs['swap_author_names'] = bool(self.opt_swap_author_names.isChecked()) prefs['add_formats_to_existing'] = bool(self.opt_add_formats_to_existing.isChecked()) + prefs['preserve_user_collections'] = bool(self.preserve_user_collections.isChecked()) return True diff --git a/src/calibre/gui2/dialogs/config/add_save.ui b/src/calibre/gui2/dialogs/config/add_save.ui index 7fda2dbc7f..a29c0fd2e6 100644 --- a/src/calibre/gui2/dialogs/config/add_save.ui +++ b/src/calibre/gui2/dialogs/config/add_save.ui @@ -51,7 +51,7 @@ - If an existing book with a similar title and author is found that does not have the format being added, the format is added + If an existing book with a similar title and author is found that does not have the format being added, the format is added to the existing book, instead of creating a new entry. If the existing book already has the format, then it is silently ignored. Title match ignores leading indefinite articles ("the", "a", "an"), punctuation, case, etc. Author match is exact. @@ -179,7 +179,31 @@ Title match ignores leading indefinite articles ("the", "a", - + + + Preserve user collections. + + + + + + + If checked, collections will not be deleted even if a book with changed metadata is resent and the collection is not in the book's metadata. In addition, editing collections on the device view will be enabled. + + + true + + + + + + + + + + + + Here you can control how calibre will save your books when you click the Send to Device button. This setting can be overriden for individual devices by customizing the device interface plugins in Preferences->Plugins diff --git a/src/calibre/gui2/dialogs/tag_list_editor.py b/src/calibre/gui2/dialogs/tag_list_editor.py index 9cf95f2b62..6d91ed2ee2 100644 --- a/src/calibre/gui2/dialogs/tag_list_editor.py +++ b/src/calibre/gui2/dialogs/tag_list_editor.py @@ -7,6 +7,36 @@ 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 +class ListWidgetItem(QListWidgetItem): + + def __init__(self, txt): + QListWidgetItem.__init__(self, txt) + self.old_value = txt + self.cur_value = txt + + def data(self, role): + if role == Qt.DisplayRole: + if self.old_value != self.cur_value: + return _('%s (was %s)'%(self.cur_value, self.old_value)) + else: + return self.cur_value + elif role == Qt.EditRole: + return self.cur_value + else: + return QListWidgetItem.data(self, role) + + def setData(self, role, data): + if role == Qt.EditRole: + self.cur_value = data.toString() + QListWidgetItem.setData(self, role, data) + + def text(self): + return self.cur_value + + def setText(self, txt): + self.cur_value = txt + QListWidgetItem.setText(txt) + class TagListEditor(QDialog, Ui_TagListEditor): def __init__(self, window, tag_to_match, data, compare): @@ -21,7 +51,7 @@ class TagListEditor(QDialog, Ui_TagListEditor): for k,v in data: self.all_tags[v] = k for tag in sorted(self.all_tags.keys(), cmp=compare): - item = QListWidgetItem(tag) + item = ListWidgetItem(tag) item.setData(Qt.UserRole, self.all_tags[tag]) self.available_tags.addItem(item) diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py index d8b85fcd0d..fcbcf043fc 100644 --- a/src/calibre/gui2/library/models.py +++ b/src/calibre/gui2/library/models.py @@ -16,7 +16,7 @@ from calibre.gui2 import NONE, config, UNDEFINED_QDATE from calibre.utils.pyparsing import ParseException from calibre.ebooks.metadata import fmt_sidx, authors_to_string, string_to_authors from calibre.ptempfile import PersistentTemporaryFile -from calibre.utils.config import tweaks +from calibre.utils.config import tweaks, prefs from calibre.utils.date import dt_factory, qt_to_dt, isoformat from calibre.ebooks.metadata.meta import set_metadata as _set_metadata from calibre.utils.search_query_parser import SearchQueryParser @@ -928,11 +928,12 @@ class DeviceBooksModel(BooksModel): # {{{ if index.isValid() and self.editable: cname = self.column_map[index.column()] if cname in ('title', 'authors') or \ - (cname == 'collections' and self.db.supports_collections()): + (cname == 'collections' and \ + self.db.supports_collections() and \ + prefs['preserve_user_collections']): flags |= Qt.ItemIsEditable return flags - def search(self, text, reset=True): if not text or not text.strip(): self.map = list(range(len(self.db))) diff --git a/src/calibre/gui2/library/views.py b/src/calibre/gui2/library/views.py index 19daa1353c..09c1f8478b 100644 --- a/src/calibre/gui2/library/views.py +++ b/src/calibre/gui2/library/views.py @@ -15,7 +15,7 @@ from calibre.gui2.library.delegates import RatingDelegate, PubDateDelegate, \ TextDelegate, DateDelegate, TagsDelegate, CcTextDelegate, \ CcBoolDelegate, CcCommentsDelegate, CcDateDelegate from calibre.gui2.library.models import BooksModel, DeviceBooksModel -from calibre.utils.config import tweaks +from calibre.utils.config import tweaks, prefs from calibre.gui2 import error_dialog, gprefs from calibre.gui2.library import DEFAULT_SORT @@ -500,7 +500,9 @@ class DeviceBooksView(BooksView): # {{{ self.setAcceptDrops(False) def contextMenuEvent(self, event): - self.edit_collections_menu.setVisible(self._model.db.supports_collections()) + self.edit_collections_menu.setVisible( + self._model.db.supports_collections() and \ + prefs['preserve_user_collections']) self.context_menu.popup(event.globalPos()) event.accept() diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index 6452890883..590329ec13 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -473,6 +473,8 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, # {{{ self.search_restriction.setEnabled(False) for action in list(self.delete_menu.actions())[1:]: action.setEnabled(False) + # Reset the view in case something changed while it was invisible + self.current_view().reset() self.set_number_of_books_shown() diff --git a/src/calibre/utils/config.py b/src/calibre/utils/config.py index 69eee4d1ed..f24a6d2e30 100644 --- a/src/calibre/utils/config.py +++ b/src/calibre/utils/config.py @@ -698,6 +698,8 @@ def _prefs(): # calibre server can execute searches c.add_opt('saved_searches', default={}, help=_('List of named saved searches')) c.add_opt('user_categories', default={}, help=_('User-created tag browser categories')) + c.add_opt('preserve_user_collections', default=True, + help=_('Preserve all collections even if not in library metadata.')) c.add_opt('migrated', default=False, help='For Internal use. Don\'t modify.') return c