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={})) default_value={}))
mi.application_id = book_id mi.application_id = book_id
mi.id = book_id mi.id = book_id
composites = []
for key, meta in self.field_metadata.custom_iteritems(): for key, meta in self.field_metadata.custom_iteritems():
mi.set_user_metadata(key, meta) mi.set_user_metadata(key, meta)
if meta['datatype'] == 'composite': if meta['datatype'] != 'composite':
composites.append(key) # composites are evaluated on demand in metadata.book.base
else: # because their value is None
val = self._field_for(key, book_id) val = self._field_for(key, book_id)
if isinstance(val, tuple): if isinstance(val, tuple):
val = list(val) val = list(val)
extra = self._field_for(key+'_index', book_id) extra = self._field_for(key+'_index', book_id)
mi.set(key, val=val, extra=extra) 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) 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']: if field in _data['user_metadata']:
d = _data['user_metadata'][field] d = _data['user_metadata'][field]
val = d['#value#'] val = d['#value#']
if d['datatype'] != 'composite': if val is None and d['datatype'] == 'composite':
return val
if val is None:
d['#value#'] = 'RECURSIVE_COMPOSITE FIELD (Metadata) ' + field d['#value#'] = 'RECURSIVE_COMPOSITE FIELD (Metadata) ' + field
val = d['#value#'] = self.formatter.safe_format( val = d['#value#'] = self.formatter.safe_format(
d['display']['composite_template'], d['display']['composite_template'],
@ -191,8 +189,9 @@ class Metadata:
langs = [val] langs = [val]
_data['languages'] = langs _data['languages'] = langs
elif field in _data['user_metadata']: elif field in _data['user_metadata']:
_data['user_metadata'][field]['#value#'] = val d = _data['user_metadata'][field]
_data['user_metadata'][field]['#extra#'] = extra d['#value#'] = val
d['#extra#'] = extra
else: else:
# You are allowed to stick arbitrary attributes onto this object as # You are allowed to stick arbitrary attributes onto this object as
# long as they don't conflict with global or user metadata names # long as they don't conflict with global or user metadata names
@ -205,11 +204,24 @@ class Metadata:
def has_key(self, key): def has_key(self, key):
return key in object.__getattribute__(self, '_data') 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)): def deepcopy(self, class_generator=lambda : Metadata(None)):
''' Do not use this method unless you know what you are doing, if you ''' 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` want to create a simple clone of this object, use :meth:`deepcopy_metadata`
instead. Class_generator must be a function that returns an instance instead. Class_generator must be a function that returns an instance
of Metadata or a subclass of it.''' 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() m = class_generator()
if not isinstance(m, Metadata): if not isinstance(m, Metadata):
return None return None
@ -217,6 +229,8 @@ class Metadata:
return m return m
def deepcopy_metadata(self): 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) m = Metadata(None)
object.__setattr__(m, '_data', copy.deepcopy(object.__getattribute__(self, '_data'))) object.__setattr__(m, '_data', copy.deepcopy(object.__getattribute__(self, '_data')))
return m return m
@ -228,6 +242,8 @@ class Metadata:
return default return default
def get_extra(self, field, default=None): 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') _data = object.__getattribute__(self, '_data')
if field in _data['user_metadata']: if field in _data['user_metadata']:
try: try:
@ -247,8 +263,7 @@ class Metadata:
needed is large. Also, we don't want any manipulations of the returned needed is large. Also, we don't want any manipulations of the returned
dict to show up in the book. dict to show up in the book.
''' '''
ans = object.__getattribute__(self, ans = object.__getattribute__(self, '_data')['identifiers']
'_data')['identifiers']
if not ans: if not ans:
ans = {} ans = {}
return copy.deepcopy(ans) return copy.deepcopy(ans)
@ -273,16 +288,14 @@ class Metadata:
typ, val = self._clean_identifier(typ, val) typ, val = self._clean_identifier(typ, val)
if not typ: if not typ:
return return
identifiers = object.__getattribute__(self, identifiers = object.__getattribute__(self, '_data')['identifiers']
'_data')['identifiers']
identifiers.pop(typ, None) identifiers.pop(typ, None)
if val: if val:
identifiers[typ] = val identifiers[typ] = val
def has_identifier(self, typ): def has_identifier(self, typ):
identifiers = object.__getattribute__(self, identifiers = object.__getattribute__(self, '_data')['identifiers']
'_data')['identifiers']
return typ in identifiers return typ in identifiers
# field-oriented interface. Intended to be the same as in LibraryDatabase # 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 return a dict containing all the custom field metadata associated with
the book. 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') _data = object.__getattribute__(self, '_data')
user_metadata = _data['user_metadata'] user_metadata = _data['user_metadata']
if not make_copy: if not make_copy:
@ -388,9 +404,12 @@ class Metadata:
None. field is the key name, not the label. Return a copy if requested, 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. just in case the user wants to change values in the dict.
''' '''
_data = object.__getattribute__(self, '_data') _data = object.__getattribute__(self, '_data')['user_metadata']
_data = _data['user_metadata']
if field in _data: 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: if make_copy:
return copy.deepcopy(_data[field]) return copy.deepcopy(_data[field])
return _data[field] return _data[field]

View File

@ -4,6 +4,8 @@
import textwrap import textwrap
from enum import IntEnum from enum import IntEnum
from functools import partial
from qt.core import ( from qt.core import (
QAction, QApplication, QBrush, QCheckBox, QDialog, QDialogButtonBox, QGridLayout, QAction, QApplication, QBrush, QCheckBox, QDialog, QDialogButtonBox, QGridLayout,
QHBoxLayout, QIcon, QKeySequence, QLabel, QListView, QModelIndex, QPalette, QPixmap, 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.gui2.widgets2 import Dialog, HTMLDisplay
from calibre.startup import connect_lambda from calibre.startup import connect_lambda
BOOK_DETAILS_DISPLAY_DELAY = 250 # 250ms is arbitrary
class Cover(CoverView): class Cover(CoverView):
@ -221,6 +225,7 @@ class BookInfo(QDialog):
self.path_to_book = None self.path_to_book = None
self.current_row = None self.current_row = None
self.slave_connected = False self.slave_connected = False
self.slave_debounce_timer = QTimer()
if library_path is not None: if library_path is not None:
self.view = None self.view = None
db = get_gui().library_broker.get_library(library_path) db = get_gui().library_broker.get_library(library_path)
@ -319,6 +324,7 @@ class BookInfo(QDialog):
ret = QDialog.done(self, r) ret = QDialog.done(self, r)
if self.slave_connected: if self.slave_connected:
self.view.model().new_bookdisplay_data.disconnect(self.slave) 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.view = self.link_delegate = self.gui = None
self.closed.emit(self) self.closed.emit(self)
return ret return ret
@ -343,6 +349,13 @@ class BookInfo(QDialog):
QTimer.singleShot(1, self.resize_cover) QTimer.singleShot(1, self.resize_cover)
def slave(self, mi): 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) self.refresh(mi.row_number, mi)
def move(self, delta=1): def move(self, delta=1):

View File

@ -13,7 +13,7 @@ from qt.core import (
QAbstractItemView, QDialog, QDialogButtonBox, QDrag, QEvent, QFont, QFontMetrics, QAbstractItemView, QDialog, QDialogButtonBox, QDrag, QEvent, QFont, QFontMetrics,
QGridLayout, QHeaderView, QIcon, QItemSelection, QItemSelectionModel, QLabel, QMenu, QGridLayout, QHeaderView, QIcon, QItemSelection, QItemSelectionModel, QLabel, QMenu,
QMimeData, QModelIndex, QPoint, QPushButton, QSize, QSpinBox, QStyle, QMimeData, QModelIndex, QPoint, QPushButton, QSize, QSpinBox, QStyle,
QStyleOptionHeader, Qt, QTableView, QUrl, pyqtSignal, QStyleOptionHeader, Qt, QTableView, QTimer, QUrl, pyqtSignal,
) )
from calibre import force_unicode from calibre import force_unicode
@ -1615,7 +1615,16 @@ class BooksView(QTableView): # {{{
self._model.search_done.connect(self.alternate_views.restore_current_book_state) self._model.search_done.connect(self.alternate_views.restore_current_book_state)
def connect_to_book_display(self, bd): 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): def search_done(self, ok):
self._search_done(self, ok) self._search_done(self, ok)