Merge from custcol trunk

This commit is contained in:
Charles Haley 2010-05-18 06:24:21 +01:00
commit bcd5d792b2
13 changed files with 880 additions and 682 deletions

View File

@ -85,10 +85,10 @@ class USBMS(CLI, Device):
lpath = lpath.replace('\\', '/')
idx = bl_cache.get(lpath, None)
if idx is not None:
bl_cache[lpath] = None
if self.update_metadata_item(bl[idx]):
#print 'update_metadata_item returned true'
changed = True
bl_cache[lpath] = None
else:
#print "adding new book", 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
# indices into the booklist if book not in filesystem, None otherwise
# 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:
need_sync = True
del bl[idx]

View File

@ -32,8 +32,7 @@ class SchedulerDialog(QDialog, Ui_Dialog):
self.search.setMinimumContentsLength(25)
self.search.initialize('scheduler_search_history')
self.recipe_box.layout().insertWidget(0, self.search)
self.connect(self.search, SIGNAL('search(PyQt_PyObject,PyQt_PyObject)'),
self.recipe_model.search)
self.search.search.connect(self.recipe_model.search)
self.connect(self.recipe_model, SIGNAL('searched(PyQt_PyObject)'),
self.search.search_done)
self.connect(self.recipe_model, SIGNAL('searched(PyQt_PyObject)'),

View 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)

View 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)
# }}}

View File

