%PDF- %PDF-
| Direktori : /lib/calibre/calibre/gui2/library/ |
| Current File : //lib/calibre/calibre/gui2/library/views.py |
#!/usr/bin/env python3
__license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import itertools, operator
from functools import partial
from collections import OrderedDict
from qt.core import (
QTableView, Qt, QAbstractItemView, QMenu, pyqtSignal, QFont, QModelIndex,
QIcon, QItemSelection, QMimeData, QDrag, QStyle, QPoint, QUrl, QHeaderView, QEvent,
QStyleOptionHeader, QItemSelectionModel, QSize, QFontMetrics, QApplication)
from calibre.constants import islinux
from calibre.gui2.dialogs.enum_values_edit import EnumValuesEdit
from calibre.gui2.library.delegates import (RatingDelegate, PubDateDelegate,
TextDelegate, DateDelegate, CompleteDelegate, CcTextDelegate, CcLongTextDelegate,
CcBoolDelegate, CcCommentsDelegate, CcDateDelegate, CcTemplateDelegate,
CcEnumDelegate, CcNumberDelegate, LanguagesDelegate, SeriesDelegate, CcSeriesDelegate)
from calibre.gui2.library.models import BooksModel, DeviceBooksModel
from calibre.gui2.pin_columns import PinTableView
from calibre.gui2.library.alternate_views import AlternateViews, setup_dnd_interface, handle_enter_press
from calibre.gui2.gestures import GestureManager
from calibre.utils.config import tweaks, prefs
from calibre.gui2 import error_dialog, gprefs, FunctionDispatcher
from calibre.gui2.library import DEFAULT_SORT
from calibre.constants import filesystem_encoding
from calibre import force_unicode
from calibre.utils.icu import primary_sort_key
from polyglot.builtins import iteritems
def restrict_column_width(self, col, old_size, new_size):
# arbitrary: scroll bar + header + some
sw = self.verticalScrollBar().width() if self.verticalScrollBar().isVisible() else 0
hw = self.verticalHeader().width() if self.verticalHeader().isVisible() else 0
max_width = max(200, self.width() - (sw + hw + 10))
if new_size > max_width:
self.column_header.blockSignals(True)
self.setColumnWidth(col, max_width)
self.column_header.blockSignals(False)
class HeaderView(QHeaderView): # {{{
def __init__(self, *args):
QHeaderView.__init__(self, *args)
if self.orientation() == Qt.Orientation.Horizontal:
self.setSectionsMovable(True)
self.setSectionsClickable(True)
self.setTextElideMode(Qt.TextElideMode.ElideRight)
self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
self.hover = -1
self.current_font = QFont(self.font())
self.current_font.setBold(True)
self.current_font.setItalic(True)
self.fm = QFontMetrics(self.current_font)
def event(self, e):
if e.type() in (QEvent.Type.HoverMove, QEvent.Type.HoverEnter):
self.hover = self.logicalIndexAt(e.pos())
elif e.type() in (QEvent.Type.Leave, QEvent.Type.HoverLeave):
self.hover = -1
return QHeaderView.event(self, e)
def sectionSizeFromContents(self, logical_index):
self.ensurePolished()
opt = QStyleOptionHeader()
self.initStyleOption(opt)
opt.section = logical_index
opt.orientation = self.orientation()
opt.fontMetrics = self.fm
model = self.parent().model()
opt.text = str(model.headerData(logical_index, opt.orientation, Qt.ItemDataRole.DisplayRole) or '')
if opt.orientation == Qt.Orientation.Vertical:
try:
val = model.headerData(logical_index, opt.orientation, Qt.ItemDataRole.DecorationRole)
if val is not None:
opt.icon = val
opt.iconAlignment = Qt.AlignmentFlag.AlignVCenter
except (IndexError, ValueError, TypeError):
pass
if self.isSortIndicatorShown():
opt.sortIndicator = QStyleOptionHeader.SortIndicator.SortDown
return self.style().sizeFromContents(QStyle.ContentsType.CT_HeaderSection, opt, QSize(), self)
def paintSection(self, painter, rect, logical_index):
opt = QStyleOptionHeader()
self.initStyleOption(opt)
opt.rect = rect
opt.section = logical_index
opt.orientation = self.orientation()
opt.textAlignment = Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter
opt.fontMetrics = self.fm
model = self.parent().model()
style = self.style()
margin = 2 * style.pixelMetric(QStyle.PixelMetric.PM_HeaderMargin, None, self)
if self.isSortIndicatorShown() and self.sortIndicatorSection() == logical_index:
opt.sortIndicator = QStyleOptionHeader.SortIndicator.SortDown if \
self.sortIndicatorOrder() == Qt.SortOrder.AscendingOrder else QStyleOptionHeader.SortIndicator.SortUp
margin += style.pixelMetric(QStyle.PixelMetric.PM_HeaderMarkSize, None, self)
opt.text = str(model.headerData(logical_index, opt.orientation, Qt.ItemDataRole.DisplayRole) or '')
if self.textElideMode() != Qt.TextElideMode.ElideNone:
opt.text = opt.fontMetrics.elidedText(opt.text, Qt.TextElideMode.ElideRight, rect.width() - margin)
if self.isEnabled():
opt.state |= QStyle.StateFlag.State_Enabled
if self.window().isActiveWindow():
opt.state |= QStyle.StateFlag.State_Active
if self.hover == logical_index:
opt.state |= QStyle.StateFlag.State_MouseOver
sm = self.selectionModel()
if opt.orientation == Qt.Orientation.Vertical:
try:
val = model.headerData(logical_index, opt.orientation, Qt.ItemDataRole.DecorationRole)
if val is not None:
opt.icon = val
opt.iconAlignment = Qt.AlignmentFlag.AlignVCenter
except (IndexError, ValueError, TypeError):
pass
if sm.isRowSelected(logical_index, QModelIndex()):
opt.state |= QStyle.StateFlag.State_Sunken
painter.save()
if (
(opt.orientation == Qt.Orientation.Horizontal and sm.currentIndex().column() == logical_index) or (
opt.orientation == Qt.Orientation.Vertical and sm.currentIndex().row() == logical_index)):
painter.setFont(self.current_font)
self.style().drawControl(QStyle.ControlElement.CE_Header, opt, painter, self)
painter.restore()
# }}}
class PreserveViewState: # {{{
'''
Save the set of selected books at enter time. If at exit time there are no
selected books, restore the previous selection, the previous current index
and dont affect the scroll position.
'''
def __init__(self, view, preserve_hpos=True, preserve_vpos=True,
require_selected_ids=True):
self.view = view
self.require_selected_ids = require_selected_ids
self.preserve_hpos = preserve_hpos
self.preserve_vpos = preserve_vpos
self.init_vals()
def init_vals(self):
self.selected_ids = set()
self.current_id = None
self.vscroll = self.hscroll = 0
self.original_view = None
def __enter__(self):
self.init_vals()
try:
view = self.original_view = self.view.alternate_views.current_view
self.selected_ids = self.view.get_selected_ids()
self.current_id = self.view.current_id
self.vscroll = view.verticalScrollBar().value()
self.hscroll = view.horizontalScrollBar().value()
except:
import traceback
traceback.print_exc()
def __exit__(self, *args):
if self.selected_ids or not self.require_selected_ids:
if self.current_id is not None:
self.view.current_id = self.current_id
if self.selected_ids:
self.view.select_rows(self.selected_ids, using_ids=True,
scroll=False, change_current=self.current_id is None)
view = self.original_view
if self.view.alternate_views.current_view is view:
if self.preserve_vpos:
if hasattr(view, 'restore_vpos'):
view.restore_vpos(self.vscroll)
else:
view.verticalScrollBar().setValue(self.vscroll)
if self.preserve_hpos:
if hasattr(view, 'restore_hpos'):
view.restore_hpos(self.hscroll)
else:
view.horizontalScrollBar().setValue(self.hscroll)
self.init_vals()
@property
def state(self):
self.__enter__()
return {x:getattr(self, x) for x in ('selected_ids', 'current_id',
'vscroll', 'hscroll')}
@state.setter
def state(self, state):
for k, v in iteritems(state):
setattr(self, k, v)
self.__exit__()
# }}}
@setup_dnd_interface
class BooksView(QTableView): # {{{
files_dropped = pyqtSignal(object)
books_dropped = pyqtSignal(object)
selection_changed = pyqtSignal()
add_column_signal = pyqtSignal()
is_library_view = True
def viewportEvent(self, event):
if (event.type() == QEvent.Type.ToolTip and not gprefs['book_list_tooltips']):
return False
try:
ret = self.gesture_manager.handle_event(event)
except AttributeError:
ret = None
if ret is not None:
return ret
return QTableView.viewportEvent(self, event)
def __init__(self, parent, modelcls=BooksModel, use_edit_metadata_dialog=True):
QTableView.__init__(self, parent)
self.pin_view = PinTableView(self, parent)
self.gesture_manager = GestureManager(self)
self.default_row_height = self.verticalHeader().defaultSectionSize()
self.gui = parent
self.setProperty('highlight_current_item', 150)
self.pin_view.setProperty('highlight_current_item', 150)
self.row_sizing_done = False
self.alternate_views = AlternateViews(self)
for wv in self, self.pin_view:
if not tweaks['horizontal_scrolling_per_column']:
wv.setHorizontalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel)
if not tweaks['vertical_scrolling_per_row']:
wv.setVerticalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel)
wv.setEditTriggers(QAbstractItemView.EditTrigger.EditKeyPressed)
tval = tweaks['doubleclick_on_library_view']
if tval == 'edit_cell':
wv.setEditTriggers(QAbstractItemView.EditTrigger.DoubleClicked|wv.editTriggers())
elif tval == 'open_viewer':
wv.setEditTriggers(QAbstractItemView.EditTrigger.SelectedClicked|wv.editTriggers())
wv.doubleClicked.connect(parent.iactions['View'].view_triggered)
elif tval == 'show_book_details':
wv.setEditTriggers(QAbstractItemView.EditTrigger.SelectedClicked|wv.editTriggers())
wv.doubleClicked.connect(parent.iactions['Show Book Details'].show_book_info)
elif tval == 'edit_metadata':
# Must not enable single-click to edit, or the field will remain
# open in edit mode underneath the edit metadata dialog
if use_edit_metadata_dialog:
wv.doubleClicked.connect(
partial(parent.iactions['Edit Metadata'].edit_metadata,
checked=False))
else:
wv.setEditTriggers(QAbstractItemView.EditTrigger.DoubleClicked|wv.editTriggers())
setup_dnd_interface(self)
for wv in self, self.pin_view:
wv.setAlternatingRowColors(True)
wv.setWordWrap(False)
self.refresh_grid()
self.rating_delegate = RatingDelegate(self)
self.half_rating_delegate = RatingDelegate(self, is_half_star=True)
self.timestamp_delegate = DateDelegate(self)
self.pubdate_delegate = PubDateDelegate(self)
self.last_modified_delegate = DateDelegate(self,
tweak_name='gui_last_modified_display_format')
self.languages_delegate = LanguagesDelegate(self)
self.tags_delegate = CompleteDelegate(self, ',', 'all_tag_names')
self.authors_delegate = CompleteDelegate(self, '&', 'all_author_names', True)
self.cc_names_delegate = CompleteDelegate(self, '&', 'all_custom', True)
self.series_delegate = SeriesDelegate(self)
self.publisher_delegate = TextDelegate(self)
self.text_delegate = TextDelegate(self)
self.cc_text_delegate = CcTextDelegate(self)
self.cc_series_delegate = CcSeriesDelegate(self)
self.cc_longtext_delegate = CcLongTextDelegate(self)
self.cc_enum_delegate = CcEnumDelegate(self)
self.cc_bool_delegate = CcBoolDelegate(self)
self.cc_comments_delegate = CcCommentsDelegate(self)
self.cc_template_delegate = CcTemplateDelegate(self)
self.cc_number_delegate = CcNumberDelegate(self)
self.display_parent = parent
self._model = modelcls(self)
self.setModel(self._model)
self.pin_view.setModel(self._model)
self._model.count_changed_signal.connect(self.do_row_sizing,
type=Qt.ConnectionType.QueuedConnection)
for wv in self, self.pin_view:
wv.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
wv.setSortingEnabled(True)
self.selectionModel().currentRowChanged.connect(self._model.current_changed)
self.selectionModel().selectionChanged.connect(self.selection_changed.emit)
self.preserve_state = partial(PreserveViewState, self)
self.marked_changed_listener = FunctionDispatcher(self.marked_changed)
# {{{ Column Header setup
self.can_add_columns = True
self.was_restored = False
self.column_header = HeaderView(Qt.Orientation.Horizontal, self)
self.pin_view.column_header = HeaderView(Qt.Orientation.Horizontal, self.pin_view)
self.setHorizontalHeader(self.column_header)
self.pin_view.setHorizontalHeader(self.pin_view.column_header)
self.column_header.sectionMoved.connect(self.save_state)
self.column_header.sortIndicatorChanged.disconnect()
self.column_header.sortIndicatorChanged.connect(self.user_sort_requested)
self.pin_view.column_header.sortIndicatorChanged.disconnect()
self.pin_view.column_header.sortIndicatorChanged.connect(self.pin_view_user_sort_requested)
self.column_header.customContextMenuRequested.connect(partial(self.show_column_header_context_menu, view=self))
self.column_header.sectionResized.connect(self.column_resized, Qt.ConnectionType.QueuedConnection)
if self.is_library_view:
self.pin_view.column_header.sectionResized.connect(self.pin_view_column_resized, Qt.ConnectionType.QueuedConnection)
self.pin_view.column_header.sectionMoved.connect(self.pin_view.save_state)
self.pin_view.column_header.customContextMenuRequested.connect(partial(self.show_column_header_context_menu, view=self.pin_view))
self.row_header = HeaderView(Qt.Orientation.Vertical, self)
self.row_header.setSectionResizeMode(QHeaderView.ResizeMode.Fixed)
self.row_header.customContextMenuRequested.connect(self.show_row_header_context_menu)
self.setVerticalHeader(self.row_header)
# }}}
self._model.database_changed.connect(self.database_changed)
hv = self.verticalHeader()
hv.setSectionsClickable(True)
hv.setCursor(Qt.CursorShape.PointingHandCursor)
self.selected_ids = []
self._model.about_to_be_sorted.connect(self.about_to_be_sorted)
self._model.sorting_done.connect(self.sorting_done,
type=Qt.ConnectionType.QueuedConnection)
self.set_row_header_visibility()
self.allow_mirroring = True
if self.is_library_view:
self.set_pin_view_visibility(gprefs['book_list_split'])
for wv in self, self.pin_view:
wv.selectionModel().currentRowChanged.connect(partial(self.mirror_selection_between_views, wv))
wv.selectionModel().selectionChanged.connect(partial(self.mirror_selection_between_views, wv))
wv.verticalScrollBar().valueChanged.connect(partial(self.mirror_vscroll, wv))
wv.verticalScrollBar().rangeChanged.connect(partial(self.mirror_vscroll, wv))
else:
self.pin_view.setVisible(False)
# Pin view {{{
def set_pin_view_visibility(self, visible=False):
self.pin_view.setVisible(visible)
self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff if visible else Qt.ScrollBarPolicy.ScrollBarAsNeeded)
self.mirror_selection_between_views(self)
def mirror_selection_between_views(self, src):
if self.allow_mirroring:
dest = self.pin_view if src is self else self
if dest is self.pin_view and not dest.isVisible():
return
self.allow_mirroring = False
dest.selectionModel().select(src.selectionModel().selection(), QItemSelectionModel.SelectionFlag.ClearAndSelect)
ci = dest.currentIndex()
nci = src.selectionModel().currentIndex()
# Save/restore horz scroll. ci column may be scrolled out of view.
hpos = dest.horizontalScrollBar().value()
if ci.isValid():
nci = dest.model().index(nci.row(), ci.column())
dest.selectionModel().setCurrentIndex(nci, QItemSelectionModel.SelectionFlag.NoUpdate)
dest.horizontalScrollBar().setValue(hpos)
self.allow_mirroring = True
def mirror_vscroll(self, src, *a):
if self.allow_mirroring:
dest = self.pin_view if src is self else self
if dest is self.pin_view and not dest.isVisible():
return
self.allow_mirroring = False
s, d = src.verticalScrollBar(), dest.verticalScrollBar()
d.setRange(s.minimum(), s.maximum()), d.setValue(s.value())
self.allow_mirroring = True
# }}}
# Column Header Context Menu {{{
def column_header_context_handler(self, action=None, column=None, view=None):
if action == 'split':
self.set_pin_view_visibility(not self.pin_view.isVisible())
gprefs['book_list_split'] = self.pin_view.isVisible()
self.save_state()
return
if not action or not column or not view:
return
try:
idx = self.column_map.index(column)
except:
return
h = view.column_header
if action == 'hide':
if h.hiddenSectionCount() >= h.count():
return error_dialog(self, _('Cannot hide all columns'), _(
'You must not hide all columns'), show=True)
h.setSectionHidden(idx, True)
elif action == 'show':
h.setSectionHidden(idx, False)
if h.sectionSize(idx) < 3:
sz = h.sectionSizeHint(idx)
h.resizeSection(idx, sz)
elif action == 'ascending':
self.sort_by_column_and_order(idx, True)
elif action == 'descending':
self.sort_by_column_and_order(idx, False)
elif action == 'defaults':
view.apply_state(view.get_default_state())
elif action == 'addcustcol':
self.add_column_signal.emit()
elif action.startswith('align_'):
alignment = action.partition('_')[-1]
self._model.change_alignment(column, alignment)
elif action.startswith('font_'):
self._model.change_column_font(column, action[len('font_'):])
elif action == 'quickview':
from calibre.gui2.actions.show_quickview import get_quickview_action_plugin
qv = get_quickview_action_plugin()
if qv:
rows = self.selectionModel().selectedRows()
if len(rows) > 0:
current_row = rows[0].row()
current_col = self.column_map.index(column)
index = self.model().index(current_row, current_col)
qv.change_quickview_column(index)
elif action == 'remember_ondevice_width':
gprefs.set('ondevice_column_width', self.columnWidth(idx))
elif action == 'reset_ondevice_width':
gprefs.set('ondevice_column_width', 0)
self.resizeColumnToContents(idx)
elif action == 'edit_enum':
EnumValuesEdit(self, self._model.db, column).exec()
self.save_state()
def create_context_menu(self, col, name, view):
ans = QMenu(view)
handler = partial(self.column_header_context_handler, view=view, column=col)
if col not in ('ondevice', 'inlibrary'):
ans.addAction(QIcon.ic('minus.png'), _('Hide column %s') % name, partial(handler, action='hide'))
m = ans.addMenu(_('Sort on %s') % name)
m.setIcon(QIcon.ic('sort.png'))
a = m.addAction(_('Ascending'), partial(handler, action='ascending'))
d = m.addAction(_('Descending'), partial(handler, action='descending'))
if self._model.sorted_on[0] == col:
ac = a if self._model.sorted_on[1] else d
ac.setCheckable(True)
ac.setChecked(True)
if col not in ('ondevice', 'inlibrary') and \
(not self.model().is_custom_column(col) or self.model().custom_columns[col]['datatype'] not in ('bool',)):
m = ans.addMenu(_('Change text alignment for %s') % name)
m.setIcon(QIcon.ic('format-justify-center.png'))
al = self._model.alignment_map.get(col, 'left')
for x, t in (('left', _('Left')), ('right', _('Right')), ('center', _('Center'))):
a = m.addAction(QIcon.ic(f'format-justify-{x}.png'), t, partial(handler, action='align_'+x))
if al == x:
a.setCheckable(True)
a.setChecked(True)
if not isinstance(view, DeviceBooksView):
col_font = self._model.styled_columns.get(col)
m = ans.addMenu(_('Change font style for %s') % name)
m.setIcon(QIcon.ic('format-text-bold.png'))
for x, t, f in (
('normal', _('Normal font'), None), ('bold', _('Bold font'), self._model.bold_font),
('italic', _('Italic font'), self._model.italic_font), ('bi', _('Bold and Italic font'), self._model.bi_font),
):
a = m.addAction(t, partial(handler, action='font_' + x))
if x in ('bold', 'italic'):
a.setIcon(QIcon.ic(f'format-text-{x}.png'))
if f is col_font:
a.setCheckable(True)
a.setChecked(True)
if self.is_library_view:
if self._model.db.field_metadata[col]['is_category']:
act = ans.addAction(QIcon.ic('quickview.png'), _('Quickview column %s') % name,
partial(handler, action='quickview'))
rows = self.selectionModel().selectedRows()
if len(rows) > 1:
act.setEnabled(False)
if self._model.db.field_metadata[col]['datatype'] == 'enumeration':
ans.addAction(QIcon.ic('edit_input.png'), _('Edit permissible values for %s') % name,
partial(handler, action='edit_enum'))
hidden_cols = {self.column_map[i]: i for i in range(view.column_header.count())
if view.column_header.isSectionHidden(i) and self.column_map[i] not in ('ondevice', 'inlibrary')}
ans.addSeparator()
if hidden_cols:
m = ans.addMenu(_('Show column'))
m.setIcon(QIcon.ic('plus.png'))
hcols = [(hcol, str(self.model().headerData(hidx, Qt.Orientation.Horizontal, Qt.ItemDataRole.DisplayRole) or ''))
for hcol, hidx in iteritems(hidden_cols)]
hcols.sort(key=lambda x: primary_sort_key(x[1]))
for hcol, hname in hcols:
m.addAction(hname.replace('&', '&&'), partial(handler, action='show', column=hcol))
ans.addSeparator()
if col == 'ondevice':
ans.addAction(_('Remember On Device column width'),
partial(handler, action='remember_ondevice_width'))
ans.addAction(_('Reset On Device column width to default'),
partial(handler, action='reset_ondevice_width'))
ans.addAction(_('Shrink column if it is too wide to fit'),
partial(self.resize_column_to_fit, view, col))
ans.addAction(_('Resize column to fit contents'),
partial(self.fit_column_to_contents, view, col))
ans.addAction(_('Restore default layout'), partial(handler, action='defaults'))
if self.can_add_columns:
ans.addAction(
QIcon(I('column.png')), _('Add your own columns'), partial(handler, action='addcustcol'))
return ans
def show_row_header_context_menu(self, pos):
menu = QMenu(self)
menu.addAction(_('Hide row numbers'), self.hide_row_numbers)
menu.popup(self.mapToGlobal(pos))
def hide_row_numbers(self):
gprefs['row_numbers_in_book_list'] = False
self.set_row_header_visibility()
def show_column_header_context_menu(self, pos, view=None):
view = view or self
idx = view.column_header.logicalIndexAt(pos)
col = None
if idx > -1 and idx < len(self.column_map):
col = self.column_map[idx]
name = str(self.model().headerData(idx, Qt.Orientation.Horizontal, Qt.ItemDataRole.DisplayRole) or '')
view.column_header_context_menu = self.create_context_menu(col, name, view)
has_context_menu = hasattr(view, 'column_header_context_menu')
if self.is_library_view and has_context_menu:
view.column_header_context_menu.addSeparator()
if not hasattr(view.column_header_context_menu, 'bl_split_action'):
view.column_header_context_menu.bl_split_action = view.column_header_context_menu.addAction(
QIcon.ic('split.png'), 'xxx', partial(self.column_header_context_handler, action='split', column='title'))
ac = view.column_header_context_menu.bl_split_action
if self.pin_view.isVisible():
ac.setText(_('Un-split the book list'))
else:
ac.setText(_('Split the book list'))
if has_context_menu:
view.column_header_context_menu.popup(view.column_header.mapToGlobal(pos))
# }}}
# Sorting {{{
def set_sort_indicator(self, logical_idx, ascending):
views = [self, self.pin_view] if self.is_library_view else [self]
for v in views:
ch = v.column_header
ch.blockSignals(True)
ch.setSortIndicator(logical_idx, Qt.SortOrder.AscendingOrder if ascending else Qt.SortOrder.DescendingOrder)
ch.blockSignals(False)
def sort_by_column_and_order(self, col, ascending):
order = Qt.SortOrder.AscendingOrder if ascending else Qt.SortOrder.DescendingOrder
self.column_header.blockSignals(True)
self.column_header.setSortIndicator(col, order)
self.column_header.blockSignals(False)
self.model().sort(col, order)
if self.is_library_view:
self.set_sort_indicator(col, ascending)
def user_sort_requested(self, col, order=Qt.SortOrder.AscendingOrder):
if 0 <= col < len(self.column_map):
field = self.column_map[col]
self.intelligent_sort(field, order == Qt.SortOrder.AscendingOrder)
def pin_view_user_sort_requested(self, col, order=Qt.SortOrder.AscendingOrder):
if col < len(self.column_map) and col >= 0:
field = self.column_map[col]
self.intelligent_sort(field, order == Qt.SortOrder.AscendingOrder)
def intelligent_sort(self, field, ascending):
m = self.model()
pname = 'previous_sort_order_' + self.__class__.__name__
previous = gprefs.get(pname, {})
if field == m.sorted_on[0] or field not in previous:
self.sort_by_named_field(field, ascending)
previous[field] = ascending
gprefs[pname] = previous
return
previous[m.sorted_on[0]] = m.sorted_on[1]
gprefs[pname] = previous
self.sort_by_named_field(field, previous[field])
def about_to_be_sorted(self, idc):
selected_rows = [r.row() for r in self.selectionModel().selectedRows()]
self.selected_ids = [idc(r) for r in selected_rows]
def sorting_done(self, indexc):
pos = self.horizontalScrollBar().value()
self.select_rows(self.selected_ids, using_ids=True, change_current=True,
scroll=True)
self.selected_ids = []
self.horizontalScrollBar().setValue(pos)
def sort_by_named_field(self, field, order, reset=True):
if field in self.column_map:
idx = self.column_map.index(field)
self.sort_by_column_and_order(idx, order)
else:
self._model.sort_by_named_field(field, order, reset)
self.set_sort_indicator(-1, True)
def multisort(self, fields, reset=True, only_if_different=False):
if len(fields) == 0:
return
sh = self.cleanup_sort_history(self._model.sort_history,
ignore_column_map=True)
if only_if_different and len(sh) >= len(fields):
ret=True
for i,t in enumerate(fields):
if t[0] != sh[i][0]:
ret = False
break
if ret:
return
for n,d in reversed(fields):
if n in list(self._model.db.field_metadata.keys()):
sh.insert(0, (n, d))
sh = self.cleanup_sort_history(sh, ignore_column_map=True)
self._model.sort_history = [tuple(x) for x in sh]
self._model.resort(reset=reset)
col = fields[0][0]
ascending = fields[0][1]
try:
idx = self.column_map.index(col)
except Exception:
idx = -1
self.set_sort_indicator(idx, ascending)
def resort(self):
with self.preserve_state(preserve_vpos=False, require_selected_ids=False):
self._model.resort(reset=True)
def reverse_sort(self):
with self.preserve_state(preserve_vpos=False, require_selected_ids=False):
m = self.model()
try:
sort_col, order = m.sorted_on
except TypeError:
sort_col, order = 'date', True
self.sort_by_named_field(sort_col, not order)
# }}}
# Ondevice column {{{
def set_ondevice_column_visibility(self):
col = self._model.column_map.index('ondevice')
self.column_header.setSectionHidden(col, not self._model.device_connected)
w = gprefs.get('ondevice_column_width', 0)
if w > 0:
self.setColumnWidth(col, w)
if self.is_library_view:
self.pin_view.column_header.setSectionHidden(col, True)
def set_device_connected(self, is_connected):
self._model.set_device_connected(is_connected)
self.set_ondevice_column_visibility()
# }}}
# Save/Restore State {{{
def get_state(self):
h = self.column_header
cm = self.column_map
state = {}
state['hidden_columns'] = [cm[i] for i in range(h.count())
if h.isSectionHidden(i) and cm[i] != 'ondevice']
state['last_modified_injected'] = True
state['languages_injected'] = True
state['sort_history'] = \
self.cleanup_sort_history(self.model().sort_history, ignore_column_map=self.is_library_view)
state['column_positions'] = {}
state['column_sizes'] = {}
state['column_alignment'] = self._model.alignment_map
for i in range(h.count()):
name = cm[i]
state['column_positions'][name] = h.visualIndex(i)
if name != 'ondevice':
state['column_sizes'][name] = h.sectionSize(i)
return state
def write_state(self, state):
db = getattr(self.model(), 'db', None)
name = str(self.objectName())
if name and db is not None:
db.new_api.set_pref(name + ' books view state', state)
def save_state(self):
# Only save if we have been initialized (set_database called)
if len(self.column_map) > 0 and self.was_restored:
state = self.get_state()
self.write_state(state)
if self.is_library_view:
self.pin_view.save_state()
def cleanup_sort_history(self, sort_history, ignore_column_map=False):
history = []
for col, order in sort_history:
if not isinstance(order, bool):
continue
col = {'date':'timestamp', 'sort':'title'}.get(col, col)
if ignore_column_map or col in self.column_map:
if (not history or history[-1][0] != col):
history.append([col, order])
return history
def apply_sort_history(self, saved_history, max_sort_levels=3):
if not saved_history:
return
if self.is_library_view:
for col, order in reversed(self.cleanup_sort_history(
saved_history, ignore_column_map=True)[:max_sort_levels]):
try:
self.sort_by_named_field(col, order)
except KeyError:
pass
else:
for col, order in reversed(self.cleanup_sort_history(
saved_history)[:max_sort_levels]):
self.sort_by_column_and_order(self.column_map.index(col), order)
def apply_state(self, state, max_sort_levels=3):
h = self.column_header
cmap = {}
hidden = state.get('hidden_columns', [])
for i, c in enumerate(self.column_map):
cmap[c] = i
if c != 'ondevice':
h.setSectionHidden(i, c in hidden)
positions = state.get('column_positions', {})
pmap = {}
for col, pos in positions.items():
if col in cmap:
pmap[pos] = col
for pos in sorted(pmap.keys()):
col = pmap[pos]
idx = cmap[col]
current_pos = h.visualIndex(idx)
if current_pos != pos:
h.moveSection(current_pos, pos)
# Because of a bug in Qt 5 we have to ensure that the header is actually
# relaid out by changing this value, without this sometimes ghost
# columns remain visible when changing libraries
for i in range(h.count()):
val = h.isSectionHidden(i)
h.setSectionHidden(i, not val)
h.setSectionHidden(i, val)
sizes = state.get('column_sizes', {})
for col, size in sizes.items():
if col in cmap:
sz = sizes[col]
if sz < 3:
sz = h.sectionSizeHint(cmap[col])
h.resizeSection(cmap[col], sz)
self.apply_sort_history(state.get('sort_history', None),
max_sort_levels=max_sort_levels)
for col, alignment in state.get('column_alignment', {}).items():
self._model.change_alignment(col, alignment)
for i in range(h.count()):
if not h.isSectionHidden(i) and h.sectionSize(i) < 3:
sz = h.sectionSizeHint(i)
h.resizeSection(i, sz)
def get_default_state(self):
old_state = {
'hidden_columns': ['last_modified', 'languages'],
'sort_history':[DEFAULT_SORT],
'column_positions': {},
'column_sizes': {},
'column_alignment': {
'size':'center',
'timestamp':'center',
'pubdate':'center'},
'last_modified_injected': True,
'languages_injected': True,
}
h = self.column_header
cm = self.column_map
for i in range(h.count()):
name = cm[i]
old_state['column_positions'][name] = i
if name != 'ondevice':
old_state['column_sizes'][name] = \
min(350, max(self.sizeHintForColumn(i),
h.sectionSizeHint(i)))
if name in ('timestamp', 'last_modified'):
old_state['column_sizes'][name] += 12
return old_state
def get_old_state(self):
ans = None
name = str(self.objectName())
if name:
name += ' books view state'
db = getattr(self.model(), 'db', None)
if db is not None:
ans = db.new_api.pref(name)
if ans is None:
ans = gprefs.get(name, None)
try:
del gprefs[name]
except:
pass
if ans is not None:
db.new_api.set_pref(name, ans)
else:
injected = False
if not ans.get('last_modified_injected', False):
injected = True
ans['last_modified_injected'] = True
hc = ans.get('hidden_columns', [])
if 'last_modified' not in hc:
hc.append('last_modified')
if not ans.get('languages_injected', False):
injected = True
ans['languages_injected'] = True
hc = ans.get('hidden_columns', [])
if 'languages' not in hc:
hc.append('languages')
if injected:
db.new_api.set_pref(name, ans)
return ans
def restore_state(self):
old_state = self.get_old_state()
if old_state is None:
old_state = self.get_default_state()
max_levels = 3
if tweaks['sort_columns_at_startup'] is not None:
sh = []
try:
for c,d in tweaks['sort_columns_at_startup']:
if not isinstance(d, bool):
d = True if d == 0 else False
sh.append((c, d))
except:
# Ignore invalid tweak values as users seem to often get them
# wrong
print('Ignoring invalid sort_columns_at_startup tweak, with error:')
import traceback
traceback.print_exc()
old_state['sort_history'] = sh
max_levels = max(3, len(sh))
if self.is_library_view:
self.pin_view.restore_state()
self.column_header.blockSignals(True)
self.apply_state(old_state, max_sort_levels=max_levels)
self.column_header.blockSignals(False)
self.do_row_sizing()
self.was_restored = True
def refresh_row_sizing(self):
self.row_sizing_done = False
self.do_row_sizing()
def refresh_grid(self):
for wv in self, self.pin_view:
wv.setShowGrid(bool(gprefs['booklist_grid']))
def do_row_sizing(self):
# Resize all rows to have the correct height
if not self.row_sizing_done and self.model().rowCount(QModelIndex()) > 0:
vh = self.verticalHeader()
h = max(vh.minimumSectionSize(), self.default_row_height + gprefs['book_list_extra_row_spacing'])
vh.setDefaultSectionSize(h)
if self.is_library_view:
self.pin_view.verticalHeader().setDefaultSectionSize(h)
self._model.set_row_height(self.rowHeight(0))
self.row_sizing_done = True
def resize_column_to_fit(self, view, column):
col = self.column_map.index(column)
w = view.columnWidth(col)
restrict_column_width(view, col, w, w)
def fit_column_to_contents(self, view, column):
col = self.column_map.index(column)
view.resizeColumnToContents(col)
def column_resized(self, col, old_size, new_size):
restrict_column_width(self, col, old_size, new_size)
def pin_view_column_resized(self, col, old_size, new_size):
restrict_column_width(self.pin_view, col, old_size, new_size)
# }}}
# Initialization/Delegate Setup {{{
def set_database(self, db):
self.alternate_views.set_database(db)
self.save_state()
self._model.set_database(db)
self.tags_delegate.set_database(db)
self.cc_names_delegate.set_database(db)
self.authors_delegate.set_database(db)
self.series_delegate.set_auto_complete_function(db.all_series)
self.publisher_delegate.set_auto_complete_function(db.all_publishers)
self.alternate_views.set_database(db, stage=1)
def marked_changed(self, old_marked, current_marked):
self.alternate_views.marked_changed(old_marked, current_marked)
if bool(old_marked) == bool(current_marked):
changed = old_marked | current_marked
i = self.model().db.data.id_to_index
def f(x):
try:
return i(x)
except ValueError:
pass
sections = tuple(x for x in map(f, changed) if x is not None)
if sections:
self.row_header.headerDataChanged(Qt.Orientation.Vertical, min(sections), max(sections))
# This is needed otherwise Qt does not always update the
# viewport correctly. See https://bugs.launchpad.net/bugs/1404697
self.row_header.viewport().update()
else:
# Marked items have either appeared or all been removed
self.model().set_row_decoration(current_marked)
self.row_header.headerDataChanged(Qt.Orientation.Vertical, 0, self.row_header.count()-1)
self.row_header.geometriesChanged.emit()
self.set_row_header_visibility()
def set_row_header_visibility(self):
visible = self.model().row_decoration is not None or gprefs['row_numbers_in_book_list']
self.row_header.setVisible(visible)
def database_changed(self, db):
db.data.add_marked_listener(self.marked_changed_listener)
for i in range(self.model().columnCount(None)):
for vw in self, self.pin_view:
if vw.itemDelegateForColumn(i) in (
self.rating_delegate, self.timestamp_delegate, self.pubdate_delegate,
self.last_modified_delegate, self.languages_delegate, self.half_rating_delegate):
vw.setItemDelegateForColumn(i, vw.itemDelegate())
cm = self.column_map
def set_item_delegate(colhead, delegate):
idx = cm.index(colhead)
self.setItemDelegateForColumn(idx, delegate)
self.pin_view.setItemDelegateForColumn(idx, delegate)
for colhead in cm:
if self._model.is_custom_column(colhead):
cc = self._model.custom_columns[colhead]
if cc['datatype'] == 'datetime':
delegate = CcDateDelegate(self)
delegate.set_format(cc['display'].get('date_format',''))
set_item_delegate(colhead, delegate)
elif cc['datatype'] == 'comments':
ctype = cc['display'].get('interpret_as', 'html')
if ctype == 'short-text':
set_item_delegate(colhead, self.cc_text_delegate)
elif ctype in ('long-text', 'markdown'):
set_item_delegate(colhead, self.cc_longtext_delegate)
else:
set_item_delegate(colhead, self.cc_comments_delegate)
elif cc['datatype'] == 'text':
if cc['is_multiple']:
if cc['display'].get('is_names', False):
set_item_delegate(colhead, self.cc_names_delegate)
else:
set_item_delegate(colhead, self.tags_delegate)
else:
set_item_delegate(colhead, self.cc_text_delegate)
elif cc['datatype'] == 'series':
set_item_delegate(colhead, self.cc_series_delegate)
elif cc['datatype'] in ('int', 'float'):
set_item_delegate(colhead, self.cc_number_delegate)
elif cc['datatype'] == 'bool':
set_item_delegate(colhead, self.cc_bool_delegate)
elif cc['datatype'] == 'rating':
d = self.half_rating_delegate if cc['display'].get('allow_half_stars', False) else self.rating_delegate
set_item_delegate(colhead, d)
elif cc['datatype'] == 'composite':
set_item_delegate(colhead, self.cc_template_delegate)
elif cc['datatype'] == 'enumeration':
set_item_delegate(colhead, self.cc_enum_delegate)
else:
dattr = colhead+'_delegate'
delegate = colhead if hasattr(self, dattr) else 'text'
set_item_delegate(colhead, getattr(self, delegate+'_delegate'))
self.restore_state()
self.set_ondevice_column_visibility()
# in case there were marked books
self.model().set_row_decoration(set())
self.row_header.headerDataChanged(Qt.Orientation.Vertical, 0, self.row_header.count()-1)
self.row_header.geometriesChanged.emit()
# }}}
# Context Menu {{{
def set_context_menu(self, menu, edit_collections_action):
self.setContextMenuPolicy(Qt.ContextMenuPolicy.DefaultContextMenu)
self.context_menu = menu
self.alternate_views.set_context_menu(menu)
self.edit_collections_action = edit_collections_action
def show_context_menu(self, menu, event):
from calibre.gui2.main_window import clone_menu
m = clone_menu(menu) if islinux else menu
m.popup(event.globalPos())
event.accept()
def contextMenuEvent(self, event):
self.show_context_menu(self.context_menu, event)
# }}}
def handle_mouse_press_event(self, ev):
if QApplication.keyboardModifiers() & Qt.KeyboardModifier.ShiftModifier:
# Shift-Click in QTableView is badly behaved.
index = self.indexAt(ev.pos())
if not index.isValid():
return QTableView.mousePressEvent(self, ev)
ci = self.currentIndex()
if not ci.isValid():
return QTableView.mousePressEvent(self, ev)
clicked_row = index.row()
current_row = ci.row()
sm = self.selectionModel()
if clicked_row == current_row:
sm.setCurrentIndex(index, QItemSelectionModel.SelectionFlag.NoUpdate)
return
sr = sm.selectedRows()
if not len(sr):
sm.select(
index,
QItemSelectionModel.SelectionFlag.Select | QItemSelectionModel.SelectionFlag.Clear |
QItemSelectionModel.SelectionFlag.Current | QItemSelectionModel.SelectionFlag.Rows)
return
m = self.model()
def new_selection(upper, lower):
top_left = m.index(upper, 0)
bottom_right = m.index(lower, m.columnCount(None) - 1)
return QItemSelection(top_left, bottom_right)
currently_selected = tuple(x.row() for x in sr)
min_row = min(currently_selected)
max_row = max(currently_selected)
outside_current_selection = clicked_row < min_row or clicked_row > max_row
existing_selection = sm.selection()
if outside_current_selection:
# We simply extend the current selection
if clicked_row < min_row:
upper, lower = clicked_row, min_row
else:
upper, lower = max_row, clicked_row
existing_selection.merge(new_selection(upper, lower), QItemSelectionModel.SelectionFlag.Select)
else:
if current_row < clicked_row:
upper, lower = current_row, clicked_row
else:
upper, lower = clicked_row, current_row
existing_selection.merge(new_selection(upper, lower), QItemSelectionModel.SelectionFlag.Toggle)
sm.select(existing_selection, QItemSelectionModel.SelectionFlag.ClearAndSelect)
sm.setCurrentIndex(
# ensure clicked row is always selected
index, QItemSelectionModel.SelectionFlag.Select | QItemSelectionModel.SelectionFlag.Rows)
else:
return QTableView.mousePressEvent(self, ev)
@property
def column_map(self):
return self._model.column_map
@property
def visible_columns(self):
h = self.horizontalHeader()
logical_indices = (x for x in range(h.count()) if not h.isSectionHidden(x))
rmap = {i:x for i, x in enumerate(self.column_map)}
return (rmap[h.visualIndex(x)] for x in logical_indices if h.visualIndex(x) > -1)
def refresh_book_details(self, force=False):
idx = self.currentIndex()
if not idx.isValid() and force:
idx = self.model().index(0, 0)
if idx.isValid():
self._model.current_changed(idx, idx)
return True
return False
def indices_for_merge(self, resolved=False):
if not resolved:
return self.alternate_views.current_view.indices_for_merge(resolved=True)
return self.selectionModel().selectedRows()
def scrollContentsBy(self, dx, dy):
# Needed as Qt bug causes headerview to not always update when scrolling
QTableView.scrollContentsBy(self, dx, dy)
if dy != 0:
self.column_header.update()
def scroll_to_row(self, row):
if row > -1:
h = self.horizontalHeader()
for i in range(h.count()):
if not h.isSectionHidden(i) and h.sectionViewportPosition(i) >= 0:
self.scrollTo(self.model().index(row, i), QAbstractItemView.ScrollHint.PositionAtCenter)
break
@property
def current_book(self):
ci = self.currentIndex()
if ci.isValid():
try:
return self.model().db.data.index_to_id(ci.row())
except (IndexError, ValueError, KeyError, TypeError, AttributeError):
pass
def current_book_state(self):
return self.current_book, self.horizontalScrollBar().value(), self.pin_view.horizontalScrollBar().value()
def restore_current_book_state(self, state):
book_id, hpos, pv_hpos = state
try:
row = self.model().db.data.id_to_index(book_id)
except (IndexError, ValueError, KeyError, TypeError, AttributeError):
return
self.set_current_row(row)
self.scroll_to_row(row)
self.horizontalScrollBar().setValue(hpos)
if self.pin_view.isVisible():
self.pin_view.horizontalScrollBar().setValue(pv_hpos)
def set_current_row(self, row=0, select=True, for_sync=False):
if row > -1 and row < self.model().rowCount(QModelIndex()):
h = self.horizontalHeader()
logical_indices = list(range(h.count()))
logical_indices = [x for x in logical_indices if not
h.isSectionHidden(x)]
pairs = [(x, h.visualIndex(x)) for x in logical_indices if
h.visualIndex(x) > -1]
if not pairs:
pairs = [(0, 0)]
pairs.sort(key=lambda x: x[1])
i = pairs[0][0]
index = self.model().index(row, i)
if for_sync:
sm = self.selectionModel()
sm.setCurrentIndex(index, QItemSelectionModel.SelectionFlag.NoUpdate)
else:
self.setCurrentIndex(index)
if select:
sm = self.selectionModel()
sm.select(index, QItemSelectionModel.SelectionFlag.ClearAndSelect|QItemSelectionModel.SelectionFlag.Rows)
def select_cell(self, row_number=0, logical_column=0):
if row_number > -1 and row_number < self.model().rowCount(QModelIndex()):
index = self.model().index(row_number, logical_column)
self.setCurrentIndex(index)
sm = self.selectionModel()
sm.select(index, QItemSelectionModel.SelectionFlag.ClearAndSelect|QItemSelectionModel.SelectionFlag.Rows)
sm.select(index, QItemSelectionModel.SelectionFlag.Current)
self.clicked.emit(index)
def row_at_top(self):
pos = 0
while pos < 100:
ans = self.rowAt(pos)
if ans > -1:
return ans
pos += 5
def row_at_bottom(self):
pos = self.viewport().height()
limit = pos - 100
while pos > limit:
ans = self.rowAt(pos)
if ans > -1:
return ans
pos -= 5
def moveCursor(self, action, modifiers):
orig = self.currentIndex()
index = QTableView.moveCursor(self, action, modifiers)
if action == QAbstractItemView.CursorAction.MovePageDown:
moved = index.row() - orig.row()
try:
rows = self.row_at_bottom() - self.row_at_top()
except TypeError:
rows = moved
if moved > rows:
index = self.model().index(orig.row() + rows, index.column())
elif action == QAbstractItemView.CursorAction.MovePageUp:
moved = orig.row() - index.row()
try:
rows = self.row_at_bottom() - self.row_at_top()
except TypeError:
rows = moved
if moved > rows:
index = self.model().index(orig.row() - rows, index.column())
elif action == QAbstractItemView.CursorAction.MoveHome and modifiers & Qt.KeyboardModifier.ControlModifier:
return self.model().index(0, orig.column())
elif action == QAbstractItemView.CursorAction.MoveEnd and modifiers & Qt.KeyboardModifier.ControlModifier:
return self.model().index(self.model().rowCount(QModelIndex()) - 1, orig.column())
return index
def selectionCommand(self, index, event):
if event and event.type() == QEvent.Type.KeyPress and event.key() in (
Qt.Key.Key_Home, Qt.Key.Key_End) and event.modifiers() & Qt.KeyboardModifier.ControlModifier:
return QItemSelectionModel.SelectionFlag.ClearAndSelect | QItemSelectionModel.SelectionFlag.Rows
return super().selectionCommand(index, event)
def keyPressEvent(self, ev):
if handle_enter_press(self, ev):
return
return QTableView.keyPressEvent(self, ev)
def ids_to_rows(self, ids):
row_map = OrderedDict()
ids = frozenset(ids)
m = self.model()
for row in range(m.rowCount(QModelIndex())):
if len(row_map) >= len(ids):
break
c = m.id(row)
if c in ids:
row_map[c] = row
return row_map
def select_rows(self, identifiers, using_ids=True, change_current=True,
scroll=True):
'''
Select rows identified by identifiers. identifiers can be a set of ids,
row numbers or QModelIndexes.
'''
rows = {x.row() if hasattr(x, 'row') else x for x in
identifiers}
if using_ids:
rows = set()
identifiers = set(identifiers)
m = self.model()
for row in range(m.rowCount(QModelIndex())):
if m.id(row) in identifiers:
rows.add(row)
rows = list(sorted(rows))
if rows:
row = rows[0]
if change_current:
self.set_current_row(row, select=False)
if scroll:
self.scroll_to_row(row)
sm = self.selectionModel()
sel = QItemSelection()
m = self.model()
max_col = m.columnCount(QModelIndex()) - 1
# Create a range based selector for each set of contiguous rows
# as supplying selectors for each individual row causes very poor
# performance if a large number of rows has to be selected.
for k, g in itertools.groupby(enumerate(rows), lambda i_x:i_x[0]-i_x[1]):
group = list(map(operator.itemgetter(1), g))
sel.merge(QItemSelection(m.index(min(group), 0),
m.index(max(group), max_col)), QItemSelectionModel.SelectionFlag.Select)
sm.select(sel, QItemSelectionModel.SelectionFlag.ClearAndSelect)
return rows
def get_selected_ids(self, as_set=False):
ans = []
seen = set()
m = self.model()
for idx in self.selectedIndexes():
r = idx.row()
i = m.id(r)
if i not in seen:
ans.append(i)
seen.add(i)
return seen if as_set else ans
@property
def current_id(self):
try:
return self.model().id(self.currentIndex())
except:
pass
return None
@current_id.setter
def current_id(self, val):
if val is None:
return
m = self.model()
for row in range(m.rowCount(QModelIndex())):
if m.id(row) == val:
self.set_current_row(row, select=False)
break
def show_next_book(self):
ci = self.currentIndex()
if not ci.isValid():
self.set_current_row()
return
n = (ci.row() + 1) % self.model().rowCount(QModelIndex())
self.set_current_row(n)
@property
def next_id(self):
'''
Return the id of the 'next' row (i.e. the first unselected row after
the current row).
'''
ci = self.currentIndex()
if not ci.isValid():
return None
selected_rows = frozenset(i.row() for i in self.selectedIndexes() if
i.isValid())
column = ci.column()
for i in range(ci.row()+1, self.row_count()):
if i in selected_rows:
continue
try:
return self.model().id(self.model().index(i, column))
except:
pass
# No unselected rows after the current row, look before
for i in range(ci.row()-1, -1, -1):
if i in selected_rows:
continue
try:
return self.model().id(self.model().index(i, column))
except:
pass
return None
def close(self):
self._model.close()
def set_editable(self, editable, supports_backloading):
self._model.set_editable(editable)
def move_highlighted_row(self, forward):
rows = self.selectionModel().selectedRows()
if len(rows) > 0:
current_row = rows[0].row()
else:
current_row = None
id_to_select = self._model.get_next_highlighted_id(current_row, forward)
if id_to_select is not None:
self.select_rows([id_to_select], using_ids=True)
def search_proxy(self, txt):
if self.is_library_view:
# Save the current book before doing the search, after the search
# is completed, this book will become the current book and be
# scrolled to if it is present in the search results
self.alternate_views.save_current_book_state()
self._model.search(txt)
id_to_select = self._model.get_current_highlighted_id()
if id_to_select is not None:
self.select_rows([id_to_select], using_ids=True)
elif self._model.highlight_only:
self.clearSelection()
if self.isVisible() and getattr(txt, 'as_you_type', False) is not True:
self.setFocus(Qt.FocusReason.OtherFocusReason)
def connect_to_search_box(self, sb, search_done):
sb.search.connect(self.search_proxy)
self._search_done = search_done
self._model.searched.connect(self.search_done)
if self.is_library_view:
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)
def search_done(self, ok):
self._search_done(self, ok)
def row_count(self):
return self._model.count()
# }}}
class DeviceBooksView(BooksView): # {{{
is_library_view = False
def __init__(self, parent):
BooksView.__init__(self, parent, DeviceBooksModel,
use_edit_metadata_dialog=False)
self._model.resize_rows.connect(self.do_row_sizing,
type=Qt.ConnectionType.QueuedConnection)
self.can_add_columns = False
self.resize_on_select = False
self.rating_delegate = None
self.half_rating_delegate = None
for i in range(10):
self.setItemDelegateForColumn(i, TextDelegate(self))
self.setDragDropMode(QAbstractItemView.DragDropMode.NoDragDrop)
self.setAcceptDrops(False)
self.set_row_header_visibility()
def set_row_header_visibility(self):
self.row_header.setVisible(gprefs['row_numbers_in_book_list'])
def drag_data(self):
m = self.model()
rows = self.selectionModel().selectedRows()
paths = [force_unicode(p, enc=filesystem_encoding) for p in m.paths(rows) if p]
md = QMimeData()
md.setData('application/calibre+from_device', b'dummy')
md.setUrls([QUrl.fromLocalFile(p) for p in paths])
drag = QDrag(self)
drag.setMimeData(md)
cover = self.drag_icon(m.cover(self.currentIndex().row()), len(paths) > 1)
drag.setHotSpot(QPoint(-15, -15))
drag.setPixmap(cover)
return drag
def contextMenuEvent(self, event):
edit_collections = callable(getattr(self._model.db, 'supports_collections', None)) and \
self._model.db.supports_collections() and \
prefs['manage_device_metadata'] == 'manual'
self.edit_collections_action.setVisible(edit_collections)
self.context_menu.popup(event.globalPos())
event.accept()
def get_old_state(self):
ans = None
name = str(self.objectName())
if name:
name += ' books view state'
ans = gprefs.get(name, None)
return ans
def write_state(self, state):
name = str(self.objectName())
if name:
gprefs.set(name + ' books view state', state)
def set_database(self, db):
self._model.set_database(db)
self.restore_state()
def connect_dirtied_signal(self, slot):
self._model.booklist_dirtied.connect(slot)
def connect_upload_collections_signal(self, func=None, oncard=None):
self._model.upload_collections.connect(partial(func, view=self, oncard=oncard))
def dropEvent(self, *args):
error_dialog(self, _('Not allowed'),
_('Dropping onto a device is not supported. First add the book to the calibre library.')).exec()
def set_editable(self, editable, supports_backloading):
self._model.set_editable(editable)
self.drag_allowed = supports_backloading
def resort(self):
h = self.horizontalHeader()
self.model().sort(h.sortIndicatorSection(), h.sortIndicatorOrder())
def reverse_sort(self):
h = self.horizontalHeader()
h.setSortIndicator(h.sortIndicatorSection(), 1 - int(h.sortIndicatorOrder()))
# }}}