Make column order sortable in book list

This commit is contained in:
Kovid Goyal 2008-11-05 00:09:10 -08:00
parent 3a15dea106
commit a30a10dac8
5 changed files with 488 additions and 170 deletions

View File

@ -17,6 +17,8 @@ import calibre.resources as resources
NONE = QVariant() #: Null value to return from the data function of item models NONE = QVariant() #: Null value to return from the data function of item models
ALL_COLUMNS = ['title', 'authors', 'size', 'timestamp', 'rating', 'publisher', 'tags', 'series']
def _config(): def _config():
c = Config('gui', 'preferences for the calibre GUI') c = Config('gui', 'preferences for the calibre GUI')
c.add_opt('frequently_used_directories', default=[], c.add_opt('frequently_used_directories', default=[],
@ -47,6 +49,10 @@ def _config():
help=_('Options for the LRF ebook viewer')) help=_('Options for the LRF ebook viewer'))
c.add_opt('internally_viewed_formats', default=['LRF', 'EPUB', 'LIT', 'MOBI', 'PRC', 'HTML', 'FB2'], c.add_opt('internally_viewed_formats', default=['LRF', 'EPUB', 'LIT', 'MOBI', 'PRC', 'HTML', 'FB2'],
help=_('Formats that are viewed using the internal viewer')) help=_('Formats that are viewed using the internal viewer'))
c.add_opt('column_map', default=ALL_COLUMNS,
help=_('Columns to be displayed in the book list'))
c.add_opt('autolaunch_server', default=False, help=_('Automatically launch content server on application startup'))
return ConfigProxy(c) return ConfigProxy(c)
config = _config() config = _config()

View File