@ -1,318 +1,47 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
__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
from operator import attrgetter
from math import cos, sin, pi
import shutil, functools, re, os, traceback
from contextlib import closing
from operator import attrgetter
from PyQt4.QtGui import QTableView, QAbstractItemView, QColor, \
QPainterPath, QLinearGradient, QBrush, \
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 PyQt4.Qt import QAbstractTableModel, Qt, pyqtSignal, QIcon, QImage, \
QModelIndex, QVariant, 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.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.utils.config import tweaks
from calibre.utils.date import dt_factory, qt_to_dt, isoformat, now
from calibre.utils.pyparsing import ParseException
from calibre.utils.date import dt_factory, qt_to_dt, isoformat
from calibre.ebooks.metadata.meta import set_metadata as _set_metadata
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 {{{
class RatingDelegate(QStyledItemDelegate):
COLOR = QColor("blue")
SIZE = 16
PEN = QPen(COLOR, 1, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)
def human_readable(size, precision=1):
""" Convert a size in bytes into megabytes """
return ('%.'+str(precision)+'f') % ((size/(1024.*1024.)),)
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:
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)
# }}}
TIME_FMT = '%d %b %Y'
class BooksModel(QAbstractTableModel): # {{{
about_to_be_sorted = pyqtSignal(object, name='aboutToBeSorted')
sorting_done = pyqtSignal(object, name='sortingDone')
database_changed = pyqtSignal(object, name='databaseChanged')
about_to_be_sorted = pyqtSignal(object, name='aboutToBeSorted')
sorting_done = pyqtSignal(object, name='sortingDone')
database_changed = pyqtSignal(object, name='databaseChanged')
new_bookdisplay_data = pyqtSignal(object)
count_changed_signal = pyqtSignal(int)
searched = pyqtSignal(object)
orig_headers = {
'title' : _("Title"),
'ondevice' : _("On Device"),
'authors' : _("Author(s)"),
'size' : _("Size (MB)"),
'timestamp' : _("Date"),
@ -321,7 +50,6 @@ class BooksModel(QAbstractTableModel): # {{{
'publisher' : _("Publisher"),
'tags' : _("Tags"),
'series' : _("Series"),
'ondevice' : _("On Device"),
}
def __init__(self, parent=None, buffer=40):
@ -331,7 +59,7 @@ class BooksModel(QAbstractTableModel): # {{{
self.editable_cols = ['title', 'authors', 'rating', 'publisher',
'tags', 'series', 'timestamp', 'pubdate']
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.last_search = '' # The last search performed on this model
self.column_map = []
@ -342,6 +70,7 @@ class BooksModel(QAbstractTableModel): # {{{
self.bool_no_icon = QIcon(I('list_remove.svg'))
self.bool_blank_icon = QIcon(I('blank.svg'))
self.device_connected = False
self.read_config()
def is_custom_column(self, cc_label):
return cc_label in self.custom_columns
@ -352,29 +81,10 @@ class BooksModel(QAbstractTableModel): # {{{
def read_config(self):
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):
self.device_connected = is_connected
self.read_config()
self.db.refresh_ondevice()
self.database_changed.emit(self.db)
def set_book_on_device_func(self, func):
self.book_on_device = func
@ -382,7 +92,24 @@ class BooksModel(QAbstractTableModel): # {{{
def set_database(self, db):
self.db = db
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)
def refresh_ids(self, ids, current_row=-1):
@ -400,7 +127,7 @@ class BooksModel(QAbstractTableModel): # {{{
id = self.db.id(row)
self.cover_cache.refresh([id])
if row == current_row:
self.emit(SIGNAL('new_bookdisplay_data(PyQt_PyObject)'),
self.new_bookdisplay_data.emit(
self.get_book_display_info(row))
self.dataChanged.emit(self.index(row, 0), self.index(row,
self.columnCount(QModelIndex())-1))
@ -427,7 +154,7 @@ class BooksModel(QAbstractTableModel): # {{{
return ret
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):
''' Return list indices of all cells in index.row()'''
@ -470,14 +197,14 @@ class BooksModel(QAbstractTableModel): # {{{
try:
self.db.search(text)
except ParseException:
self.emit(SIGNAL('searched(PyQt_PyObject)'), False)
self.searched.emit(False)
return
self.last_search = text
if reset:
self.clear_caches()
self.reset()
if self.last_search:
self.emit(SIGNAL('searched(PyQt_PyObject)'), True)
self.searched.emit(True)
def sort(self, col, order, reset=True):
@ -491,7 +218,6 @@ class BooksModel(QAbstractTableModel): # {{{
self.reset()
self.sorted_on = (self.column_map[col], order)
self.sort_history.insert(0, self.sorted_on)
del self.sort_history[3:] # clean up older searches
self.sorting_done.emit(self.db.index)
def refresh(self, reset=True):
@ -505,7 +231,7 @@ class BooksModel(QAbstractTableModel): # {{{
def resort(self, reset=True):
try:
col = self.column_map.index(self.sorted_on[0])
except:
except ValueError:
col = 0
self.sort(col, self.sorted_on[1], reset=reset)
@ -576,7 +302,7 @@ class BooksModel(QAbstractTableModel): # {{{
self.set_cache(idx)
data = self.get_book_display_info(idx)
if emit_signal:
self.emit(SIGNAL('new_bookdisplay_data(PyQt_PyObject)'), data)
self.new_bookdisplay_data.emit(data)
else:
return data
@ -973,8 +699,7 @@ class BooksModel(QAbstractTableModel): # {{{
self.db.set_pubdate(id, qt_to_dt(val, as_utc=False))
else:
self.db.set(row, column, val)
self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), \
index, index)
self.dataChanged.emit(index, index)
return True
def set_search_restriction(self, s):
@ -982,249 +707,7 @@ class BooksModel(QAbstractTableModel): # {{{
# }}}
class BooksView(TableView):
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):
class OnDeviceSearch(SearchQueryParser): # {{{
def __init__(self, model):
SearchQueryParser.__init__(self)
@ -1282,15 +765,30 @@ class OnDeviceSearch(SearchQueryParser):
traceback.print_exc()
return matches
# }}}
class DeviceBooksModel(BooksModel):
class DeviceBooksModel(BooksModel): # {{{
booklist_dirtied = pyqtSignal()
def __init__(self, parent):
BooksModel.__init__(self, parent)
self.db = []
self.map = []
self.sorted_map = []
self.sorted_on = DEFAULT_SORT
self.sort_history = [self.sorted_on]
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.search_engine = OnDeviceSearch(self)
self.editable = True
@ -1300,7 +798,7 @@ class DeviceBooksModel(BooksModel):
self.marked_for_deletion[job] = self.indices(rows)
for row in rows:
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):
if not self.marked_for_deletion.has_key(job):
@ -1309,7 +807,7 @@ class DeviceBooksModel(BooksModel):
for row in rows:
if not succeeded:
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):
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
flags = QAbstractTableModel.flags(self, index)
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
return flags
@ -1339,7 +838,7 @@ class DeviceBooksModel(BooksModel):
try:
matches = self.search_engine.parse(text)
except ParseException:
self.emit(SIGNAL('searched(PyQt_PyObject)'), False)
self.searched.emit(False)
return
self.map = []
@ -1351,12 +850,9 @@ class DeviceBooksModel(BooksModel):
self.reset()
self.last_search = text
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):
descending = order != Qt.AscendingOrder
def strcmp(attr):
@ -1395,22 +891,30 @@ class DeviceBooksModel(BooksModel):
x, y = authors_to_string(self.db[x].authors), \
authors_to_string(self.db[y].authors)
return cmp(x, y)
fcmp = strcmp('title_sorter') if col == 0 else authorcmp if col == 1 else \
sizecmp if col == 2 else datecmp if col == 3 else tagscmp if col == 4 else libcmp
cname = self.column_map[col]
fcmp = {
'title': strcmp('title_sorter'),
'authors' : authorcmp,
'size' : sizecmp,
'timestamp': datecmp,
'tags': tagscmp,
'inlibrary': libcmp,
}[cname]
self.map.sort(cmp=fcmp, reverse=descending)
if len(self.map) == len(self.db):
self.sorted_map = list(self.map)
else:
self.sorted_map = list(range(len(self.db)))
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:
self.reset()
def columnCount(self, parent):
if parent and parent.isValid():
return 0
return 6
return len(self.column_map)
def rowCount(self, parent):
if parent and parent.isValid():
@ -1443,7 +947,7 @@ class DeviceBooksModel(BooksModel):
dt = dt_factory(item.datetime, assume_utc=True)
data[_('Timestamp')] = isoformat(dt, sep=' ', as_utc=False)
data[_('Tags')] = ', '.join(item.tags)
self.emit(SIGNAL('new_bookdisplay_data(PyQt_PyObject)'), data)
self.new_bookdisplay_data.emit(data)
def paths(self, 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):
row, col = index.row(), index.column()
cname = self.column_map[col]
if role == Qt.DisplayRole or role == Qt.EditRole:
if col == 0:
if cname == 'title':
text = self.db[self.map[row]].title
if not text:
text = self.unknown
return QVariant(text)
elif col == 1:
elif cname == 'authors':
au = self.db[self.map[row]].authors
if not au:
au = self.unknown
# if role == Qt.EditRole:
# return QVariant(au)
return QVariant(authors_to_string(au))
elif col == 2:
elif cname == 'size':
size = self.db[self.map[row]].size
return QVariant(BooksView.human_readable(size))
elif col == 3:
return QVariant(human_readable(size))
elif cname == 'timestamp':
dt = self.db[self.map[row]].datetime
dt = dt_factory(dt, assume_utc=True, as_utc=False)
return QVariant(strftime(BooksView.TIME_FMT, dt.timetuple()))
elif col == 4:
return QVariant(strftime(TIME_FMT, dt.timetuple()))
elif cname == 'tags':
tags = self.db[self.map[row]].tags
if 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():
if self.map[index.row()] in self.indices_to_be_deleted():
return QVariant('Marked for deletion')
col = index.column()
if col in [0, 1] or (col == 4 and self.db.supports_tags()):
if self.map[row] in self.indices_to_be_deleted():
return QVariant(_('Marked for deletion'))
if cname in ['title', 'authors'] or (cname == 'tags' and self.db.supports_tags()):
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:
return QVariant(self.bool_yes_icon)
@ -1497,14 +997,9 @@ class DeviceBooksModel(BooksModel):
def headerData(self, section, orientation, role):
if role != Qt.DisplayRole:
return NONE
text = ""
if orientation == Qt.Horizontal:
if section == 0: text = _("Title")
elif section == 1: text = _("Author(s)")
elif section == 2: text = _("Size (MB)")
elif section == 3: text = _("Date")
elif section == 4: text = _("Tags")
elif section == 5: text = _("In Library")
cname = self.column_map[section]
text = self.headers[cname]
return QVariant(text)
else:
return QVariant(section+1)
@ -1513,23 +1008,22 @@ class DeviceBooksModel(BooksModel):
done = False
if role == Qt.EditRole:
row, col = index.row(), index.column()
if col in [2, 3]:
cname = self.column_map[col]
if cname in ('size', 'timestamp', 'inlibrary'):
return False
val = unicode(value.toString()).strip()
idx = self.map[row]
if col == 0:
if cname == 'title' :
self.db[idx].title = val
self.db[idx].title_sorter = val
elif col == 1:
elif cname == 'authors':
self.db[idx].authors = string_to_authors(val)
elif col == 4:
elif cname == 'tags':
tags = [i.strip() for i in val.split(',')]
tags = [t for t in tags if t]
self.db.set_tags(self.db[idx], tags)
self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), index, index)
self.emit(SIGNAL('booklist_dirtied()'))
if col == self.sorted_on[0]:
self.sort(col, self.sorted_on[1])
self.dataChanged.emit(index, index)
self.booklist_dirtied.emit()
done = True
return done
@ -1538,3 +1032,6 @@ class DeviceBooksModel(BooksModel):
def set_search_restriction(self, s):
pass
# }}}

