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")) + + + + +