Reorganized GUI code. Split widget specific code into widgets.py.

Added infrastructure for Drag'nDrop support. Implemented DnD for the cover.
Added initial support for updating metadata in LRF files when it is updated for a logical file. This needs testing. Especially worrying are the deprecation warnings about integer overflow in struct.pack when updating the thumbnail
This commit is contained in:
Kovid Goyal 2006-12-07 00:38:43 +00:00
parent 844f4e1e08
commit 1fba894a89
8 changed files with 800 additions and 485 deletions

View File

@ -18,10 +18,8 @@ This package provides an interface to the SONY Reader PRS-500 over USB.
The public interface of libprs500 is in L{libprs500.communicate}. To use it
>>> from libprs500.communicate import PRS500Device
>>> dev = PRS500Device()
>>> dev.open()
>>> dev.get_device_information()
('Sony Reader', 'PRS-500/U', '1.0.00.21081', 'application/x-bbeb-book')
>>> dev.close()
There is also a script L{prs500} that provides a command-line interface to libprs500. See the script
for more usage examples. A GUI is available via the command prs500-gui.
@ -37,3 +35,4 @@ You may have to adjust the GROUP and the location of the rules file to suit your
__version__ = "0.2.1"
__docformat__ = "epytext"
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
TEMPORARY_FILENAME_TEMPLATE = "libprs500_"+__version__+"_temp"

View File

@ -67,6 +67,9 @@ class Book(object):
return self.__repr__()
class BookList(list):
__getslice__ = None
__setslice__ = None
def __init__(self, prefix="xs1:", root="/Data/media/", file=None):
list.__init__(self)
if file:

View File

@ -51,6 +51,7 @@ from array import array
from libprs500.prstypes import *
from libprs500.errors import *
from libprs500.books import *
from libprs500 import __author__ as AUTHOR
MINIMUM_COL_WIDTH = 12 #: Minimum width of columns in ls output
_packet_number = 0 #: Keep track of the packet number of packet tracing
@ -168,7 +169,7 @@ class PRS500Device(object):
raise TimeoutError(func.__name__)
elif "Protocol error" in str(e):
dev.close()
raise ProtocolError("There was an unknown error in the protocol. Contact the developer.")
raise ProtocolError("There was an unknown error in the protocol. Contact " + AUTHOR)
dev.close()
raise e
if not kwargs.has_key("end_session") or kwargs["end_session"]:
@ -221,7 +222,7 @@ class PRS500Device(object):
raise DeviceError()
self.handle = self.device.open()
self.handle.claimInterface(self.device_descriptor.interface_id)
res = self._send_validated_command(GetUSBProtocolVersion(), timeout=10000) # Large timeout as device mat still be initializing
res = self._send_validated_command(GetUSBProtocolVersion(), timeout=20000) # Large timeout as device may still be initializing
if res.code != 0: raise ProtocolError("Unable to get USB Protocol version.")
version = self._bulk_read(24, data_type=USBProtocolVersion)[0].version
if version not in KNOWN_USB_PROTOCOL_VERSIONS:

View File

@ -17,6 +17,7 @@ __author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
import pkg_resources, sys, os, StringIO
from PyQt4 import QtCore, QtGui # Needed for classes imported with import_ui
from libprs500.gui.widgets import LibraryBooksView, DeviceBooksView, CoverDisplay, DeviceView # Needed for import_ui
from PyQt4.uic.Compiler import compiler
def import_ui(name):

View File

@ -13,9 +13,11 @@
## with this program; if not, write to the Free Software Foundation, Inc.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
import sqlite3 as sqlite
import os, os.path, zlib
import os, os.path
from zlib import compress, decompress
from stat import ST_SIZE
from libprs500.lrf.meta import LRFMetaFile
from cStringIO import StringIO as cStringIO
class LibraryDatabase(object):
@ -32,7 +34,7 @@ class LibraryDatabase(object):
def get_cover(self, id):
raw = self.con.execute("select cover from books_meta where id=?", (id,)).next()["cover"]
return zlib.decompress(str(raw)) if raw else None
return decompress(str(raw)) if raw else None
def get_extensions(self, id):
exts = []
@ -50,8 +52,8 @@ class LibraryDatabase(object):
title, author, cover, publisher = lrf.title, lrf.author.strip(), lrf.thumbnail, lrf.publisher.strip()
if "unknown" in publisher.lower(): publisher = None
if "unknown" in author.lower(): author = None
file = zlib.compress(open(file).read())
if cover: cover = sqlite.Binary(zlib.compress(cover))
file = compress(open(file).read())
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)))
@ -83,13 +85,28 @@ class LibraryDatabase(object):
return rows
def get_format(self, id, ext):
"""
Return format C{ext} corresponding to the logical book C{id} or None if the format is unavailable.
Format is returned as a string of binary data suitable for C{ file.write} operations.
"""
ext = ext.lower()
cur = self.cur.execute("select data from books_data where id=? and extension=?",(id, ext))
cur = self.con.execute("select data from books_data where id=? and extension=?",(id, ext))
try: data = cur.next()
except: pass
else: return zlib.decompress(str(data["data"]))
else: return decompress(str(data["data"]))
def add_format(self, id, ext, data):
"""
If data for format ext already exists, it is replaced
@type ext: string or None
@type data: string
"""
try:
data.seek(0)
data = data.read()
except AttributeError: pass
if ext: ext = ext.strip().lower()
data = sqlite.Binary(compress(data))
cur = self.con.execute("select extension from books_data where id=? and extension=?", (id, ext))
present = True
try: cur.next()
@ -113,15 +130,37 @@ class LibraryDatabase(object):
if publisher and not len(publisher): publisher = None
if tags and not len(tags): tags = None
if comments and not len(comments): comments = None
if cover: cover = sqlite.Binary(zlib.compress(cover))
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()
def set_metadata_item(self, id, col, val):
self.con.execute('update books_meta set '+col+'=? where id=?',(val, id))
if col in ["authors", "title"]:
lrf = self.get_format(id, "lrf")
if lrf:
c = cStringIO()
c.write(lrf)
lrf = LRFMetaFile(c)
if col == "authors": lrf.authors = val
else: lrf.title = val
self.add_format(id, "lrf", c.getvalue())
self.con.commit()
def search(self, query): pass
def update_cover(self, id, cover):
data = None
if cover: data = sqlite.Binary(compress(cover))
self.con.execute('update books_meta set cover=? where id=?', (data, id))
lrf = self.get_format(id, "lrf")
if lrf:
c = cStringIO()
c.write(lrf)
lrf = LRFMetaFile(c)
lrf.thumbnail = cover
self.add_format(id, "lrf", c.getvalue())
self.commit()
#if __name__ == "__main__":
# lbm = LibraryDatabase("/home/kovid/library.db")

View File