View 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_()
# }}}

View File

@ -81,7 +81,7 @@ class Main(MainWindow, Ui_MainWindow):
self.search = SearchBox2(self)
self.search.initialize('lrf_viewer_search_history')
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_previous_page.setShortcuts([QKeySequence.MoveToPreviousPage, QKeySequence(Qt.Key_Backspace)])

View File

@ -793,7 +793,7 @@
<customwidget>
<class>BooksView</class>
<extends>QTableView</extends>
<header>library.h</header>
<header>calibre/gui2/library/views.h</header>
</customwidget>
<customwidget>
<class>LocationView</class>
@ -803,7 +803,7 @@
<customwidget>
<class>DeviceBooksView</class>
<extends>QTableView</extends>
<header>library.h</header>
<header>calibre/gui2/library/views.h</header>
</customwidget>
<customwidget>
<class>TagsView</class>

View File

@ -6,7 +6,8 @@ __license__ = 'GPL v3'
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__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 calibre.gui2 import config
@ -56,6 +57,8 @@ class SearchBox2(QComboBox):
INTERVAL = 1500 #: Time to wait before emitting search signal
MAX_COUNT = 25
search = pyqtSignal(object, object)
def __init__(self, parent=None):
QComboBox.__init__(self, parent)
self.normal_background = 'rgb(255, 255, 255, 0%)'
@ -108,7 +111,7 @@ class SearchBox2(QComboBox):
def clear(self):
self.clear_to_help()
self.emit(SIGNAL('search(PyQt_PyObject, PyQt_PyObject)'), '', False)
self.search.emit('', False)
def search_done(self, ok):
if not unicode(self.currentText()).strip():
@ -155,7 +158,7 @@ class SearchBox2(QComboBox):
self.help_state = False
refinement = text.startswith(self.prev_search) and ':' not in 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)
self.block_signals(True)
@ -187,7 +190,7 @@ class SearchBox2(QComboBox):
def set_search_string(self, txt):
self.normalize_state()
self.setEditText(txt)
self.emit(SIGNAL('search(PyQt_PyObject, PyQt_PyObject)'), txt, False)
self.search.emit(txt, False)
self.line_edit.end(False)
self.initial_state = False