@ -2,21 +2,24 @@ __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import os, re import os, re
from PyQt4.QtGui import QDialog, QMessageBox, QListWidgetItem, QIcon from PyQt4.QtGui import QDialog, QMessageBox, QListWidgetItem, QIcon, \
from PyQt4.QtCore import SIGNAL, QTimer, Qt, QSize, QVariant QDesktopServices, QVBoxLayout, QLabel, QPlainTextEdit
from PyQt4.QtCore import SIGNAL, QTimer, Qt, QSize, QVariant, QUrl
from calibre import islinux from calibre import islinux
from calibre.gui2.dialogs.config_ui import Ui_Dialog from calibre.gui2.dialogs.config_ui import Ui_Dialog
from calibre.gui2 import qstring_to_unicode, choose_dir, error_dialog, config, warning_dialog from calibre.gui2 import qstring_to_unicode, choose_dir, error_dialog, config, \
warning_dialog, ALL_COLUMNS
from calibre.utils.config import prefs from calibre.utils.config import prefs
from calibre.gui2.widgets import FilenamePattern from calibre.gui2.widgets import FilenamePattern
from calibre.gui2.library import BooksModel
from calibre.ebooks import BOOK_EXTENSIONS from calibre.ebooks import BOOK_EXTENSIONS
from calibre.ebooks.epub.iterator import is_supported from calibre.ebooks.epub.iterator import is_supported
from calibre.library import server_config
class ConfigDialog(QDialog, Ui_Dialog): class ConfigDialog(QDialog, Ui_Dialog):
def __init__(self, window, db, columns): def __init__(self, window, db, server=None):
QDialog.__init__(self, window) QDialog.__init__(self, window)
Ui_Dialog.__init__(self) Ui_Dialog.__init__(self)
self.ICON_SIZES = {0:QSize(48, 48), 1:QSize(32,32), 2:QSize(24,24)} self.ICON_SIZES = {0:QSize(48, 48), 1:QSize(32,32), 2:QSize(24,24)}
@ -24,8 +27,9 @@ class ConfigDialog(QDialog, Ui_Dialog):
self.item1 = QListWidgetItem(QIcon(':/images/metadata.svg'), _('General'), self.category_list) self.item1 = QListWidgetItem(QIcon(':/images/metadata.svg'), _('General'), self.category_list)
self.item2 = QListWidgetItem(QIcon(':/images/lookfeel.svg'), _('Interface'), self.category_list) self.item2 = QListWidgetItem(QIcon(':/images/lookfeel.svg'), _('Interface'), self.category_list)
self.item3 = QListWidgetItem(QIcon(':/images/view.svg'), _('Advanced'), self.category_list) self.item3 = QListWidgetItem(QIcon(':/images/view.svg'), _('Advanced'), self.category_list)
self.item4 = QListWidgetItem(QIcon(':/images/network-server.svg'), _('Content\nServer'), self.category_list)
self.db = db self.db = db
self.current_cols = columns self.server = None
path = prefs['library_path'] path = prefs['library_path']
self.location.setText(path if path else '') self.location.setText(path if path else '')
self.connect(self.browse_button, SIGNAL('clicked(bool)'), self.browse) self.connect(self.browse_button, SIGNAL('clicked(bool)'), self.browse)
@ -45,14 +49,18 @@ class ConfigDialog(QDialog, Ui_Dialog):
self.priority.addItem('Idle') self.priority.addItem('Idle')
if not islinux: if not islinux:
self.dirs_box.setVisible(False) self.dirs_box.setVisible(False)
for hidden, hdr in self.current_cols: column_map = config['column_map']
item = QListWidgetItem(hdr, self.columns) for col in column_map + [i for i in ALL_COLUMNS if i not in column_map]:
item.setFlags(Qt.ItemIsEnabled|Qt.ItemIsUserCheckable) item = QListWidgetItem(BooksModel.headers[col], self.columns)
if hidden: item.setData(Qt.UserRole, QVariant(col))
item.setCheckState(Qt.Unchecked) item.setFlags(Qt.ItemIsEnabled|Qt.ItemIsUserCheckable|Qt.ItemIsSelectable)
else: if col in column_map:
item.setCheckState(Qt.Checked) item.setCheckState(Qt.Checked)
else:
item.setCheckState(Qt.Unchecked)
self.connect(self.column_up, SIGNAL('clicked()'), self.up_column)
self.connect(self.column_down, SIGNAL('clicked()'), self.down_column)
self.filename_pattern = FilenamePattern(self) self.filename_pattern = FilenamePattern(self)
self.metadata_box.layout().insertWidget(0, self.filename_pattern) self.metadata_box.layout().insertWidget(0, self.filename_pattern)
@ -96,8 +104,75 @@ class ConfigDialog(QDialog, Ui_Dialog):
self.viewer.item(self.viewer.count()-1).setCheckState(Qt.Checked if ext.upper() in config['internally_viewed_formats'] else Qt.Unchecked) self.viewer.item(self.viewer.count()-1).setCheckState(Qt.Checked if ext.upper() in config['internally_viewed_formats'] else Qt.Unchecked)
added_html = ext == 'html' added_html = ext == 'html'
self.viewer.sortItems() self.viewer.sortItems()
self.start.setEnabled(not getattr(self.server, 'is_running', False))
self.test.setEnabled(not self.start.isEnabled())
self.stop.setDisabled(self.start.isEnabled())
self.connect(self.start, SIGNAL('clicked()'), self.start_server)
self.connect(self.view_logs, SIGNAL('clicked()'), self.view_server_logs)
self.connect(self.stop, SIGNAL('clicked()'), self.stop_server)
self.connect(self.test, SIGNAL('clicked()'), self.test_server)
opts = server_config().parse()
self.port.setValue(opts.port)
self.username.setText(opts.username)
self.password.setText(opts.password if opts.password else '')
self.auto_launch.setChecked(config['autolaunch_server'])
def up_column(self):
idx = self.columns.currentRow()
if idx > 0:
self.columns.insertItem(idx-1, self.columns.takeItem(idx))
self.columns.setCurrentRow(idx-1)
def down_column(self):
idx = self.columns.currentRow()
if idx < self.columns.count()-1:
self.columns.insertItem(idx+1, self.columns.takeItem(idx))
self.columns.setCurrentRow(idx+1)
def view_server_logs(self):
from calibre.library.server import log_access_file, log_error_file
d = QDialog(self)
d.resize(QSize(800, 600))
layout = QVBoxLayout()
d.setLayout(layout)
layout.addWidget(QLabel(_('Error log:')))
el = QPlainTextEdit(d)
layout.addWidget(el)
el.setPlainText(open(log_error_file, 'rb').read().decode('utf8', 'replace'))
layout.addWidget(QLabel(_('Access log:')))
al = QPlainTextEdit(d)
layout.addWidget(al)
al.setPlainText(open(log_access_file, 'rb').read().decode('utf8', 'replace'))
d.show()
def set_server_options(self):
c = server_config()
c.set('port', self.port.value())
c.set('username', unicode(self.username.text()).strip())
p = unicode(self.password.text()).strip()
if not p:
p = None
c.set('password', p)
def start_server(self):
self.set_server_options()
from calibre.library.server import start_threaded_server
self.server = start_threaded_server(self.db, server_config().parse())
self.start.setEnabled(False)
self.test.setEnabled(True)
self.stop.setEnabled(True)
def stop_server(self):
from calibre.library.server import stop_threaded_server
stop_threaded_server(self.server)
self.server = None
self.start.setEnabled(True)
self.test.setEnabled(False)
self.stop.setEnabled(False)
def test_server(self):
QDesktopServices.openUrl(QUrl('http://127.0.0.1:'+str(self.port.value())))
def compact(self, toggled): def compact(self, toggled):
d = Vacuum(self, self.db) d = Vacuum(self, self.db)
@ -123,7 +198,10 @@ class ConfigDialog(QDialog, Ui_Dialog):
config['new_version_notification'] = bool(self.new_version_notification.isChecked()) config['new_version_notification'] = bool(self.new_version_notification.isChecked())
prefs['network_timeout'] = int(self.timeout.value()) prefs['network_timeout'] = int(self.timeout.value())
path = qstring_to_unicode(self.location.text()) path = qstring_to_unicode(self.location.text())
self.final_columns = [self.columns.item(i).checkState() == Qt.Checked for i in range(self.columns.count())] cols = []
for i in range(self.columns.count()):
cols.append(unicode(self.columns.item(i).data(Qt.UserRole).toString()))
config['column_map'] = cols
config['toolbar_icon_size'] = self.ICON_SIZES[self.toolbar_button_size.currentIndex()] config['toolbar_icon_size'] = self.ICON_SIZES[self.toolbar_button_size.currentIndex()]
config['show_text_in_toolbar'] = bool(self.show_toolbar_text.isChecked()) config['show_text_in_toolbar'] = bool(self.show_toolbar_text.isChecked())
config['confirm_delete'] = bool(self.confirm_delete.isChecked()) config['confirm_delete'] = bool(self.confirm_delete.isChecked())
@ -133,6 +211,11 @@ class ConfigDialog(QDialog, Ui_Dialog):
config['save_to_disk_single_format'] = BOOK_EXTENSIONS[self.single_format.currentIndex()] config['save_to_disk_single_format'] = BOOK_EXTENSIONS[self.single_format.currentIndex()]
config['cover_flow_queue_length'] = self.cover_browse.value() config['cover_flow_queue_length'] = self.cover_browse.value()
prefs['language'] = str(self.language.itemData(self.language.currentIndex()).toString()) prefs['language'] = str(self.language.itemData(self.language.currentIndex()).toString())
config['autolaunch_server'] = self.auto_launch.isChecked()
sc = server_config()
sc.set('username', unicode(self.username.text()).strip())
sc.set('password', unicode(self.password.text()).strip())
sc.set('port', self.port.value())
of = str(self.output_format.currentText()) of = str(self.output_format.currentText())
fmts = [] fmts = []
for i in range(self.viewer.count()): for i in range(self.viewer.count()):