@ -12,410 +12,22 @@
## 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.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
from PyQt4.QtCore import Qt, SIGNAL, QObject, QCoreApplication, QSettings, QVariant, QSize, QEventLoop
from PyQt4.QtGui import QPixmap, QAbstractItemView, QErrorMessage, QMessageBox, QFileDialog
from PyQt4.Qt import qInstallMsgHandler, qDebug, qFatal, qWarning, qCritical
from PyQt4 import uic
from libprs500.communicate import PRS500Device as device
from libprs500.errors import *
from libprs500.lrf.meta import LRFMetaFile, LRFException
from libprs500.gui import import_ui
from libprs500.gui.widgets import LibraryBooksModel, DeviceBooksModel, DeviceModel, human_readable
from database import LibraryDatabase
from editbook import EditBookDialog
from PyQt4.QtCore import Qt, SIGNAL
from PyQt4.Qt import QObject, QThread, QCoreApplication, QEventLoop, QString, QTreeWidgetItem, QStandardItemModel, QStatusBar, QVariant, QAbstractTableModel, \
QAbstractItemView, QImage, QPixmap, QIcon, QSize, QMessageBox, QSettings, QFileDialog, QErrorMessage, QDialog, QSpinBox,\
QPainterPath, QItemDelegate, QPainter, QPen, QColor, QLinearGradient, QBrush, QStyle,\
qInstallMsgHandler, qDebug, qFatal, qWarning, qCritical
from PyQt4 import uic
import sys, re, string, time, os, os.path, traceback, textwrap, zlib
from stat import ST_SIZE
from tempfile import TemporaryFile, NamedTemporaryFile
from exceptions import Exception as Exception
import xml.dom.minidom as dom
from xml.dom.ext import PrettyPrint as PrettyPrint
from operator import itemgetter, attrgetter
from math import sin, cos, pi
import sys, re, os, traceback
DEFAULT_BOOK_COVER = None
NONE = QVariant()
TIME_WRITE_FMT = "%d %b %Y"
COVER_HEIGHT = 80
def human_readable(size):
""" Convert a size in bytes into a human readable form """
if size < 1024: divisor, suffix = 1, "B"
elif size < 1024*1024: 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)
if size.find(".") > -1: size = size[:size.find(".")+2]
return size + " " + suffix
def wrap(s, width=20):
return textwrap.fill(str(s), width)
class LibraryDelegate(QItemDelegate):
COLOR = QColor("blue")
SIZE = 16
PEN = QPen(COLOR, 1, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)
def __init__(self, parent):
QItemDelegate.__init__(self, parent)
self.star_path = QPainterPath()
self.star_path.moveTo(90, 50)
for i in range(1, 5):
self.star_path.lineTo(50 + 40 * cos(0.8 * i * pi), 50 + 40 * sin(0.8 * i * pi))
self.star_path.closeSubpath()
self.star_path.setFillRule(Qt.WindingFill)
gradient = QLinearGradient(0, 0, 0, 100)
gradient.setColorAt(0.0, self.COLOR)
gradient.setColorAt(1.0, self.COLOR)
self. brush = QBrush(gradient)
self.factor = self.SIZE/100.
def sizeHint(self, option, index):
if index.column() != 4:
return QItemDelegate.sizeHint(self, option, index)
num = index.model().data(index, Qt.DisplayRole).toInt()[0]
return QSize(num*(self.SIZE), self.SIZE+4)
def paint(self, painter, option, index):
if index.column() != 4:
return QItemDelegate.paint(self, painter, option, index)
num = index.model().data(index, Qt.DisplayRole).toInt()[0]
def draw_star():
painter.save()
painter.scale(self.factor, self.factor)
painter.translate(50.0, 50.0)
painter.rotate(-20)
painter.translate(-50.0, -50.0)
painter.drawPath(self.star_path)
painter.restore()
painter.save()
try:
if option.state & QStyle.State_Selected:
painter.fillRect(option.rect, option.palette.highlight())
painter.setRenderHint(QPainter.Antialiasing)
y = option.rect.center().y()-self.SIZE/2.
x = option.rect.right() - self.SIZE
painter.setPen(self.PEN)
painter.setBrush(self.brush)
painter.translate(x, y)
for i in range(num):
draw_star()
painter.translate(-self.SIZE, 0)
except Exception, e:
traceback.print_exc(e)
painter.restore()
def createEditor(self, parent, option, index):
if index.column() != 4:
return QItemDelegate.createEditor(self, parent, option, index)
editor = QSpinBox(parent)
editor.setSuffix(" stars")
editor.setMinimum(0)
editor.setMaximum(5)
editor.installEventFilter(self)
return editor
def setEditorData(self, editor, index):
if index.column() != 4:
return QItemDelegate.setEditorData(self, editor, index)
val = index.model()._data[index.row()]["rating"]
if not val: val = 0
editor.setValue(val)
def setModelData(self, editor, model, index):
if index.column() != 4:
return QItemDelegate.setModelData(self, editor, model, index)
editor.interpretText()
index.model().setData(index, QVariant(editor.value()), Qt.EditRole)
def updateEditorGeometry(self, editor, option, index):
if index.column() != 4:
return QItemDelegate.updateEditorGeometry(self, editor, option, index)
editor.setGeometry(option.rect)
class LibraryBooksModel(QAbstractTableModel):
FIELDS = ["id", "title", "authors", "size", "date", "rating", "publisher", "tags"]
TIME_READ_FMT = "%Y-%m-%d %H:%M:%S"
def __init__(self, parent):
QAbstractTableModel.__init__(self, parent)
self.db = None
self._data = None
self._orig_data = None
self.image_file = None
def rowCount(self, parent): return len(self._data)
def columnCount(self, parent): return len(self.FIELDS)-2
def setData(self, index, value, role):
done = False
if role == Qt.EditRole:
row = index.row()
id = self._data[row]["id"]
col = index.column()
val = str(value.toString())
if col == 0: col = "title"
elif col == 1: col = "authors"
elif col == 2: return False
elif col == 3: return False
elif col == 4:
col, val = "rating", int(value.toInt()[0])
if val < 0: val =0
if val > 5: val = 5
elif col == 5: col = "publisher"
else: return False
self.db.set_metadata_item(id, col, val)
self._data[row][col] = val
self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), index, index)
for i in range(len(self._orig_data)):
if self._orig_data[i]["id"] == self._data[row]["id"]:
self._orig_data[i][col] = self._data[row][col]
break
done = True
return done
def flags(self, index):
flags = Qt.ItemIsSelectable | Qt.ItemIsEnabled
col = index.column()
if col not in [2,3]:
flags |= Qt.ItemIsEditable
return flags
def set_data(self, db):
self.emit(SIGNAL("layoutAboutToBeChanged()"))
self.db = db
self._data = self.db.get_table(self.FIELDS)
self._orig_data = self._data
self.sort(0, Qt.DescendingOrder)
def headerData(self, section, orientation, role):
if role != Qt.DisplayRole:
return NONE
text = ""
if orientation == Qt.Horizontal:
if section == 0: text = "Title"
elif section == 1: text = "Author(s)"
elif section == 2: text = "Size"
elif section == 3: text = "Date"
elif section == 4: text = "Rating"
elif section == 5: text = "Publisher"
return QVariant(self.trUtf8(text))
else: return QVariant(str(1+section))
def info(self, row):
row = self._data[row]
cover = self.db.get_cover(row["id"])
exts = ",".join(self.db.get_extensions(row["id"]))
if not cover:
cover = DEFAULT_BOOK_COVER
self.image_file = None
else:
pix = QPixmap()
self.image_file = NamedTemporaryFile()
self.image_file.write(cover)
self.image_file.flush()
pix.loadFromData(cover, "", Qt.AutoColor)
if not pix.isNull(): cover = pix.scaledToHeight(COVER_HEIGHT, Qt.SmoothTransformation)
else: self.image_file, cover = None, DEFAULT_BOOK_COVER
return row["title"], row["authors"], human_readable(int(row["size"])), exts, cover
def id_from_index(self, index): return self._data[index.row()]["id"]
def refresh_row(self, row):
self._data[row] = self.db.get_row_by_id(self._data[row]["id"], self.FIELDS)
for i in range(len(self._orig_data)):
if self._orig_data[i]["id"] == self._data[row]["id"]:
self._orig_data[i:i+1] = self._data[row]
break
self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), self.index(row, 0), self.index(row, self.columnCount(0)-1))
def data(self, index, role):
if role == Qt.DisplayRole or role == Qt.EditRole:
row, col = index.row(), index.column()
text = None
row = self._data[row]
if col == 4:
r = row["rating"] if row["rating"] else 0
if r < 0: r= 0
if r > 5: r=5
return QVariant(r)
if col == 0: text = wrap(row["title"], width=25)
elif col == 1:
au = row["authors"]
if au : text = wrap(re.sub("&", "\n", au), width=25)
elif col == 2: text = human_readable(row["size"])
elif col == 3: text = time.strftime(TIME_WRITE_FMT, time.strptime(row["date"], self.TIME_READ_FMT))
elif col == 5:
pub = row["publisher"]
if pub: text = wrap(pub, 20)
if text == None: text = "Unknown"
return QVariant(text)
elif role == Qt.TextAlignmentRole and index.column() in [2,3,4]:
return QVariant(Qt.AlignRight)
return NONE
def sort(self, col, order):
descending = order != Qt.AscendingOrder
def getter(key, func): return lambda x : func(itemgetter(key)(x))
if col == 0: key, func = "title", string.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 == 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 == 5: key, func = "publisher", lambda x : x.lower() if x else ""
self.emit(SIGNAL("layoutAboutToBeChanged()"))
self._data.sort(key=getter(key, func))
if descending: self._data.reverse()
self.emit(SIGNAL("layoutChanged()"))
self.emit(SIGNAL("sorted()"))
def search(self, query):
def query_in(book, q):
au = book["authors"]
if not au : au = "unknown"
pub = book["publisher"]
if not pub : pub = "unknown"
return q in book["title"].lower() or q in au.lower() or q in pub.lower()
queries = unicode(query, 'utf-8').lower().split()
self.emit(SIGNAL("layoutAboutToBeChanged()"))
self._data = []
for book in self._orig_data:
match = True
for q in queries:
if query_in(book, q) : continue
else:
match = False
break
if match: self._data.append(book)
self.emit(SIGNAL("layoutChanged()"))
self.emit(SIGNAL("searched()"))
def delete(self, indices):
if len(indices): self.emit(SIGNAL("layoutAboutToBeChanged()"))
items = [ self._data[index.row()] for index in indices ]
for item in items:
id = item["id"]
try:
self._data.remove(item)
except ValueError: continue
self.db.delete_by_id(id)
for x in self._orig_data:
if x["id"] == id: self._orig_data.remove(x)
self.emit(SIGNAL("layoutChanged()"))
self.emit(SIGNAL("deleted()"))
self.db.commit()
def add_book(self, path):
""" Must call search and sort after this """
id = self.db.add_book(path)
self._orig_data.append(self.db.get_row_by_id(id, self.FIELDS))
class DeviceBooksModel(QAbstractTableModel):
def __init__(self, parent):
QAbstractTableModel.__init__(self, parent)
self._data = []
self._orig_data = []
def set_data(self, book_list):
self.emit(SIGNAL("layoutAboutToBeChanged()"))
self._data = book_list
self._orig_data = book_list
def rowCount(self, parent): return len(self._data)
def columnCount(self, parent): return 4
def headerData(self, section, orientation, role):
if role != Qt.DisplayRole:
return NONE
text = ""
if orientation == Qt.Horizontal:
if section == 0: text = "Title"
elif section == 1: text = "Author(s)"
elif section == 2: text = "Size"
elif section == 3: text = "Date"
return QVariant(self.trUtf8(text))
else: return QVariant(str(1+section))
def data(self, index, role):
if role == Qt.DisplayRole:
row, col = index.row(), index.column()
book = self._data[row]
if col == 0: text = wrap(book.title, width=40)
elif col == 1: text = re.sub("&\s*","\n", book.author)
elif col == 2: text = human_readable(book.size)
elif col == 3: text = time.strftime(TIME_WRITE_FMT, book.datetime)
return QVariant(text)
elif role == Qt.TextAlignmentRole and index.column() in [2,3]:
return QVariant(Qt.AlignRight)
return NONE
def info(self, row):
row = self._data[row]
try:
cover = row.thumbnail
pix = QPixmap()
self.image_file = NamedTemporaryFile()
self.image_file.write(cover)
self.image_file.flush()
pix.loadFromData(cover, "", Qt.AutoColor)
cover = pix.scaledToHeight(COVER_HEIGHT, Qt.SmoothTransformation)
except Exception, e:
self.image_file = None
cover = DEFAULT_BOOK_COVER
return row.title, row.author, human_readable(row.size), row.mime, cover
def sort(self, col, order):
def getter(key, func): return lambda x : func(attrgetter(key)(x))
if col == 0: key, func = "title", string.lower
if col == 1: key, func = "author", lambda x : x.split()[-1:][0].lower()
if col == 2: key, func = "size", int
if col == 3: key, func = "datetime", lambda x: x
descending = order != Qt.AscendingOrder
self.emit(SIGNAL("layoutAboutToBeChanged()"))
self._data.sort(key=getter(key, func))
if descending: self._data.reverse()
self.emit(SIGNAL("layoutChanged()"))
self.emit(SIGNAL("sorted()"))
def search(self, query):
queries = unicode(query, 'utf-8').lower().split()
self.emit(SIGNAL("layoutAboutToBeChanged()"))
self._data = []
for book in self._orig_data:
match = True
for q in queries:
if q in book.title.lower() or q in book.author.lower(): continue
else:
match = False
break
if match: self._data.append(book)
self.emit(SIGNAL("layoutChanged()"))
self.emit(SIGNAL("searched()"))
def delete_by_path(self, path):
self.emit(SIGNAL("layoutAboutToBeChanged()"))
index = -1
for book in self._data:
if path in book["path"]:
self._data.remove(book)
break
for book in self._orig_data:
if path in book["path"]:
self._orig_data.remove(book)
break
self.emit(SIGNAL("layoutChanged()"))
self.emit(SIGNAL("deleted()"))
def path(self, index): return self._data[index.row()].path
def title(self, index): return self._data[index.row()].title
Ui_MainWindow = import_ui("main.ui")
class MainWindow(QObject, Ui_MainWindow):
@ -426,17 +38,19 @@ class MainWindow(QObject, Ui_MainWindow):
self.book_cover.hide(), self.book_info.hide()
if yes:
self.device_view.show(), self.library_view.hide()
self.book_cover.setAcceptDrops(False)
self.current_view = self.device_view
else:
self.device_view.hide(), self.library_view.show()
self.book_cover.setAcceptDrops(True)
self.current_view = self.library_view
self.current_view.sortByColumn(3, Qt.DescendingOrder)
def tree_clicked(self, item, col):
if item:
text = str(item.text(0))
if text == "Books": text = str(item.parent().text(0))
def tree_clicked(self, index):
if index.isValid():
text = (index.data(Qt.DisplayRole).toString())
if "Books" in text: text = str(index.parent().data(Qt.DisplayRole).toString())
if "Library" in text:
self.show_device(False)
elif "SONY Reader" in text:
@ -466,6 +80,7 @@ class MainWindow(QObject, Ui_MainWindow):
def show_book(self, current, previous):
title, author, size, mime, thumbnail = current.model().info(current.row())
self.book_info.setText(self.BOOK_TEMPLATE.arg(title).arg(size).arg(author).arg(mime))
if not thumbnail: thumbnail = DEFAULT_BOOK_COVER
self.book_cover.setPixmap(thumbnail)
try:
name = os.path.abspath(current.model().image_file.name)
@ -475,11 +90,6 @@ class MainWindow(QObject, Ui_MainWindow):
self.book_info.show()
def list_context_event(self, event):
print "TODO:"
def delete(self, action):
count = str(len(self.current_view.selectionModel().selectedRows()))
ret = QMessageBox.question(self.window, self.trUtf8("SONY Reader - confirm"), self.trUtf8("Are you sure you want to <b>permanently delete</b> these ") +count+self.trUtf8(" items?"), QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes)
@ -574,7 +184,14 @@ class MainWindow(QObject, Ui_MainWindow):
def show_error(self, e, msg):
QErrorMessage(self.window).showMessage(msg+"<br><b>Error: </b>"+str(e)+"<br><br>Traceback:<br>"+traceback.format_exc(e))
QErrorMessage(self.window).showMessage(msg+"<br><b>Error: </b>"+str(e)+"<br><br>"+re.sub("\n","<br>", traceback.format_exc(e)))
def update_cover(self, pix):
if not pix.isNull():
try:
self.library_view.model().update_cover(self.library_view.currentIndex(), pix)
self.book_cover.setPixmap(pix)
except Exception, e: self.show_error(e, "Unable to change cover")
def __init__(self, window, log_packets):
QObject.__init__(self)
@ -593,10 +210,6 @@ class MainWindow(QObject, Ui_MainWindow):
self.library_model.set_data(LibraryDatabase(str(self.database_path)))
self.library_view.setModel(self.library_model)
self.current_view = self.library_view
self.library_view.setSelectionBehavior(QAbstractItemView.SelectRows)
self.library_view.setSortingEnabled(True)
self.library_view.contextMenuEvent = self.list_context_event
self.library_view.setItemDelegate(LibraryDelegate(self.library_view))
QObject.connect(self.library_model, SIGNAL("layoutChanged()"), self.library_view.resizeRowsToContents)
QObject.connect(self.library_view.selectionModel(), SIGNAL("currentChanged(QModelIndex, QModelIndex)"), self.show_book)
QObject.connect(self.search, SIGNAL("textChanged(QString)"), self.library_model.search)
@ -607,49 +220,20 @@ class MainWindow(QObject, Ui_MainWindow):
self.library_view.resizeColumnsToContents()
# Create Device tree
QObject.connect(self.device_tree, SIGNAL("itemClicked ( QTreeWidgetItem *, int )"), self.tree_clicked)
QObject.connect(self.device_tree, SIGNAL("itemActivated ( QTreeWidgetItem *, int )"), self.tree_clicked)
self.device_tree.header().hide()
library = QTreeWidgetItem(self.device_tree, QTreeWidgetItem.Type)
library.setData(0, Qt.DisplayRole, QVariant("Library"))
library.setData(0, Qt.DecorationRole, QVariant(QIcon(":/library")))
books =QTreeWidgetItem(library, QTreeWidgetItem.Type)
books.setData(0, Qt.DisplayRole, QVariant("Books"))
self.device_tree.expandItem(library)
buffer = QTreeWidgetItem(self.device_tree, QTreeWidgetItem.Type)
buffer.setFlags(Qt.ItemFlags())
library = QTreeWidgetItem(self.device_tree, QTreeWidgetItem.Type)
library.setData(0, Qt.DisplayRole, QVariant("SONY Reader"))
library.setData(0, Qt.DecorationRole, QVariant(QIcon(":/reader")))
books =QTreeWidgetItem(library, QTreeWidgetItem.Type)
books.setData(0, Qt.DisplayRole, QVariant("Books"))
self.device_tree.expandItem(library)
buffer = QTreeWidgetItem(self.device_tree, QTreeWidgetItem.Type)
buffer.setFlags(Qt.ItemFlags())
library = QTreeWidgetItem(self.device_tree, QTreeWidgetItem.Type)
library.setData(0, Qt.DisplayRole, QVariant("Storage Card"))
library.setData(0, Qt.DecorationRole, QVariant(QIcon(":/card")))
books =QTreeWidgetItem(library, QTreeWidgetItem.Type)
books.setData(0, Qt.DisplayRole, QVariant("Books"))
self.device_tree.expandItem(library)
self.device_tree.reader = self.device_tree.topLevelItem(2)
self.device_tree.card = self.device_tree.topLevelItem(4)
def hider(i):
def do(s, x): s.topLevelItem(i).setHidden(x), s.topLevelItem(i+1).setHidden(x)
return do
self.device_tree.hide_reader = hider(1)
self.device_tree.hide_card = hider(3)
self.device_tree.hide_reader(self.device_tree, True)
self.device_tree.hide_card(self.device_tree, True)
QObject.connect(self.device_tree, SIGNAL("activated(QModelIndex)"), self.tree_clicked)
QObject.connect(self.device_tree, SIGNAL("clicked(QModelIndex)"), self.tree_clicked)
model = DeviceModel(self.device_tree)
self.device_tree.setModel(model)
self.device_tree.expand(model.indexFromItem(model.library))
self.device_tree.expand(model.indexFromItem(model.reader))
self.device_tree.expand(model.indexFromItem(model.card))
self.device_tree.hide_reader(True)
self.device_tree.hide_card(True)
# Create Device Book list
self.reader_model = DeviceBooksModel(window)
self.card_model = DeviceBooksModel(window)
self.device_view.setModel(self.reader_model)
self.device_view.setSelectionBehavior(QAbstractItemView.SelectRows)
self.device_view.setSortingEnabled(True)
self.device_view.contextMenuEvent = self.list_context_event
QObject.connect(self.device_view.selectionModel(), SIGNAL("currentChanged(QModelIndex, QModelIndex)"), self.show_book)
for model in (self.reader_model, self. card_model):
QObject.connect(model, SIGNAL("layoutChanged()"), self.device_view.resizeRowsToContents)
@ -670,6 +254,9 @@ class MainWindow(QObject, Ui_MainWindow):
QObject.connect(self.action_del, SIGNAL("triggered(bool)"), self.delete)
QObject.connect(self.action_edit, SIGNAL("triggered(bool)"), self.edit)
# DnD setup
QObject.connect(self.book_cover, SIGNAL("cover_received(QPixmap)"), self.update_cover)
self.device_detector = self.startTimer(1000)
self.splitter.setStretchFactor(1,100)
self.search.setFocus(Qt.OtherFocusReason)
@ -688,8 +275,8 @@ class MainWindow(QObject, Ui_MainWindow):
""" @todo: only reset stuff if library is not shown """
self.is_connected = False
self.df.setText("SONY Reader: <br><br>Storage card:")
self.device_tree.hide_reader(self.device_tree, True)
self.device_tree.hide_card(self.device_tree, True)
self.device_tree.hide_reader(True)
self.device_tree.hide_card(True)
self.book_cover.hide()
self.book_info.hide()
self.device_view.hide()
@ -718,7 +305,6 @@ class MainWindow(QObject, Ui_MainWindow):
return
except ProtocolError, e:
traceback.print_exc(e)
print >> sys.stderr, "Unable to connect to device. Please try unplugging and reconnecting it"
qFatal("Unable to connect to device. Please try unplugging and reconnecting it")
sc = space[1][1] if space[1][1] else space[2][1]
@ -727,9 +313,9 @@ class MainWindow(QObject, Ui_MainWindow):
if space[1][2] > 0: card = "a:"
elif space[2][2] > 0: card = "b:"
else: card = None
if card: self.device_tree.hide_card(self.device_tree, False)
else: self.device_tree.hide_card(self.device_tree, True)
self.device_tree.hide_reader(self.device_tree, False)
if card: self.device_tree.hide_card(False)
else: self.device_tree.hide_card(True)
self.device_tree.hide_reader(False)
self.status("Loading media list from SONY Reader")
self.reader_model.set_data(self.dev.books())
if card: self.status("Loading media list from Storage Card")

