Another try at debouncing book details.

1) The debouncing is done where the display is called: views.py and book_details.py.
2) The 250ms timeout defined in book_info.py is arbitrary. I like it but some might want it smaller.
3) This commit implements delayed evaluation of composites when using get_metadata().
This commit is contained in:
Charles Haley 2024-03-04 15:23:00 +00:00
parent 57da1f5320
commit 642d44abbf
4 changed files with 59 additions and 21 deletions

View File

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

View File

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

View File

@ -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):

View File

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