View File

@ -20,6 +20,7 @@ from calibre.library.database2 import Tag
class TagsView(QTreeView):
need_refresh = pyqtSignal()
restriction_set = pyqtSignal(object)
def __init__(self, *args):
QTreeView.__init__(self, *args)
@ -66,7 +67,7 @@ class TagsView(QTreeView):
else:
self.search_restriction = 'search:"%s"' % unicode(s).strip()
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.emit(SIGNAL('tags_marked(PyQt_PyObject, PyQt_PyObject)'),
self._model.tokens(), self.match_all)

View File

@ -507,9 +507,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
self.card_b_view.set_context_menu(None, None, None,
self.action_view, self.action_save, None, None, self.action_del)
QObject.connect(self.library_view,
SIGNAL('files_dropped(PyQt_PyObject)'),
self.files_dropped, Qt.QueuedConnection)
self.library_view.files_dropped.connect(self.files_dropped, type=Qt.QueuedConnection)
for func, args in [
('connect_to_search_box', (self.search,
self.search_done)),
@ -534,9 +532,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
self.library_view.set_database(db)
self.library_view.model().set_book_on_device_func(self.book_on_device)
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.cover_cache = CoverCache(self.library_path)
self.cover_cache.start()
@ -546,24 +541,16 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
self.connect(self.tags_view,
SIGNAL('tags_marked(PyQt_PyObject, PyQt_PyObject)'),
self.search.search_from_tags)
self.connect(self.tags_view,
SIGNAL('restriction_set(PyQt_PyObject)'),
self.saved_search.clear_to_help)
self.connect(self.tags_view,
SIGNAL('restriction_set(PyQt_PyObject)'),
self.mark_restriction_set)
for x in (self.saved_search.clear_to_help, self.mark_restriction_set):
self.tags_view.restriction_set.connect(x)
self.connect(self.tags_view,
SIGNAL('tags_marked(PyQt_PyObject, PyQt_PyObject)'),
self.saved_search.clear_to_help)
self.connect(self.search,
SIGNAL('search(PyQt_PyObject, PyQt_PyObject)'),
self.tags_view.model().reinit)
self.connect(self.library_view.model(),
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.search.search.connect(self.tags_view.model().reinit)
for x in (self.location_view.count_changed, self.tags_view.recount,
self.restriction_count_changed):
self.library_view.model().count_changed_signal.connect(x)
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)
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):
model = self.location_view.model()
self.memory_view.write_settings()
return
#self.memory_view.write_settings()
for x in range(model.rowCount()):
if x > 1:
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_b_view.set_database(cardblist)
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_catalogs()
self.refresh_ondevice_info(device_connected = True)
@ -1048,8 +1026,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
self.book_on_device(None, reset=True)
if reset_only:
return
self.library_view.write_settings()
self.library_view.model().set_device_connected(device_connected)
self.library_view.set_device_connected(device_connected)
############################################################################
######################### Fetch annotations ################################
@ -2262,8 +2239,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
return
d = ConfigDialog(self, self.library_view.model(),
server=self.content_server)
# Save current column widths in case columns are turned on or off
self.library_view.write_settings()
d.exec_()
self.content_server = d.server
@ -2302,7 +2277,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
self.status_bar.clearMessage()
self.search.clear_to_help()
self.status_bar.reset_info()
self.library_view.sortByColumn(3, Qt.DescendingOrder)
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
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.sidebar.location_changed(location)
if location == 'library':
@ -2442,9 +2408,9 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
config.set('main_window_geometry', self.saveGeometry())
dynamic.set('sort_history', self.library_view.model().sort_history)
self.sidebar.save_state()
self.library_view.write_settings()
if self.device_connected:
self.save_device_view_settings()
for view in ('library_view', 'memory_view', 'card_a_view',
'card_b_view'):
getattr(self, view).save_state()
def restart(self):
self.quit(restart=True)

