mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-06-23 15:30:45 -04:00
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
643 lines
22 KiB
Python
643 lines
22 KiB
Python
## 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"))
|
|
|
|
|
|
|
|
|
|
|