View File

@ -53,7 +53,10 @@
<number>6</number>
</property>
<item>
<widget class="QTreeWidget" name="device_tree" >
<widget class="DeviceView" name="device_tree" >
<property name="acceptDrops" >
<bool>true</bool>
</property>
<property name="dragDropMode" >
<enum>QAbstractItemView::DropOnly</enum>
</property>
@ -69,14 +72,6 @@
<property name="allColumnsShowFocus" >
<bool>true</bool>
</property>
<property name="columnCount" >
<number>1</number>
</property>
<column>
<property name="text" >
<string>1</string>
</property>
</column>
</widget>
</item>
<item>
@ -169,36 +164,60 @@
</layout>
</item>
<item>
<widget class="QTableView" name="device_view" >
<widget class="DeviceBooksView" name="device_view" >
<property name="sizePolicy" >
<sizepolicy>
<hsizetype>7</hsizetype>
<vsizetype>7</vsizetype>
<hsizetype>13</hsizetype>
<vsizetype>13</vsizetype>
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="dragEnabled" >
<bool>true</bool>
</property>
<property name="dragDropOverwriteMode" >
<bool>false</bool>
</property>
<property name="alternatingRowColors" >
<bool>true</bool>
</property>
<property name="selectionBehavior" >
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="showGrid" >
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QTableView" name="library_view" >
<widget class="LibraryBooksView" name="library_view" >
<property name="sizePolicy" >
<sizepolicy>
<hsizetype>7</hsizetype>
<vsizetype>7</vsizetype>
<hsizetype>13</hsizetype>
<vsizetype>13</vsizetype>
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="acceptDrops" >
<bool>true</bool>
</property>
<property name="dragEnabled" >
<bool>true</bool>
</property>
<property name="dragDropOverwriteMode" >
<bool>false</bool>
</property>
<property name="dragDropMode" >
<enum>QAbstractItemView::DragDrop</enum>
</property>
<property name="alternatingRowColors" >
<bool>true</bool>
</property>
<property name="selectionBehavior" >
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="showGrid" >
<bool>false</bool>
</property>
@ -215,13 +234,16 @@
<number>6</number>
</property>
<item>
<widget class="QLabel" name="book_cover" >
<widget class="CoverDisplay" name="book_cover" >
<property name="maximumSize" >
<size>
<width>60</width>
<height>80</height>
</size>
</property>
<property name="acceptDrops" >
<bool>true</bool>
</property>
<property name="text" >
<string/>
</property>
@ -325,6 +347,28 @@
</property>
</action>
</widget>
<customwidgets>
<customwidget>
<class>CoverDisplay</class>
<extends>QLabel</extends>
<header>widgets.h</header>
</customwidget>
<customwidget>
<class>LibraryBooksView</class>
<extends>QTableView</extends>
<header>widgets.h</header>
</customwidget>
<customwidget>
<class>DeviceView</class>
<extends>QTreeView</extends>
<header>widgets.h</header>
</customwidget>
<customwidget>
<class>DeviceBooksView</class>
<extends>QTableView</extends>
<header>widgets.h</header>
</customwidget>
</customwidgets>
<resources>
<include location="images.qrc" />
</resources>

