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('\\', '/') 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]

View File

@ -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)'),

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' __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
# }}}

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 = 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)])

View File

@ -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>

View File

@ -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

View File

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

View File

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

View File

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

View File

@ -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}