mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Merge from custcol trunk
This commit is contained in:
commit
bcd5d792b2
@ -85,10 +85,10 @@ class USBMS(CLI, Device):
|
|||||||
lpath = lpath.replace('\\', '/')
|
lpath = lpath.replace('\\', '/')
|
||||||
idx = bl_cache.get(lpath, None)
|
idx = bl_cache.get(lpath, None)
|
||||||
if idx is not None:
|
if idx is not None:
|
||||||
|
bl_cache[lpath] = None
|
||||||
if self.update_metadata_item(bl[idx]):
|
if self.update_metadata_item(bl[idx]):
|
||||||
#print 'update_metadata_item returned true'
|
#print 'update_metadata_item returned true'
|
||||||
changed = True
|
changed = True
|
||||||
bl_cache[lpath] = None
|
|
||||||
else:
|
else:
|
||||||
#print "adding new book", lpath
|
#print "adding new book", lpath
|
||||||
if bl.add_book(self.book_from_path(prefix, lpath),
|
if bl.add_book(self.book_from_path(prefix, lpath),
|
||||||
@ -130,7 +130,7 @@ class USBMS(CLI, Device):
|
|||||||
# Remove books that are no longer in the filesystem. Cache contains
|
# Remove books that are no longer in the filesystem. Cache contains
|
||||||
# indices into the booklist if book not in filesystem, None otherwise
|
# indices into the booklist if book not in filesystem, None otherwise
|
||||||
# Do the operation in reverse order so indices remain valid
|
# Do the operation in reverse order so indices remain valid
|
||||||
for idx in bl_cache.itervalues().reversed():
|
for idx in sorted(bl_cache.itervalues(), reverse=True):
|
||||||
if idx is not None:
|
if idx is not None:
|
||||||
need_sync = True
|
need_sync = True
|
||||||
del bl[idx]
|
del bl[idx]
|
||||||
|
@ -32,8 +32,7 @@ class SchedulerDialog(QDialog, Ui_Dialog):
|
|||||||
self.search.setMinimumContentsLength(25)
|
self.search.setMinimumContentsLength(25)
|
||||||
self.search.initialize('scheduler_search_history')
|
self.search.initialize('scheduler_search_history')
|
||||||
self.recipe_box.layout().insertWidget(0, self.search)
|
self.recipe_box.layout().insertWidget(0, self.search)
|
||||||
self.connect(self.search, SIGNAL('search(PyQt_PyObject,PyQt_PyObject)'),
|
self.search.search.connect(self.recipe_model.search)
|
||||||
self.recipe_model.search)
|
|
||||||
self.connect(self.recipe_model, SIGNAL('searched(PyQt_PyObject)'),
|
self.connect(self.recipe_model, SIGNAL('searched(PyQt_PyObject)'),
|
||||||
self.search.search_done)
|
self.search.search_done)
|
||||||
self.connect(self.recipe_model, SIGNAL('searched(PyQt_PyObject)'),
|
self.connect(self.recipe_model, SIGNAL('searched(PyQt_PyObject)'),
|
||||||
|
10
src/calibre/gui2/library/__init__.py
Normal file
10
src/calibre/gui2/library/__init__.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
from PyQt4.Qt import Qt
|
||||||
|
|
||||||
|
DEFAULT_SORT = ('timestamp', Qt.DescendingOrder)
|
311
src/calibre/gui2/library/delegates.py
Normal file
311
src/calibre/gui2/library/delegates.py
Normal file
@ -0,0 +1,311 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from math import cos, sin, pi
|
||||||
|
|
||||||
|
from PyQt4.Qt import QColor, Qt, QModelIndex, QSize, \
|
||||||
|
QPainterPath, QLinearGradient, QBrush, \
|
||||||
|
QPen, QStyle, QPainter, QStyleOptionViewItemV4, \
|
||||||
|
QIcon, QDoubleSpinBox, QVariant, QSpinBox, \
|
||||||
|
QStyledItemDelegate, QCompleter, \
|
||||||
|
QComboBox
|
||||||
|
|
||||||
|
from calibre.gui2 import UNDEFINED_QDATE
|
||||||
|
from calibre.gui2.widgets import EnLineEdit, TagsLineEdit
|
||||||
|
from calibre.utils.date import now
|
||||||
|
from calibre.utils.config import tweaks
|
||||||
|
from calibre.gui2.dialogs.comments_dialog import CommentsDialog
|
||||||
|
|
||||||
|
class RatingDelegate(QStyledItemDelegate): # {{{
|
||||||
|
COLOR = QColor("blue")
|
||||||
|
SIZE = 16
|
||||||
|
PEN = QPen(COLOR, 1, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)
|
||||||
|
|
||||||
|
def __init__(self, parent):
|
||||||
|
QStyledItemDelegate.__init__(self, parent)
|
||||||
|
self._parent = parent
|
||||||
|
self.dummy = QModelIndex()
|
||||||
|
self.star_path = QPainterPath()
|
||||||
|
self.star_path.moveTo(90, 50)
|
||||||
|
for i in range(1, 5):
|
||||||
|
self.star_path.lineTo(50 + 40 * cos(0.8 * i * pi), \
|
||||||
|
50 + 40 * sin(0.8 * i * pi))
|
||||||
|
self.star_path.closeSubpath()
|
||||||
|
self.star_path.setFillRule(Qt.WindingFill)
|
||||||
|
gradient = QLinearGradient(0, 0, 0, 100)
|
||||||
|
gradient.setColorAt(0.0, self.COLOR)
|
||||||
|
gradient.setColorAt(1.0, self.COLOR)
|
||||||
|
self.brush = QBrush(gradient)
|
||||||
|
self.factor = self.SIZE/100.
|
||||||
|
|
||||||
|
def sizeHint(self, option, index):
|
||||||
|
#num = index.model().data(index, Qt.DisplayRole).toInt()[0]
|
||||||
|
return QSize(5*(self.SIZE), self.SIZE+4)
|
||||||
|
|
||||||
|
def paint(self, painter, option, index):
|
||||||
|
style = self._parent.style()
|
||||||
|
option = QStyleOptionViewItemV4(option)
|
||||||
|
self.initStyleOption(option, self.dummy)
|
||||||
|
num = index.model().data(index, Qt.DisplayRole).toInt()[0]
|
||||||
|
def draw_star():
|
||||||
|
painter.save()
|
||||||
|
painter.scale(self.factor, self.factor)
|
||||||
|
painter.translate(50.0, 50.0)
|
||||||
|
painter.rotate(-20)
|
||||||
|
painter.translate(-50.0, -50.0)
|
||||||
|
painter.drawPath(self.star_path)
|
||||||
|
painter.restore()
|
||||||
|
|
||||||
|
painter.save()
|
||||||
|
if hasattr(QStyle, 'CE_ItemViewItem'):
|
||||||
|
style.drawControl(QStyle.CE_ItemViewItem, option,
|
||||||
|
painter, self._parent)
|
||||||
|
elif option.state & QStyle.State_Selected:
|
||||||
|
painter.fillRect(option.rect, option.palette.highlight())
|
||||||
|
try:
|
||||||
|
painter.setRenderHint(QPainter.Antialiasing)
|
||||||
|
painter.setClipRect(option.rect)
|
||||||
|
y = option.rect.center().y()-self.SIZE/2.
|
||||||
|
x = option.rect.left()
|
||||||
|
painter.setPen(self.PEN)
|
||||||
|
painter.setBrush(self.brush)
|
||||||
|
painter.translate(x, y)
|
||||||
|
i = 0
|
||||||
|
while i < num:
|
||||||
|
draw_star()
|
||||||
|
painter.translate(self.SIZE, 0)
|
||||||
|
i += 1
|
||||||
|
except:
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
painter.restore()
|
||||||
|
|
||||||
|
def createEditor(self, parent, option, index):
|
||||||
|
sb = QStyledItemDelegate.createEditor(self, parent, option, index)
|
||||||
|
sb.setMinimum(0)
|
||||||
|
sb.setMaximum(5)
|
||||||
|
return sb
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
class DateDelegate(QStyledItemDelegate): # {{{
|
||||||
|
|
||||||
|
def displayText(self, val, locale):
|
||||||
|
d = val.toDate()
|
||||||
|
if d == UNDEFINED_QDATE:
|
||||||
|
return ''
|
||||||
|
return d.toString('dd MMM yyyy')
|
||||||
|
|
||||||
|
def createEditor(self, parent, option, index):
|
||||||
|
qde = QStyledItemDelegate.createEditor(self, parent, option, index)
|
||||||
|
stdformat = unicode(qde.displayFormat())
|
||||||
|
if 'yyyy' not in stdformat:
|
||||||
|
stdformat = stdformat.replace('yy', 'yyyy')
|
||||||
|
qde.setDisplayFormat(stdformat)
|
||||||
|
qde.setMinimumDate(UNDEFINED_QDATE)
|
||||||
|
qde.setSpecialValueText(_('Undefined'))
|
||||||
|
qde.setCalendarPopup(True)
|
||||||
|
return qde
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
class PubDateDelegate(QStyledItemDelegate): # {{{
|
||||||
|
|
||||||
|
def displayText(self, val, locale):
|
||||||
|
d = val.toDate()
|
||||||
|
if d == UNDEFINED_QDATE:
|
||||||
|
return ''
|
||||||
|
format = tweaks['gui_pubdate_display_format']
|
||||||
|
if format is None:
|
||||||
|
format = 'MMM yyyy'
|
||||||
|
return d.toString(format)
|
||||||
|
|
||||||
|
def createEditor(self, parent, option, index):
|
||||||
|
qde = QStyledItemDelegate.createEditor(self, parent, option, index)
|
||||||
|
qde.setDisplayFormat('MM yyyy')
|
||||||
|
qde.setMinimumDate(UNDEFINED_QDATE)
|
||||||
|
qde.setSpecialValueText(_('Undefined'))
|
||||||
|
qde.setCalendarPopup(True)
|
||||||
|
return qde
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
class TextDelegate(QStyledItemDelegate): # {{{
|
||||||
|
def __init__(self, parent):
|
||||||
|
'''
|
||||||
|
Delegate for text data. If auto_complete_function needs to return a list
|
||||||
|
of text items to auto-complete with. The funciton is None no
|
||||||
|
auto-complete will be used.
|
||||||
|
'''
|
||||||
|
QStyledItemDelegate.__init__(self, parent)
|
||||||
|
self.auto_complete_function = None
|
||||||
|
|
||||||
|
def set_auto_complete_function(self, f):
|
||||||
|
self.auto_complete_function = f
|
||||||
|
|
||||||
|
def createEditor(self, parent, option, index):
|
||||||
|
editor = EnLineEdit(parent)
|
||||||
|
if self.auto_complete_function:
|
||||||
|
complete_items = [i[1] for i in self.auto_complete_function()]
|
||||||
|
completer = QCompleter(complete_items, self)
|
||||||
|
completer.setCaseSensitivity(Qt.CaseInsensitive)
|
||||||
|
completer.setCompletionMode(QCompleter.InlineCompletion)
|
||||||
|
editor.setCompleter(completer)
|
||||||
|
return editor
|
||||||
|
#}}}
|
||||||
|
|
||||||
|
class TagsDelegate(QStyledItemDelegate): # {{{
|
||||||
|
def __init__(self, parent):
|
||||||
|
QStyledItemDelegate.__init__(self, parent)
|
||||||
|
self.db = None
|
||||||
|
|
||||||
|
def set_database(self, db):
|
||||||
|
self.db = db
|
||||||
|
|
||||||
|
def createEditor(self, parent, option, index):
|
||||||
|
if self.db:
|
||||||
|
col = index.model().column_map[index.column()]
|
||||||
|
if not index.model().is_custom_column(col):
|
||||||
|
editor = TagsLineEdit(parent, self.db.all_tags())
|
||||||
|
else:
|
||||||
|
editor = TagsLineEdit(parent, sorted(list(self.db.all_custom(label=col))))
|
||||||
|
return editor
|
||||||
|
else:
|
||||||
|
editor = EnLineEdit(parent)
|
||||||
|
return editor
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
class CcDateDelegate(QStyledItemDelegate): # {{{
|
||||||
|
'''
|
||||||
|
Delegate for custom columns dates. Because this delegate stores the
|
||||||
|
format as an instance variable, a new instance must be created for each
|
||||||
|
column. This differs from all the other delegates.
|
||||||
|
'''
|
||||||
|
|
||||||
|
def set_format(self, format):
|
||||||
|
if not format:
|
||||||
|
self.format = 'dd MMM yyyy'
|
||||||
|
else:
|
||||||
|
self.format = format
|
||||||
|
|
||||||
|
def displayText(self, val, locale):
|
||||||
|
d = val.toDate()
|
||||||
|
if d == UNDEFINED_QDATE:
|
||||||
|
return ''
|
||||||
|
return d.toString(self.format)
|
||||||
|
|
||||||
|
def createEditor(self, parent, option, index):
|
||||||
|
qde = QStyledItemDelegate.createEditor(self, parent, option, index)
|
||||||
|
qde.setDisplayFormat(self.format)
|
||||||
|
qde.setMinimumDate(UNDEFINED_QDATE)
|
||||||
|
qde.setSpecialValueText(_('Undefined'))
|
||||||
|
qde.setCalendarPopup(True)
|
||||||
|
return qde
|
||||||
|
|
||||||
|
def setEditorData(self, editor, index):
|
||||||
|
m = index.model()
|
||||||
|
# db col is not named for the field, but for the table number. To get it,
|
||||||
|
# gui column -> column label -> table number -> db column
|
||||||
|
val = m.db.data[index.row()][m.db.FIELD_MAP[m.custom_columns[m.column_map[index.column()]]['num']]]
|
||||||
|
if val is None:
|
||||||
|
val = now()
|
||||||
|
editor.setDate(val)
|
||||||
|
|
||||||
|
def setModelData(self, editor, model, index):
|
||||||
|
val = editor.date()
|
||||||
|
if val == UNDEFINED_QDATE:
|
||||||
|
val = None
|
||||||
|
model.setData(index, QVariant(val), Qt.EditRole)
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
class CcTextDelegate(QStyledItemDelegate): # {{{
|
||||||
|
'''
|
||||||
|
Delegate for text/int/float data.
|
||||||
|
'''
|
||||||
|
|
||||||
|
def createEditor(self, parent, option, index):
|
||||||
|
m = index.model()
|
||||||
|
col = m.column_map[index.column()]
|
||||||
|
typ = m.custom_columns[col]['datatype']
|
||||||
|
if typ == 'int':
|
||||||
|
editor = QSpinBox(parent)
|
||||||
|
editor.setRange(-100, sys.maxint)
|
||||||
|
editor.setSpecialValueText(_('Undefined'))
|
||||||
|
editor.setSingleStep(1)
|
||||||
|
elif typ == 'float':
|
||||||
|
editor = QDoubleSpinBox(parent)
|
||||||
|
editor.setSpecialValueText(_('Undefined'))
|
||||||
|
editor.setRange(-100., float(sys.maxint))
|
||||||
|
editor.setDecimals(2)
|
||||||
|
else:
|
||||||
|
editor = EnLineEdit(parent)
|
||||||
|
complete_items = sorted(list(m.db.all_custom(label=col)))
|
||||||
|
completer = QCompleter(complete_items, self)
|
||||||
|
completer.setCaseSensitivity(Qt.CaseInsensitive)
|
||||||
|
completer.setCompletionMode(QCompleter.PopupCompletion)
|
||||||
|
editor.setCompleter(completer)
|
||||||
|
return editor
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
class CcCommentsDelegate(QStyledItemDelegate): # {{{
|
||||||
|
'''
|
||||||
|
Delegate for comments data.
|
||||||
|
'''
|
||||||
|
|
||||||
|
def createEditor(self, parent, option, index):
|
||||||
|
m = index.model()
|
||||||
|
col = m.column_map[index.column()]
|
||||||
|
# db col is not named for the field, but for the table number. To get it,
|
||||||
|
# gui column -> column label -> table number -> db column
|
||||||
|
text = m.db.data[index.row()][m.db.FIELD_MAP[m.custom_columns[col]['num']]]
|
||||||
|
editor = CommentsDialog(parent, text)
|
||||||
|
d = editor.exec_()
|
||||||
|
if d:
|
||||||
|
m.setData(index, QVariant(editor.textbox.toPlainText()), Qt.EditRole)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def setModelData(self, editor, model, index):
|
||||||
|
model.setData(index, QVariant(editor.textbox.toPlainText()), Qt.EditRole)
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
class CcBoolDelegate(QStyledItemDelegate): # {{{
|
||||||
|
def __init__(self, parent):
|
||||||
|
'''
|
||||||
|
Delegate for custom_column bool data.
|
||||||
|
'''
|
||||||
|
QStyledItemDelegate.__init__(self, parent)
|
||||||
|
|
||||||
|
def createEditor(self, parent, option, index):
|
||||||
|
editor = QComboBox(parent)
|
||||||
|
items = [_('Y'), _('N'), ' ']
|
||||||
|
icons = [I('ok.svg'), I('list_remove.svg'), I('blank.svg')]
|
||||||
|
if tweaks['bool_custom_columns_are_tristate'] == 'no':
|
||||||
|
items = items[:-1]
|
||||||
|
icons = icons[:-1]
|
||||||
|
for icon, text in zip(icons, items):
|
||||||
|
editor.addItem(QIcon(icon), text)
|
||||||
|
return editor
|
||||||
|
|
||||||
|
def setModelData(self, editor, model, index):
|
||||||
|
val = {0:True, 1:False, 2:None}[editor.currentIndex()]
|
||||||
|
model.setData(index, QVariant(val), Qt.EditRole)
|
||||||
|
|
||||||
|
def setEditorData(self, editor, index):
|
||||||
|
m = index.model()
|
||||||
|
# db col is not named for the field, but for the table number. To get it,
|
||||||
|
# gui column -> column label -> table number -> db column
|
||||||
|
val = m.db.data[index.row()][m.db.FIELD_MAP[m.custom_columns[m.column_map[index.column()]]['num']]]
|
||||||
|
if tweaks['bool_custom_columns_are_tristate'] == 'no':
|
||||||
|
val = 1 if not val else 0
|
||||||
|
else:
|
||||||
|
val = 2 if val is None else 1 if not val else 0
|
||||||
|
editor.setCurrentIndex(val)
|
||||||
|
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
@ -1,318 +1,47 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||||
|
|
||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
import os, textwrap, traceback, re, shutil, functools, sys
|
import shutil, functools, re, os, traceback
|
||||||
|
|
||||||
from operator import attrgetter
|
|
||||||
from math import cos, sin, pi
|
|
||||||
from contextlib import closing
|
from contextlib import closing
|
||||||
|
from operator import attrgetter
|
||||||
|
|
||||||
from PyQt4.QtGui import QTableView, QAbstractItemView, QColor, \
|
from PyQt4.Qt import QAbstractTableModel, Qt, pyqtSignal, QIcon, QImage, \
|
||||||
QPainterPath, QLinearGradient, QBrush, \
|
QModelIndex, QVariant, QDate
|
||||||
QPen, QStyle, QPainter, QStyleOptionViewItemV4, \
|
|
||||||
QIcon, QImage, QMenu, QSpinBox, QDoubleSpinBox, \
|
|
||||||
QStyledItemDelegate, QCompleter, \
|
|
||||||
QComboBox
|
|
||||||
from PyQt4.QtCore import QAbstractTableModel, QVariant, Qt, pyqtSignal, \
|
|
||||||
SIGNAL, QObject, QSize, QModelIndex, QDate
|
|
||||||
|
|
||||||
from calibre import strftime
|
from calibre.gui2 import NONE, config, UNDEFINED_QDATE
|
||||||
|
from calibre.utils.pyparsing import ParseException
|
||||||
from calibre.ebooks.metadata import fmt_sidx, authors_to_string, string_to_authors
|
from calibre.ebooks.metadata import fmt_sidx, authors_to_string, string_to_authors
|
||||||
from calibre.ebooks.metadata.meta import set_metadata as _set_metadata
|
|
||||||
from calibre.gui2 import NONE, TableView, config, error_dialog, UNDEFINED_QDATE
|
|
||||||
from calibre.gui2.dialogs.comments_dialog import CommentsDialog
|
|
||||||
from calibre.gui2.widgets import EnLineEdit, TagsLineEdit
|
|
||||||
from calibre.library.caches import _match, CONTAINS_MATCH, EQUALS_MATCH, REGEXP_MATCH
|
|
||||||
from calibre.ptempfile import PersistentTemporaryFile
|
from calibre.ptempfile import PersistentTemporaryFile
|
||||||
from calibre.utils.config import tweaks
|
from calibre.utils.config import tweaks
|
||||||
from calibre.utils.date import dt_factory, qt_to_dt, isoformat, now
|
from calibre.utils.date import dt_factory, qt_to_dt, isoformat
|
||||||
from calibre.utils.pyparsing import ParseException
|
from calibre.ebooks.metadata.meta import set_metadata as _set_metadata
|
||||||
from calibre.utils.search_query_parser import SearchQueryParser
|
from calibre.utils.search_query_parser import SearchQueryParser
|
||||||
|
from calibre.library.caches import _match, CONTAINS_MATCH, EQUALS_MATCH, REGEXP_MATCH
|
||||||
|
from calibre import strftime
|
||||||
|
from calibre.gui2.library import DEFAULT_SORT
|
||||||
|
|
||||||
# Delegates {{{
|
def human_readable(size, precision=1):
|
||||||
class RatingDelegate(QStyledItemDelegate):
|
""" Convert a size in bytes into megabytes """
|
||||||
COLOR = QColor("blue")
|
return ('%.'+str(precision)+'f') % ((size/(1024.*1024.)),)
|
||||||
SIZE = 16
|
|
||||||
PEN = QPen(COLOR, 1, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)
|
|
||||||
|
|
||||||
def __init__(self, parent):
|
TIME_FMT = '%d %b %Y'
|
||||||
QStyledItemDelegate.__init__(self, parent)
|
|
||||||
self._parent = parent
|
|
||||||
self.dummy = QModelIndex()
|
|
||||||
self.star_path = QPainterPath()
|
|
||||||
self.star_path.moveTo(90, 50)
|
|
||||||
for i in range(1, 5):
|
|
||||||
self.star_path.lineTo(50 + 40 * cos(0.8 * i * pi), \
|
|
||||||
50 + 40 * sin(0.8 * i * pi))
|
|
||||||
self.star_path.closeSubpath()
|
|
||||||
self.star_path.setFillRule(Qt.WindingFill)
|
|
||||||
gradient = QLinearGradient(0, 0, 0, 100)
|
|
||||||
gradient.setColorAt(0.0, self.COLOR)
|
|
||||||
gradient.setColorAt(1.0, self.COLOR)
|
|
||||||
self.brush = QBrush(gradient)
|
|
||||||
self.factor = self.SIZE/100.
|
|
||||||
|
|
||||||
def sizeHint(self, option, index):
|
|
||||||
#num = index.model().data(index, Qt.DisplayRole).toInt()[0]
|
|
||||||
return QSize(5*(self.SIZE), self.SIZE+4)
|
|
||||||
|
|
||||||
def paint(self, painter, option, index):
|
|
||||||
style = self._parent.style()
|
|
||||||
option = QStyleOptionViewItemV4(option)
|
|
||||||
self.initStyleOption(option, self.dummy)
|
|
||||||
num = index.model().data(index, Qt.DisplayRole).toInt()[0]
|
|
||||||
def draw_star():
|
|
||||||
painter.save()
|
|
||||||
painter.scale(self.factor, self.factor)
|
|
||||||
painter.translate(50.0, 50.0)
|
|
||||||
painter.rotate(-20)
|
|
||||||
painter.translate(-50.0, -50.0)
|
|
||||||
painter.drawPath(self.star_path)
|
|
||||||
painter.restore()
|
|
||||||
|
|
||||||
painter.save()
|
|
||||||
if hasattr(QStyle, 'CE_ItemViewItem'):
|
|
||||||
style.drawControl(QStyle.CE_ItemViewItem, option,
|
|
||||||
painter, self._parent)
|
|
||||||
elif option.state & QStyle.State_Selected:
|
|
||||||
painter.fillRect(option.rect, option.palette.highlight())
|
|
||||||
try:
|
|
||||||
painter.setRenderHint(QPainter.Antialiasing)
|
|
||||||
painter.setClipRect(option.rect)
|
|
||||||
y = option.rect.center().y()-self.SIZE/2.
|
|
||||||
x = option.rect.left()
|
|
||||||
painter.setPen(self.PEN)
|
|
||||||
painter.setBrush(self.brush)
|
|
||||||
painter.translate(x, y)
|
|
||||||
i = 0
|
|
||||||
while i < num:
|
|
||||||
draw_star()
|
|
||||||
painter.translate(self.SIZE, 0)
|
|
||||||
i += 1
|
|
||||||
except:
|
|
||||||
traceback.print_exc()
|
|
||||||
painter.restore()
|
|
||||||
|
|
||||||
def createEditor(self, parent, option, index):
|
|
||||||
sb = QStyledItemDelegate.createEditor(self, parent, option, index)
|
|
||||||
sb.setMinimum(0)
|
|
||||||
sb.setMaximum(5)
|
|
||||||
return sb
|
|
||||||
|
|
||||||
class DateDelegate(QStyledItemDelegate):
|
|
||||||
|
|
||||||
def displayText(self, val, locale):
|
|
||||||
d = val.toDate()
|
|
||||||
if d == UNDEFINED_QDATE:
|
|
||||||
return ''
|
|
||||||
return d.toString('dd MMM yyyy')
|
|
||||||
|
|
||||||
def createEditor(self, parent, option, index):
|
|
||||||
qde = QStyledItemDelegate.createEditor(self, parent, option, index)
|
|
||||||
stdformat = unicode(qde.displayFormat())
|
|
||||||
if 'yyyy' not in stdformat:
|
|
||||||
stdformat = stdformat.replace('yy', 'yyyy')
|
|
||||||
qde.setDisplayFormat(stdformat)
|
|
||||||
qde.setMinimumDate(UNDEFINED_QDATE)
|
|
||||||
qde.setSpecialValueText(_('Undefined'))
|
|
||||||
qde.setCalendarPopup(True)
|
|
||||||
return qde
|
|
||||||
|
|
||||||
class PubDateDelegate(QStyledItemDelegate):
|
|
||||||
|
|
||||||
def displayText(self, val, locale):
|
|
||||||
d = val.toDate()
|
|
||||||
if d == UNDEFINED_QDATE:
|
|
||||||
return ''
|
|
||||||
format = tweaks['gui_pubdate_display_format']
|
|
||||||
if format is None:
|
|
||||||
format = 'MMM yyyy'
|
|
||||||
return d.toString(format)
|
|
||||||
|
|
||||||
def createEditor(self, parent, option, index):
|
|
||||||
qde = QStyledItemDelegate.createEditor(self, parent, option, index)
|
|
||||||
qde.setDisplayFormat('MM yyyy')
|
|
||||||
qde.setMinimumDate(UNDEFINED_QDATE)
|
|
||||||
qde.setSpecialValueText(_('Undefined'))
|
|
||||||
qde.setCalendarPopup(True)
|
|
||||||
return qde
|
|
||||||
|
|
||||||
class TextDelegate(QStyledItemDelegate):
|
|
||||||
def __init__(self, parent):
|
|
||||||
'''
|
|
||||||
Delegate for text data. If auto_complete_function needs to return a list
|
|
||||||
of text items to auto-complete with. The funciton is None no
|
|
||||||
auto-complete will be used.
|
|
||||||
'''
|
|
||||||
QStyledItemDelegate.__init__(self, parent)
|
|
||||||
self.auto_complete_function = None
|
|
||||||
|
|
||||||
def set_auto_complete_function(self, f):
|
|
||||||
self.auto_complete_function = f
|
|
||||||
|
|
||||||
def createEditor(self, parent, option, index):
|
|
||||||
editor = EnLineEdit(parent)
|
|
||||||
if self.auto_complete_function:
|
|
||||||
complete_items = [i[1] for i in self.auto_complete_function()]
|
|
||||||
completer = QCompleter(complete_items, self)
|
|
||||||
completer.setCaseSensitivity(Qt.CaseInsensitive)
|
|
||||||
completer.setCompletionMode(QCompleter.InlineCompletion)
|
|
||||||
editor.setCompleter(completer)
|
|
||||||
return editor
|
|
||||||
|
|
||||||
class TagsDelegate(QStyledItemDelegate):
|
|
||||||
def __init__(self, parent):
|
|
||||||
QStyledItemDelegate.__init__(self, parent)
|
|
||||||
self.db = None
|
|
||||||
|
|
||||||
def set_database(self, db):
|
|
||||||
self.db = db
|
|
||||||
|
|
||||||
def createEditor(self, parent, option, index):
|
|
||||||
if self.db:
|
|
||||||
col = index.model().column_map[index.column()]
|
|
||||||
if not index.model().is_custom_column(col):
|
|
||||||
editor = TagsLineEdit(parent, self.db.all_tags())
|
|
||||||
else:
|
|
||||||
editor = TagsLineEdit(parent, sorted(list(self.db.all_custom(label=col))))
|
|
||||||
return editor
|
|
||||||
else:
|
|
||||||
editor = EnLineEdit(parent)
|
|
||||||
return editor
|
|
||||||
|
|
||||||
class CcDateDelegate(QStyledItemDelegate):
|
|
||||||
'''
|
|
||||||
Delegate for custom columns dates. Because this delegate stores the
|
|
||||||
format as an instance variable, a new instance must be created for each
|
|
||||||
column. This differs from all the other delegates.
|
|
||||||
'''
|
|
||||||
|
|
||||||
def set_format(self, format):
|
|
||||||
if not format:
|
|
||||||
self.format = 'dd MMM yyyy'
|
|
||||||
else:
|
|
||||||
self.format = format
|
|
||||||
|
|
||||||
def displayText(self, val, locale):
|
|
||||||
d = val.toDate()
|
|
||||||
if d == UNDEFINED_QDATE:
|
|
||||||
return ''
|
|
||||||
return d.toString(self.format)
|
|
||||||
|
|
||||||
def createEditor(self, parent, option, index):
|
|
||||||
qde = QStyledItemDelegate.createEditor(self, parent, option, index)
|
|
||||||
qde.setDisplayFormat(self.format)
|
|
||||||
qde.setMinimumDate(UNDEFINED_QDATE)
|
|
||||||
qde.setSpecialValueText(_('Undefined'))
|
|
||||||
qde.setCalendarPopup(True)
|
|
||||||
return qde
|
|
||||||
|
|
||||||
def setEditorData(self, editor, index):
|
|
||||||
m = index.model()
|
|
||||||
# db col is not named for the field, but for the table number. To get it,
|
|
||||||
# gui column -> column label -> table number -> db column
|
|
||||||
val = m.db.data[index.row()][m.db.FIELD_MAP[m.custom_columns[m.column_map[index.column()]]['num']]]
|
|
||||||
if val is None:
|
|
||||||
val = now()
|
|
||||||
editor.setDate(val)
|
|
||||||
|
|
||||||
def setModelData(self, editor, model, index):
|
|
||||||
val = editor.date()
|
|
||||||
if val == UNDEFINED_QDATE:
|
|
||||||
val = None
|
|
||||||
model.setData(index, QVariant(val), Qt.EditRole)
|
|
||||||
|
|
||||||
class CcTextDelegate(QStyledItemDelegate):
|
|
||||||
'''
|
|
||||||
Delegate for text/int/float data.
|
|
||||||
'''
|
|
||||||
|
|
||||||
def createEditor(self, parent, option, index):
|
|
||||||
m = index.model()
|
|
||||||
col = m.column_map[index.column()]
|
|
||||||
typ = m.custom_columns[col]['datatype']
|
|
||||||
if typ == 'int':
|
|
||||||
editor = QSpinBox(parent)
|
|
||||||
editor.setRange(-100, sys.maxint)
|
|
||||||
editor.setSpecialValueText(_('Undefined'))
|
|
||||||
editor.setSingleStep(1)
|
|
||||||
elif typ == 'float':
|
|
||||||
editor = QDoubleSpinBox(parent)
|
|
||||||
editor.setSpecialValueText(_('Undefined'))
|
|
||||||
editor.setRange(-100., float(sys.maxint))
|
|
||||||
editor.setDecimals(2)
|
|
||||||
else:
|
|
||||||
editor = EnLineEdit(parent)
|
|
||||||
complete_items = sorted(list(m.db.all_custom(label=col)))
|
|
||||||
completer = QCompleter(complete_items, self)
|
|
||||||
completer.setCaseSensitivity(Qt.CaseInsensitive)
|
|
||||||
completer.setCompletionMode(QCompleter.PopupCompletion)
|
|
||||||
editor.setCompleter(completer)
|
|
||||||
return editor
|
|
||||||
|
|
||||||
class CcCommentsDelegate(QStyledItemDelegate):
|
|
||||||
'''
|
|
||||||
Delegate for comments data.
|
|
||||||
'''
|
|
||||||
|
|
||||||
def createEditor(self, parent, option, index):
|
|
||||||
m = index.model()
|
|
||||||
col = m.column_map[index.column()]
|
|
||||||
# db col is not named for the field, but for the table number. To get it,
|
|
||||||
# gui column -> column label -> table number -> db column
|
|
||||||
text = m.db.data[index.row()][m.db.FIELD_MAP[m.custom_columns[col]['num']]]
|
|
||||||
editor = CommentsDialog(parent, text)
|
|
||||||
d = editor.exec_()
|
|
||||||
if d:
|
|
||||||
m.setData(index, QVariant(editor.textbox.toPlainText()), Qt.EditRole)
|
|
||||||
return None
|
|
||||||
|
|
||||||
def setModelData(self, editor, model, index):
|
|
||||||
model.setData(index, QVariant(editor.textbox.toPlainText()), Qt.EditRole)
|
|
||||||
|
|
||||||
class CcBoolDelegate(QStyledItemDelegate):
|
|
||||||
def __init__(self, parent):
|
|
||||||
'''
|
|
||||||
Delegate for custom_column bool data.
|
|
||||||
'''
|
|
||||||
QStyledItemDelegate.__init__(self, parent)
|
|
||||||
|
|
||||||
def createEditor(self, parent, option, index):
|
|
||||||
editor = QComboBox(parent)
|
|
||||||
items = [_('Y'), _('N'), ' ']
|
|
||||||
icons = [I('ok.svg'), I('list_remove.svg'), I('blank.svg')]
|
|
||||||
if tweaks['bool_custom_columns_are_tristate'] == 'no':
|
|
||||||
items = items[:-1]
|
|
||||||
icons = icons[:-1]
|
|
||||||
for icon, text in zip(icons, items):
|
|
||||||
editor.addItem(QIcon(icon), text)
|
|
||||||
return editor
|
|
||||||
|
|
||||||
def setModelData(self, editor, model, index):
|
|
||||||
val = {0:True, 1:False, 2:None}[editor.currentIndex()]
|
|
||||||
model.setData(index, QVariant(val), Qt.EditRole)
|
|
||||||
|
|
||||||
def setEditorData(self, editor, index):
|
|
||||||
m = index.model()
|
|
||||||
# db col is not named for the field, but for the table number. To get it,
|
|
||||||
# gui column -> column label -> table number -> db column
|
|
||||||
val = m.db.data[index.row()][m.db.FIELD_MAP[m.custom_columns[m.column_map[index.column()]]['num']]]
|
|
||||||
if tweaks['bool_custom_columns_are_tristate'] == 'no':
|
|
||||||
val = 1 if not val else 0
|
|
||||||
else:
|
|
||||||
val = 2 if val is None else 1 if not val else 0
|
|
||||||
editor.setCurrentIndex(val)
|
|
||||||
|
|
||||||
# }}}
|
|
||||||
|
|
||||||
class BooksModel(QAbstractTableModel): # {{{
|
class BooksModel(QAbstractTableModel): # {{{
|
||||||
|
|
||||||
about_to_be_sorted = pyqtSignal(object, name='aboutToBeSorted')
|
about_to_be_sorted = pyqtSignal(object, name='aboutToBeSorted')
|
||||||
sorting_done = pyqtSignal(object, name='sortingDone')
|
sorting_done = pyqtSignal(object, name='sortingDone')
|
||||||
database_changed = pyqtSignal(object, name='databaseChanged')
|
database_changed = pyqtSignal(object, name='databaseChanged')
|
||||||
|
new_bookdisplay_data = pyqtSignal(object)
|
||||||
|
count_changed_signal = pyqtSignal(int)
|
||||||
|
searched = pyqtSignal(object)
|
||||||
|
|
||||||
orig_headers = {
|
orig_headers = {
|
||||||
'title' : _("Title"),
|
'title' : _("Title"),
|
||||||
|
'ondevice' : _("On Device"),
|
||||||
'authors' : _("Author(s)"),
|
'authors' : _("Author(s)"),
|
||||||
'size' : _("Size (MB)"),
|
'size' : _("Size (MB)"),
|
||||||
'timestamp' : _("Date"),
|
'timestamp' : _("Date"),
|
||||||
@ -321,7 +50,6 @@ class BooksModel(QAbstractTableModel): # {{{
|
|||||||
'publisher' : _("Publisher"),
|
'publisher' : _("Publisher"),
|
||||||
'tags' : _("Tags"),
|
'tags' : _("Tags"),
|
||||||
'series' : _("Series"),
|
'series' : _("Series"),
|
||||||
'ondevice' : _("On Device"),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, parent=None, buffer=40):
|
def __init__(self, parent=None, buffer=40):
|
||||||
@ -331,7 +59,7 @@ class BooksModel(QAbstractTableModel): # {{{
|
|||||||
self.editable_cols = ['title', 'authors', 'rating', 'publisher',
|
self.editable_cols = ['title', 'authors', 'rating', 'publisher',
|
||||||
'tags', 'series', 'timestamp', 'pubdate']
|
'tags', 'series', 'timestamp', 'pubdate']
|
||||||
self.default_image = QImage(I('book.svg'))
|
self.default_image = QImage(I('book.svg'))
|
||||||
self.sorted_on = ('timestamp', Qt.AscendingOrder)
|
self.sorted_on = DEFAULT_SORT
|
||||||
self.sort_history = [self.sorted_on]
|
self.sort_history = [self.sorted_on]
|
||||||
self.last_search = '' # The last search performed on this model
|
self.last_search = '' # The last search performed on this model
|
||||||
self.column_map = []
|
self.column_map = []
|
||||||
@ -342,6 +70,7 @@ class BooksModel(QAbstractTableModel): # {{{
|
|||||||
self.bool_no_icon = QIcon(I('list_remove.svg'))
|
self.bool_no_icon = QIcon(I('list_remove.svg'))
|
||||||
self.bool_blank_icon = QIcon(I('blank.svg'))
|
self.bool_blank_icon = QIcon(I('blank.svg'))
|
||||||
self.device_connected = False
|
self.device_connected = False
|
||||||
|
self.read_config()
|
||||||
|
|
||||||
def is_custom_column(self, cc_label):
|
def is_custom_column(self, cc_label):
|
||||||
return cc_label in self.custom_columns
|
return cc_label in self.custom_columns
|
||||||
@ -352,29 +81,10 @@ class BooksModel(QAbstractTableModel): # {{{
|
|||||||
|
|
||||||
def read_config(self):
|
def read_config(self):
|
||||||
self.use_roman_numbers = config['use_roman_numerals_for_series_number']
|
self.use_roman_numbers = config['use_roman_numerals_for_series_number']
|
||||||
cmap = config['column_map'][:] # force a copy
|
|
||||||
self.headers = {}
|
|
||||||
self.column_map = []
|
|
||||||
for col in cmap: # take out any columns no longer in the db
|
|
||||||
if col == 'ondevice':
|
|
||||||
if self.device_connected:
|
|
||||||
self.column_map.append(col)
|
|
||||||
elif col in self.orig_headers or col in self.custom_columns:
|
|
||||||
self.column_map.append(col)
|
|
||||||
for col in self.column_map:
|
|
||||||
if col in self.orig_headers:
|
|
||||||
self.headers[col] = self.orig_headers[col]
|
|
||||||
elif col in self.custom_columns:
|
|
||||||
self.headers[col] = self.custom_columns[col]['name']
|
|
||||||
self.build_data_convertors()
|
|
||||||
self.reset()
|
|
||||||
self.emit(SIGNAL('columns_sorted()'))
|
|
||||||
|
|
||||||
def set_device_connected(self, is_connected):
|
def set_device_connected(self, is_connected):
|
||||||
self.device_connected = is_connected
|
self.device_connected = is_connected
|
||||||
self.read_config()
|
|
||||||
self.db.refresh_ondevice()
|
self.db.refresh_ondevice()
|
||||||
self.database_changed.emit(self.db)
|
|
||||||
|
|
||||||
def set_book_on_device_func(self, func):
|
def set_book_on_device_func(self, func):
|
||||||
self.book_on_device = func
|
self.book_on_device = func
|
||||||
@ -382,7 +92,24 @@ class BooksModel(QAbstractTableModel): # {{{
|
|||||||
def set_database(self, db):
|
def set_database(self, db):
|
||||||
self.db = db
|
self.db = db
|
||||||
self.custom_columns = self.db.custom_column_label_map
|
self.custom_columns = self.db.custom_column_label_map
|
||||||
self.read_config()
|
self.column_map = list(self.orig_headers.keys()) + \
|
||||||
|
list(self.custom_columns)
|
||||||
|
def col_idx(name):
|
||||||
|
if name == 'ondevice':
|
||||||
|
return -1
|
||||||
|
if name not in self.db.FIELD_MAP:
|
||||||
|
return 100000
|
||||||
|
return self.db.FIELD_MAP[name]
|
||||||
|
|
||||||
|
self.column_map.sort(cmp=lambda x,y: cmp(col_idx(x), col_idx(y)))
|
||||||
|
for col in self.column_map:
|
||||||
|
if col in self.orig_headers:
|
||||||
|
self.headers[col] = self.orig_headers[col]
|
||||||
|
elif col in self.custom_columns:
|
||||||
|
self.headers[col] = self.custom_columns[col]['name']
|
||||||
|
|
||||||
|
self.build_data_convertors()
|
||||||
|
self.reset()
|
||||||
self.database_changed.emit(db)
|
self.database_changed.emit(db)
|
||||||
|
|
||||||
def refresh_ids(self, ids, current_row=-1):
|
def refresh_ids(self, ids, current_row=-1):
|
||||||
@ -400,7 +127,7 @@ class BooksModel(QAbstractTableModel): # {{{
|
|||||||
id = self.db.id(row)
|
id = self.db.id(row)
|
||||||
self.cover_cache.refresh([id])
|
self.cover_cache.refresh([id])
|
||||||
if row == current_row:
|
if row == current_row:
|
||||||
self.emit(SIGNAL('new_bookdisplay_data(PyQt_PyObject)'),
|
self.new_bookdisplay_data.emit(
|
||||||
self.get_book_display_info(row))
|
self.get_book_display_info(row))
|
||||||
self.dataChanged.emit(self.index(row, 0), self.index(row,
|
self.dataChanged.emit(self.index(row, 0), self.index(row,
|
||||||
self.columnCount(QModelIndex())-1))
|
self.columnCount(QModelIndex())-1))
|
||||||
@ -427,7 +154,7 @@ class BooksModel(QAbstractTableModel): # {{{
|
|||||||
return ret
|
return ret
|
||||||
|
|
||||||
def count_changed(self, *args):
|
def count_changed(self, *args):
|
||||||
self.emit(SIGNAL('count_changed(int)'), self.db.count())
|
self.count_changed_signal.emit(self.db.count())
|
||||||
|
|
||||||
def row_indices(self, index):
|
def row_indices(self, index):
|
||||||
''' Return list indices of all cells in index.row()'''
|
''' Return list indices of all cells in index.row()'''
|
||||||
@ -470,14 +197,14 @@ class BooksModel(QAbstractTableModel): # {{{
|
|||||||
try:
|
try:
|
||||||
self.db.search(text)
|
self.db.search(text)
|
||||||
except ParseException:
|
except ParseException:
|
||||||
self.emit(SIGNAL('searched(PyQt_PyObject)'), False)
|
self.searched.emit(False)
|
||||||
return
|
return
|
||||||
self.last_search = text
|
self.last_search = text
|
||||||
if reset:
|
if reset:
|
||||||
self.clear_caches()
|
self.clear_caches()
|
||||||
self.reset()
|
self.reset()
|
||||||
if self.last_search:
|
if self.last_search:
|
||||||
self.emit(SIGNAL('searched(PyQt_PyObject)'), True)
|
self.searched.emit(True)
|
||||||
|
|
||||||
|
|
||||||
def sort(self, col, order, reset=True):
|
def sort(self, col, order, reset=True):
|
||||||
@ -491,7 +218,6 @@ class BooksModel(QAbstractTableModel): # {{{
|
|||||||
self.reset()
|
self.reset()
|
||||||
self.sorted_on = (self.column_map[col], order)
|
self.sorted_on = (self.column_map[col], order)
|
||||||
self.sort_history.insert(0, self.sorted_on)
|
self.sort_history.insert(0, self.sorted_on)
|
||||||
del self.sort_history[3:] # clean up older searches
|
|
||||||
self.sorting_done.emit(self.db.index)
|
self.sorting_done.emit(self.db.index)
|
||||||
|
|
||||||
def refresh(self, reset=True):
|
def refresh(self, reset=True):
|
||||||
@ -505,7 +231,7 @@ class BooksModel(QAbstractTableModel): # {{{
|
|||||||
def resort(self, reset=True):
|
def resort(self, reset=True):
|
||||||
try:
|
try:
|
||||||
col = self.column_map.index(self.sorted_on[0])
|
col = self.column_map.index(self.sorted_on[0])
|
||||||
except:
|
except ValueError:
|
||||||
col = 0
|
col = 0
|
||||||
self.sort(col, self.sorted_on[1], reset=reset)
|
self.sort(col, self.sorted_on[1], reset=reset)
|
||||||
|
|
||||||
@ -576,7 +302,7 @@ class BooksModel(QAbstractTableModel): # {{{
|
|||||||
self.set_cache(idx)
|
self.set_cache(idx)
|
||||||
data = self.get_book_display_info(idx)
|
data = self.get_book_display_info(idx)
|
||||||
if emit_signal:
|
if emit_signal:
|
||||||
self.emit(SIGNAL('new_bookdisplay_data(PyQt_PyObject)'), data)
|
self.new_bookdisplay_data.emit(data)
|
||||||
else:
|
else:
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@ -973,8 +699,7 @@ class BooksModel(QAbstractTableModel): # {{{
|
|||||||
self.db.set_pubdate(id, qt_to_dt(val, as_utc=False))
|
self.db.set_pubdate(id, qt_to_dt(val, as_utc=False))
|
||||||
else:
|
else:
|
||||||
self.db.set(row, column, val)
|
self.db.set(row, column, val)
|
||||||
self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), \
|
self.dataChanged.emit(index, index)
|
||||||
index, index)
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def set_search_restriction(self, s):
|
def set_search_restriction(self, s):
|
||||||
@ -982,249 +707,7 @@ class BooksModel(QAbstractTableModel): # {{{
|
|||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
class BooksView(TableView):
|
class OnDeviceSearch(SearchQueryParser): # {{{
|
||||||
TIME_FMT = '%d %b %Y'
|
|
||||||
wrapper = textwrap.TextWrapper(width=20)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def wrap(cls, s, width=20):
|
|
||||||
cls.wrapper.width = width
|
|
||||||
return cls.wrapper.fill(s)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def human_readable(cls, size, precision=1):
|
|
||||||
""" Convert a size in bytes into megabytes """
|
|
||||||
return ('%.'+str(precision)+'f') % ((size/(1024.*1024.)),)
|
|
||||||
|
|
||||||
def __init__(self, parent, modelcls=BooksModel):
|
|
||||||
TableView.__init__(self, parent)
|
|
||||||
self.rating_delegate = RatingDelegate(self)
|
|
||||||
self.timestamp_delegate = DateDelegate(self)
|
|
||||||
self.pubdate_delegate = PubDateDelegate(self)
|
|
||||||
self.tags_delegate = TagsDelegate(self)
|
|
||||||
self.authors_delegate = TextDelegate(self)
|
|
||||||
self.series_delegate = TextDelegate(self)
|
|
||||||
self.publisher_delegate = TextDelegate(self)
|
|
||||||
self.cc_text_delegate = CcTextDelegate(self)
|
|
||||||
self.cc_bool_delegate = CcBoolDelegate(self)
|
|
||||||
self.cc_comments_delegate = CcCommentsDelegate(self)
|
|
||||||
self.display_parent = parent
|
|
||||||
self._model = modelcls(self)
|
|
||||||
self.setModel(self._model)
|
|
||||||
self.setSelectionBehavior(QAbstractItemView.SelectRows)
|
|
||||||
self.setSortingEnabled(True)
|
|
||||||
for i in range(10):
|
|
||||||
self.setItemDelegateForColumn(i, TextDelegate(self))
|
|
||||||
self.columns_sorted()
|
|
||||||
QObject.connect(self.selectionModel(), SIGNAL('currentRowChanged(QModelIndex, QModelIndex)'),
|
|
||||||
self._model.current_changed)
|
|
||||||
self.connect(self._model, SIGNAL('columns_sorted()'),
|
|
||||||
self.columns_sorted, Qt.QueuedConnection)
|
|
||||||
hv = self.verticalHeader()
|
|
||||||
hv.setClickable(True)
|
|
||||||
hv.setCursor(Qt.PointingHandCursor)
|
|
||||||
self.selected_ids = []
|
|
||||||
self._model.about_to_be_sorted.connect(self.about_to_be_sorted)
|
|
||||||
self._model.sorting_done.connect(self.sorting_done)
|
|
||||||
|
|
||||||
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):
|
|
||||||
if self.selected_ids:
|
|
||||||
indices = [self.model().index(indexc(i), 0) for i in
|
|
||||||
self.selected_ids]
|
|
||||||
sm = self.selectionModel()
|
|
||||||
for idx in indices:
|
|
||||||
sm.select(idx, sm.Select|sm.Rows)
|
|
||||||
self.selected_ids = []
|
|
||||||
|
|
||||||
def columns_sorted(self):
|
|
||||||
for i in range(self.model().columnCount(None)):
|
|
||||||
if self.itemDelegateForColumn(i) in (self.rating_delegate,
|
|
||||||
self.timestamp_delegate, self.pubdate_delegate):
|
|
||||||
self.setItemDelegateForColumn(i, self.itemDelegate())
|
|
||||||
|
|
||||||
cm = self._model.column_map
|
|
||||||
|
|
||||||
if 'rating' in cm:
|
|
||||||
self.setItemDelegateForColumn(cm.index('rating'), self.rating_delegate)
|
|
||||||
if 'timestamp' in cm:
|
|
||||||
self.setItemDelegateForColumn(cm.index('timestamp'), self.timestamp_delegate)
|
|
||||||
if 'pubdate' in cm:
|
|
||||||
self.setItemDelegateForColumn(cm.index('pubdate'), self.pubdate_delegate)
|
|
||||||
if 'tags' in cm:
|
|
||||||
self.setItemDelegateForColumn(cm.index('tags'), self.tags_delegate)
|
|
||||||
if 'authors' in cm:
|
|
||||||
self.setItemDelegateForColumn(cm.index('authors'), self.authors_delegate)
|
|
||||||
if 'publisher' in cm:
|
|
||||||
self.setItemDelegateForColumn(cm.index('publisher'), self.publisher_delegate)
|
|
||||||
if 'series' in cm:
|
|
||||||
self.setItemDelegateForColumn(cm.index('series'), self.series_delegate)
|
|
||||||
for colhead in cm:
|
|
||||||
if not self._model.is_custom_column(colhead):
|
|
||||||
continue
|
|
||||||
cc = self._model.custom_columns[colhead]
|
|
||||||
if cc['datatype'] == 'datetime':
|
|
||||||
delegate = CcDateDelegate(self)
|
|
||||||
delegate.set_format(cc['display'].get('date_format',''))
|
|
||||||
self.setItemDelegateForColumn(cm.index(colhead), delegate)
|
|
||||||
elif cc['datatype'] == 'comments':
|
|
||||||
self.setItemDelegateForColumn(cm.index(colhead), self.cc_comments_delegate)
|
|
||||||
elif cc['datatype'] == 'text':
|
|
||||||
if cc['is_multiple']:
|
|
||||||
self.setItemDelegateForColumn(cm.index(colhead), self.tags_delegate)
|
|
||||||
else:
|
|
||||||
self.setItemDelegateForColumn(cm.index(colhead), self.cc_text_delegate)
|
|
||||||
elif cc['datatype'] in ('int', 'float'):
|
|
||||||
self.setItemDelegateForColumn(cm.index(colhead), self.cc_text_delegate)
|
|
||||||
elif cc['datatype'] == 'bool':
|
|
||||||
self.setItemDelegateForColumn(cm.index(colhead), self.cc_bool_delegate)
|
|
||||||
elif cc['datatype'] == 'rating':
|
|
||||||
self.setItemDelegateForColumn(cm.index(colhead), self.rating_delegate)
|
|
||||||
if not self.restore_column_widths():
|
|
||||||
self.resizeColumnsToContents()
|
|
||||||
|
|
||||||
sort_col = self._model.sorted_on[0]
|
|
||||||
if sort_col in cm:
|
|
||||||
idx = cm.index(sort_col)
|
|
||||||
self.horizontalHeader().setSortIndicator(idx, self._model.sorted_on[1])
|
|
||||||
|
|
||||||
def set_context_menu(self, edit_metadata, send_to_device, convert, view,
|
|
||||||
save, open_folder, book_details, delete, similar_menu=None):
|
|
||||||
self.setContextMenuPolicy(Qt.DefaultContextMenu)
|
|
||||||
self.context_menu = QMenu(self)
|
|
||||||
if edit_metadata is not None:
|
|
||||||
self.context_menu.addAction(edit_metadata)
|
|
||||||
if send_to_device is not None:
|
|
||||||
self.context_menu.addAction(send_to_device)
|
|
||||||
if convert is not None:
|
|
||||||
self.context_menu.addAction(convert)
|
|
||||||
self.context_menu.addAction(view)
|
|
||||||
self.context_menu.addAction(save)
|
|
||||||
if open_folder is not None:
|
|
||||||
self.context_menu.addAction(open_folder)
|
|
||||||
if delete is not None:
|
|
||||||
self.context_menu.addAction(delete)
|
|
||||||
if book_details is not None:
|
|
||||||
self.context_menu.addAction(book_details)
|
|
||||||
if similar_menu is not None:
|
|
||||||
self.context_menu.addMenu(similar_menu)
|
|
||||||
|
|
||||||
def contextMenuEvent(self, event):
|
|
||||||
self.context_menu.popup(event.globalPos())
|
|
||||||
event.accept()
|
|
||||||
|
|
||||||
def restore_sort_at_startup(self, saved_history):
|
|
||||||
if tweaks['sort_columns_at_startup'] is not None:
|
|
||||||
saved_history = tweaks['sort_columns_at_startup']
|
|
||||||
|
|
||||||
if saved_history is None:
|
|
||||||
return
|
|
||||||
for col,order in reversed(saved_history):
|
|
||||||
self.sortByColumn(col, order)
|
|
||||||
self.model().sort_history = saved_history
|
|
||||||
|
|
||||||
def sortByColumn(self, colname, order):
|
|
||||||
try:
|
|
||||||
idx = self._model.column_map.index(colname)
|
|
||||||
except ValueError:
|
|
||||||
idx = 0
|
|
||||||
TableView.sortByColumn(self, idx, order)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def paths_from_event(cls, event):
|
|
||||||
'''
|
|
||||||
Accept a drop event and return a list of paths that can be read from
|
|
||||||
and represent files with extensions.
|
|
||||||
'''
|
|
||||||
if event.mimeData().hasFormat('text/uri-list'):
|
|
||||||
urls = [unicode(u.toLocalFile()) for u in event.mimeData().urls()]
|
|
||||||
return [u for u in urls if os.path.splitext(u)[1] and os.access(u, os.R_OK)]
|
|
||||||
|
|
||||||
def dragEnterEvent(self, event):
|
|
||||||
if int(event.possibleActions() & Qt.CopyAction) + \
|
|
||||||
int(event.possibleActions() & Qt.MoveAction) == 0:
|
|
||||||
return
|
|
||||||
paths = self.paths_from_event(event)
|
|
||||||
|
|
||||||
if paths:
|
|
||||||
event.acceptProposedAction()
|
|
||||||
|
|
||||||
def dragMoveEvent(self, event):
|
|
||||||
event.acceptProposedAction()
|
|
||||||
|
|
||||||
def dropEvent(self, event):
|
|
||||||
paths = self.paths_from_event(event)
|
|
||||||
event.setDropAction(Qt.CopyAction)
|
|
||||||
event.accept()
|
|
||||||
self.emit(SIGNAL('files_dropped(PyQt_PyObject)'), paths)
|
|
||||||
|
|
||||||
def set_database(self, db):
|
|
||||||
self._model.set_database(db)
|
|
||||||
self.tags_delegate.set_database(db)
|
|
||||||
self.authors_delegate.set_auto_complete_function(db.all_authors)
|
|
||||||
self.series_delegate.set_auto_complete_function(db.all_series)
|
|
||||||
self.publisher_delegate.set_auto_complete_function(db.all_publishers)
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
self._model.close()
|
|
||||||
|
|
||||||
def set_editable(self, editable):
|
|
||||||
self._model.set_editable(editable)
|
|
||||||
|
|
||||||
def connect_to_search_box(self, sb, search_done):
|
|
||||||
QObject.connect(sb, SIGNAL('search(PyQt_PyObject, PyQt_PyObject)'),
|
|
||||||
self._model.search)
|
|
||||||
self._search_done = search_done
|
|
||||||
self.connect(self._model, SIGNAL('searched(PyQt_PyObject)'),
|
|
||||||
self.search_done)
|
|
||||||
|
|
||||||
def connect_to_restriction_set(self, tv):
|
|
||||||
QObject.connect(tv, SIGNAL('restriction_set(PyQt_PyObject)'),
|
|
||||||
self._model.set_search_restriction) # must be synchronous (not queued)
|
|
||||||
|
|
||||||
def connect_to_book_display(self, bd):
|
|
||||||
QObject.connect(self._model, SIGNAL('new_bookdisplay_data(PyQt_PyObject)'),
|
|
||||||
bd)
|
|
||||||
|
|
||||||
def search_done(self, ok):
|
|
||||||
self._search_done(self, ok)
|
|
||||||
|
|
||||||
def row_count(self):
|
|
||||||
return self._model.count()
|
|
||||||
|
|
||||||
class DeviceBooksView(BooksView):
|
|
||||||
|
|
||||||
def __init__(self, parent):
|
|
||||||
BooksView.__init__(self, parent, DeviceBooksModel)
|
|
||||||
self.columns_resized = False
|
|
||||||
self.resize_on_select = False
|
|
||||||
self.rating_delegate = None
|
|
||||||
for i in range(10):
|
|
||||||
self.setItemDelegateForColumn(i, TextDelegate(self))
|
|
||||||
self.setDragDropMode(self.NoDragDrop)
|
|
||||||
self.setAcceptDrops(False)
|
|
||||||
|
|
||||||
def set_database(self, db):
|
|
||||||
self._model.set_database(db)
|
|
||||||
|
|
||||||
def resizeColumnsToContents(self):
|
|
||||||
QTableView.resizeColumnsToContents(self)
|
|
||||||
self.columns_resized = True
|
|
||||||
|
|
||||||
def connect_dirtied_signal(self, slot):
|
|
||||||
QObject.connect(self._model, SIGNAL('booklist_dirtied()'), slot)
|
|
||||||
|
|
||||||
def sortByColumn(self, col, order):
|
|
||||||
TableView.sortByColumn(self, col, order)
|
|
||||||
|
|
||||||
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_()
|
|
||||||
|
|
||||||
class OnDeviceSearch(SearchQueryParser):
|
|
||||||
|
|
||||||
def __init__(self, model):
|
def __init__(self, model):
|
||||||
SearchQueryParser.__init__(self)
|
SearchQueryParser.__init__(self)
|
||||||
@ -1282,15 +765,30 @@ class OnDeviceSearch(SearchQueryParser):
|
|||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return matches
|
return matches
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
class DeviceBooksModel(BooksModel):
|
class DeviceBooksModel(BooksModel): # {{{
|
||||||
|
|
||||||
|
booklist_dirtied = pyqtSignal()
|
||||||
|
|
||||||
def __init__(self, parent):
|
def __init__(self, parent):
|
||||||
BooksModel.__init__(self, parent)
|
BooksModel.__init__(self, parent)
|
||||||
self.db = []
|
self.db = []
|
||||||
self.map = []
|
self.map = []
|
||||||
self.sorted_map = []
|
self.sorted_map = []
|
||||||
|
self.sorted_on = DEFAULT_SORT
|
||||||
|
self.sort_history = [self.sorted_on]
|
||||||
self.unknown = _('Unknown')
|
self.unknown = _('Unknown')
|
||||||
|
self.column_map = ['inlibrary', 'title', 'authors', 'timestamp', 'size',
|
||||||
|
'tags']
|
||||||
|
self.headers = {
|
||||||
|
'inlibrary' : _('In Library'),
|
||||||
|
'title' : _('Title'),
|
||||||
|
'authors' : _('Author(s)'),
|
||||||
|
'timestamp' : _('Date'),
|
||||||
|
'size' : _('Size'),
|
||||||
|
'tags' : _('Tags')
|
||||||
|
}
|
||||||
self.marked_for_deletion = {}
|
self.marked_for_deletion = {}
|
||||||
self.search_engine = OnDeviceSearch(self)
|
self.search_engine = OnDeviceSearch(self)
|
||||||
self.editable = True
|
self.editable = True
|
||||||
@ -1300,7 +798,7 @@ class DeviceBooksModel(BooksModel):
|
|||||||
self.marked_for_deletion[job] = self.indices(rows)
|
self.marked_for_deletion[job] = self.indices(rows)
|
||||||
for row in rows:
|
for row in rows:
|
||||||
indices = self.row_indices(row)
|
indices = self.row_indices(row)
|
||||||
self.emit(SIGNAL('dataChanged(QModelIndex, QModelIndex)'), indices[0], indices[-1])
|
self.dataChanged.emit(indices[0], indices[-1])
|
||||||
|
|
||||||
def deletion_done(self, job, succeeded=True):
|
def deletion_done(self, job, succeeded=True):
|
||||||
if not self.marked_for_deletion.has_key(job):
|
if not self.marked_for_deletion.has_key(job):
|
||||||
@ -1309,7 +807,7 @@ class DeviceBooksModel(BooksModel):
|
|||||||
for row in rows:
|
for row in rows:
|
||||||
if not succeeded:
|
if not succeeded:
|
||||||
indices = self.row_indices(self.index(row, 0))
|
indices = self.row_indices(self.index(row, 0))
|
||||||
self.emit(SIGNAL('dataChanged(QModelIndex, QModelIndex)'), indices[0], indices[-1])
|
self.dataChanged.emit(indices[0], indices[-1])
|
||||||
|
|
||||||
def paths_deleted(self, paths):
|
def paths_deleted(self, paths):
|
||||||
self.map = list(range(0, len(self.db)))
|
self.map = list(range(0, len(self.db)))
|
||||||
@ -1327,7 +825,8 @@ class DeviceBooksModel(BooksModel):
|
|||||||
return Qt.ItemIsUserCheckable # Can't figure out how to get the disabled flag in python
|
return Qt.ItemIsUserCheckable # Can't figure out how to get the disabled flag in python
|
||||||
flags = QAbstractTableModel.flags(self, index)
|
flags = QAbstractTableModel.flags(self, index)
|
||||||
if index.isValid() and self.editable:
|
if index.isValid() and self.editable:
|
||||||
if index.column() in [0, 1] or (index.column() == 4 and self.db.supports_tags()):
|
cname = self.column_map[index.column()]
|
||||||
|
if cname in ('title', 'authors') or (cname == 'tags' and self.db.supports_tags()):
|
||||||
flags |= Qt.ItemIsEditable
|
flags |= Qt.ItemIsEditable
|
||||||
return flags
|
return flags
|
||||||
|
|
||||||
@ -1339,7 +838,7 @@ class DeviceBooksModel(BooksModel):
|
|||||||
try:
|
try:
|
||||||
matches = self.search_engine.parse(text)
|
matches = self.search_engine.parse(text)
|
||||||
except ParseException:
|
except ParseException:
|
||||||
self.emit(SIGNAL('searched(PyQt_PyObject)'), False)
|
self.searched.emit(False)
|
||||||
return
|
return
|
||||||
|
|
||||||
self.map = []
|
self.map = []
|
||||||
@ -1351,12 +850,9 @@ class DeviceBooksModel(BooksModel):
|
|||||||
self.reset()
|
self.reset()
|
||||||
self.last_search = text
|
self.last_search = text
|
||||||
if self.last_search:
|
if self.last_search:
|
||||||
self.emit(SIGNAL('searched(PyQt_PyObject)'), True)
|
self.searched.emit(False)
|
||||||
|
|
||||||
|
|
||||||
def resort(self, reset):
|
|
||||||
self.sort(self.sorted_on[0], self.sorted_on[1], reset=reset)
|
|
||||||
|
|
||||||
def sort(self, col, order, reset=True):
|
def sort(self, col, order, reset=True):
|
||||||
descending = order != Qt.AscendingOrder
|
descending = order != Qt.AscendingOrder
|
||||||
def strcmp(attr):
|
def strcmp(attr):
|
||||||
@ -1395,22 +891,30 @@ class DeviceBooksModel(BooksModel):
|
|||||||
x, y = authors_to_string(self.db[x].authors), \
|
x, y = authors_to_string(self.db[x].authors), \
|
||||||
authors_to_string(self.db[y].authors)
|
authors_to_string(self.db[y].authors)
|
||||||
return cmp(x, y)
|
return cmp(x, y)
|
||||||
fcmp = strcmp('title_sorter') if col == 0 else authorcmp if col == 1 else \
|
cname = self.column_map[col]
|
||||||
sizecmp if col == 2 else datecmp if col == 3 else tagscmp if col == 4 else libcmp
|
fcmp = {
|
||||||
|
'title': strcmp('title_sorter'),
|
||||||
|
'authors' : authorcmp,
|
||||||
|
'size' : sizecmp,
|
||||||
|
'timestamp': datecmp,
|
||||||
|
'tags': tagscmp,
|
||||||
|
'inlibrary': libcmp,
|
||||||
|
}[cname]
|
||||||
self.map.sort(cmp=fcmp, reverse=descending)
|
self.map.sort(cmp=fcmp, reverse=descending)
|
||||||
if len(self.map) == len(self.db):
|
if len(self.map) == len(self.db):
|
||||||
self.sorted_map = list(self.map)
|
self.sorted_map = list(self.map)
|
||||||
else:
|
else:
|
||||||
self.sorted_map = list(range(len(self.db)))
|
self.sorted_map = list(range(len(self.db)))
|
||||||
self.sorted_map.sort(cmp=fcmp, reverse=descending)
|
self.sorted_map.sort(cmp=fcmp, reverse=descending)
|
||||||
self.sorted_on = (col, order)
|
self.sorted_on = (self.column_map[col], order)
|
||||||
|
self.sort_history.insert(0, self.sorted_on)
|
||||||
if reset:
|
if reset:
|
||||||
self.reset()
|
self.reset()
|
||||||
|
|
||||||
def columnCount(self, parent):
|
def columnCount(self, parent):
|
||||||
if parent and parent.isValid():
|
if parent and parent.isValid():
|
||||||
return 0
|
return 0
|
||||||
return 6
|
return len(self.column_map)
|
||||||
|
|
||||||
def rowCount(self, parent):
|
def rowCount(self, parent):
|
||||||
if parent and parent.isValid():
|
if parent and parent.isValid():
|
||||||
@ -1443,7 +947,7 @@ class DeviceBooksModel(BooksModel):
|
|||||||
dt = dt_factory(item.datetime, assume_utc=True)
|
dt = dt_factory(item.datetime, assume_utc=True)
|
||||||
data[_('Timestamp')] = isoformat(dt, sep=' ', as_utc=False)
|
data[_('Timestamp')] = isoformat(dt, sep=' ', as_utc=False)
|
||||||
data[_('Tags')] = ', '.join(item.tags)
|
data[_('Tags')] = ', '.join(item.tags)
|
||||||
self.emit(SIGNAL('new_bookdisplay_data(PyQt_PyObject)'), data)
|
self.new_bookdisplay_data.emit(data)
|
||||||
|
|
||||||
def paths(self, rows):
|
def paths(self, rows):
|
||||||
return [self.db[self.map[r.row()]].path for r in rows ]
|
return [self.db[self.map[r.row()]].path for r in rows ]
|
||||||
@ -1456,39 +960,35 @@ class DeviceBooksModel(BooksModel):
|
|||||||
|
|
||||||
def data(self, index, role):
|
def data(self, index, role):
|
||||||
row, col = index.row(), index.column()
|
row, col = index.row(), index.column()
|
||||||
|
cname = self.column_map[col]
|
||||||
if role == Qt.DisplayRole or role == Qt.EditRole:
|
if role == Qt.DisplayRole or role == Qt.EditRole:
|
||||||
if col == 0:
|
if cname == 'title':
|
||||||
text = self.db[self.map[row]].title
|
text = self.db[self.map[row]].title
|
||||||
if not text:
|
if not text:
|
||||||
text = self.unknown
|
text = self.unknown
|
||||||
return QVariant(text)
|
return QVariant(text)
|
||||||
elif col == 1:
|
elif cname == 'authors':
|
||||||
au = self.db[self.map[row]].authors
|
au = self.db[self.map[row]].authors
|
||||||
if not au:
|
if not au:
|
||||||
au = self.unknown
|
au = self.unknown
|
||||||
# if role == Qt.EditRole:
|
|
||||||
# return QVariant(au)
|
|
||||||
return QVariant(authors_to_string(au))
|
return QVariant(authors_to_string(au))
|
||||||
elif col == 2:
|
elif cname == 'size':
|
||||||
size = self.db[self.map[row]].size
|
size = self.db[self.map[row]].size
|
||||||
return QVariant(BooksView.human_readable(size))
|
return QVariant(human_readable(size))
|
||||||
elif col == 3:
|
elif cname == 'timestamp':
|
||||||
dt = self.db[self.map[row]].datetime
|
dt = self.db[self.map[row]].datetime
|
||||||
dt = dt_factory(dt, assume_utc=True, as_utc=False)
|
dt = dt_factory(dt, assume_utc=True, as_utc=False)
|
||||||
return QVariant(strftime(BooksView.TIME_FMT, dt.timetuple()))
|
return QVariant(strftime(TIME_FMT, dt.timetuple()))
|
||||||
elif col == 4:
|
elif cname == 'tags':
|
||||||
tags = self.db[self.map[row]].tags
|
tags = self.db[self.map[row]].tags
|
||||||
if tags:
|
if tags:
|
||||||
return QVariant(', '.join(tags))
|
return QVariant(', '.join(tags))
|
||||||
elif role == Qt.TextAlignmentRole and index.column() in [2, 3]:
|
|
||||||
return QVariant(Qt.AlignRight | Qt.AlignVCenter)
|
|
||||||
elif role == Qt.ToolTipRole and index.isValid():
|
elif role == Qt.ToolTipRole and index.isValid():
|
||||||
if self.map[index.row()] in self.indices_to_be_deleted():
|
if self.map[row] in self.indices_to_be_deleted():
|
||||||
return QVariant('Marked for deletion')
|
return QVariant(_('Marked for deletion'))
|
||||||
col = index.column()
|
if cname in ['title', 'authors'] or (cname == 'tags' and self.db.supports_tags()):
|
||||||
if col in [0, 1] or (col == 4 and self.db.supports_tags()):
|
|
||||||
return QVariant(_("Double click to <b>edit</b> me<br><br>"))
|
return QVariant(_("Double click to <b>edit</b> me<br><br>"))
|
||||||
elif role == Qt.DecorationRole and col == 5:
|
elif role == Qt.DecorationRole and cname == 'inlibrary':
|
||||||
if self.db[self.map[row]].in_library:
|
if self.db[self.map[row]].in_library:
|
||||||
return QVariant(self.bool_yes_icon)
|
return QVariant(self.bool_yes_icon)
|
||||||
|
|
||||||
@ -1497,14 +997,9 @@ class DeviceBooksModel(BooksModel):
|
|||||||
def headerData(self, section, orientation, role):
|
def headerData(self, section, orientation, role):
|
||||||
if role != Qt.DisplayRole:
|
if role != Qt.DisplayRole:
|
||||||
return NONE
|
return NONE
|
||||||
text = ""
|
|
||||||
if orientation == Qt.Horizontal:
|
if orientation == Qt.Horizontal:
|
||||||
if section == 0: text = _("Title")
|
cname = self.column_map[section]
|
||||||
elif section == 1: text = _("Author(s)")
|
text = self.headers[cname]
|
||||||
elif section == 2: text = _("Size (MB)")
|
|
||||||
elif section == 3: text = _("Date")
|
|
||||||
elif section == 4: text = _("Tags")
|
|
||||||
elif section == 5: text = _("In Library")
|
|
||||||
return QVariant(text)
|
return QVariant(text)
|
||||||
else:
|
else:
|
||||||
return QVariant(section+1)
|
return QVariant(section+1)
|
||||||
@ -1513,23 +1008,22 @@ class DeviceBooksModel(BooksModel):
|
|||||||
done = False
|
done = False
|
||||||
if role == Qt.EditRole:
|
if role == Qt.EditRole:
|
||||||
row, col = index.row(), index.column()
|
row, col = index.row(), index.column()
|
||||||
if col in [2, 3]:
|
cname = self.column_map[col]
|
||||||
|
if cname in ('size', 'timestamp', 'inlibrary'):
|
||||||
return False
|
return False
|
||||||
val = unicode(value.toString()).strip()
|
val = unicode(value.toString()).strip()
|
||||||
idx = self.map[row]
|
idx = self.map[row]
|
||||||
if col == 0:
|
if cname == 'title' :
|
||||||
self.db[idx].title = val
|
self.db[idx].title = val
|
||||||
self.db[idx].title_sorter = val
|
self.db[idx].title_sorter = val
|
||||||
elif col == 1:
|
elif cname == 'authors':
|
||||||
self.db[idx].authors = string_to_authors(val)
|
self.db[idx].authors = string_to_authors(val)
|
||||||
elif col == 4:
|
elif cname == 'tags':
|
||||||
tags = [i.strip() for i in val.split(',')]
|
tags = [i.strip() for i in val.split(',')]
|
||||||
tags = [t for t in tags if t]
|
tags = [t for t in tags if t]
|
||||||
self.db.set_tags(self.db[idx], tags)
|
self.db.set_tags(self.db[idx], tags)
|
||||||
self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), index, index)
|
self.dataChanged.emit(index, index)
|
||||||
self.emit(SIGNAL('booklist_dirtied()'))
|
self.booklist_dirtied.emit()
|
||||||
if col == self.sorted_on[0]:
|
|
||||||
self.sort(col, self.sorted_on[1])
|
|
||||||
done = True
|
done = True
|
||||||
return done
|
return done
|
||||||
|
|
||||||
@ -1538,3 +1032,6 @@ class DeviceBooksModel(BooksModel):
|
|||||||
|
|
||||||
def set_search_restriction(self, s):
|
def set_search_restriction(self, s):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
410
src/calibre/gui2/library/views.py
Normal file
410
src/calibre/gui2/library/views.py
Normal file
@ -0,0 +1,410 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
import os
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
|
from PyQt4.Qt import QTableView, Qt, QAbstractItemView, QMenu, pyqtSignal
|
||||||
|
|
||||||
|
from calibre.gui2.library.delegates import RatingDelegate, PubDateDelegate, \
|
||||||
|
TextDelegate, DateDelegate, TagsDelegate, CcTextDelegate, \
|
||||||
|
CcBoolDelegate, CcCommentsDelegate, CcDateDelegate
|
||||||
|
from calibre.gui2.library.models import BooksModel, DeviceBooksModel
|
||||||
|
from calibre.utils.config import tweaks
|
||||||
|
from calibre.gui2 import error_dialog, gprefs
|
||||||
|
from calibre.gui2.library import DEFAULT_SORT
|
||||||
|
|
||||||
|
|
||||||
|
class BooksView(QTableView): # {{{
|
||||||
|
|
||||||
|
files_dropped = pyqtSignal(object)
|
||||||
|
|
||||||
|
def __init__(self, parent, modelcls=BooksModel):
|
||||||
|
QTableView.__init__(self, parent)
|
||||||
|
self.rating_delegate = RatingDelegate(self)
|
||||||
|
self.timestamp_delegate = DateDelegate(self)
|
||||||
|
self.pubdate_delegate = PubDateDelegate(self)
|
||||||
|
self.tags_delegate = TagsDelegate(self)
|
||||||
|
self.authors_delegate = TextDelegate(self)
|
||||||
|
self.series_delegate = TextDelegate(self)
|
||||||
|
self.publisher_delegate = TextDelegate(self)
|
||||||
|
self.text_delegate = TextDelegate(self)
|
||||||
|
self.cc_text_delegate = CcTextDelegate(self)
|
||||||
|
self.cc_bool_delegate = CcBoolDelegate(self)
|
||||||
|
self.cc_comments_delegate = CcCommentsDelegate(self)
|
||||||
|
self.display_parent = parent
|
||||||
|
self._model = modelcls(self)
|
||||||
|
self.setModel(self._model)
|
||||||
|
self.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||||
|
self.setSortingEnabled(True)
|
||||||
|
self.selectionModel().currentRowChanged.connect(self._model.current_changed)
|
||||||
|
|
||||||
|
# {{{ Column Header setup
|
||||||
|
self.column_header = self.horizontalHeader()
|
||||||
|
self.column_header.setMovable(True)
|
||||||
|
self.column_header.sectionMoved.connect(self.save_state)
|
||||||
|
self.column_header.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||||
|
self.column_header.customContextMenuRequested.connect(self.show_column_header_context_menu)
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
self._model.database_changed.connect(self.database_changed)
|
||||||
|
hv = self.verticalHeader()
|
||||||
|
hv.setClickable(True)
|
||||||
|
hv.setCursor(Qt.PointingHandCursor)
|
||||||
|
self.selected_ids = []
|
||||||
|
self._model.about_to_be_sorted.connect(self.about_to_be_sorted)
|
||||||
|
self._model.sorting_done.connect(self.sorting_done)
|
||||||
|
|
||||||
|
def column_header_context_handler(self, action=None, column=None):
|
||||||
|
if not action or not column:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
idx = self.column_map.index(column)
|
||||||
|
except:
|
||||||
|
return
|
||||||
|
h = self.column_header
|
||||||
|
|
||||||
|
if action == 'hide':
|
||||||
|
h.setSectionHidden(idx, True)
|
||||||
|
elif action == 'show':
|
||||||
|
h.setSectionHidden(idx, False)
|
||||||
|
elif action == 'ascending':
|
||||||
|
self.sortByColumn(idx, Qt.AscendingOrder)
|
||||||
|
elif action == 'descending':
|
||||||
|
self.sortByColumn(idx, Qt.DescendingOrder)
|
||||||
|
elif action == 'defaults':
|
||||||
|
self.apply_state(self.get_default_state())
|
||||||
|
|
||||||
|
self.save_state()
|
||||||
|
|
||||||
|
def show_column_header_context_menu(self, pos):
|
||||||
|
idx = self.column_header.logicalIndexAt(pos)
|
||||||
|
if idx > -1 and idx < len(self.column_map):
|
||||||
|
col = self.column_map[idx]
|
||||||
|
name = unicode(self.model().headerData(idx, Qt.Horizontal,
|
||||||
|
Qt.DisplayRole).toString())
|
||||||
|
self.column_header_context_menu = QMenu(self)
|
||||||
|
if col != 'ondevice':
|
||||||
|
self.column_header_context_menu.addAction(_('Hide column %s') %
|
||||||
|
name,
|
||||||
|
partial(self.column_header_context_handler, action='hide',
|
||||||
|
column=col))
|
||||||
|
self.column_header_context_menu.addAction(
|
||||||
|
_('Sort on column %s (ascending)') % name,
|
||||||
|
partial(self.column_header_context_handler,
|
||||||
|
action='ascending', column=col))
|
||||||
|
self.column_header_context_menu.addAction(
|
||||||
|
_('Sort on column %s (descending)') % name,
|
||||||
|
partial(self.column_header_context_handler,
|
||||||
|
action='descending', column=col))
|
||||||
|
|
||||||
|
hidden_cols = [self.column_map[i] for i in
|
||||||
|
range(self.column_header.count()) if
|
||||||
|
self.column_header.isSectionHidden(i)]
|
||||||
|
try:
|
||||||
|
hidden_cols.remove('ondevice')
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
if hidden_cols:
|
||||||
|
self.column_header_context_menu.addSeparator()
|
||||||
|
m = self.column_header_context_menu.addMenu(_('Show column'))
|
||||||
|
for col in hidden_cols:
|
||||||
|
hidx = self.column_map.index(col)
|
||||||
|
name = unicode(self.model().headerData(hidx, Qt.Horizontal,
|
||||||
|
Qt.DisplayRole).toString())
|
||||||
|
m.addAction(name,
|
||||||
|
partial(self.column_header_context_handler,
|
||||||
|
action='show', column=col))
|
||||||
|
|
||||||
|
self.column_header_context_menu.addSeparator()
|
||||||
|
self.column_header_context_menu.addAction(
|
||||||
|
_('Restore default layout'),
|
||||||
|
partial(self.column_header_context_handler,
|
||||||
|
action='defaults', column=col))
|
||||||
|
|
||||||
|
self.column_header_context_menu.popup(self.column_header.mapToGlobal(pos))
|
||||||
|
|
||||||
|
|
||||||
|
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):
|
||||||
|
if self.selected_ids:
|
||||||
|
indices = [self.model().index(indexc(i), 0) for i in
|
||||||
|
self.selected_ids]
|
||||||
|
sm = self.selectionModel()
|
||||||
|
for idx in indices:
|
||||||
|
sm.select(idx, sm.Select|sm.Rows)
|
||||||
|
self.selected_ids = []
|
||||||
|
|
||||||
|
def set_ondevice_column_visibility(self):
|
||||||
|
m = self._model
|
||||||
|
self.column_header.setSectionHidden(m.column_map.index('ondevice'),
|
||||||
|
not m.device_connected)
|
||||||
|
|
||||||
|
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['sort_history'] = \
|
||||||
|
self.cleanup_sort_history(self.model().sort_history)
|
||||||
|
state['column_positions'] = {}
|
||||||
|
state['column_sizes'] = {}
|
||||||
|
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 save_state(self):
|
||||||
|
# Only save if we have been initialized (set_database called)
|
||||||
|
if len(self.column_map) > 0:
|
||||||
|
state = self.get_state()
|
||||||
|
name = unicode(self.objectName())
|
||||||
|
if name:
|
||||||
|
gprefs.set(name + ' books view state', state)
|
||||||
|
|
||||||
|
def cleanup_sort_history(self, sort_history):
|
||||||
|
history = []
|
||||||
|
for col, order in sort_history:
|
||||||
|
if col in self.column_map and (not history or history[0][0] != col):
|
||||||
|
history.append([col, order])
|
||||||
|
return history
|
||||||
|
|
||||||
|
def apply_sort_history(self, saved_history):
|
||||||
|
if not saved_history:
|
||||||
|
return
|
||||||
|
for col, order in reversed(self.cleanup_sort_history(saved_history)[:3]):
|
||||||
|
self.sortByColumn(self.column_map.index(col), order)
|
||||||
|
#self.model().sort_history = saved_history
|
||||||
|
|
||||||
|
def apply_state(self, state):
|
||||||
|
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(), reverse=True):
|
||||||
|
col = pmap[pos]
|
||||||
|
idx = cmap[col]
|
||||||
|
current_pos = h.visualIndex(idx)
|
||||||
|
if current_pos != pos:
|
||||||
|
h.moveSection(current_pos, pos)
|
||||||
|
|
||||||
|
sizes = state.get('column_sizes', {})
|
||||||
|
for col, size in sizes.items():
|
||||||
|
if col in cmap:
|
||||||
|
h.resizeSection(cmap[col], sizes[col])
|
||||||
|
self.apply_sort_history(state.get('sort_history', None))
|
||||||
|
|
||||||
|
def get_default_state(self):
|
||||||
|
old_state = {'hidden_columns': [],
|
||||||
|
'sort_history':[DEFAULT_SORT],
|
||||||
|
'column_positions': {},
|
||||||
|
'column_sizes': {}}
|
||||||
|
h = self.column_header
|
||||||
|
cm = self.column_map
|
||||||
|
for i in range(h.count()):
|
||||||
|
name = cm[i]
|
||||||
|
old_state['column_positions'][name] = h.logicalIndex(i)
|
||||||
|
if name != 'ondevice':
|
||||||
|
old_state['column_sizes'][name] = \
|
||||||
|
max(self.sizeHintForColumn(i), h.sectionSizeHint(i))
|
||||||
|
if name == 'timestamp':
|
||||||
|
old_state['column_sizes'][name] += 12
|
||||||
|
return old_state
|
||||||
|
|
||||||
|
def restore_state(self):
|
||||||
|
name = unicode(self.objectName())
|
||||||
|
old_state = None
|
||||||
|
if name:
|
||||||
|
old_state = gprefs.get(name + ' books view state', None)
|
||||||
|
if old_state is None:
|
||||||
|
old_state = self.get_default_state()
|
||||||
|
|
||||||
|
if tweaks['sort_columns_at_startup'] is not None:
|
||||||
|
old_state['sort_history'] = tweaks['sort_columns_at_startup']
|
||||||
|
|
||||||
|
self.apply_state(old_state)
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def column_map(self):
|
||||||
|
return self._model.column_map
|
||||||
|
|
||||||
|
def database_changed(self, db):
|
||||||
|
for i in range(self.model().columnCount(None)):
|
||||||
|
if self.itemDelegateForColumn(i) in (self.rating_delegate,
|
||||||
|
self.timestamp_delegate, self.pubdate_delegate):
|
||||||
|
self.setItemDelegateForColumn(i, self.itemDelegate())
|
||||||
|
|
||||||
|
cm = self.column_map
|
||||||
|
|
||||||
|
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',''))
|
||||||
|
self.setItemDelegateForColumn(cm.index(colhead), delegate)
|
||||||
|
elif cc['datatype'] == 'comments':
|
||||||
|
self.setItemDelegateForColumn(cm.index(colhead), self.cc_comments_delegate)
|
||||||
|
elif cc['datatype'] == 'text':
|
||||||
|
if cc['is_multiple']:
|
||||||
|
self.setItemDelegateForColumn(cm.index(colhead), self.tags_delegate)
|
||||||
|
else:
|
||||||
|
self.setItemDelegateForColumn(cm.index(colhead), self.cc_text_delegate)
|
||||||
|
elif cc['datatype'] in ('int', 'float'):
|
||||||
|
self.setItemDelegateForColumn(cm.index(colhead), self.cc_text_delegate)
|
||||||
|
elif cc['datatype'] == 'bool':
|
||||||
|
self.setItemDelegateForColumn(cm.index(colhead), self.cc_bool_delegate)
|
||||||
|
elif cc['datatype'] == 'rating':
|
||||||
|
self.setItemDelegateForColumn(cm.index(colhead), self.rating_delegate)
|
||||||
|
else:
|
||||||
|
dattr = colhead+'_delegate'
|
||||||
|
delegate = colhead if hasattr(self, dattr) else 'text'
|
||||||
|
self.setItemDelegateForColumn(cm.index(colhead), getattr(self,
|
||||||
|
delegate+'_delegate'))
|
||||||
|
|
||||||
|
self.restore_state()
|
||||||
|
self.set_ondevice_column_visibility()
|
||||||
|
|
||||||
|
def set_context_menu(self, edit_metadata, send_to_device, convert, view,
|
||||||
|
save, open_folder, book_details, delete, similar_menu=None):
|
||||||
|
self.setContextMenuPolicy(Qt.DefaultContextMenu)
|
||||||
|
self.context_menu = QMenu(self)
|
||||||
|
if edit_metadata is not None:
|
||||||
|
self.context_menu.addAction(edit_metadata)
|
||||||
|
if send_to_device is not None:
|
||||||
|
self.context_menu.addAction(send_to_device)
|
||||||
|
if convert is not None:
|
||||||
|
self.context_menu.addAction(convert)
|
||||||
|
self.context_menu.addAction(view)
|
||||||
|
self.context_menu.addAction(save)
|
||||||
|
if open_folder is not None:
|
||||||
|
self.context_menu.addAction(open_folder)
|
||||||
|
if delete is not None:
|
||||||
|
self.context_menu.addAction(delete)
|
||||||
|
if book_details is not None:
|
||||||
|
self.context_menu.addAction(book_details)
|
||||||
|
if similar_menu is not None:
|
||||||
|
self.context_menu.addMenu(similar_menu)
|
||||||
|
|
||||||
|
def contextMenuEvent(self, event):
|
||||||
|
self.context_menu.popup(event.globalPos())
|
||||||
|
event.accept()
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def paths_from_event(cls, event):
|
||||||
|
'''
|
||||||
|
Accept a drop event and return a list of paths that can be read from
|
||||||
|
and represent files with extensions.
|
||||||
|
'''
|
||||||
|
if event.mimeData().hasFormat('text/uri-list'):
|
||||||
|
urls = [unicode(u.toLocalFile()) for u in event.mimeData().urls()]
|
||||||
|
return [u for u in urls if os.path.splitext(u)[1] and os.access(u, os.R_OK)]
|
||||||
|
|
||||||
|
def dragEnterEvent(self, event):
|
||||||
|
if int(event.possibleActions() & Qt.CopyAction) + \
|
||||||
|
int(event.possibleActions() & Qt.MoveAction) == 0:
|
||||||
|
return
|
||||||
|
paths = self.paths_from_event(event)
|
||||||
|
|
||||||
|
if paths:
|
||||||
|
event.acceptProposedAction()
|
||||||
|
|
||||||
|
def dragMoveEvent(self, event):
|
||||||
|
event.acceptProposedAction()
|
||||||
|
|
||||||
|
def dropEvent(self, event):
|
||||||
|
paths = self.paths_from_event(event)
|
||||||
|
event.setDropAction(Qt.CopyAction)
|
||||||
|
event.accept()
|
||||||
|
self.files_dropped.emit(paths)
|
||||||
|
|
||||||
|
def set_database(self, db):
|
||||||
|
self.save_state()
|
||||||
|
self._model.set_database(db)
|
||||||
|
self.tags_delegate.set_database(db)
|
||||||
|
self.authors_delegate.set_auto_complete_function(db.all_authors)
|
||||||
|
self.series_delegate.set_auto_complete_function(db.all_series)
|
||||||
|
self.publisher_delegate.set_auto_complete_function(db.all_publishers)
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self._model.close()
|
||||||
|
|
||||||
|
def set_editable(self, editable):
|
||||||
|
self._model.set_editable(editable)
|
||||||
|
|
||||||
|
def connect_to_search_box(self, sb, search_done):
|
||||||
|
sb.search.connect(self._model.search)
|
||||||
|
self._search_done = search_done
|
||||||
|
self._model.searched.connect(self.search_done)
|
||||||
|
|
||||||
|
def connect_to_restriction_set(self, tv):
|
||||||
|
# must be synchronous (not queued)
|
||||||
|
tv.restriction_set.connect(self._model.set_search_restriction)
|
||||||
|
|
||||||
|
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): # {{{
|
||||||
|
|
||||||
|
def __init__(self, parent):
|
||||||
|
BooksView.__init__(self, parent, DeviceBooksModel)
|
||||||
|
self.columns_resized = False
|
||||||
|
self.resize_on_select = False
|
||||||
|
self.rating_delegate = None
|
||||||
|
for i in range(10):
|
||||||
|
self.setItemDelegateForColumn(i, TextDelegate(self))
|
||||||
|
self.setDragDropMode(self.NoDragDrop)
|
||||||
|
self.setAcceptDrops(False)
|
||||||
|
|
||||||
|
def set_database(self, db):
|
||||||
|
self._model.set_database(db)
|
||||||
|
self.restore_state()
|
||||||
|
|
||||||
|
def resizeColumnsToContents(self):
|
||||||
|
QTableView.resizeColumnsToContents(self)
|
||||||
|
self.columns_resized = True
|
||||||
|
|
||||||
|
def connect_dirtied_signal(self, slot):
|
||||||
|
self._model.booklist_dirtied.connect(slot)
|
||||||
|
|
||||||
|
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_()
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
@ -81,7 +81,7 @@ class Main(MainWindow, Ui_MainWindow):
|
|||||||
self.search = SearchBox2(self)
|
self.search = SearchBox2(self)
|
||||||
self.search.initialize('lrf_viewer_search_history')
|
self.search.initialize('lrf_viewer_search_history')
|
||||||
self.search_action = self.tool_bar.addWidget(self.search)
|
self.search_action = self.tool_bar.addWidget(self.search)
|
||||||
QObject.connect(self.search, SIGNAL('search(PyQt_PyObject, PyQt_PyObject)'), self.find)
|
self.search.search.connect(self.find)
|
||||||
|
|
||||||
self.action_next_page.setShortcuts([QKeySequence.MoveToNextPage, QKeySequence(Qt.Key_Space)])
|
self.action_next_page.setShortcuts([QKeySequence.MoveToNextPage, QKeySequence(Qt.Key_Space)])
|
||||||
self.action_previous_page.setShortcuts([QKeySequence.MoveToPreviousPage, QKeySequence(Qt.Key_Backspace)])
|
self.action_previous_page.setShortcuts([QKeySequence.MoveToPreviousPage, QKeySequence(Qt.Key_Backspace)])
|
||||||
|
@ -793,7 +793,7 @@
|
|||||||
<customwidget>
|
<customwidget>
|
||||||
<class>BooksView</class>
|
<class>BooksView</class>
|
||||||
<extends>QTableView</extends>
|
<extends>QTableView</extends>
|
||||||
<header>library.h</header>
|
<header>calibre/gui2/library/views.h</header>
|
||||||
</customwidget>
|
</customwidget>
|
||||||
<customwidget>
|
<customwidget>
|
||||||
<class>LocationView</class>
|
<class>LocationView</class>
|
||||||
@ -803,7 +803,7 @@
|
|||||||
<customwidget>
|
<customwidget>
|
||||||
<class>DeviceBooksView</class>
|
<class>DeviceBooksView</class>
|
||||||
<extends>QTableView</extends>
|
<extends>QTableView</extends>
|
||||||
<header>library.h</header>
|
<header>calibre/gui2/library/views.h</header>
|
||||||
</customwidget>
|
</customwidget>
|
||||||
<customwidget>
|
<customwidget>
|
||||||
<class>TagsView</class>
|
<class>TagsView</class>
|
||||||
|
@ -6,7 +6,8 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
from PyQt4.Qt import QComboBox, SIGNAL, Qt, QLineEdit, QStringList, pyqtSlot
|
from PyQt4.Qt import QComboBox, Qt, QLineEdit, QStringList, pyqtSlot, \
|
||||||
|
pyqtSignal, SIGNAL
|
||||||
from PyQt4.QtGui import QCompleter
|
from PyQt4.QtGui import QCompleter
|
||||||
|
|
||||||
from calibre.gui2 import config
|
from calibre.gui2 import config
|
||||||
@ -56,6 +57,8 @@ class SearchBox2(QComboBox):
|
|||||||
INTERVAL = 1500 #: Time to wait before emitting search signal
|
INTERVAL = 1500 #: Time to wait before emitting search signal
|
||||||
MAX_COUNT = 25
|
MAX_COUNT = 25
|
||||||
|
|
||||||
|
search = pyqtSignal(object, object)
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
QComboBox.__init__(self, parent)
|
QComboBox.__init__(self, parent)
|
||||||
self.normal_background = 'rgb(255, 255, 255, 0%)'
|
self.normal_background = 'rgb(255, 255, 255, 0%)'
|
||||||
@ -108,7 +111,7 @@ class SearchBox2(QComboBox):
|
|||||||
|
|
||||||
def clear(self):
|
def clear(self):
|
||||||
self.clear_to_help()
|
self.clear_to_help()
|
||||||
self.emit(SIGNAL('search(PyQt_PyObject, PyQt_PyObject)'), '', False)
|
self.search.emit('', False)
|
||||||
|
|
||||||
def search_done(self, ok):
|
def search_done(self, ok):
|
||||||
if not unicode(self.currentText()).strip():
|
if not unicode(self.currentText()).strip():
|
||||||
@ -155,7 +158,7 @@ class SearchBox2(QComboBox):
|
|||||||
self.help_state = False
|
self.help_state = False
|
||||||
refinement = text.startswith(self.prev_search) and ':' not in text
|
refinement = text.startswith(self.prev_search) and ':' not in text
|
||||||
self.prev_search = text
|
self.prev_search = text
|
||||||
self.emit(SIGNAL('search(PyQt_PyObject, PyQt_PyObject)'), text, refinement)
|
self.search.emit(text, refinement)
|
||||||
|
|
||||||
idx = self.findText(text, Qt.MatchFixedString)
|
idx = self.findText(text, Qt.MatchFixedString)
|
||||||
self.block_signals(True)
|
self.block_signals(True)
|
||||||
@ -187,7 +190,7 @@ class SearchBox2(QComboBox):
|
|||||||
def set_search_string(self, txt):
|
def set_search_string(self, txt):
|
||||||
self.normalize_state()
|
self.normalize_state()
|
||||||
self.setEditText(txt)
|
self.setEditText(txt)
|
||||||
self.emit(SIGNAL('search(PyQt_PyObject, PyQt_PyObject)'), txt, False)
|
self.search.emit(txt, False)
|
||||||
self.line_edit.end(False)
|
self.line_edit.end(False)
|
||||||
self.initial_state = False
|
self.initial_state = False
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@ from calibre.library.database2 import Tag
|
|||||||
class TagsView(QTreeView):
|
class TagsView(QTreeView):
|
||||||
|
|
||||||
need_refresh = pyqtSignal()
|
need_refresh = pyqtSignal()
|
||||||
|
restriction_set = pyqtSignal(object)
|
||||||
|
|
||||||
def __init__(self, *args):
|
def __init__(self, *args):
|
||||||
QTreeView.__init__(self, *args)
|
QTreeView.__init__(self, *args)
|
||||||
@ -66,7 +67,7 @@ class TagsView(QTreeView):
|
|||||||
else:
|
else:
|
||||||
self.search_restriction = 'search:"%s"' % unicode(s).strip()
|
self.search_restriction = 'search:"%s"' % unicode(s).strip()
|
||||||
self.model().set_search_restriction(self.search_restriction)
|
self.model().set_search_restriction(self.search_restriction)
|
||||||
self.emit(SIGNAL('restriction_set(PyQt_PyObject)'), self.search_restriction)
|
self.restriction_set.emit(self.search_restriction)
|
||||||
self.recount() # Must happen after the emission of the restriction_set signal
|
self.recount() # Must happen after the emission of the restriction_set signal
|
||||||
self.emit(SIGNAL('tags_marked(PyQt_PyObject, PyQt_PyObject)'),
|
self.emit(SIGNAL('tags_marked(PyQt_PyObject, PyQt_PyObject)'),
|
||||||
self._model.tokens(), self.match_all)
|
self._model.tokens(), self.match_all)
|
||||||
|
@ -507,9 +507,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
self.card_b_view.set_context_menu(None, None, None,
|
self.card_b_view.set_context_menu(None, None, None,
|
||||||
self.action_view, self.action_save, None, None, self.action_del)
|
self.action_view, self.action_save, None, None, self.action_del)
|
||||||
|
|
||||||
QObject.connect(self.library_view,
|
self.library_view.files_dropped.connect(self.files_dropped, type=Qt.QueuedConnection)
|
||||||
SIGNAL('files_dropped(PyQt_PyObject)'),
|
|
||||||
self.files_dropped, Qt.QueuedConnection)
|
|
||||||
for func, args in [
|
for func, args in [
|
||||||
('connect_to_search_box', (self.search,
|
('connect_to_search_box', (self.search,
|
||||||
self.search_done)),
|
self.search_done)),
|
||||||
@ -534,9 +532,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
self.library_view.set_database(db)
|
self.library_view.set_database(db)
|
||||||
self.library_view.model().set_book_on_device_func(self.book_on_device)
|
self.library_view.model().set_book_on_device_func(self.book_on_device)
|
||||||
prefs['library_path'] = self.library_path
|
prefs['library_path'] = self.library_path
|
||||||
self.library_view.restore_sort_at_startup(dynamic.get('sort_history', [('timestamp', Qt.DescendingOrder)]))
|
|
||||||
if not self.library_view.restore_column_widths():
|
|
||||||
self.library_view.resizeColumnsToContents()
|
|
||||||
self.search.setFocus(Qt.OtherFocusReason)
|
self.search.setFocus(Qt.OtherFocusReason)
|
||||||
self.cover_cache = CoverCache(self.library_path)
|
self.cover_cache = CoverCache(self.library_path)
|
||||||
self.cover_cache.start()
|
self.cover_cache.start()
|
||||||
@ -546,24 +541,16 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
self.connect(self.tags_view,
|
self.connect(self.tags_view,
|
||||||
SIGNAL('tags_marked(PyQt_PyObject, PyQt_PyObject)'),
|
SIGNAL('tags_marked(PyQt_PyObject, PyQt_PyObject)'),
|
||||||
self.search.search_from_tags)
|
self.search.search_from_tags)
|
||||||
self.connect(self.tags_view,
|
for x in (self.saved_search.clear_to_help, self.mark_restriction_set):
|
||||||
SIGNAL('restriction_set(PyQt_PyObject)'),
|
self.tags_view.restriction_set.connect(x)
|
||||||
self.saved_search.clear_to_help)
|
|
||||||
self.connect(self.tags_view,
|
|
||||||
SIGNAL('restriction_set(PyQt_PyObject)'),
|
|
||||||
self.mark_restriction_set)
|
|
||||||
self.connect(self.tags_view,
|
self.connect(self.tags_view,
|
||||||
SIGNAL('tags_marked(PyQt_PyObject, PyQt_PyObject)'),
|
SIGNAL('tags_marked(PyQt_PyObject, PyQt_PyObject)'),
|
||||||
self.saved_search.clear_to_help)
|
self.saved_search.clear_to_help)
|
||||||
self.connect(self.search,
|
self.search.search.connect(self.tags_view.model().reinit)
|
||||||
SIGNAL('search(PyQt_PyObject, PyQt_PyObject)'),
|
for x in (self.location_view.count_changed, self.tags_view.recount,
|
||||||
self.tags_view.model().reinit)
|
self.restriction_count_changed):
|
||||||
self.connect(self.library_view.model(),
|
self.library_view.model().count_changed_signal.connect(x)
|
||||||
SIGNAL('count_changed(int)'), self.location_view.count_changed)
|
|
||||||
self.connect(self.library_view.model(), SIGNAL('count_changed(int)'),
|
|
||||||
self.tags_view.recount, Qt.QueuedConnection)
|
|
||||||
self.connect(self.library_view.model(), SIGNAL('count_changed(int)'),
|
|
||||||
self.restriction_count_changed, Qt.QueuedConnection)
|
|
||||||
self.connect(self.search, SIGNAL('cleared()'), self.search_box_cleared)
|
self.connect(self.search, SIGNAL('cleared()'), self.search_box_cleared)
|
||||||
self.connect(self.saved_search, SIGNAL('changed()'), self.tags_view.saved_searches_changed, Qt.QueuedConnection)
|
self.connect(self.saved_search, SIGNAL('changed()'), self.tags_view.saved_searches_changed, Qt.QueuedConnection)
|
||||||
if not gprefs.get('quick_start_guide_added', False):
|
if not gprefs.get('quick_start_guide_added', False):
|
||||||
@ -943,7 +930,8 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
|
|
||||||
def save_device_view_settings(self):
|
def save_device_view_settings(self):
|
||||||
model = self.location_view.model()
|
model = self.location_view.model()
|
||||||
self.memory_view.write_settings()
|
return
|
||||||
|
#self.memory_view.write_settings()
|
||||||
for x in range(model.rowCount()):
|
for x in range(model.rowCount()):
|
||||||
if x > 1:
|
if x > 1:
|
||||||
if model.location_for_row(x) == 'carda':
|
if model.location_for_row(x) == 'carda':
|
||||||
@ -1028,16 +1016,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
self.card_a_view.set_editable(self.device_manager.device.CAN_SET_METADATA)
|
self.card_a_view.set_editable(self.device_manager.device.CAN_SET_METADATA)
|
||||||
self.card_b_view.set_database(cardblist)
|
self.card_b_view.set_database(cardblist)
|
||||||
self.card_b_view.set_editable(self.device_manager.device.CAN_SET_METADATA)
|
self.card_b_view.set_editable(self.device_manager.device.CAN_SET_METADATA)
|
||||||
for view in (self.memory_view, self.card_a_view, self.card_b_view):
|
|
||||||
view.sortByColumn(3, Qt.DescendingOrder)
|
|
||||||
view.read_settings()
|
|
||||||
if not view.restore_column_widths():
|
|
||||||
view.resizeColumnsToContents()
|
|
||||||
view.resize_on_select = not view.isVisible()
|
|
||||||
if view.model().rowCount(None) > 1:
|
|
||||||
view.resizeRowToContents(0)
|
|
||||||
height = view.rowHeight(0)
|
|
||||||
view.verticalHeader().setDefaultSectionSize(height)
|
|
||||||
self.sync_news()
|
self.sync_news()
|
||||||
self.sync_catalogs()
|
self.sync_catalogs()
|
||||||
self.refresh_ondevice_info(device_connected = True)
|
self.refresh_ondevice_info(device_connected = True)
|
||||||
@ -1048,8 +1026,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
self.book_on_device(None, reset=True)
|
self.book_on_device(None, reset=True)
|
||||||
if reset_only:
|
if reset_only:
|
||||||
return
|
return
|
||||||
self.library_view.write_settings()
|
self.library_view.set_device_connected(device_connected)
|
||||||
self.library_view.model().set_device_connected(device_connected)
|
|
||||||
############################################################################
|
############################################################################
|
||||||
|
|
||||||
######################### Fetch annotations ################################
|
######################### Fetch annotations ################################
|
||||||
@ -2262,8 +2239,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
return
|
return
|
||||||
d = ConfigDialog(self, self.library_view.model(),
|
d = ConfigDialog(self, self.library_view.model(),
|
||||||
server=self.content_server)
|
server=self.content_server)
|
||||||
# Save current column widths in case columns are turned on or off
|
|
||||||
self.library_view.write_settings()
|
|
||||||
|
|
||||||
d.exec_()
|
d.exec_()
|
||||||
self.content_server = d.server
|
self.content_server = d.server
|
||||||
@ -2302,7 +2277,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
self.status_bar.clearMessage()
|
self.status_bar.clearMessage()
|
||||||
self.search.clear_to_help()
|
self.search.clear_to_help()
|
||||||
self.status_bar.reset_info()
|
self.status_bar.reset_info()
|
||||||
self.library_view.sortByColumn(3, Qt.DescendingOrder)
|
|
||||||
self.library_view.model().count_changed()
|
self.library_view.model().count_changed()
|
||||||
|
|
||||||
############################################################################
|
############################################################################
|
||||||
@ -2328,14 +2302,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
'''
|
'''
|
||||||
page = 0 if location == 'library' else 1 if location == 'main' else 2 if location == 'carda' else 3
|
page = 0 if location == 'library' else 1 if location == 'main' else 2 if location == 'carda' else 3
|
||||||
self.stack.setCurrentIndex(page)
|
self.stack.setCurrentIndex(page)
|
||||||
view = self.memory_view if page == 1 else \
|
|
||||||
self.card_a_view if page == 2 else \
|
|
||||||
self.card_b_view if page == 3 else None
|
|
||||||
if view:
|
|
||||||
if view.resize_on_select:
|
|
||||||
if not view.restore_column_widths():
|
|
||||||
view.resizeColumnsToContents()
|
|
||||||
view.resize_on_select = False
|
|
||||||
self.status_bar.reset_info()
|
self.status_bar.reset_info()
|
||||||
self.sidebar.location_changed(location)
|
self.sidebar.location_changed(location)
|
||||||
if location == 'library':
|
if location == 'library':
|
||||||
@ -2442,9 +2408,9 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
config.set('main_window_geometry', self.saveGeometry())
|
config.set('main_window_geometry', self.saveGeometry())
|
||||||
dynamic.set('sort_history', self.library_view.model().sort_history)
|
dynamic.set('sort_history', self.library_view.model().sort_history)
|
||||||
self.sidebar.save_state()
|
self.sidebar.save_state()
|
||||||
self.library_view.write_settings()
|
for view in ('library_view', 'memory_view', 'card_a_view',
|
||||||
if self.device_connected:
|
'card_b_view'):
|
||||||
self.save_device_view_settings()
|
getattr(self, view).save_state()
|
||||||
|
|
||||||
def restart(self):
|
def restart(self):
|
||||||
self.quit(restart=True)
|
self.quit(restart=True)
|
||||||
|
@ -244,7 +244,7 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
|
|||||||
self.pos.editingFinished.connect(self.goto_page_num)
|
self.pos.editingFinished.connect(self.goto_page_num)
|
||||||
self.connect(self.vertical_scrollbar, SIGNAL('valueChanged(int)'),
|
self.connect(self.vertical_scrollbar, SIGNAL('valueChanged(int)'),
|
||||||
lambda x: self.goto_page(x/100.))
|
lambda x: self.goto_page(x/100.))
|
||||||
self.connect(self.search, SIGNAL('search(PyQt_PyObject, PyQt_PyObject)'), self.find)
|
self.search.search.connect(self.find)
|
||||||
self.connect(self.toc, SIGNAL('clicked(QModelIndex)'), self.toc_clicked)
|
self.connect(self.toc, SIGNAL('clicked(QModelIndex)'), self.toc_clicked)
|
||||||
self.connect(self.reference, SIGNAL('goto(PyQt_PyObject)'), self.goto)
|
self.connect(self.reference, SIGNAL('goto(PyQt_PyObject)'), self.goto)
|
||||||
|
|
||||||
|
@ -182,13 +182,13 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
columns = ['id', 'title',
|
columns = ['id', 'title',
|
||||||
# col table link_col query
|
# col table link_col query
|
||||||
('authors', 'authors', 'author', 'sortconcat(link.id, name)'),
|
('authors', 'authors', 'author', 'sortconcat(link.id, name)'),
|
||||||
('publisher', 'publishers', 'publisher', 'name'),
|
|
||||||
('rating', 'ratings', 'rating', 'ratings.rating'),
|
|
||||||
'timestamp',
|
'timestamp',
|
||||||
'(SELECT MAX(uncompressed_size) FROM data WHERE book=books.id) size',
|
'(SELECT MAX(uncompressed_size) FROM data WHERE book=books.id) size',
|
||||||
|
('rating', 'ratings', 'rating', 'ratings.rating'),
|
||||||
('tags', 'tags', 'tag', 'group_concat(name)'),
|
('tags', 'tags', 'tag', 'group_concat(name)'),
|
||||||
'(SELECT text FROM comments WHERE book=books.id) comments',
|
'(SELECT text FROM comments WHERE book=books.id) comments',
|
||||||
('series', 'series', 'series', 'name'),
|
('series', 'series', 'series', 'name'),
|
||||||
|
('publisher', 'publishers', 'publisher', 'name'),
|
||||||
'series_index',
|
'series_index',
|
||||||
'sort',
|
'sort',
|
||||||
'author_sort',
|
'author_sort',
|
||||||
@ -212,8 +212,9 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
custom_cols = list(sorted(custom_map.keys()))
|
custom_cols = list(sorted(custom_map.keys()))
|
||||||
lines.extend([custom_map[x] for x in custom_cols])
|
lines.extend([custom_map[x] for x in custom_cols])
|
||||||
|
|
||||||
self.FIELD_MAP = {'id':0, 'title':1, 'authors':2, 'publisher':3, 'rating':4, 'timestamp':5,
|
self.FIELD_MAP = {'id':0, 'title':1, 'authors':2, 'timestamp':3,
|
||||||
'size':6, 'tags':7, 'comments':8, 'series':9, 'series_index':10,
|
'size':4, 'rating':5, 'tags':6, 'comments':7, 'series':8,
|
||||||
|
'publisher':9, 'series_index':10,
|
||||||
'sort':11, 'author_sort':12, 'formats':13, 'isbn':14, 'path':15,
|
'sort':11, 'author_sort':12, 'formats':13, 'isbn':14, 'path':15,
|
||||||
'lccn':16, 'pubdate':17, 'flags':18, 'uuid':19}
|
'lccn':16, 'pubdate':17, 'flags':18, 'uuid':19}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user