642
libprs500/gui/widgets.py Normal file
View File

@ -0,0 +1,642 @@
## Copyright (C) 2006 Kovid Goyal kovid@kovidgoyal.net
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU General Public License for more details.
##
## 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.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
from PyQt4 import QtGui, QtCore
from PyQt4.QtCore import Qt, SIGNAL
from PyQt4.Qt import QApplication, QString, QFont, QStandardItemModel, QStandardItem, QVariant, QAbstractTableModel, QTableView, QTreeView, QLabel,\
QAbstractItemView, QPixmap, QIcon, QSize, QMessageBox, QSettings, QFileDialog, QErrorMessage, QDialog, QSpinBox, QPoint, QTemporaryFile, QDir,\
QPainterPath, QItemDelegate, QPainter, QPen, QColor, QLinearGradient, QBrush, QStyle,\
QStringList, QByteArray, QBuffer, QMimeData, QTextStream, QIODevice, QDrag,\
qDebug, qFatal, qWarning, qCritical
import re, os, string, textwrap, time, traceback
from operator import itemgetter, attrgetter
from socket import gethostname
from urlparse import urlparse, urlunparse
from urllib import quote, unquote
from math import sin, cos, pi
from libprs500 import TEMPORARY_FILENAME_TEMPLATE as TFT
from libprs500.lrf.meta import LRFMetaFile
NONE = QVariant()
TIME_WRITE_FMT = "%d %b %Y"
COVER_HEIGHT = 80
def human_readable(size):
""" Convert a size in bytes into a human readable form """
if size < 1024: divisor, suffix = 1, "B"
elif size < 1024*1024: 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)
if size.find(".") > -1: size = size[:size.find(".")+2]
return size + " " + suffix
def wrap(s, width=20):
return textwrap.fill(str(s), width)
def get_r_ok_files(event):
""" @type event: QDropEvent """
files = []
md = event.mimeData()
if md.hasFormat("text/uri-list"):
candidates = bytes_to_string(md.data("text/uri-list")).split("\n")
print candidates
for path in candidates:
path = os.path.abspath(re.sub(r"^file://", "", path))
if os.path.isfile(path) and os.access(path, os.R_OK): files.append(path)
return files
def bytes_to_string(qba):
""" @type qba: QByteArray """
return unicode(QString.fromUtf8(qba.data())).strip()
class FileDragAndDrop(object):
_drag_start_position = QPoint()
@classmethod
def _bytes_to_string(cls, qba):
""" @type qba: QByteArray """
return unicode(QString.fromUtf8(qba.data())).strip()
@classmethod
def _get_r_ok_files(cls, event):
files = []
md = event.mimeData()
if md.hasFormat("text/uri-list"):
candidates = cls._bytes_to_string(md.data("text/uri-list")).split()
for url in candidates:
o = urlparse(url)
if o.scheme != 'file':
qWarning(o.scheme + " not supported in drop events")
continue
path = unquote(o.path)
if not os.access(path, os.R_OK):
qWarning("You do not have read permission for: " + path)
continue
if os.path.isdir(path):
root, dirs, files2 = os.walk(path)
for file in files2:
path = root + file
if os.access(path, os.R_OK): files.append(path)
else: files.append(path)
return files
def mousePressEvent(self, event):
if event.button == Qt.LeftButton:
self._drag_start_position = event.pos()
def mouseMoveEvent(self, event):
if event.buttons() & Qt.LeftButton != Qt.LeftButton: return
if (event.pos() - self._drag_start_position).manhattanLength() < QApplication.startDragDistance(): return
self.start_drag(self._drag_start_position)
def start_drag(self, pos): pass
def dragEnterEvent(self, event):
if event.mimeData().hasFormat("text/uri-list"): event.acceptProposedAction()
def dragMoveEvent(self, event):
event.acceptProposedAction()
def dropEvent(self, event):
files = self._get_r_ok_files(event)
if files:
if self.files_dropped(files): event.acceptProposedAction()
def files_dropped(self, files): return False
def drag_object(self, extensions):
if extensions:
drag = QDrag(self)
mime_data = QMimeData()
self._dragged_files, urls = [], []
for ext in extensions:
f = TemporaryFile(ext=ext)
f.open()
urls.append(urlunparse(('file', quote(gethostname()), quote(str(f.fileName())), '','','')))
self._dragged_files.append(f)
mime_data.setData("text/uri-list", QByteArray("\n".join(urls)))
user = None
try: user = os.environ['USER']
except: pass
if user: mime_data.setData("text/x-xdnd-username", QByteArray(user))
drag.setMimeData(mime_data)
return drag, self._dragged_files
class TemporaryFile(QTemporaryFile):
def __init__(self, ext=""):
if ext: ext = "." + ext
path = QDir.tempPath() + "/" + TFT + "_XXXXXX"+ext
QTemporaryFile.__init__(self, path)
class CoverDisplay(FileDragAndDrop, QLabel):
def files_dropped(self, files):
pix = QPixmap()
for file in files:
pix = QPixmap(file)
if not pix.isNull(): break
if not pix.isNull():
self.emit(SIGNAL("cover_received(QPixmap)"), pix)
return True
def start_drag(self, event):
drag, files = self.drag_object(["jpeg"])
if drag and files:
file = files[0]
drag.setPixmap(self.pixmap())
self.pixmap().save(file)
file.close()
drag.start(Qt.MoveAction)
class DeviceView(QTreeView):
def __init__(self, parent):
QTreeView.__init__(self, parent)
self.header().hide()
self.setIconSize(QSize(32,32))
def hide_reader(self, x):
self.setRowHidden(2, self.model().indexFromItem(self.model().invisibleRootItem()), x)
def hide_card(self, x):
self.setRowHidden(4, self.model().indexFromItem(self.model().invisibleRootItem()), x)
class DeviceBooksView(QTableView):
def __init__(self, parent):
QTableView.__init__(self, parent)
self.setSelectionBehavior(QAbstractItemView.SelectRows)
self.setSortingEnabled(True)
class LibraryBooksView(QTableView):
def __init__(self, parent):
QTableView.__init__(self, parent)
self.setSelectionBehavior(QAbstractItemView.SelectRows)
self.setSortingEnabled(True)
self.setItemDelegate(LibraryDelegate(self))
def get_verified_path(self, mime_data):
if mime_data.hasFormat("text/plain"):
text = unicode(mime_data.text())
text = re.sub(r"^file://", "", text)
if os.access(os.path.abspath(text), os.R_OK): return text
return None
def dragEnterEvent(self, event):
if self.get_verified_path(event.mimeData()): event.acceptProposedAction()
def dragMoveEvent(self, event): event.acceptProposedAction()
def dropEvent(self, event):
path = self.get_verified_path(event.mimeData())
if path:
if self.model().handle_drop(path, self.indexAt(event.pos())): event.acceptProposedAction()
class LibraryDelegate(QItemDelegate):
COLOR = QColor("blue")
SIZE = 16
PEN = QPen(COLOR, 1, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)
def __init__(self, parent):
QItemDelegate.__init__(self, parent)
self.star_path = QPainterPath()
self.star_path.moveTo(90, 50)
for i in range(1, 5):
self.star_path.lineTo(50 + 40 * cos(0.8 * i * pi), 50 + 40 * sin(0.8 * i * pi))
self.star_path.closeSubpath()
self.star_path.setFillRule(Qt.WindingFill)
gradient = QLinearGradient(0, 0, 0, 100)
gradient.setColorAt(0.0, self.COLOR)
gradient.setColorAt(1.0, self.COLOR)
self. brush = QBrush(gradient)
self.factor = self.SIZE/100.
def sizeHint(self, option, index):
if index.column() != 4:
return QItemDelegate.sizeHint(self, option, index)
num = index.model().data(index, Qt.DisplayRole).toInt()[0]
return QSize(num*(self.SIZE), self.SIZE+4)
def paint(self, painter, option, index):
if index.column() != 4:
return QItemDelegate.paint(self, painter, option, index)
num = index.model().data(index, Qt.DisplayRole).toInt()[0]
def draw_star():
painter.save()
painter.scale(self.factor, self.factor)
painter.translate(50.0, 50.0)
painter.rotate(-20)
painter.translate(-50.0, -50.0)
painter.drawPath(self.star_path)
painter.restore()
painter.save()
try:
if option.state & QStyle.State_Selected:
painter.fillRect(option.rect, option.palette.highlight())
painter.setRenderHint(QPainter.Antialiasing)
y = option.rect.center().y()-self.SIZE/2.
x = option.rect.right() - self.SIZE
painter.setPen(self.PEN)
painter.setBrush(self.brush)
painter.translate(x, y)
for i in range(num):
draw_star()
painter.translate(-self.SIZE, 0)
except Exception, e:
traceback.print_exc(e)
painter.restore()
def createEditor(self, parent, option, index):
if index.column() != 4:
return QItemDelegate.createEditor(self, parent, option, index)
editor = QSpinBox(parent)
editor.setSuffix(" stars")
editor.setMinimum(0)
editor.setMaximum(5)
editor.installEventFilter(self)
return editor
def setEditorData(self, editor, index):
if index.column() != 4:
return QItemDelegate.setEditorData(self, editor, index)
val = index.model()._data[index.row()]["rating"]
if not val: val = 0
editor.setValue(val)
def setModelData(self, editor, model, index):
if index.column() != 4:
return QItemDelegate.setModelData(self, editor, model, index)
editor.interpretText()
index.model().setData(index, QVariant(editor.value()), Qt.EditRole)
def updateEditorGeometry(self, editor, option, index):
if index.column() != 4:
return QItemDelegate.updateEditorGeometry(self, editor, option, index)
editor.setGeometry(option.rect)
class LibraryBooksModel(QAbstractTableModel):
FIELDS = ["id", "title", "authors", "size", "date", "rating", "publisher", "tags"]
TIME_READ_FMT = "%Y-%m-%d %H:%M:%S"
def __init__(self, parent):
QAbstractTableModel.__init__(self, parent)
self.db = None
self._data = None
self._orig_data = None
self.image_file = None
def update_cover(self, index, pix):
id = self.id_from_index(index)
qb = QBuffer()
qb.open(QBuffer.ReadWrite);
pix.save(qb, "JPG")
data = str(qb.data())
qb.close()
self.db.update_cover(id, data)
def handle_drop(self, path, index):
print "249", path, index.row()
if index.isValid():
f = open(path, "rb")
title = os.path.basename(path)
ext = title[title.rfind(".")+1:].lower() if "." in title > -1 else None
self.db.add_format(self.id_from_index(index), ext, f)
f.close()
else:
pass # TODO: emit book add signal
return True
def rowCount(self, parent): return len(self._data)
def columnCount(self, parent): return len(self.FIELDS)-2
def setData(self, index, value, role):
done = False
if role == Qt.EditRole:
row = index.row()
id = self._data[row]["id"]
col = index.column()
val = str(value.toString())
if col == 0: col = "title"
elif col == 1: col = "authors"
elif col == 2: return False
elif col == 3: return False
elif col == 4:
col, val = "rating", int(value.toInt()[0])
if val < 0: val =0
if val > 5: val = 5
elif col == 5: col = "publisher"
else: return False
self.db.set_metadata_item(id, col, val)
self._data[row][col] = val
self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), index, index)
for i in range(len(self._orig_data)):
if self._orig_data[i]["id"] == self._data[row]["id"]:
self._orig_data[i][col] = self._data[row][col]
break
done = True
return done
def flags(self, index):
flags = QAbstractTableModel.flags(self, index)
if index.isValid():
flags |= Qt.ItemIsDragEnabled | Qt.ItemIsDropEnabled
if index.column() not in [2,3]: flags |= Qt.ItemIsEditable
else: flags |= Qt.ItemIsDropEnabled
return flags
def set_data(self, db):
self.db = db
self._data = self.db.get_table(self.FIELDS)
self._orig_data = self._data
self.sort(0, Qt.DescendingOrder)
self.reset()
def headerData(self, section, orientation, role):
if role != Qt.DisplayRole:
return NONE
text = ""
if orientation == Qt.Horizontal:
if section == 0: text = "Title"
elif section == 1: text = "Author(s)"
elif section == 2: text = "Size"
elif section == 3: text = "Date"
elif section == 4: text = "Rating"
elif section == 5: text = "Publisher"
return QVariant(self.trUtf8(text))
else: return QVariant(str(1+section))
def info(self, row):
row = self._data[row]
cover = self.db.get_cover(row["id"])
exts = ",".join(self.db.get_extensions(row["id"]))
if cover:
pix = QPixmap()
pix.loadFromData(cover, "", Qt.AutoColor)
cover = None if pix.isNull() else pix
au = row["authors"]
if not au: au = "Unknown"
return row["title"], au, human_readable(int(row["size"])), exts, cover
def id_from_index(self, index): return self._data[index.row()]["id"]
def refresh_row(self, row):
self._data[row] = self.db.get_row_by_id(self._data[row]["id"], self.FIELDS)
for i in range(len(self._orig_data)):
if self._orig_data[i]["id"] == self._data[row]["id"]:
self._orig_data[i:i+1] = self._data[row]
break
self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), self.index(row, 0), self.index(row, self.columnCount(0)-1))
def data(self, index, role):
if role == Qt.DisplayRole or role == Qt.EditRole:
row, col = index.row(), index.column()
text = None
row = self._data[row]
if col == 4:
r = row["rating"] if row["rating"] else 0
if r < 0: r= 0
if r > 5: r=5
return QVariant(r)
if col == 0: text = wrap(row["title"], width=25)
elif col == 1:
au = row["authors"]
if au : text = wrap(re.sub("&", "\n", au), width=25)
elif col == 2: text = human_readable(row["size"])
elif col == 3: text = time.strftime(TIME_WRITE_FMT, time.strptime(row["date"], self.TIME_READ_FMT))
elif col == 5:
pub = row["publisher"]
if pub: text = wrap(pub, 20)
if text == None: text = "Unknown"
return QVariant(text)
elif role == Qt.TextAlignmentRole and index.column() in [2,3,4]:
return QVariant(Qt.AlignRight)
return NONE
def sort(self, col, order):
descending = order != Qt.AscendingOrder
def getter(key, func): return lambda x : func(itemgetter(key)(x))
if col == 0: key, func = "title", string.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 == 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 == 5: key, func = "publisher", lambda x : x.lower() if x else ""
self.emit(SIGNAL("layoutAboutToBeChanged()"))
self._data.sort(key=getter(key, func))
if descending: self._data.reverse()
self.emit(SIGNAL("layoutChanged()"))
self.emit(SIGNAL("sorted()"))
def search(self, query):
def query_in(book, q):
au = book["authors"]
if not au : au = "unknown"
pub = book["publisher"]
if not pub : pub = "unknown"
return q in book["title"].lower() or q in au.lower() or q in pub.lower()
queries = unicode(query, 'utf-8').lower().split()
self.emit(SIGNAL("layoutAboutToBeChanged()"))
self._data = []
for book in self._orig_data:
match = True
for q in queries:
if query_in(book, q) : continue
else:
match = False
break
if match: self._data.append(book)
self.emit(SIGNAL("layoutChanged()"))
self.emit(SIGNAL("searched()"))
def delete(self, indices):
if len(indices): self.emit(SIGNAL("layoutAboutToBeChanged()"))
items = [ self._data[index.row()] for index in indices ]
for item in items:
id = item["id"]
try:
self._data.remove(item)
except ValueError: continue
self.db.delete_by_id(id)
for x in self._orig_data:
if x["id"] == id: self._orig_data.remove(x)
self.emit(SIGNAL("layoutChanged()"))
self.emit(SIGNAL("deleted()"))
self.db.commit()
def add_book(self, path):
""" Must call search and sort after this """
id = self.db.add_book(path)
self._orig_data.append(self.db.get_row_by_id(id, self.FIELDS))
def mimeTypes(self):
s = QStringList()
s << "application/vnd.text.list" # Title, authors
s << "image/jpeg" # 60x80 thumbnail
s << "application/x-sony-bbeb"
s << "application/pdf"
s << "text/rtf"
s << "text/plain"
return s
def mimeData(self, indices):
mime_data = QMimeData()
encoded_data = QByteArray()
rows = []
for index in indices:
if index.isValid():
row = index.row()
if row in rows: continue
title, authors, size, exts, cover = self.info(row)
encoded_data.append(title)
encoded_data.append(authors)
rows.append(row)
mime_data.setData("application/vnd.text.list", encoded_data)
return mime_data
class DeviceBooksModel(QAbstractTableModel):
def __init__(self, parent):
QAbstractTableModel.__init__(self, parent)
self._data = []
self._orig_data = []
def set_data(self, book_list):
self._data = book_list
self._orig_data = book_list
self.reset()
def rowCount(self, parent): return len(self._data)
def columnCount(self, parent): return 4
def headerData(self, section, orientation, role):
if role != Qt.DisplayRole:
return NONE
text = ""
if orientation == Qt.Horizontal:
if section == 0: text = "Title"
elif section == 1: text = "Author(s)"
elif section == 2: text = "Size"
elif section == 3: text = "Date"
return QVariant(self.trUtf8(text))
else: return QVariant(str(1+section))
def data(self, index, role):
if role == Qt.DisplayRole:
row, col = index.row(), index.column()
book = self._data[row]
if col == 0: text = wrap(book.title, width=40)
elif col == 1: text = re.sub("&\s*","\n", book.author)
elif col == 2: text = human_readable(book.size)
elif col == 3: text = time.strftime(TIME_WRITE_FMT, book.datetime)
return QVariant(text)
elif role == Qt.TextAlignmentRole and index.column() in [2,3]:
return QVariant(Qt.AlignRight)
return NONE
def info(self, row):
row = self._data[row]
cover = None
try:
cover = row.thumbnail
pix = QPixmap()
pix.loadFromData(cover, "", Qt.AutoColor)
cover = None if pix.isNull() else pix
except:
traceback.print_exc()
au = row.author if row.author else "Unknown"
return row.title, au, human_readable(row.size), row.mime, cover
def sort(self, col, order):
def getter(key, func): return lambda x : func(attrgetter(key)(x))
if col == 0: key, func = "title", string.lower
if col == 1: key, func = "author", lambda x : x.split()[-1:][0].lower()
if col == 2: key, func = "size", int
if col == 3: key, func = "datetime", lambda x: x
descending = order != Qt.AscendingOrder
self.emit(SIGNAL("layoutAboutToBeChanged()"))
self._data.sort(key=getter(key, func))
if descending: self._data.reverse()
self.emit(SIGNAL("layoutChanged()"))
self.emit(SIGNAL("sorted()"))
def search(self, query):
queries = unicode(query, 'utf-8').lower().split()
self.emit(SIGNAL("layoutAboutToBeChanged()"))
self._data = []
for book in self._orig_data:
match = True
for q in queries:
if q in book.title.lower() or q in book.author.lower(): continue
else:
match = False
break
if match: self._data.append(book)
self.emit(SIGNAL("layoutChanged()"))
self.emit(SIGNAL("searched()"))
def delete_by_path(self, path):
self.emit(SIGNAL("layoutAboutToBeChanged()"))
index = -1
for book in self._data:
if path in book["path"]:
self._data.remove(book)
break
for book in self._orig_data:
if path in book["path"]:
self._orig_data.remove(book)
break
self.emit(SIGNAL("layoutChanged()"))
self.emit(SIGNAL("deleted()"))
def path(self, index): return self._data[index.row()].path
def title(self, index): return self._data[index.row()].title
class DeviceModel(QStandardItemModel):
def __init__(self, parent):
QStandardItemModel.__init__(self, parent)
root = self.invisibleRootItem()
font = QFont()
font.setBold(True)
self.library = QStandardItem(QIcon(":/library"), QString("Library"))
self.reader = QStandardItem(QIcon(":/reader"), "SONY Reader")
self.card = QStandardItem(QIcon(":/card"), "Storage Card")
self.library.setFont(font)
self.reader.setFont(font)
self.card.setFont(font)
self.blank = QStandardItem("")
self.blank.setFlags(Qt.ItemFlags())
root.appendRow(self.library)
root.appendRow(self.blank)
root.appendRow(self.reader)
root.appendRow(self.blank.clone())
root.appendRow(self.card)
self.library.appendRow(QStandardItem("Books"))
self.reader.appendRow(QStandardItem("Books"))
self.card.appendRow(QStandardItem("Books"))