mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-11-17 03:53:02 -05:00
867 lines
34 KiB
Python
867 lines
34 KiB
Python
#!/usr/bin/env python
|
|
|
|
|
|
__license__ = 'GPL v3'
|
|
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
|
__docformat__ = 'restructuredtext en'
|
|
|
|
|
|
import traceback
|
|
from functools import partial
|
|
|
|
from qt.core import (
|
|
Qt, QDialog, QAbstractItemView, QTableWidgetItem, QIcon, QListWidgetItem,
|
|
QCoreApplication, QEvent, QObject, QApplication, pyqtSignal, QByteArray, QMenu,
|
|
QShortcut, QTimer, QStyle)
|
|
|
|
from calibre.customize.ui import find_plugin
|
|
from calibre.gui2 import gprefs
|
|
from calibre.gui2.dialogs.quickview_ui import Ui_Quickview
|
|
from calibre.utils.date import timestampfromdt
|
|
from calibre.utils.icu import sort_key
|
|
from calibre.utils.iso8601 import UNDEFINED_DATE
|
|
|
|
|
|
class TableItem(QTableWidgetItem):
|
|
|
|
'''
|
|
A QTableWidgetItem that sorts on a separate string and uses ICU rules
|
|
'''
|
|
|
|
def __init__(self, getter=None):
|
|
self.val = ''
|
|
self.sort = None
|
|
self.sort_idx = 0
|
|
self.getter = getter
|
|
self.resolved = False
|
|
QTableWidgetItem.__init__(self, '')
|
|
self.setFlags(Qt.ItemFlag.ItemIsEnabled|Qt.ItemFlag.ItemIsSelectable)
|
|
|
|
def __ge__(self, other):
|
|
self.get_data()
|
|
other.get_data()
|
|
if self.sort is None:
|
|
if other.sort is None:
|
|
# None == None therefore >=
|
|
return True
|
|
# self is None, other is not None therefore self < other
|
|
return False
|
|
if other.sort is None:
|
|
# self is not None and other is None therefore self >= other
|
|
return True
|
|
|
|
if isinstance(self.sort, (bytes, str)):
|
|
l = sort_key(self.sort)
|
|
r = sort_key(other.sort)
|
|
else:
|
|
l = self.sort
|
|
r = other.sort
|
|
if l > r:
|
|
return 1
|
|
if l == r:
|
|
return self.sort_idx >= other.sort_idx
|
|
return 0
|
|
|
|
def __lt__(self, other):
|
|
self.get_data()
|
|
other.get_data()
|
|
if self.sort is None:
|
|
if other.sort is None:
|
|
# None == None therefore not <
|
|
return False
|
|
# self is None, other is not None therefore self < other
|
|
return True
|
|
if other.sort is None:
|
|
# self is not None therefore self > other
|
|
return False
|
|
|
|
if isinstance(self.sort, (bytes, str)):
|
|
l = sort_key(self.sort)
|
|
r = sort_key(other.sort)
|
|
else:
|
|
l = self.sort
|
|
r = other.sort
|
|
if l < r:
|
|
return 1
|
|
if l == r:
|
|
return self.sort_idx < other.sort_idx
|
|
return 0
|
|
|
|
def get_data(self):
|
|
if not self.resolved and self.getter:
|
|
self.resolved = True
|
|
self.val, self.sort, self.sort_idx = self.getter()
|
|
|
|
def data(self, role):
|
|
self.get_data()
|
|
if role == Qt.DisplayRole:
|
|
return self.val
|
|
return QTableWidgetItem.data(self, role)
|
|
|
|
|
|
IN_WIDGET_ITEMS = 0
|
|
IN_WIDGET_BOOKS = 1
|
|
IN_WIDGET_LOCK = 2
|
|
IN_WIDGET_DOCK = 3
|
|
IN_WIDGET_SEARCH = 4
|
|
IN_WIDGET_CLOSE = 5
|
|
|
|
|
|
class BooksTableFilter(QObject):
|
|
|
|
return_pressed_signal = pyqtSignal()
|
|
|
|
def eventFilter(self, obj, event):
|
|
if event.type() == QEvent.Type.KeyPress and event.key() == Qt.Key.Key_Return:
|
|
self.return_pressed_signal.emit()
|
|
return True
|
|
return False
|
|
|
|
|
|
class WidgetFocusFilter(QObject):
|
|
|
|
focus_entered_signal = pyqtSignal(object)
|
|
|
|
def eventFilter(self, obj, event):
|
|
if event.type() == QEvent.Type.FocusIn:
|
|
self.focus_entered_signal.emit(obj)
|
|
return False
|
|
|
|
|
|
class WidgetTabFilter(QObject):
|
|
|
|
def __init__(self, attach_to_Class, which_widget, tab_signal):
|
|
QObject.__init__(self, attach_to_Class)
|
|
self.tab_signal = tab_signal
|
|
self.which_widget = which_widget
|
|
|
|
def eventFilter(self, obj, event):
|
|
if event.type() == QEvent.Type.KeyPress:
|
|
if event.key() == Qt.Key.Key_Tab:
|
|
self.tab_signal.emit(self.which_widget, True)
|
|
return True
|
|
if event.key() == Qt.Key.Key_Backtab:
|
|
self.tab_signal.emit(self.which_widget, False)
|
|
return True
|
|
return False
|
|
|
|
|
|
class Quickview(QDialog, Ui_Quickview):
|
|
|
|
reopen_after_dock_change = pyqtSignal()
|
|
tab_pressed_signal = pyqtSignal(object, object)
|
|
quickview_closed = pyqtSignal()
|
|
|
|
def __init__(self, gui, row, toggle_shortcut):
|
|
self.is_pane = gprefs.get('quickview_is_pane', False)
|
|
|
|
if not self.is_pane:
|
|
QDialog.__init__(self, gui, flags=Qt.WindowType.Widget)
|
|
else:
|
|
QDialog.__init__(self, gui)
|
|
Ui_Quickview.__init__(self)
|
|
self.setupUi(self)
|
|
self.isClosed = False
|
|
self.current_book = None
|
|
self.closed_by_button = False
|
|
|
|
if self.is_pane:
|
|
self.main_grid_layout.setContentsMargins(0, 0, 0, 0)
|
|
else:
|
|
self.setWindowIcon(self.windowIcon())
|
|
|
|
self.books_table_column_widths = None
|
|
try:
|
|
self.books_table_column_widths = \
|
|
gprefs.get('quickview_dialog_books_table_widths', None)
|
|
if not self.is_pane:
|
|
geom = gprefs.get('quickview_dialog_geometry', None)
|
|
if geom:
|
|
QApplication.instance().safe_restore_geometry(self, QByteArray(geom))
|
|
except:
|
|
pass
|
|
|
|
self.view = gui.library_view
|
|
self.db = self.view.model().db
|
|
self.gui = gui
|
|
self.is_closed = False
|
|
self.current_book_id = None # the db id of the book used to fill the lh pane
|
|
self.current_column = None # current logical column in books list
|
|
self.current_key = None # current lookup key in books list
|
|
self.last_search = None
|
|
self.no_valid_items = False
|
|
self.follow_library_view = True
|
|
|
|
self.apply_vls.setCheckState(Qt.CheckState.Checked if gprefs['qv_respects_vls']
|
|
else Qt.CheckState.Unchecked)
|
|
self.apply_vls.stateChanged.connect(self.vl_box_changed)
|
|
|
|
self.fm = self.db.field_metadata
|
|
|
|
self.items.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
|
|
self.items.currentTextChanged.connect(self.item_selected)
|
|
self.items.setProperty('highlight_current_item', 150)
|
|
self.items.itemDoubleClicked.connect(self.item_doubleclicked)
|
|
self.items.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
|
self.items.customContextMenuRequested.connect(self.show_item_context_menu)
|
|
|
|
focus_filter = WidgetFocusFilter(self.items)
|
|
focus_filter.focus_entered_signal.connect(self.focus_entered)
|
|
self.items.installEventFilter(focus_filter)
|
|
|
|
self.tab_pressed_signal.connect(self.tab_pressed)
|
|
return_filter = BooksTableFilter(self.books_table)
|
|
return_filter.return_pressed_signal.connect(self.return_pressed)
|
|
self.books_table.installEventFilter(return_filter)
|
|
|
|
focus_filter = WidgetFocusFilter(self.books_table)
|
|
focus_filter.focus_entered_signal.connect(self.focus_entered)
|
|
self.books_table.installEventFilter(focus_filter)
|
|
|
|
self.close_button.clicked.connect(self.close_button_clicked)
|
|
self.refresh_button.clicked.connect(self.refill)
|
|
|
|
self.tab_order_widgets = [self.items, self.books_table, self.lock_qv,
|
|
self.dock_button, self.refresh_button,
|
|
self.close_button]
|
|
for idx,widget in enumerate(self.tab_order_widgets):
|
|
widget.installEventFilter(WidgetTabFilter(widget, idx, self.tab_pressed_signal))
|
|
|
|
self.books_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
|
self.books_table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
|
|
self.books_table.setProperty('highlight_current_item', 150)
|
|
|
|
# Set up the books table columns
|
|
self.add_columns_to_widget()
|
|
|
|
self.books_table_header_height = self.books_table.height()
|
|
self.books_table.cellDoubleClicked.connect(self.book_doubleclicked)
|
|
self.books_table.currentCellChanged.connect(self.books_table_cell_changed)
|
|
self.books_table.cellClicked.connect(self.books_table_set_search_string)
|
|
self.books_table.cellActivated.connect(self.books_table_set_search_string)
|
|
self.books_table.sortByColumn(0, Qt.SortOrder.AscendingOrder)
|
|
|
|
# get the standard table row height. Do this here because calling
|
|
# resizeRowsToContents can word wrap long cell contents, creating
|
|
# double-high rows
|
|
self.books_table.setRowCount(1)
|
|
self.books_table.setItem(0, 0, TableItem())
|
|
self.books_table.resizeRowsToContents()
|
|
self.books_table_row_height = self.books_table.rowHeight(0)
|
|
self.books_table.setRowCount(0)
|
|
|
|
# Add the data
|
|
self.refresh(row)
|
|
|
|
self.slave_timers = [QTimer(self), QTimer(self), QTimer(self)]
|
|
self.view.clicked.connect(partial(self.delayed_slave, func=self.slave, dex=0))
|
|
self.view.selectionModel().currentColumnChanged.connect(
|
|
partial(self.delayed_slave, func=self.column_slave, dex=1))
|
|
QCoreApplication.instance().aboutToQuit.connect(self.save_state)
|
|
self.view.model().new_bookdisplay_data.connect(
|
|
partial(self.delayed_slave, func=self.book_was_changed, dex=2))
|
|
|
|
self.close_button.setDefault(False)
|
|
self.close_button_tooltip = _('The Quickview shortcut ({0}) shows/hides the Quickview panel')
|
|
self.refresh_button.setIcon(QIcon.ic('view-refresh.png'))
|
|
self.close_button.setIcon(self.style().standardIcon(QStyle.StandardPixmap.SP_DialogCloseButton))
|
|
if self.is_pane:
|
|
self.dock_button.setText(_('Undock'))
|
|
self.dock_button.setToolTip(_('Show the Quickview panel in its own floating window'))
|
|
self.dock_button.setIcon(QIcon(I('arrow-up.png')))
|
|
# Remove the ampersands from the buttons because shortcuts exist.
|
|
self.lock_qv.setText(_('Lock Quickview contents'))
|
|
self.refresh_button.setText(_('Refresh'))
|
|
self.gui.quickview_splitter.add_quickview_dialog(self)
|
|
self.close_button.setVisible(False)
|
|
else:
|
|
self.dock_button.setToolTip(_('Embed the Quickview panel into the main calibre window'))
|
|
self.dock_button.setIcon(QIcon(I('arrow-down.png')))
|
|
self.set_focus()
|
|
|
|
self.books_table.horizontalHeader().sectionResized.connect(self.section_resized)
|
|
self.dock_button.clicked.connect(self.show_as_pane_changed)
|
|
self.view.model().search_done.connect(self.check_for_no_items)
|
|
|
|
# Enable the refresh button only when QV is locked
|
|
self.refresh_button.setEnabled(False)
|
|
self.lock_qv.stateChanged.connect(self.lock_qv_changed)
|
|
|
|
self.view_icon = QIcon(I('view.png'))
|
|
self.view_plugin = self.gui.iactions['View']
|
|
self.edit_metadata_icon = QIcon(I('edit_input.png'))
|
|
self.quickview_icon = QIcon(I('quickview.png'))
|
|
self.select_book_icon = QIcon(I('library.png'))
|
|
self.search_icon = QIcon(I('search.png'))
|
|
self.books_table.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
|
self.books_table.customContextMenuRequested.connect(self.show_context_menu)
|
|
|
|
# Add the quickview toggle as a shortcut for the close button
|
|
# Don't add it if it identical to the current &X shortcut because that
|
|
# breaks &X
|
|
if (not self.is_pane and toggle_shortcut and
|
|
self.close_button.shortcut() != toggle_shortcut):
|
|
toggle_sc = QShortcut(toggle_shortcut, self.close_button)
|
|
toggle_sc.activated.connect(lambda: self.close_button_clicked())
|
|
toggle_sc.setEnabled(True)
|
|
self.close_button.setToolTip(_('Alternate shortcut: ') +
|
|
toggle_shortcut.toString())
|
|
|
|
def delayed_slave(self, current, func=None, dex=None):
|
|
self.slave_timers[dex].stop()
|
|
t = self.slave_timers[dex] = QTimer(self)
|
|
t.timeout.connect(partial(func, current))
|
|
t.setSingleShot(True)
|
|
t.setInterval(200)
|
|
t.start()
|
|
|
|
def item_doubleclicked(self, item):
|
|
tb = self.gui.stack.tb_widget
|
|
tb.set_focus_to_find_box()
|
|
tb.item_search.lineEdit().setText(self.current_key + ':=' + item.text())
|
|
tb.do_find()
|
|
|
|
def show_item_context_menu(self, point):
|
|
item = self.items.currentItem()
|
|
self.context_menu = QMenu(self)
|
|
self.context_menu.addAction(self.search_icon, _('Find item in the Tag browser'),
|
|
partial(self.item_doubleclicked, item))
|
|
self.context_menu.addAction(self.search_icon, _('Find item in the library'),
|
|
partial(self.do_search, follow_library_view=False))
|
|
self.context_menu.popup(self.items.mapToGlobal(point))
|
|
self.context_menu = QMenu(self)
|
|
|
|
def show_context_menu(self, point):
|
|
index = self.books_table.indexAt(point)
|
|
row = index.row()
|
|
column = index.column()
|
|
item = self.books_table.item(index.row(), 0)
|
|
if item is None:
|
|
return False
|
|
book_id = int(item.data(Qt.ItemDataRole.UserRole))
|
|
book_displayed = self.book_displayed_in_library_view(book_id)
|
|
m = self.context_menu = QMenu(self)
|
|
a = m.addAction(self.select_book_icon, _('Select this book in the library'),
|
|
partial(self.select_book, book_id))
|
|
a.setEnabled(book_displayed)
|
|
m.addAction(self.search_icon, _('Find item in the library'),
|
|
partial(self.do_search, follow_library_view=False))
|
|
a = m.addAction(self.edit_metadata_icon, _('Edit metadata'),
|
|
partial(self.edit_metadata, book_id, follow_library_view=False))
|
|
a.setEnabled(book_displayed)
|
|
a = m.addAction(self.quickview_icon, _('Quickview this cell'),
|
|
partial(self.quickview_item, row, column))
|
|
a.setEnabled(self.is_category(self.column_order[column]) and
|
|
book_displayed and not self.lock_qv.isChecked())
|
|
m.addSeparator()
|
|
m.addAction(self.view_icon, _('Open book in the E-book viewer'),
|
|
partial(self.view_plugin._view_calibre_books, [book_id]))
|
|
self.context_menu.popup(self.books_table.mapToGlobal(point))
|
|
return True
|
|
|
|
def lock_qv_changed(self, state):
|
|
self.refresh_button.setEnabled(state)
|
|
|
|
def add_columns_to_widget(self):
|
|
'''
|
|
Get the list of columns from the preferences. Clear the current table
|
|
and add the current column set
|
|
'''
|
|
self.column_order = [x[0] for x in get_qv_field_list(self.fm) if x[1]]
|
|
self.books_table.clear()
|
|
self.books_table.setRowCount(0)
|
|
self.books_table.setColumnCount(len(self.column_order))
|
|
for idx,col in enumerate(self.column_order):
|
|
t = QTableWidgetItem(self.fm[col]['name'])
|
|
self.books_table.setHorizontalHeaderItem(idx, t)
|
|
|
|
def refill(self):
|
|
'''
|
|
Refill the table in case the columns displayed changes
|
|
'''
|
|
self.add_columns_to_widget()
|
|
self.refresh(self.view.currentIndex(), ignore_lock=True)
|
|
|
|
def set_search_text(self, txt):
|
|
self.last_search = txt
|
|
|
|
def focus_entered(self, obj):
|
|
if obj == self.books_table:
|
|
self.books_table_set_search_string(self.books_table.currentRow(),
|
|
self.books_table.currentColumn())
|
|
elif obj.currentItem():
|
|
self.item_selected(obj.currentItem().text())
|
|
|
|
def books_table_cell_changed(self, cur_row, cur_col, prev_row, prev_col):
|
|
self.books_table_set_search_string(cur_row, cur_col)
|
|
|
|
def books_table_set_search_string(self, current_row, current_col):
|
|
'''
|
|
Given the contents of a cell, compute a search string that will find
|
|
that book and any others with identical contents in the cell.
|
|
'''
|
|
current = self.books_table.item(current_row, current_col)
|
|
if current is None:
|
|
return
|
|
book_id = current.data(Qt.ItemDataRole.UserRole)
|
|
|
|
if current is None:
|
|
return
|
|
col = self.column_order[current.column()]
|
|
if col == 'title':
|
|
self.set_search_text('title:="' + current.text().replace('"', '\\"') + '"')
|
|
elif col == 'authors':
|
|
authors = []
|
|
for aut in [t.strip() for t in current.text().split('&')]:
|
|
authors.append('authors:="' + aut.replace('"', '\\"') + '"')
|
|
self.set_search_text(' and '.join(authors))
|
|
elif self.fm[col]['datatype'] == 'series':
|
|
mi = self.db.get_metadata(book_id, index_is_id=True, get_user_categories=False)
|
|
t = mi.get(col)
|
|
if t:
|
|
self.set_search_text(col+ ':="' + t + '"')
|
|
else:
|
|
self.set_search_text(None)
|
|
else:
|
|
if self.fm[col]['is_multiple']:
|
|
items = [(col + ':"=' + v.strip() + '"') for v in
|
|
current.text().split(self.fm[col]['is_multiple']['ui_to_list'])]
|
|
self.set_search_text(' and '.join(items))
|
|
else:
|
|
self.set_search_text(col + ':"=' + current.text() + '"')
|
|
|
|
def tab_pressed(self, in_widget, isForward):
|
|
if isForward:
|
|
in_widget += 1
|
|
if in_widget >= len(self.tab_order_widgets):
|
|
in_widget = 0
|
|
else:
|
|
in_widget -= 1
|
|
if in_widget < 0:
|
|
in_widget = len(self.tab_order_widgets) - 1
|
|
self.tab_order_widgets[in_widget].setFocus(Qt.FocusReason.TabFocusReason)
|
|
|
|
def show(self):
|
|
QDialog.show(self)
|
|
if self.is_pane:
|
|
self.gui.quickview_splitter.show_quickview_widget()
|
|
|
|
def show_as_pane_changed(self):
|
|
gprefs['quickview_is_pane'] = not gprefs.get('quickview_is_pane', False)
|
|
self.reopen_after_dock_change.emit()
|
|
|
|
# search button
|
|
def do_search(self, follow_library_view=True):
|
|
if self.no_valid_items:
|
|
return
|
|
if self.last_search is not None:
|
|
try:
|
|
self.follow_library_view = follow_library_view
|
|
self.gui.search.set_search_string(self.last_search)
|
|
finally:
|
|
self.follow_library_view = True
|
|
|
|
def book_was_changed(self, mi):
|
|
'''
|
|
Called when book information is changed in the library view. Make that
|
|
book info current. This means that prev and next in edit metadata will move
|
|
the current book and change quickview
|
|
'''
|
|
if self.is_closed or self.current_column is None or not self.follow_library_view:
|
|
return
|
|
# There is an ordering problem when libraries are changed. The library
|
|
# view is changed, triggering a book_was_changed signal. Unfortunately
|
|
# this happens before the library_changed actions are run, meaning we
|
|
# still have the old database. To avoid the problem we just ignore the
|
|
# operation if we get an exception. The "close" will come
|
|
# eventually.
|
|
try:
|
|
self.refresh(self.view.model().index(self.db.row(mi.id), self.current_column))
|
|
except:
|
|
pass
|
|
|
|
# clicks on the items listWidget
|
|
def item_selected(self, txt):
|
|
if self.no_valid_items:
|
|
return
|
|
self.fill_in_books_box(str(txt))
|
|
self.set_search_text(self.current_key + ':"=' + txt.replace('"', '\\"') + '"')
|
|
|
|
def vl_box_changed(self):
|
|
gprefs['qv_respects_vls'] = self.apply_vls.isChecked()
|
|
self._refresh(self.current_book_id, self.current_key)
|
|
|
|
def refresh(self, idx, ignore_lock=False):
|
|
'''
|
|
Given a cell in the library view, display the information. This method
|
|
converts the index into the lookup key
|
|
'''
|
|
if (not ignore_lock and self.lock_qv.isChecked()):
|
|
return
|
|
if not idx.isValid():
|
|
from calibre.constants import DEBUG
|
|
if DEBUG:
|
|
from calibre import prints
|
|
prints('QuickView: current index is not valid')
|
|
return
|
|
|
|
try:
|
|
self.current_column = (
|
|
self.view.column_map.index('authors') if (
|
|
self.current_column is None and self.view.column_map[idx.column()] == 'title'
|
|
) else idx.column())
|
|
key = self.view.column_map[self.current_column]
|
|
book_id = self.view.model().id(idx.row())
|
|
if self.current_book_id == book_id and self.current_key == key:
|
|
return
|
|
self._refresh(book_id, key)
|
|
except:
|
|
traceback.print_exc()
|
|
self.indicate_no_items()
|
|
|
|
def is_category(self, key):
|
|
return key is not None and (self.fm[key]['is_category'] or
|
|
(self.fm[key]['datatype'] == 'composite' and
|
|
self.fm[key]['display'].get('make_category', False)))
|
|
|
|
def _refresh(self, book_id, key):
|
|
'''
|
|
Actually fill in the left-hand panel from the information in the
|
|
selected column of the selected book
|
|
'''
|
|
# Only show items for categories
|
|
if not self.is_category(key):
|
|
if self.current_key is None:
|
|
self.indicate_no_items()
|
|
return
|
|
key = self.current_key
|
|
label_text = _('&Item: {0} ({1})')
|
|
if self.is_pane:
|
|
label_text = label_text.replace('&', '')
|
|
|
|
self.items.blockSignals(True)
|
|
self.items.clear()
|
|
self.books_table.setRowCount(0)
|
|
|
|
mi = self.db.new_api.get_proxy_metadata(book_id)
|
|
vals = mi.get(key, None)
|
|
if self.fm[key]['datatype'] == 'composite' and self.fm[key]['is_multiple']:
|
|
sep = self.fm[key]['is_multiple'].get('cache_to_list', ',')
|
|
vals = [v.strip() for v in vals.split(sep) if v.strip()]
|
|
try:
|
|
# Check if we are in the GridView and there are no values for the
|
|
# selected column. In this case switch the column to 'authors'
|
|
# because there isn't an easy way to switch columns in GridView
|
|
# when the QV box is empty.
|
|
if not vals:
|
|
is_grid_view = (self.gui.current_view().alternate_views.current_view !=
|
|
self.gui.current_view().alternate_views.main_view)
|
|
if is_grid_view:
|
|
key = 'authors'
|
|
vals = mi.get(key, None)
|
|
except:
|
|
traceback.print_exc()
|
|
|
|
self.current_book_id = book_id
|
|
self.current_key = key
|
|
self.items_label.setText(label_text.format(self.fm[key]['name'], key))
|
|
|
|
if vals:
|
|
self.no_valid_items = False
|
|
if self.fm[key]['datatype'] == 'rating':
|
|
if self.fm[key]['display'].get('allow_half_stars', False):
|
|
vals = str(vals/2.0)
|
|
else:
|
|
vals = str(vals//2)
|
|
if not isinstance(vals, list):
|
|
vals = [vals]
|
|
vals.sort(key=sort_key)
|
|
|
|
for v in vals:
|
|
a = QListWidgetItem(v)
|
|
a.setToolTip(
|
|
'<p>' + _(
|
|
'Click to show only books with this item. '
|
|
'Double click to search for this item in the Tag browser') + '</p>')
|
|
self.items.addItem(a)
|
|
self.items.setCurrentRow(0)
|
|
|
|
self.fill_in_books_box(vals[0])
|
|
else:
|
|
self.indicate_no_items()
|
|
self.items.blockSignals(False)
|
|
|
|
def check_for_no_items(self):
|
|
if not self.is_closed and self.view.model().count() == 0:
|
|
self.indicate_no_items()
|
|
|
|
def indicate_no_items(self):
|
|
self.no_valid_items = True
|
|
self.items.clear()
|
|
self.add_columns_to_widget()
|
|
self.items.addItem(QListWidgetItem(_('**No items found**')))
|
|
self.books_label.setText(_('Click in a column in the library view '
|
|
'to see the information for that book'))
|
|
|
|
def fill_in_books_box(self, selected_item):
|
|
'''
|
|
Given the selected row in the left-hand box, fill in the grid with
|
|
the books that contain that data.
|
|
'''
|
|
# Do a bit of fix-up on the items so that the search works.
|
|
if selected_item.startswith('.'):
|
|
sv = '.' + selected_item
|
|
else:
|
|
sv = selected_item
|
|
sv = self.current_key + ':"=' + sv.replace('"', r'\"') + '"'
|
|
if self.apply_vls.isChecked():
|
|
books = self.db.search(sv, return_matches=True, sort_results=False)
|
|
else:
|
|
books = self.db.new_api.search(sv)
|
|
|
|
self.books_table.setRowCount(len(books))
|
|
label_text = _('&Books with selected item "{0}": {1}')
|
|
if self.is_pane:
|
|
label_text = label_text.replace('&', '')
|
|
self.books_label.setText(label_text.format(selected_item, len(books)))
|
|
|
|
select_item = None
|
|
self.books_table.setSortingEnabled(False)
|
|
self.books_table.blockSignals(True)
|
|
tt = ('<p>' + _(
|
|
'Double click on a book to change the selection in the library view or '
|
|
'change the column shown in the left-hand panel. '
|
|
'Shift- or Ctrl- double click to edit the metadata of a book, '
|
|
'which also changes the selected book.'
|
|
) + '</p>')
|
|
for row, b in enumerate(books):
|
|
for col in self.column_order:
|
|
a = TableItem(partial(self.get_item_data, b, col))
|
|
if col == 'title':
|
|
if b == self.current_book_id:
|
|
select_item = a
|
|
# The data is supplied on demand when the item is displayed
|
|
a.setData(Qt.ItemDataRole.UserRole, b)
|
|
a.setToolTip(tt)
|
|
self.books_table.setItem(row, self.key_to_table_widget_column(col), a)
|
|
self.books_table.setRowHeight(row, self.books_table_row_height)
|
|
self.books_table.blockSignals(False)
|
|
self.books_table.setSortingEnabled(True)
|
|
if select_item is not None:
|
|
self.books_table.setCurrentItem(select_item)
|
|
self.books_table.scrollToItem(select_item, QAbstractItemView.ScrollHint.PositionAtCenter)
|
|
self.set_search_text(sv)
|
|
|
|
def get_item_data(self, book_id, col):
|
|
mi = self.db.new_api.get_proxy_metadata(book_id)
|
|
try:
|
|
if col == 'title':
|
|
return (mi.title, mi.title_sort, 0)
|
|
elif col == 'authors':
|
|
return (' & '.join(mi.authors), mi.author_sort, 0)
|
|
elif col == 'series':
|
|
series = mi.format_field('series')[1]
|
|
if series is None:
|
|
return ('', None, 0)
|
|
else:
|
|
return (series, mi.series, mi.series_index)
|
|
elif col == 'size':
|
|
v = mi.get('book_size')
|
|
if v is not None:
|
|
return (f'{v:n}', v, 0)
|
|
else:
|
|
return ('', None, 0)
|
|
elif self.fm[col]['datatype'] == 'series':
|
|
v = mi.format_field(col)[1]
|
|
return (v, mi.get(col), mi.get(col+'_index'))
|
|
elif self.fm[col]['datatype'] == 'datetime':
|
|
v = mi.format_field(col)[1]
|
|
d = mi.get(col)
|
|
if d is None:
|
|
d = UNDEFINED_DATE
|
|
return (v, timestampfromdt(d), 0)
|
|
elif self.fm[col]['datatype'] in ('float', 'int'):
|
|
v = mi.format_field(col)[1]
|
|
sort_val = mi.get(col)
|
|
return (v, sort_val, 0)
|
|
else:
|
|
v = mi.format_field(col)[1]
|
|
return (v, v, 0)
|
|
except:
|
|
traceback.print_exc()
|
|
return (_('Something went wrong while filling in the table'), '', 0)
|
|
|
|
# Deal with sizing the table columns. Done here because the numbers are not
|
|
# correct until the first paint.
|
|
def resizeEvent(self, *args):
|
|
QDialog.resizeEvent(self, *args)
|
|
|
|
# Do this if we are resizing for the first time to reset state.
|
|
if self.is_pane and self.height() == 0:
|
|
self.gui.quickview_splitter.set_sizes()
|
|
|
|
if self.books_table_column_widths is not None:
|
|
for c,w in enumerate(self.books_table_column_widths):
|
|
self.books_table.setColumnWidth(c, w)
|
|
else:
|
|
# the vertical scroll bar might not be rendered, so might not yet
|
|
# have a width. Assume 25. Not a problem because user-changed column
|
|
# widths will be remembered
|
|
w = self.books_table.width() - 25 - self.books_table.verticalHeader().width()
|
|
w //= self.books_table.columnCount()
|
|
for c in range(0, self.books_table.columnCount()):
|
|
self.books_table.setColumnWidth(c, w)
|
|
self.save_state()
|
|
|
|
def key_to_table_widget_column(self, key):
|
|
return self.column_order.index(key)
|
|
|
|
def return_pressed(self):
|
|
row = self.books_table.currentRow()
|
|
if gprefs['qv_retkey_changes_column']:
|
|
self.select_book_and_qv(row, self.books_table.currentColumn())
|
|
else:
|
|
self.select_book_and_qv(row, self.key_to_table_widget_column(self.current_key))
|
|
|
|
def book_not_in_view_error(self):
|
|
from calibre.gui2 import error_dialog
|
|
error_dialog(self, _('Quickview: Book not in library view'),
|
|
_('The book you selected is not currently displayed in '
|
|
'the library view, perhaps because of a search or a '
|
|
'Virtual library, so Quickview cannot select it.'),
|
|
show=True,
|
|
show_copy_button=False)
|
|
|
|
def book_displayed_in_library_view(self, book_id):
|
|
try:
|
|
self.db.data.index(book_id)
|
|
return True
|
|
except:
|
|
return False
|
|
|
|
def quickview_item(self, row, column):
|
|
self.select_book_and_qv(row, column)
|
|
|
|
def book_doubleclicked(self, row, column):
|
|
if self.no_valid_items:
|
|
return
|
|
try:
|
|
if gprefs['qv_dclick_changes_column']:
|
|
self.quickview_item(row, column)
|
|
else:
|
|
self.quickview_item(row, self.key_to_table_widget_column(self.current_key))
|
|
except:
|
|
self.book_not_in_view_error()
|
|
|
|
def edit_metadata(self, book_id, follow_library_view=True):
|
|
try:
|
|
self.follow_library_view = follow_library_view
|
|
self.view.select_rows([book_id])
|
|
em = find_plugin('Edit Metadata')
|
|
if em and em.actual_plugin_:
|
|
em.actual_plugin_.edit_metadata(None)
|
|
finally:
|
|
self.follow_library_view = True
|
|
|
|
def select_book(self, book_id):
|
|
'''
|
|
Select a book in the library view without changing the QV lists
|
|
'''
|
|
try:
|
|
self.follow_library_view = False
|
|
self.view.select_cell(self.db.data.id_to_index(book_id),
|
|
self.current_column)
|
|
finally:
|
|
self.follow_library_view = True
|
|
|
|
def select_book_and_qv(self, row, column):
|
|
'''
|
|
row and column both refer the qv table. In particular, column is not
|
|
the logical column in the book list.
|
|
'''
|
|
item = self.books_table.item(row, column)
|
|
if item is None:
|
|
return
|
|
book_id = int(self.books_table.item(row, column).data(Qt.ItemDataRole.UserRole))
|
|
if not self.book_displayed_in_library_view(book_id):
|
|
self.book_not_in_view_error()
|
|
return
|
|
key = self.column_order[column]
|
|
if QApplication.keyboardModifiers() in (Qt.KeyboardModifier.ControlModifier, Qt.KeyboardModifier.ShiftModifier):
|
|
self.edit_metadata(book_id)
|
|
else:
|
|
self.view.select_cell(self.db.data.id_to_index(book_id),
|
|
self.view.column_map.index(key))
|
|
|
|
def set_focus(self):
|
|
self.activateWindow()
|
|
self.books_table.setFocus()
|
|
|
|
def column_slave(self, current):
|
|
'''
|
|
called when the column is changed on the booklist
|
|
'''
|
|
if self.follow_library_view and gprefs['qv_follows_column']:
|
|
self.slave(current)
|
|
|
|
def slave(self, current):
|
|
'''
|
|
called when a book is clicked on the library view
|
|
'''
|
|
if self.is_closed or not self.follow_library_view:
|
|
return
|
|
self.refresh(current)
|
|
self.view.activateWindow()
|
|
|
|
def section_resized(self, logicalIndex, oldSize, newSize):
|
|
self.save_state()
|
|
|
|
def save_state(self):
|
|
if self.is_closed:
|
|
return
|
|
self.books_table_column_widths = []
|
|
for c in range(0, self.books_table.columnCount()):
|
|
self.books_table_column_widths.append(self.books_table.columnWidth(c))
|
|
gprefs['quickview_dialog_books_table_widths'] = self.books_table_column_widths
|
|
if not self.is_pane:
|
|
gprefs['quickview_dialog_geometry'] = bytearray(self.saveGeometry())
|
|
|
|
def _close(self):
|
|
self.save_state()
|
|
# clean up to prevent memory leaks
|
|
self.db = self.view = self.gui = None
|
|
self.is_closed = True
|
|
|
|
def close_button_clicked(self):
|
|
self.closed_by_button = True
|
|
self.quickview_closed.emit()
|
|
|
|
def reject(self):
|
|
if not self.closed_by_button:
|
|
self.close_button_clicked()
|
|
else:
|
|
self._reject()
|
|
|
|
def _reject(self):
|
|
if self.is_pane:
|
|
self.gui.quickview_splitter.hide_quickview_widget()
|
|
self.gui.library_view.setFocus(Qt.FocusReason.ActiveWindowFocusReason)
|
|
self._close()
|
|
QDialog.reject(self)
|
|
|
|
|
|
def get_qv_field_list(fm, use_defaults=False):
|
|
from calibre.gui2.ui import get_gui
|
|
db = get_gui().current_db
|
|
if use_defaults:
|
|
src = db.prefs.defaults
|
|
else:
|
|
src = db.prefs
|
|
fieldlist = list(src['qv_display_fields'])
|
|
names = frozenset(x[0] for x in fieldlist)
|
|
for field in fm.displayable_field_keys():
|
|
if (field != 'comments' and fm[field]['datatype'] != 'comments' and field not in names):
|
|
fieldlist.append((field, False))
|
|
available = frozenset(fm.displayable_field_keys())
|
|
return [(f, d) for f, d in fieldlist if f in available]
|