mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Reorganized GUI code. Split widget specific code into widgets.py.
Added infrastructure for Drag'nDrop support. Implemented DnD for the cover. Added initial support for updating metadata in LRF files when it is updated for a logical file. This needs testing. Especially worrying are the deprecation warnings about integer overflow in struct.pack when updating the thumbnail
This commit is contained in:
parent
844f4e1e08
commit
1fba894a89
@ -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
|
The public interface of libprs500 is in L{libprs500.communicate}. To use it
|
||||||
>>> from libprs500.communicate import PRS500Device
|
>>> from libprs500.communicate import PRS500Device
|
||||||
>>> dev = PRS500Device()
|
>>> dev = PRS500Device()
|
||||||
>>> dev.open()
|
|
||||||
>>> dev.get_device_information()
|
>>> dev.get_device_information()
|
||||||
('Sony Reader', 'PRS-500/U', '1.0.00.21081', 'application/x-bbeb-book')
|
('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
|
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.
|
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"
|
__version__ = "0.2.1"
|
||||||
__docformat__ = "epytext"
|
__docformat__ = "epytext"
|
||||||
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
|
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
|
||||||
|
TEMPORARY_FILENAME_TEMPLATE = "libprs500_"+__version__+"_temp"
|
||||||
|
@ -67,6 +67,9 @@ class Book(object):
|
|||||||
return self.__repr__()
|
return self.__repr__()
|
||||||
|
|
||||||
class BookList(list):
|
class BookList(list):
|
||||||
|
__getslice__ = None
|
||||||
|
__setslice__ = None
|
||||||
|
|
||||||
def __init__(self, prefix="xs1:", root="/Data/media/", file=None):
|
def __init__(self, prefix="xs1:", root="/Data/media/", file=None):
|
||||||
list.__init__(self)
|
list.__init__(self)
|
||||||
if file:
|
if file:
|
||||||
|
@ -51,6 +51,7 @@ from array import array
|
|||||||
from libprs500.prstypes import *
|
from libprs500.prstypes import *
|
||||||
from libprs500.errors import *
|
from libprs500.errors import *
|
||||||
from libprs500.books import *
|
from libprs500.books import *
|
||||||
|
from libprs500 import __author__ as AUTHOR
|
||||||
|
|
||||||
MINIMUM_COL_WIDTH = 12 #: Minimum width of columns in ls output
|
MINIMUM_COL_WIDTH = 12 #: Minimum width of columns in ls output
|
||||||
_packet_number = 0 #: Keep track of the packet number of packet tracing
|
_packet_number = 0 #: Keep track of the packet number of packet tracing
|
||||||
@ -168,7 +169,7 @@ class PRS500Device(object):
|
|||||||
raise TimeoutError(func.__name__)
|
raise TimeoutError(func.__name__)
|
||||||
elif "Protocol error" in str(e):
|
elif "Protocol error" in str(e):
|
||||||
dev.close()
|
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()
|
dev.close()
|
||||||
raise e
|
raise e
|
||||||
if not kwargs.has_key("end_session") or kwargs["end_session"]:
|
if not kwargs.has_key("end_session") or kwargs["end_session"]:
|
||||||
@ -221,7 +222,7 @@ class PRS500Device(object):
|
|||||||
raise DeviceError()
|
raise DeviceError()
|
||||||
self.handle = self.device.open()
|
self.handle = self.device.open()
|
||||||
self.handle.claimInterface(self.device_descriptor.interface_id)
|
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.")
|
if res.code != 0: raise ProtocolError("Unable to get USB Protocol version.")
|
||||||
version = self._bulk_read(24, data_type=USBProtocolVersion)[0].version
|
version = self._bulk_read(24, data_type=USBProtocolVersion)[0].version
|
||||||
if version not in KNOWN_USB_PROTOCOL_VERSIONS:
|
if version not in KNOWN_USB_PROTOCOL_VERSIONS:
|
||||||
|
@ -17,6 +17,7 @@ __author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
|
|||||||
|
|
||||||
import pkg_resources, sys, os, StringIO
|
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
|
from PyQt4.uic.Compiler import compiler
|
||||||
|
|
||||||
def import_ui(name):
|
def import_ui(name):
|
||||||
|
@ -13,9 +13,11 @@
|
|||||||
## with this program; if not, write to the Free Software Foundation, Inc.,
|
## with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
import sqlite3 as sqlite
|
import sqlite3 as sqlite
|
||||||
import os, os.path, zlib
|
import os, os.path
|
||||||
|
from zlib import compress, decompress
|
||||||
from stat import ST_SIZE
|
from stat import ST_SIZE
|
||||||
from libprs500.lrf.meta import LRFMetaFile
|
from libprs500.lrf.meta import LRFMetaFile
|
||||||
|
from cStringIO import StringIO as cStringIO
|
||||||
|
|
||||||
class LibraryDatabase(object):
|
class LibraryDatabase(object):
|
||||||
|
|
||||||
@ -32,7 +34,7 @@ class LibraryDatabase(object):
|
|||||||
|
|
||||||
def get_cover(self, id):
|
def get_cover(self, id):
|
||||||
raw = self.con.execute("select cover from books_meta where id=?", (id,)).next()["cover"]
|
raw = self.con.execute("select cover from books_meta where id=?", (id,)).next()["cover"]
|
||||||
return zlib.decompress(str(raw)) if raw else None
|
return decompress(str(raw)) if raw else None
|
||||||
|
|
||||||
def get_extensions(self, id):
|
def get_extensions(self, id):
|
||||||
exts = []
|
exts = []
|
||||||
@ -50,8 +52,8 @@ class LibraryDatabase(object):
|
|||||||
title, author, cover, publisher = lrf.title, lrf.author.strip(), lrf.thumbnail, lrf.publisher.strip()
|
title, author, cover, publisher = lrf.title, lrf.author.strip(), lrf.thumbnail, lrf.publisher.strip()
|
||||||
if "unknown" in publisher.lower(): publisher = None
|
if "unknown" in publisher.lower(): publisher = None
|
||||||
if "unknown" in author.lower(): author = None
|
if "unknown" in author.lower(): author = None
|
||||||
file = zlib.compress(open(file).read())
|
file = compress(open(file).read())
|
||||||
if cover: cover = sqlite.Binary(zlib.compress(cover))
|
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))
|
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]
|
id = self.con.execute("select max(id) from books_meta").next()[0]
|
||||||
self.con.execute("insert into books_data values (?,?,?)", (id, ext, sqlite.Binary(file)))
|
self.con.execute("insert into books_data values (?,?,?)", (id, ext, sqlite.Binary(file)))
|
||||||
@ -83,13 +85,28 @@ class LibraryDatabase(object):
|
|||||||
return rows
|
return rows
|
||||||
|
|
||||||
def get_format(self, id, ext):
|
def get_format(self, id, ext):
|
||||||
|
"""
|
||||||
|
Return format C{ext} corresponding to the logical book C{id} or None if the format is unavailable.
|
||||||
|
Format is returned as a string of binary data suitable for C{ file.write} operations.
|
||||||
|
"""
|
||||||
ext = ext.lower()
|
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()
|
try: data = cur.next()
|
||||||
except: pass
|
except: pass
|
||||||
else: return zlib.decompress(str(data["data"]))
|
else: return decompress(str(data["data"]))
|
||||||
|
|
||||||
def add_format(self, id, ext, data):
|
def add_format(self, id, ext, data):
|
||||||
|
"""
|
||||||
|
If data for format ext already exists, it is replaced
|
||||||
|
@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))
|
cur = self.con.execute("select extension from books_data where id=? and extension=?", (id, ext))
|
||||||
present = True
|
present = True
|
||||||
try: cur.next()
|
try: cur.next()
|
||||||
@ -113,15 +130,37 @@ class LibraryDatabase(object):
|
|||||||
if publisher and not len(publisher): publisher = None
|
if publisher and not len(publisher): publisher = None
|
||||||
if tags and not len(tags): tags = None
|
if tags and not len(tags): tags = None
|
||||||
if comments and not len(comments): comments = 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.execute('update books_meta set title=?, authors=?, publisher=?, tags=?, cover=?, comments=?, rating=? where id=?', (title, authors, publisher, tags, cover, comments, rating, id))
|
||||||
self.con.commit()
|
self.con.commit()
|
||||||
|
|
||||||
def set_metadata_item(self, id, col, val):
|
def set_metadata_item(self, id, col, val):
|
||||||
self.con.execute('update books_meta set '+col+'=? where id=?',(val, id))
|
self.con.execute('update books_meta set '+col+'=? where id=?',(val, id))
|
||||||
|
if col in ["authors", "title"]:
|
||||||
|
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()
|
self.con.commit()
|
||||||
|
|
||||||
def search(self, query): pass
|
def update_cover(self, id, cover):
|
||||||
|
data = None
|
||||||
|
if cover: data = sqlite.Binary(compress(cover))
|
||||||
|
self.con.execute('update books_meta set cover=? where id=?', (data, id))
|
||||||
|
lrf = self.get_format(id, "lrf")
|
||||||
|
if lrf:
|
||||||
|
c = cStringIO()
|
||||||
|
c.write(lrf)
|
||||||
|
lrf = LRFMetaFile(c)
|
||||||
|
lrf.thumbnail = cover
|
||||||
|
self.add_format(id, "lrf", c.getvalue())
|
||||||
|
self.commit()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#if __name__ == "__main__":
|
#if __name__ == "__main__":
|
||||||
# lbm = LibraryDatabase("/home/kovid/library.db")
|
# lbm = LibraryDatabase("/home/kovid/library.db")
|
||||||
|
@ -12,410 +12,22 @@
|
|||||||
## You should have received a copy of the GNU General Public License along
|
## You should have received a copy of the GNU General Public License along
|
||||||
## with this program; if not, write to the Free Software Foundation, Inc.,
|
## with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
|
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.communicate import PRS500Device as device
|
||||||
from libprs500.errors import *
|
from libprs500.errors import *
|
||||||
from libprs500.lrf.meta import LRFMetaFile, LRFException
|
from libprs500.lrf.meta import LRFMetaFile, LRFException
|
||||||
from libprs500.gui import import_ui
|
from libprs500.gui import import_ui
|
||||||
|
from libprs500.gui.widgets import LibraryBooksModel, DeviceBooksModel, DeviceModel, human_readable
|
||||||
from database import LibraryDatabase
|
from database import LibraryDatabase
|
||||||
from editbook import EditBookDialog
|
from editbook import EditBookDialog
|
||||||
|
|
||||||
from PyQt4.QtCore import Qt, SIGNAL
|
import sys, re, os, traceback
|
||||||
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
|
|
||||||
|
|
||||||
DEFAULT_BOOK_COVER = None
|
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")
|
Ui_MainWindow = import_ui("main.ui")
|
||||||
class MainWindow(QObject, Ui_MainWindow):
|
class MainWindow(QObject, Ui_MainWindow):
|
||||||
@ -426,17 +38,19 @@ class MainWindow(QObject, Ui_MainWindow):
|
|||||||
self.book_cover.hide(), self.book_info.hide()
|
self.book_cover.hide(), self.book_info.hide()
|
||||||
if yes:
|
if yes:
|
||||||
self.device_view.show(), self.library_view.hide()
|
self.device_view.show(), self.library_view.hide()
|
||||||
|
self.book_cover.setAcceptDrops(False)
|
||||||
self.current_view = self.device_view
|
self.current_view = self.device_view
|
||||||
else:
|
else:
|
||||||
self.device_view.hide(), self.library_view.show()
|
self.device_view.hide(), self.library_view.show()
|
||||||
|
self.book_cover.setAcceptDrops(True)
|
||||||
self.current_view = self.library_view
|
self.current_view = self.library_view
|
||||||
self.current_view.sortByColumn(3, Qt.DescendingOrder)
|
self.current_view.sortByColumn(3, Qt.DescendingOrder)
|
||||||
|
|
||||||
|
|
||||||
def tree_clicked(self, item, col):
|
def tree_clicked(self, index):
|
||||||
if item:
|
if index.isValid():
|
||||||
text = str(item.text(0))
|
text = (index.data(Qt.DisplayRole).toString())
|
||||||
if text == "Books": text = str(item.parent().text(0))
|
if "Books" in text: text = str(index.parent().data(Qt.DisplayRole).toString())
|
||||||
if "Library" in text:
|
if "Library" in text:
|
||||||
self.show_device(False)
|
self.show_device(False)
|
||||||
elif "SONY Reader" in text:
|
elif "SONY Reader" in text:
|
||||||
@ -466,6 +80,7 @@ class MainWindow(QObject, Ui_MainWindow):
|
|||||||
def show_book(self, current, previous):
|
def show_book(self, current, previous):
|
||||||
title, author, size, mime, thumbnail = current.model().info(current.row())
|
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))
|
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)
|
self.book_cover.setPixmap(thumbnail)
|
||||||
try:
|
try:
|
||||||
name = os.path.abspath(current.model().image_file.name)
|
name = os.path.abspath(current.model().image_file.name)
|
||||||
@ -475,11 +90,6 @@ class MainWindow(QObject, Ui_MainWindow):
|
|||||||
self.book_info.show()
|
self.book_info.show()
|
||||||
|
|
||||||
|
|
||||||
def list_context_event(self, event):
|
|
||||||
print "TODO:"
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def delete(self, action):
|
def delete(self, action):
|
||||||
count = str(len(self.current_view.selectionModel().selectedRows()))
|
count = str(len(self.current_view.selectionModel().selectedRows()))
|
||||||
ret = QMessageBox.question(self.window, self.trUtf8("SONY Reader - confirm"), self.trUtf8("Are you sure you want to <b>permanently delete</b> these ") +count+self.trUtf8(" items?"), QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes)
|
ret = QMessageBox.question(self.window, self.trUtf8("SONY Reader - confirm"), self.trUtf8("Are you sure you want to <b>permanently delete</b> these ") +count+self.trUtf8(" items?"), QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes)
|
||||||
@ -574,7 +184,14 @@ class MainWindow(QObject, Ui_MainWindow):
|
|||||||
|
|
||||||
|
|
||||||
def show_error(self, e, msg):
|
def show_error(self, e, msg):
|
||||||
QErrorMessage(self.window).showMessage(msg+"<br><b>Error: </b>"+str(e)+"<br><br>Traceback:<br>"+traceback.format_exc(e))
|
QErrorMessage(self.window).showMessage(msg+"<br><b>Error: </b>"+str(e)+"<br><br>"+re.sub("\n","<br>", traceback.format_exc(e)))
|
||||||
|
|
||||||
|
def update_cover(self, pix):
|
||||||
|
if not pix.isNull():
|
||||||
|
try:
|
||||||
|
self.library_view.model().update_cover(self.library_view.currentIndex(), pix)
|
||||||
|
self.book_cover.setPixmap(pix)
|
||||||
|
except Exception, e: self.show_error(e, "Unable to change cover")
|
||||||
|
|
||||||
def __init__(self, window, log_packets):
|
def __init__(self, window, log_packets):
|
||||||
QObject.__init__(self)
|
QObject.__init__(self)
|
||||||
@ -593,10 +210,6 @@ class MainWindow(QObject, Ui_MainWindow):
|
|||||||
self.library_model.set_data(LibraryDatabase(str(self.database_path)))
|
self.library_model.set_data(LibraryDatabase(str(self.database_path)))
|
||||||
self.library_view.setModel(self.library_model)
|
self.library_view.setModel(self.library_model)
|
||||||
self.current_view = self.library_view
|
self.current_view = self.library_view
|
||||||
self.library_view.setSelectionBehavior(QAbstractItemView.SelectRows)
|
|
||||||
self.library_view.setSortingEnabled(True)
|
|
||||||
self.library_view.contextMenuEvent = self.list_context_event
|
|
||||||
self.library_view.setItemDelegate(LibraryDelegate(self.library_view))
|
|
||||||
QObject.connect(self.library_model, SIGNAL("layoutChanged()"), self.library_view.resizeRowsToContents)
|
QObject.connect(self.library_model, SIGNAL("layoutChanged()"), self.library_view.resizeRowsToContents)
|
||||||
QObject.connect(self.library_view.selectionModel(), SIGNAL("currentChanged(QModelIndex, QModelIndex)"), self.show_book)
|
QObject.connect(self.library_view.selectionModel(), SIGNAL("currentChanged(QModelIndex, QModelIndex)"), self.show_book)
|
||||||
QObject.connect(self.search, SIGNAL("textChanged(QString)"), self.library_model.search)
|
QObject.connect(self.search, SIGNAL("textChanged(QString)"), self.library_model.search)
|
||||||
@ -607,49 +220,20 @@ class MainWindow(QObject, Ui_MainWindow):
|
|||||||
self.library_view.resizeColumnsToContents()
|
self.library_view.resizeColumnsToContents()
|
||||||
|
|
||||||
# Create Device tree
|
# Create Device tree
|
||||||
QObject.connect(self.device_tree, SIGNAL("itemClicked ( QTreeWidgetItem *, int )"), self.tree_clicked)
|
QObject.connect(self.device_tree, SIGNAL("activated(QModelIndex)"), self.tree_clicked)
|
||||||
QObject.connect(self.device_tree, SIGNAL("itemActivated ( QTreeWidgetItem *, int )"), self.tree_clicked)
|
QObject.connect(self.device_tree, SIGNAL("clicked(QModelIndex)"), self.tree_clicked)
|
||||||
self.device_tree.header().hide()
|
model = DeviceModel(self.device_tree)
|
||||||
library = QTreeWidgetItem(self.device_tree, QTreeWidgetItem.Type)
|
self.device_tree.setModel(model)
|
||||||
library.setData(0, Qt.DisplayRole, QVariant("Library"))
|
self.device_tree.expand(model.indexFromItem(model.library))
|
||||||
library.setData(0, Qt.DecorationRole, QVariant(QIcon(":/library")))
|
self.device_tree.expand(model.indexFromItem(model.reader))
|
||||||
books =QTreeWidgetItem(library, QTreeWidgetItem.Type)
|
self.device_tree.expand(model.indexFromItem(model.card))
|
||||||
books.setData(0, Qt.DisplayRole, QVariant("Books"))
|
self.device_tree.hide_reader(True)
|
||||||
self.device_tree.expandItem(library)
|
self.device_tree.hide_card(True)
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
# Create Device Book list
|
# Create Device Book list
|
||||||
self.reader_model = DeviceBooksModel(window)
|
self.reader_model = DeviceBooksModel(window)
|
||||||
self.card_model = DeviceBooksModel(window)
|
self.card_model = DeviceBooksModel(window)
|
||||||
self.device_view.setModel(self.reader_model)
|
self.device_view.setModel(self.reader_model)
|
||||||
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)
|
QObject.connect(self.device_view.selectionModel(), SIGNAL("currentChanged(QModelIndex, QModelIndex)"), self.show_book)
|
||||||
for model in (self.reader_model, self. card_model):
|
for model in (self.reader_model, self. card_model):
|
||||||
QObject.connect(model, SIGNAL("layoutChanged()"), self.device_view.resizeRowsToContents)
|
QObject.connect(model, SIGNAL("layoutChanged()"), 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_del, SIGNAL("triggered(bool)"), self.delete)
|
||||||
QObject.connect(self.action_edit, SIGNAL("triggered(bool)"), self.edit)
|
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.device_detector = self.startTimer(1000)
|
||||||
self.splitter.setStretchFactor(1,100)
|
self.splitter.setStretchFactor(1,100)
|
||||||
self.search.setFocus(Qt.OtherFocusReason)
|
self.search.setFocus(Qt.OtherFocusReason)
|
||||||
@ -688,8 +275,8 @@ class MainWindow(QObject, Ui_MainWindow):
|
|||||||
""" @todo: only reset stuff if library is not shown """
|
""" @todo: only reset stuff if library is not shown """
|
||||||
self.is_connected = False
|
self.is_connected = False
|
||||||
self.df.setText("SONY Reader: <br><br>Storage card:")
|
self.df.setText("SONY Reader: <br><br>Storage card:")
|
||||||
self.device_tree.hide_reader(self.device_tree, True)
|
self.device_tree.hide_reader(True)
|
||||||
self.device_tree.hide_card(self.device_tree, True)
|
self.device_tree.hide_card(True)
|
||||||
self.book_cover.hide()
|
self.book_cover.hide()
|
||||||
self.book_info.hide()
|
self.book_info.hide()
|
||||||
self.device_view.hide()
|
self.device_view.hide()
|
||||||
@ -718,7 +305,6 @@ class MainWindow(QObject, Ui_MainWindow):
|
|||||||
return
|
return
|
||||||
except ProtocolError, e:
|
except ProtocolError, e:
|
||||||
traceback.print_exc(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")
|
qFatal("Unable to connect to device. Please try unplugging and reconnecting it")
|
||||||
|
|
||||||
sc = space[1][1] if space[1][1] else space[2][1]
|
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:"
|
if space[1][2] > 0: card = "a:"
|
||||||
elif space[2][2] > 0: card = "b:"
|
elif space[2][2] > 0: card = "b:"
|
||||||
else: card = None
|
else: card = None
|
||||||
if card: self.device_tree.hide_card(self.device_tree, False)
|
if card: self.device_tree.hide_card(False)
|
||||||
else: self.device_tree.hide_card(self.device_tree, True)
|
else: self.device_tree.hide_card(True)
|
||||||
self.device_tree.hide_reader(self.device_tree, False)
|
self.device_tree.hide_reader(False)
|
||||||
self.status("Loading media list from SONY Reader")
|
self.status("Loading media list from SONY Reader")
|
||||||
self.reader_model.set_data(self.dev.books())
|
self.reader_model.set_data(self.dev.books())
|
||||||
if card: self.status("Loading media list from Storage Card")
|
if card: self.status("Loading media list from Storage Card")
|
||||||
|
@ -53,7 +53,10 @@
|
|||||||
<number>6</number>
|
<number>6</number>
|
||||||
</property>
|
</property>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QTreeWidget" name="device_tree" >
|
<widget class="DeviceView" name="device_tree" >
|
||||||
|
<property name="acceptDrops" >
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
<property name="dragDropMode" >
|
<property name="dragDropMode" >
|
||||||
<enum>QAbstractItemView::DropOnly</enum>
|
<enum>QAbstractItemView::DropOnly</enum>
|
||||||
</property>
|
</property>
|
||||||
@ -69,14 +72,6 @@
|
|||||||
<property name="allColumnsShowFocus" >
|
<property name="allColumnsShowFocus" >
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
<property name="columnCount" >
|
|
||||||
<number>1</number>
|
|
||||||
</property>
|
|
||||||
<column>
|
|
||||||
<property name="text" >
|
|
||||||
<string>1</string>
|
|
||||||
</property>
|
|
||||||
</column>
|
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
@ -169,36 +164,60 @@
|
|||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QTableView" name="device_view" >
|
<widget class="DeviceBooksView" name="device_view" >
|
||||||
<property name="sizePolicy" >
|
<property name="sizePolicy" >
|
||||||
<sizepolicy>
|
<sizepolicy>
|
||||||
<hsizetype>7</hsizetype>
|
<hsizetype>13</hsizetype>
|
||||||
<vsizetype>7</vsizetype>
|
<vsizetype>13</vsizetype>
|
||||||
<horstretch>0</horstretch>
|
<horstretch>0</horstretch>
|
||||||
<verstretch>0</verstretch>
|
<verstretch>0</verstretch>
|
||||||
</sizepolicy>
|
</sizepolicy>
|
||||||
</property>
|
</property>
|
||||||
|
<property name="dragEnabled" >
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<property name="dragDropOverwriteMode" >
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
<property name="alternatingRowColors" >
|
<property name="alternatingRowColors" >
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
|
<property name="selectionBehavior" >
|
||||||
|
<enum>QAbstractItemView::SelectRows</enum>
|
||||||
|
</property>
|
||||||
<property name="showGrid" >
|
<property name="showGrid" >
|
||||||
<bool>false</bool>
|
<bool>false</bool>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QTableView" name="library_view" >
|
<widget class="LibraryBooksView" name="library_view" >
|
||||||
<property name="sizePolicy" >
|
<property name="sizePolicy" >
|
||||||
<sizepolicy>
|
<sizepolicy>
|
||||||
<hsizetype>7</hsizetype>
|
<hsizetype>13</hsizetype>
|
||||||
<vsizetype>7</vsizetype>
|
<vsizetype>13</vsizetype>
|
||||||
<horstretch>0</horstretch>
|
<horstretch>0</horstretch>
|
||||||
<verstretch>0</verstretch>
|
<verstretch>0</verstretch>
|
||||||
</sizepolicy>
|
</sizepolicy>
|
||||||
</property>
|
</property>
|
||||||
|
<property name="acceptDrops" >
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<property name="dragEnabled" >
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<property name="dragDropOverwriteMode" >
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
<property name="dragDropMode" >
|
||||||
|
<enum>QAbstractItemView::DragDrop</enum>
|
||||||
|
</property>
|
||||||
<property name="alternatingRowColors" >
|
<property name="alternatingRowColors" >
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
|
<property name="selectionBehavior" >
|
||||||
|
<enum>QAbstractItemView::SelectRows</enum>
|
||||||
|
</property>
|
||||||
<property name="showGrid" >
|
<property name="showGrid" >
|
||||||
<bool>false</bool>
|
<bool>false</bool>
|
||||||
</property>
|
</property>
|
||||||
@ -215,13 +234,16 @@
|
|||||||
<number>6</number>
|
<number>6</number>
|
||||||
</property>
|
</property>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QLabel" name="book_cover" >
|
<widget class="CoverDisplay" name="book_cover" >
|
||||||
<property name="maximumSize" >
|
<property name="maximumSize" >
|
||||||
<size>
|
<size>
|
||||||
<width>60</width>
|
<width>60</width>
|
||||||
<height>80</height>
|
<height>80</height>
|
||||||
</size>
|
</size>
|
||||||
</property>
|
</property>
|
||||||
|
<property name="acceptDrops" >
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
<property name="text" >
|
<property name="text" >
|
||||||
<string/>
|
<string/>
|
||||||
</property>
|
</property>
|
||||||
@ -325,6 +347,28 @@
|
|||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
</widget>
|
</widget>
|
||||||
|
<customwidgets>
|
||||||
|
<customwidget>
|
||||||
|
<class>CoverDisplay</class>
|
||||||
|
<extends>QLabel</extends>
|
||||||
|
<header>widgets.h</header>
|
||||||
|
</customwidget>
|
||||||
|
<customwidget>
|
||||||
|
<class>LibraryBooksView</class>
|
||||||
|
<extends>QTableView</extends>
|
||||||
|
<header>widgets.h</header>
|
||||||
|
</customwidget>
|
||||||
|
<customwidget>
|
||||||
|
<class>DeviceView</class>
|
||||||
|
<extends>QTreeView</extends>
|
||||||
|
<header>widgets.h</header>
|
||||||
|
</customwidget>
|
||||||
|
<customwidget>
|
||||||
|
<class>DeviceBooksView</class>
|
||||||
|
<extends>QTableView</extends>
|
||||||
|
<header>widgets.h</header>
|
||||||
|
</customwidget>
|
||||||
|
</customwidgets>
|
||||||
<resources>
|
<resources>
|
||||||
<include location="images.qrc" />
|
<include location="images.qrc" />
|
||||||
</resources>
|
</resources>
|
||||||
|
642
libprs500/gui/widgets.py
Normal file
642
libprs500/gui/widgets.py
Normal file
@ -0,0 +1,642 @@
|
|||||||
|
## Copyright (C) 2006 Kovid Goyal kovid@kovidgoyal.net
|
||||||
|
## This program is free software; you can redistribute it and/or modify
|
||||||
|
## it under the terms of the GNU General Public License as published by
|
||||||
|
## the Free Software Foundation; either version 2 of the License, or
|
||||||
|
## (at your option) any later version.
|
||||||
|
##
|
||||||
|
## This program is distributed in the hope that it will be useful,
|
||||||
|
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
## GNU General Public License for more details.
|
||||||
|
##
|
||||||
|
## You should have received a copy of the GNU General Public License along
|
||||||
|
## with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
|
|
||||||
|
from PyQt4 import QtGui, QtCore
|
||||||
|
from PyQt4.QtCore import Qt, SIGNAL
|
||||||
|
from PyQt4.Qt import QApplication, QString, QFont, QStandardItemModel, QStandardItem, QVariant, QAbstractTableModel, QTableView, QTreeView, QLabel,\
|
||||||
|
QAbstractItemView, QPixmap, QIcon, QSize, QMessageBox, QSettings, QFileDialog, QErrorMessage, QDialog, QSpinBox, QPoint, QTemporaryFile, QDir,\
|
||||||
|
QPainterPath, QItemDelegate, QPainter, QPen, QColor, QLinearGradient, QBrush, QStyle,\
|
||||||
|
QStringList, QByteArray, QBuffer, QMimeData, QTextStream, QIODevice, QDrag,\
|
||||||
|
qDebug, qFatal, qWarning, qCritical
|
||||||
|
import re, os, string, textwrap, time, traceback
|
||||||
|
|
||||||
|
from operator import itemgetter, attrgetter
|
||||||
|
from socket import gethostname
|
||||||
|
from urlparse import urlparse, urlunparse
|
||||||
|
from urllib import quote, unquote
|
||||||
|
from math import sin, cos, pi
|
||||||
|
from libprs500 import TEMPORARY_FILENAME_TEMPLATE as TFT
|
||||||
|
from libprs500.lrf.meta import LRFMetaFile
|
||||||
|
|
||||||
|
NONE = QVariant()
|
||||||
|
TIME_WRITE_FMT = "%d %b %Y"
|
||||||
|
COVER_HEIGHT = 80
|
||||||
|
|
||||||
|
def human_readable(size):
|
||||||
|
""" Convert a size in bytes into a human readable form """
|
||||||
|
if size < 1024: divisor, suffix = 1, "B"
|
||||||
|
elif size < 1024*1024: divisor, suffix = 1024., "KB"
|
||||||
|
elif size < 1024*1024*1024: divisor, suffix = 1024*1024, "MB"
|
||||||
|
elif size < 1024*1024*1024*1024: divisor, suffix = 1024*1024, "GB"
|
||||||
|
size = str(size/divisor)
|
||||||
|
if size.find(".") > -1: size = size[:size.find(".")+2]
|
||||||
|
return size + " " + suffix
|
||||||
|
|
||||||
|
|
||||||
|
def wrap(s, width=20):
|
||||||
|
return textwrap.fill(str(s), width)
|
||||||
|
|
||||||
|
def get_r_ok_files(event):
|
||||||
|
""" @type event: QDropEvent """
|
||||||
|
files = []
|
||||||
|
md = event.mimeData()
|
||||||
|
if md.hasFormat("text/uri-list"):
|
||||||
|
candidates = bytes_to_string(md.data("text/uri-list")).split("\n")
|
||||||
|
print candidates
|
||||||
|
for path in candidates:
|
||||||
|
path = os.path.abspath(re.sub(r"^file://", "", path))
|
||||||
|
if os.path.isfile(path) and os.access(path, os.R_OK): files.append(path)
|
||||||
|
return files
|
||||||
|
|
||||||
|
|
||||||
|
def bytes_to_string(qba):
|
||||||
|
""" @type qba: QByteArray """
|
||||||
|
return unicode(QString.fromUtf8(qba.data())).strip()
|
||||||
|
|
||||||
|
class FileDragAndDrop(object):
|
||||||
|
_drag_start_position = QPoint()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _bytes_to_string(cls, qba):
|
||||||
|
""" @type qba: QByteArray """
|
||||||
|
return unicode(QString.fromUtf8(qba.data())).strip()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _get_r_ok_files(cls, event):
|
||||||
|
files = []
|
||||||
|
md = event.mimeData()
|
||||||
|
if md.hasFormat("text/uri-list"):
|
||||||
|
candidates = cls._bytes_to_string(md.data("text/uri-list")).split()
|
||||||
|
for url in candidates:
|
||||||
|
o = urlparse(url)
|
||||||
|
if o.scheme != 'file':
|
||||||
|
qWarning(o.scheme + " not supported in drop events")
|
||||||
|
continue
|
||||||
|
path = unquote(o.path)
|
||||||
|
if not os.access(path, os.R_OK):
|
||||||
|
qWarning("You do not have read permission for: " + path)
|
||||||
|
continue
|
||||||
|
if os.path.isdir(path):
|
||||||
|
root, dirs, files2 = os.walk(path)
|
||||||
|
for file in files2:
|
||||||
|
path = root + file
|
||||||
|
if os.access(path, os.R_OK): files.append(path)
|
||||||
|
else: files.append(path)
|
||||||
|
return files
|
||||||
|
|
||||||
|
def mousePressEvent(self, event):
|
||||||
|
if event.button == Qt.LeftButton:
|
||||||
|
self._drag_start_position = event.pos()
|
||||||
|
|
||||||
|
def mouseMoveEvent(self, event):
|
||||||
|
if event.buttons() & Qt.LeftButton != Qt.LeftButton: return
|
||||||
|
if (event.pos() - self._drag_start_position).manhattanLength() < QApplication.startDragDistance(): return
|
||||||
|
self.start_drag(self._drag_start_position)
|
||||||
|
|
||||||
|
def start_drag(self, pos): pass
|
||||||
|
|
||||||
|
def dragEnterEvent(self, event):
|
||||||
|
if event.mimeData().hasFormat("text/uri-list"): event.acceptProposedAction()
|
||||||
|
|
||||||
|
def dragMoveEvent(self, event):
|
||||||
|
event.acceptProposedAction()
|
||||||
|
|
||||||
|
def dropEvent(self, event):
|
||||||
|
files = self._get_r_ok_files(event)
|
||||||
|
if files:
|
||||||
|
if self.files_dropped(files): event.acceptProposedAction()
|
||||||
|
|
||||||
|
def files_dropped(self, files): return False
|
||||||
|
|
||||||
|
def drag_object(self, extensions):
|
||||||
|
if extensions:
|
||||||
|
drag = QDrag(self)
|
||||||
|
mime_data = QMimeData()
|
||||||
|
self._dragged_files, urls = [], []
|
||||||
|
for ext in extensions:
|
||||||
|
f = TemporaryFile(ext=ext)
|
||||||
|
f.open()
|
||||||
|
urls.append(urlunparse(('file', quote(gethostname()), quote(str(f.fileName())), '','','')))
|
||||||
|
self._dragged_files.append(f)
|
||||||
|
mime_data.setData("text/uri-list", QByteArray("\n".join(urls)))
|
||||||
|
user = None
|
||||||
|
try: user = os.environ['USER']
|
||||||
|
except: pass
|
||||||
|
if user: mime_data.setData("text/x-xdnd-username", QByteArray(user))
|
||||||
|
drag.setMimeData(mime_data)
|
||||||
|
return drag, self._dragged_files
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class TemporaryFile(QTemporaryFile):
|
||||||
|
def __init__(self, ext=""):
|
||||||
|
if ext: ext = "." + ext
|
||||||
|
path = QDir.tempPath() + "/" + TFT + "_XXXXXX"+ext
|
||||||
|
QTemporaryFile.__init__(self, path)
|
||||||
|
|
||||||
|
class CoverDisplay(FileDragAndDrop, QLabel):
|
||||||
|
def files_dropped(self, files):
|
||||||
|
pix = QPixmap()
|
||||||
|
for file in files:
|
||||||
|
pix = QPixmap(file)
|
||||||
|
if not pix.isNull(): break
|
||||||
|
if not pix.isNull():
|
||||||
|
self.emit(SIGNAL("cover_received(QPixmap)"), pix)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def start_drag(self, event):
|
||||||
|
drag, files = self.drag_object(["jpeg"])
|
||||||
|
if drag and files:
|
||||||
|
file = files[0]
|
||||||
|
drag.setPixmap(self.pixmap())
|
||||||
|
self.pixmap().save(file)
|
||||||
|
file.close()
|
||||||
|
drag.start(Qt.MoveAction)
|
||||||
|
|
||||||
|
class DeviceView(QTreeView):
|
||||||
|
def __init__(self, parent):
|
||||||
|
QTreeView.__init__(self, parent)
|
||||||
|
self.header().hide()
|
||||||
|
self.setIconSize(QSize(32,32))
|
||||||
|
|
||||||
|
def hide_reader(self, x):
|
||||||
|
self.setRowHidden(2, self.model().indexFromItem(self.model().invisibleRootItem()), x)
|
||||||
|
|
||||||
|
def hide_card(self, x):
|
||||||
|
self.setRowHidden(4, self.model().indexFromItem(self.model().invisibleRootItem()), x)
|
||||||
|
|
||||||
|
class DeviceBooksView(QTableView):
|
||||||
|
def __init__(self, parent):
|
||||||
|
QTableView.__init__(self, parent)
|
||||||
|
self.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||||
|
self.setSortingEnabled(True)
|
||||||
|
|
||||||
|
class LibraryBooksView(QTableView):
|
||||||
|
def __init__(self, parent):
|
||||||
|
QTableView.__init__(self, parent)
|
||||||
|
self.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||||
|
self.setSortingEnabled(True)
|
||||||
|
self.setItemDelegate(LibraryDelegate(self))
|
||||||
|
|
||||||
|
def get_verified_path(self, mime_data):
|
||||||
|
if mime_data.hasFormat("text/plain"):
|
||||||
|
text = unicode(mime_data.text())
|
||||||
|
text = re.sub(r"^file://", "", text)
|
||||||
|
if os.access(os.path.abspath(text), os.R_OK): return text
|
||||||
|
return None
|
||||||
|
def dragEnterEvent(self, event):
|
||||||
|
if self.get_verified_path(event.mimeData()): event.acceptProposedAction()
|
||||||
|
|
||||||
|
def dragMoveEvent(self, event): event.acceptProposedAction()
|
||||||
|
|
||||||
|
def dropEvent(self, event):
|
||||||
|
path = self.get_verified_path(event.mimeData())
|
||||||
|
if path:
|
||||||
|
if self.model().handle_drop(path, self.indexAt(event.pos())): event.acceptProposedAction()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class LibraryDelegate(QItemDelegate):
|
||||||
|
COLOR = QColor("blue")
|
||||||
|
SIZE = 16
|
||||||
|
PEN = QPen(COLOR, 1, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)
|
||||||
|
|
||||||
|
def __init__(self, parent):
|
||||||
|
QItemDelegate.__init__(self, parent)
|
||||||
|
self.star_path = QPainterPath()
|
||||||
|
self.star_path.moveTo(90, 50)
|
||||||
|
for i in range(1, 5):
|
||||||
|
self.star_path.lineTo(50 + 40 * cos(0.8 * i * pi), 50 + 40 * sin(0.8 * i * pi))
|
||||||
|
self.star_path.closeSubpath()
|
||||||
|
self.star_path.setFillRule(Qt.WindingFill)
|
||||||
|
gradient = QLinearGradient(0, 0, 0, 100)
|
||||||
|
gradient.setColorAt(0.0, self.COLOR)
|
||||||
|
gradient.setColorAt(1.0, self.COLOR)
|
||||||
|
self. brush = QBrush(gradient)
|
||||||
|
self.factor = self.SIZE/100.
|
||||||
|
|
||||||
|
|
||||||
|
def sizeHint(self, option, index):
|
||||||
|
if index.column() != 4:
|
||||||
|
return QItemDelegate.sizeHint(self, option, index)
|
||||||
|
num = index.model().data(index, Qt.DisplayRole).toInt()[0]
|
||||||
|
return QSize(num*(self.SIZE), self.SIZE+4)
|
||||||
|
|
||||||
|
def paint(self, painter, option, index):
|
||||||
|
if index.column() != 4:
|
||||||
|
return QItemDelegate.paint(self, painter, option, index)
|
||||||
|
num = index.model().data(index, Qt.DisplayRole).toInt()[0]
|
||||||
|
def draw_star():
|
||||||
|
painter.save()
|
||||||
|
painter.scale(self.factor, self.factor)
|
||||||
|
painter.translate(50.0, 50.0)
|
||||||
|
painter.rotate(-20)
|
||||||
|
painter.translate(-50.0, -50.0)
|
||||||
|
painter.drawPath(self.star_path)
|
||||||
|
painter.restore()
|
||||||
|
|
||||||
|
painter.save()
|
||||||
|
try:
|
||||||
|
if option.state & QStyle.State_Selected:
|
||||||
|
painter.fillRect(option.rect, option.palette.highlight())
|
||||||
|
painter.setRenderHint(QPainter.Antialiasing)
|
||||||
|
y = option.rect.center().y()-self.SIZE/2.
|
||||||
|
x = option.rect.right() - self.SIZE
|
||||||
|
painter.setPen(self.PEN)
|
||||||
|
painter.setBrush(self.brush)
|
||||||
|
painter.translate(x, y)
|
||||||
|
for i in range(num):
|
||||||
|
draw_star()
|
||||||
|
painter.translate(-self.SIZE, 0)
|
||||||
|
except Exception, e:
|
||||||
|
traceback.print_exc(e)
|
||||||
|
painter.restore()
|
||||||
|
|
||||||
|
def createEditor(self, parent, option, index):
|
||||||
|
if index.column() != 4:
|
||||||
|
return QItemDelegate.createEditor(self, parent, option, index)
|
||||||
|
editor = QSpinBox(parent)
|
||||||
|
editor.setSuffix(" stars")
|
||||||
|
editor.setMinimum(0)
|
||||||
|
editor.setMaximum(5)
|
||||||
|
editor.installEventFilter(self)
|
||||||
|
return editor
|
||||||
|
|
||||||
|
def setEditorData(self, editor, index):
|
||||||
|
if index.column() != 4:
|
||||||
|
return QItemDelegate.setEditorData(self, editor, index)
|
||||||
|
val = index.model()._data[index.row()]["rating"]
|
||||||
|
if not val: val = 0
|
||||||
|
editor.setValue(val)
|
||||||
|
|
||||||
|
def setModelData(self, editor, model, index):
|
||||||
|
if index.column() != 4:
|
||||||
|
return QItemDelegate.setModelData(self, editor, model, index)
|
||||||
|
editor.interpretText()
|
||||||
|
index.model().setData(index, QVariant(editor.value()), Qt.EditRole)
|
||||||
|
|
||||||
|
def updateEditorGeometry(self, editor, option, index):
|
||||||
|
if index.column() != 4:
|
||||||
|
return QItemDelegate.updateEditorGeometry(self, editor, option, index)
|
||||||
|
editor.setGeometry(option.rect)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class LibraryBooksModel(QAbstractTableModel):
|
||||||
|
FIELDS = ["id", "title", "authors", "size", "date", "rating", "publisher", "tags"]
|
||||||
|
TIME_READ_FMT = "%Y-%m-%d %H:%M:%S"
|
||||||
|
def __init__(self, parent):
|
||||||
|
QAbstractTableModel.__init__(self, parent)
|
||||||
|
self.db = None
|
||||||
|
self._data = None
|
||||||
|
self._orig_data = None
|
||||||
|
self.image_file = None
|
||||||
|
|
||||||
|
def update_cover(self, index, pix):
|
||||||
|
id = self.id_from_index(index)
|
||||||
|
qb = QBuffer()
|
||||||
|
qb.open(QBuffer.ReadWrite);
|
||||||
|
pix.save(qb, "JPG")
|
||||||
|
data = str(qb.data())
|
||||||
|
qb.close()
|
||||||
|
self.db.update_cover(id, data)
|
||||||
|
|
||||||
|
def handle_drop(self, path, index):
|
||||||
|
print "249", path, index.row()
|
||||||
|
if index.isValid():
|
||||||
|
f = open(path, "rb")
|
||||||
|
title = os.path.basename(path)
|
||||||
|
ext = title[title.rfind(".")+1:].lower() if "." in title > -1 else None
|
||||||
|
self.db.add_format(self.id_from_index(index), ext, f)
|
||||||
|
f.close()
|
||||||
|
else:
|
||||||
|
pass # TODO: emit book add signal
|
||||||
|
return True
|
||||||
|
|
||||||
|
def rowCount(self, parent): return len(self._data)
|
||||||
|
def columnCount(self, parent): return len(self.FIELDS)-2
|
||||||
|
|
||||||
|
def setData(self, index, value, role):
|
||||||
|
done = False
|
||||||
|
if role == Qt.EditRole:
|
||||||
|
row = index.row()
|
||||||
|
id = self._data[row]["id"]
|
||||||
|
col = index.column()
|
||||||
|
val = str(value.toString())
|
||||||
|
if col == 0: col = "title"
|
||||||
|
elif col == 1: col = "authors"
|
||||||
|
elif col == 2: return False
|
||||||
|
elif col == 3: return False
|
||||||
|
elif col == 4:
|
||||||
|
col, val = "rating", int(value.toInt()[0])
|
||||||
|
if val < 0: val =0
|
||||||
|
if val > 5: val = 5
|
||||||
|
elif col == 5: col = "publisher"
|
||||||
|
else: return False
|
||||||
|
self.db.set_metadata_item(id, col, val)
|
||||||
|
self._data[row][col] = val
|
||||||
|
self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), index, index)
|
||||||
|
for i in range(len(self._orig_data)):
|
||||||
|
if self._orig_data[i]["id"] == self._data[row]["id"]:
|
||||||
|
self._orig_data[i][col] = self._data[row][col]
|
||||||
|
break
|
||||||
|
done = True
|
||||||
|
return done
|
||||||
|
|
||||||
|
def flags(self, index):
|
||||||
|
flags = QAbstractTableModel.flags(self, index)
|
||||||
|
if index.isValid():
|
||||||
|
flags |= Qt.ItemIsDragEnabled | Qt.ItemIsDropEnabled
|
||||||
|
if index.column() not in [2,3]: flags |= Qt.ItemIsEditable
|
||||||
|
else: flags |= Qt.ItemIsDropEnabled
|
||||||
|
return flags
|
||||||
|
|
||||||
|
def set_data(self, db):
|
||||||
|
self.db = db
|
||||||
|
self._data = self.db.get_table(self.FIELDS)
|
||||||
|
self._orig_data = self._data
|
||||||
|
self.sort(0, Qt.DescendingOrder)
|
||||||
|
self.reset()
|
||||||
|
|
||||||
|
def headerData(self, section, orientation, role):
|
||||||
|
if role != Qt.DisplayRole:
|
||||||
|
return NONE
|
||||||
|
text = ""
|
||||||
|
if orientation == Qt.Horizontal:
|
||||||
|
if section == 0: text = "Title"
|
||||||
|
elif section == 1: text = "Author(s)"
|
||||||
|
elif section == 2: text = "Size"
|
||||||
|
elif section == 3: text = "Date"
|
||||||
|
elif section == 4: text = "Rating"
|
||||||
|
elif section == 5: text = "Publisher"
|
||||||
|
return QVariant(self.trUtf8(text))
|
||||||
|
else: return QVariant(str(1+section))
|
||||||
|
|
||||||
|
def info(self, row):
|
||||||
|
row = self._data[row]
|
||||||
|
cover = self.db.get_cover(row["id"])
|
||||||
|
exts = ",".join(self.db.get_extensions(row["id"]))
|
||||||
|
if cover:
|
||||||
|
pix = QPixmap()
|
||||||
|
pix.loadFromData(cover, "", Qt.AutoColor)
|
||||||
|
cover = None if pix.isNull() else pix
|
||||||
|
au = row["authors"]
|
||||||
|
if not au: au = "Unknown"
|
||||||
|
return row["title"], au, human_readable(int(row["size"])), exts, cover
|
||||||
|
|
||||||
|
def id_from_index(self, index): return self._data[index.row()]["id"]
|
||||||
|
|
||||||
|
def refresh_row(self, row):
|
||||||
|
self._data[row] = self.db.get_row_by_id(self._data[row]["id"], self.FIELDS)
|
||||||
|
for i in range(len(self._orig_data)):
|
||||||
|
if self._orig_data[i]["id"] == self._data[row]["id"]:
|
||||||
|
self._orig_data[i:i+1] = self._data[row]
|
||||||
|
break
|
||||||
|
self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), self.index(row, 0), self.index(row, self.columnCount(0)-1))
|
||||||
|
|
||||||
|
def data(self, index, role):
|
||||||
|
if role == Qt.DisplayRole or role == Qt.EditRole:
|
||||||
|
row, col = index.row(), index.column()
|
||||||
|
text = None
|
||||||
|
row = self._data[row]
|
||||||
|
if col == 4:
|
||||||
|
r = row["rating"] if row["rating"] else 0
|
||||||
|
if r < 0: r= 0
|
||||||
|
if r > 5: r=5
|
||||||
|
return QVariant(r)
|
||||||
|
if col == 0: text = wrap(row["title"], width=25)
|
||||||
|
elif col == 1:
|
||||||
|
au = row["authors"]
|
||||||
|
if au : text = wrap(re.sub("&", "\n", au), width=25)
|
||||||
|
elif col == 2: text = human_readable(row["size"])
|
||||||
|
elif col == 3: text = time.strftime(TIME_WRITE_FMT, time.strptime(row["date"], self.TIME_READ_FMT))
|
||||||
|
elif col == 5:
|
||||||
|
pub = row["publisher"]
|
||||||
|
if pub: text = wrap(pub, 20)
|
||||||
|
if text == None: text = "Unknown"
|
||||||
|
return QVariant(text)
|
||||||
|
elif role == Qt.TextAlignmentRole and index.column() in [2,3,4]:
|
||||||
|
return QVariant(Qt.AlignRight)
|
||||||
|
return NONE
|
||||||
|
|
||||||
|
def sort(self, col, order):
|
||||||
|
descending = order != Qt.AscendingOrder
|
||||||
|
def getter(key, func): return lambda x : func(itemgetter(key)(x))
|
||||||
|
if col == 0: key, func = "title", string.lower
|
||||||
|
if col == 1: key, func = "authors", lambda x : x.split()[-1:][0].lower() if x else ""
|
||||||
|
if col == 2: key, func = "size", int
|
||||||
|
if col == 3: key, func = "date", lambda x: time.mktime(time.strptime(x, self.TIME_READ_FMT))
|
||||||
|
if col == 4: key, func = "rating", lambda x: x if x else 0
|
||||||
|
if col == 5: key, func = "publisher", lambda x : x.lower() if x else ""
|
||||||
|
self.emit(SIGNAL("layoutAboutToBeChanged()"))
|
||||||
|
self._data.sort(key=getter(key, func))
|
||||||
|
if descending: self._data.reverse()
|
||||||
|
self.emit(SIGNAL("layoutChanged()"))
|
||||||
|
self.emit(SIGNAL("sorted()"))
|
||||||
|
|
||||||
|
def search(self, query):
|
||||||
|
def query_in(book, q):
|
||||||
|
au = book["authors"]
|
||||||
|
if not au : au = "unknown"
|
||||||
|
pub = book["publisher"]
|
||||||
|
if not pub : pub = "unknown"
|
||||||
|
return q in book["title"].lower() or q in au.lower() or q in pub.lower()
|
||||||
|
queries = unicode(query, 'utf-8').lower().split()
|
||||||
|
self.emit(SIGNAL("layoutAboutToBeChanged()"))
|
||||||
|
self._data = []
|
||||||
|
for book in self._orig_data:
|
||||||
|
match = True
|
||||||
|
for q in queries:
|
||||||
|
if query_in(book, q) : continue
|
||||||
|
else:
|
||||||
|
match = False
|
||||||
|
break
|
||||||
|
if match: self._data.append(book)
|
||||||
|
self.emit(SIGNAL("layoutChanged()"))
|
||||||
|
self.emit(SIGNAL("searched()"))
|
||||||
|
|
||||||
|
def delete(self, indices):
|
||||||
|
if len(indices): self.emit(SIGNAL("layoutAboutToBeChanged()"))
|
||||||
|
items = [ self._data[index.row()] for index in indices ]
|
||||||
|
for item in items:
|
||||||
|
id = item["id"]
|
||||||
|
try:
|
||||||
|
self._data.remove(item)
|
||||||
|
except ValueError: continue
|
||||||
|
self.db.delete_by_id(id)
|
||||||
|
for x in self._orig_data:
|
||||||
|
if x["id"] == id: self._orig_data.remove(x)
|
||||||
|
self.emit(SIGNAL("layoutChanged()"))
|
||||||
|
self.emit(SIGNAL("deleted()"))
|
||||||
|
self.db.commit()
|
||||||
|
|
||||||
|
def add_book(self, path):
|
||||||
|
""" Must call search and sort after this """
|
||||||
|
id = self.db.add_book(path)
|
||||||
|
self._orig_data.append(self.db.get_row_by_id(id, self.FIELDS))
|
||||||
|
|
||||||
|
def mimeTypes(self):
|
||||||
|
s = QStringList()
|
||||||
|
s << "application/vnd.text.list" # Title, authors
|
||||||
|
s << "image/jpeg" # 60x80 thumbnail
|
||||||
|
s << "application/x-sony-bbeb"
|
||||||
|
s << "application/pdf"
|
||||||
|
s << "text/rtf"
|
||||||
|
s << "text/plain"
|
||||||
|
return s
|
||||||
|
|
||||||
|
def mimeData(self, indices):
|
||||||
|
mime_data = QMimeData()
|
||||||
|
encoded_data = QByteArray()
|
||||||
|
rows = []
|
||||||
|
for index in indices:
|
||||||
|
if index.isValid():
|
||||||
|
row = index.row()
|
||||||
|
if row in rows: continue
|
||||||
|
title, authors, size, exts, cover = self.info(row)
|
||||||
|
encoded_data.append(title)
|
||||||
|
encoded_data.append(authors)
|
||||||
|
rows.append(row)
|
||||||
|
mime_data.setData("application/vnd.text.list", encoded_data)
|
||||||
|
return mime_data
|
||||||
|
|
||||||
|
class DeviceBooksModel(QAbstractTableModel):
|
||||||
|
def __init__(self, parent):
|
||||||
|
QAbstractTableModel.__init__(self, parent)
|
||||||
|
self._data = []
|
||||||
|
self._orig_data = []
|
||||||
|
|
||||||
|
def set_data(self, book_list):
|
||||||
|
self._data = book_list
|
||||||
|
self._orig_data = book_list
|
||||||
|
self.reset()
|
||||||
|
|
||||||
|
def rowCount(self, parent): return len(self._data)
|
||||||
|
def columnCount(self, parent): return 4
|
||||||
|
|
||||||
|
def headerData(self, section, orientation, role):
|
||||||
|
if role != Qt.DisplayRole:
|
||||||
|
return NONE
|
||||||
|
text = ""
|
||||||
|
if orientation == Qt.Horizontal:
|
||||||
|
if section == 0: text = "Title"
|
||||||
|
elif section == 1: text = "Author(s)"
|
||||||
|
elif section == 2: text = "Size"
|
||||||
|
elif section == 3: text = "Date"
|
||||||
|
return QVariant(self.trUtf8(text))
|
||||||
|
else: return QVariant(str(1+section))
|
||||||
|
|
||||||
|
def data(self, index, role):
|
||||||
|
if role == Qt.DisplayRole:
|
||||||
|
row, col = index.row(), index.column()
|
||||||
|
book = self._data[row]
|
||||||
|
if col == 0: text = wrap(book.title, width=40)
|
||||||
|
elif col == 1: text = re.sub("&\s*","\n", book.author)
|
||||||
|
elif col == 2: text = human_readable(book.size)
|
||||||
|
elif col == 3: text = time.strftime(TIME_WRITE_FMT, book.datetime)
|
||||||
|
return QVariant(text)
|
||||||
|
elif role == Qt.TextAlignmentRole and index.column() in [2,3]:
|
||||||
|
return QVariant(Qt.AlignRight)
|
||||||
|
return NONE
|
||||||
|
|
||||||
|
def info(self, row):
|
||||||
|
row = self._data[row]
|
||||||
|
cover = None
|
||||||
|
try:
|
||||||
|
cover = row.thumbnail
|
||||||
|
pix = QPixmap()
|
||||||
|
pix.loadFromData(cover, "", Qt.AutoColor)
|
||||||
|
cover = None if pix.isNull() else pix
|
||||||
|
except:
|
||||||
|
traceback.print_exc()
|
||||||
|
au = row.author if row.author else "Unknown"
|
||||||
|
return row.title, au, human_readable(row.size), row.mime, cover
|
||||||
|
|
||||||
|
def sort(self, col, order):
|
||||||
|
def getter(key, func): return lambda x : func(attrgetter(key)(x))
|
||||||
|
if col == 0: key, func = "title", string.lower
|
||||||
|
if col == 1: key, func = "author", lambda x : x.split()[-1:][0].lower()
|
||||||
|
if col == 2: key, func = "size", int
|
||||||
|
if col == 3: key, func = "datetime", lambda x: x
|
||||||
|
descending = order != Qt.AscendingOrder
|
||||||
|
self.emit(SIGNAL("layoutAboutToBeChanged()"))
|
||||||
|
self._data.sort(key=getter(key, func))
|
||||||
|
if descending: self._data.reverse()
|
||||||
|
self.emit(SIGNAL("layoutChanged()"))
|
||||||
|
self.emit(SIGNAL("sorted()"))
|
||||||
|
|
||||||
|
def search(self, query):
|
||||||
|
queries = unicode(query, 'utf-8').lower().split()
|
||||||
|
self.emit(SIGNAL("layoutAboutToBeChanged()"))
|
||||||
|
self._data = []
|
||||||
|
for book in self._orig_data:
|
||||||
|
match = True
|
||||||
|
for q in queries:
|
||||||
|
if q in book.title.lower() or q in book.author.lower(): continue
|
||||||
|
else:
|
||||||
|
match = False
|
||||||
|
break
|
||||||
|
if match: self._data.append(book)
|
||||||
|
self.emit(SIGNAL("layoutChanged()"))
|
||||||
|
self.emit(SIGNAL("searched()"))
|
||||||
|
|
||||||
|
def delete_by_path(self, path):
|
||||||
|
self.emit(SIGNAL("layoutAboutToBeChanged()"))
|
||||||
|
index = -1
|
||||||
|
for book in self._data:
|
||||||
|
if path in book["path"]:
|
||||||
|
self._data.remove(book)
|
||||||
|
break
|
||||||
|
for book in self._orig_data:
|
||||||
|
if path in book["path"]:
|
||||||
|
self._orig_data.remove(book)
|
||||||
|
break
|
||||||
|
self.emit(SIGNAL("layoutChanged()"))
|
||||||
|
self.emit(SIGNAL("deleted()"))
|
||||||
|
|
||||||
|
def path(self, index): return self._data[index.row()].path
|
||||||
|
def title(self, index): return self._data[index.row()].title
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceModel(QStandardItemModel):
|
||||||
|
def __init__(self, parent):
|
||||||
|
QStandardItemModel.__init__(self, parent)
|
||||||
|
root = self.invisibleRootItem()
|
||||||
|
font = QFont()
|
||||||
|
font.setBold(True)
|
||||||
|
self.library = QStandardItem(QIcon(":/library"), QString("Library"))
|
||||||
|
self.reader = QStandardItem(QIcon(":/reader"), "SONY Reader")
|
||||||
|
self.card = QStandardItem(QIcon(":/card"), "Storage Card")
|
||||||
|
self.library.setFont(font)
|
||||||
|
self.reader.setFont(font)
|
||||||
|
self.card.setFont(font)
|
||||||
|
self.blank = QStandardItem("")
|
||||||
|
self.blank.setFlags(Qt.ItemFlags())
|
||||||
|
root.appendRow(self.library)
|
||||||
|
root.appendRow(self.blank)
|
||||||
|
root.appendRow(self.reader)
|
||||||
|
root.appendRow(self.blank.clone())
|
||||||
|
root.appendRow(self.card)
|
||||||
|
self.library.appendRow(QStandardItem("Books"))
|
||||||
|
self.reader.appendRow(QStandardItem("Books"))
|
||||||
|
self.card.appendRow(QStandardItem("Books"))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
x
Reference in New Issue
Block a user