View File

@ -244,7 +244,7 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
self.pos.editingFinished.connect(self.goto_page_num)
self.connect(self.vertical_scrollbar, SIGNAL('valueChanged(int)'),
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.reference, SIGNAL('goto(PyQt_PyObject)'), self.goto)

View File

@ -182,13 +182,13 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
columns = ['id', 'title',
# col table link_col query
('authors', 'authors', 'author', 'sortconcat(link.id, name)'),
('publisher', 'publishers', 'publisher', 'name'),
('rating', 'ratings', 'rating', 'ratings.rating'),
'timestamp',
'(SELECT MAX(uncompressed_size) FROM data WHERE book=books.id) size',
('rating', 'ratings', 'rating', 'ratings.rating'),
('tags', 'tags', 'tag', 'group_concat(name)'),
'(SELECT text FROM comments WHERE book=books.id) comments',
('series', 'series', 'series', 'name'),
('publisher', 'publishers', 'publisher', 'name'),
'series_index',
'sort',
'author_sort',
@ -212,8 +212,9 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
custom_cols = list(sorted(custom_map.keys()))
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,
'size':6, 'tags':7, 'comments':8, 'series':9, 'series_index':10,
self.FIELD_MAP = {'id':0, 'title':1, 'authors':2, 'timestamp':3,
'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,
'lccn':16, 'pubdate':17, 'flags':18, 'uuid':19}