mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
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:
parent
57da1f5320
commit
642d44abbf
@ -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)
|
||||||
|
|
||||||
|
@ -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]
|
||||||
|
@ -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):
|
||||||
|
@ -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)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user