From 1fba894a89fe1044cec66382e0c115e1dbd68e2c Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 7 Dec 2006 00:38:43 +0000 Subject: [PATCH] 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 --- libprs500/__init__.py | 3 +- libprs500/books.py | 3 + libprs500/communicate.py | 5 +- libprs500/gui/__init__.py | 3 +- libprs500/gui/database.py | 57 +++- libprs500/gui/main.py | 496 +++-------------------------- libprs500/gui/main.ui | 76 ++++- libprs500/gui/widgets.py | 642 ++++++++++++++++++++++++++++++++++++++ 8 files changed, 800 insertions(+), 485 deletions(-) create mode 100644 libprs500/gui/widgets.py diff --git a/libprs500/__init__.py b/libprs500/__init__.py index e217b56bd0..3ceb0a71d9 100644 --- a/libprs500/__init__.py +++ b/libprs500/__init__.py @@ -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 " +TEMPORARY_FILENAME_TEMPLATE = "libprs500_"+__version__+"_temp" diff --git a/libprs500/books.py b/libprs500/books.py index c6fac5c608..dfb9542120 100644 --- a/libprs500/books.py +++ b/libprs500/books.py @@ -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: diff --git a/libprs500/communicate.py b/libprs500/communicate.py index 32a2a41464..d58696ffa6 100755 --- a/libprs500/communicate.py +++ b/libprs500/communicate.py @@ -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: diff --git a/libprs500/gui/__init__.py b/libprs500/gui/__init__.py index 12653c6e7e..419c882827 100644 --- a/libprs500/gui/__init__.py +++ b/libprs500/gui/__init__.py @@ -16,7 +16,8 @@ __docformat__ = "epytext" __author__ = "Kovid Goyal " import pkg_resources, sys, os, StringIO -from PyQt4 import QtCore, QtGui # Needed for classes imported with import_ui +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): diff --git a/libprs500/gui/database.py b/libprs500/gui/database.py index d67f5b63ee..68704b3616 100644 --- a/libprs500/gui/database.py +++ b/libprs500/gui/database.py @@ -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)) + 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 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() - def search(self, query): pass + #if __name__ == "__main__": # lbm = LibraryDatabase("/home/kovid/library.db") diff --git a/libprs500/gui/main.py b/libprs500/gui/main.py index ac300676a4..ec2b4d9041 100644 --- a/libprs500/gui/main.py +++ b/libprs500/gui/main.py @@ -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.current_view = self.device_view + 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 permanently delete these ") +count+self.trUtf8(" items?"), QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes) @@ -574,8 +184,15 @@ class MainWindow(QObject, Ui_MainWindow): def show_error(self, e, msg): - QErrorMessage(self.window).showMessage(msg+"
Error: "+str(e)+"

Traceback:
"+traceback.format_exc(e)) + QErrorMessage(self.window).showMessage(msg+"
Error: "+str(e)+"

"+re.sub("\n","
", 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) Ui_MainWindow.__init__(self) @@ -592,11 +209,7 @@ class MainWindow(QObject, Ui_MainWindow): self.library_model = LibraryBooksModel(window) 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)) + self.current_view = 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:

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") diff --git a/libprs500/gui/main.ui b/libprs500/gui/main.ui index cd4011a208..668dbf1cc5 100644 --- a/libprs500/gui/main.ui +++ b/libprs500/gui/main.ui @@ -53,7 +53,10 @@ 6 - + + + true + QAbstractItemView::DropOnly @@ -69,14 +72,6 @@ true - - 1 - - - - 1 - - @@ -169,36 +164,60 @@ - + - 7 - 7 + 13 + 13 0 0 + + true + + + false + true + + QAbstractItemView::SelectRows + false - + - 7 - 7 + 13 + 13 0 0 + + true + + + true + + + false + + + QAbstractItemView::DragDrop + true + + QAbstractItemView::SelectRows + false @@ -215,13 +234,16 @@ 6 - + 60 80 + + true + @@ -325,6 +347,28 @@ + + + CoverDisplay + QLabel +
widgets.h
+
+ + LibraryBooksView + QTableView +
widgets.h
+
+ + DeviceView + QTreeView +
widgets.h
+
+ + DeviceBooksView + QTableView +
widgets.h
+
+
diff --git a/libprs500/gui/widgets.py b/libprs500/gui/widgets.py new file mode 100644 index 0000000000..400df115db --- /dev/null +++ b/libprs500/gui/widgets.py @@ -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")) + + + + +