View File

@ -7,7 +7,7 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>709</width> <width>709</width>
<height>676</height> <height>840</height>
</rect> </rect>
</property> </property>
<property name="windowTitle" > <property name="windowTitle" >
@ -67,12 +67,6 @@
<property name="viewMode" > <property name="viewMode" >
<enum>QListView::IconMode</enum> <enum>QListView::IconMode</enum>
</property> </property>
<property name="uniformItemSizes" >
<bool>true</bool>
</property>
<property name="currentRow" >
<number>-1</number>
</property>
</widget> </widget>
</item> </item>
<item> <item>
@ -433,15 +427,61 @@
<property name="title" > <property name="title" >
<string>Select visible &amp;columns in library view</string> <string>Select visible &amp;columns in library view</string>
</property> </property>
<layout class="QGridLayout" name="_4" > <layout class="QVBoxLayout" name="verticalLayout_4" >
<item row="0" column="0" > <item>
<widget class="QListWidget" name="columns" > <layout class="QHBoxLayout" name="horizontalLayout_3" >
<property name="selectionMode" > <item>
<enum>QAbstractItemView::NoSelection</enum> <widget class="QListWidget" name="columns" >
</property> <property name="alternatingRowColors" >
</widget> <bool>true</bool>
</property>
<property name="selectionBehavior" >
<enum>QAbstractItemView::SelectRows</enum>
</property>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_3" >
<item>
<widget class="QToolButton" name="column_up" >
<property name="text" >
<string>...</string>
</property>
<property name="icon" >
<iconset resource="../images.qrc" >
<normaloff>:/images/arrow-up.svg</normaloff>:/images/arrow-up.svg</iconset>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_2" >
<property name="orientation" >
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0" >
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QToolButton" name="column_down" >
<property name="text" >
<string>...</string>
</property>
<property name="icon" >
<iconset resource="../images.qrc" >
<normaloff>:/images/arrow-down.svg</normaloff>:/images/arrow-down.svg</iconset>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item> </item>
<item row="1" column="0" > <item>
<widget class="QGroupBox" name="groupBox_3" > <widget class="QGroupBox" name="groupBox_3" >
<property name="title" > <property name="title" >
<string>Use internal &amp;viewer for the following formats:</string> <string>Use internal &amp;viewer for the following formats:</string>
@ -449,6 +489,9 @@
<layout class="QGridLayout" name="gridLayout_4" > <layout class="QGridLayout" name="gridLayout_4" >
<item row="0" column="0" > <item row="0" column="0" >
<widget class="QListWidget" name="viewer" > <widget class="QListWidget" name="viewer" >
<property name="alternatingRowColors" >
<bool>true</bool>
</property>
<property name="selectionMode" > <property name="selectionMode" >
<enum>QAbstractItemView::NoSelection</enum> <enum>QAbstractItemView::NoSelection</enum>
</property> </property>
@ -528,6 +571,142 @@
</item> </item>
</layout> </layout>
</widget> </widget>
<widget class="QWidget" name="page_4" >
<layout class="QVBoxLayout" name="verticalLayout_2" >
<item>
<widget class="QLabel" name="label_9" >
<property name="text" >
<string>calibre contains a network server that allows you to access your book collection using a browser from anywhere in the world. Any changes to the settings will only take effect after a server restart.</string>
</property>
<property name="wordWrap" >
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QGridLayout" name="gridLayout_5" >
<item row="0" column="0" >
<widget class="QLabel" name="label_10" >
<property name="text" >
<string>Server &amp;port:</string>
</property>
<property name="buddy" >
<cstring>port</cstring>
</property>
</widget>
</item>
<item row="0" column="1" >
<widget class="QSpinBox" name="port" >
<property name="minimum" >
<number>1025</number>
</property>
<property name="maximum" >
<number>16000</number>
</property>
<property name="value" >
<number>8080</number>
</property>
</widget>
</item>
<item row="1" column="0" >
<widget class="QLabel" name="label_11" >
<property name="text" >
<string>&amp;Username:</string>
</property>
<property name="buddy" >
<cstring>username</cstring>
</property>
</widget>
</item>
<item row="1" column="1" >
<widget class="QLineEdit" name="username" />
</item>
<item row="2" column="0" >
<widget class="QLabel" name="label_12" >
<property name="text" >
<string>&amp;Password:</string>
</property>
<property name="buddy" >
<cstring>password</cstring>
</property>
</widget>
</item>
<item row="2" column="1" >
<widget class="QLineEdit" name="password" >
<property name="toolTip" >
<string>If you leave the password blank, anyone will be able to access your book collection using the web interface.</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2" >
<item>
<widget class="QPushButton" name="start" >
<property name="text" >
<string>&amp;Start Server</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="stop" >
<property name="text" >
<string>St&amp;op Server</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer" >
<property name="orientation" >
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0" >
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="test" >
<property name="text" >
<string>&amp;Test Server</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QCheckBox" name="auto_launch" >
<property name="text" >
<string>Run server &amp;automatically on startup</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="view_logs" >
<property name="text" >
<string>View &amp;server logs</string>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer" >
<property name="orientation" >
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0" >
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</widget> </widget>
</item> </item>
</layout> </layout>

