diff --git a/src/calibre/devices/usbms/driver.py b/src/calibre/devices/usbms/driver.py index 361ee2300b..5273ffe579 100644 --- a/src/calibre/devices/usbms/driver.py +++ b/src/calibre/devices/usbms/driver.py @@ -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] diff --git a/src/calibre/gui2/dialogs/scheduler.py b/src/calibre/gui2/dialogs/scheduler.py index 74ae400524..7e2d75e9e7 100644 --- a/src/calibre/gui2/dialogs/scheduler.py +++ b/src/calibre/gui2/dialogs/scheduler.py @@ -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)'), diff --git a/src/calibre/gui2/library/__init__.py b/src/calibre/gui2/library/__init__.py new file mode 100644 index 0000000000..d7180de99a --- /dev/null +++ b/src/calibre/gui2/library/__init__.py @@ -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 ' +__docformat__ = 'restructuredtext en' + +from PyQt4.Qt import Qt + +DEFAULT_SORT = ('timestamp', Qt.DescendingOrder) diff --git a/src/calibre/gui2/library/delegates.py b/src/calibre/gui2/library/delegates.py new file mode 100644 index 0000000000..c1e4915db1 --- /dev/null +++ b/src/calibre/gui2/library/delegates.py @@ -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 ' +__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) + + +# }}} + diff --git a/src/calibre/gui2/library.py b/src/calibre/gui2/library/models.py similarity index 58% rename from src/calibre/gui2/library.py rename to src/calibre/gui2/library/models.py index b5d2d653e5..f5fbc822b8 100644 --- a/src/calibre/gui2/library.py +++ b/src/calibre/gui2/library/models.py @@ -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 ' +__copyright__ = '2010, Kovid Goyal ' +__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 edit me

")) - 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 + +# }}} + diff --git a/src/calibre/gui2/library/views.py b/src/calibre/gui2/library/views.py new file mode 100644 index 0000000000..70a0e05a47 --- /dev/null +++ b/src/calibre/gui2/library/views.py @@ -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 ' +__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_() + +# }}} + diff --git a/src/calibre/gui2/lrf_renderer/main.py b/src/calibre/gui2/lrf_renderer/main.py index 1e27137580..2b76ab0fea 100644 --- a/src/calibre/gui2/lrf_renderer/main.py +++ b/src/calibre/gui2/lrf_renderer/main.py @@ -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)]) diff --git a/src/calibre/gui2/main.ui b/src/calibre/gui2/main.ui index 29292747f8..b7f797f1e0 100644 --- a/src/calibre/gui2/main.ui +++ b/src/calibre/gui2/main.ui @@ -793,7 +793,7 @@ BooksView QTableView -
library.h
+
calibre/gui2/library/views.h
LocationView @@ -803,7 +803,7 @@ DeviceBooksView QTableView -
library.h
+
calibre/gui2/library/views.h
TagsView diff --git a/src/calibre/gui2/search_box.py b/src/calibre/gui2/search_box.py index 776127b698..230debd598 100644 --- a/src/calibre/gui2/search_box.py +++ b/src/calibre/gui2/search_box.py @@ -6,7 +6,8 @@ __license__ = 'GPL v3' __copyright__ = '2009, Kovid Goyal ' __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 diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index 5d85dec0cb..22658291f5 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -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) diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index 16b003a5c6..c8f1ae5ded 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -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) diff --git a/src/calibre/gui2/viewer/main.py b/src/calibre/gui2/viewer/main.py index 77d7269e17..06abb7181c 100644 --- a/src/calibre/gui2/viewer/main.py +++ b/src/calibre/gui2/viewer/main.py @@ -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) diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 5971333078..ed56d35bdc 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -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}