Raising the pylint score of libprs500/gui/*.py

This commit is contained in:
Kovid Goyal 2006-12-20 02:08:57 +00:00
parent 97fe142281
commit 18111d2d50
6 changed files with 1576 additions and 1360 deletions

View File

@ -12,6 +12,7 @@
## You should have received a copy of the GNU General Public License along ## You should have received a copy of the GNU General Public License along
## with this program; if not, write to the Free Software Foundation, Inc., ## with this program; if not, write to the Free Software Foundation, Inc.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
""" The GUI to libprs500. Also has ebook library management features. """
__docformat__ = "epytext" __docformat__ = "epytext"
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>" __author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
APP_TITLE = "libprs500" APP_TITLE = "libprs500"
@ -33,7 +34,7 @@ def installErrorHandler(dialog):
error_dialog.setModal(True) error_dialog.setModal(True)
def Warning(msg, e): def _Warning(msg, e):
print >> sys.stderr, msg print >> sys.stderr, msg
if e: traceback.print_exc(e) if e: traceback.print_exc(e)
@ -53,4 +54,6 @@ def import_ui(name):
exec code_string.getvalue() exec code_string.getvalue()
return locals()[winfo["uiclass"]] return locals()[winfo["uiclass"]]
from libprs500.gui.widgets import LibraryBooksView, DeviceBooksView, CoverDisplay, DeviceView # Needed in globals() for import_ui # Needed in globals() for import_ui
from libprs500.gui.widgets import LibraryBooksView, \
DeviceBooksView, CoverDisplay, DeviceView

View File

@ -22,57 +22,72 @@ from cStringIO import StringIO as cStringIO
class LibraryDatabase(object): class LibraryDatabase(object):
BOOKS_SQL = """ BOOKS_SQL = """
create table if not exists books_meta(id INTEGER PRIMARY KEY, title TEXT, authors TEXT, publisher TEXT, size INTEGER, tags TEXT, create table if not exists books_meta(id INTEGER PRIMARY KEY, title TEXT,
cover BLOB, date DATE DEFAULT CURRENT_TIMESTAMP, comments TEXT, rating INTEGER); authors TEXT, publisher TEXT, size INTEGER, tags TEXT,
cover BLOB, date DATE DEFAULT CURRENT_TIMESTAMP,
comments TEXT, rating INTEGER);
create table if not exists books_data(id INTEGER, extension TEXT, data BLOB); create table if not exists books_data(id INTEGER, extension TEXT, data BLOB);
""" """
def __init__(self, dbpath): def __init__(self, dbpath):
self.con = sqlite.connect(dbpath) self.con = sqlite.connect(dbpath)
self.con.row_factory = sqlite.Row # Allow case insensitive field access by name # Allow case insensitive field access by name
self.con.row_factory = sqlite.Row
self.con.executescript(LibraryDatabase.BOOKS_SQL) self.con.executescript(LibraryDatabase.BOOKS_SQL)
def get_cover(self, id): def get_cover(self, _id):
raw = self.con.execute("select cover from books_meta where id=?", (id,)).next()["cover"] raw = self.con.execute("select cover from books_meta where id=?", (_id,))\
.next()["cover"]
return decompress(str(raw)) if raw else None return decompress(str(raw)) if raw else None
def get_extensions(self, id): def get_extensions(self, _id):
exts = [] exts = []
cur = self.con.execute("select extension from books_data where id=?", (id,)) cur = self.con.execute("select extension from books_data where id=?", (_id,))
for row in cur: for row in cur:
exts.append(row["extension"]) exts.append(row["extension"])
return exts return exts
def add_book(self, path): def add_book(self, path):
file = os.path.abspath(path) _file = os.path.abspath(path)
title, author, publisher, size, cover = os.path.basename(file), None, None, os.stat(file)[ST_SIZE], None title, author, publisher, size, cover = os.path.basename(_file), \
None, None, os.stat(_file)[ST_SIZE], None
ext = title[title.rfind(".")+1:].lower() if title.find(".") > -1 else None ext = title[title.rfind(".")+1:].lower() if title.find(".") > -1 else None
if ext == "lrf": if ext == "lrf":
lrf = LRFMetaFile(open(file, "r+b")) lrf = LRFMetaFile(open(_file, "r+b"))
title, author, cover, publisher = lrf.title, lrf.author.strip(), lrf.thumbnail, lrf.publisher.strip() title, author, cover, publisher = lrf.title, lrf.author.strip(), \
if "unknown" in publisher.lower(): publisher = None lrf.thumbnail, lrf.publisher.strip()
if "unknown" in author.lower(): author = None if "unknown" in publisher.lower():
file = compress(open(file).read()) publisher = None
if cover: cover = sqlite.Binary(compress(cover)) if "unknown" in author.lower():
self.con.execute("insert into books_meta (title, authors, publisher, size, tags, cover, comments, rating) values (?,?,?,?,?,?,?,?)", (title, author, publisher, size, None, cover, None, None)) author = None
id = self.con.execute("select max(id) from books_meta").next()[0] _file = compress(open(_file).read())
self.con.execute("insert into books_data values (?,?,?)", (id, ext, sqlite.Binary(file))) if cover:
cover = sqlite.Binary(compress(cover))
self.con.execute("insert into books_meta (title, authors, publisher, "+\
"size, tags, cover, comments, rating) values "+\
"(?,?,?,?,?,?,?,?)", \
(title, author, publisher, size, None, cover, None, None))
_id = self.con.execute("select max(id) from books_meta").next()[0]
self.con.execute("insert into books_data values (?,?,?)", \
(_id, ext, sqlite.Binary(_file)))
self.con.commit() self.con.commit()
return id return _id
def get_row_by_id(self, id, columns): def get_row_by_id(self, _id, columns):
""" @param columns: list of column names """ """ @param columns: list of column names """
cols = ",".join([ c for c in columns]) cols = ",".join([ c for c in columns])
cur = self.con.execute("select " + cols + " from books_meta where id=?", (id,)) cur = self.con.execute("select " + cols + " from books_meta where id=?"\
, (_id,))
row, r = cur.next(), {} row, r = cur.next(), {}
for c in columns: r[c] = row[c] for c in columns:
r[c] = row[c]
return r return r
def commit(self): self.con.commit() def commit(self): self.con.commit()
def delete_by_id(self, id): def delete_by_id(self, _id):
self.con.execute("delete from books_meta where id=?", (id,)) self.con.execute("delete from books_meta where id=?", (_id,))
self.con.execute("delete from books_data where id=?", (id,)) self.con.execute("delete from books_data where id=?", (_id,))
def get_table(self, columns): def get_table(self, columns):
cols = ",".join([ c for c in columns]) cols = ",".join([ c for c in columns])
@ -80,22 +95,29 @@ class LibraryDatabase(object):
rows = [] rows = []
for row in cur: for row in cur:
r = {} r = {}
for c in columns: r[c] = row[c] for c in columns:
r[c] = row[c]
rows.append(r) rows.append(r)
return rows return rows
def get_format(self, id, ext): def get_format(self, _id, ext):
""" """
Return format C{ext} corresponding to the logical book C{id} or None if the format is unavailable. Return format C{ext} corresponding to the logical book C{id} or
Format is returned as a string of binary data suitable for C{ file.write} operations. None if the format is unavailable.
Format is returned as a string of binary data suitable for
C{ file.write} operations.
""" """
ext = ext.lower() ext = ext.lower()
cur = self.con.execute("select data from books_data where id=? and extension=?",(id, ext)) cur = self.con.execute("select data from books_data where id=? and "+\
try: data = cur.next() "extension=?",(_id, ext))
except: pass try:
else: return decompress(str(data["data"])) data = cur.next()
except:
pass
else:
return decompress(str(data["data"]))
def add_format(self, id, ext, data): def add_format(self, _id, ext, data):
""" """
If data for format ext already exists, it is replaced If data for format ext already exists, it is replaced
@type ext: string or None @type ext: string or None
@ -107,57 +129,78 @@ class LibraryDatabase(object):
except AttributeError: pass except AttributeError: pass
if ext: ext = ext.strip().lower() if ext: ext = ext.strip().lower()
data = sqlite.Binary(compress(data)) data = sqlite.Binary(compress(data))
cur = self.con.execute("select extension from books_data where id=? and extension=?", (id, ext)) cur = self.con.execute("select extension from books_data where id=? "+\
"and extension=?", (_id, ext))
present = True present = True
try: cur.next() try: cur.next()
except: present = False except: present = False
if present: if present:
self.con.execute("update books_data set data=? where id=? and extension=?", (data, id, ext)) self.con.execute("update books_data set data=? where id=? "+\
"and extension=?", (data, _id, ext))
else: else:
self.con.execute("insert into books_data (id, extension, data) values (?, ?, ?)", (id, ext, data)) self.con.execute("insert into books_data (id, extension, data) "+\
"values (?, ?, ?)", (_id, ext, data))
self.con.commit() self.con.commit()
def get_meta_data(self, id): def get_meta_data(self, _id):
try: row = self.con.execute("select * from books_meta where id=?", (id,)).next() try:
except StopIteration: return None row = self.con.execute("select * from books_meta where id=?", \
(_id,)).next()
except StopIteration:
return None
data = {} data = {}
for field in ("id", "title", "authors", "publisher", "size", "tags", "cover", "date"): for field in ("id", "title", "authors", "publisher", "size", "tags",
"cover", "date"):
data[field] = row[field] data[field] = row[field]
return data return data
def set_metadata(self, id, title=None, authors=None, rating=None, publisher=None, tags=None, cover=None, comments=None): def set_metadata(self, _id, title=None, authors=None, rating=None, \
if authors and not len(authors): authors = None publisher=None, tags=None, cover=None, \
if publisher and not len(publisher): publisher = None comments=None):
if tags and not len(tags): tags = None if authors and not len(authors):
if comments and not len(comments): comments = None authors = None
if cover: cover = sqlite.Binary(compress(cover)) if publisher and not len(publisher):
self.con.execute('update books_meta set title=?, authors=?, publisher=?, tags=?, cover=?, comments=?, rating=? where id=?', (title, authors, publisher, tags, cover, comments, rating, id)) publisher = None
if tags and not len(tags):
tags = None
if comments and not len(comments):
comments = None
if cover:
cover = sqlite.Binary(compress(cover))
self.con.execute('update books_meta set title=?, authors=?, '+\
'publisher=?, tags=?, cover=?, comments=?, rating=? '+\
'where id=?', \
(title, authors, publisher, tags, cover, comments, \
rating, _id))
self.con.commit() self.con.commit()
def set_metadata_item(self, id, col, val): def set_metadata_item(self, _id, col, val):
self.con.execute('update books_meta set '+col+'=? where id=?',(val, id)) self.con.execute('update books_meta set '+col+'=? where id=?', \
(val, _id))
if col in ["authors", "title"]: if col in ["authors", "title"]:
lrf = self.get_format(id, "lrf") lrf = self.get_format(_id, "lrf")
if lrf: if lrf:
c = cStringIO() c = cStringIO()
c.write(lrf) c.write(lrf)
lrf = LRFMetaFile(c) lrf = LRFMetaFile(c)
if col == "authors": lrf.authors = val if col == "authors":
lrf.authors = val
else: lrf.title = val else: lrf.title = val
self.add_format(id, "lrf", c.getvalue()) self.add_format(_id, "lrf", c.getvalue())
self.con.commit() self.con.commit()
def update_cover(self, id, cover): def update_cover(self, _id, cover):
data = None data = None
if cover: data = sqlite.Binary(compress(cover)) if cover:
self.con.execute('update books_meta set cover=? where id=?', (data, id)) data = sqlite.Binary(compress(cover))
lrf = self.get_format(id, "lrf") self.con.execute('update books_meta set cover=? where id=?', (data, _id))
lrf = self.get_format(_id, "lrf")
if lrf: if lrf:
c = cStringIO() c = cStringIO()
c.write(lrf) c.write(lrf)
lrf = LRFMetaFile(c) lrf = LRFMetaFile(c)
lrf.thumbnail = cover lrf.thumbnail = cover
self.add_format(id, "lrf", c.getvalue()) self.add_format(_id, "lrf", c.getvalue())
self.commit() self.commit()

View File

@ -11,40 +11,61 @@
## ##
## You should have received a copy of the GNU General Public License along ## You should have received a copy of the GNU General Public License along
## with this program; if not, write to the Free Software Foundation, Inc., ## with this program; if not, write to the Free Software Foundation, Inc.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.Warning
from PyQt4.QtCore import Qt, SIGNAL, QObject, QCoreApplication, QSettings, QVariant, QSize, QEventLoop, QString, QBuffer, QIODevice, QModelIndex """ Create and launch the GUI """
from PyQt4.QtGui import QPixmap, QAbstractItemView, QErrorMessage, QMessageBox, QFileDialog, QIcon import sys
from PyQt4.Qt import qInstallMsgHandler, qDebug, qFatal, qWarning, qCritical import re
from PyQt4 import uic import os
import traceback
import tempfile
from PyQt4.QtCore import Qt, SIGNAL, QObject, QCoreApplication, \
QSettings, QVariant, QSize, QEventLoop, QString, \
QBuffer, QIODevice, QModelIndex
from PyQt4.QtGui import QPixmap, QErrorMessage, \
QMessageBox, QFileDialog, QIcon, QDialog
from PyQt4.Qt import qDebug, qFatal, qWarning, qCritical
from libprs500.communicate import PRS500Device as device from libprs500.communicate import PRS500Device as device
from libprs500.books import fix_ids from libprs500.books import fix_ids
from libprs500.errors import * from libprs500.errors import *
from libprs500.lrf.meta import LRFMetaFile, LRFException from libprs500.gui import import_ui, installErrorHandler, Error, _Warning, \
from libprs500.gui import import_ui, installErrorHandler, Error, Warning, extension, APP_TITLE extension, APP_TITLE
from libprs500.gui.widgets import LibraryBooksModel, DeviceBooksModel, DeviceModel, TableView from libprs500.gui.widgets import LibraryBooksModel, DeviceBooksModel, \
DeviceModel
from database import LibraryDatabase from database import LibraryDatabase
from editbook import EditBookDialog from editbook import EditBookDialog
import sys, re, os, traceback, tempfile
DEFAULT_BOOK_COVER = None DEFAULT_BOOK_COVER = None
LIBRARY_BOOK_TEMPLATE = QString("<table><tr><td><b>Formats:</b> %1 </td><td><b>Tags:</b> %2</td></tr><tr><td><b>Comments:</b>%3</td></tr></table>") LIBRARY_BOOK_TEMPLATE = QString("<table><tr><td><b>Formats:</b> %1 \
DEVICE_BOOK_TEMPLATE = QString("<table><tr><td><b>Title: </b>%1</td><td><b>&nbsp;Size:</b> %2</td></tr><tr><td><b>Author: </b>%3</td><td><b>&nbsp;Type: </b>%4</td></tr></table>") </td><td><b>Tags:</b> %2</td></tr> \
<tr><td><b>Comments:</b>%3</td></tr></table>")
DEVICE_BOOK_TEMPLATE = QString("<table><tr><td><b>Title: </b>%1</td><td> \
<b>&nbsp;Size:</b> %2</td></tr>\
<tr><td><b>Author: </b>%3</td>\
<td><b>&nbsp;Type: </b>%4</td></tr></table>")
Ui_MainWindow = import_ui("main.ui") Ui_MainWindow = import_ui("main.ui")
class MainWindow(QObject, Ui_MainWindow): class Main(QObject, Ui_MainWindow):
""" Create GUI """
def show_device(self, yes): def show_device(self, yes):
""" If C{yes} show the items on the device otherwise show the items in the library """ """
self.device_view.selectionModel().reset(), self.library_view.selectionModel().reset() If C{yes} show the items on the device otherwise show the items
self.book_cover.hide(), self.book_info.hide() in the library
"""
self.device_view.selectionModel().reset()
self.library_view.selectionModel().reset()
self.book_cover.hide()
self.book_info.hide()
if yes: if yes:
self.device_view.show(), self.library_view.hide() self.device_view.show()
self.library_view.hide()
self.book_cover.setAcceptDrops(False) self.book_cover.setAcceptDrops(False)
self.current_view = self.device_view self.current_view = self.device_view
else: else:
self.device_view.hide(), self.library_view.show() self.device_view.hide()
self.library_view.show()
self.book_cover.setAcceptDrops(True) self.book_cover.setAcceptDrops(True)
self.current_view = self.library_view self.current_view = self.library_view
self.current_view.sortByColumn(3, Qt.DescendingOrder) self.current_view.sortByColumn(3, Qt.DescendingOrder)
@ -59,10 +80,14 @@ class MainWindow(QObject, Ui_MainWindow):
show_dev = False show_dev = False
elif model.is_reader(index): elif model.is_reader(index):
self.device_view.setModel(self.reader_model) self.device_view.setModel(self.reader_model)
QObject.connect(self.device_view.selectionModel(), SIGNAL("currentChanged(QModelIndex, QModelIndex)"), self.show_book) QObject.connect(self.device_view.selectionModel(), \
SIGNAL("currentChanged(QModelIndex, QModelIndex)"), \
self.show_book)
elif model.is_card(index): elif model.is_card(index):
self.device_view.setModel(self.card_model) self.device_view.setModel(self.card_model)
QObject.connect(self.device_view.selectionModel(), SIGNAL("currentChanged(QModelIndex, QModelIndex)"), self.show_book) QObject.connect(self.device_view.selectionModel(), \
SIGNAL("currentChanged(QModelIndex, QModelIndex)"), \
self.show_book)
self.show_device(show_dev) self.show_device(show_dev)
@ -76,7 +101,8 @@ class MainWindow(QObject, Ui_MainWindow):
QCoreApplication.processEvents(QEventLoop.ExcludeUserInputEvents) QCoreApplication.processEvents(QEventLoop.ExcludeUserInputEvents)
def resize_columns(self, topleft, bottomright): def resize_columns(self, topleft, bottomright):
if self.library_view.isVisible(): view = self.library_view if self.library_view.isVisible():
view = self.library_view
else: view = self.device_view else: view = self.device_view
for c in range(topleft.column(), bottomright.column()+1): for c in range(topleft.column(), bottomright.column()+1):
view.resizeColumnToContents(c) view.resizeColumnToContents(c)
@ -85,7 +111,8 @@ class MainWindow(QObject, Ui_MainWindow):
if self.library_view.isVisible(): if self.library_view.isVisible():
formats, tags, comments, cover = current.model().info(current.row()) formats, tags, comments, cover = current.model().info(current.row())
data = LIBRARY_BOOK_TEMPLATE.arg(formats).arg(tags).arg(comments) data = LIBRARY_BOOK_TEMPLATE.arg(formats).arg(tags).arg(comments)
tooltip = "To save the cover, drag it to the desktop.<br>To change the cover drag the new cover onto this picture" tooltip = "To save the cover, drag it to the desktop.<br>To \
change the cover drag the new cover onto this picture"
else: else:
title, author, size, mime, cover = current.model().info(current.row()) title, author, size, mime, cover = current.model().info(current.row())
data = DEVICE_BOOK_TEMPLATE.arg(title).arg(size).arg(author).arg(mime) data = DEVICE_BOOK_TEMPLATE.arg(title).arg(size).arg(author).arg(mime)
@ -103,13 +130,19 @@ class MainWindow(QObject, Ui_MainWindow):
def delete(self, action): def delete(self, action):
rows = self.current_view.selectionModel().selectedRows() rows = self.current_view.selectionModel().selectedRows()
if not len(rows): return if not len(rows):
return
count = str(len(rows)) count = str(len(rows))
ret = QMessageBox.question(self.window, self.trUtf8(APP_TITLE + " - confirm"), self.trUtf8("Are you sure you want to <b>permanently delete</b> these ") +count+self.trUtf8(" item(s)?"), QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes) ret = QMessageBox.question(self.window, self.trUtf8(APP_TITLE + \
if ret != QMessageBox.Yes: return " - confirm"), self.trUtf8("Are you sure you want to \
<b>permanently delete</b> these ") +count+self.trUtf8(" item(s)?"), \
QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes)
if ret != QMessageBox.Yes:
return
self.window.setCursor(Qt.WaitCursor) self.window.setCursor(Qt.WaitCursor)
if self.library_view.isVisible(): if self.library_view.isVisible():
self.library_model.delete(self.library_view.selectionModel().selectedRows()) self.library_model.delete(self.library_view.selectionModel()\
.selectedRows())
else: else:
self.status("Deleting files from device") self.status("Deleting files from device")
paths = self.device_view.model().delete(rows) paths = self.device_view.model().delete(rows)
@ -129,9 +162,11 @@ class MainWindow(QObject, Ui_MainWindow):
def read_settings(self): def read_settings(self):
settings = QSettings() settings = QSettings()
settings.beginGroup("MainWindow") settings.beginGroup("MainWindow")
self.window.resize(settings.value("size", QVariant(QSize(1000, 700))).toSize()) self.window.resize(settings.value("size", QVariant(QSize(1000, 700))).\
toSize())
settings.endGroup() settings.endGroup()
self.database_path = settings.value("database path", QVariant(os.path.expanduser("~/library.db"))).toString() self.database_path = settings.value("database path", QVariant(os.path\
.expanduser("~/library.db"))).toString()
def write_settings(self): def write_settings(self):
settings = QSettings() settings = QSettings()
@ -145,8 +180,11 @@ class MainWindow(QObject, Ui_MainWindow):
def add(self, action): def add(self, action):
settings = QSettings() settings = QSettings()
dir = settings.value("add books dialog dir", QVariant(os.path.expanduser("~"))).toString() _dir = settings.value("add books dialog dir", \
files = QFileDialog.getOpenFileNames(self.window, "Choose books to add to library", dir, "Books (*.lrf *.lrx *.rtf *.pdf *.txt);;All files (*)") QVariant(os.path.expanduser("~"))).toString()
files = QFileDialog.getOpenFileNames(self.window, \
"Choose books to add to library", _dir, \
"Books (*.lrf *.lrx *.rtf *.pdf *.txt);;All files (*)")
if not files.isEmpty(): if not files.isEmpty():
x = str(files[0]) x = str(files[0])
settings.setValue("add books dialog dir", QVariant(os.path.dirname(x))) settings.setValue("add books dialog dir", QVariant(os.path.dirname(x)))
@ -155,9 +193,9 @@ class MainWindow(QObject, Ui_MainWindow):
def add_books(self, files): def add_books(self, files):
self.window.setCursor(Qt.WaitCursor) self.window.setCursor(Qt.WaitCursor)
for file in files: for _file in files:
file = os.path.abspath(file) _file = os.path.abspath(_file)
self.library_view.model().add_book(file) self.library_view.model().add_book(_file)
if self.library_view.isVisible(): self.search.clear() if self.library_view.isVisible(): self.search.clear()
else: self.library_model.search("") else: self.library_model.search("")
hv = self.library_view.horizontalHeader() hv = self.library_view.horizontalHeader()
@ -171,9 +209,9 @@ class MainWindow(QObject, Ui_MainWindow):
if self.library_view.isVisible(): if self.library_view.isVisible():
rows = self.library_view.selectionModel().selectedRows() rows = self.library_view.selectionModel().selectedRows()
for row in rows: for row in rows:
id = self.library_model.id_from_index(row) _id = self.library_model.id_from_index(row)
dialog = QDialog(self.window) dialog = QDialog(self.window)
ed = EditBookDialog(dialog, id, self.library_model.db) EditBookDialog(dialog, _id, self.library_model.db)
if dialog.exec_() == QDialog.Accepted: if dialog.exec_() == QDialog.Accepted:
self.library_model.refresh_row(row.row()) self.library_model.refresh_row(row.row())
@ -181,7 +219,8 @@ class MainWindow(QObject, Ui_MainWindow):
def update_cover(self, pix): def update_cover(self, pix):
if not pix.isNull(): if not pix.isNull():
try: try:
self.library_view.model().update_cover(self.library_view.currentIndex(), pix) self.library_view.model().update_cover(self.library_view\
.currentIndex(), pix)
self.book_cover.setPixmap(pix) self.book_cover.setPixmap(pix)
except Exception, e: Error("Unable to change cover", e) except Exception, e: Error("Unable to change cover", e)
@ -194,7 +233,8 @@ class MainWindow(QObject, Ui_MainWindow):
order = hv.sortIndicatorOrder() order = hv.sortIndicatorOrder()
model = self.card_model if oncard else self.reader_model model = self.card_model if oncard else self.reader_model
model.sort(col, order) model.sort(col, order)
if self.device_view.isVisible() and self.device_view.model() == model: self.search.clear() if self.device_view.isVisible() and self.device_view.model()\
== model: self.search.clear()
else: model.search("") else: model.search("")
def sync_lists(): def sync_lists():
@ -208,61 +248,71 @@ class MainWindow(QObject, Ui_MainWindow):
ename = "file" ename = "file"
try: try:
if ids: if ids:
for id in ids: for _id in ids:
formats = [] formats = []
info = self.library_view.model().book_info(id) info = self.library_view.model().book_info(_id)
if info["cover"]: if info["cover"]:
pix = QPixmap() pix = QPixmap()
pix.loadFromData(str(info["cover"])) pix.loadFromData(str(info["cover"]))
if pix.isNull(): pix = DEFAULT_BOOK_COVER if pix.isNull():
pix = pix.scaledToHeight(self.dev.THUMBNAIL_HEIGHT, Qt.SmoothTransformation) pix = DEFAULT_BOOK_COVER
buffer = QBuffer() pix = pix.scaledToHeight(self.dev.THUMBNAIL_HEIGHT, \
buffer.open(QIODevice.WriteOnly) Qt.SmoothTransformation)
pix.save(buffer, "JPEG") _buffer = QBuffer()
info["cover"] = (pix.width(), pix.height(), str(buffer.buffer())) _buffer.open(QIODevice.WriteOnly)
pix.save(_buffer, "JPEG")
info["cover"] = (pix.width(), pix.height(), \
str(_buffer.buffer()))
ename = info["title"] ename = info["title"]
for f in files: for f in files:
if re.match("......_"+str(id)+"_", os.path.basename(f)): if re.match("......_"+str(_id)+"_", os.path.basename(f)):
formats.append(f) formats.append(f)
file = None _file = None
try: try:
for format in self.dev.FORMATS: for format in self.dev.FORMATS:
for f in formats: for f in formats:
if extension(f) == format: if extension(f) == format:
file = f _file = f
raise StopIteration() raise StopIteration()
except StopIteration: pass except StopIteration: pass
if not file: if not _file:
Error("The library does not have any formats that can be viewed on the device for " + ename, None) Error("The library does not have any formats that "+\
"can be viewed on the device for " + ename, None)
continue continue
f = open(file, "rb") f = open(_file, "rb")
self.status("Sending "+info["title"]+" to device") self.status("Sending "+info["title"]+" to device")
try: try:
self.dev.add_book(f, "libprs500_"+str(id)+"."+extension(file), info, booklists, oncard=oncard, end_session=False) self.dev.add_book(f, "libprs500_"+str(_id)+"."+\
extension(_file), info, booklists, oncard=oncard, \
end_session=False)
update_models() update_models()
except PathError, e: except PathError, e:
if "already exists" in str(e): if "already exists" in str(e):
Error(info["title"] + " already exists on the device", None) Error(info["title"] + \
" already exists on the device", None)
self.progress(100) self.progress(100)
continue continue
else: raise else: raise
finally: f.close() finally: f.close()
sync_lists() sync_lists()
else: else:
for file in files: for _file in files:
ename = file ename = _file
if extension(file) not in self.dev.FORMATS: if extension(_file) not in self.dev.FORMATS:
Error(ename + " is not in a supported format") Error(ename + " is not in a supported format")
continue continue
info = { "title":os.path.basename(file), "authors":"Unknown", "cover":(None, None, None) } info = { "title":os.path.basename(_file), \
f = open(file, "rb") "authors":"Unknown", "cover":(None, None, None) }
f = open(_file, "rb")
self.status("Sending "+info["title"]+" to device") self.status("Sending "+info["title"]+" to device")
try: try:
self.dev.add_book(f, os.path.basename(file), info, booklists, oncard=oncard, end_session=False) self.dev.add_book(f, os.path.basename(_file), info, \
booklists, oncard=oncard, end_session=False)
update_models() update_models()
except PathError, e: except PathError, e:
if "already exists" in str(e): if "already exists" in str(e):
Error(info["title"] + " already exists on the device", None) Error(info["title"] + \
" already exists on the device", None)
self.progress(100) self.progress(100)
continue continue
else: raise else: raise
@ -290,21 +340,31 @@ class MainWindow(QObject, Ui_MainWindow):
self.library_model.set_data(LibraryDatabase(str(self.database_path))) self.library_model.set_data(LibraryDatabase(str(self.database_path)))
self.library_view.setModel(self.library_model) self.library_view.setModel(self.library_model)
self.current_view = self.library_view self.current_view = self.library_view
QObject.connect(self.library_model, SIGNAL("layoutChanged()"), self.library_view.resizeRowsToContents) QObject.connect(self.library_model, SIGNAL("layoutChanged()"), \
QObject.connect(self.library_view.selectionModel(), SIGNAL("currentChanged(QModelIndex, QModelIndex)"), self.show_book) self.library_view.resizeRowsToContents)
QObject.connect(self.search, SIGNAL("textChanged(QString)"), self.library_model.search) QObject.connect(self.library_view.selectionModel(), \
SIGNAL("currentChanged(QModelIndex, QModelIndex)"), self.show_book)
QObject.connect(self.search, SIGNAL("textChanged(QString)"), \
self.library_model.search)
QObject.connect(self.library_model, SIGNAL("sorted()"), self.model_modified) QObject.connect(self.library_model, SIGNAL("sorted()"), self.model_modified)
QObject.connect(self.library_model, SIGNAL("searched()"), self.model_modified) QObject.connect(self.library_model, SIGNAL("searched()"), \
QObject.connect(self.library_model, SIGNAL("deleted()"), self.model_modified) self.model_modified)
QObject.connect(self.library_model, SIGNAL("dataChanged(QModelIndex, QModelIndex)"), self.resize_columns) QObject.connect(self.library_model, SIGNAL("deleted()"), \
QObject.connect(self.library_view, SIGNAL('books_dropped'), self.add_books) self.model_modified)
QObject.connect(self.library_model, SIGNAL('formats_added'), self.formats_added) QObject.connect(self.library_model, \
SIGNAL("dataChanged(QModelIndex, QModelIndex)"), self.resize_columns)
QObject.connect(self.library_view, \
SIGNAL('books_dropped'), self.add_books)
QObject.connect(self.library_model, \
SIGNAL('formats_added'), self.formats_added)
self.library_view.resizeColumnsToContents() self.library_view.resizeColumnsToContents()
# Create Device tree # Create Device tree
model = DeviceModel(self.device_tree) model = DeviceModel(self.device_tree)
QObject.connect(self.device_tree, SIGNAL("activated(QModelIndex)"), self.tree_clicked) QObject.connect(self.device_tree, SIGNAL("activated(QModelIndex)"), \
QObject.connect(self.device_tree, SIGNAL("clicked(QModelIndex)"), self.tree_clicked) self.tree_clicked)
QObject.connect(self.device_tree, SIGNAL("clicked(QModelIndex)"), \
self.tree_clicked)
QObject.connect(model, SIGNAL('books_dropped'), self.add_books) QObject.connect(model, SIGNAL('books_dropped'), self.add_books)
QObject.connect(model, SIGNAL('upload_books'), self.upload_books) QObject.connect(model, SIGNAL('upload_books'), self.upload_books)
self.device_tree.setModel(model) self.device_tree.setModel(model)
@ -313,14 +373,18 @@ class MainWindow(QObject, Ui_MainWindow):
self.reader_model = DeviceBooksModel(window) self.reader_model = DeviceBooksModel(window)
self.card_model = DeviceBooksModel(window) self.card_model = DeviceBooksModel(window)
self.device_view.setModel(self.reader_model) self.device_view.setModel(self.reader_model)
QObject.connect(self.device_view.selectionModel(), SIGNAL("currentChanged(QModelIndex, QModelIndex)"), self.show_book) QObject.connect(self.device_view.selectionModel(), \
SIGNAL("currentChanged(QModelIndex, QModelIndex)"), self.show_book)
for model in (self.reader_model, self. card_model): for model in (self.reader_model, self. card_model):
QObject.connect(model, SIGNAL("layoutChanged()"), self.device_view.resizeRowsToContents) QObject.connect(model, SIGNAL("layoutChanged()"), \
QObject.connect(self.search, SIGNAL("textChanged(QString)"), model.search) self.device_view.resizeRowsToContents)
QObject.connect(self.search, SIGNAL("textChanged(QString)"), \
model.search)
QObject.connect(model, SIGNAL("sorted()"), self.model_modified) QObject.connect(model, SIGNAL("sorted()"), self.model_modified)
QObject.connect(model, SIGNAL("searched()"), self.model_modified) QObject.connect(model, SIGNAL("searched()"), self.model_modified)
QObject.connect(model, SIGNAL("deleted()"), self.model_modified) QObject.connect(model, SIGNAL("deleted()"), self.model_modified)
QObject.connect(model, SIGNAL("dataChanged(QModelIndex, QModelIndex)"), self.resize_columns) QObject.connect(model, SIGNAL("dataChanged(QModelIndex, QModelIndex)")\
, self.resize_columns)
# Setup book display # Setup book display
self.book_cover.hide() self.book_cover.hide()
@ -332,10 +396,12 @@ class MainWindow(QObject, Ui_MainWindow):
QObject.connect(self.action_edit, SIGNAL("triggered(bool)"), self.edit) QObject.connect(self.action_edit, SIGNAL("triggered(bool)"), self.edit)
# DnD setup # DnD setup
QObject.connect(self.book_cover, SIGNAL("cover_received(QPixmap)"), self.update_cover) QObject.connect(self.book_cover, SIGNAL("cover_received(QPixmap)"), \
self.update_cover)
self.detector = DeviceConnectDetector(self.dev) self.detector = DeviceConnectDetector(self.dev)
self.connect(self.detector, SIGNAL("device_connected()"), self.establish_connection) self.connect(self.detector, SIGNAL("device_connected()"), \
self.establish_connection)
self.connect(self.detector, SIGNAL("device_removed()"), self.device_removed) self.connect(self.detector, SIGNAL("device_removed()"), self.device_removed)
self.search.setFocus(Qt.OtherFocusReason) self.search.setFocus(Qt.OtherFocusReason)
self.show_device(False) self.show_device(False)
@ -379,8 +445,10 @@ class MainWindow(QObject, Ui_MainWindow):
return return
except ProtocolError, e: except ProtocolError, e:
traceback.print_exc(e) traceback.print_exc(e)
qFatal("Unable to connect to device. Please try unplugging and reconnecting it") qFatal("Unable to connect to device. Please try unplugging and"+\
self.df.setText(self.df_template.arg("Connected: "+info[0]).arg(info[1]).arg(info[2])) " reconnecting it")
self.df.setText(self.df_template.arg("Connected: "+info[0])\
.arg(info[1]).arg(info[2]))
self.update_availabe_space(end_session=False) self.update_availabe_space(end_session=False)
self.card = self.dev.card() self.card = self.dev.card()
self.is_connected = True self.is_connected = True
@ -430,7 +498,8 @@ class DeviceConnectDetector(QObject):
devobj = bus.get_object('org.freedesktop.Hal', udi) devobj = bus.get_object('org.freedesktop.Hal', udi)
dev = dbus.Interface(devobj, "org.freedesktop.Hal.Device") dev = dbus.Interface(devobj, "org.freedesktop.Hal.Device")
properties = dev.GetAllProperties() properties = dev.GetAllProperties()
vendor_id, product_id = int(properties["usb_device.vendor_id"]), int(properties["usb_device.product_id"]) vendor_id = int(properties["usb_device.vendor_id"]),
product_id = int(properties["usb_device.product_id"])
if self.dev.signature() == (vendor_id, product_id): ans = True if self.dev.signature() == (vendor_id, product_id): ans = True
except: except:
self.device_detector = self.startTimer(1000) self.device_detector = self.startTimer(1000)
@ -451,12 +520,16 @@ class DeviceConnectDetector(QObject):
raise Exception("DBUS doesn't support the Qt mainloop") raise Exception("DBUS doesn't support the Qt mainloop")
import dbus import dbus
bus = dbus.SystemBus() bus = dbus.SystemBus()
hal_manager_obj = bus.get_object('org.freedesktop.Hal', '/org/freedesktop/Hal/Manager') hal_manager_obj = bus.get_object('org.freedesktop.Hal',\
hal_manager = dbus.Interface(hal_manager_obj, 'org.freedesktop.Hal.Manager') '/org/freedesktop/Hal/Manager')
hal_manager.connect_to_signal('DeviceAdded', self.device_added_callback) hal_manager = dbus.Interface(hal_manager_obj,\
hal_manager.connect_to_signal('DeviceRemoved', self.device_removed_callback) 'org.freedesktop.Hal.Manager')
hal_manager.connect_to_signal('DeviceAdded', \
self.device_added_callback)
hal_manager.connect_to_signal('DeviceRemoved', \
self.device_removed_callback)
except Exception, e: except Exception, e:
#Warning("Could not connect to HAL", e) #_Warning("Could not connect to HAL", e)
self.is_connected = False self.is_connected = False
self.device_detector = self.startTimer(1000) self.device_detector = self.startTimer(1000)
@ -466,11 +539,13 @@ def main():
lock = os.path.join(tempfile.gettempdir(),"libprs500_gui_lock") lock = os.path.join(tempfile.gettempdir(),"libprs500_gui_lock")
if os.access(lock, os.F_OK): if os.access(lock, os.F_OK):
print >>sys.stderr, "Another instance of", APP_TITLE, "is running" print >>sys.stderr, "Another instance of", APP_TITLE, "is running"
print >>sys.stderr, "If you are sure this is not the case then manually delete the file", lock print >>sys.stderr, "If you are sure this is not the case then "+\
"manually delete the file", lock
sys.exit(1) sys.exit(1)
parser = OptionParser(usage="usage: %prog [options]", version=VERSION) parser = OptionParser(usage="usage: %prog [options]", version=VERSION)
parser.add_option("--log-packets", help="print out packet stream to stdout. "+\ parser.add_option("--log-packets", help="print out packet stream to stdout. "+\
"The numbers in the left column are byte offsets that allow the packet size to be read off easily.", \ "The numbers in the left column are byte offsets that allow"+\
" the packet size to be read off easily.", \
dest="log_packets", action="store_true", default=False) dest="log_packets", action="store_true", default=False)
options, args = parser.parse_args() options, args = parser.parse_args()
from PyQt4.Qt import QApplication, QMainWindow from PyQt4.Qt import QApplication, QMainWindow
@ -483,7 +558,7 @@ def main():
installErrorHandler(QErrorMessage(window)) installErrorHandler(QErrorMessage(window))
QCoreApplication.setOrganizationName("KovidsBrain") QCoreApplication.setOrganizationName("KovidsBrain")
QCoreApplication.setApplicationName(APP_TITLE) QCoreApplication.setApplicationName(APP_TITLE)
gui = MainWindow(window, options.log_packets) Main(window, options.log_packets)
lock = LockFile(lock) lock = LockFile(lock)
return app.exec_() return app.exec_()

View File

@ -12,7 +12,12 @@
## You should have received a copy of the GNU General Public License along ## You should have received a copy of the GNU General Public License along
## with this program; if not, write to the Free Software Foundation, Inc., ## with this program; if not, write to the Free Software Foundation, Inc.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
import re, os, string, textwrap, time, traceback, sys import re
import os
import textwrap
import time
import traceback
import sys
from operator import itemgetter, attrgetter from operator import itemgetter, attrgetter
from socket import gethostname from socket import gethostname
from urlparse import urlparse, urlunparse from urlparse import urlparse, urlunparse
@ -20,15 +25,19 @@ from urllib import quote, unquote
from math import sin, cos, pi from math import sin, cos, pi
from libprs500 import TEMPORARY_FILENAME_TEMPLATE as TFT from libprs500 import TEMPORARY_FILENAME_TEMPLATE as TFT
from libprs500.lrf.meta import LRFMetaFile from libprs500.gui import Error, _Warning
from libprs500.gui import Error, Warning
from PyQt4 import QtGui, QtCore from PyQt4 import QtGui, QtCore
from PyQt4.QtCore import Qt, SIGNAL from PyQt4.QtCore import Qt, SIGNAL
from PyQt4.Qt import QApplication, QString, QFont, QAbstractListModel, QVariant, QAbstractTableModel, QTableView, QListView, QLabel,\ from PyQt4.Qt import QApplication, QString, QFont, QAbstractListModel, \
QAbstractItemView, QPixmap, QIcon, QSize, QMessageBox, QSettings, QFileDialog, QErrorMessage, QDialog, QSpinBox, QPoint, QTemporaryFile, QDir, QFile, QIODevice,\ QVariant, QAbstractTableModel, QTableView, QListView, \
QPainterPath, QItemDelegate, QPainter, QPen, QColor, QLinearGradient, QBrush, QStyle,\ QLabel, QAbstractItemView, QPixmap, QIcon, QSize, \
QStringList, QByteArray, QBuffer, QMimeData, QTextStream, QIODevice, QDrag, QRect QMessageBox, QSettings, QFileDialog, QErrorMessage, \
QSpinBox, QPoint, QTemporaryFile, QDir, QFile, \
QIODevice, QPainterPath, QItemDelegate, QPainter, QPen, \
QColor, QLinearGradient, QBrush, QStyle, QStringList, \
QByteArray, QBuffer, QMimeData, QTextStream, QIODevice, \
QDrag, QRect
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
TIME_WRITE_FMT = "%d %b %Y" #: The display format used to show dates TIME_WRITE_FMT = "%d %b %Y" #: The display format used to show dates
@ -44,6 +53,10 @@ class FileDragAndDrop(object):
@classmethod @classmethod
def _get_r_ok_files(cls, event): def _get_r_ok_files(cls, event):
"""
Return list of paths from event that point to files to
which the user has read permission.
"""
files = [] files = []
md = event.mimeData() md = event.mimeData()
if md.hasFormat("text/uri-list"): if md.hasFormat("text/uri-list"):
@ -51,18 +64,20 @@ class FileDragAndDrop(object):
for url in candidates: for url in candidates:
o = urlparse(url) o = urlparse(url)
if o.scheme and o.scheme != 'file': if o.scheme and o.scheme != 'file':
Warning(o.scheme + " not supported in drop events", None) _Warning(o.scheme + " not supported in drop events", None)
continue continue
path = unquote(o.path) path = unquote(o.path)
if not os.access(path, os.R_OK): if not os.access(path, os.R_OK):
Warning("You do not have read permission for: " + path) _Warning("You do not have read permission for: " + path)
continue continue
if os.path.isdir(path): if os.path.isdir(path):
root, dirs, files2 = os.walk(path) root, dirs, files2 = os.walk(path)
for file in files2: for _file in files2:
path = root + file path = root + _file
if os.access(path, os.R_OK): files.append(path) if os.access(path, os.R_OK):
else: files.append(path) files.append(path)
else:
files.append(path)
return files return files
def __init__(self, QtBaseClass, enable_drag=True): def __init__(self, QtBaseClass, enable_drag=True):
@ -79,15 +94,20 @@ class FileDragAndDrop(object):
def mouseMoveEvent(self, event): def mouseMoveEvent(self, event):
self.QtBaseClass.mousePressEvent(self, event) self.QtBaseClass.mousePressEvent(self, event)
if self.enable_drag: if self.enable_drag:
if event.buttons() & Qt.LeftButton != Qt.LeftButton: return if event.buttons() & Qt.LeftButton != Qt.LeftButton:
if (event.pos() - self._drag_start_position).manhattanLength() < QApplication.startDragDistance(): return return
if (event.pos() - self._drag_start_position).manhattanLength() < \
QApplication.startDragDistance():
return
self.start_drag(self._drag_start_position) self.start_drag(self._drag_start_position)
def start_drag(self, pos): pass def start_drag(self, pos):
raise NotImplementedError()
def dragEnterEvent(self, event): def dragEnterEvent(self, event):
if event.mimeData().hasFormat("text/uri-list"): event.acceptProposedAction() if event.mimeData().hasFormat("text/uri-list"):
event.acceptProposedAction()
def dragMoveEvent(self, event): def dragMoveEvent(self, event):
event.acceptProposedAction() event.acceptProposedAction()
@ -97,25 +117,29 @@ class FileDragAndDrop(object):
if files: if files:
try: try:
event.setDropAction(Qt.CopyAction) event.setDropAction(Qt.CopyAction)
if self.files_dropped(files, event): event.accept() if self.files_dropped(files, event):
event.accept()
except Exception, e: except Exception, e:
Error("There was an error processing the dropped files.", e) Error("There was an error processing the dropped files.", e)
raise e raise e
def files_dropped(self, files, event): return False def files_dropped(self, files, event):
raise NotImplementedError()
def drag_object_from_files(self, files): def drag_object_from_files(self, files):
if files: if files:
drag = QDrag(self) drag = QDrag(self)
mime_data = QMimeData() mime_data = QMimeData()
self._dragged_files, urls = [], [] self._dragged_files, urls = [], []
for file in files: for _file in files:
urls.append(urlunparse(('file', quote(gethostname()), quote(str(file.name)), '','',''))) urls.append(urlunparse(('file', quote(gethostname()), \
self._dragged_files.append(file) quote(str(_file.name)), '','','')))
self._dragged_files.append(_file)
mime_data.setData("text/uri-list", QByteArray("\n".join(urls))) mime_data.setData("text/uri-list", QByteArray("\n".join(urls)))
user = os.getenv('USER') user = os.getenv('USER')
if user: mime_data.setData("text/x-xdnd-username", QByteArray(user)) if user:
mime_data.setData("text/x-xdnd-username", QByteArray(user))
drag.setMimeData(mime_data) drag.setMimeData(mime_data)
return drag return drag
@ -135,17 +159,23 @@ class TableView(FileDragAndDrop, QTableView):
QTableView.__init__(self, parent) QTableView.__init__(self, parent)
@classmethod @classmethod
def wrap(cls, s, width=20): return textwrap.fill(str(s), width) def wrap(cls, s, width=20):
return textwrap.fill(str(s), width)
@classmethod @classmethod
def human_readable(cls, size): def human_readable(cls, size):
""" Convert a size in bytes into a human readable form """ """ Convert a size in bytes into a human readable form """
if size < 1024: divisor, suffix = 1, "B" if size < 1024:
elif size < 1024*1024: divisor, suffix = 1024., "KB" divisor, suffix = 1, "B"
elif size < 1024*1024*1024: divisor, suffix = 1024*1024, "MB" elif size < 1024*1024:
elif size < 1024*1024*1024*1024: divisor, suffix = 1024*1024, "GB" divisor, suffix = 1024., "KB"
elif size < 1024*1024*1024:
divisor, suffix = 1024*1024, "MB"
elif size < 1024*1024*1024*1024:
divisor, suffix = 1024*1024, "GB"
size = str(size/divisor) size = str(size/divisor)
if size.find(".") > -1: size = size[:size.find(".")+2] if size.find(".") > -1:
size = size[:size.find(".")+2]
return size + " " + suffix return size + " " + suffix
def render_to_pixmap(self, indices): def render_to_pixmap(self, indices):
@ -161,7 +191,8 @@ class TableView(FileDragAndDrop, QTableView):
option = self.viewOptions() option = self.viewOptions()
option.state |= QStyle.State_Selected option.state |= QStyle.State_Selected
for j in range(len(indices)): for j in range(len(indices)):
option.rect = QRect(rects[j].topLeft() - rect.topLeft(), rects[j].size()) option.rect = QRect(rects[j].topLeft() - rect.topLeft(), \
rects[j].size())
self.itemDelegate(indices[j]).paint(painter, option, indices[j]) self.itemDelegate(indices[j]).paint(painter, option, indices[j])
painter.end() painter.end()
return pixmap return pixmap
@ -182,7 +213,8 @@ class TemporaryFile(QTemporaryFile):
def open(self): def open(self):
ok = QFile.open(self, QIODevice.ReadWrite) ok = QFile.open(self, QIODevice.ReadWrite)
self._file_name = os.path.normpath(os.path.abspath(str(QTemporaryFile.fileName(self)))) self._file_name = os.path.normpath(os.path.abspath(\
str(QTemporaryFile.fileName(self))))
return ok return ok
@apply @apply
@ -202,8 +234,8 @@ class CoverDisplay(FileDragAndDrop, QLabel):
QLabel.__init__(self, parent) QLabel.__init__(self, parent)
def files_dropped(self, files, event): def files_dropped(self, files, event):
pix = QPixmap() pix = QPixmap()
for file in files: for _file in files:
pix = QPixmap(file) pix = QPixmap(_file)
if not pix.isNull(): break if not pix.isNull(): break
if not pix.isNull(): if not pix.isNull():
self.emit(SIGNAL("cover_received(QPixmap)"), pix) self.emit(SIGNAL("cover_received(QPixmap)"), pix)
@ -212,10 +244,10 @@ class CoverDisplay(FileDragAndDrop, QLabel):
def start_drag(self, event): def start_drag(self, event):
drag, files = self.drag_object(["jpeg"]) drag, files = self.drag_object(["jpeg"])
if drag and files: if drag and files:
file = files[0] _file = files[0]
drag.setPixmap(self.pixmap()) drag.setPixmap(self.pixmap())
self.pixmap().save(file) self.pixmap().save(_file)
file.close() _file.close()
drag.start(Qt.MoveAction) drag.start(Qt.MoveAction)
class DeviceView(FileDragAndDrop, QListView): class DeviceView(FileDragAndDrop, QListView):
@ -233,7 +265,8 @@ class DeviceView(FileDragAndDrop, QListView):
ids = [] ids = []
md = event.mimeData() md = event.mimeData()
if md.hasFormat("application/x-libprs500-id"): if md.hasFormat("application/x-libprs500-id"):
ids = [ int(id) for id in FileDragAndDrop._bytes_to_string(md.data("application/x-libprs500-id")).split()] ids = [ int(id) for id in FileDragAndDrop._bytes_to_string(\
md.data("application/x-libprs500-id")).split()]
index = self.indexAt(event.pos()) index = self.indexAt(event.pos())
if index.isValid(): if index.isValid():
return self.model().files_dropped(files, index, ids) return self.model().files_dropped(files, index, ids)
@ -264,7 +297,8 @@ class LibraryBooksView(TableView):
drag = self.drag_object_from_files(files) drag = self.drag_object_from_files(files)
if drag: if drag:
ids = [ str(self.model().id_from_row(row)) for row in rows ] ids = [ str(self.model().id_from_row(row)) for row in rows ]
drag.mimeData().setData("application/x-libprs500-id", QByteArray("\n".join(ids))) drag.mimeData().setData("application/x-libprs500-id", \
QByteArray("\n".join(ids)))
drag.start() drag.start()
@ -289,7 +323,8 @@ class LibraryDelegate(QItemDelegate):
self.star_path = QPainterPath() self.star_path = QPainterPath()
self.star_path.moveTo(90, 50) self.star_path.moveTo(90, 50)
for i in range(1, 5): 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.lineTo(50 + 40 * cos(0.8 * i * pi), \
50 + 40 * sin(0.8 * i * pi))
self.star_path.closeSubpath() self.star_path.closeSubpath()
self.star_path.setFillRule(Qt.WindingFill) self.star_path.setFillRule(Qt.WindingFill)
gradient = QLinearGradient(0, 0, 0, 100) gradient = QLinearGradient(0, 0, 0, 100)
@ -366,7 +401,8 @@ class LibraryDelegate(QItemDelegate):
class LibraryBooksModel(QAbstractTableModel): class LibraryBooksModel(QAbstractTableModel):
FIELDS = ["id", "title", "authors", "size", "date", "rating", "publisher", "tags", "comments"] FIELDS = ["id", "title", "authors", "size", "date", "rating", "publisher", \
"tags", "comments"]
TIME_READ_FMT = "%Y-%m-%d %H:%M:%S" TIME_READ_FMT = "%Y-%m-%d %H:%M:%S"
def __init__(self, parent): def __init__(self, parent):
QAbstractTableModel.__init__(self, parent) QAbstractTableModel.__init__(self, parent)
@ -377,31 +413,36 @@ class LibraryBooksModel(QAbstractTableModel):
def extract_formats(self, rows): def extract_formats(self, rows):
files = [] files = []
for row in rows: for row in rows:
id = self.id_from_row(row) _id = self.id_from_row(row)
au = self._data[row]["authors"] if self._data[row]["authors"] else "Unknown" au = self._data[row]["authors"] if self._data[row]["authors"] \
basename = re.sub("\n", "", "_"+str(id)+"_"+self._data[row]["title"]+" by "+ au) else "Unknown"
exts = self.db.get_extensions(id) basename = re.sub("\n", "", "_"+str(_id)+"_"+\
self._data[row]["title"]+" by "+ au)
exts = self.db.get_extensions(_id)
for ext in exts: for ext in exts:
fmt = self.db.get_format(id, ext) fmt = self.db.get_format(_id, ext)
if not ext: ext ="" if not ext:
else: ext = "."+ext ext =""
else:
ext = "."+ext
name = basename+ext name = basename+ext
file = NamedTemporaryFile(name) file = NamedTemporaryFile(name)
file.open() file.open()
if not fmt: continue if not fmt:
continue
file.write(QByteArray(fmt)) file.write(QByteArray(fmt))
file.close() file.close()
files.append(file) files.append(file)
return files return files
def update_cover(self, index, pix): def update_cover(self, index, pix):
id = self.id_from_index(index) _id = self.id_from_index(index)
qb = QBuffer() qb = QBuffer()
qb.open(QBuffer.ReadWrite); qb.open(QBuffer.ReadWrite)
pix.save(qb, "JPG") pix.save(qb, "JPG")
data = str(qb.data()) data = str(qb.data())
qb.close() qb.close()
self.db.update_cover(id, data) self.db.update_cover(_id, data)
def add_formats(self, paths, index): def add_formats(self, paths, index):
for path in paths: for path in paths:
@ -412,29 +453,41 @@ class LibraryBooksModel(QAbstractTableModel):
f.close() f.close()
self.emit(SIGNAL('formats_added'), index) self.emit(SIGNAL('formats_added'), index)
def rowCount(self, parent): return len(self._data) def rowCount(self, parent):
def columnCount(self, parent): return len(self.FIELDS)-3 return len(self._data)
def columnCount(self, parent):
return len(self.FIELDS)-3
def setData(self, index, value, role): def setData(self, index, value, role):
done = False done = False
if role == Qt.EditRole: if role == Qt.EditRole:
row = index.row() row = index.row()
id = self._data[row]["id"] _id = self._data[row]["id"]
col = index.column() col = index.column()
val = str(value.toString()) val = str(value.toString())
if col == 0: col = "title" if col == 0:
elif col == 1: col = "authors" col = "title"
elif col == 2: return False elif col == 1:
elif col == 3: return False col = "authors"
elif col == 2:
return False
elif col == 3:
return False
elif col == 4: elif col == 4:
col, val = "rating", int(value.toInt()[0]) col, val = "rating", int(value.toInt()[0])
if val < 0: val =0 if val < 0:
if val > 5: val = 5 val = 0
elif col == 5: col = "publisher" if val > 5:
else: return False val = 5
self.db.set_metadata_item(id, col, val) elif col == 5:
col = "publisher"
else:
return False
self.db.set_metadata_item(_id, col, val)
self._data[row][col] = val self._data[row][col] = val
self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), index, index) self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), \
index, index)
for i in range(len(self._orig_data)): for i in range(len(self._orig_data)):
if self._orig_data[i]["id"] == self._data[row]["id"]: if self._orig_data[i]["id"] == self._data[row]["id"]:
self._orig_data[i][col] = self._data[row][col] self._orig_data[i][col] = self._data[row][col]
@ -445,7 +498,8 @@ class LibraryBooksModel(QAbstractTableModel):
def flags(self, index): def flags(self, index):
flags = QAbstractTableModel.flags(self, index) flags = QAbstractTableModel.flags(self, index)
if index.isValid(): if index.isValid():
if index.column() not in [2,3]: flags |= Qt.ItemIsEditable if index.column() not in [2, 3]:
flags |= Qt.ItemIsEditable
return flags return flags
def set_data(self, db): def set_data(self, db):
@ -487,17 +541,19 @@ class LibraryBooksModel(QAbstractTableModel):
def id_from_row(self, row): return self._data[row]["id"] def id_from_row(self, row): return self._data[row]["id"]
def refresh_row(self, row): def refresh_row(self, row):
self._data[row] = self.db.get_row_by_id(self._data[row]["id"], self.FIELDS) self._data[row] = self.db.get_row_by_id(self._data[row]["id"], \
self.FIELDS)
for i in range(len(self._orig_data)): for i in range(len(self._orig_data)):
if self._orig_data[i]["id"] == self._data[row]["id"]: if self._orig_data[i]["id"] == self._data[row]["id"]:
self._orig_data[i:i+1] = self._data[row] self._orig_data[i:i+1] = self._data[row]
break break
self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), self.index(row, 0), self.index(row, self.columnCount(0)-1)) self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), \
self.index(row, 0), self.index(row, self.columnCount(0)-1))
def book_info(self, id): def book_info(self, _id):
""" Return title, authors and cover in a dict """ """ Return title, authors and cover in a dict """
cover = self.db.get_cover(id) cover = self.db.get_cover(_id)
info = self.db.get_row_by_id(id, ["title", "authors"]) info = self.db.get_row_by_id(_id, ["title", "authors"])
info["cover"] = cover info["cover"] = cover
return info return info
@ -508,19 +564,27 @@ class LibraryBooksModel(QAbstractTableModel):
row = self._data[row] row = self._data[row]
if col == 4: if col == 4:
r = row["rating"] if row["rating"] else 0 r = row["rating"] if row["rating"] else 0
if r < 0: r= 0 if r < 0:
if r > 5: r=5 r = 0
if r > 5:
r = 5
return QVariant(r) return QVariant(r)
if col == 0: text = TableView.wrap(row["title"], width=25) if col == 0:
text = TableView.wrap(row["title"], width=25)
elif col == 1: elif col == 1:
au = row["authors"] au = row["authors"]
if au : text = TableView.wrap(re.sub("&", "\n", au), width=25) if au:
elif col == 2: text = TableView.human_readable(row["size"]) text = TableView.wrap(re.sub("&", "\n", au), width=25)
elif col == 3: text = time.strftime(TIME_WRITE_FMT, time.strptime(row["date"], self.TIME_READ_FMT)) elif col == 2:
text = TableView.human_readable(row["size"])
elif col == 3:
text = time.strftime(TIME_WRITE_FMT, \
time.strptime(row["date"], self.TIME_READ_FMT))
elif col == 5: elif col == 5:
pub = row["publisher"] pub = row["publisher"]
if pub: text = TableView.wrap(pub, 20) if pub: text = TableView.wrap(pub, 20)
if text == None: text = "Unknown" if text == None:
text = "Unknown"
return QVariant(text) return QVariant(text)
elif role == Qt.TextAlignmentRole and index.column() in [2,3,4]: elif role == Qt.TextAlignmentRole and index.column() in [2,3,4]:
return QVariant(Qt.AlignRight | Qt.AlignVCenter) return QVariant(Qt.AlignRight | Qt.AlignVCenter)
@ -528,11 +592,14 @@ class LibraryBooksModel(QAbstractTableModel):
def sort(self, col, order): def sort(self, col, order):
descending = order != Qt.AscendingOrder descending = order != Qt.AscendingOrder
def getter(key, func): return lambda x : func(itemgetter(key)(x)) def getter(key, func):
if col == 0: key, func = "title", string.lower return lambda x : func(itemgetter(key)(x))
if col == 1: key, func = "authors", lambda x : x.split()[-1:][0].lower() if x else "" if col == 0: key, func = "title", lambda x : x.lower()
if col == 1: key, func = "authors", lambda x : x.split()[-1:][0].lower()\
if x else ""
if col == 2: key, func = "size", int if col == 2: key, func = "size", int
if col == 3: key, func = "date", lambda x: time.mktime(time.strptime(x, self.TIME_READ_FMT)) if col == 3: key, func = "date", lambda x: time.mktime(\
time.strptime(x, self.TIME_READ_FMT))
if col == 4: key, func = "rating", lambda x: x if x else 0 if col == 4: key, func = "rating", lambda x: x if x else 0
if col == 5: key, func = "publisher", lambda x : x.lower() if x else "" if col == 5: key, func = "publisher", lambda x : x.lower() if x else ""
self.emit(SIGNAL("layoutAboutToBeChanged()")) self.emit(SIGNAL("layoutAboutToBeChanged()"))
@ -547,7 +614,8 @@ class LibraryBooksModel(QAbstractTableModel):
if not au : au = "unknown" if not au : au = "unknown"
pub = book["publisher"] pub = book["publisher"]
if not pub : pub = "unknown" if not pub : pub = "unknown"
return q in book["title"].lower() or q in au.lower() or q in pub.lower() return q in book["title"].lower() or q in au.lower() or \
q in pub.lower()
queries = unicode(query, 'utf-8').lower().split() queries = unicode(query, 'utf-8').lower().split()
self.emit(SIGNAL("layoutAboutToBeChanged()")) self.emit(SIGNAL("layoutAboutToBeChanged()"))
self._data = [] self._data = []
@ -566,21 +634,21 @@ class LibraryBooksModel(QAbstractTableModel):
if len(indices): self.emit(SIGNAL("layoutAboutToBeChanged()")) if len(indices): self.emit(SIGNAL("layoutAboutToBeChanged()"))
items = [ self._data[index.row()] for index in indices ] items = [ self._data[index.row()] for index in indices ]
for item in items: for item in items:
id = item["id"] _id = item["id"]
try: try:
self._data.remove(item) self._data.remove(item)
except ValueError: continue except ValueError: continue
self.db.delete_by_id(id) self.db.delete_by_id(_id)
for x in self._orig_data: for x in self._orig_data:
if x["id"] == id: self._orig_data.remove(x) if x["id"] == _id: self._orig_data.remove(x)
self.emit(SIGNAL("layoutChanged()")) self.emit(SIGNAL("layoutChanged()"))
self.emit(SIGNAL("deleted()")) self.emit(SIGNAL("deleted()"))
self.db.commit() self.db.commit()
def add_book(self, path): def add_book(self, path):
""" Must call search and sort on this models view after this """ """ Must call search and sort on this models view after this """
id = self.db.add_book(path) _id = self.db.add_book(path)
self._orig_data.append(self.db.get_row_by_id(id, self.FIELDS)) self._orig_data.append(self.db.get_row_by_id(_id, self.FIELDS))
class DeviceBooksModel(QAbstractTableModel): class DeviceBooksModel(QAbstractTableModel):
@apply @apply
@ -588,7 +656,7 @@ class DeviceBooksModel(QAbstractTableModel):
doc = """ The booklist this model is based on """ doc = """ The booklist this model is based on """
def fget(self): def fget(self):
return self._orig_data return self._orig_data
return property(**locals()) return property(doc=doc, fget=fget)
def __init__(self, parent): def __init__(self, parent):
QAbstractTableModel.__init__(self, parent) QAbstractTableModel.__init__(self, parent)
@ -619,10 +687,14 @@ class DeviceBooksModel(QAbstractTableModel):
if role == Qt.DisplayRole: if role == Qt.DisplayRole:
row, col = index.row(), index.column() row, col = index.row(), index.column()
book = self._data[row] book = self._data[row]
if col == 0: text = TableView.wrap(book.title, width=40) if col == 0:
elif col == 1: text = re.sub("&\s*","\n", book.author) text = TableView.wrap(book.title, width=40)
elif col == 2: text = TableView.human_readable(book.size) elif col == 1:
elif col == 3: text = time.strftime(TIME_WRITE_FMT, book.datetime) text = re.sub("&\s*", "\n", book.author)
elif col == 2:
text = TableView.human_readable(book.size)
elif col == 3:
text = time.strftime(TIME_WRITE_FMT, book.datetime)
return QVariant(text) return QVariant(text)
elif role == Qt.TextAlignmentRole and index.column() in [2,3]: elif role == Qt.TextAlignmentRole and index.column() in [2,3]:
return QVariant(Qt.AlignRight | Qt.AlignVCenter) return QVariant(Qt.AlignRight | Qt.AlignVCenter)
@ -642,8 +714,9 @@ class DeviceBooksModel(QAbstractTableModel):
return row.title, au, TableView.human_readable(row.size), row.mime, cover return row.title, au, TableView.human_readable(row.size), row.mime, cover
def sort(self, col, order): def sort(self, col, order):
def getter(key, func): return lambda x : func(attrgetter(key)(x)) def getter(key, func):
if col == 0: key, func = "title", string.lower return lambda x : func(attrgetter(key)(x))
if col == 0: key, func = "title", lambda x : x.lower()
if col == 1: key, func = "author", lambda x : x.split()[-1:][0].lower() if col == 1: key, func = "author", lambda x : x.split()[-1:][0].lower()
if col == 2: key, func = "size", int if col == 2: key, func = "size", int
if col == 3: key, func = "datetime", lambda x: x if col == 3: key, func = "datetime", lambda x: x
@ -672,20 +745,25 @@ class DeviceBooksModel(QAbstractTableModel):
def delete(self, indices): def delete(self, indices):
paths = [] paths = []
rows = [ index.row() for index in indices ] rows = [ index.row() for index in indices ]
if not rows: return if not rows:
return
self.emit(SIGNAL("layoutAboutToBeChanged()")) self.emit(SIGNAL("layoutAboutToBeChanged()"))
elems = [ self._data[row] for row in rows ] elems = [ self._data[row] for row in rows ]
for e in elems: for e in elems:
id = e.id _id = e.id
paths.append(e.path) paths.append(e.path)
self._orig_data.delete_book(id) self._orig_data.delete_book(_id)
try: self._data.remove(e) try:
except ValueError: pass self._data.remove(e)
except ValueError:
pass
self.emit(SIGNAL("layoutChanged()")) self.emit(SIGNAL("layoutChanged()"))
return paths return paths
def path(self, index): return self._data[index.row()].path def path(self, index):
def title(self, index): return self._data[index.row()].title return self._data[index.row()].path
def title(self, index):
return self._data[index.row()].title
@ -699,45 +777,61 @@ class DeviceModel(QAbstractListModel):
show_card = False show_card = False
def update_devices(self, reader=None, card=None): def update_devices(self, reader=None, card=None):
if reader != None: self.show_reader = reader if reader != None:
if card != None: self.show_card = card self.show_reader = reader
self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), self.index(1), self.index(2)) if card != None:
self.show_card = card
self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), \
self.index(1), self.index(2))
def rowCount(self, parent): return 3 def rowCount(self, parent): return 3
def update_free_space(self, reader, card): def update_free_space(self, reader, card):
self.memory_free = reader self.memory_free = reader
self.card_free = card self.card_free = card
self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), self.index(1), self.index(2)) self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), \
self.index(1), self.index(2))
def data(self, index, role): def data(self, index, role):
row = index.row() row = index.row()
data = NONE data = NONE
if role == Qt.DisplayRole: if role == Qt.DisplayRole:
text = None text = None
if row == 0: text = "Library" if row == 0:
text = "Library"
if row == 1 and self.show_reader: if row == 1 and self.show_reader:
text = "Reader\n" + TableView.human_readable(self.memory_free) + " available" text = "Reader\n" + TableView.human_readable(self.memory_free) \
+ " available"
elif row == 2 and self.show_card: elif row == 2 and self.show_card:
text = "Card\n" + TableView.human_readable(self.card_free) + " available" text = "Card\n" + TableView.human_readable(self.card_free) \
if text: data = QVariant(text) + " available"
if text:
data = QVariant(text)
elif role == Qt.DecorationRole: elif role == Qt.DecorationRole:
icon = None icon = None
if row == 0: icon = QIcon(":/library") if row == 0:
elif row == 1 and self.show_reader: icon = QIcon(":/reader") icon = QIcon(":/library")
elif self.show_card: icon = QIcon(":/card") elif row == 1 and self.show_reader:
if icon: data = QVariant(icon) icon = QIcon(":/reader")
elif self.show_card:
icon = QIcon(":/card")
if icon:
data = QVariant(icon)
elif role == Qt.SizeHintRole: elif role == Qt.SizeHintRole:
if row == 1: return QVariant(QSize(150, 70)) if row == 1:
return QVariant(QSize(150, 70))
elif role == Qt.FontRole: elif role == Qt.FontRole:
font = QFont() font = QFont()
font.setBold(True) font.setBold(True)
data = QVariant(font) data = QVariant(font)
return data return data
def is_library(self, index): return index.row() == 0 def is_library(self, index):
def is_reader(self, index): return index.row() == 1 return index.row() == 0
def is_card(self, index): return index.row() == 2 def is_reader(self, index):
return index.row() == 1
def is_card(self, index):
return index.row() == 2
def files_dropped(self, files, index, ids): def files_dropped(self, files, index, ids):
ret = False ret = False

View File

@ -89,5 +89,6 @@ try:
except ImportError: except ImportError:
print "You do not have PyQt4 installed. The GUI will not work. You can obtain PyQt4 from http://www.riverbankcomputing.co.uk/pyqt/download.php" print "You do not have PyQt4 installed. The GUI will not work. You can obtain PyQt4 from http://www.riverbankcomputing.co.uk/pyqt/download.php"
else: else:
import PyQt4.Qt
if PyQt4.Qt.PYQT_VERSION < 0x40101: if PyQt4.Qt.PYQT_VERSION < 0x40101:
print "WARNING: The GUI needs PyQt >= 4.1.1" print "WARNING: The GUI needs PyQt >= 4.1.1"