View File

@ -5,18 +5,18 @@ from datetime import timedelta, datetime
from operator import attrgetter from operator import attrgetter
from math import cos, sin, pi from math import cos, sin, pi
from itertools import repeat from PyQt4.QtGui import QTableView, QAbstractItemView, QColor, \
from PyQt4.QtGui import QTableView, QProgressDialog, QAbstractItemView, QColor, \
QItemDelegate, QPainterPath, QLinearGradient, QBrush, \ QItemDelegate, QPainterPath, QLinearGradient, QBrush, \
QPen, QStyle, QPainter, QLineEdit, \ QPen, QStyle, QPainter, QLineEdit, \
QPalette, QImage, QApplication QPalette, QImage, QApplication
from PyQt4.QtCore import QAbstractTableModel, QVariant, Qt, QString, \ from PyQt4.QtCore import QAbstractTableModel, QVariant, Qt, QString, \
QCoreApplication, SIGNAL, QObject, QSize, QModelIndex SIGNAL, QObject, QSize, QModelIndex
from calibre import strftime from calibre import strftime
from calibre.ptempfile import PersistentTemporaryFile from calibre.ptempfile import PersistentTemporaryFile
from calibre.library.database import LibraryDatabase, text_to_tokens from calibre.library.database2 import FIELD_MAP
from calibre.gui2 import NONE, TableView, qstring_to_unicode, config from calibre.gui2 import NONE, TableView, qstring_to_unicode, config
from calibre.utils.search_query_parser import SearchQueryParser
class LibraryDelegate(QItemDelegate): class LibraryDelegate(QItemDelegate):
COLOR = QColor("blue") COLOR = QColor("blue")
@ -85,6 +85,18 @@ class BooksModel(QAbstractTableModel):
[1000,900,500,400,100,90,50,40,10,9,5,4,1], [1000,900,500,400,100,90,50,40,10,9,5,4,1],
["M","CM","D","CD","C","XC","L","XL","X","IX","V","IV","I"] ["M","CM","D","CD","C","XC","L","XL","X","IX","V","IV","I"]
) )
headers = {
'title' : _("Title"),
'authors' : _("Author(s)"),
'size' : _("Size (MB)"),
'timestamp' : _("Date"),
'rating' : _('Rating'),
'publisher' : _("Publisher"),
'tags' : _("Tags"),
'series' : _("Series"),
}
@classmethod @classmethod
def roman(cls, num): def roman(cls, num):
if num <= 0 or num >= 4000 or int(num) != num: if num <= 0 or num >= 4000 or int(num) != num:
@ -99,10 +111,10 @@ class BooksModel(QAbstractTableModel):
def __init__(self, parent=None, buffer=40): def __init__(self, parent=None, buffer=40):
QAbstractTableModel.__init__(self, parent) QAbstractTableModel.__init__(self, parent)
self.db = None self.db = None
self.cols = ['title', 'authors', 'size', 'date', 'rating', 'publisher', 'tags', 'series'] self.column_map = config['column_map']
self.editable_cols = [0, 1, 4, 5, 6, 7] self.editable_cols = ['title', 'authors', 'rating', 'publisher', 'tags', 'series']
self.default_image = QImage(':/images/book.svg') self.default_image = QImage(':/images/book.svg')
self.sorted_on = (3, Qt.AscendingOrder) self.sorted_on = ('timestamp', Qt.AscendingOrder)
self.last_search = '' # The last search performed on this model self.last_search = '' # The last search performed on this model
self.read_config() self.read_config()
self.buffer_size = buffer self.buffer_size = buffer
@ -114,14 +126,15 @@ class BooksModel(QAbstractTableModel):
def read_config(self): def read_config(self):
self.use_roman_numbers = config['use_roman_numerals_for_series_number'] self.use_roman_numbers = config['use_roman_numerals_for_series_number']
cols = config['column_map']
if cols != self.column_map:
self.column_map = cols
self.reset()
def set_database(self, db): def set_database(self, db):
if isinstance(db, (QString, basestring)):
if isinstance(db, QString):
db = qstring_to_unicode(db)
db = LibraryDatabase(os.path.expanduser(db))
self.db = db self.db = db
self.build_data_convertors()
def refresh_ids(self, ids, current_row=-1): def refresh_ids(self, ids, current_row=-1):
rows = self.db.refresh_ids(ids) rows = self.db.refresh_ids(ids)
@ -151,7 +164,8 @@ class BooksModel(QAbstractTableModel):
def save_to_disk(self, rows, path, single_dir=False, single_format=None): def save_to_disk(self, rows, path, single_dir=False, single_format=None):
rows = [row.row() for row in rows] rows = [row.row() for row in rows]
if single_format is None: if single_format is None:
return self.db.export_to_dir(path, rows, self.sorted_on[0] == 1, single_dir=single_dir) return self.db.export_to_dir(path, rows, self.sorted_on[0] == 'authors',
single_dir=single_dir)
else: else:
return self.db.export_single_format_to_dir(path, rows, single_format) return self.db.export_single_format_to_dir(path, rows, single_format)
@ -166,9 +180,6 @@ class BooksModel(QAbstractTableModel):
self.clear_caches() self.clear_caches()
self.reset() self.reset()
def search_tokens(self, text):
return text_to_tokens(text)
def books_added(self, num): def books_added(self, num):
if num > 0: if num > 0:
self.beginInsertRows(QModelIndex(), 0, num-1) self.beginInsertRows(QModelIndex(), 0, num-1)
@ -185,29 +196,27 @@ class BooksModel(QAbstractTableModel):
if not self.db: if not self.db:
return return
ascending = order == Qt.AscendingOrder ascending = order == Qt.AscendingOrder
self.db.sort(self.cols[col], ascending) self.db.sort(self.column_map[col], ascending)
self.research() self.research(reset=False)
if reset: if reset:
self.clear_caches() self.clear_caches()
self.reset() self.reset()
self.sorted_on = (col, order) self.sorted_on = (self.column_map[col], order)
def resort(self, reset=True): def resort(self, reset=True):
self.sort(*self.sorted_on, **dict(reset=reset)) try:
col = self.column_map.index(self.sorted_on[0])
except:
col = 0
self.sort(col, self.sorted_on[1], reset=reset)
def research(self, reset=True): def research(self, reset=True):
self.search(self.last_search, False, reset=reset) self.search(self.last_search, False, reset=reset)
def database_needs_migration(self):
path = os.path.expanduser('~/library.db')
return self.db.is_empty() and \
os.path.exists(path) and\
LibraryDatabase.sizeof_old_database(path) > 0
def columnCount(self, parent): def columnCount(self, parent):
if parent and parent.isValid(): if parent and parent.isValid():
return 0 return 0
return len(self.cols) return len(self.column_map)
def rowCount(self, parent): def rowCount(self, parent):
if parent and parent.isValid(): if parent and parent.isValid():
@ -374,65 +383,77 @@ class BooksModel(QAbstractTableModel):
img = self.default_image img = self.default_image
return img return img
def build_data_convertors(self):
tidx = FIELD_MAP['title']
aidx = FIELD_MAP['authors']
sidx = FIELD_MAP['size']
ridx = FIELD_MAP['rating']
pidx = FIELD_MAP['publisher']
tmdx = FIELD_MAP['timestamp']
srdx = FIELD_MAP['series']
tgdx = FIELD_MAP['tags']
siix = FIELD_MAP['series_index']
def authors(r):
au = self.db.data[r][aidx]
if au:
au = [a.strip().replace('|', ',') for a in au.split(',')]
return '\n'.join(au)
def timestamp(r):
dt = self.db.data[r][tmdx]
if dt:
dt = dt - timedelta(seconds=time.timezone) + timedelta(hours=time.daylight)
return strftime(BooksView.TIME_FMT, dt.timetuple())
def rating(r):
r = self.db.data[r][ridx]
r = r/2 if r else 0
return r
def publisher(r):
pub = self.db.data[r][pidx]
if pub:
return pub
def tags(r):
tags = self.db.data[r][tgdx]
if tags:
return ', '.join(tags.split(','))
def series(r):
series = self.db.data[r][srdx]
if series:
return series + ' [%d]'%self.db.data[r][siix]
self.dc = {
'title' : lambda r : self.db.data[r][tidx],
'authors' : authors,
'size' : lambda r : self.db.data[r][sidx],
'timestamp': timestamp,
'rating' : rating,
'publisher': publisher,
'tags' : tags,
'series' : series,
}
def data(self, index, role): def data(self, index, role):
if role == Qt.DisplayRole or role == Qt.EditRole: if role in (Qt.DisplayRole, Qt.EditRole):
row, col = index.row(), index.column() ans = self.dc[self.column_map[index.column()]](index.row())
if col == 0: return NONE if ans is None else QVariant(ans)
text = self.db.title(row) elif role == Qt.TextAlignmentRole and self.column_map[index.column()] in ('size', 'timestamp', 'rating'):
if text: return QVariant(Qt.AlignCenter | Qt.AlignVCenter)
return QVariant(text) #elif role == Qt.ToolTipRole and index.isValid():
elif col == 1: # if self.column_map[index.column()] in self.editable_cols:
au = self.db.authors(row) # return QVariant(_("Double click to <b>edit</b> me<br><br>"))
if au:
au = [a.strip().replace('|', ',') for a in au.split(',')]
return QVariant("\n".join(au))
elif col == 2:
size = self.db.max_size(row)
if size:
return QVariant(BooksView.human_readable(size))
elif col == 3:
dt = self.db.timestamp(row)
if dt:
dt = dt - timedelta(seconds=time.timezone) + timedelta(hours=time.daylight)
return QVariant(strftime(BooksView.TIME_FMT, dt.timetuple()))
elif col == 4:
r = self.db.rating(row)
r = r/2 if r else 0
return QVariant(r)
elif col == 5:
pub = self.db.publisher(row)
if pub:
return QVariant(pub)
elif col == 6:
tags = self.db.tags(row)
if tags:
return QVariant(', '.join(tags.split(',')))
elif col == 7:
series = self.db.series(row)
if series:
return QVariant(series + ' [%d]'%self.db.series_index(row))
return NONE
elif role == Qt.TextAlignmentRole and index.column() in [2, 3, 4]:
return QVariant(Qt.AlignRight | Qt.AlignVCenter)
elif role == Qt.ToolTipRole and index.isValid():
if index.column() in self.editable_cols:
return QVariant(_("Double click to <b>edit</b> me<br><br>"))
return NONE return NONE
def headerData(self, section, orientation, role): def headerData(self, section, orientation, role):
if role != Qt.DisplayRole: if role != Qt.DisplayRole:
return NONE return NONE
text = ""
if orientation == Qt.Horizontal: if orientation == Qt.Horizontal:
if section == 0: text = _("Title") return QVariant(self.headers[self.column_map[section]])
elif section == 1: text = _("Author(s)")
elif section == 2: text = _("Size (MB)")
elif section == 3: text = _("Date")
elif section == 4: text = _("Rating")
elif section == 5: text = _("Publisher")
elif section == 6: text = _("Tags")
elif section == 7: text = _("Series")
return QVariant(text)
else: else:
return QVariant(section+1) return QVariant(section+1)
@ -447,14 +468,15 @@ class BooksModel(QAbstractTableModel):
done = False done = False
if role == Qt.EditRole: if role == Qt.EditRole:
row, col = index.row(), index.column() row, col = index.row(), index.column()
if col not in self.editable_cols: column = self.column_map[col]
if column not in self.editable_cols:
return False return False
val = unicode(value.toString().toUtf8(), 'utf-8').strip() if col != 4 else \ val = unicode(value.toString().toUtf8(), 'utf-8').strip() if column != 'rating' else \
int(value.toInt()[0]) int(value.toInt()[0])
if col == 4: if col == 'rating':
val = 0 if val < 0 else 5 if val > 5 else val val = 0 if val < 0 else 5 if val > 5 else val
val *= 2 val *= 2
if col == 7: if col == 'series':
pat = re.compile(r'\[(\d+)\]') pat = re.compile(r'\[(\d+)\]')
match = pat.search(val) match = pat.search(val)
id = self.db.id(row) id = self.db.id(row)
@ -465,12 +487,11 @@ class BooksModel(QAbstractTableModel):
if val: if val:
self.db.set_series(id, val) self.db.set_series(id, val)
else: else:
column = self.cols[col]
self.db.set(row, column, val) self.db.set(row, column, val)
self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), \ self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), \
index, index) index, index)
if col == self.sorted_on[0]: if column == self.sorted_on[0]:
self.sort(col, self.sorted_on[1]) self.resort()
done = True done = True
return done return done
@ -495,8 +516,7 @@ class BooksView(TableView):
self.setModel(self._model) self.setModel(self._model)
self.setSelectionBehavior(QAbstractItemView.SelectRows) self.setSelectionBehavior(QAbstractItemView.SelectRows)
self.setSortingEnabled(True) self.setSortingEnabled(True)
if self.__class__.__name__ == 'BooksView': # Subclasses may not have rating as col 4 self.columns_sorted()
self.setItemDelegateForColumn(4, LibraryDelegate(self))
QObject.connect(self.selectionModel(), SIGNAL('currentRowChanged(QModelIndex, QModelIndex)'), QObject.connect(self.selectionModel(), SIGNAL('currentRowChanged(QModelIndex, QModelIndex)'),
self._model.current_changed) self._model.current_changed)
# Adding and removing rows should resize rows to contents # Adding and removing rows should resize rows to contents
@ -505,7 +525,23 @@ class BooksView(TableView):
# Resetting the model should resize rows (model is reset after search and sort operations) # Resetting the model should resize rows (model is reset after search and sort operations)
QObject.connect(self.model(), SIGNAL('modelReset()'), self.resizeRowsToContents) QObject.connect(self.model(), SIGNAL('modelReset()'), self.resizeRowsToContents)
self.set_visible_columns() self.set_visible_columns()
def columns_sorted(self):
if self.__class__.__name__ == 'BooksView':
try:
idx = self._model.column_map.index('rating')
self.setItemDelegateForColumn(idx, LibraryDelegate(self))
except ValueError:
pass
def sortByColumn(self, colname, order):
try:
idx = self._model.column_map.index(colname)
except ValueError:
idx = 0
TableView.sortByColumn(self, idx, order)
@classmethod @classmethod
def paths_from_event(cls, event): def paths_from_event(cls, event):
''' '''
@ -541,25 +577,6 @@ class BooksView(TableView):
def close(self): def close(self):
self._model.close() self._model.close()
def migrate_database(self):
if self.model().database_needs_migration():
print 'Migrating database from pre 0.4.0 version'
path = os.path.abspath(os.path.expanduser('~/library.db'))
progress = QProgressDialog('Upgrading database from pre 0.4.0 version.<br>'+\
'The new database is stored in the file <b>'+self._model.db.dbpath,
QString(), 0, LibraryDatabase.sizeof_old_database(path),
self)
progress.setModal(True)
progress.setValue(0)
app = QCoreApplication.instance()
def meter(count):
progress.setValue(count)
app.processEvents()
progress.setWindowTitle('Upgrading database')
progress.show()
LibraryDatabase.import_old_database(path, self._model.db.conn, meter)
def connect_to_search_box(self, sb): def connect_to_search_box(self, sb):
QObject.connect(sb, SIGNAL('search(PyQt_PyObject, PyQt_PyObject)'), QObject.connect(sb, SIGNAL('search(PyQt_PyObject, PyQt_PyObject)'),
self._model.search) self._model.search)
@ -582,6 +599,40 @@ class DeviceBooksView(BooksView):
def connect_dirtied_signal(self, slot): def connect_dirtied_signal(self, slot):
QObject.connect(self._model, SIGNAL('booklist_dirtied()'), slot) QObject.connect(self._model, SIGNAL('booklist_dirtied()'), slot)
def sortByColumn(self, col, order):
TableView.sortByColumn(self, col, order)
class OnDeviceSearch(SearchQueryParser):
def __init__(self, model):
SearchQueryParser.__init__(self)
self.model = model
def universal_set(self):
return set(range(0, len(self.model.db)))
def get_matches(self, location, query):
location = location.lower().strip()
query = query.lower().strip()
if location not in ('title', 'authors', 'tags', 'all'):
return set([])
matches = set([])
locations = ['title', 'authors', 'tags'] if location == 'all' else [location]
q = {
'title' : lambda x : getattr(x, 'title').lower(),
'authors': lambda x: getattr(x, 'authors').lower(),
'tags':lambda x: ','.join(getattr(x, 'tags')).lower()
}
for i, v in enumerate(locations):
locations[i] = q[v]
for i, r in enumerate(self.model.db):
for loc in locations:
if query in loc(r):
matches.add(i)
break
return matches
class DeviceBooksModel(BooksModel): class DeviceBooksModel(BooksModel):
@ -592,6 +643,7 @@ class DeviceBooksModel(BooksModel):
self.sorted_map = [] self.sorted_map = []
self.unknown = str(self.trUtf8('Unknown')) self.unknown = str(self.trUtf8('Unknown'))
self.marked_for_deletion = {} self.marked_for_deletion = {}
self.search_engine = OnDeviceSearch(self)
def mark_for_deletion(self, job, rows): def mark_for_deletion(self, job, rows):
@ -632,34 +684,22 @@ class DeviceBooksModel(BooksModel):
def search(self, text, refinement, reset=True): def search(self, text, refinement, reset=True):
tokens, OR = self.search_tokens(text) if not text:
base = self.map if refinement else self.sorted_map self.map = list(range(len(self.db)))
result = [] else:
for i in base: matches = self.search_engine.parse(text)
q = ['', self.db[i].title, self.db[i].authors, '', ', '.join(self.db[i].tags)] + list(repeat('', 10)) self.map = []
if OR: for i in range(len(self.db)):
add = False if i in matches:
for token in tokens: self.map.append(i)
if token.match(q): self.resort(reset=False)
add = True
break
if add:
result.append(i)
else:
add = True
for token in tokens:
if not token.match(q):
add = False
break
if add:
result.append(i)
self.map = result
if reset: if reset:
self.reset() self.reset()
self.last_search = text self.last_search = text
def resort(self, reset):
self.sort(self.sorted_on[0], self.sorted_on[1], reset=reset)
def sort(self, col, order, reset=True): def sort(self, col, order, reset=True):
descending = order != Qt.AscendingOrder descending = order != Qt.AscendingOrder
def strcmp(attr): def strcmp(attr):

