diff --git a/src/calibre/db/cache.py b/src/calibre/db/cache.py index 83a039c5f2..c4cb1036dd 100644 --- a/src/calibre/db/cache.py +++ b/src/calibre/db/cache.py @@ -394,19 +394,16 @@ class Cache: default_value={})) mi.application_id = book_id mi.id = book_id - composites = [] for key, meta in self.field_metadata.custom_iteritems(): mi.set_user_metadata(key, meta) - if meta['datatype'] == 'composite': - composites.append(key) - else: + if meta['datatype'] != 'composite': + # composites are evaluated on demand in metadata.book.base + # because their value is None val = self._field_for(key, book_id) if isinstance(val, tuple): val = list(val) extra = self._field_for(key+'_index', book_id) mi.set(key, val=val, extra=extra) - for key in composites: - mi.set(key, val=self._composite_for(key, book_id, mi)) mi.link_maps = self._get_all_link_maps_for_book(book_id) diff --git a/src/calibre/ebooks/metadata/book/base.py b/src/calibre/ebooks/metadata/book/base.py index b28ed647ef..b37756bb8b 100644 --- a/src/calibre/ebooks/metadata/book/base.py +++ b/src/calibre/ebooks/metadata/book/base.py @@ -150,9 +150,7 @@ class Metadata: if field in _data['user_metadata']: d = _data['user_metadata'][field] val = d['#value#'] - if d['datatype'] != 'composite': - return val - if val is None: + if val is None and d['datatype'] == 'composite': d['#value#'] = 'RECURSIVE_COMPOSITE FIELD (Metadata) ' + field val = d['#value#'] = self.formatter.safe_format( d['display']['composite_template'], @@ -191,8 +189,9 @@ class Metadata: langs = [val] _data['languages'] = langs elif field in _data['user_metadata']: - _data['user_metadata'][field]['#value#'] = val - _data['user_metadata'][field]['#extra#'] = extra + d = _data['user_metadata'][field] + d['#value#'] = val + d['#extra#'] = extra else: # You are allowed to stick arbitrary attributes onto this object as # long as they don't conflict with global or user metadata names @@ -205,11 +204,24 @@ class Metadata: def has_key(self, key): return key in object.__getattribute__(self, '_data') + def _evaluate_all_composites(self): + custom_fields = object.__getattribute__(self, '_data')['user_metadata'] + for field in custom_fields: + self._evaluate_composite(field) + + def _evaluate_composite(self, field): + f = object.__getattribute__(self, '_data')['user_metadata'].get(field, None) + if f is not None: + if f['datatype'] == 'composite' and f['#value#'] is None: + self.get(field) + def deepcopy(self, class_generator=lambda : Metadata(None)): ''' Do not use this method unless you know what you are doing, if you want to create a simple clone of this object, use :meth:`deepcopy_metadata` instead. Class_generator must be a function that returns an instance of Metadata or a subclass of it.''' + # We don't need to evaluate all the composites here because we + # are returning a "real" Metadata instance that has __get_attribute__. m = class_generator() if not isinstance(m, Metadata): return None @@ -217,6 +229,8 @@ class Metadata: return m def deepcopy_metadata(self): + # We don't need to evaluate all the composites here because we + # are returning a "real" Metadata instance that has __get_attribute__. m = Metadata(None) object.__setattr__(m, '_data', copy.deepcopy(object.__getattribute__(self, '_data'))) return m @@ -228,6 +242,8 @@ class Metadata: return default def get_extra(self, field, default=None): + # Don't need to evaluate all composites because a composite can't have + # an extra value _data = object.__getattribute__(self, '_data') if field in _data['user_metadata']: try: @@ -247,8 +263,7 @@ class Metadata: needed is large. Also, we don't want any manipulations of the returned dict to show up in the book. ''' - ans = object.__getattribute__(self, - '_data')['identifiers'] + ans = object.__getattribute__(self, '_data')['identifiers'] if not ans: ans = {} return copy.deepcopy(ans) @@ -273,16 +288,14 @@ class Metadata: typ, val = self._clean_identifier(typ, val) if not typ: return - identifiers = object.__getattribute__(self, - '_data')['identifiers'] + identifiers = object.__getattribute__(self, '_data')['identifiers'] identifiers.pop(typ, None) if val: identifiers[typ] = val def has_identifier(self, typ): - identifiers = object.__getattribute__(self, - '_data')['identifiers'] + identifiers = object.__getattribute__(self, '_data')['identifiers'] return typ in identifiers # field-oriented interface. Intended to be the same as in LibraryDatabase @@ -373,6 +386,9 @@ class Metadata: return a dict containing all the custom field metadata associated with the book. ''' + # Must evaluate all composites because we are returning a dict, not a + # Metadata instance + self._evaluate_all_composites() _data = object.__getattribute__(self, '_data') user_metadata = _data['user_metadata'] if not make_copy: @@ -388,9 +404,12 @@ class Metadata: None. field is the key name, not the label. Return a copy if requested, just in case the user wants to change values in the dict. ''' - _data = object.__getattribute__(self, '_data') - _data = _data['user_metadata'] + _data = object.__getattribute__(self, '_data')['user_metadata'] if field in _data: + # Must evaluate the field because it might be a composite. It won't + # be evaluated on demand because we are returning its dict, not a + # Metadata instance + self._evaluate_composite(field) if make_copy: return copy.deepcopy(_data[field]) return _data[field] diff --git a/src/calibre/gui2/dialogs/book_info.py b/src/calibre/gui2/dialogs/book_info.py index 242ae683f3..221543d289 100644 --- a/src/calibre/gui2/dialogs/book_info.py +++ b/src/calibre/gui2/dialogs/book_info.py @@ -4,6 +4,8 @@ import textwrap from enum import IntEnum +from functools import partial + from qt.core import ( QAction, QApplication, QBrush, QCheckBox, QDialog, QDialogButtonBox, QGridLayout, QHBoxLayout, QIcon, QKeySequence, QLabel, QListView, QModelIndex, QPalette, QPixmap, @@ -22,6 +24,8 @@ from calibre.gui2.widgets import CoverView from calibre.gui2.widgets2 import Dialog, HTMLDisplay from calibre.startup import connect_lambda +BOOK_DETAILS_DISPLAY_DELAY = 250 # 250ms is arbitrary + class Cover(CoverView): @@ -221,6 +225,7 @@ class BookInfo(QDialog): self.path_to_book = None self.current_row = None self.slave_connected = False + self.slave_debounce_timer = QTimer() if library_path is not None: self.view = None db = get_gui().library_broker.get_library(library_path) @@ -319,6 +324,7 @@ class BookInfo(QDialog): ret = QDialog.done(self, r) if self.slave_connected: self.view.model().new_bookdisplay_data.disconnect(self.slave) + self.slave_debounce_timer.stop() # OK if it isn't running self.view = self.link_delegate = self.gui = None self.closed.emit(self) return ret @@ -343,6 +349,13 @@ class BookInfo(QDialog): QTimer.singleShot(1, self.resize_cover) def slave(self, mi): + self.slave_debounce_timer.stop() + t = self.book_display_info_timer = QTimer() + t.setSingleShot(True) + t.timeout.connect(partial(self._timed_slave, mi)) + t.start(BOOK_DETAILS_DISPLAY_DELAY) + + def _timed_slave(self, mi): self.refresh(mi.row_number, mi) def move(self, delta=1): diff --git a/src/calibre/gui2/library/views.py b/src/calibre/gui2/library/views.py index 7ab0f69761..881d7655c8 100644 --- a/src/calibre/gui2/library/views.py +++ b/src/calibre/gui2/library/views.py @@ -13,7 +13,7 @@ from qt.core import ( QAbstractItemView, QDialog, QDialogButtonBox, QDrag, QEvent, QFont, QFontMetrics, QGridLayout, QHeaderView, QIcon, QItemSelection, QItemSelectionModel, QLabel, QMenu, QMimeData, QModelIndex, QPoint, QPushButton, QSize, QSpinBox, QStyle, - QStyleOptionHeader, Qt, QTableView, QUrl, pyqtSignal, + QStyleOptionHeader, Qt, QTableView, QTimer, QUrl, pyqtSignal, ) from calibre import force_unicode @@ -1615,7 +1615,16 @@ class BooksView(QTableView): # {{{ self._model.search_done.connect(self.alternate_views.restore_current_book_state) def connect_to_book_display(self, bd): - self._model.new_bookdisplay_data.connect(bd) + self.connect_to_book_display_timer = QTimer() + self._model.new_bookdisplay_data.connect(partial(self._timed_connect_to_book_display, bd)) + + def _timed_connect_to_book_display(self, bd, data): + self.connect_to_book_display_timer.stop() + t = self.connect_to_book_display_timer = QTimer() + t.setSingleShot(True) + t.timeout.connect(partial(bd, data)) + from calibre.gui2.dialogs.book_info import BOOK_DETAILS_DISPLAY_DELAY + t.start(BOOK_DETAILS_DISPLAY_DELAY) def search_done(self, ok): self._search_done(self, ok)