From d393b430bdfd9601e14f84ad8facf841fce7e979 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 17 May 2010 16:04:30 -0600 Subject: [PATCH 1/6] Start to move column display logic into the view classes, where it belongs --- src/calibre/gui2/library.py | 148 ++++++++++++++++++------------------ src/calibre/gui2/ui.py | 25 +----- 2 files changed, 77 insertions(+), 96 deletions(-) diff --git a/src/calibre/gui2/library.py b/src/calibre/gui2/library.py index b5d2d653e5..806b5851bc 100644 --- a/src/calibre/gui2/library.py +++ b/src/calibre/gui2/library.py @@ -19,7 +19,7 @@ from PyQt4.QtCore import QAbstractTableModel, QVariant, Qt, pyqtSignal, \ from calibre import strftime 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 import NONE, 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 @@ -30,6 +30,15 @@ from calibre.utils.pyparsing import ParseException from calibre.utils.search_query_parser import SearchQueryParser # Delegates {{{ + +class DummyDelegate(QStyledItemDelegate): + + def sizeHint(self, option, index): + return QSize(0, 0) + + def paint(self, painter, option, index): + pass + class RatingDelegate(QStyledItemDelegate): COLOR = QColor("blue") SIZE = 16 @@ -313,6 +322,7 @@ class BooksModel(QAbstractTableModel): # {{{ orig_headers = { 'title' : _("Title"), + 'ondevice' : _("On Device"), 'authors' : _("Author(s)"), 'size' : _("Size (MB)"), 'timestamp' : _("Date"), @@ -321,7 +331,6 @@ class BooksModel(QAbstractTableModel): # {{{ 'publisher' : _("Publisher"), 'tags' : _("Tags"), 'series' : _("Series"), - 'ondevice' : _("On Device"), } def __init__(self, parent=None, buffer=40): @@ -342,6 +351,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 +362,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 +373,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): @@ -982,7 +990,7 @@ class BooksModel(QAbstractTableModel): # {{{ # }}} -class BooksView(TableView): +class BooksView(QTableView): # {{{ TIME_FMT = '%d %b %Y' wrapper = textwrap.TextWrapper(width=20) @@ -997,7 +1005,7 @@ class BooksView(TableView): return ('%.'+str(precision)+'f') % ((size/(1024.*1024.)),) def __init__(self, parent, modelcls=BooksModel): - TableView.__init__(self, parent) + QTableView.__init__(self, parent) self.rating_delegate = RatingDelegate(self) self.timestamp_delegate = DateDelegate(self) self.pubdate_delegate = PubDateDelegate(self) @@ -1005,6 +1013,7 @@ class BooksView(TableView): 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) @@ -1013,13 +1022,9 @@ class BooksView(TableView): 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) + self.selectionModel().currentRowChanged.connect(self._model.current_changed) + self.column_header = self.horizontalHeader() + self._model.database_changed.connect(self.database_changed) hv = self.verticalHeader() hv.setClickable(True) hv.setCursor(Qt.PointingHandCursor) @@ -1040,56 +1045,49 @@ class BooksView(TableView): sm.select(idx, sm.Select|sm.Rows) self.selected_ids = [] - def columns_sorted(self): + 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() + + 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._model.column_map + self.set_ondevice_column_visibility() - 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: + 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'] 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]) + 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')) def set_context_menu(self, edit_metadata, send_to_device, convert, view, save, open_folder, book_details, delete, similar_menu=None): @@ -1131,7 +1129,7 @@ class BooksView(TableView): idx = self._model.column_map.index(colname) except ValueError: idx = 0 - TableView.sortByColumn(self, idx, order) + QTableView.sortByColumn(self, idx, order) @classmethod def paths_from_event(cls, event): @@ -1195,6 +1193,8 @@ class BooksView(TableView): def row_count(self): return self._model.count() +# }}} + class DeviceBooksView(BooksView): def __init__(self, parent): @@ -1218,7 +1218,7 @@ class DeviceBooksView(BooksView): QObject.connect(self._model, SIGNAL('booklist_dirtied()'), slot) def sortByColumn(self, col, order): - TableView.sortByColumn(self, col, order) + QTableView.sortByColumn(self, col, order) def dropEvent(self, *args): error_dialog(self, _('Not allowed'), diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index 16b003a5c6..c65a1ba81c 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -535,8 +535,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): 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() @@ -943,7 +941,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': @@ -1030,10 +1029,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): 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) @@ -1048,8 +1043,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 +2256,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 @@ -2328,14 +2320,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 +2426,6 @@ 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() def restart(self): self.quit(restart=True) From d600ef514bb9c52133fbd092b2dfb4ec1a3351f9 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 17 May 2010 17:17:42 -0600 Subject: [PATCH 2/6] Re-organize code in gui2.library module --- src/calibre/gui2/dialogs/scheduler.py | 3 +- src/calibre/gui2/library/__init__.py | 9 + src/calibre/gui2/library/delegates.py | 311 +++++++++ .../gui2/{library.py => library/models.py} | 607 ++---------------- src/calibre/gui2/library/views.py | 242 +++++++ src/calibre/gui2/lrf_renderer/main.py | 2 +- src/calibre/gui2/main.ui | 4 +- src/calibre/gui2/search_box.py | 11 +- src/calibre/gui2/tag_view.py | 3 +- src/calibre/gui2/ui.py | 26 +- src/calibre/gui2/viewer/main.py | 2 +- 11 files changed, 632 insertions(+), 588 deletions(-) create mode 100644 src/calibre/gui2/library/__init__.py create mode 100644 src/calibre/gui2/library/delegates.py rename src/calibre/gui2/{library.py => library/models.py} (62%) create mode 100644 src/calibre/gui2/library/views.py 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..0080175bfa --- /dev/null +++ b/src/calibre/gui2/library/__init__.py @@ -0,0 +1,9 @@ +#!/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' + + + 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 62% rename from src/calibre/gui2/library.py rename to src/calibre/gui2/library/models.py index 806b5851bc..abff227ae3 100644 --- a/src/calibre/gui2/library.py +++ b/src/calibre/gui2/library/models.py @@ -1,324 +1,42 @@ +#!/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, 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 -# Delegates {{{ +def human_readable(size, precision=1): + """ Convert a size in bytes into megabytes """ + return ('%.'+str(precision)+'f') % ((size/(1024.*1024.)),) -class DummyDelegate(QStyledItemDelegate): - - def sizeHint(self, option, index): - return QSize(0, 0) - - def paint(self, painter, option, index): - pass - -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: - 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"), @@ -408,7 +126,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)) @@ -435,7 +153,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()''' @@ -478,14 +196,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): @@ -584,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 @@ -981,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): @@ -990,241 +707,7 @@ class BooksModel(QAbstractTableModel): # {{{ # }}} -class BooksView(QTableView): # {{{ - 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): - 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) - self.column_header = self.horizontalHeader() - 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 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() - - 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._model.column_map - self.set_ondevice_column_visibility() - - 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')) - - 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 - QTableView.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): - QTableView.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,8 +765,11 @@ 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) @@ -1300,7 +786,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 +795,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))) @@ -1339,7 +825,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,7 +837,7 @@ 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): @@ -1443,7 +929,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 ] @@ -1471,11 +957,11 @@ class DeviceBooksModel(BooksModel): return QVariant(authors_to_string(au)) elif col == 2: size = self.db[self.map[row]].size - return QVariant(BooksView.human_readable(size)) + return QVariant(human_readable(size)) elif col == 3: 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())) + return QVariant(strftime(TIME_FMT, dt.timetuple())) elif col == 4: tags = self.db[self.map[row]].tags if tags: @@ -1526,8 +1012,8 @@ class DeviceBooksModel(BooksModel): 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()')) + self.dataChanged.emit(index, index) + self.booklist_dirtied.emit() if col == self.sorted_on[0]: self.sort(col, self.sorted_on[1]) done = True @@ -1538,3 +1024,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..9f9532687c --- /dev/null +++ b/src/calibre/gui2/library/views.py @@ -0,0 +1,242 @@ +#!/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 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 + + +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) + self.column_header = self.horizontalHeader() + 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 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() + + 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._model.column_map + self.set_ondevice_column_visibility() + + 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')) + + 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 + QTableView.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.files_dropped.emit(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): + 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) + + def resizeColumnsToContents(self): + QTableView.resizeColumnsToContents(self) + self.columns_resized = True + + def connect_dirtied_signal(self, slot): + self._model.booklist_dirtied.connect(slot) + + def sortByColumn(self, col, order): + QTableView.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_() + +# }}} + 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 c65a1ba81c..ff063800d5 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)), @@ -544,24 +542,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): 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) From e4b0f51363fb0bb391fdafa27c1a210214980a21 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 17 May 2010 19:33:00 -0600 Subject: [PATCH 3/6] Fix the booklist delete algorithm --- src/calibre/devices/usbms/driver.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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] From 61e78b6a173848bccaaf0cd24c226ef5aae23d22 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 17 May 2010 20:51:46 -0600 Subject: [PATCH 4/6] Right click menu for hiding/showing/sorting columns --- src/calibre/gui2/library/views.py | 113 +++++++++++++++++++++++++++++- 1 file changed, 111 insertions(+), 2 deletions(-) diff --git a/src/calibre/gui2/library/views.py b/src/calibre/gui2/library/views.py index 9f9532687c..8734e7582a 100644 --- a/src/calibre/gui2/library/views.py +++ b/src/calibre/gui2/library/views.py @@ -6,6 +6,7 @@ __copyright__ = '2010, Kovid Goyal ' __docformat__ = 'restructuredtext en' import os +from functools import partial from PyQt4.Qt import QTableView, Qt, QAbstractItemView, QMenu, pyqtSignal @@ -40,7 +41,15 @@ class BooksView(QTableView): # {{{ 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) @@ -49,6 +58,69 @@ class BooksView(QTableView): # {{{ 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._model.sort(idx, Qt.AscendingOrder) + h.setSortIndicator(idx, Qt.AscendingOrder) + elif action == 'descending': + self._model.sort(idx, Qt.DescendingOrder) + h.setSortIndicator(idx, Qt.DescendingOrder) + + 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.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] @@ -71,14 +143,47 @@ class BooksView(QTableView): # {{{ self._model.set_device_connected(is_connected) self.set_ondevice_column_visibility() + 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['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) + import pprint + pprint.pprint(state) + 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() + state + + def apply_state(self, state): + pass + + def restore_state(self): + pass + + + @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._model.column_map - self.set_ondevice_column_visibility() + cm = self.column_map for colhead in cm: if self._model.is_custom_column(colhead): @@ -106,6 +211,9 @@ class BooksView(QTableView): # {{{ 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) @@ -177,6 +285,7 @@ class BooksView(QTableView): # {{{ 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) From 428cebd36505617f885702100968df37c7b66439 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 17 May 2010 22:41:53 -0600 Subject: [PATCH 5/6] Framework for saving/restoring state in the table views. Needs to be linked up fully. --- src/calibre/gui2/library/__init__.py | 3 +- src/calibre/gui2/library/models.py | 79 ++++++++++++--------- src/calibre/gui2/library/views.py | 102 +++++++++++++++++++-------- src/calibre/gui2/ui.py | 8 --- 4 files changed, 120 insertions(+), 72 deletions(-) diff --git a/src/calibre/gui2/library/__init__.py b/src/calibre/gui2/library/__init__.py index 0080175bfa..8aa897b413 100644 --- a/src/calibre/gui2/library/__init__.py +++ b/src/calibre/gui2/library/__init__.py @@ -5,5 +5,6 @@ __license__ = 'GPL v3' __copyright__ = '2010, Kovid Goyal ' __docformat__ = 'restructuredtext en' +from PyQt4.Qt import Qt - +DEFAULT_SORT = ('timestamp', Qt.AscendingOrder) diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py index abff227ae3..97e2317dce 100644 --- a/src/calibre/gui2/library/models.py +++ b/src/calibre/gui2/library/models.py @@ -22,6 +22,7 @@ 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 def human_readable(size, precision=1): """ Convert a size in bytes into megabytes """ @@ -58,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 = [] @@ -217,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): @@ -776,7 +776,19 @@ class DeviceBooksModel(BooksModel): # {{{ 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 @@ -813,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 @@ -881,22 +894,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(): @@ -942,39 +963,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(human_readable(size)) - elif col == 3: + elif cname == 'timestamp': dt = self.db[self.map[row]].datetime dt = dt_factory(dt, assume_utc=True, as_utc=False) return QVariant(strftime(TIME_FMT, dt.timetuple())) - elif col == 4: + 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) @@ -983,14 +1000,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) @@ -999,23 +1011,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.dataChanged.emit(index, index) self.booklist_dirtied.emit() - if col == self.sorted_on[0]: - self.sort(col, self.sorted_on[1]) done = True return done diff --git a/src/calibre/gui2/library/views.py b/src/calibre/gui2/library/views.py index 8734e7582a..ee7ab5e838 100644 --- a/src/calibre/gui2/library/views.py +++ b/src/calibre/gui2/library/views.py @@ -15,7 +15,8 @@ from calibre.gui2.library.delegates import RatingDelegate, PubDateDelegate, \ CcBoolDelegate, CcCommentsDelegate, CcDateDelegate from calibre.gui2.library.models import BooksModel, DeviceBooksModel from calibre.utils.config import tweaks -from calibre.gui2 import error_dialog +from calibre.gui2 import error_dialog, gprefs +from calibre.gui2.library import DEFAULT_SORT class BooksView(QTableView): # {{{ @@ -72,11 +73,9 @@ class BooksView(QTableView): # {{{ elif action == 'show': h.setSectionHidden(idx, False) elif action == 'ascending': - self._model.sort(idx, Qt.AscendingOrder) - h.setSortIndicator(idx, Qt.AscendingOrder) + self.sortByColumn(idx, Qt.AscendingOrder) elif action == 'descending': - self._model.sort(idx, Qt.DescendingOrder) - h.setSortIndicator(idx, Qt.DescendingOrder) + self.sortByColumn(idx, Qt.DescendingOrder) self.save_state() @@ -143,12 +142,15 @@ class BooksView(QTableView): # {{{ 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()): @@ -156,22 +158,83 @@ class BooksView(QTableView): # {{{ state['column_positions'][name] = h.visualIndex(i) if name != 'ondevice': state['column_sizes'][name] = h.sectionSize(i) - import pprint - pprint.pprint(state) 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() - 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): - pass + 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 restore_state(self): - pass + name = unicode(self.objectName()) + old_state = None + if name: + old_state = gprefs.get(name + ' books view state', None) + if old_state is None: + # Default layout + 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 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): @@ -239,22 +302,6 @@ class BooksView(QTableView): # {{{ 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 - QTableView.sortByColumn(self, idx, order) @classmethod def paths_from_event(cls, event): @@ -340,9 +387,6 @@ class DeviceBooksView(BooksView): # {{{ def connect_dirtied_signal(self, slot): self._model.booklist_dirtied.connect(slot) - def sortByColumn(self, col, order): - QTableView.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_() diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index ff063800d5..536c68f77d 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -532,7 +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)])) self.search.setFocus(Qt.OtherFocusReason) self.cover_cache = CoverCache(self.library_path) self.cover_cache.start() @@ -1017,12 +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) - 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) @@ -2284,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() ############################################################################ From e0e0093fe552ddacca4d4ffc709e4ff16389186d Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 17 May 2010 23:08:30 -0600 Subject: [PATCH 6/6] Link up save/restore column layout functionality --- src/calibre/gui2/library/__init__.py | 2 +- src/calibre/gui2/library/models.py | 5 +--- src/calibre/gui2/library/views.py | 43 +++++++++++++++++++--------- src/calibre/gui2/ui.py | 3 ++ src/calibre/library/database2.py | 9 +++--- 5 files changed, 39 insertions(+), 23 deletions(-) diff --git a/src/calibre/gui2/library/__init__.py b/src/calibre/gui2/library/__init__.py index 8aa897b413..d7180de99a 100644 --- a/src/calibre/gui2/library/__init__.py +++ b/src/calibre/gui2/library/__init__.py @@ -7,4 +7,4 @@ __docformat__ = 'restructuredtext en' from PyQt4.Qt import Qt -DEFAULT_SORT = ('timestamp', Qt.AscendingOrder) +DEFAULT_SORT = ('timestamp', Qt.DescendingOrder) diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py index 97e2317dce..f5fbc822b8 100644 --- a/src/calibre/gui2/library/models.py +++ b/src/calibre/gui2/library/models.py @@ -231,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) @@ -853,9 +853,6 @@ class DeviceBooksModel(BooksModel): # {{{ 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): diff --git a/src/calibre/gui2/library/views.py b/src/calibre/gui2/library/views.py index ee7ab5e838..70a0e05a47 100644 --- a/src/calibre/gui2/library/views.py +++ b/src/calibre/gui2/library/views.py @@ -49,8 +49,8 @@ class BooksView(QTableView): # {{{ 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) @@ -76,6 +76,8 @@ class BooksView(QTableView): # {{{ 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() @@ -117,6 +119,13 @@ class BooksView(QTableView): # {{{ 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)) @@ -209,25 +218,30 @@ class BooksView(QTableView): # {{{ 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: - # Default layout - 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)) + old_state = self.get_default_state() if tweaks['sort_columns_at_startup'] is not None: old_state['sort_history'] = tweaks['sort_columns_at_startup'] @@ -379,6 +393,7 @@ class DeviceBooksView(BooksView): # {{{ def set_database(self, db): self._model.set_database(db) + self.restore_state() def resizeColumnsToContents(self): QTableView.resizeColumnsToContents(self) diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index 536c68f77d..c8f1ae5ded 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -2408,6 +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() + 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/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}