View File

@ -87,7 +87,8 @@ class Main(MainWindow, Ui_MainWindow):
self.tb_wrapper = textwrap.TextWrapper(width=40) self.tb_wrapper = textwrap.TextWrapper(width=40)
self.device_connected = False self.device_connected = False
self.viewers = collections.deque() self.viewers = collections.deque()
self.content_server = None
####################### Location View ######################## ####################### Location View ########################
QObject.connect(self.location_view, SIGNAL('location_selected(PyQt_PyObject)'), QObject.connect(self.location_view, SIGNAL('location_selected(PyQt_PyObject)'),
self.location_selected) self.location_selected)
@ -239,7 +240,7 @@ class Main(MainWindow, Ui_MainWindow):
os.remove(self.olddb.dbpath) os.remove(self.olddb.dbpath)
self.olddb = None self.olddb = None
prefs['library_path'] = self.library_path prefs['library_path'] = self.library_path
self.library_view.sortByColumn(*dynamic.get('sort_column', (3, Qt.DescendingOrder))) self.library_view.sortByColumn(*dynamic.get('sort_column', ('timestamp', Qt.DescendingOrder)))
if not self.library_view.restore_column_widths(): if not self.library_view.restore_column_widths():
self.library_view.resizeColumnsToContents() self.library_view.resizeColumnsToContents()
self.library_view.resizeRowsToContents() self.library_view.resizeRowsToContents()
@ -282,6 +283,12 @@ class Main(MainWindow, Ui_MainWindow):
self.device_manager.start() self.device_manager.start()
self.news_menu.set_custom_feeds(self.library_view.model().db.get_feeds()) self.news_menu.set_custom_feeds(self.library_view.model().db.get_feeds())
if config['autolaunch_server']:
from calibre.library.server import start_threaded_server
from calibre.library import server_config
self.server = start_threaded_server(db, server_config().parse())
def toggle_cover_flow(self, show): def toggle_cover_flow(self, show):
if show: if show:
@ -1004,13 +1011,10 @@ class Main(MainWindow, Ui_MainWindow):
d = error_dialog(self, _('Cannot configure'), _('Cannot configure while there are running jobs.')) d = error_dialog(self, _('Cannot configure'), _('Cannot configure while there are running jobs.'))
d.exec_() d.exec_()
return return
columns = [(self.library_view.isColumnHidden(i), \ d = ConfigDialog(self, self.library_view.model().db, self.content_server)
self.library_view.model().headerData(i, Qt.Horizontal, Qt.DisplayRole).toString())\
for i in range(self.library_view.model().columnCount(None))]
d = ConfigDialog(self, self.library_view.model().db, columns)
d.exec_() d.exec_()
self.content_server = d.server
if d.result() == d.Accepted: if d.result() == d.Accepted:
self.library_view.set_visible_columns(d.final_columns)
self.tool_bar.setIconSize(config['toolbar_icon_size']) self.tool_bar.setIconSize(config['toolbar_icon_size'])
self.tool_bar.setToolButtonStyle(Qt.ToolButtonTextUnderIcon if config['show_text_in_toolbar'] else Qt.ToolButtonIconOnly) self.tool_bar.setToolButtonStyle(Qt.ToolButtonTextUnderIcon if config['show_text_in_toolbar'] else Qt.ToolButtonIconOnly)
self.save_menu.actions()[2].setText(_('Save only %s format to disk')%config.get('save_to_disk_single_format').upper()) self.save_menu.actions()[2].setText(_('Save only %s format to disk')%config.get('save_to_disk_single_format').upper())
@ -1055,6 +1059,7 @@ class Main(MainWindow, Ui_MainWindow):
if hasattr(d, 'directories'): if hasattr(d, 'directories'):
set_sidebar_directories(d.directories) set_sidebar_directories(d.directories)
self.library_view.model().read_config() self.library_view.model().read_config()
self.library_view.columns_sorted()
############################################################################ ############################################################################
@ -1215,8 +1220,13 @@ in which you want to store your books files. Any existing books will be automati
self.device_manager.keep_going = False self.device_manager.keep_going = False
self.cover_cache.stop() self.cover_cache.stop()
self.hide() self.hide()
time.sleep(2)
self.cover_cache.terminate() self.cover_cache.terminate()
try:
if self.server is not None:
self.server.exit()
except:
pass
time.sleep(2)
e.accept() e.accept()
def update_found(self, version): def update_found(self, version):